mirror of
https://github.com/element-hq/element-web
synced 2024-11-22 01:05:42 +03:00
Prepare for Element Call integration (#9224)
* Improve accessibility and testability of Tooltip Adding a role to Tooltip was motivated by React Testing Library's reliance on accessibility-related attributes to locate elements. * Make the ReadyWatchingStore constructor safer The ReadyWatchingStore constructor previously had a chance to immediately call onReady, which was dangerous because it was potentially calling the derived class's onReady at a point when the derived class hadn't even finished construction yet. In normal usage, I guess this never was a problem, but it was causing some of the tests I was writing to crash. This is solved by separating out the onReady call into a start method. * Rename 1:1 call components to 'LegacyCall' to reflect the fact that they're slated for removal, and to not clash with the new Call code. * Refactor VideoChannelStore into Call and CallStore Call is an abstract class that currently only has a Jitsi implementation, but this will make it easy to later add an Element Call implementation. * Remove WidgetReady, ClientReady, and ForceHangupCall hacks These are no longer used by the new Jitsi call implementation, and can be removed. * yarn i18n * Delete call map entries instead of inserting nulls * Allow multiple active calls and consolidate call listeners * Fix a race condition when creating a video room * Un-hardcode the media device fallback labels * Apply misc code review fixes * yarn i18n * Disconnect from calls more politely on logout * Fix some strict mode errors * Fix another updateRoom race condition
This commit is contained in:
parent
50f6986f6c
commit
0d6a550c33
107 changed files with 2573 additions and 2157 deletions
|
@ -117,6 +117,10 @@ module.exports = {
|
|||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
|
||||
// The non-TypeScript rule produces false positives
|
||||
"func-call-spacing": "off",
|
||||
"@typescript-eslint/func-call-spacing": ["error"],
|
||||
},
|
||||
},
|
||||
// temporary override for offending icon require files
|
||||
|
|
|
@ -752,7 +752,7 @@ legend {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
@define-mixin CallButton {
|
||||
@define-mixin LegacyCallButton {
|
||||
box-sizing: border-box;
|
||||
font-weight: 600;
|
||||
height: $font-24px;
|
||||
|
|
|
@ -97,9 +97,9 @@
|
|||
@import "./views/avatars/_DecoratedRoomAvatar.pcss";
|
||||
@import "./views/avatars/_WidgetAvatar.pcss";
|
||||
@import "./views/beta/_BetaCard.pcss";
|
||||
@import "./views/context_menus/_CallContextMenu.pcss";
|
||||
@import "./views/context_menus/_DeviceContextMenu.pcss";
|
||||
@import "./views/context_menus/_IconizedContextMenu.pcss";
|
||||
@import "./views/context_menus/_LegacyCallContextMenu.pcss";
|
||||
@import "./views/context_menus/_MessageContextMenu.pcss";
|
||||
@import "./views/context_menus/_RoomGeneralContextMenu.pcss";
|
||||
@import "./views/context_menus/_RoomNotificationContextMenu.pcss";
|
||||
|
@ -207,13 +207,13 @@
|
|||
@import "./views/elements/_Validation.pcss";
|
||||
@import "./views/emojipicker/_EmojiPicker.pcss";
|
||||
@import "./views/location/_LocationPicker.pcss";
|
||||
@import "./views/messages/_CallEvent.pcss";
|
||||
@import "./views/messages/_CreateEvent.pcss";
|
||||
@import "./views/messages/_DateSeparator.pcss";
|
||||
@import "./views/messages/_DisambiguatedProfile.pcss";
|
||||
@import "./views/messages/_EventTileBubble.pcss";
|
||||
@import "./views/messages/_HiddenBody.pcss";
|
||||
@import "./views/messages/_JumpToDatePicker.pcss";
|
||||
@import "./views/messages/_LegacyCallEvent.pcss";
|
||||
@import "./views/messages/_MEmoteBody.pcss";
|
||||
@import "./views/messages/_MFileBody.pcss";
|
||||
@import "./views/messages/_MImageBody.pcss";
|
||||
|
@ -282,13 +282,13 @@
|
|||
@import "./views/rooms/_RoomPreviewCard.pcss";
|
||||
@import "./views/rooms/_RoomSublist.pcss";
|
||||
@import "./views/rooms/_RoomTile.pcss";
|
||||
@import "./views/rooms/_RoomTileCallSummary.pcss";
|
||||
@import "./views/rooms/_RoomUpgradeWarningBar.pcss";
|
||||
@import "./views/rooms/_SearchBar.pcss";
|
||||
@import "./views/rooms/_SendMessageComposer.pcss";
|
||||
@import "./views/rooms/_Stickers.pcss";
|
||||
@import "./views/rooms/_ThreadSummary.pcss";
|
||||
@import "./views/rooms/_TopUnreadMessagesBar.pcss";
|
||||
@import "./views/rooms/_VideoRoomSummary.pcss";
|
||||
@import "./views/rooms/_VoiceRecordComposerTile.pcss";
|
||||
@import "./views/rooms/_WhoIsTypingTile.pcss";
|
||||
@import "./views/settings/_AvatarSetting.pcss";
|
||||
|
@ -333,7 +333,7 @@
|
|||
@import "./views/spaces/_SpacePublicShare.pcss";
|
||||
@import "./views/terms/_InlineTermsAgreement.pcss";
|
||||
@import "./views/toasts/_AnalyticsToast.pcss";
|
||||
@import "./views/toasts/_IncomingCallToast.pcss";
|
||||
@import "./views/toasts/_IncomingLegacyCallToast.pcss";
|
||||
@import "./views/toasts/_NonUrgentEchoFailureToast.pcss";
|
||||
@import "./views/typography/_Heading.pcss";
|
||||
@import "./views/user-onboarding/_UserOnboardingButton.pcss";
|
||||
|
@ -343,15 +343,15 @@
|
|||
@import "./views/user-onboarding/_UserOnboardingPage.pcss";
|
||||
@import "./views/user-onboarding/_UserOnboardingTask.pcss";
|
||||
@import "./views/verification/_VerificationShowSas.pcss";
|
||||
@import "./views/voip/CallView/_CallViewButtons.pcss";
|
||||
@import "./views/voip/_CallPreview.pcss";
|
||||
@import "./views/voip/_CallView.pcss";
|
||||
@import "./views/voip/_CallViewForRoom.pcss";
|
||||
@import "./views/voip/_CallViewHeader.pcss";
|
||||
@import "./views/voip/_CallViewSidebar.pcss";
|
||||
@import "./views/voip/LegacyCallView/_LegacyCallViewButtons.pcss";
|
||||
@import "./views/voip/_CallLobby.pcss";
|
||||
@import "./views/voip/_DialPad.pcss";
|
||||
@import "./views/voip/_DialPadContextMenu.pcss";
|
||||
@import "./views/voip/_DialPadModal.pcss";
|
||||
@import "./views/voip/_LegacyCallPreview.pcss";
|
||||
@import "./views/voip/_LegacyCallView.pcss";
|
||||
@import "./views/voip/_LegacyCallViewForRoom.pcss";
|
||||
@import "./views/voip/_LegacyCallViewHeader.pcss";
|
||||
@import "./views/voip/_LegacyCallViewSidebar.pcss";
|
||||
@import "./views/voip/_PiPContainer.pcss";
|
||||
@import "./views/voip/_VideoFeed.pcss";
|
||||
@import "./views/voip/_VideoLobby.pcss";
|
||||
|
|
|
@ -34,7 +34,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
/* While the lobby is shown, the widget needs to stay loaded but hidden in the background */
|
||||
.mx_VideoLobby ~ .mx_AppTile {
|
||||
.mx_CallLobby ~ .mx_AppTile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_CallContextMenu_item {
|
||||
.mx_LegacyCallContextMenu_item {
|
||||
width: 205px;
|
||||
height: 40px;
|
||||
padding-left: 16px;
|
|
@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_CallEvent_wrapper {
|
||||
.mx_LegacyCallEvent_wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.mx_CallEvent {
|
||||
.mx_LegacyCallEvent {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
@ -35,7 +35,7 @@ limitations under the License.
|
|||
width: 65%;
|
||||
height: fit-content;
|
||||
|
||||
.mx_CallEvent_iconButton {
|
||||
.mx_LegacyCallEvent_iconButton {
|
||||
display: inline-flex;
|
||||
|
||||
&::before {
|
||||
|
@ -50,74 +50,74 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallEvent_silence::before {
|
||||
.mx_LegacyCallEvent_silence::before {
|
||||
mask-image: url('$(res)/img/voip/silence.svg');
|
||||
}
|
||||
|
||||
.mx_CallEvent_unSilence::before {
|
||||
.mx_LegacyCallEvent_unSilence::before {
|
||||
mask-image: url('$(res)/img/voip/un-silence.svg');
|
||||
}
|
||||
|
||||
&.mx_CallEvent_voice {
|
||||
.mx_CallEvent_type_icon::before,
|
||||
.mx_CallEvent_content_button_callBack span::before,
|
||||
.mx_CallEvent_content_button_answer span::before {
|
||||
&.mx_LegacyCallEvent_voice {
|
||||
.mx_LegacyCallEvent_type_icon::before,
|
||||
.mx_LegacyCallEvent_content_button_callBack span::before,
|
||||
.mx_LegacyCallEvent_content_button_answer span::before {
|
||||
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
||||
}
|
||||
|
||||
&.mx_CallEvent_rejected,
|
||||
&.mx_CallEvent_noAnswer {
|
||||
.mx_CallEvent_type_icon::before {
|
||||
&.mx_LegacyCallEvent_rejected,
|
||||
&.mx_LegacyCallEvent_noAnswer {
|
||||
.mx_LegacyCallEvent_type_icon::before {
|
||||
mask-image: url('$(res)/img/voip/declined-voice.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_CallEvent_video {
|
||||
.mx_CallEvent_type_icon::before,
|
||||
.mx_CallEvent_content_button_callBack span::before,
|
||||
.mx_CallEvent_content_button_answer span::before {
|
||||
&.mx_LegacyCallEvent_video {
|
||||
.mx_LegacyCallEvent_type_icon::before,
|
||||
.mx_LegacyCallEvent_content_button_callBack span::before,
|
||||
.mx_LegacyCallEvent_content_button_answer span::before {
|
||||
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
||||
}
|
||||
|
||||
&.mx_CallEvent_rejected,
|
||||
&.mx_CallEvent_noAnswer {
|
||||
.mx_CallEvent_type_icon::before {
|
||||
&.mx_LegacyCallEvent_rejected,
|
||||
&.mx_LegacyCallEvent_noAnswer {
|
||||
.mx_LegacyCallEvent_type_icon::before {
|
||||
mask-image: url('$(res)/img/voip/declined-video.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_CallEvent_missed {
|
||||
&.mx_CallEvent_voice {
|
||||
.mx_CallEvent_type_icon::before {
|
||||
&.mx_LegacyCallEvent_missed {
|
||||
&.mx_LegacyCallEvent_voice {
|
||||
.mx_LegacyCallEvent_type_icon::before {
|
||||
mask-image: url('$(res)/img/voip/missed-voice.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_CallEvent_video {
|
||||
.mx_CallEvent_type_icon::before {
|
||||
&.mx_LegacyCallEvent_video {
|
||||
.mx_LegacyCallEvent_type_icon::before {
|
||||
mask-image: url('$(res)/img/voip/missed-video.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallEvent_info {
|
||||
.mx_LegacyCallEvent_info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
|
||||
.mx_CallEvent_info_basic {
|
||||
.mx_LegacyCallEvent_info_basic {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-4;
|
||||
margin-left: 10px; /* To match mx_CallEvent */
|
||||
margin-left: 10px; /* To match mx_LegacyCallEvent */
|
||||
margin-right: 10px;
|
||||
min-width: 0;
|
||||
|
||||
.mx_CallEvent_sender {
|
||||
.mx_LegacyCallEvent_sender {
|
||||
font-weight: 600;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.8rem;
|
||||
|
@ -128,7 +128,7 @@ limitations under the License.
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mx_CallEvent_type {
|
||||
.mx_LegacyCallEvent_type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 400;
|
||||
|
@ -136,7 +136,7 @@ limitations under the License.
|
|||
font-size: 1.2rem;
|
||||
line-height: $font-13px;
|
||||
|
||||
.mx_CallEvent_type_icon {
|
||||
.mx_LegacyCallEvent_type_icon {
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
margin-right: 5px;
|
||||
|
@ -155,18 +155,18 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallEvent_content {
|
||||
.mx_LegacyCallEvent_content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
color: $secondary-content;
|
||||
gap: $spacing-12; /* See mx_IncomingCallToast_buttons */
|
||||
margin-inline-start: 42px; /* avatar (32px) + mx_CallEvent_info_basic margin (10px) */
|
||||
gap: $spacing-12; /* See mx_IncomingLegacyCallToast_buttons */
|
||||
margin-inline-start: 42px; /* avatar (32px) + mx_LegacyCallEvent_info_basic margin (10px) */
|
||||
word-break: break-word;
|
||||
max-width: fit-content;
|
||||
|
||||
.mx_CallEvent_content_button {
|
||||
@mixin CallButton;
|
||||
.mx_LegacyCallEvent_content_button {
|
||||
@mixin LegacyCallButton;
|
||||
padding: 0 $spacing-12;
|
||||
|
||||
span::before {
|
||||
|
@ -177,25 +177,25 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallEvent_content_button_reject {
|
||||
.mx_LegacyCallEvent_content_button_reject {
|
||||
span::before {
|
||||
mask-image: url('$(res)/img/element-icons/call/hangup.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallEvent_content_tooltip {
|
||||
.mx_LegacyCallEvent_content_tooltip {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_CallEvent_narrow {
|
||||
&.mx_LegacyCallEvent_narrow {
|
||||
flex-direction: column;
|
||||
align-items: unset;
|
||||
gap: $spacing-4 $spacing-16;
|
||||
height: unset;
|
||||
min-width: 290px;
|
||||
|
||||
.mx_CallEvent_iconButton {
|
||||
.mx_LegacyCallEvent_iconButton {
|
||||
position: absolute;
|
||||
margin-right: 0;
|
||||
top: 12px;
|
||||
|
@ -205,7 +205,7 @@ limitations under the License.
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.mx_CallEvent_info {
|
||||
.mx_LegacyCallEvent_info {
|
||||
align-items: unset;
|
||||
}
|
||||
}
|
||||
|
@ -213,8 +213,8 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_EventTile[data-layout="bubble"] {
|
||||
.mx_EventTile_e2eIcon + .mx_CallEvent_wrapper {
|
||||
.mx_CallEvent {
|
||||
.mx_EventTile_e2eIcon + .mx_LegacyCallEvent_wrapper {
|
||||
.mx_LegacyCallEvent {
|
||||
position: relative;
|
||||
|
||||
/* 5px (gap) + 14px (e2e icon size * mask-size) + 9px (margin-left of e2e icon) */
|
||||
|
@ -224,9 +224,9 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_EventTile_leftAlignedBubble {
|
||||
.mx_CallEvent_wrapper {
|
||||
.mx_CallEvent {
|
||||
&.mx_CallEvent_narrow {
|
||||
.mx_LegacyCallEvent_wrapper {
|
||||
.mx_LegacyCallEvent {
|
||||
&.mx_LegacyCallEvent_narrow {
|
||||
gap: $spacing-8 $spacing-4;
|
||||
}
|
||||
}
|
||||
|
@ -234,8 +234,8 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_IRCLayout {
|
||||
.mx_CallEvent_wrapper {
|
||||
.mx_CallEvent {
|
||||
.mx_LegacyCallEvent_wrapper {
|
||||
.mx_LegacyCallEvent {
|
||||
margin-inline-start: $spacing-4; /* display green line */
|
||||
}
|
||||
}
|
|
@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_VideoRoomSummary {
|
||||
.mx_VideoRoomSummary_indicator {
|
||||
.mx_RoomTileCallSummary {
|
||||
.mx_RoomTileCallSummary_text {
|
||||
&::before {
|
||||
display: inline-block;
|
||||
vertical-align: text-bottom;
|
||||
|
@ -28,7 +28,7 @@ limitations under the License.
|
|||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&.mx_VideoRoomSummary_indicator_active {
|
||||
&.mx_RoomTileCallSummary_text_active {
|
||||
color: $accent;
|
||||
|
||||
&::before {
|
||||
|
@ -37,7 +37,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_VideoRoomSummary_participants::before {
|
||||
.mx_RoomTileCallSummary_participants::before {
|
||||
display: inline-block;
|
||||
vertical-align: text-bottom;
|
||||
content: '';
|
|
@ -15,17 +15,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_IncomingCallToast {
|
||||
.mx_IncomingLegacyCallToast {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
pointer-events: initial; /* restore pointer events so the user can accept/decline */
|
||||
|
||||
.mx_IncomingCallToast_content {
|
||||
.mx_IncomingLegacyCallToast_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 8px;
|
||||
|
||||
.mx_CallEvent_caller {
|
||||
.mx_LegacyCallEvent_caller {
|
||||
font-weight: bold;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-18px;
|
||||
|
@ -40,7 +40,7 @@ limitations under the License.
|
|||
max-width: 200px;
|
||||
}
|
||||
|
||||
.mx_CallEvent_type {
|
||||
.mx_LegacyCallEvent_type {
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $tertiary-content;
|
||||
|
@ -52,7 +52,7 @@ limitations under the License.
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.mx_CallEvent_type_icon {
|
||||
.mx_LegacyCallEvent_type_icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-right: 6px;
|
||||
|
@ -69,28 +69,28 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
&.mx_IncomingCallToast_content_voice {
|
||||
.mx_CallEvent_type .mx_CallEvent_type_icon::before,
|
||||
.mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before {
|
||||
&.mx_IncomingLegacyCallToast_content_voice {
|
||||
.mx_LegacyCallEvent_type .mx_LegacyCallEvent_type_icon::before,
|
||||
.mx_IncomingLegacyCallToast_buttons .mx_IncomingLegacyCallToast_button_accept span::before {
|
||||
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_IncomingCallToast_content_video {
|
||||
.mx_CallEvent_type .mx_CallEvent_type_icon::before,
|
||||
.mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before {
|
||||
&.mx_IncomingLegacyCallToast_content_video {
|
||||
.mx_LegacyCallEvent_type .mx_LegacyCallEvent_type_icon::before,
|
||||
.mx_IncomingLegacyCallToast_buttons .mx_IncomingLegacyCallToast_button_accept span::before {
|
||||
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_IncomingCallToast_buttons {
|
||||
.mx_IncomingLegacyCallToast_buttons {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
|
||||
.mx_IncomingCallToast_button {
|
||||
@mixin CallButton;
|
||||
.mx_IncomingLegacyCallToast_button {
|
||||
@mixin LegacyCallButton;
|
||||
padding: 0px 8px;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
|
@ -100,13 +100,13 @@ limitations under the License.
|
|||
padding: 8px 0;
|
||||
}
|
||||
|
||||
&.mx_IncomingCallToast_button_accept span::before {
|
||||
&.mx_IncomingLegacyCallToast_button_accept span::before {
|
||||
mask-size: 13px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
&.mx_IncomingCallToast_button_decline span::before {
|
||||
&.mx_IncomingLegacyCallToast_button_decline span::before {
|
||||
mask-image: url('$(res)/img/element-icons/call/hangup.svg');
|
||||
mask-size: 16px;
|
||||
width: 16px;
|
||||
|
@ -116,7 +116,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_IncomingCallToast_iconButton {
|
||||
.mx_IncomingLegacyCallToast_iconButton {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
@ -133,11 +133,11 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_IncomingCallToast_silence::before {
|
||||
.mx_IncomingLegacyCallToast_silence::before {
|
||||
mask-image: url('$(res)/img/voip/silence.svg');
|
||||
}
|
||||
|
||||
.mx_IncomingCallToast_unSilence::before {
|
||||
.mx_IncomingLegacyCallToast_unSilence::before {
|
||||
mask-image: url('$(res)/img/voip/un-silence.svg');
|
||||
}
|
||||
}
|
|
@ -18,13 +18,13 @@ limitations under the License.
|
|||
|
||||
/* data-whatintent makes more sense here semantically but then the tooltip would stay visible without the button */
|
||||
/* which looks broken, so we match the behaviour of tooltips which is fine too. */
|
||||
[data-whatinput="mouse"] .mx_CallViewButtons.mx_CallViewButtons_hidden {
|
||||
[data-whatinput="mouse"] .mx_LegacyCallViewButtons.mx_LegacyCallViewButtons_hidden {
|
||||
opacity: 0.001; /* opacity 0 can cause a re-layout */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mx_CallViewButtons {
|
||||
--CallViewButtons_dropdownButton-size: 16px;
|
||||
.mx_LegacyCallViewButtons {
|
||||
--LegacyCallViewButtons_dropdownButton-size: 16px;
|
||||
|
||||
position: absolute;
|
||||
display: flex;
|
||||
|
@ -35,7 +35,7 @@ limitations under the License.
|
|||
z-index: 200; /* To be above _all_ feeds */
|
||||
gap: 18px;
|
||||
|
||||
.mx_CallViewButtons_button {
|
||||
.mx_LegacyCallViewButtons_button {
|
||||
cursor: pointer;
|
||||
|
||||
background-color: $call-view-button-on-background;
|
||||
|
@ -66,9 +66,9 @@ limitations under the License.
|
|||
width: 24px;
|
||||
}
|
||||
|
||||
&.mx_CallViewButtons_dropdownButton {
|
||||
width: var(--CallViewButtons_dropdownButton-size);
|
||||
height: var(--CallViewButtons_dropdownButton-size);
|
||||
&.mx_LegacyCallViewButtons_dropdownButton {
|
||||
width: var(--LegacyCallViewButtons_dropdownButton-size);
|
||||
height: var(--LegacyCallViewButtons_dropdownButton-size);
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
@ -80,28 +80,28 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/element-icons/message/chevron-up.svg');
|
||||
}
|
||||
|
||||
&.mx_CallViewButtons_dropdownButton_collapsed::before {
|
||||
&.mx_LegacyCallViewButtons_dropdownButton_collapsed::before {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* State buttons */
|
||||
&.mx_CallViewButtons_button_on {
|
||||
&.mx_LegacyCallViewButtons_button_on {
|
||||
background-color: $call-view-button-on-background;
|
||||
|
||||
&::before {
|
||||
background-color: $call-view-button-on-foreground;
|
||||
}
|
||||
|
||||
&.mx_CallViewButtons_button_mic::before {
|
||||
&.mx_LegacyCallViewButtons_button_mic::before {
|
||||
mask-image: url('$(res)/img/voip/call-view/mic-on.svg');
|
||||
}
|
||||
|
||||
&.mx_CallViewButtons_button_vid::before {
|
||||
&.mx_LegacyCallViewButtons_button_vid::before {
|
||||
mask-image: url('$(res)/img/voip/call-view/cam-on.svg');
|
||||
}
|
||||
|
||||
&.mx_CallViewButtons_button_screensharing {
|
||||
&.mx_LegacyCallViewButtons_button_screensharing {
|
||||
background-color: $accent;
|
||||
|
||||
&::before {
|
||||
|
@ -110,27 +110,27 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
&.mx_CallViewButtons_button_sidebar::before {
|
||||
&.mx_LegacyCallViewButtons_button_sidebar::before {
|
||||
mask-image: url('$(res)/img/voip/call-view/sidebar-on.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_CallViewButtons_button_off {
|
||||
&.mx_LegacyCallViewButtons_button_off {
|
||||
background-color: $call-view-button-off-background;
|
||||
|
||||
&::before {
|
||||
background-color: $call-view-button-off-foreground;
|
||||
}
|
||||
|
||||
&.mx_CallViewButtons_button_mic::before {
|
||||
&.mx_LegacyCallViewButtons_button_mic::before {
|
||||
mask-image: url('$(res)/img/voip/call-view/mic-off.svg');
|
||||
}
|
||||
|
||||
&.mx_CallViewButtons_button_vid::before {
|
||||
&.mx_LegacyCallViewButtons_button_vid::before {
|
||||
mask-image: url('$(res)/img/voip/call-view/cam-off.svg');
|
||||
}
|
||||
|
||||
&.mx_CallViewButtons_button_screensharing {
|
||||
&.mx_LegacyCallViewButtons_button_screensharing {
|
||||
background-color: $call-view-button-on-background;
|
||||
|
||||
&::before {
|
||||
|
@ -139,7 +139,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
&.mx_CallViewButtons_button_sidebar {
|
||||
&.mx_LegacyCallViewButtons_button_sidebar {
|
||||
background-color: $call-view-button-on-background;
|
||||
|
||||
&::before {
|
||||
|
@ -151,11 +151,11 @@ limitations under the License.
|
|||
/* State buttons */
|
||||
|
||||
/* Stateless buttons */
|
||||
&.mx_CallViewButtons_dialpad::before {
|
||||
&.mx_LegacyCallViewButtons_dialpad::before {
|
||||
mask-image: url('$(res)/img/voip/call-view/dialpad.svg');
|
||||
}
|
||||
|
||||
&.mx_CallViewButtons_button_hangup {
|
||||
&.mx_LegacyCallViewButtons_button_hangup {
|
||||
background-color: $alert;
|
||||
|
||||
&::before {
|
||||
|
@ -164,13 +164,13 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
&.mx_CallViewButtons_button_more::before {
|
||||
&.mx_LegacyCallViewButtons_button_more::before {
|
||||
mask-image: url('$(res)/img/voip/call-view/more.svg');
|
||||
}
|
||||
/* Stateless buttons */
|
||||
|
||||
/* Invisible state */
|
||||
&.mx_CallViewButtons_button_invisible {
|
||||
&.mx_LegacyCallViewButtons_button_invisible {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
|
@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_VideoLobby {
|
||||
.mx_CallLobby {
|
||||
min-height: 0;
|
||||
flex-grow: 1;
|
||||
padding: $spacing-12;
|
||||
color: $video-lobby-primary-content;
|
||||
background-color: $video-lobby-background;
|
||||
color: $call-lobby-primary-content;
|
||||
background-color: $call-lobby-background;
|
||||
border-radius: 8px;
|
||||
|
||||
display: flex;
|
||||
|
@ -33,16 +33,16 @@ limitations under the License.
|
|||
margin: $spacing-8 auto 0;
|
||||
|
||||
.mx_FacePile_faces .mx_BaseAvatar_image {
|
||||
border-color: $video-lobby-background;
|
||||
border-color: $call-lobby-background;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_VideoLobby_preview {
|
||||
.mx_CallLobby_preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
aspect-ratio: 1.5;
|
||||
background-color: $video-lobby-system;
|
||||
background-color: $call-lobby-system;
|
||||
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
|
@ -74,29 +74,29 @@ limitations under the License.
|
|||
background-color: black;
|
||||
}
|
||||
|
||||
.mx_VideoLobby_controls {
|
||||
.mx_CallLobby_controls {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
background-color: rgba($video-lobby-background, 0.9);
|
||||
background-color: rgba($call-lobby-background, 0.9);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: $spacing-24;
|
||||
|
||||
.mx_VideoLobby_deviceButtonWrapper {
|
||||
.mx_CallLobby_deviceButtonWrapper {
|
||||
position: relative;
|
||||
margin: 6px 0 10px;
|
||||
|
||||
.mx_VideoLobby_deviceButton {
|
||||
.mx_CallLobby_deviceButton {
|
||||
$size: 50px;
|
||||
|
||||
width: $size;
|
||||
height: $size;
|
||||
|
||||
background-color: $video-lobby-primary-content;
|
||||
background-color: $call-lobby-system;
|
||||
border-radius: calc($size / 2);
|
||||
|
||||
&::before {
|
||||
|
@ -105,21 +105,21 @@ limitations under the License.
|
|||
mask-repeat: no-repeat;
|
||||
mask-size: 20px;
|
||||
mask-position: center;
|
||||
background-color: $video-lobby-system;
|
||||
background-color: $call-lobby-primary-content;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.mx_VideoLobby_deviceButton_audio::before {
|
||||
mask-image: url('$(res)/img/voip/call-view/mic-off.svg');
|
||||
&.mx_CallLobby_deviceButton_audio::before {
|
||||
mask-image: url('$(res)/img/voip/call-view/mic-on.svg');
|
||||
}
|
||||
|
||||
&.mx_VideoLobby_deviceButton_video::before {
|
||||
mask-image: url('$(res)/img/voip/call-view/cam-off.svg');
|
||||
&.mx_CallLobby_deviceButton_video::before {
|
||||
mask-image: url('$(res)/img/voip/call-view/cam-on.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_VideoLobby_deviceListButton {
|
||||
.mx_CallLobby_deviceListButton {
|
||||
$size: 15px;
|
||||
|
||||
position: absolute;
|
||||
|
@ -128,7 +128,7 @@ limitations under the License.
|
|||
width: $size;
|
||||
height: $size;
|
||||
|
||||
background-color: $video-lobby-primary-content;
|
||||
background-color: $call-lobby-system;
|
||||
border-radius: calc($size / 2);
|
||||
|
||||
&::before {
|
||||
|
@ -137,29 +137,29 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||
mask-size: $size;
|
||||
mask-position: center;
|
||||
background-color: $video-lobby-system;
|
||||
background-color: $call-lobby-primary-content;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_VideoLobby_deviceButtonWrapper_active {
|
||||
.mx_VideoLobby_deviceButton,
|
||||
.mx_VideoLobby_deviceListButton {
|
||||
background-color: $video-lobby-system;
|
||||
&.mx_CallLobby_deviceButtonWrapper_muted {
|
||||
.mx_CallLobby_deviceButton,
|
||||
.mx_CallLobby_deviceListButton {
|
||||
background-color: $call-lobby-primary-content;
|
||||
|
||||
&::before {
|
||||
background-color: $video-lobby-primary-content;
|
||||
background-color: $call-lobby-system;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_VideoLobby_deviceButton {
|
||||
&.mx_VideoLobby_deviceButton_audio::before {
|
||||
mask-image: url('$(res)/img/voip/call-view/mic-on.svg');
|
||||
.mx_CallLobby_deviceButton {
|
||||
&.mx_CallLobby_deviceButton_audio::before {
|
||||
mask-image: url('$(res)/img/voip/call-view/mic-off.svg');
|
||||
}
|
||||
|
||||
&.mx_VideoLobby_deviceButton_video::before {
|
||||
mask-image: url('$(res)/img/voip/call-view/cam-on.svg');
|
||||
&.mx_CallLobby_deviceButton_video::before {
|
||||
mask-image: url('$(res)/img/voip/call-view/cam-off.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -167,7 +167,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_VideoLobby_joinButton {
|
||||
.mx_CallLobby_connectButton {
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_CallPreview {
|
||||
.mx_LegacyCallPreview {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
|
@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_CallView {
|
||||
.mx_LegacyCallView {
|
||||
border-radius: 8px;
|
||||
background-color: $dark-panel-bg-color;
|
||||
padding-left: 8px;
|
||||
|
@ -24,7 +24,7 @@ limitations under the License.
|
|||
/* XXX: PiPContainer sets pointer-events: none - should probably be set back in a better place */
|
||||
pointer-events: initial;
|
||||
|
||||
.mx_CallView_toast {
|
||||
.mx_LegacyCallView_toast {
|
||||
position: absolute;
|
||||
top: 74px;
|
||||
|
||||
|
@ -38,7 +38,7 @@ limitations under the License.
|
|||
background-color: #17191c;
|
||||
}
|
||||
|
||||
.mx_CallView_content_wrapper {
|
||||
.mx_LegacyCallView_content_wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
|
@ -47,7 +47,7 @@ limitations under the License.
|
|||
|
||||
overflow: hidden;
|
||||
|
||||
.mx_CallView_content {
|
||||
.mx_LegacyCallView_content {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
|
@ -65,12 +65,12 @@ limitations under the License.
|
|||
|
||||
background-color: $call-view-content-background;
|
||||
|
||||
.mx_CallView_status {
|
||||
.mx_LegacyCallView_status {
|
||||
z-index: 50;
|
||||
color: $accent-fg-color;
|
||||
}
|
||||
|
||||
.mx_CallView_avatarsContainer {
|
||||
.mx_LegacyCallView_avatarsContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
@ -82,7 +82,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallView_holdBackground {
|
||||
.mx_LegacyCallView_holdBackground {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
@ -107,7 +107,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
&.mx_CallView_content_hold .mx_CallView_status {
|
||||
&.mx_LegacyCallView_content_hold .mx_LegacyCallView_status {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
|
||||
|
@ -123,7 +123,7 @@ limitations under the License.
|
|||
background-size: cover;
|
||||
}
|
||||
|
||||
.mx_CallView_pip &::before {
|
||||
.mx_LegacyCallView_pip &::before {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
@ -131,7 +131,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
&:not(.mx_CallView_sidebar) .mx_CallView_content {
|
||||
&:not(.mx_LegacyCallView_sidebar) .mx_LegacyCallView_content {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
@ -145,7 +145,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
&.mx_CallView_pip {
|
||||
&.mx_LegacyCallView_pip {
|
||||
width: 320px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
|
@ -154,16 +154,16 @@ limitations under the License.
|
|||
background-color: $system;
|
||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.mx_CallViewButtons {
|
||||
.mx_LegacyCallViewButtons {
|
||||
bottom: 13px;
|
||||
|
||||
.mx_CallViewButtons_button {
|
||||
.mx_LegacyCallViewButtons_button {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
|
||||
&.mx_CallViewButtons_dropdownButton {
|
||||
width: var(--CallViewButtons_dropdownButton-size);
|
||||
height: var(--CallViewButtons_dropdownButton-size);
|
||||
&.mx_LegacyCallViewButtons_dropdownButton {
|
||||
width: var(--LegacyCallViewButtons_dropdownButton-size);
|
||||
height: var(--LegacyCallViewButtons_dropdownButton-size);
|
||||
}
|
||||
|
||||
&::before {
|
||||
|
@ -173,12 +173,12 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallView_content {
|
||||
.mx_LegacyCallView_content {
|
||||
min-height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_CallView_large {
|
||||
&.mx_LegacyCallView_large {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@ -193,7 +193,7 @@ limitations under the License.
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&.mx_CallView_belowWidget {
|
||||
&.mx_LegacyCallView_belowWidget {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
|
@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_CallViewForRoom {
|
||||
.mx_LegacyCallViewForRoom {
|
||||
overflow: hidden;
|
||||
|
||||
.mx_CallViewForRoom_ResizeWrapper {
|
||||
.mx_LegacyCallViewForRoom_ResizeWrapper {
|
||||
display: flex;
|
||||
|
||||
&:hover .mx_CallViewForRoom_ResizeHandle {
|
||||
&:hover .mx_LegacyCallViewForRoom_ResizeHandle {
|
||||
/* Need to use important to override element style attributes */
|
||||
/* set by re-resizable */
|
||||
width: 100% !important;
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_CallViewHeader {
|
||||
.mx_LegacyCallViewHeader {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -24,18 +24,18 @@ limitations under the License.
|
|||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
|
||||
&.mx_CallViewHeader_pip {
|
||||
&.mx_LegacyCallViewHeader_pip {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallViewHeader_text {
|
||||
.mx_LegacyCallViewHeader_text {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mx_CallViewHeader_secondaryCallInfo {
|
||||
.mx_LegacyCallViewHeader_secondaryCallInfo {
|
||||
&::before {
|
||||
content: '·';
|
||||
margin-left: 6px;
|
||||
|
@ -43,13 +43,13 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_CallViewHeader_controls {
|
||||
.mx_LegacyCallViewHeader_controls {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.mx_CallViewHeader_button {
|
||||
.mx_LegacyCallViewHeader_button {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
|
@ -66,32 +66,32 @@ limitations under the License.
|
|||
mask-position: center;
|
||||
}
|
||||
|
||||
&.mx_CallViewHeader_button_fullscreen {
|
||||
&.mx_LegacyCallViewHeader_button_fullscreen {
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_CallViewHeader_button_pin {
|
||||
&.mx_LegacyCallViewHeader_button_pin {
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_CallViewHeader_button_expand {
|
||||
&.mx_LegacyCallViewHeader_button_expand {
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/element-icons/call/expand.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallViewHeader_callInfo {
|
||||
.mx_LegacyCallViewHeader_callInfo {
|
||||
margin-left: 12px;
|
||||
margin-right: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mx_CallViewHeader_roomName {
|
||||
.mx_LegacyCallViewHeader_roomName {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
line-height: initial;
|
||||
|
@ -102,11 +102,11 @@ limitations under the License.
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mx_CallView_secondaryCall_roomName {
|
||||
.mx_LegacyCallView_secondaryCall_roomName {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.mx_CallViewHeader_icon {
|
||||
.mx_LegacyCallViewHeader_icon {
|
||||
display: inline-block;
|
||||
margin-right: 6px;
|
||||
height: 16px;
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_CallViewSidebar {
|
||||
.mx_LegacyCallViewSidebar {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
|
||||
|
@ -41,7 +41,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
&.mx_CallViewSidebar_pipMode {
|
||||
&.mx_LegacyCallViewSidebar_pipMode {
|
||||
top: 16px;
|
||||
bottom: unset;
|
||||
justify-content: flex-end;
|
|
@ -188,9 +188,9 @@ $call-view-content-background: $quinary-content;
|
|||
|
||||
$video-feed-secondary-background: $system;
|
||||
|
||||
$video-lobby-system: $system;
|
||||
$video-lobby-background: $background;
|
||||
$video-lobby-primary-content: $primary-content;
|
||||
$call-lobby-system: $system;
|
||||
$call-lobby-background: $background;
|
||||
$call-lobby-primary-content: $primary-content;
|
||||
/* ******************** */
|
||||
|
||||
/* Location sharing */
|
||||
|
|
|
@ -119,9 +119,9 @@ $call-view-content-background: $quinary-content;
|
|||
|
||||
$video-feed-secondary-background: $system;
|
||||
|
||||
$video-lobby-system: $system;
|
||||
$video-lobby-background: $background;
|
||||
$video-lobby-primary-content: $primary-content;
|
||||
$call-lobby-system: $system;
|
||||
$call-lobby-background: $background;
|
||||
$call-lobby-primary-content: $primary-content;
|
||||
|
||||
$roomlist-filter-active-bg-color: $panel-actions;
|
||||
$roomlist-bg-color: $header-panel-bg-color;
|
||||
|
|
|
@ -180,9 +180,9 @@ $call-view-content-background: #21262C;
|
|||
$video-feed-secondary-background: #394049; /* XXX: Color from dark theme */
|
||||
|
||||
/* All of these are from dark theme */
|
||||
$video-lobby-system: #21262C;
|
||||
$video-lobby-background: #15191E;
|
||||
$video-lobby-primary-content: #FFFFFF;
|
||||
$call-lobby-system: #21262C;
|
||||
$call-lobby-background: #15191E;
|
||||
$call-lobby-primary-content: #FFFFFF;
|
||||
|
||||
$username-variant1-color: #368bd6;
|
||||
$username-variant2-color: #ac3ba8;
|
||||
|
|
|
@ -287,9 +287,9 @@ $video-feed-secondary-background: #394049; /* XXX: Color from dark theme */
|
|||
$voipcall-plinth-color: $system;
|
||||
|
||||
/* All of these are from dark theme */
|
||||
$video-lobby-system: #21262C;
|
||||
$video-lobby-background: #15191E;
|
||||
$video-lobby-primary-content: #FFFFFF;
|
||||
$call-lobby-system: #21262C;
|
||||
$call-lobby-background: #15191E;
|
||||
$call-lobby-primary-content: #FFFFFF;
|
||||
/* ******************** */
|
||||
|
||||
/* One-off colors */
|
||||
|
|
4
src/@types/global.d.ts
vendored
4
src/@types/global.d.ts
vendored
|
@ -33,7 +33,7 @@ import { Notifier } from "../Notifier";
|
|||
import type { Renderer } from "react-dom";
|
||||
import RightPanelStore from "../stores/right-panel/RightPanelStore";
|
||||
import WidgetStore from "../stores/WidgetStore";
|
||||
import CallHandler from "../CallHandler";
|
||||
import LegacyCallHandler from "../LegacyCallHandler";
|
||||
import UserActivity from "../UserActivity";
|
||||
import { ModalWidgetStore } from "../stores/ModalWidgetStore";
|
||||
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||
|
@ -89,7 +89,7 @@ declare global {
|
|||
mxRightPanelStore: RightPanelStore;
|
||||
mxWidgetStore: WidgetStore;
|
||||
mxWidgetLayoutStore: WidgetLayoutStore;
|
||||
mxCallHandler: CallHandler;
|
||||
mxLegacyCallHandler: LegacyCallHandler;
|
||||
mxUserActivity: UserActivity;
|
||||
mxModalWidgetStore: ModalWidgetStore;
|
||||
mxVoipUserMapper: VoipUserMapper;
|
||||
|
|
|
@ -54,7 +54,7 @@ import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/
|
|||
import SdkConfig from './SdkConfig';
|
||||
import { ensureDMExists } from './createRoom';
|
||||
import { Container, WidgetLayoutStore } from './stores/widgets/WidgetLayoutStore';
|
||||
import IncomingCallToast, { getIncomingCallToastKey } from './toasts/IncomingCallToast';
|
||||
import IncomingLegacyCallToast, { getIncomingLegacyCallToastKey } from './toasts/IncomingLegacyCallToast';
|
||||
import ToastStore from './stores/ToastStore';
|
||||
import Resend from './Resend';
|
||||
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
||||
|
@ -100,7 +100,7 @@ interface ThirdpartyLookupResponse {
|
|||
fields: ThirdpartyLookupResponseFields;
|
||||
}
|
||||
|
||||
export enum CallHandlerEvent {
|
||||
export enum LegacyCallHandlerEvent {
|
||||
CallsChanged = "calls_changed",
|
||||
CallChangeRoom = "call_change_room",
|
||||
SilencedCallsChanged = "silenced_calls_changed",
|
||||
|
@ -108,11 +108,11 @@ export enum CallHandlerEvent {
|
|||
}
|
||||
|
||||
/**
|
||||
* CallHandler manages all currently active calls. It should be used for
|
||||
* LegacyCallHandler manages all currently active calls. It should be used for
|
||||
* placing, answering, rejecting and hanging up calls. It also handles ringing,
|
||||
* PSTN support and other things.
|
||||
*/
|
||||
export default class CallHandler extends EventEmitter {
|
||||
export default class LegacyCallHandler extends EventEmitter {
|
||||
private calls = new Map<string, MatrixCall>(); // roomId -> call
|
||||
// Calls started as an attended transfer, ie. with the intention of transferring another
|
||||
// call with a different party to this one.
|
||||
|
@ -130,11 +130,11 @@ export default class CallHandler extends EventEmitter {
|
|||
private silencedCalls = new Set<string>(); // callIds
|
||||
|
||||
public static get instance() {
|
||||
if (!window.mxCallHandler) {
|
||||
window.mxCallHandler = new CallHandler();
|
||||
if (!window.mxLegacyCallHandler) {
|
||||
window.mxLegacyCallHandler = new LegacyCallHandler();
|
||||
}
|
||||
|
||||
return window.mxCallHandler;
|
||||
return window.mxLegacyCallHandler;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -186,7 +186,7 @@ export default class CallHandler extends EventEmitter {
|
|||
|
||||
public silenceCall(callId: string): void {
|
||||
this.silencedCalls.add(callId);
|
||||
this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
|
||||
this.emit(LegacyCallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
|
||||
|
||||
// Don't pause audio if we have calls which are still ringing
|
||||
if (this.areAnyCallsUnsilenced()) return;
|
||||
|
@ -195,7 +195,7 @@ export default class CallHandler extends EventEmitter {
|
|||
|
||||
public unSilenceCall(callId: string): void {
|
||||
this.silencedCalls.delete(callId);
|
||||
this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
|
||||
this.emit(LegacyCallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
|
||||
this.play(AudioID.Ring);
|
||||
}
|
||||
|
||||
|
@ -311,7 +311,7 @@ export default class CallHandler extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
const mappedRoomId = CallHandler.instance.roomIdForCall(call);
|
||||
const mappedRoomId = LegacyCallHandler.instance.roomIdForCall(call);
|
||||
if (this.getCallForRoom(mappedRoomId)) {
|
||||
logger.log(
|
||||
"Got incoming call for room " + mappedRoomId +
|
||||
|
@ -389,7 +389,7 @@ export default class CallHandler extends EventEmitter {
|
|||
}
|
||||
|
||||
public play(audioId: AudioID): void {
|
||||
const logPrefix = `CallHandler.play(${audioId}):`;
|
||||
const logPrefix = `LegacyCallHandler.play(${audioId}):`;
|
||||
logger.debug(`${logPrefix} beginning of function`);
|
||||
// TODO: Attach an invisible element for this instead
|
||||
// which listens?
|
||||
|
@ -424,7 +424,7 @@ export default class CallHandler extends EventEmitter {
|
|||
}
|
||||
|
||||
public pause(audioId: AudioID): void {
|
||||
const logPrefix = `CallHandler.pause(${audioId}):`;
|
||||
const logPrefix = `LegacyCallHandler.pause(${audioId}):`;
|
||||
logger.debug(`${logPrefix} beginning of function`);
|
||||
// TODO: Attach an invisible element for this instead
|
||||
// which listens?
|
||||
|
@ -688,32 +688,32 @@ export default class CallHandler extends EventEmitter {
|
|||
}
|
||||
|
||||
private setCallState(call: MatrixCall, status: CallState): void {
|
||||
const mappedRoomId = CallHandler.instance.roomIdForCall(call);
|
||||
const mappedRoomId = LegacyCallHandler.instance.roomIdForCall(call);
|
||||
|
||||
logger.log(
|
||||
`Call state in ${mappedRoomId} changed to ${status}`,
|
||||
);
|
||||
|
||||
const toastKey = getIncomingCallToastKey(call.callId);
|
||||
const toastKey = getIncomingLegacyCallToastKey(call.callId);
|
||||
if (status === CallState.Ringing) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: toastKey,
|
||||
priority: 100,
|
||||
component: IncomingCallToast,
|
||||
bodyClassName: "mx_IncomingCallToast",
|
||||
component: IncomingLegacyCallToast,
|
||||
bodyClassName: "mx_IncomingLegacyCallToast",
|
||||
props: { call },
|
||||
});
|
||||
} else {
|
||||
ToastStore.sharedInstance().dismissToast(toastKey);
|
||||
}
|
||||
|
||||
this.emit(CallHandlerEvent.CallState, mappedRoomId, status);
|
||||
this.emit(LegacyCallHandlerEvent.CallState, mappedRoomId, status);
|
||||
}
|
||||
|
||||
private removeCallForRoom(roomId: string): void {
|
||||
logger.log("Removing call for room ", roomId);
|
||||
this.calls.delete(roomId);
|
||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||
this.emit(LegacyCallHandlerEvent.CallsChanged, this.calls);
|
||||
}
|
||||
|
||||
private showICEFallbackPrompt(): void {
|
||||
|
@ -1115,9 +1115,9 @@ export default class CallHandler extends EventEmitter {
|
|||
|
||||
// Should we always emit CallsChanged too?
|
||||
if (changedRooms) {
|
||||
this.emit(CallHandlerEvent.CallChangeRoom, call);
|
||||
this.emit(LegacyCallHandlerEvent.CallChangeRoom, call);
|
||||
} else {
|
||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||
this.emit(LegacyCallHandlerEvent.CallsChanged, this.calls);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -48,7 +48,7 @@ import { Jitsi } from "./widgets/Jitsi";
|
|||
import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY } from "./BasePlatform";
|
||||
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
||||
import { PosthogAnalytics } from "./PosthogAnalytics";
|
||||
import CallHandler from './CallHandler';
|
||||
import LegacyCallHandler from './LegacyCallHandler';
|
||||
import LifecycleCustomisations from "./customisations/Lifecycle";
|
||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||
import { _t } from "./languageHandler";
|
||||
|
@ -59,8 +59,6 @@ import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialo
|
|||
import { setSentryUser } from "./sentry";
|
||||
import SdkConfig from "./SdkConfig";
|
||||
import { DialogOpener } from "./utils/DialogOpener";
|
||||
import VideoChannelStore from "./stores/VideoChannelStore";
|
||||
import { fixStuckDevices } from "./utils/VideoChannelUtils";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLocalStorageSettingsHandler";
|
||||
import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload";
|
||||
|
@ -808,7 +806,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
|
|||
DMRoomMap.makeShared().start();
|
||||
IntegrationManagers.sharedInstance().startWatching();
|
||||
ActiveWidgetStore.instance.start();
|
||||
CallHandler.instance.start();
|
||||
LegacyCallHandler.instance.start();
|
||||
|
||||
// Start Mjolnir even though we haven't checked the feature flag yet. Starting
|
||||
// the thing just wastes CPU cycles, but should result in no actual functionality
|
||||
|
@ -840,11 +838,6 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
|
|||
// Now that we have a MatrixClientPeg, update the Jitsi info
|
||||
Jitsi.getInstance().start();
|
||||
|
||||
// In case we disconnected uncleanly from a video room, clean up the stuck device
|
||||
if (VideoChannelStore.instance.roomId) {
|
||||
fixStuckDevices(MatrixClientPeg.get().getRoom(VideoChannelStore.instance.roomId), false);
|
||||
}
|
||||
|
||||
// dispatch that we finished starting up to wire up any other bits
|
||||
// of the matrix client that cannot be set prior to starting up.
|
||||
dis.dispatch({ action: 'client_started' });
|
||||
|
@ -932,7 +925,7 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
|
|||
*/
|
||||
export function stopMatrixClient(unsetClient = true): void {
|
||||
Notifier.stop();
|
||||
CallHandler.instance.stop();
|
||||
LegacyCallHandler.instance.stop();
|
||||
UserActivity.sharedInstance().stop();
|
||||
TypingStore.sharedInstance().reset();
|
||||
Presence.stop();
|
||||
|
|
|
@ -137,4 +137,18 @@ export default class MediaDeviceHandler extends EventEmitter {
|
|||
case MediaDeviceKindEnum.VideoInput: return this.getVideoInput();
|
||||
}
|
||||
}
|
||||
|
||||
public static get startWithAudioMuted(): boolean {
|
||||
return SettingsStore.getValue("audioInputMuted");
|
||||
}
|
||||
public static set startWithAudioMuted(value: boolean) {
|
||||
SettingsStore.setValue("audioInputMuted", null, SettingLevel.DEVICE, value);
|
||||
}
|
||||
|
||||
public static get startWithVideoMuted(): boolean {
|
||||
return SettingsStore.getValue("videoInputMuted");
|
||||
}
|
||||
public static set startWithVideoMuted(value: boolean) {
|
||||
SettingsStore.setValue("videoInputMuted", null, SettingLevel.DEVICE, value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ import { RoomViewStore } from "./stores/RoomViewStore";
|
|||
import UserActivity from "./UserActivity";
|
||||
import { mediaFromMxc } from "./customisations/Media";
|
||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||
import CallHandler from "./CallHandler";
|
||||
import LegacyCallHandler from "./LegacyCallHandler";
|
||||
import VoipUserMapper from "./VoipUserMapper";
|
||||
|
||||
/*
|
||||
|
@ -397,7 +397,7 @@ export const Notifier = {
|
|||
|
||||
_evaluateEvent: function(ev: MatrixEvent) {
|
||||
let roomId = ev.getRoomId();
|
||||
if (CallHandler.instance.getSupportsVirtualRooms()) {
|
||||
if (LegacyCallHandler.instance.getSupportsVirtualRooms()) {
|
||||
// Attempt to translate a virtual room to a native one
|
||||
const nativeRoomId = VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(roomId);
|
||||
if (nativeRoomId) {
|
||||
|
|
|
@ -52,7 +52,7 @@ import SdkConfig from "./SdkConfig";
|
|||
import SettingsStore from "./settings/SettingsStore";
|
||||
import { UIComponent, UIFeature } from "./settings/UIFeature";
|
||||
import { CHAT_EFFECTS } from "./effects";
|
||||
import CallHandler from "./CallHandler";
|
||||
import LegacyCallHandler from "./LegacyCallHandler";
|
||||
import { guessAndSetDMRoom } from "./Rooms";
|
||||
import { upgradeRoom } from './utils/RoomUpgrade';
|
||||
import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog';
|
||||
|
@ -1183,7 +1183,7 @@ export const Commands = [
|
|||
description: _td("Switches to this room's virtual room, if it has one"),
|
||||
category: CommandCategories.advanced,
|
||||
isEnabled(): boolean {
|
||||
return CallHandler.instance.getSupportsVirtualRooms() && !isCurrentLocalRoom();
|
||||
return LegacyCallHandler.instance.getSupportsVirtualRooms() && !isCurrentLocalRoom();
|
||||
},
|
||||
runFn: (roomId) => {
|
||||
return success((async () => {
|
||||
|
@ -1212,7 +1212,7 @@ export const Commands = [
|
|||
|
||||
return success((async () => {
|
||||
if (isPhoneNumber) {
|
||||
const results = await CallHandler.instance.pstnLookup(this.state.value);
|
||||
const results = await LegacyCallHandler.instance.pstnLookup(this.state.value);
|
||||
if (!results || results.length === 0 || !results[0].userid) {
|
||||
throw newTranslatableError("Unable to find Matrix ID for phone number");
|
||||
}
|
||||
|
@ -1269,7 +1269,7 @@ export const Commands = [
|
|||
category: CommandCategories.other,
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
const call = CallHandler.instance.getCallForRoom(roomId);
|
||||
const call = LegacyCallHandler.instance.getCallForRoom(roomId);
|
||||
if (!call) {
|
||||
return reject(newTranslatableError("No active call in this room"));
|
||||
}
|
||||
|
@ -1284,7 +1284,7 @@ export const Commands = [
|
|||
category: CommandCategories.other,
|
||||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function(roomId, args) {
|
||||
const call = CallHandler.instance.getCallForRoom(roomId);
|
||||
const call = LegacyCallHandler.instance.getCallForRoom(roomId);
|
||||
if (!call) {
|
||||
return reject(newTranslatableError("No active call in this room"));
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import { EventType } from 'matrix-js-sdk/src/@types/event';
|
|||
import { ensureVirtualRoomExists } from './createRoom';
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import DMRoomMap from "./utils/DMRoomMap";
|
||||
import CallHandler from './CallHandler';
|
||||
import LegacyCallHandler from './LegacyCallHandler';
|
||||
import { VIRTUAL_ROOM_EVENT_TYPE } from "./call-types";
|
||||
import { findDMForUser } from './utils/dm/findDMForUser';
|
||||
|
||||
|
@ -39,7 +39,7 @@ export default class VoipUserMapper {
|
|||
}
|
||||
|
||||
private async userToVirtualUser(userId: string): Promise<string> {
|
||||
const results = await CallHandler.instance.sipVirtualLookup(userId);
|
||||
const results = await LegacyCallHandler.instance.sipVirtualLookup(userId);
|
||||
if (results.length === 0 || !results[0].fields.lookup_success) return null;
|
||||
return results[0].userid;
|
||||
}
|
||||
|
@ -118,11 +118,11 @@ export default class VoipUserMapper {
|
|||
}
|
||||
|
||||
public async onNewInvitedRoom(invitedRoom: Room): Promise<void> {
|
||||
if (!CallHandler.instance.getSupportsVirtualRooms()) return;
|
||||
if (!LegacyCallHandler.instance.getSupportsVirtualRooms()) return;
|
||||
|
||||
const inviterId = invitedRoom.getDMInviter();
|
||||
logger.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
|
||||
const result = await CallHandler.instance.sipNativeLookup(inviterId);
|
||||
const result = await LegacyCallHandler.instance.sipNativeLookup(inviterId);
|
||||
if (result.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import classNames from "classnames";
|
|||
import dis from "../../dispatcher/dispatcher";
|
||||
import { _t } from "../../languageHandler";
|
||||
import RoomList from "../views/rooms/RoomList";
|
||||
import CallHandler from "../../CallHandler";
|
||||
import LegacyCallHandler from "../../LegacyCallHandler";
|
||||
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import RoomSearch from "./RoomSearch";
|
||||
|
@ -325,7 +325,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
|
||||
// If we have dialer support, show a button to bring up the dial pad
|
||||
// to start a new call
|
||||
if (CallHandler.instance.getSupportsPstnProtocol()) {
|
||||
if (LegacyCallHandler.instance.getSupportsPstnProtocol()) {
|
||||
dialPadButton =
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_LeftPanel_dialPadButton", {})}
|
||||
|
|
|
@ -19,10 +19,10 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
import { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
|
||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../LegacyCallHandler';
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
|
||||
export enum CallEventGrouperEvent {
|
||||
export enum LegacyCallEventGrouperEvent {
|
||||
StateChanged = "state_changed",
|
||||
SilencedChanged = "silenced_changed",
|
||||
LengthChanged = "length_changed",
|
||||
|
@ -44,10 +44,10 @@ export enum CustomCallState {
|
|||
Missed = "missed",
|
||||
}
|
||||
|
||||
export function buildCallEventGroupers(
|
||||
callEventGroupers: Map<string, CallEventGrouper>,
|
||||
export function buildLegacyCallEventGroupers(
|
||||
callEventGroupers: Map<string, LegacyCallEventGrouper>,
|
||||
events?: MatrixEvent[],
|
||||
): Map<string, CallEventGrouper> {
|
||||
): Map<string, LegacyCallEventGrouper> {
|
||||
const newCallEventGroupers = new Map();
|
||||
events?.forEach(ev => {
|
||||
if (!ev.getType().startsWith("m.call.") && !ev.getType().startsWith("org.matrix.call.")) {
|
||||
|
@ -57,10 +57,10 @@ export function buildCallEventGroupers(
|
|||
const callId = ev.getContent().call_id;
|
||||
if (!newCallEventGroupers.has(callId)) {
|
||||
if (callEventGroupers.has(callId)) {
|
||||
// reuse the CallEventGrouper object where possible
|
||||
// reuse the LegacyCallEventGrouper object where possible
|
||||
newCallEventGroupers.set(callId, callEventGroupers.get(callId));
|
||||
} else {
|
||||
newCallEventGroupers.set(callId, new CallEventGrouper());
|
||||
newCallEventGroupers.set(callId, new LegacyCallEventGrouper());
|
||||
}
|
||||
}
|
||||
newCallEventGroupers.get(callId).add(ev);
|
||||
|
@ -68,7 +68,7 @@ export function buildCallEventGroupers(
|
|||
return newCallEventGroupers;
|
||||
}
|
||||
|
||||
export default class CallEventGrouper extends EventEmitter {
|
||||
export default class LegacyCallEventGrouper extends EventEmitter {
|
||||
private events: Set<MatrixEvent> = new Set<MatrixEvent>();
|
||||
private call: MatrixCall;
|
||||
public state: CallState | CustomCallState;
|
||||
|
@ -76,8 +76,10 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
CallHandler.instance.addListener(CallHandlerEvent.CallsChanged, this.setCall);
|
||||
CallHandler.instance.addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
||||
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallsChanged, this.setCall);
|
||||
LegacyCallHandler.instance.addListener(
|
||||
LegacyCallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged,
|
||||
);
|
||||
}
|
||||
|
||||
private get invite(): MatrixEvent {
|
||||
|
@ -138,31 +140,31 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
}
|
||||
|
||||
private onSilencedCallsChanged = () => {
|
||||
const newState = CallHandler.instance.isCallSilenced(this.callId);
|
||||
this.emit(CallEventGrouperEvent.SilencedChanged, newState);
|
||||
const newState = LegacyCallHandler.instance.isCallSilenced(this.callId);
|
||||
this.emit(LegacyCallEventGrouperEvent.SilencedChanged, newState);
|
||||
};
|
||||
|
||||
private onLengthChanged = (length: number): void => {
|
||||
this.emit(CallEventGrouperEvent.LengthChanged, length);
|
||||
this.emit(LegacyCallEventGrouperEvent.LengthChanged, length);
|
||||
};
|
||||
|
||||
public answerCall = (): void => {
|
||||
CallHandler.instance.answerCall(this.roomId);
|
||||
LegacyCallHandler.instance.answerCall(this.roomId);
|
||||
};
|
||||
|
||||
public rejectCall = (): void => {
|
||||
CallHandler.instance.hangupOrReject(this.roomId, true);
|
||||
LegacyCallHandler.instance.hangupOrReject(this.roomId, true);
|
||||
};
|
||||
|
||||
public callBack = (): void => {
|
||||
CallHandler.instance.placeCall(this.roomId, this.isVoice ? CallType.Voice : CallType.Video);
|
||||
LegacyCallHandler.instance.placeCall(this.roomId, this.isVoice ? CallType.Voice : CallType.Video);
|
||||
};
|
||||
|
||||
public toggleSilenced = () => {
|
||||
const silenced = CallHandler.instance.isCallSilenced(this.callId);
|
||||
const silenced = LegacyCallHandler.instance.isCallSilenced(this.callId);
|
||||
silenced ?
|
||||
CallHandler.instance.unSilenceCall(this.callId) :
|
||||
CallHandler.instance.silenceCall(this.callId);
|
||||
LegacyCallHandler.instance.unSilenceCall(this.callId) :
|
||||
LegacyCallHandler.instance.silenceCall(this.callId);
|
||||
};
|
||||
|
||||
private setCallListeners() {
|
||||
|
@ -182,13 +184,13 @@ export default class CallEventGrouper extends EventEmitter {
|
|||
else if (this.hangup) this.state = CallState.Ended;
|
||||
else if (this.invite && this.call) this.state = CallState.Connecting;
|
||||
}
|
||||
this.emit(CallEventGrouperEvent.StateChanged, this.state);
|
||||
this.emit(LegacyCallEventGrouperEvent.StateChanged, this.state);
|
||||
};
|
||||
|
||||
private setCall = () => {
|
||||
if (this.call) return;
|
||||
|
||||
this.call = CallHandler.instance.getCallById(this.callId);
|
||||
this.call = LegacyCallHandler.instance.getCallById(this.callId);
|
||||
this.setCallListeners();
|
||||
this.setState();
|
||||
};
|
|
@ -51,8 +51,8 @@ import HostSignupContainer from '../views/host_signup/HostSignupContainer';
|
|||
import { getKeyBindingsManager } from '../../KeyBindingsManager';
|
||||
import { IOpts } from "../../createRoom";
|
||||
import SpacePanel from "../views/spaces/SpacePanel";
|
||||
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
|
||||
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
|
||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../LegacyCallHandler';
|
||||
import AudioFeedArrayForLegacyCall from '../views/voip/AudioFeedArrayForLegacyCall';
|
||||
import { OwnProfileStore } from '../../stores/OwnProfileStore';
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import RoomView from './RoomView';
|
||||
|
@ -146,7 +146,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
// use compact timeline view
|
||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||
usageLimitDismissed: false,
|
||||
activeCalls: CallHandler.instance.getAllActiveCalls(),
|
||||
activeCalls: LegacyCallHandler.instance.getAllActiveCalls(),
|
||||
};
|
||||
|
||||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
|
@ -163,7 +163,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.onNativeKeyDown, false);
|
||||
CallHandler.instance.addListener(CallHandlerEvent.CallState, this.onCallState);
|
||||
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.onCallState);
|
||||
|
||||
this.updateServerNoticeEvents();
|
||||
|
||||
|
@ -195,7 +195,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.onNativeKeyDown, false);
|
||||
CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.onCallState);
|
||||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState);
|
||||
this._matrixClient.removeListener(ClientEvent.AccountData, this.onAccountData);
|
||||
this._matrixClient.removeListener(ClientEvent.Sync, this.onSync);
|
||||
this._matrixClient.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
|
@ -207,7 +207,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private onCallState = (): void => {
|
||||
const activeCalls = CallHandler.instance.getAllActiveCalls();
|
||||
const activeCalls = LegacyCallHandler.instance.getAllActiveCalls();
|
||||
if (activeCalls === this.state.activeCalls) return;
|
||||
this.setState({ activeCalls });
|
||||
};
|
||||
|
@ -658,7 +658,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
const audioFeedArraysForCalls = this.state.activeCalls.map((call) => {
|
||||
return (
|
||||
<AudioFeedArrayForCall call={call} key={call.callId} />
|
||||
<AudioFeedArrayForLegacyCall call={call} key={call.callId} />
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@ import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
|||
import { copyPlaintext } from "../../utils/strings";
|
||||
import { PosthogAnalytics } from '../../PosthogAnalytics';
|
||||
import { initSentry } from "../../sentry";
|
||||
import CallHandler from "../../CallHandler";
|
||||
import LegacyCallHandler from "../../LegacyCallHandler";
|
||||
import { showSpaceInvite } from "../../utils/space";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
|
@ -128,7 +128,7 @@ import { ViewStartChatOrReusePayload } from '../../dispatcher/payloads/ViewStart
|
|||
import { IConfigOptions } from "../../IConfigOptions";
|
||||
import { SnakedObject } from "../../utils/SnakedObject";
|
||||
import { leaveRoomBehaviour } from "../../utils/leave-behaviour";
|
||||
import VideoChannelStore from "../../stores/VideoChannelStore";
|
||||
import { CallStore } from "../../stores/CallStore";
|
||||
import { IRoomStateEventsActionPayload } from "../../actions/MatrixActionCreators";
|
||||
import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";
|
||||
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
|
||||
|
@ -576,9 +576,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
break;
|
||||
case 'logout':
|
||||
CallHandler.instance.hangupAllCalls();
|
||||
if (VideoChannelStore.instance.connected) VideoChannelStore.instance.setDisconnected();
|
||||
Lifecycle.logout();
|
||||
LegacyCallHandler.instance.hangupAllCalls();
|
||||
Promise.all([...CallStore.instance.activeCalls].map(call => call.disconnect()))
|
||||
.finally(() => Lifecycle.logout());
|
||||
break;
|
||||
case 'require_registration':
|
||||
startAnyRegistrationFlow(payload as any);
|
||||
|
|
|
@ -40,7 +40,7 @@ import DMRoomMap from "../../utils/DMRoomMap";
|
|||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||
import HistoryTile from "../views/rooms/HistoryTile";
|
||||
import defaultDispatcher from '../../dispatcher/dispatcher';
|
||||
import CallEventGrouper from "./CallEventGrouper";
|
||||
import LegacyCallEventGrouper from "./LegacyCallEventGrouper";
|
||||
import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
|
||||
import ScrollPanel, { IScrollState } from "./ScrollPanel";
|
||||
import GenericEventListSummary from '../views/elements/GenericEventListSummary';
|
||||
|
@ -188,7 +188,7 @@ interface IProps {
|
|||
hideThreadedMessages?: boolean;
|
||||
disableGrouping?: boolean;
|
||||
|
||||
callEventGroupers: Map<string, CallEventGrouper>;
|
||||
callEventGroupers: Map<string, LegacyCallEventGrouper>;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
|
|
@ -45,7 +45,7 @@ import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
|||
import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||
import ContentMessages from '../../ContentMessages';
|
||||
import Modal from '../../Modal';
|
||||
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
|
||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../LegacyCallHandler';
|
||||
import dis, { defaultDispatcher } from '../../dispatcher/dispatcher';
|
||||
import * as Rooms from '../../Rooms';
|
||||
import eventSearch, { searchPagination } from '../../Searching';
|
||||
|
@ -78,7 +78,7 @@ import EffectsOverlay from "../views/elements/EffectsOverlay";
|
|||
import { containsEmoji } from '../../effects/utils';
|
||||
import { CHAT_EFFECTS } from '../../effects';
|
||||
import WidgetStore from "../../stores/WidgetStore";
|
||||
import VideoRoomView from "./VideoRoomView";
|
||||
import { VideoRoomView } from "./VideoRoomView";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
|
||||
|
@ -810,7 +810,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
callState: callState,
|
||||
});
|
||||
|
||||
CallHandler.instance.on(CallHandlerEvent.CallState, this.onCallState);
|
||||
LegacyCallHandler.instance.on(LegacyCallHandlerEvent.CallState, this.onCallState);
|
||||
window.addEventListener('beforeunload', this.onPageUnload);
|
||||
}
|
||||
|
||||
|
@ -847,7 +847,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
// (We could use isMounted, but facebook have deprecated that.)
|
||||
this.unmounted = true;
|
||||
|
||||
CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.onCallState);
|
||||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState);
|
||||
|
||||
// update the scroll map before we get unmounted
|
||||
if (this.state.roomId) {
|
||||
|
@ -896,7 +896,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
);
|
||||
}
|
||||
|
||||
CallHandler.instance.off(CallHandlerEvent.CallState, this.onCallState);
|
||||
LegacyCallHandler.instance.off(LegacyCallHandlerEvent.CallState, this.onCallState);
|
||||
|
||||
// cancel any pending calls to the throttled updated
|
||||
this.updateRoomMembers.cancel();
|
||||
|
@ -1655,7 +1655,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
|
||||
private onCallPlaced = (type: CallType): void => {
|
||||
CallHandler.instance.placeCall(this.state.room?.roomId, type);
|
||||
LegacyCallHandler.instance.placeCall(this.state.room?.roomId, type);
|
||||
};
|
||||
|
||||
private onAppsClick = () => {
|
||||
|
@ -1872,7 +1872,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
if (!this.state.room) {
|
||||
return null;
|
||||
}
|
||||
return CallHandler.instance.getCallForRoom(this.state.room.roomId);
|
||||
return LegacyCallHandler.instance.getCallForRoom(this.state.room.roomId);
|
||||
}
|
||||
|
||||
// this has to be a proper method rather than an unnamed function,
|
||||
|
|
|
@ -52,7 +52,7 @@ import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
|||
import Spinner from "../views/elements/Spinner";
|
||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||
import ErrorDialog from '../views/dialogs/ErrorDialog';
|
||||
import CallEventGrouper, { buildCallEventGroupers } from "./CallEventGrouper";
|
||||
import LegacyCallEventGrouper, { buildLegacyCallEventGroupers } from "./LegacyCallEventGrouper";
|
||||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||
|
@ -240,8 +240,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
private readReceiptActivityTimer: Timer;
|
||||
private readMarkerActivityTimer: Timer;
|
||||
|
||||
// A map of <callId, CallEventGrouper>
|
||||
private callEventGroupers = new Map<string, CallEventGrouper>();
|
||||
// A map of <callId, LegacyCallEventGrouper>
|
||||
private callEventGroupers = new Map<string, LegacyCallEventGrouper>();
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
@ -493,7 +493,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
this.timelineWindow.unpaginate(count, backwards);
|
||||
|
||||
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
|
||||
this.buildCallEventGroupers(events);
|
||||
this.buildLegacyCallEventGroupers(events);
|
||||
const newState: Partial<IState> = {
|
||||
events,
|
||||
liveEvents,
|
||||
|
@ -555,7 +555,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
debuglog("paginate complete backwards:"+backwards+"; success:"+r);
|
||||
|
||||
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
|
||||
this.buildCallEventGroupers(events);
|
||||
this.buildLegacyCallEventGroupers(events);
|
||||
const newState: Partial<IState> = {
|
||||
[paginatingKey]: false,
|
||||
[canPaginateKey]: r,
|
||||
|
@ -686,7 +686,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
if (this.unmounted) { return; }
|
||||
|
||||
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
|
||||
this.buildCallEventGroupers(events);
|
||||
this.buildLegacyCallEventGroupers(events);
|
||||
const lastLiveEvent = liveEvents[liveEvents.length - 1];
|
||||
|
||||
const updatedState: Partial<IState> = {
|
||||
|
@ -855,7 +855,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
// TODO: We should restrict this to only events in our timeline,
|
||||
// but possibly the event tile itself should just update when this
|
||||
// happens to save us re-rendering the whole timeline.
|
||||
this.buildCallEventGroupers(this.state.events);
|
||||
this.buildLegacyCallEventGroupers(this.state.events);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
|
@ -1405,7 +1405,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
onLoaded();
|
||||
} else {
|
||||
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE);
|
||||
this.buildCallEventGroupers();
|
||||
this.buildLegacyCallEventGroupers();
|
||||
this.setState({
|
||||
events: [],
|
||||
liveEvents: [],
|
||||
|
@ -1426,7 +1426,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
if (this.unmounted) return;
|
||||
|
||||
const state = this.getEvents();
|
||||
this.buildCallEventGroupers(state.events);
|
||||
this.buildLegacyCallEventGroupers(state.events);
|
||||
this.setState(state);
|
||||
}
|
||||
|
||||
|
@ -1707,8 +1707,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
eventType: EventType | string,
|
||||
) => this.props.timelineSet.relations?.getChildEventsForEvent(eventId, relationType, eventType);
|
||||
|
||||
private buildCallEventGroupers(events?: MatrixEvent[]): void {
|
||||
this.callEventGroupers = buildCallEventGroupers(this.callEventGroupers, events);
|
||||
private buildLegacyCallEventGroupers(events?: MatrixEvent[]): void {
|
||||
this.callEventGroupers = buildLegacyCallEventGroupers(this.callEventGroupers, events);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -14,79 +14,46 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC, useContext, useState, useMemo, useEffect } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import React, { FC, useContext, useEffect } from "react";
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import type { Call } from "../../models/Call";
|
||||
import { useCall, useConnectionState } from "../../hooks/useCall";
|
||||
import { isConnected } from "../../models/Call";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { useEventEmitter } from "../../hooks/useEventEmitter";
|
||||
import WidgetUtils from "../../utils/WidgetUtils";
|
||||
import { addVideoChannel, getVideoChannel, fixStuckDevices } from "../../utils/VideoChannelUtils";
|
||||
import WidgetStore, { IApp } from "../../stores/WidgetStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import VideoChannelStore, { VideoChannelEvent } from "../../stores/VideoChannelStore";
|
||||
import AppTile from "../views/elements/AppTile";
|
||||
import VideoLobby from "../views/voip/VideoLobby";
|
||||
import { CallLobby } from "../views/voip/CallLobby";
|
||||
|
||||
interface IProps {
|
||||
interface Props {
|
||||
room: Room;
|
||||
resizing: boolean;
|
||||
}
|
||||
|
||||
const VideoRoomView: FC<IProps> = ({ room, resizing }) => {
|
||||
const LoadedVideoRoomView: FC<Props & { call: Call }> = ({ room, resizing, call }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const store = VideoChannelStore.instance;
|
||||
const connected = isConnected(useConnectionState(call));
|
||||
|
||||
// In case we mount before the WidgetStore knows about our Jitsi widget
|
||||
const [widgetStoreReady, setWidgetStoreReady] = useState(Boolean(WidgetStore.instance.matrixClient));
|
||||
const [widgetLoaded, setWidgetLoaded] = useState(false);
|
||||
useEventEmitter(WidgetStore.instance, UPDATE_EVENT, (roomId: string) => {
|
||||
if (roomId === null) setWidgetStoreReady(true);
|
||||
if (roomId === null || roomId === room.roomId) {
|
||||
setWidgetLoaded(Boolean(getVideoChannel(room.roomId)));
|
||||
}
|
||||
});
|
||||
// We'll take this opportunity to tidy up our room state
|
||||
useEffect(() => { call?.clean(); }, [call]);
|
||||
|
||||
const app: IApp = useMemo(() => {
|
||||
if (widgetStoreReady) {
|
||||
const app = getVideoChannel(room.roomId);
|
||||
if (!app) {
|
||||
logger.warn(`No video channel for room ${room.roomId}`);
|
||||
// Since widgets in video rooms are mutable, we'll take this opportunity to
|
||||
// reinstate the Jitsi widget in case another client removed it
|
||||
if (WidgetUtils.canUserModifyWidgets(room.roomId)) {
|
||||
addVideoChannel(room.roomId, room.name);
|
||||
}
|
||||
}
|
||||
return app;
|
||||
}
|
||||
}, [room, widgetStoreReady, widgetLoaded]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// We'll also take this opportunity to fix any stuck devices.
|
||||
// The linter thinks that store.connected should be a dependency, but we explicitly
|
||||
// *only* want this to happen at mount to avoid racing with normal device updates.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => { fixStuckDevices(room, store.connected); }, [room]);
|
||||
|
||||
const [connected, setConnected] = useState(store.connected && store.roomId === room.roomId);
|
||||
useEventEmitter(store, VideoChannelEvent.Connect, () => setConnected(store.roomId === room.roomId));
|
||||
useEventEmitter(store, VideoChannelEvent.Disconnect, () => setConnected(false));
|
||||
|
||||
if (!app) return null;
|
||||
if (!call) return null;
|
||||
|
||||
return <div className="mx_VideoRoomView">
|
||||
{ connected ? null : <VideoLobby room={room} /> }
|
||||
{ connected ? null : <CallLobby room={room} call={call} /> }
|
||||
{ /* We render the widget even if we're disconnected, so it stays loaded */ }
|
||||
<AppTile
|
||||
app={app}
|
||||
app={call.widget}
|
||||
room={room}
|
||||
userId={cli.credentials.userId}
|
||||
creatorUserId={app.creatorUserId}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
creatorUserId={call.widget.creatorUserId}
|
||||
waitForIframeLoad={call.widget.waitForIframeLoad}
|
||||
showMenubar={false}
|
||||
pointerEvents={resizing ? "none" : null}
|
||||
pointerEvents={resizing ? "none" : undefined}
|
||||
/>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default VideoRoomView;
|
||||
export const VideoRoomView: FC<Props> = ({ room, resizing }) => {
|
||||
const call = useCall(room.roomId);
|
||||
return call ? <LoadedVideoRoomView room={room} resizing={resizing} call={call} /> : null;
|
||||
};
|
||||
|
|
|
@ -20,13 +20,13 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
|||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import ContextMenu, { IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import LegacyCallHandler from '../../../LegacyCallHandler';
|
||||
|
||||
interface IProps extends IContextMenuProps {
|
||||
call: MatrixCall;
|
||||
}
|
||||
|
||||
export default class CallContextMenu extends React.Component<IProps> {
|
||||
export default class LegacyCallContextMenu extends React.Component<IProps> {
|
||||
static propTypes = {
|
||||
// js-sdk User object. Not required because it might not exist.
|
||||
user: PropTypes.object,
|
||||
|
@ -42,13 +42,13 @@ export default class CallContextMenu extends React.Component<IProps> {
|
|||
};
|
||||
|
||||
onUnholdClick = () => {
|
||||
CallHandler.instance.setActiveCallRoomId(this.props.call.roomId);
|
||||
LegacyCallHandler.instance.setActiveCallRoomId(this.props.call.roomId);
|
||||
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
onTransferClick = () => {
|
||||
CallHandler.instance.showTransferDialog(this.props.call);
|
||||
LegacyCallHandler.instance.showTransferDialog(this.props.call);
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
|
@ -58,13 +58,13 @@ export default class CallContextMenu extends React.Component<IProps> {
|
|||
|
||||
let transferItem;
|
||||
if (this.props.call.opponentCanBeTransferred()) {
|
||||
transferItem = <MenuItem className="mx_CallContextMenu_item" onClick={this.onTransferClick}>
|
||||
transferItem = <MenuItem className="mx_LegacyCallContextMenu_item" onClick={this.onTransferClick}>
|
||||
{ _t("Transfer") }
|
||||
</MenuItem>;
|
||||
}
|
||||
|
||||
return <ContextMenu {...this.props}>
|
||||
<MenuItem className="mx_CallContextMenu_item" onClick={handler}>
|
||||
<MenuItem className="mx_LegacyCallContextMenu_item" onClick={handler}>
|
||||
{ holdUnholdCaption }
|
||||
</MenuItem>
|
||||
{ transferItem }
|
|
@ -56,7 +56,7 @@ import QuestionDialog from "./QuestionDialog";
|
|||
import Spinner from "../elements/Spinner";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
|
||||
import CopyableText from "../elements/CopyableText";
|
||||
import { ScreenName } from '../../../PosthogTrackers';
|
||||
|
@ -510,13 +510,13 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
return;
|
||||
}
|
||||
|
||||
CallHandler.instance.startTransferToMatrixID(
|
||||
LegacyCallHandler.instance.startTransferToMatrixID(
|
||||
this.props.call,
|
||||
targetIds[0],
|
||||
this.state.consultFirst,
|
||||
);
|
||||
} else {
|
||||
CallHandler.instance.startTransferToPhoneNumber(
|
||||
LegacyCallHandler.instance.startTransferToPhoneNumber(
|
||||
this.props.call,
|
||||
this.state.dialPadValue,
|
||||
this.state.consultFirst,
|
||||
|
|
|
@ -36,10 +36,9 @@ import { aboveLeftOf, ContextMenuButton } from "../../structures/ContextMenu";
|
|||
import PersistedElement, { getPersistKey } from "./PersistedElement";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import { ElementWidget, StopGapWidget } from "../../../stores/widgets/StopGapWidget";
|
||||
import { ElementWidgetActions } from "../../../stores/widgets/ElementWidgetActions";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import LegacyCallHandler from '../../../LegacyCallHandler';
|
||||
import { IApp } from "../../../stores/WidgetStore";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { OwnProfileStore } from '../../../stores/OwnProfileStore';
|
||||
|
@ -305,7 +304,6 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
|
||||
private setupSgListeners() {
|
||||
this.sgWidget.on("preparing", this.onWidgetPreparing);
|
||||
this.sgWidget.on("ready", this.onWidgetReady);
|
||||
// emits when the capabilities have been set up or changed
|
||||
this.sgWidget.on("capabilitiesNotified", this.onWidgetCapabilitiesNotified);
|
||||
}
|
||||
|
@ -313,7 +311,6 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
private stopSgListeners() {
|
||||
if (!this.sgWidget) return;
|
||||
this.sgWidget.off("preparing", this.onWidgetPreparing);
|
||||
this.sgWidget.off("ready", this.onWidgetReady);
|
||||
this.sgWidget.off("capabilitiesNotified", this.onWidgetCapabilitiesNotified);
|
||||
}
|
||||
|
||||
|
@ -393,7 +390,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
if (WidgetType.JITSI.matches(this.props.app.type) && this.props.room) {
|
||||
CallHandler.instance.hangupCallApp(this.props.room.roomId);
|
||||
LegacyCallHandler.instance.hangupCallApp(this.props.room.roomId);
|
||||
}
|
||||
|
||||
// Delete the widget from the persisted store for good measure.
|
||||
|
@ -407,12 +404,6 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
this.setState({ loading: false });
|
||||
};
|
||||
|
||||
private onWidgetReady = (): void => {
|
||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||
this.sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
|
||||
}
|
||||
};
|
||||
|
||||
private onWidgetCapabilitiesNotified = (): void => {
|
||||
this.setState({
|
||||
requiresClient: this.sgWidget.widgetApi.hasCapability(ElementWidgetCapabilities.RequiresClient),
|
||||
|
|
|
@ -181,7 +181,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
|||
style.display = this.props.visible ? "block" : "none";
|
||||
|
||||
const tooltip = (
|
||||
<div className={tooltipClasses} style={style}>
|
||||
<div role="tooltip" className={tooltipClasses} style={style}>
|
||||
<div className="mx_Tooltip_chevron" />
|
||||
{ this.props.label }
|
||||
</div>
|
||||
|
|
|
@ -21,7 +21,10 @@ import classNames from 'classnames';
|
|||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import CallEventGrouper, { CallEventGrouperEvent, CustomCallState } from '../../structures/CallEventGrouper';
|
||||
import LegacyCallEventGrouper, {
|
||||
LegacyCallEventGrouperEvent,
|
||||
CustomCallState,
|
||||
} from '../../structures/LegacyCallEventGrouper';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
|
||||
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||
|
@ -32,7 +35,7 @@ const MAX_NON_NARROW_WIDTH = 450 / 70 * 100;
|
|||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
callEventGrouper: CallEventGrouper;
|
||||
callEventGrouper: LegacyCallEventGrouper;
|
||||
timestamp?: JSX.Element;
|
||||
}
|
||||
|
||||
|
@ -43,7 +46,7 @@ interface IState {
|
|||
length: number;
|
||||
}
|
||||
|
||||
export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||
export default class LegacyCallEvent extends React.PureComponent<IProps, IState> {
|
||||
private wrapperElement = createRef<HTMLDivElement>();
|
||||
private resizeObserver: ResizeObserver;
|
||||
|
||||
|
@ -59,18 +62,18 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.callEventGrouper.addListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
|
||||
this.props.callEventGrouper.addListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
||||
this.props.callEventGrouper.addListener(CallEventGrouperEvent.LengthChanged, this.onLengthChanged);
|
||||
this.props.callEventGrouper.addListener(LegacyCallEventGrouperEvent.StateChanged, this.onStateChanged);
|
||||
this.props.callEventGrouper.addListener(LegacyCallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
||||
this.props.callEventGrouper.addListener(LegacyCallEventGrouperEvent.LengthChanged, this.onLengthChanged);
|
||||
|
||||
this.resizeObserver = new ResizeObserver(this.resizeObserverCallback);
|
||||
this.wrapperElement.current && this.resizeObserver.observe(this.wrapperElement.current);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
|
||||
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
||||
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.LengthChanged, this.onLengthChanged);
|
||||
this.props.callEventGrouper.removeListener(LegacyCallEventGrouperEvent.StateChanged, this.onStateChanged);
|
||||
this.props.callEventGrouper.removeListener(LegacyCallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
||||
this.props.callEventGrouper.removeListener(LegacyCallEventGrouperEvent.LengthChanged, this.onLengthChanged);
|
||||
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
|
@ -97,7 +100,7 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
|||
private renderCallBackButton(text: string): JSX.Element {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_CallEvent_content_button mx_CallEvent_content_button_callBack"
|
||||
className="mx_LegacyCallEvent_content_button mx_LegacyCallEvent_content_button_callBack"
|
||||
onClick={this.props.callEventGrouper.callBack}
|
||||
kind="primary"
|
||||
>
|
||||
|
@ -108,9 +111,9 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private renderSilenceIcon(): JSX.Element {
|
||||
const silenceClass = classNames({
|
||||
"mx_CallEvent_iconButton": true,
|
||||
"mx_CallEvent_unSilence": this.state.silenced,
|
||||
"mx_CallEvent_silence": !this.state.silenced,
|
||||
"mx_LegacyCallEvent_iconButton": true,
|
||||
"mx_LegacyCallEvent_unSilence": this.state.silenced,
|
||||
"mx_LegacyCallEvent_silence": !this.state.silenced,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -130,17 +133,17 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
<div className="mx_LegacyCallEvent_content">
|
||||
{ silenceIcon }
|
||||
<AccessibleButton
|
||||
className="mx_CallEvent_content_button mx_CallEvent_content_button_reject"
|
||||
className="mx_LegacyCallEvent_content_button mx_LegacyCallEvent_content_button_reject"
|
||||
onClick={this.props.callEventGrouper.rejectCall}
|
||||
kind="danger"
|
||||
>
|
||||
<span> { _t("Decline") } </span>
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className="mx_CallEvent_content_button mx_CallEvent_content_button_answer"
|
||||
className="mx_LegacyCallEvent_content_button mx_LegacyCallEvent_content_button_answer"
|
||||
onClick={this.props.callEventGrouper.answerCall}
|
||||
kind="primary"
|
||||
>
|
||||
|
@ -156,7 +159,7 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
|||
|
||||
if (gotRejected) {
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
<div className="mx_LegacyCallEvent_content">
|
||||
{ _t("Call declined") }
|
||||
{ this.renderCallBackButton(_t("Call back")) }
|
||||
{ this.props.timestamp }
|
||||
|
@ -175,14 +178,14 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
|||
text += " • " + formatCallTime(duration);
|
||||
}
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
<div className="mx_LegacyCallEvent_content">
|
||||
{ text }
|
||||
{ this.props.timestamp }
|
||||
</div>
|
||||
);
|
||||
} else if (hangupReason === CallErrorCode.InviteTimeout) {
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
<div className="mx_LegacyCallEvent_content">
|
||||
{ _t("No answer") }
|
||||
{ this.renderCallBackButton(_t("Call back")) }
|
||||
{ this.props.timestamp }
|
||||
|
@ -212,10 +215,10 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
<div className="mx_LegacyCallEvent_content">
|
||||
<InfoTooltip
|
||||
tooltip={reason}
|
||||
className="mx_CallEvent_content_tooltip"
|
||||
className="mx_LegacyCallEvent_content_tooltip"
|
||||
kind={InfoTooltipKind.Warning}
|
||||
/>
|
||||
{ _t("Connection failed") }
|
||||
|
@ -226,7 +229,7 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
if (state === CallState.Connected) {
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
<div className="mx_LegacyCallEvent_content">
|
||||
<Clock seconds={this.state.length} aria-live="off" />
|
||||
{ this.props.timestamp }
|
||||
</div>
|
||||
|
@ -234,7 +237,7 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
if (state === CallState.Connecting) {
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
<div className="mx_LegacyCallEvent_content">
|
||||
{ _t("Connecting") }
|
||||
{ this.props.timestamp }
|
||||
</div>
|
||||
|
@ -242,7 +245,7 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
if (state === CustomCallState.Missed) {
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
<div className="mx_LegacyCallEvent_content">
|
||||
{ _t("Missed call") }
|
||||
{ this.renderCallBackButton(_t("Call back")) }
|
||||
{ this.props.timestamp }
|
||||
|
@ -251,7 +254,7 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_CallEvent_content">
|
||||
<div className="mx_LegacyCallEvent_content">
|
||||
{ _t("The call is in an unknown state!") }
|
||||
{ this.props.timestamp }
|
||||
</div>
|
||||
|
@ -266,13 +269,13 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
|||
const callState = this.state.callState;
|
||||
const hangupReason = this.props.callEventGrouper.hangupReason;
|
||||
const content = this.renderContent(callState);
|
||||
const className = classNames("mx_CallEvent", {
|
||||
mx_CallEvent_voice: isVoice,
|
||||
mx_CallEvent_video: !isVoice,
|
||||
mx_CallEvent_narrow: this.state.narrow,
|
||||
mx_CallEvent_missed: callState === CustomCallState.Missed,
|
||||
mx_CallEvent_noAnswer: callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout,
|
||||
mx_CallEvent_rejected: callState === CallState.Ended && this.props.callEventGrouper.gotRejected,
|
||||
const className = classNames("mx_LegacyCallEvent", {
|
||||
mx_LegacyCallEvent_voice: isVoice,
|
||||
mx_LegacyCallEvent_video: !isVoice,
|
||||
mx_LegacyCallEvent_narrow: this.state.narrow,
|
||||
mx_LegacyCallEvent_missed: callState === CustomCallState.Missed,
|
||||
mx_LegacyCallEvent_noAnswer: callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout,
|
||||
mx_LegacyCallEvent_rejected: callState === CallState.Ended && this.props.callEventGrouper.gotRejected,
|
||||
});
|
||||
let silenceIcon;
|
||||
if (this.state.narrow && this.state.callState === CallState.Ringing) {
|
||||
|
@ -280,21 +283,21 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_CallEvent_wrapper" ref={this.wrapperElement}>
|
||||
<div className="mx_LegacyCallEvent_wrapper" ref={this.wrapperElement}>
|
||||
<div className={className}>
|
||||
{ silenceIcon }
|
||||
<div className="mx_CallEvent_info">
|
||||
<div className="mx_LegacyCallEvent_info">
|
||||
<MemberAvatar
|
||||
member={event.sender}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<div className="mx_CallEvent_info_basic">
|
||||
<div className="mx_CallEvent_sender">
|
||||
<div className="mx_LegacyCallEvent_info_basic">
|
||||
<div className="mx_LegacyCallEvent_sender">
|
||||
{ sender }
|
||||
</div>
|
||||
<div className="mx_CallEvent_type">
|
||||
<div className="mx_CallEvent_type_icon" />
|
||||
<div className="mx_LegacyCallEvent_type">
|
||||
<div className="mx_LegacyCallEvent_type_icon" />
|
||||
{ callType }
|
||||
</div>
|
||||
</div>
|
|
@ -27,7 +27,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import CallViewForRoom from '../voip/CallViewForRoom';
|
||||
import LegacyCallViewForRoom from '../voip/LegacyCallViewForRoom';
|
||||
import { objectHasDiff } from "../../../utils/objects";
|
||||
|
||||
interface IProps {
|
||||
|
@ -123,7 +123,7 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
|
||||
render() {
|
||||
const callView = (
|
||||
<CallViewForRoom
|
||||
<LegacyCallViewForRoom
|
||||
roomId={this.props.room.roomId}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showApps={this.props.showApps}
|
||||
|
|
|
@ -47,7 +47,7 @@ import EditorStateTransfer from "../../../utils/EditorStateTransfer";
|
|||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import CallEventGrouper from "../../structures/CallEventGrouper";
|
||||
import LegacyCallEventGrouper from "../../structures/LegacyCallEventGrouper";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
|
@ -200,8 +200,8 @@ interface IProps {
|
|||
// Helper to build permalinks for the room
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
|
||||
// CallEventGrouper for this event
|
||||
callEventGrouper?: CallEventGrouper;
|
||||
// LegacyCallEventGrouper for this event
|
||||
callEventGrouper?: LegacyCallEventGrouper;
|
||||
|
||||
// Symbol of the root node
|
||||
as?: string;
|
||||
|
|
|
@ -19,11 +19,11 @@ import React, { createRef } from "react";
|
|||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from "classnames";
|
||||
|
||||
import type { Call } from "../../../models/Call";
|
||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||
import defaultDispatcher from '../../../dispatcher/dispatcher';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
|
@ -45,8 +45,9 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
|||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { RoomViewStore } from "../../../stores/RoomViewStore";
|
||||
import VideoRoomSummary from "./VideoRoomSummary";
|
||||
import { RoomTileCallSummary } from "./RoomTileCallSummary";
|
||||
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
|
||||
import { CallStore, CallStoreEvent } from "../../../stores/CallStore";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -61,6 +62,7 @@ interface IState {
|
|||
selected: boolean;
|
||||
notificationsMenuPosition: PartialDOMRect;
|
||||
generalMenuPosition: PartialDOMRect;
|
||||
call: Call | null;
|
||||
messagePreview?: string;
|
||||
}
|
||||
|
||||
|
@ -79,7 +81,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
private roomTileRef = createRef<HTMLDivElement>();
|
||||
private notificationState: NotificationState;
|
||||
private roomProps: RoomEchoChamber;
|
||||
private isVideoRoom: boolean;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -88,6 +89,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
selected: RoomViewStore.instance.getRoomId() === this.props.room.roomId,
|
||||
notificationsMenuPosition: null,
|
||||
generalMenuPosition: null,
|
||||
call: CallStore.instance.get(this.props.room.roomId),
|
||||
// generatePreview() will return nothing if the user has previews disabled
|
||||
messagePreview: "",
|
||||
};
|
||||
|
@ -95,7 +97,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
|
||||
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||
this.isVideoRoom = SettingsStore.getValue("feature_video_rooms") && this.props.room.isElementVideoRoom();
|
||||
}
|
||||
|
||||
private onRoomNameUpdate = (room: Room) => {
|
||||
|
@ -154,6 +155,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.notificationState.on(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||
this.props.room.on(RoomEvent.Name, this.onRoomNameUpdate);
|
||||
CallStore.instance.on(CallStoreEvent.Call, this.onCallChanged);
|
||||
|
||||
// Recalculate the call for this room, since it could've changed between
|
||||
// construction and mounting
|
||||
this.setState({ call: CallStore.instance.get(this.props.room.roomId) });
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -166,6 +172,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||
CallStore.instance.off(CallStoreEvent.Call, this.onCallChanged);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
|
@ -185,6 +192,10 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onCallChanged = (call: Call, roomId: string) => {
|
||||
if (roomId === this.props.room?.roomId) this.setState({ call });
|
||||
};
|
||||
|
||||
private async generatePreview() {
|
||||
if (!this.showMessagePreview) {
|
||||
return null;
|
||||
|
@ -362,10 +373,10 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
let subtitle;
|
||||
if (this.isVideoRoom) {
|
||||
if (this.state.call) {
|
||||
subtitle = (
|
||||
<div className="mx_RoomTile_subtitle">
|
||||
<VideoRoomSummary room={this.props.room} />
|
||||
<RoomTileCallSummary call={this.state.call} />
|
||||
</div>
|
||||
);
|
||||
} else if (this.showMessagePreview && this.state.messagePreview) {
|
||||
|
|
|
@ -16,66 +16,56 @@ limitations under the License.
|
|||
|
||||
import React, { FC } from "react";
|
||||
import classNames from "classnames";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import type { Call } from "../../../models/Call";
|
||||
import { _t, TranslatedString } from "../../../languageHandler";
|
||||
import {
|
||||
ConnectionState,
|
||||
useConnectionState,
|
||||
useConnectedMembers,
|
||||
useJitsiParticipants,
|
||||
} from "../../../utils/VideoChannelUtils";
|
||||
import { useConnectionState, useParticipants } from "../../../hooks/useCall";
|
||||
import { ConnectionState } from "../../../models/Call";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
interface Props {
|
||||
call: Call;
|
||||
}
|
||||
|
||||
const VideoRoomSummary: FC<IProps> = ({ room }) => {
|
||||
const connectionState = useConnectionState(room);
|
||||
const videoMembers = useConnectedMembers(room, connectionState === ConnectionState.Connected);
|
||||
const jitsiParticipants = useJitsiParticipants(room);
|
||||
export const RoomTileCallSummary: FC<Props> = ({ call }) => {
|
||||
const connectionState = useConnectionState(call);
|
||||
const participants = useParticipants(call);
|
||||
|
||||
let indicator: TranslatedString;
|
||||
let text: TranslatedString;
|
||||
let active: boolean;
|
||||
let participantCount: number;
|
||||
|
||||
switch (connectionState) {
|
||||
case ConnectionState.Disconnected:
|
||||
indicator = _t("Video");
|
||||
text = _t("Video");
|
||||
active = false;
|
||||
participantCount = videoMembers.size;
|
||||
break;
|
||||
case ConnectionState.Connecting:
|
||||
indicator = _t("Joining…");
|
||||
text = _t("Joining…");
|
||||
active = true;
|
||||
participantCount = videoMembers.size;
|
||||
break;
|
||||
case ConnectionState.Connected:
|
||||
indicator = _t("Joined");
|
||||
case ConnectionState.Disconnecting:
|
||||
text = _t("Joined");
|
||||
active = true;
|
||||
participantCount = jitsiParticipants.length;
|
||||
break;
|
||||
}
|
||||
|
||||
return <span className="mx_VideoRoomSummary">
|
||||
return <span className="mx_RoomTileCallSummary">
|
||||
<span
|
||||
className={classNames(
|
||||
"mx_VideoRoomSummary_indicator",
|
||||
{ "mx_VideoRoomSummary_indicator_active": active },
|
||||
"mx_RoomTileCallSummary_text",
|
||||
{ "mx_RoomTileCallSummary_text_active": active },
|
||||
)}
|
||||
>
|
||||
{ indicator }
|
||||
{ text }
|
||||
</span>
|
||||
{ participantCount ? <>
|
||||
{ participants.size ? <>
|
||||
{ " · " }
|
||||
<span
|
||||
className="mx_VideoRoomSummary_participants"
|
||||
aria-label={_t("%(count)s participants", { count: participantCount })}
|
||||
className="mx_RoomTileCallSummary_participants"
|
||||
aria-label={_t("%(count)s participants", { count: participants.size })}
|
||||
>
|
||||
{ participantCount }
|
||||
{ participants.size }
|
||||
</span>
|
||||
</> : null }
|
||||
</span>;
|
||||
};
|
||||
|
||||
export default VideoRoomSummary;
|
|
@ -26,7 +26,7 @@ import DateSeparator from "../messages/DateSeparator";
|
|||
import EventTile from "./EventTile";
|
||||
import { shouldFormContinuation } from "../../structures/MessagePanel";
|
||||
import { wantsDateSeparator } from "../../../DateUtils";
|
||||
import CallEventGrouper, { buildCallEventGroupers } from "../../structures/CallEventGrouper";
|
||||
import LegacyCallEventGrouper, { buildLegacyCallEventGroupers } from "../../structures/LegacyCallEventGrouper";
|
||||
import { haveRendererForEvent } from "../../../events/EventTileFactory";
|
||||
|
||||
interface IProps {
|
||||
|
@ -44,17 +44,17 @@ export default class SearchResultTile extends React.Component<IProps> {
|
|||
static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
|
||||
// A map of <callId, CallEventGrouper>
|
||||
private callEventGroupers = new Map<string, CallEventGrouper>();
|
||||
// A map of <callId, LegacyCallEventGrouper>
|
||||
private callEventGroupers = new Map<string, LegacyCallEventGrouper>();
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.buildCallEventGroupers(this.props.searchResult.context.getTimeline());
|
||||
this.buildLegacyCallEventGroupers(this.props.searchResult.context.getTimeline());
|
||||
}
|
||||
|
||||
private buildCallEventGroupers(events?: MatrixEvent[]): void {
|
||||
this.callEventGroupers = buildCallEventGroupers(this.callEventGroupers, events);
|
||||
private buildLegacyCallEventGroupers(events?: MatrixEvent[]): void {
|
||||
this.callEventGroupers = buildLegacyCallEventGroupers(this.callEventGroupers, events);
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
|
|
@ -28,7 +28,7 @@ interface IState {
|
|||
feeds: Array<CallFeed>;
|
||||
}
|
||||
|
||||
export default class AudioFeedArrayForCall extends React.Component<IProps, IState> {
|
||||
export default class AudioFeedArrayForLegacyCall extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
|
@ -14,15 +14,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC, useState, useMemo, useRef, useEffect } from "react";
|
||||
import React, { FC, useState, useMemo, useRef, useEffect, useCallback } from "react";
|
||||
import classNames from "classnames";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
import { useConnectedMembers } from "../../../utils/VideoChannelUtils";
|
||||
import VideoChannelStore from "../../../stores/VideoChannelStore";
|
||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
|
||||
import { useParticipants } from "../../../hooks/useCall";
|
||||
import { CallStore } from "../../../stores/CallStore";
|
||||
import { Call } from "../../../models/Call";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
|
@ -34,25 +36,22 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
|||
import FacePile from "../elements/FacePile";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
|
||||
interface IDeviceButtonProps {
|
||||
interface DeviceButtonProps {
|
||||
kind: string;
|
||||
devices: MediaDeviceInfo[];
|
||||
setDevice: (device: MediaDeviceInfo) => void;
|
||||
deviceListLabel: string;
|
||||
active: boolean;
|
||||
fallbackDeviceLabel: (n: number) => string;
|
||||
muted: boolean;
|
||||
disabled: boolean;
|
||||
toggle: () => void;
|
||||
activeTitle: string;
|
||||
inactiveTitle: string;
|
||||
unmutedTitle: string;
|
||||
mutedTitle: string;
|
||||
}
|
||||
|
||||
const DeviceButton: FC<IDeviceButtonProps> = ({
|
||||
kind, devices, setDevice, deviceListLabel, active, disabled, toggle, activeTitle, inactiveTitle,
|
||||
const DeviceButton: FC<DeviceButtonProps> = ({
|
||||
kind, devices, setDevice, deviceListLabel, fallbackDeviceLabel, muted, disabled, toggle, unmutedTitle, mutedTitle,
|
||||
}) => {
|
||||
// Depending on permissions, the browser might not let us know device labels,
|
||||
// in which case there's nothing helpful we can display
|
||||
const labelledDevices = useMemo(() => devices.filter(d => d.label.length), [devices]);
|
||||
|
||||
const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu();
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
|
@ -61,13 +60,13 @@ const DeviceButton: FC<IDeviceButtonProps> = ({
|
|||
closeMenu();
|
||||
};
|
||||
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect();
|
||||
const buttonRect = buttonRef.current!.getBoundingClientRect();
|
||||
contextMenu = <IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
||||
<IconizedContextMenuOptionList>
|
||||
{ labelledDevices.map(d =>
|
||||
{ devices.map((d, index) =>
|
||||
<IconizedContextMenuOption
|
||||
key={d.deviceId}
|
||||
label={d.label}
|
||||
label={d.label || fallbackDeviceLabel(index + 1)}
|
||||
onClick={() => selectDevice(d)}
|
||||
/>,
|
||||
) }
|
||||
|
@ -78,21 +77,20 @@ const DeviceButton: FC<IDeviceButtonProps> = ({
|
|||
if (!devices.length) return null;
|
||||
|
||||
return <div
|
||||
className={classNames({
|
||||
"mx_VideoLobby_deviceButtonWrapper": true,
|
||||
"mx_VideoLobby_deviceButtonWrapper_active": active,
|
||||
className={classNames("mx_CallLobby_deviceButtonWrapper", {
|
||||
"mx_CallLobby_deviceButtonWrapper_muted": muted,
|
||||
})}
|
||||
>
|
||||
<AccessibleTooltipButton
|
||||
className={`mx_VideoLobby_deviceButton mx_VideoLobby_deviceButton_${kind}`}
|
||||
title={active ? activeTitle : inactiveTitle}
|
||||
className={`mx_CallLobby_deviceButton mx_CallLobby_deviceButton_${kind}`}
|
||||
title={muted ? mutedTitle : unmutedTitle}
|
||||
alignment={Alignment.Top}
|
||||
onClick={toggle}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{ labelledDevices.length > 1 ? (
|
||||
{ devices.length > 1 ? (
|
||||
<ContextMenuButton
|
||||
className="mx_VideoLobby_deviceListButton"
|
||||
className="mx_CallLobby_deviceListButton"
|
||||
inputRef={buttonRef}
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
|
@ -106,57 +104,65 @@ const DeviceButton: FC<IDeviceButtonProps> = ({
|
|||
|
||||
const MAX_FACES = 8;
|
||||
|
||||
const VideoLobby: FC<{ room: Room }> = ({ room }) => {
|
||||
const store = VideoChannelStore.instance;
|
||||
interface Props {
|
||||
room: Room;
|
||||
call: Call;
|
||||
}
|
||||
|
||||
export const CallLobby: FC<Props> = ({ room, call }) => {
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const me = useMemo(() => room.getMember(room.myUserId), [room]);
|
||||
const connectedMembers = useConnectedMembers(room, false);
|
||||
const videoRef = useRef<HTMLVideoElement>();
|
||||
const me = useMemo(() => room.getMember(room.myUserId)!, [room]);
|
||||
const participants = useParticipants(call);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
const devices = useAsyncMemo(async () => {
|
||||
const [audioInputs, videoInputs] = useAsyncMemo(async () => {
|
||||
try {
|
||||
return await navigator.mediaDevices.enumerateDevices();
|
||||
const devices = await MediaDeviceHandler.getDevices();
|
||||
return [devices[MediaDeviceKindEnum.AudioInput], devices[MediaDeviceKindEnum.VideoInput]];
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to get media device list: ${e}`);
|
||||
return [];
|
||||
logger.warn(`Failed to get media device list`, e);
|
||||
return [[], []];
|
||||
}
|
||||
}, [], []);
|
||||
const audioDevices = useMemo(() => devices.filter(d => d.kind === "audioinput"), [devices]);
|
||||
const videoDevices = useMemo(() => devices.filter(d => d.kind === "videoinput"), [devices]);
|
||||
}, [], [[], []]);
|
||||
|
||||
const [selectedAudioDevice, selectAudioDevice] = useState<MediaDeviceInfo>(null);
|
||||
const [selectedVideoDevice, selectVideoDevice] = useState<MediaDeviceInfo>(null);
|
||||
const [videoInputId, setVideoInputId] = useState<string>(() => MediaDeviceHandler.getVideoInput());
|
||||
|
||||
const audioDevice = selectedAudioDevice ?? audioDevices[0];
|
||||
const videoDevice = selectedVideoDevice ?? videoDevices[0];
|
||||
const setAudioInput = useCallback((device: MediaDeviceInfo) => {
|
||||
MediaDeviceHandler.instance.setAudioInput(device.deviceId);
|
||||
}, []);
|
||||
const setVideoInput = useCallback((device: MediaDeviceInfo) => {
|
||||
MediaDeviceHandler.instance.setVideoInput(device.deviceId);
|
||||
setVideoInputId(device.deviceId);
|
||||
}, []);
|
||||
|
||||
const [audioActive, setAudioActive] = useState(!store.audioMuted);
|
||||
const [videoActive, setVideoActive] = useState(!store.videoMuted);
|
||||
const toggleAudio = () => {
|
||||
store.audioMuted = audioActive;
|
||||
setAudioActive(!audioActive);
|
||||
};
|
||||
const toggleVideo = () => {
|
||||
store.videoMuted = videoActive;
|
||||
setVideoActive(!videoActive);
|
||||
};
|
||||
const [audioMuted, setAudioMuted] = useState(() => MediaDeviceHandler.startWithAudioMuted);
|
||||
const [videoMuted, setVideoMuted] = useState(() => MediaDeviceHandler.startWithVideoMuted);
|
||||
|
||||
const toggleAudio = useCallback(() => {
|
||||
MediaDeviceHandler.startWithAudioMuted = !audioMuted;
|
||||
setAudioMuted(!audioMuted);
|
||||
}, [audioMuted, setAudioMuted]);
|
||||
const toggleVideo = useCallback(() => {
|
||||
MediaDeviceHandler.startWithVideoMuted = !videoMuted;
|
||||
setVideoMuted(!videoMuted);
|
||||
}, [videoMuted, setVideoMuted]);
|
||||
|
||||
const videoStream = useAsyncMemo(async () => {
|
||||
if (videoDevice && videoActive) {
|
||||
if (videoInputId && !videoMuted) {
|
||||
try {
|
||||
return await navigator.mediaDevices.getUserMedia({
|
||||
video: { deviceId: videoDevice.deviceId },
|
||||
video: { deviceId: videoInputId },
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get stream for device ${videoDevice.deviceId}: ${e}`);
|
||||
logger.error(`Failed to get stream for device ${videoInputId}`, e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [videoDevice, videoActive]);
|
||||
}, [videoInputId, videoMuted]);
|
||||
|
||||
useEffect(() => {
|
||||
if (videoStream) {
|
||||
const videoElement = videoRef.current;
|
||||
const videoElement = videoRef.current!;
|
||||
videoElement.srcObject = videoStream;
|
||||
videoElement.play();
|
||||
|
||||
|
@ -167,67 +173,69 @@ const VideoLobby: FC<{ room: Room }> = ({ room }) => {
|
|||
}
|
||||
}, [videoStream]);
|
||||
|
||||
const connect = async () => {
|
||||
const connect = useCallback(async () => {
|
||||
setConnecting(true);
|
||||
try {
|
||||
await store.connect(
|
||||
room.roomId, audioActive ? audioDevice : null, videoActive ? videoDevice : null,
|
||||
);
|
||||
// Disconnect from any other active calls first, since we don't yet support holding
|
||||
await Promise.all([...CallStore.instance.activeCalls].map(call => call.disconnect()));
|
||||
await call.connect();
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
setConnecting(false);
|
||||
}
|
||||
};
|
||||
}, [call, setConnecting]);
|
||||
|
||||
let facePile;
|
||||
if (connectedMembers.size) {
|
||||
const shownMembers = [...connectedMembers].slice(0, MAX_FACES);
|
||||
const overflow = connectedMembers.size > shownMembers.length;
|
||||
let facePile: JSX.Element | null = null;
|
||||
if (participants.size) {
|
||||
const shownMembers = [...participants].slice(0, MAX_FACES);
|
||||
const overflow = participants.size > shownMembers.length;
|
||||
|
||||
facePile = <div className="mx_VideoLobby_connectedMembers">
|
||||
{ _t("%(count)s people joined", { count: connectedMembers.size }) }
|
||||
facePile = <div className="mx_CallLobby_participants">
|
||||
{ _t("%(count)s people joined", { count: participants.size }) }
|
||||
<FacePile members={shownMembers} faceSize={24} overflow={overflow} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className="mx_VideoLobby">
|
||||
return <div className="mx_CallLobby">
|
||||
{ facePile }
|
||||
<div className="mx_VideoLobby_preview">
|
||||
<div className="mx_CallLobby_preview">
|
||||
<MemberAvatar key={me.userId} member={me} width={200} height={200} resizeMethod="scale" />
|
||||
<video
|
||||
ref={videoRef}
|
||||
style={{ visibility: videoActive ? null : "hidden" }}
|
||||
style={{ visibility: videoMuted ? "hidden" : undefined }}
|
||||
muted
|
||||
playsInline
|
||||
disablePictureInPicture
|
||||
/>
|
||||
<div className="mx_VideoLobby_controls">
|
||||
<div className="mx_CallLobby_controls">
|
||||
<DeviceButton
|
||||
kind="audio"
|
||||
devices={audioDevices}
|
||||
setDevice={selectAudioDevice}
|
||||
devices={audioInputs}
|
||||
setDevice={setAudioInput}
|
||||
deviceListLabel={_t("Audio devices")}
|
||||
active={audioActive}
|
||||
fallbackDeviceLabel={n => _t("Audio input %(n)s", { n })}
|
||||
muted={audioMuted}
|
||||
disabled={connecting}
|
||||
toggle={toggleAudio}
|
||||
activeTitle={_t("Mute microphone")}
|
||||
inactiveTitle={_t("Unmute microphone")}
|
||||
unmutedTitle={_t("Mute microphone")}
|
||||
mutedTitle={_t("Unmute microphone")}
|
||||
/>
|
||||
<DeviceButton
|
||||
kind="video"
|
||||
devices={videoDevices}
|
||||
setDevice={selectVideoDevice}
|
||||
devices={videoInputs}
|
||||
setDevice={setVideoInput}
|
||||
deviceListLabel={_t("Video devices")}
|
||||
active={videoActive}
|
||||
fallbackDeviceLabel={n => _t("Video input %(n)s", { n })}
|
||||
muted={videoMuted}
|
||||
disabled={connecting}
|
||||
toggle={toggleVideo}
|
||||
activeTitle={_t("Turn off camera")}
|
||||
inactiveTitle={_t("Turn on camera")}
|
||||
unmutedTitle={_t("Turn off camera")}
|
||||
mutedTitle={_t("Turn on camera")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AccessibleButton
|
||||
className="mx_VideoLobby_joinButton"
|
||||
className="mx_CallLobby_connectButton"
|
||||
kind="primary"
|
||||
disabled={connecting}
|
||||
onClick={connect}
|
||||
|
@ -236,5 +244,3 @@ const VideoLobby: FC<{ room: Room }> = ({ room }) => {
|
|||
</AccessibleButton>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default VideoLobby;
|
|
@ -21,7 +21,7 @@ import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
|||
import Field from "../elements/Field";
|
||||
import DialPad from './DialPad';
|
||||
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (boolean) => void;
|
||||
|
@ -78,7 +78,7 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
|
||||
onDialPress = async () => {
|
||||
CallHandler.instance.dialNumber(this.state.value);
|
||||
LegacyCallHandler.instance.dialNumber(this.state.value);
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
|
|||
import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import LegacyCallHandler from '../../../LegacyCallHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import VideoFeed from './VideoFeed';
|
||||
|
@ -32,9 +32,9 @@ import AccessibleButton from '../elements/AccessibleButton';
|
|||
import { avatarUrlForMember } from '../../../Avatar';
|
||||
import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker";
|
||||
import Modal from '../../../Modal';
|
||||
import CallViewSidebar from './CallViewSidebar';
|
||||
import CallViewHeader from './CallView/CallViewHeader';
|
||||
import CallViewButtons from "./CallView/CallViewButtons";
|
||||
import LegacyCallViewSidebar from './LegacyCallViewSidebar';
|
||||
import LegacyCallViewHeader from './LegacyCallView/LegacyCallViewHeader';
|
||||
import LegacyCallViewButtons from "./LegacyCallView/LegacyCallViewButtons";
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
|
@ -47,7 +47,7 @@ interface IProps {
|
|||
// Another ongoing call to display information about
|
||||
secondaryCall?: MatrixCall;
|
||||
|
||||
// a callback which is called when the content in the CallView changes
|
||||
// a callback which is called when the content in the LegacyCallView changes
|
||||
// in a way that is likely to cause a resize.
|
||||
onResize?: (event: Event) => void;
|
||||
|
||||
|
@ -57,7 +57,7 @@ interface IProps {
|
|||
// need to control those things separately, so this is simpler.
|
||||
pipMode?: boolean;
|
||||
|
||||
// Used for dragging the PiP CallView
|
||||
// Used for dragging the PiP LegacyCallView
|
||||
onMouseDownOnHeader?: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
|
||||
showApps?: boolean;
|
||||
|
@ -104,15 +104,15 @@ function exitFullscreen() {
|
|||
if (exitMethod) exitMethod.call(document);
|
||||
}
|
||||
|
||||
export default class CallView extends React.Component<IProps, IState> {
|
||||
export default class LegacyCallView extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private contentWrapperRef = createRef<HTMLDivElement>();
|
||||
private buttonsRef = createRef<CallViewButtons>();
|
||||
private buttonsRef = createRef<LegacyCallViewButtons>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const { primary, secondary, sidebar } = CallView.getOrderedFeeds(this.props.call.getFeeds());
|
||||
const { primary, secondary, sidebar } = LegacyCallView.getOrderedFeeds(this.props.call.getFeeds());
|
||||
|
||||
this.state = {
|
||||
isLocalOnHold: this.props.call.isLocalOnHold(),
|
||||
|
@ -146,7 +146,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
static getDerivedStateFromProps(props: IProps): Partial<IState> {
|
||||
const { primary, secondary, sidebar } = CallView.getOrderedFeeds(props.call.getFeeds());
|
||||
const { primary, secondary, sidebar } = LegacyCallView.getOrderedFeeds(props.call.getFeeds());
|
||||
|
||||
return {
|
||||
primaryFeed: primary,
|
||||
|
@ -209,7 +209,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onFeedsChanged = (newFeeds: Array<CallFeed>): void => {
|
||||
const { primary, secondary, sidebar } = CallView.getOrderedFeeds(newFeeds);
|
||||
const { primary, secondary, sidebar } = LegacyCallView.getOrderedFeeds(newFeeds);
|
||||
this.setState({
|
||||
primaryFeed: primary,
|
||||
secondaryFeed: secondary,
|
||||
|
@ -310,8 +310,8 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
|
||||
// Note that this assumes we always have a CallView on screen at any given time
|
||||
// CallHandler would probably be a better place for this
|
||||
// Note that this assumes we always have a LegacyCallView on screen at any given time
|
||||
// LegacyCallHandler would probably be a better place for this
|
||||
private onNativeKeyDown = (ev): void => {
|
||||
let handled = false;
|
||||
|
||||
|
@ -339,17 +339,17 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onCallResumeClick = (): void => {
|
||||
const userFacingRoomId = CallHandler.instance.roomIdForCall(this.props.call);
|
||||
CallHandler.instance.setActiveCallRoomId(userFacingRoomId);
|
||||
const userFacingRoomId = LegacyCallHandler.instance.roomIdForCall(this.props.call);
|
||||
LegacyCallHandler.instance.setActiveCallRoomId(userFacingRoomId);
|
||||
};
|
||||
|
||||
private onTransferClick = (): void => {
|
||||
const transfereeCall = CallHandler.instance.getTransfereeForCallId(this.props.call.callId);
|
||||
const transfereeCall = LegacyCallHandler.instance.getTransfereeForCallId(this.props.call.callId);
|
||||
this.props.call.transferToCall(transfereeCall);
|
||||
};
|
||||
|
||||
private onHangupClick = (): void => {
|
||||
CallHandler.instance.hangupOrReject(CallHandler.instance.roomIdForCall(this.props.call));
|
||||
LegacyCallHandler.instance.hangupOrReject(LegacyCallHandler.instance.roomIdForCall(this.props.call));
|
||||
};
|
||||
|
||||
private onToggleSidebar = (): void => {
|
||||
|
@ -380,7 +380,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
);
|
||||
|
||||
return (
|
||||
<CallViewButtons
|
||||
<LegacyCallViewButtons
|
||||
ref={this.buttonsRef}
|
||||
call={call}
|
||||
pipMode={pipMode}
|
||||
|
@ -431,7 +431,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_CallView_toast">
|
||||
<div className="mx_LegacyCallView_toast">
|
||||
{ text }
|
||||
</div>
|
||||
);
|
||||
|
@ -443,7 +443,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
|
||||
const callRoom = MatrixClientPeg.get().getRoom(call.roomId);
|
||||
const avatarSize = pipMode ? 76 : 160;
|
||||
const transfereeCall = CallHandler.instance.getTransfereeForCallId(call.callId);
|
||||
const transfereeCall = LegacyCallHandler.instance.getTransfereeForCallId(call.callId);
|
||||
const isOnHold = isLocalOnHold || isRemoteOnHold;
|
||||
|
||||
let secondaryFeedElement: React.ReactNode;
|
||||
|
@ -460,23 +460,23 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
if (transfereeCall || isOnHold) {
|
||||
const containerClasses = classNames("mx_CallView_content", {
|
||||
mx_CallView_content_hold: isOnHold,
|
||||
const containerClasses = classNames("mx_LegacyCallView_content", {
|
||||
mx_LegacyCallView_content_hold: isOnHold,
|
||||
});
|
||||
const backgroundAvatarUrl = avatarUrlForMember(call.getOpponentMember(), 1024, 1024, 'crop');
|
||||
|
||||
let holdTransferContent: React.ReactNode;
|
||||
if (transfereeCall) {
|
||||
const transferTargetRoom = MatrixClientPeg.get().getRoom(
|
||||
CallHandler.instance.roomIdForCall(call),
|
||||
LegacyCallHandler.instance.roomIdForCall(call),
|
||||
);
|
||||
const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person");
|
||||
const transfereeRoom = MatrixClientPeg.get().getRoom(
|
||||
CallHandler.instance.roomIdForCall(transfereeCall),
|
||||
LegacyCallHandler.instance.roomIdForCall(transfereeCall),
|
||||
);
|
||||
const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person");
|
||||
|
||||
holdTransferContent = <div className="mx_CallView_status">
|
||||
holdTransferContent = <div className="mx_LegacyCallView_status">
|
||||
{ _t(
|
||||
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
|
||||
{
|
||||
|
@ -494,7 +494,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
let onHoldText: React.ReactNode;
|
||||
if (isRemoteOnHold) {
|
||||
onHoldText = _t(
|
||||
CallHandler.instance.hasAnyUnheldCall()
|
||||
LegacyCallHandler.instance.hasAnyUnheldCall()
|
||||
? _td("You held the call <a>Switch</a>")
|
||||
: _td("You held the call <a>Resume</a>"),
|
||||
{},
|
||||
|
@ -511,7 +511,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
holdTransferContent = (
|
||||
<div className="mx_CallView_status">
|
||||
<div className="mx_LegacyCallView_status">
|
||||
{ onHoldText }
|
||||
</div>
|
||||
);
|
||||
|
@ -519,16 +519,16 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
|
||||
return (
|
||||
<div className={containerClasses} onMouseMove={this.onMouseMove}>
|
||||
<div className="mx_CallView_holdBackground" style={{ backgroundImage: 'url(' + backgroundAvatarUrl + ')' }} />
|
||||
<div className="mx_LegacyCallView_holdBackground" style={{ backgroundImage: 'url(' + backgroundAvatarUrl + ')' }} />
|
||||
{ holdTransferContent }
|
||||
</div>
|
||||
);
|
||||
} else if (call.noIncomingFeeds()) {
|
||||
return (
|
||||
<div className="mx_CallView_content" onMouseMove={this.onMouseMove}>
|
||||
<div className="mx_CallView_avatarsContainer">
|
||||
<div className="mx_LegacyCallView_content" onMouseMove={this.onMouseMove}>
|
||||
<div className="mx_LegacyCallView_avatarsContainer">
|
||||
<div
|
||||
className="mx_CallView_avatarContainer"
|
||||
className="mx_LegacyCallView_avatarContainer"
|
||||
style={{ width: avatarSize, height: avatarSize }}
|
||||
>
|
||||
<RoomAvatar
|
||||
|
@ -538,14 +538,14 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_CallView_status">{ _t("Connecting") }</div>
|
||||
<div className="mx_LegacyCallView_status">{ _t("Connecting") }</div>
|
||||
{ secondaryFeedElement }
|
||||
</div>
|
||||
);
|
||||
} else if (pipMode) {
|
||||
return (
|
||||
<div
|
||||
className="mx_CallView_content"
|
||||
className="mx_LegacyCallView_content"
|
||||
onMouseMove={this.onMouseMove}
|
||||
>
|
||||
<VideoFeed
|
||||
|
@ -559,7 +559,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
);
|
||||
} else if (secondaryFeed) {
|
||||
return (
|
||||
<div className="mx_CallView_content" onMouseMove={this.onMouseMove}>
|
||||
<div className="mx_LegacyCallView_content" onMouseMove={this.onMouseMove}>
|
||||
<VideoFeed
|
||||
feed={primaryFeed}
|
||||
call={call}
|
||||
|
@ -572,7 +572,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_CallView_content" onMouseMove={this.onMouseMove}>
|
||||
<div className="mx_LegacyCallView_content" onMouseMove={this.onMouseMove}>
|
||||
<VideoFeed
|
||||
feed={primaryFeed}
|
||||
call={call}
|
||||
|
@ -580,7 +580,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
onResize={onResize}
|
||||
primary={true}
|
||||
/>
|
||||
{ sidebarShown && <CallViewSidebar
|
||||
{ sidebarShown && <LegacyCallViewSidebar
|
||||
feeds={sidebarFeeds}
|
||||
call={call}
|
||||
pipMode={pipMode}
|
||||
|
@ -604,27 +604,27 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
} = this.state;
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
const callRoomId = CallHandler.instance.roomIdForCall(call);
|
||||
const secondaryCallRoomId = CallHandler.instance.roomIdForCall(secondaryCall);
|
||||
const callRoomId = LegacyCallHandler.instance.roomIdForCall(call);
|
||||
const secondaryCallRoomId = LegacyCallHandler.instance.roomIdForCall(secondaryCall);
|
||||
const callRoom = client.getRoom(callRoomId);
|
||||
const secCallRoom = secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
|
||||
|
||||
const callViewClasses = classNames({
|
||||
mx_CallView: true,
|
||||
mx_CallView_pip: pipMode,
|
||||
mx_CallView_large: !pipMode,
|
||||
mx_CallView_sidebar: sidebarShown && sidebarFeeds.length !== 0 && !pipMode,
|
||||
mx_CallView_belowWidget: showApps, // css to correct the margins if the call is below the AppsDrawer.
|
||||
mx_LegacyCallView: true,
|
||||
mx_LegacyCallView_pip: pipMode,
|
||||
mx_LegacyCallView_large: !pipMode,
|
||||
mx_LegacyCallView_sidebar: sidebarShown && sidebarFeeds.length !== 0 && !pipMode,
|
||||
mx_LegacyCallView_belowWidget: showApps, // css to correct the margins if the call is below the AppsDrawer.
|
||||
});
|
||||
|
||||
return <div className={callViewClasses}>
|
||||
<CallViewHeader
|
||||
<LegacyCallViewHeader
|
||||
onPipMouseDown={onMouseDownOnHeader}
|
||||
pipMode={pipMode}
|
||||
callRooms={[callRoom, secCallRoom]}
|
||||
onMaximize={this.onMaximizeClick}
|
||||
/>
|
||||
<div className="mx_CallView_content_wrapper" ref={this.contentWrapperRef}>
|
||||
<div className="mx_LegacyCallView_content_wrapper" ref={this.contentWrapperRef}>
|
||||
{ this.renderToast() }
|
||||
{ this.renderContent() }
|
||||
{ this.renderCallControls() }
|
|
@ -21,7 +21,7 @@ import classNames from "classnames";
|
|||
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton";
|
||||
import CallContextMenu from "../../context_menus/CallContextMenu";
|
||||
import LegacyCallContextMenu from "../../context_menus/LegacyCallContextMenu";
|
||||
import DialpadContextMenu from "../../context_menus/DialpadContextMenu";
|
||||
import { Alignment } from "../../elements/Tooltip";
|
||||
import {
|
||||
|
@ -49,7 +49,7 @@ interface IButtonProps extends Omit<React.ComponentProps<typeof AccessibleToolti
|
|||
onClick: (event: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const CallViewToggleButton: React.FC<IButtonProps> = ({
|
||||
const LegacyCallViewToggleButton: React.FC<IButtonProps> = ({
|
||||
children,
|
||||
state: isOn,
|
||||
className,
|
||||
|
@ -57,9 +57,9 @@ const CallViewToggleButton: React.FC<IButtonProps> = ({
|
|||
offLabel,
|
||||
...props
|
||||
}) => {
|
||||
const classes = classNames("mx_CallViewButtons_button", className, {
|
||||
mx_CallViewButtons_button_on: isOn,
|
||||
mx_CallViewButtons_button_off: !isOn,
|
||||
const classes = classNames("mx_LegacyCallViewButtons_button", className, {
|
||||
mx_LegacyCallViewButtons_button_on: isOn,
|
||||
mx_LegacyCallViewButtons_button_off: !isOn,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -78,12 +78,12 @@ interface IDropdownButtonProps extends IButtonProps {
|
|||
deviceKinds: MediaDeviceKindEnum[];
|
||||
}
|
||||
|
||||
const CallViewDropdownButton: React.FC<IDropdownButtonProps> = ({ state, deviceKinds, ...props }) => {
|
||||
const LegacyCallViewDropdownButton: React.FC<IDropdownButtonProps> = ({ state, deviceKinds, ...props }) => {
|
||||
const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu();
|
||||
const [hoveringDropdown, setHoveringDropdown] = useState(false);
|
||||
|
||||
const classes = classNames("mx_CallViewButtons_button", "mx_CallViewButtons_dropdownButton", {
|
||||
mx_CallViewButtons_dropdownButton_collapsed: !menuDisplayed,
|
||||
const classes = classNames("mx_LegacyCallViewButtons_button", "mx_LegacyCallViewButtons_dropdownButton", {
|
||||
mx_LegacyCallViewButtons_dropdownButton_collapsed: !menuDisplayed,
|
||||
});
|
||||
|
||||
const onClick = (event: React.MouseEvent): void => {
|
||||
|
@ -92,8 +92,8 @@ const CallViewDropdownButton: React.FC<IDropdownButtonProps> = ({ state, deviceK
|
|||
};
|
||||
|
||||
return (
|
||||
<CallViewToggleButton inputRef={buttonRef} forceHide={menuDisplayed || hoveringDropdown} state={state} {...props}>
|
||||
<CallViewToggleButton
|
||||
<LegacyCallViewToggleButton inputRef={buttonRef} forceHide={menuDisplayed || hoveringDropdown} state={state} {...props}>
|
||||
<LegacyCallViewToggleButton
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
onHover={(hovering) => setHoveringDropdown(hovering)}
|
||||
|
@ -105,7 +105,7 @@ const CallViewDropdownButton: React.FC<IDropdownButtonProps> = ({ state, deviceK
|
|||
onFinished={closeMenu}
|
||||
deviceKinds={deviceKinds}
|
||||
/> }
|
||||
</CallViewToggleButton>
|
||||
</LegacyCallViewToggleButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -141,7 +141,7 @@ interface IState {
|
|||
showMoreMenu: boolean;
|
||||
}
|
||||
|
||||
export default class CallViewButtons extends React.Component<IProps, IState> {
|
||||
export default class LegacyCallViewButtons extends React.Component<IProps, IState> {
|
||||
private dialpadButton = createRef<HTMLDivElement>();
|
||||
private contextMenuButton = createRef<HTMLDivElement>();
|
||||
private controlsHideTimer: number = null;
|
||||
|
@ -212,8 +212,8 @@ export default class CallViewButtons extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const callControlsClasses = classNames("mx_CallViewButtons", {
|
||||
mx_CallViewButtons_hidden: !this.state.visible,
|
||||
const callControlsClasses = classNames("mx_LegacyCallViewButtons", {
|
||||
mx_LegacyCallViewButtons_hidden: !this.state.visible,
|
||||
});
|
||||
|
||||
let dialPad;
|
||||
|
@ -236,7 +236,7 @@ export default class CallViewButtons extends React.Component<IProps, IState> {
|
|||
|
||||
let contextMenu;
|
||||
if (this.state.showMoreMenu) {
|
||||
contextMenu = <CallContextMenu
|
||||
contextMenu = <LegacyCallContextMenu
|
||||
{...alwaysAboveLeftOf(
|
||||
this.contextMenuButton.current.getBoundingClientRect(),
|
||||
ChevronFace.None,
|
||||
|
@ -258,45 +258,45 @@ export default class CallViewButtons extends React.Component<IProps, IState> {
|
|||
{ contextMenu }
|
||||
|
||||
{ this.props.buttonsVisibility.dialpad && <ContextMenuTooltipButton
|
||||
className="mx_CallViewButtons_button mx_CallViewButtons_dialpad"
|
||||
className="mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_dialpad"
|
||||
inputRef={this.dialpadButton}
|
||||
onClick={this.onDialpadClick}
|
||||
isExpanded={this.state.showDialpad}
|
||||
title={_t("Dialpad")}
|
||||
alignment={Alignment.Top}
|
||||
/> }
|
||||
<CallViewDropdownButton
|
||||
<LegacyCallViewDropdownButton
|
||||
state={!this.props.buttonsState.micMuted}
|
||||
className="mx_CallViewButtons_button_mic"
|
||||
className="mx_LegacyCallViewButtons_button_mic"
|
||||
onLabel={_t("Mute the microphone")}
|
||||
offLabel={_t("Unmute the microphone")}
|
||||
onClick={this.props.handlers.onMicMuteClick}
|
||||
deviceKinds={[MediaDeviceKindEnum.AudioInput, MediaDeviceKindEnum.AudioOutput]}
|
||||
/>
|
||||
{ this.props.buttonsVisibility.vidMute && <CallViewDropdownButton
|
||||
{ this.props.buttonsVisibility.vidMute && <LegacyCallViewDropdownButton
|
||||
state={!this.props.buttonsState.vidMuted}
|
||||
className="mx_CallViewButtons_button_vid"
|
||||
className="mx_LegacyCallViewButtons_button_vid"
|
||||
onLabel={_t("Stop the camera")}
|
||||
offLabel={_t("Start the camera")}
|
||||
onClick={this.props.handlers.onVidMuteClick}
|
||||
deviceKinds={[MediaDeviceKindEnum.VideoInput]}
|
||||
/> }
|
||||
{ this.props.buttonsVisibility.screensharing && <CallViewToggleButton
|
||||
{ this.props.buttonsVisibility.screensharing && <LegacyCallViewToggleButton
|
||||
state={this.props.buttonsState.screensharing}
|
||||
className="mx_CallViewButtons_button_screensharing"
|
||||
className="mx_LegacyCallViewButtons_button_screensharing"
|
||||
onLabel={_t("Stop sharing your screen")}
|
||||
offLabel={_t("Start sharing your screen")}
|
||||
onClick={this.props.handlers.onScreenshareClick}
|
||||
/> }
|
||||
{ this.props.buttonsVisibility.sidebar && <CallViewToggleButton
|
||||
{ this.props.buttonsVisibility.sidebar && <LegacyCallViewToggleButton
|
||||
state={this.props.buttonsState.sidebarShown}
|
||||
className="mx_CallViewButtons_button_sidebar"
|
||||
className="mx_LegacyCallViewButtons_button_sidebar"
|
||||
onLabel={_t("Hide sidebar")}
|
||||
offLabel={_t("Show sidebar")}
|
||||
onClick={this.props.handlers.onToggleSidebarClick}
|
||||
/> }
|
||||
{ this.props.buttonsVisibility.contextMenu && <ContextMenuTooltipButton
|
||||
className="mx_CallViewButtons_button mx_CallViewButtons_button_more"
|
||||
className="mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_more"
|
||||
onClick={this.onMoreClick}
|
||||
inputRef={this.contextMenuButton}
|
||||
isExpanded={this.state.showMoreMenu}
|
||||
|
@ -304,7 +304,7 @@ export default class CallViewButtons extends React.Component<IProps, IState> {
|
|||
alignment={Alignment.Top}
|
||||
/> }
|
||||
<AccessibleTooltipButton
|
||||
className="mx_CallViewButtons_button mx_CallViewButtons_button_hangup"
|
||||
className="mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_hangup"
|
||||
onClick={this.props.handlers.onHangupClick}
|
||||
title={_t("Hangup")}
|
||||
alignment={Alignment.Top}
|
|
@ -21,26 +21,26 @@ import { _t } from '../../../../languageHandler';
|
|||
import RoomAvatar from '../../avatars/RoomAvatar';
|
||||
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';
|
||||
|
||||
interface CallControlsProps {
|
||||
interface LegacyCallControlsProps {
|
||||
onExpand?: () => void;
|
||||
onPin?: () => void;
|
||||
onMaximize?: () => void;
|
||||
}
|
||||
|
||||
const CallViewHeaderControls: React.FC<CallControlsProps> = ({ onExpand, onPin, onMaximize }) => {
|
||||
return <div className="mx_CallViewHeader_controls">
|
||||
const LegacyCallViewHeaderControls: React.FC<LegacyCallControlsProps> = ({ onExpand, onPin, onMaximize }) => {
|
||||
return <div className="mx_LegacyCallViewHeader_controls">
|
||||
{ onMaximize && <AccessibleTooltipButton
|
||||
className="mx_CallViewHeader_button mx_CallViewHeader_button_fullscreen"
|
||||
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_fullscreen"
|
||||
onClick={onMaximize}
|
||||
title={_t("Fill Screen")}
|
||||
/> }
|
||||
{ onPin && <AccessibleTooltipButton
|
||||
className="mx_CallViewHeader_button mx_CallViewHeader_button_pin"
|
||||
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_pin"
|
||||
onClick={onPin}
|
||||
title={_t("Pin")}
|
||||
/> }
|
||||
{ onExpand && <AccessibleTooltipButton
|
||||
className="mx_CallViewHeader_button mx_CallViewHeader_button_expand"
|
||||
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_expand"
|
||||
onClick={onExpand}
|
||||
title={_t("Return to call")}
|
||||
/> }
|
||||
|
@ -52,15 +52,15 @@ interface ISecondaryCallInfoProps {
|
|||
}
|
||||
|
||||
const SecondaryCallInfo: React.FC<ISecondaryCallInfoProps> = ({ callRoom }) => {
|
||||
return <span className="mx_CallViewHeader_secondaryCallInfo">
|
||||
return <span className="mx_LegacyCallViewHeader_secondaryCallInfo">
|
||||
<RoomAvatar room={callRoom} height={16} width={16} />
|
||||
<span className="mx_CallView_secondaryCall_roomName">
|
||||
<span className="mx_LegacyCallView_secondaryCall_roomName">
|
||||
{ _t("%(name)s on hold", { name: callRoom.name }) }
|
||||
</span>
|
||||
</span>;
|
||||
};
|
||||
|
||||
interface CallViewHeaderProps {
|
||||
interface LegacyCallViewHeaderProps {
|
||||
pipMode: boolean;
|
||||
callRooms?: Room[];
|
||||
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
|
@ -69,7 +69,7 @@ interface CallViewHeaderProps {
|
|||
onMaximize?: () => void;
|
||||
}
|
||||
|
||||
const CallViewHeader: React.FC<CallViewHeaderProps> = ({
|
||||
const LegacyCallViewHeader: React.FC<LegacyCallViewHeaderProps> = ({
|
||||
pipMode = false,
|
||||
callRooms = [],
|
||||
onPipMouseDown,
|
||||
|
@ -81,25 +81,25 @@ const CallViewHeader: React.FC<CallViewHeaderProps> = ({
|
|||
const callRoomName = callRoom.name;
|
||||
|
||||
if (!pipMode) {
|
||||
return <div className="mx_CallViewHeader">
|
||||
<div className="mx_CallViewHeader_icon" />
|
||||
<span className="mx_CallViewHeader_text">{ _t("Call") }</span>
|
||||
<CallViewHeaderControls onMaximize={onMaximize} />
|
||||
return <div className="mx_LegacyCallViewHeader">
|
||||
<div className="mx_LegacyCallViewHeader_icon" />
|
||||
<span className="mx_LegacyCallViewHeader_text">{ _t("Call") }</span>
|
||||
<LegacyCallViewHeaderControls onMaximize={onMaximize} />
|
||||
</div>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="mx_CallViewHeader mx_CallViewHeader_pip"
|
||||
className="mx_LegacyCallViewHeader mx_LegacyCallViewHeader_pip"
|
||||
onMouseDown={onPipMouseDown}
|
||||
>
|
||||
<RoomAvatar room={callRoom} height={32} width={32} />
|
||||
<div className="mx_CallViewHeader_callInfo">
|
||||
<div className="mx_CallViewHeader_roomName">{ callRoomName }</div>
|
||||
<div className="mx_LegacyCallViewHeader_callInfo">
|
||||
<div className="mx_LegacyCallViewHeader_roomName">{ callRoomName }</div>
|
||||
{ onHoldCallRoom && <SecondaryCallInfo callRoom={onHoldCallRoom} /> }
|
||||
</div>
|
||||
<CallViewHeaderControls onExpand={onExpand} onPin={onPin} onMaximize={onMaximize} />
|
||||
<LegacyCallViewHeaderControls onExpand={onExpand} onPin={onPin} onMaximize={onMaximize} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallViewHeader;
|
||||
export default LegacyCallViewHeader;
|
|
@ -18,8 +18,8 @@ import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
|||
import React from 'react';
|
||||
import { Resizable } from "re-resizable";
|
||||
|
||||
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
|
||||
import CallView from './CallView';
|
||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../../LegacyCallHandler';
|
||||
import LegacyCallView from './LegacyCallView';
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
|
||||
interface IProps {
|
||||
|
@ -32,14 +32,14 @@ interface IProps {
|
|||
}
|
||||
|
||||
interface IState {
|
||||
call: MatrixCall;
|
||||
call: MatrixCall | null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Wrapper for CallView that always display the call in a given room,
|
||||
* Wrapper for LegacyCallView that always display the call in a given room,
|
||||
* or nothing if there is no call in that room.
|
||||
*/
|
||||
export default class CallViewForRoom extends React.Component<IProps, IState> {
|
||||
export default class LegacyCallViewForRoom extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -48,13 +48,13 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentDidMount() {
|
||||
CallHandler.instance.addListener(CallHandlerEvent.CallState, this.updateCall);
|
||||
CallHandler.instance.addListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
|
||||
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.updateCall);
|
||||
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCall);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.updateCall);
|
||||
CallHandler.instance.removeListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
|
||||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCall);
|
||||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCall);
|
||||
}
|
||||
|
||||
private updateCall = () => {
|
||||
|
@ -64,8 +64,8 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private getCall(): MatrixCall {
|
||||
const call = CallHandler.instance.getCallForRoom(this.props.roomId);
|
||||
private getCall(): MatrixCall | null {
|
||||
const call = LegacyCallHandler.instance.getCallForRoom(this.props.roomId);
|
||||
|
||||
if (call && [CallState.Ended, CallState.Ringing].includes(call.state)) return null;
|
||||
return call;
|
||||
|
@ -87,7 +87,7 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
|
|||
if (!this.state.call) return null;
|
||||
|
||||
return (
|
||||
<div className="mx_CallViewForRoom">
|
||||
<div className="mx_LegacyCallViewForRoom">
|
||||
<Resizable
|
||||
minHeight={380}
|
||||
maxHeight="80vh"
|
||||
|
@ -104,10 +104,10 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
|
|||
onResizeStart={this.onResizeStart}
|
||||
onResize={this.onResize}
|
||||
onResizeStop={this.onResizeStop}
|
||||
className="mx_CallViewForRoom_ResizeWrapper"
|
||||
handleClasses={{ bottom: "mx_CallViewForRoom_ResizeHandle" }}
|
||||
className="mx_LegacyCallViewForRoom_ResizeWrapper"
|
||||
handleClasses={{ bottom: "mx_LegacyCallViewForRoom_ResizeHandle" }}
|
||||
>
|
||||
<CallView
|
||||
<LegacyCallView
|
||||
call={this.state.call}
|
||||
pipMode={false}
|
||||
showApps={this.props.showApps}
|
|
@ -27,7 +27,7 @@ interface IProps {
|
|||
pipMode: boolean;
|
||||
}
|
||||
|
||||
export default class CallViewSidebar extends React.Component<IProps> {
|
||||
export default class LegacyCallViewSidebar extends React.Component<IProps> {
|
||||
render() {
|
||||
const feeds = this.props.feeds.map((feed) => {
|
||||
return (
|
||||
|
@ -41,8 +41,8 @@ export default class CallViewSidebar extends React.Component<IProps> {
|
|||
);
|
||||
});
|
||||
|
||||
const className = classNames("mx_CallViewSidebar", {
|
||||
mx_CallViewSidebar_pipMode: this.props.pipMode,
|
||||
const className = classNames("mx_LegacyCallViewSidebar", {
|
||||
mx_LegacyCallViewSidebar_pipMode: this.props.pipMode,
|
||||
});
|
||||
|
||||
return (
|
|
@ -21,9 +21,9 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
import classNames from 'classnames';
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import CallView from "./CallView";
|
||||
import LegacyCallView from "./LegacyCallView";
|
||||
import { RoomViewStore } from '../../../stores/RoomViewStore';
|
||||
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
|
||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../../LegacyCallHandler';
|
||||
import PersistentApp from "../elements/PersistentApp";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
|
@ -31,7 +31,7 @@ import PictureInPictureDragger from './PictureInPictureDragger';
|
|||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { Container, WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
|
||||
import CallViewHeader from './CallView/CallViewHeader';
|
||||
import LegacyCallViewHeader from './LegacyCallView/LegacyCallViewHeader';
|
||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
|
||||
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
|
@ -81,7 +81,7 @@ const getRoomAndAppForWidget = (widgetId: string, roomId: string): [Room, IApp]
|
|||
// The primary will be the one not on hold, or an arbitrary one
|
||||
// if they're all on hold)
|
||||
function getPrimarySecondaryCallsForPip(roomId: string): [MatrixCall, MatrixCall[]] {
|
||||
const calls = CallHandler.instance.getAllActiveCallsForPip(roomId);
|
||||
const calls = LegacyCallHandler.instance.getAllActiveCallsForPip(roomId);
|
||||
|
||||
let primary: MatrixCall = null;
|
||||
let secondaries: MatrixCall[] = [];
|
||||
|
@ -110,7 +110,7 @@ function getPrimarySecondaryCallsForPip(roomId: string): [MatrixCall, MatrixCall
|
|||
}
|
||||
|
||||
/**
|
||||
* PipView shows a small version of the CallView or a sticky widget hovering over the UI in 'picture-in-picture'
|
||||
* PipView shows a small version of the LegacyCallView or a sticky widget hovering over the UI in 'picture-in-picture'
|
||||
* (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing
|
||||
* and all widgets that are active but not shown in any other possible container.
|
||||
*/
|
||||
|
@ -139,8 +139,8 @@ export default class PipView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentDidMount() {
|
||||
CallHandler.instance.addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
CallHandler.instance.addListener(CallHandlerEvent.CallState, this.updateCalls);
|
||||
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
|
||||
this.roomStoreToken = RoomViewStore.instance.addListener(this.onRoomViewStoreUpdate);
|
||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
const room = MatrixClientPeg.get()?.getRoom(this.state.viewedRoomId);
|
||||
|
@ -154,8 +154,8 @@ export default class PipView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
CallHandler.instance.removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.updateCalls);
|
||||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
|
||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
this.roomStoreToken?.remove();
|
||||
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||
|
@ -308,7 +308,7 @@ export default class PipView extends React.Component<IProps, IState> {
|
|||
|
||||
if (this.state.primaryCall) {
|
||||
pipContent = ({ onStartMoving, onResize }) =>
|
||||
<CallView
|
||||
<LegacyCallView
|
||||
onMouseDownOnHeader={onStartMoving}
|
||||
call={this.state.primaryCall}
|
||||
secondaryCall={this.state.secondaryCall}
|
||||
|
@ -329,7 +329,7 @@ export default class PipView extends React.Component<IProps, IState> {
|
|||
|
||||
pipContent = ({ onStartMoving, _onResize }) =>
|
||||
<div className={pipViewClasses}>
|
||||
<CallViewHeader
|
||||
<LegacyCallViewHeader
|
||||
onPipMouseDown={(event) => { onStartMoving(event); this.onStartMoving.bind(this)(); }}
|
||||
pipMode={pipMode}
|
||||
callRooms={[roomForWidget]}
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { EventType, RoomCreateTypeField, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
||||
|
@ -37,7 +37,7 @@ import { getAddressType } from "./UserAddress";
|
|||
import { VIRTUAL_ROOM_EVENT_TYPE } from "./call-types";
|
||||
import SpaceStore from "./stores/spaces/SpaceStore";
|
||||
import { makeSpaceParentEvent } from "./utils/space";
|
||||
import { VIDEO_CHANNEL_MEMBER, addVideoChannel } from "./utils/VideoChannelUtils";
|
||||
import { JitsiCall } from "./models/Call";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||
import Spinner from "./components/views/elements/Spinner";
|
||||
|
@ -131,8 +131,8 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
|
|||
if (opts.roomType === RoomType.ElementVideo) {
|
||||
createOpts.power_level_content_override = {
|
||||
events: {
|
||||
// Allow all users to send video member updates
|
||||
[VIDEO_CHANNEL_MEMBER]: 0,
|
||||
// Allow all users to send call membership updates
|
||||
[JitsiCall.MEMBER_EVENT_TYPE]: 0,
|
||||
// Make widgets immutable, even to admins
|
||||
"im.vector.modular.widgets": 200,
|
||||
// Annoyingly, we have to reiterate all the defaults here
|
||||
|
@ -239,7 +239,8 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
|
|||
let modal;
|
||||
if (opts.spinner) modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
|
||||
|
||||
let roomId;
|
||||
let roomId: string;
|
||||
let room: Promise<Room>;
|
||||
return client.createRoom(createOpts).catch(function(err) {
|
||||
// NB This checks for the Synapse-specific error condition of a room creation
|
||||
// having been denied because the requesting user wanted to publish the room,
|
||||
|
@ -254,32 +255,43 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
|
|||
}
|
||||
}).finally(function() {
|
||||
if (modal) modal.close();
|
||||
}).then(function(res) {
|
||||
}).then(async res => {
|
||||
roomId = res.room_id;
|
||||
if (opts.dmUserId) {
|
||||
return Rooms.setDMRoom(roomId, opts.dmUserId);
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
room = new Promise(resolve => {
|
||||
const storedRoom = client.getRoom(roomId);
|
||||
if (storedRoom) {
|
||||
resolve(storedRoom);
|
||||
} else {
|
||||
// The room hasn't arrived down sync yet
|
||||
const onRoom = (emittedRoom: Room) => {
|
||||
if (emittedRoom.roomId === roomId) {
|
||||
resolve(emittedRoom);
|
||||
client.off(ClientEvent.Room, onRoom);
|
||||
}
|
||||
};
|
||||
client.on(ClientEvent.Room, onRoom);
|
||||
}
|
||||
});
|
||||
|
||||
if (opts.dmUserId) await Rooms.setDMRoom(roomId, opts.dmUserId);
|
||||
}).then(() => {
|
||||
if (opts.parentSpace) {
|
||||
return SpaceStore.instance.addRoomToSpace(opts.parentSpace, roomId, [client.getDomain()], opts.suggested);
|
||||
}
|
||||
}).then(async () => {
|
||||
if (opts.roomType === RoomType.ElementVideo) {
|
||||
// Set up video rooms with a Jitsi widget
|
||||
await addVideoChannel(roomId, createOpts.name);
|
||||
// Set up video rooms with a Jitsi call
|
||||
await JitsiCall.create(await room);
|
||||
|
||||
// Reset our power level back to admin so that the widget becomes immutable
|
||||
const room = client.getRoom(roomId);
|
||||
const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
await client.setPowerLevel(roomId, client.getUserId(), 100, plEvent);
|
||||
const plEvent = (await room)?.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
await client.setPowerLevel(roomId, client.getUserId()!, 100, plEvent);
|
||||
}
|
||||
}).then(function() {
|
||||
// NB createRoom doesn't block on the client seeing the echo that the
|
||||
// room has been created, so we race here with the client knowing that
|
||||
// the room exists, causing things like
|
||||
// https://github.com/vector-im/vector-web/issues/1813
|
||||
// NB we haven't necessarily blocked on the room promise, so we race
|
||||
// here with the client knowing that the room exists, causing things
|
||||
// like https://github.com/vector-im/vector-web/issues/1813
|
||||
// Even if we were to block on the echo, servers tend to split the room
|
||||
// state over multiple syncs so we can't atomically know when we have the
|
||||
// entire thing.
|
||||
|
|
|
@ -22,12 +22,12 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
|
|||
|
||||
import EditorStateTransfer from "../utils/EditorStateTransfer";
|
||||
import { RoomPermalinkCreator } from "../utils/permalinks/Permalinks";
|
||||
import CallEventGrouper from "../components/structures/CallEventGrouper";
|
||||
import LegacyCallEventGrouper from "../components/structures/LegacyCallEventGrouper";
|
||||
import { GetRelationsForEvent } from "../components/views/rooms/EventTile";
|
||||
import { TimelineRenderingType } from "../contexts/RoomContext";
|
||||
import MessageEvent from "../components/views/messages/MessageEvent";
|
||||
import MKeyVerificationConclusion from "../components/views/messages/MKeyVerificationConclusion";
|
||||
import CallEvent from "../components/views/messages/CallEvent";
|
||||
import LegacyCallEvent from "../components/views/messages/LegacyCallEvent";
|
||||
import TextualEvent from "../components/views/messages/TextualEvent";
|
||||
import EncryptionEvent from "../components/views/messages/EncryptionEvent";
|
||||
import RoomCreate from "../components/views/messages/RoomCreate";
|
||||
|
@ -57,7 +57,7 @@ export interface EventTileTypeProps {
|
|||
editState?: EditorStateTransfer;
|
||||
replacingEventId?: string;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
callEventGrouper?: CallEventGrouper;
|
||||
callEventGrouper?: LegacyCallEventGrouper;
|
||||
isSeeingThroughMessageHiddenForModeration?: boolean;
|
||||
timestamp?: JSX.Element;
|
||||
maxImageHeight?: number; // pixels
|
||||
|
@ -71,8 +71,8 @@ type FactoryMap = Record<string, Factory>;
|
|||
|
||||
const MessageEventFactory: Factory = (ref, props) => <MessageEvent ref={ref} {...props} />;
|
||||
const KeyVerificationConclFactory: Factory = (ref, props) => <MKeyVerificationConclusion ref={ref} {...props} />;
|
||||
const CallEventFactory: Factory<FactoryProps & { callEventGrouper: CallEventGrouper }> = (ref, props) => (
|
||||
<CallEvent ref={ref} {...props} />
|
||||
const LegacyCallEventFactory: Factory<FactoryProps & { callEventGrouper: LegacyCallEventGrouper }> = (ref, props) => (
|
||||
<LegacyCallEvent ref={ref} {...props} />
|
||||
);
|
||||
const TextualEventFactory: Factory = (ref, props) => <TextualEvent ref={ref} {...props} />;
|
||||
const VerificationReqFactory: Factory = (ref, props) => <MKeyVerificationRequest ref={ref} {...props} />;
|
||||
|
@ -89,7 +89,7 @@ const EVENT_TILE_TYPES: FactoryMap = {
|
|||
[M_POLL_START.altName]: MessageEventFactory,
|
||||
[EventType.KeyVerificationCancel]: KeyVerificationConclFactory,
|
||||
[EventType.KeyVerificationDone]: KeyVerificationConclFactory,
|
||||
[EventType.CallInvite]: CallEventFactory, // note that this requires a special factory type
|
||||
[EventType.CallInvite]: LegacyCallEventFactory, // note that this requires a special factory type
|
||||
};
|
||||
|
||||
const STATE_EVENT_TILE_TYPES: FactoryMap = {
|
||||
|
|
46
src/hooks/useCall.ts
Normal file
46
src/hooks/useCall.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import type { Call, ConnectionState } from "../models/Call";
|
||||
import { useTypedEventEmitterState } from "./useEventEmitter";
|
||||
import { CallEvent } from "../models/Call";
|
||||
import { CallStore, CallStoreEvent } from "../stores/CallStore";
|
||||
import { useEventEmitter } from "./useEventEmitter";
|
||||
|
||||
export const useCall = (roomId: string): Call | null => {
|
||||
const [call, setCall] = useState(() => CallStore.instance.get(roomId));
|
||||
useEventEmitter(CallStore.instance, CallStoreEvent.Call, (call: Call | null, forRoomId: string) => {
|
||||
if (forRoomId === roomId) setCall(call);
|
||||
});
|
||||
return call;
|
||||
};
|
||||
|
||||
export const useConnectionState = (call: Call): ConnectionState =>
|
||||
useTypedEventEmitterState(
|
||||
call,
|
||||
CallEvent.ConnectionState,
|
||||
useCallback(state => state ?? call.connectionState, [call]),
|
||||
);
|
||||
|
||||
export const useParticipants = (call: Call): Set<RoomMember> =>
|
||||
useTypedEventEmitterState(
|
||||
call,
|
||||
CallEvent.Participants,
|
||||
useCallback(state => state ?? call.participants, [call]),
|
||||
);
|
|
@ -16,40 +16,6 @@
|
|||
"Error": "Error",
|
||||
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
|
||||
"Dismiss": "Dismiss",
|
||||
"Call Failed": "Call Failed",
|
||||
"User Busy": "User Busy",
|
||||
"The user you called is busy.": "The user you called is busy.",
|
||||
"The call could not be established": "The call could not be established",
|
||||
"Answered Elsewhere": "Answered Elsewhere",
|
||||
"The call was answered on another device.": "The call was answered on another device.",
|
||||
"Call failed due to misconfigured server": "Call failed due to misconfigured server",
|
||||
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.",
|
||||
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.",
|
||||
"Try using turn.matrix.org": "Try using turn.matrix.org",
|
||||
"OK": "OK",
|
||||
"Unable to access microphone": "Unable to access microphone",
|
||||
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.",
|
||||
"Unable to access webcam / microphone": "Unable to access webcam / microphone",
|
||||
"Call failed because webcam or microphone could not be accessed. Check that:": "Call failed because webcam or microphone could not be accessed. Check that:",
|
||||
"A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly",
|
||||
"Permission is granted to use the webcam": "Permission is granted to use the webcam",
|
||||
"No other application is using the webcam": "No other application is using the webcam",
|
||||
"Already in call": "Already in call",
|
||||
"You're already in a call with this person.": "You're already in a call with this person.",
|
||||
"Calls are unsupported": "Calls are unsupported",
|
||||
"You cannot place calls in this browser.": "You cannot place calls in this browser.",
|
||||
"Connectivity to the server has been lost": "Connectivity to the server has been lost",
|
||||
"You cannot place calls without a connection to the server.": "You cannot place calls without a connection to the server.",
|
||||
"Too Many Calls": "Too Many Calls",
|
||||
"You've reached the maximum number of simultaneous calls.": "You've reached the maximum number of simultaneous calls.",
|
||||
"You cannot place a call with yourself.": "You cannot place a call with yourself.",
|
||||
"Unable to look up phone number": "Unable to look up phone number",
|
||||
"There was an error looking up the phone number": "There was an error looking up the phone number",
|
||||
"Unable to transfer call": "Unable to transfer call",
|
||||
"Transfer Failed": "Transfer Failed",
|
||||
"Failed to transfer call": "Failed to transfer call",
|
||||
"Permission Required": "Permission Required",
|
||||
"You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room",
|
||||
"The file '%(fileName)s' failed to upload.": "The file '%(fileName)s' failed to upload.",
|
||||
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "The file '%(fileName)s' exceeds this homeserver's size limit for uploads",
|
||||
"Upload Failed": "Upload Failed",
|
||||
|
@ -92,6 +58,40 @@
|
|||
"This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.",
|
||||
"Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.",
|
||||
"Trust": "Trust",
|
||||
"Call Failed": "Call Failed",
|
||||
"User Busy": "User Busy",
|
||||
"The user you called is busy.": "The user you called is busy.",
|
||||
"The call could not be established": "The call could not be established",
|
||||
"Answered Elsewhere": "Answered Elsewhere",
|
||||
"The call was answered on another device.": "The call was answered on another device.",
|
||||
"Call failed due to misconfigured server": "Call failed due to misconfigured server",
|
||||
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.",
|
||||
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.",
|
||||
"Try using turn.matrix.org": "Try using turn.matrix.org",
|
||||
"OK": "OK",
|
||||
"Unable to access microphone": "Unable to access microphone",
|
||||
"Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.",
|
||||
"Unable to access webcam / microphone": "Unable to access webcam / microphone",
|
||||
"Call failed because webcam or microphone could not be accessed. Check that:": "Call failed because webcam or microphone could not be accessed. Check that:",
|
||||
"A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly",
|
||||
"Permission is granted to use the webcam": "Permission is granted to use the webcam",
|
||||
"No other application is using the webcam": "No other application is using the webcam",
|
||||
"Already in call": "Already in call",
|
||||
"You're already in a call with this person.": "You're already in a call with this person.",
|
||||
"Calls are unsupported": "Calls are unsupported",
|
||||
"You cannot place calls in this browser.": "You cannot place calls in this browser.",
|
||||
"Connectivity to the server has been lost": "Connectivity to the server has been lost",
|
||||
"You cannot place calls without a connection to the server.": "You cannot place calls without a connection to the server.",
|
||||
"Too Many Calls": "Too Many Calls",
|
||||
"You've reached the maximum number of simultaneous calls.": "You've reached the maximum number of simultaneous calls.",
|
||||
"You cannot place a call with yourself.": "You cannot place a call with yourself.",
|
||||
"Unable to look up phone number": "Unable to look up phone number",
|
||||
"There was an error looking up the phone number": "There was an error looking up the phone number",
|
||||
"Unable to transfer call": "Unable to transfer call",
|
||||
"Transfer Failed": "Transfer Failed",
|
||||
"Failed to transfer call": "Failed to transfer call",
|
||||
"Permission Required": "Permission Required",
|
||||
"You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room",
|
||||
"We couldn't log you in": "We couldn't log you in",
|
||||
"We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.",
|
||||
"Try again": "Try again",
|
||||
|
@ -1039,6 +1039,18 @@
|
|||
"You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?",
|
||||
"Hint: Begin your message with <code>//</code> to start it with a slash.": "Hint: Begin your message with <code>//</code> to start it with a slash.",
|
||||
"Send as message": "Send as message",
|
||||
"%(count)s people joined|other": "%(count)s people joined",
|
||||
"%(count)s people joined|one": "%(count)s person joined",
|
||||
"Audio devices": "Audio devices",
|
||||
"Audio input %(n)s": "Audio input %(n)s",
|
||||
"Mute microphone": "Mute microphone",
|
||||
"Unmute microphone": "Unmute microphone",
|
||||
"Video devices": "Video devices",
|
||||
"Video input %(n)s": "Video input %(n)s",
|
||||
"Turn off camera": "Turn off camera",
|
||||
"Turn on camera": "Turn on camera",
|
||||
"Join": "Join",
|
||||
"Dial": "Dial",
|
||||
"You are presenting": "You are presenting",
|
||||
"%(sharerName)s is presenting": "%(sharerName)s is presenting",
|
||||
"Your camera is turned off": "Your camera is turned off",
|
||||
|
@ -1049,16 +1061,6 @@
|
|||
"You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
|
||||
"%(peerName)s held the call": "%(peerName)s held the call",
|
||||
"Connecting": "Connecting",
|
||||
"Dial": "Dial",
|
||||
"%(count)s people joined|other": "%(count)s people joined",
|
||||
"%(count)s people joined|one": "%(count)s person joined",
|
||||
"Audio devices": "Audio devices",
|
||||
"Mute microphone": "Mute microphone",
|
||||
"Unmute microphone": "Unmute microphone",
|
||||
"Video devices": "Video devices",
|
||||
"Turn off camera": "Turn off camera",
|
||||
"Turn on camera": "Turn on camera",
|
||||
"Join": "Join",
|
||||
"Dialpad": "Dialpad",
|
||||
"Mute the microphone": "Mute the microphone",
|
||||
"Unmute the microphone": "Unmute the microphone",
|
||||
|
@ -1972,6 +1974,11 @@
|
|||
"%(count)s unread messages.|other": "%(count)s unread messages.",
|
||||
"%(count)s unread messages.|one": "1 unread message.",
|
||||
"Unread messages.": "Unread messages.",
|
||||
"Video": "Video",
|
||||
"Joining…": "Joining…",
|
||||
"Joined": "Joined",
|
||||
"%(count)s participants|other": "%(count)s participants",
|
||||
"%(count)s participants|one": "1 participant",
|
||||
"Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.",
|
||||
"This room has already been upgraded.": "This room has already been upgraded.",
|
||||
"This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.": "This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.",
|
||||
|
@ -1993,11 +2000,6 @@
|
|||
"Open thread": "Open thread",
|
||||
"Jump to first unread message.": "Jump to first unread message.",
|
||||
"Mark all as read": "Mark all as read",
|
||||
"Video": "Video",
|
||||
"Joining…": "Joining…",
|
||||
"Joined": "Joined",
|
||||
"%(count)s participants|other": "%(count)s participants",
|
||||
"%(count)s participants|one": "1 participant",
|
||||
"Unable to access your microphone": "Unable to access your microphone",
|
||||
"We were unable to access your microphone. Please check your browser settings and try again.": "We were unable to access your microphone. Please check your browser settings and try again.",
|
||||
"No microphone found": "No microphone found",
|
||||
|
@ -2155,17 +2157,6 @@
|
|||
"%(displayName)s cancelled verification.": "%(displayName)s cancelled verification.",
|
||||
"You cancelled verification.": "You cancelled verification.",
|
||||
"Verification cancelled": "Verification cancelled",
|
||||
"Call declined": "Call declined",
|
||||
"Call back": "Call back",
|
||||
"No answer": "No answer",
|
||||
"Could not connect media": "Could not connect media",
|
||||
"Connection failed": "Connection failed",
|
||||
"Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone",
|
||||
"An unknown error occurred": "An unknown error occurred",
|
||||
"Unknown failure: %(reason)s": "Unknown failure: %(reason)s",
|
||||
"Retry": "Retry",
|
||||
"Missed call": "Missed call",
|
||||
"The call is in an unknown state!": "The call is in an unknown state!",
|
||||
"Sunday": "Sunday",
|
||||
"Monday": "Monday",
|
||||
"Tuesday": "Tuesday",
|
||||
|
@ -2196,6 +2187,17 @@
|
|||
"Message pending moderation": "Message pending moderation",
|
||||
"Pick a date to jump to": "Pick a date to jump to",
|
||||
"Go": "Go",
|
||||
"Call declined": "Call declined",
|
||||
"Call back": "Call back",
|
||||
"No answer": "No answer",
|
||||
"Could not connect media": "Could not connect media",
|
||||
"Connection failed": "Connection failed",
|
||||
"Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone",
|
||||
"An unknown error occurred": "An unknown error occurred",
|
||||
"Unknown failure: %(reason)s": "Unknown failure: %(reason)s",
|
||||
"Retry": "Retry",
|
||||
"Missed call": "Missed call",
|
||||
"The call is in an unknown state!": "The call is in an unknown state!",
|
||||
"Error processing audio message": "Error processing audio message",
|
||||
"View live location": "View live location",
|
||||
"React": "React",
|
||||
|
@ -3018,11 +3020,11 @@
|
|||
"Observe only": "Observe only",
|
||||
"No verification requests found": "No verification requests found",
|
||||
"There was an error finding this widget.": "There was an error finding this widget.",
|
||||
"Resume": "Resume",
|
||||
"Hold": "Hold",
|
||||
"Input devices": "Input devices",
|
||||
"Output devices": "Output devices",
|
||||
"Cameras": "Cameras",
|
||||
"Resume": "Resume",
|
||||
"Hold": "Hold",
|
||||
"Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)",
|
||||
"Open in OpenStreetMap": "Open in OpenStreetMap",
|
||||
"Forward": "Forward",
|
||||
|
|
539
src/models/Call.ts
Normal file
539
src/models/Call.ts
Normal file
|
@ -0,0 +1,539 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { IWidgetApiRequest } from "matrix-widget-api";
|
||||
|
||||
import type EventEmitter from "events";
|
||||
import type { IMyDevice } from "matrix-js-sdk/src/client";
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||
import type { IApp } from "../stores/WidgetStore";
|
||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../MediaDeviceHandler";
|
||||
import { timeout } from "../utils/promise";
|
||||
import WidgetUtils from "../utils/WidgetUtils";
|
||||
import { WidgetType } from "../widgets/WidgetType";
|
||||
import { ElementWidgetActions } from "../stores/widgets/ElementWidgetActions";
|
||||
import WidgetStore from "../stores/WidgetStore";
|
||||
import { WidgetMessagingStore, WidgetMessagingStoreEvent } from "../stores/widgets/WidgetMessagingStore";
|
||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../stores/ActiveWidgetStore";
|
||||
|
||||
const TIMEOUT_MS = 16000;
|
||||
|
||||
// Waits until an event is emitted satisfying the given predicate
|
||||
const waitForEvent = async (emitter: EventEmitter, event: string, pred: (...args) => boolean = () => true) => {
|
||||
let listener: (...args) => void;
|
||||
const wait = new Promise<void>(resolve => {
|
||||
listener = (...args) => { if (pred(...args)) resolve(); };
|
||||
emitter.on(event, listener);
|
||||
});
|
||||
|
||||
const timedOut = await timeout(wait, false, TIMEOUT_MS) === false;
|
||||
emitter.off(event, listener);
|
||||
if (timedOut) throw new Error("Timed out");
|
||||
};
|
||||
|
||||
export enum ConnectionState {
|
||||
Disconnected = "disconnected",
|
||||
Connecting = "connecting",
|
||||
Connected = "connected",
|
||||
Disconnecting = "disconnecting",
|
||||
}
|
||||
|
||||
export const isConnected = (state: ConnectionState): boolean =>
|
||||
state === ConnectionState.Connected || state === ConnectionState.Disconnecting;
|
||||
|
||||
export enum CallEvent {
|
||||
ConnectionState = "connection_state",
|
||||
Participants = "participants",
|
||||
Destroy = "destroy",
|
||||
}
|
||||
|
||||
interface CallEventHandlerMap {
|
||||
[CallEvent.ConnectionState]: (state: ConnectionState, prevState: ConnectionState) => void;
|
||||
[CallEvent.Participants]: (participants: Set<RoomMember>) => void;
|
||||
[CallEvent.Destroy]: () => void;
|
||||
}
|
||||
|
||||
interface JitsiCallMemberContent {
|
||||
// Connected device IDs
|
||||
devices: string[];
|
||||
// Time at which this state event should be considered stale
|
||||
expires_ts: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A group call accessed through a widget.
|
||||
*/
|
||||
export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandlerMap> {
|
||||
protected readonly widgetUid = WidgetUtils.getWidgetUid(this.widget);
|
||||
|
||||
private _messaging: ClientWidgetApi | null = null;
|
||||
/**
|
||||
* The widget's messaging, or null if disconnected.
|
||||
*/
|
||||
protected get messaging(): ClientWidgetApi | null {
|
||||
return this._messaging;
|
||||
}
|
||||
private set messaging(value: ClientWidgetApi | null) {
|
||||
this._messaging = value;
|
||||
}
|
||||
|
||||
public get roomId(): string {
|
||||
return this.widget.roomId;
|
||||
}
|
||||
|
||||
private _connectionState: ConnectionState = ConnectionState.Disconnected;
|
||||
public get connectionState(): ConnectionState {
|
||||
return this._connectionState;
|
||||
}
|
||||
protected set connectionState(value: ConnectionState) {
|
||||
const prevValue = this._connectionState;
|
||||
this._connectionState = value;
|
||||
this.emit(CallEvent.ConnectionState, value, prevValue);
|
||||
}
|
||||
|
||||
public get connected(): boolean {
|
||||
return isConnected(this.connectionState);
|
||||
}
|
||||
|
||||
private _participants = new Set<RoomMember>();
|
||||
public get participants(): Set<RoomMember> {
|
||||
return this._participants;
|
||||
}
|
||||
protected set participants(value: Set<RoomMember>) {
|
||||
this._participants = value;
|
||||
this.emit(CallEvent.Participants, value);
|
||||
}
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* The widget used to access this call.
|
||||
*/
|
||||
public readonly widget: IApp,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the call associated with the given room, if any.
|
||||
* @param {Room} room The room.
|
||||
* @returns {Call | null} The call.
|
||||
*/
|
||||
public static get(room: Room): Call | null {
|
||||
// There's currently only one implementation
|
||||
return JitsiCall.get(room);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a routine check of the call's associated room state, cleaning up
|
||||
* any data left over from an unclean disconnection.
|
||||
*/
|
||||
public abstract clean(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Contacts the widget to connect to the call.
|
||||
* @param {MediaDeviceInfo | null} audioDevice The audio input to use, or
|
||||
* null to start muted.
|
||||
* @param {MediaDeviceInfo | null} audioDevice The video input to use, or
|
||||
* null to start muted.
|
||||
*/
|
||||
protected abstract performConnection(
|
||||
audioInput: MediaDeviceInfo | null,
|
||||
videoInput: MediaDeviceInfo | null,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Contacts the widget to disconnect from the call.
|
||||
*/
|
||||
protected abstract performDisconnection(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Connects the user to the call using the media devices set in
|
||||
* MediaDeviceHandler. The widget associated with the call must be active
|
||||
* for this to succeed.
|
||||
*/
|
||||
public async connect(): Promise<void> {
|
||||
this.connectionState = ConnectionState.Connecting;
|
||||
|
||||
const {
|
||||
[MediaDeviceKindEnum.AudioInput]: audioInputs,
|
||||
[MediaDeviceKindEnum.VideoInput]: videoInputs,
|
||||
} = await MediaDeviceHandler.getDevices();
|
||||
|
||||
let audioInput: MediaDeviceInfo | null = null;
|
||||
if (!MediaDeviceHandler.startWithAudioMuted) {
|
||||
const deviceId = MediaDeviceHandler.getAudioInput();
|
||||
audioInput = audioInputs.find(d => d.deviceId === deviceId) ?? audioInputs[0] ?? null;
|
||||
}
|
||||
let videoInput: MediaDeviceInfo | null = null;
|
||||
if (!MediaDeviceHandler.startWithVideoMuted) {
|
||||
const deviceId = MediaDeviceHandler.getVideoInput();
|
||||
videoInput = videoInputs.find(d => d.deviceId === deviceId) ?? videoInputs[0] ?? null;
|
||||
}
|
||||
|
||||
const messagingStore = WidgetMessagingStore.instance;
|
||||
this.messaging = messagingStore.getMessagingForUid(this.widgetUid);
|
||||
if (!this.messaging) {
|
||||
// The widget might still be initializing, so wait for it
|
||||
try {
|
||||
await waitForEvent(
|
||||
messagingStore,
|
||||
WidgetMessagingStoreEvent.StoreMessaging,
|
||||
(uid: string, widgetApi: ClientWidgetApi) => {
|
||||
if (uid === this.widgetUid) {
|
||||
this.messaging = widgetApi;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to bind call widget in room ${this.roomId}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.performConnection(audioInput, videoInput);
|
||||
} catch (e) {
|
||||
this.connectionState = ConnectionState.Disconnected;
|
||||
throw e;
|
||||
}
|
||||
|
||||
this.connectionState = ConnectionState.Connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the user from the call.
|
||||
*/
|
||||
public async disconnect(): Promise<void> {
|
||||
if (this.connectionState !== ConnectionState.Connected) throw new Error("Not connected");
|
||||
|
||||
this.connectionState = ConnectionState.Disconnecting;
|
||||
await this.performDisconnection();
|
||||
this.setDisconnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually marks the call as disconnected and cleans up.
|
||||
*/
|
||||
public setDisconnected() {
|
||||
this.messaging = null;
|
||||
this.connectionState = ConnectionState.Disconnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all internal timers and tasks to prepare for garbage collection.
|
||||
*/
|
||||
public destroy() {
|
||||
if (this.connected) this.setDisconnected();
|
||||
this.emit(CallEvent.Destroy);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A group call using Jitsi as a backend.
|
||||
*/
|
||||
export class JitsiCall extends Call {
|
||||
public static readonly MEMBER_EVENT_TYPE = "io.element.video.member";
|
||||
public static readonly STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour
|
||||
|
||||
private room: Room = this.client.getRoom(this.roomId)!;
|
||||
private resendDevicesTimer: number | null = null;
|
||||
private participantsExpirationTimer: number | null = null;
|
||||
|
||||
private constructor(widget: IApp, private readonly client: MatrixClient) {
|
||||
super(widget);
|
||||
|
||||
this.room.on(RoomStateEvent.Update, this.onRoomState);
|
||||
this.on(CallEvent.ConnectionState, this.onConnectionState);
|
||||
this.updateParticipants();
|
||||
}
|
||||
|
||||
public static get(room: Room): JitsiCall | null {
|
||||
const apps = WidgetStore.instance.getApps(room.roomId);
|
||||
// The isVideoChannel field differentiates rich Jitsi calls from bare Jitsi widgets
|
||||
const jitsiWidget = apps.find(app => WidgetType.JITSI.matches(app.type) && app.data?.isVideoChannel);
|
||||
return jitsiWidget ? new JitsiCall(jitsiWidget, room.client) : null;
|
||||
}
|
||||
|
||||
public static async create(room: Room): Promise<void> {
|
||||
await WidgetUtils.addJitsiWidget(room.roomId, CallType.Video, "Group call", true, room.name);
|
||||
}
|
||||
|
||||
private updateParticipants() {
|
||||
if (this.participantsExpirationTimer !== null) {
|
||||
clearTimeout(this.participantsExpirationTimer);
|
||||
this.participantsExpirationTimer = null;
|
||||
}
|
||||
|
||||
const members = new Set<RoomMember>();
|
||||
const now = Date.now();
|
||||
let allExpireAt = Infinity;
|
||||
|
||||
for (const e of this.room.currentState.getStateEvents(JitsiCall.MEMBER_EVENT_TYPE)) {
|
||||
const member = this.room.getMember(e.getStateKey()!);
|
||||
const content = e.getContent<JitsiCallMemberContent>();
|
||||
let devices = Array.isArray(content.devices) ? content.devices : [];
|
||||
const expiresAt = typeof content.expires_ts === "number" ? content.expires_ts : -Infinity;
|
||||
|
||||
// Apply local echo for the disconnected case
|
||||
if (!this.connected && member?.userId === this.client.getUserId()) {
|
||||
devices = devices.filter(d => d !== this.client.getDeviceId());
|
||||
}
|
||||
// Must have a connected device, be unexpired, and still be joined to the room
|
||||
if (devices.length && expiresAt > now && member?.membership === "join") {
|
||||
members.add(member);
|
||||
if (expiresAt < allExpireAt) allExpireAt = expiresAt;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply local echo for the connected case
|
||||
if (this.connected) members.add(this.room.getMember(this.client.getUserId()!)!);
|
||||
|
||||
this.participants = members;
|
||||
if (allExpireAt < Infinity) {
|
||||
this.participantsExpirationTimer = setTimeout(() => this.updateParticipants(), allExpireAt - now);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method that updates our member state with the devices returned by
|
||||
// the given function. If it returns null, the update is skipped.
|
||||
private async updateDevices(fn: (devices: string[]) => (string[] | null)): Promise<void> {
|
||||
if (this.room.getMyMembership() !== "join") return;
|
||||
|
||||
const devicesState = this.room.currentState.getStateEvents(
|
||||
JitsiCall.MEMBER_EVENT_TYPE, this.client.getUserId()!,
|
||||
);
|
||||
const devices = devicesState?.getContent<JitsiCallMemberContent>().devices ?? [];
|
||||
const newDevices = fn(devices);
|
||||
|
||||
if (newDevices) {
|
||||
const content: JitsiCallMemberContent = {
|
||||
devices: newDevices,
|
||||
expires_ts: Date.now() + JitsiCall.STUCK_DEVICE_TIMEOUT_MS,
|
||||
};
|
||||
|
||||
await this.client.sendStateEvent(
|
||||
this.roomId, JitsiCall.MEMBER_EVENT_TYPE, content, this.client.getUserId()!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async addOurDevice(): Promise<void> {
|
||||
await this.updateDevices(devices => Array.from(new Set(devices).add(this.client.getDeviceId())));
|
||||
}
|
||||
|
||||
private async removeOurDevice(): Promise<void> {
|
||||
await this.updateDevices(devices => {
|
||||
const devicesSet = new Set(devices);
|
||||
devicesSet.delete(this.client.getDeviceId());
|
||||
return Array.from(devicesSet);
|
||||
});
|
||||
}
|
||||
|
||||
public async clean(): Promise<void> {
|
||||
const now = Date.now();
|
||||
const { devices: myDevices } = await this.client.getDevices();
|
||||
const deviceMap = new Map<string, IMyDevice>(myDevices.map(d => [d.device_id, d]));
|
||||
|
||||
// Clean up our member state by filtering out logged out devices,
|
||||
// inactive devices, and our own device (if we're disconnected)
|
||||
await this.updateDevices(devices => {
|
||||
const newDevices = devices.filter(d => {
|
||||
const device = deviceMap.get(d);
|
||||
return device?.last_seen_ts
|
||||
&& !(d === this.client.getDeviceId() && !this.connected)
|
||||
&& (now - device.last_seen_ts) < JitsiCall.STUCK_DEVICE_TIMEOUT_MS;
|
||||
});
|
||||
|
||||
// Skip the update if the devices are unchanged
|
||||
return newDevices.length === devices.length ? null : newDevices;
|
||||
});
|
||||
}
|
||||
|
||||
protected async performConnection(
|
||||
audioInput: MediaDeviceInfo | null,
|
||||
videoInput: MediaDeviceInfo | null,
|
||||
): Promise<void> {
|
||||
// Ensure that the messaging doesn't get stopped while we're waiting for responses
|
||||
const dontStopMessaging = new Promise<void>((resolve, reject) => {
|
||||
const messagingStore = WidgetMessagingStore.instance;
|
||||
|
||||
const listener = (uid: string) => {
|
||||
if (uid === this.widgetUid) {
|
||||
cleanup();
|
||||
reject(new Error("Messaging stopped"));
|
||||
}
|
||||
};
|
||||
const done = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const cleanup = () => {
|
||||
messagingStore.off(WidgetMessagingStoreEvent.StopMessaging, listener);
|
||||
this.off(CallEvent.ConnectionState, done);
|
||||
};
|
||||
|
||||
messagingStore.on(WidgetMessagingStoreEvent.StopMessaging, listener);
|
||||
this.on(CallEvent.ConnectionState, done);
|
||||
});
|
||||
|
||||
// Empirically, it's possible for Jitsi Meet to crash instantly at startup,
|
||||
// sending a hangup event that races with the rest of this method, so we need
|
||||
// to add the hangup listener now rather than later
|
||||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
|
||||
// Actually perform the join
|
||||
const response = waitForEvent(
|
||||
this.messaging!,
|
||||
`action:${ElementWidgetActions.JoinCall}`,
|
||||
(ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
return true;
|
||||
},
|
||||
);
|
||||
const request = this.messaging!.transport.send(ElementWidgetActions.JoinCall, {
|
||||
audioInput: audioInput?.label ?? null,
|
||||
videoInput: videoInput?.label ?? null,
|
||||
});
|
||||
try {
|
||||
await Promise.race([Promise.all([request, response]), dontStopMessaging]);
|
||||
} catch (e) {
|
||||
// If it timed out, clean up our advance preparations
|
||||
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
|
||||
if (this.messaging!.transport.ready) {
|
||||
// The messaging still exists, which means Jitsi might still be going in the background
|
||||
this.messaging!.transport.send(ElementWidgetActions.HangupCall, { force: true });
|
||||
}
|
||||
|
||||
throw new Error(`Failed to join call in room ${this.roomId}: ${e}`);
|
||||
}
|
||||
|
||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Dock, this.onDock);
|
||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Undock, this.onUndock);
|
||||
this.room.on(RoomEvent.MyMembership, this.onMyMembership);
|
||||
window.addEventListener("beforeunload", this.beforeUnload);
|
||||
}
|
||||
|
||||
protected async performDisconnection(): Promise<void> {
|
||||
const response = waitForEvent(
|
||||
this.messaging!,
|
||||
`action:${ElementWidgetActions.HangupCall}`,
|
||||
(ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
return true;
|
||||
},
|
||||
);
|
||||
const request = this.messaging!.transport.send(ElementWidgetActions.HangupCall, {});
|
||||
try {
|
||||
await Promise.all([request, response]);
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to hangup call in room ${this.roomId}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
public setDisconnected() {
|
||||
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Dock, this.onDock);
|
||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onUndock);
|
||||
this.room.off(RoomEvent.MyMembership, this.onMyMembership);
|
||||
window.removeEventListener("beforeunload", this.beforeUnload);
|
||||
|
||||
super.setDisconnected();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.room.off(RoomStateEvent.Update, this.updateParticipants);
|
||||
this.on(CallEvent.ConnectionState, this.onConnectionState);
|
||||
if (this.participantsExpirationTimer !== null) {
|
||||
clearTimeout(this.participantsExpirationTimer);
|
||||
this.participantsExpirationTimer = null;
|
||||
}
|
||||
if (this.resendDevicesTimer !== null) {
|
||||
clearInterval(this.resendDevicesTimer);
|
||||
this.resendDevicesTimer = null;
|
||||
}
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
private onRoomState = () => this.updateParticipants();
|
||||
|
||||
private onConnectionState = async (state: ConnectionState, prevState: ConnectionState) => {
|
||||
if (state === ConnectionState.Connected && prevState === ConnectionState.Connecting) {
|
||||
this.updateParticipants();
|
||||
|
||||
// Tell others that we're connected, by adding our device to room state
|
||||
await this.addOurDevice();
|
||||
// Re-add this device every so often so our video member event doesn't become stale
|
||||
this.resendDevicesTimer = setInterval(async () => {
|
||||
logger.log(`Resending video member event for ${this.roomId}`);
|
||||
await this.addOurDevice();
|
||||
}, (JitsiCall.STUCK_DEVICE_TIMEOUT_MS * 3) / 4);
|
||||
} else if (state === ConnectionState.Disconnected && isConnected(prevState)) {
|
||||
this.updateParticipants();
|
||||
|
||||
clearInterval(this.resendDevicesTimer);
|
||||
this.resendDevicesTimer = null;
|
||||
// Tell others that we're disconnected, by removing our device from room state
|
||||
await this.removeOurDevice();
|
||||
}
|
||||
};
|
||||
|
||||
private onDock = async () => {
|
||||
// The widget is no longer a PiP, so let's restore the default layout
|
||||
await this.messaging!.transport.send(ElementWidgetActions.TileLayout, {});
|
||||
};
|
||||
|
||||
private onUndock = async () => {
|
||||
// The widget has become a PiP, so let's switch Jitsi to spotlight mode
|
||||
// to only show the active speaker and economize on space
|
||||
await this.messaging!.transport.send(ElementWidgetActions.SpotlightLayout, {});
|
||||
};
|
||||
|
||||
private onMyMembership = async (room: Room, membership: string) => {
|
||||
if (membership !== "join") this.setDisconnected();
|
||||
};
|
||||
|
||||
private beforeUnload = () => this.setDisconnected();
|
||||
|
||||
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
// If we're already in the middle of a client-initiated disconnection,
|
||||
// ignore the event
|
||||
if (this.connectionState === ConnectionState.Disconnecting) return;
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
// In case this hangup is caused by Jitsi Meet crashing at startup,
|
||||
// wait for the connection event in order to avoid racing
|
||||
if (this.connectionState === ConnectionState.Connecting) {
|
||||
await waitForEvent(this, CallEvent.ConnectionState);
|
||||
}
|
||||
|
||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.setDisconnected();
|
||||
};
|
||||
}
|
|
@ -962,9 +962,9 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
default: false,
|
||||
},
|
||||
"videoChannelRoomId": {
|
||||
"activeCallRoomIds": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
default: null,
|
||||
default: [],
|
||||
},
|
||||
[UIFeature.RoomHistorySettings]: {
|
||||
supportedLevels: LEVELS_UI_FEATURE,
|
||||
|
|
|
@ -449,7 +449,12 @@ export default class SettingsStore {
|
|||
*/
|
||||
|
||||
/* eslint-enable valid-jsdoc */
|
||||
public static async setValue(settingName: string, roomId: string, level: SettingLevel, value: any): Promise<void> {
|
||||
public static async setValue(
|
||||
settingName: string,
|
||||
roomId: string | null,
|
||||
level: SettingLevel,
|
||||
value: any,
|
||||
): Promise<void> {
|
||||
// Verify that the setting is actually a setting
|
||||
const setting = SETTINGS[settingName];
|
||||
if (!setting) {
|
||||
|
|
|
@ -44,6 +44,10 @@ export abstract class AsyncStoreWithClient<T extends Object> extends AsyncStore<
|
|||
})(dispatcher);
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
await this.readyStore.start();
|
||||
}
|
||||
|
||||
get matrixClient(): MatrixClient {
|
||||
return this.readyStore.mxClient;
|
||||
}
|
||||
|
|
|
@ -45,7 +45,11 @@ interface IState {
|
|||
* reported.
|
||||
*/
|
||||
export default class AutoRageshakeStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new AutoRageshakeStore();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new AutoRageshakeStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher, {
|
||||
|
|
|
@ -37,7 +37,11 @@ interface IState {
|
|||
}
|
||||
|
||||
export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new BreadcrumbsStore();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new BreadcrumbsStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
private waitingRooms: { roomId: string, addedTs: number }[] = [];
|
||||
|
||||
|
|
185
src/stores/CallStore.ts
Normal file
185
src/stores/CallStore.ts
Normal file
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
|
||||
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import type { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import { UPDATE_EVENT } from "./AsyncStore";
|
||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||
import WidgetStore from "./WidgetStore";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
import { Call, CallEvent, ConnectionState } from "../models/Call";
|
||||
|
||||
export enum CallStoreEvent {
|
||||
// Signals a change in the call associated with a given room
|
||||
Call = "call",
|
||||
// Signals a change in the active calls
|
||||
ActiveCalls = "active_calls",
|
||||
}
|
||||
|
||||
export class CallStore extends AsyncStoreWithClient<{}> {
|
||||
private static _instance: CallStore;
|
||||
public static get instance(): CallStore {
|
||||
if (!this._instance) {
|
||||
this._instance = new CallStore();
|
||||
this._instance.start();
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher);
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload): Promise<void> {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
protected async onReady(): Promise<any> {
|
||||
// We assume that the calls present in a room are a function of room
|
||||
// state and room widgets, so we initialize the room map here and then
|
||||
// update it whenever those change
|
||||
for (const room of this.matrixClient.getRooms()) {
|
||||
this.updateRoom(room);
|
||||
}
|
||||
this.matrixClient.on(ClientEvent.Room, this.onRoom);
|
||||
this.matrixClient.on(RoomStateEvent.Events, this.onRoomState);
|
||||
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgets);
|
||||
|
||||
// If the room ID of a previously connected call is still in settings at
|
||||
// this time, that's a sign that we failed to disconnect from it
|
||||
// properly, and need to clean up after ourselves
|
||||
const uncleanlyDisconnectedRoomIds = SettingsStore.getValue<string[]>("activeCallRoomIds");
|
||||
if (uncleanlyDisconnectedRoomIds.length) {
|
||||
await Promise.all([
|
||||
...uncleanlyDisconnectedRoomIds.map(async uncleanlyDisconnectedRoomId => {
|
||||
logger.log(`Cleaning up call state for room ${uncleanlyDisconnectedRoomId}`);
|
||||
await this.get(uncleanlyDisconnectedRoomId)?.clean();
|
||||
}),
|
||||
SettingsStore.setValue("activeCallRoomIds", null, SettingLevel.DEVICE, []),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onNotReady(): Promise<any> {
|
||||
for (const [call, listenerMap] of this.callListeners) {
|
||||
// It's important that we remove the listeners before destroying the
|
||||
// call, because otherwise the call's onDestroy callback would fire
|
||||
// and immediately repopulate the map
|
||||
for (const [event, listener] of listenerMap) call.off(event, listener);
|
||||
call.destroy();
|
||||
}
|
||||
this.callListeners.clear();
|
||||
this.calls.clear();
|
||||
this.activeCalls = new Set();
|
||||
|
||||
this.matrixClient.off(ClientEvent.Room, this.onRoom);
|
||||
this.matrixClient.off(RoomStateEvent.Events, this.onRoomState);
|
||||
WidgetStore.instance.off(UPDATE_EVENT, this.onWidgets);
|
||||
}
|
||||
|
||||
private _activeCalls: Set<Call> = new Set();
|
||||
/**
|
||||
* The calls to which the user is currently connected.
|
||||
*/
|
||||
public get activeCalls(): Set<Call> {
|
||||
return this._activeCalls;
|
||||
}
|
||||
private set activeCalls(value: Set<Call>) {
|
||||
this._activeCalls = value;
|
||||
this.emit(CallStoreEvent.ActiveCalls, value);
|
||||
|
||||
// The room IDs are persisted to settings so we can detect unclean disconnects
|
||||
SettingsStore.setValue("activeCallRoomIds", null, SettingLevel.DEVICE, [...value].map(call => call.roomId));
|
||||
}
|
||||
|
||||
private calls = new Map<string, Call>(); // Key is room ID
|
||||
private callListeners = new Map<Call, Map<CallEvent, (...args: unknown[]) => unknown>>();
|
||||
|
||||
private updateRoom(room: Room) {
|
||||
if (!this.calls.has(room.roomId)) {
|
||||
const call = Call.get(room);
|
||||
|
||||
if (call) {
|
||||
const onConnectionState = (state: ConnectionState) => {
|
||||
if (state === ConnectionState.Connected) {
|
||||
this.activeCalls = new Set([...this.activeCalls, call]);
|
||||
} else if (state === ConnectionState.Disconnected) {
|
||||
this.activeCalls = new Set([...this.activeCalls].filter(c => c !== call));
|
||||
}
|
||||
};
|
||||
const onDestroy = () => {
|
||||
this.calls.delete(room.roomId);
|
||||
for (const [event, listener] of this.callListeners.get(call)!) call.off(event, listener);
|
||||
this.updateRoom(room);
|
||||
};
|
||||
|
||||
call.on(CallEvent.ConnectionState, onConnectionState);
|
||||
call.on(CallEvent.Destroy, onDestroy);
|
||||
|
||||
this.calls.set(room.roomId, call);
|
||||
this.callListeners.set(call, new Map<CallEvent, (...args: unknown[]) => unknown>([
|
||||
[CallEvent.ConnectionState, onConnectionState],
|
||||
[CallEvent.Destroy, onDestroy],
|
||||
]));
|
||||
}
|
||||
|
||||
this.emit(CallStoreEvent.Call, call, room.roomId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the call associated with the given room, if any.
|
||||
* @param {string} roomId The room's ID.
|
||||
* @returns {Call | null} The call.
|
||||
*/
|
||||
public get(roomId: string): Call | null {
|
||||
return this.calls.get(roomId) ?? null;
|
||||
}
|
||||
|
||||
private onRoom = (room: Room) => this.updateRoom(room);
|
||||
|
||||
private onRoomState = (event: MatrixEvent, state: RoomState) => {
|
||||
// If there's already a call stored for this room, it's understood to
|
||||
// still be valid until destroyed
|
||||
if (!this.calls.has(state.roomId)) {
|
||||
const room = this.matrixClient.getRoom(state.roomId);
|
||||
// State events can arrive before the room does, when creating a room
|
||||
if (room !== null) this.updateRoom(room);
|
||||
}
|
||||
};
|
||||
|
||||
private onWidgets = (roomId: string | null) => {
|
||||
if (roomId === null) {
|
||||
// This store happened to start before the widget store was done
|
||||
// loading all rooms, so we need to initialize each room again
|
||||
for (const room of this.matrixClient.getRooms()) {
|
||||
this.updateRoom(room);
|
||||
}
|
||||
} else {
|
||||
const room = this.matrixClient.getRoom(roomId);
|
||||
// Widget updates can arrive before the room does, empirically
|
||||
if (room !== null) this.updateRoom(room);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -30,10 +30,14 @@ interface IState {
|
|||
}
|
||||
|
||||
export class ModalWidgetStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new ModalWidgetStore();
|
||||
private modalInstance: IHandle<void[]> = null;
|
||||
private openSourceWidgetId: string = null;
|
||||
private openSourceWidgetRoomId: string = null;
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new ModalWidgetStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
private modalInstance: IHandle<void[]> | null = null;
|
||||
private openSourceWidgetId: string | null = null;
|
||||
private openSourceWidgetRoomId: string | null = null;
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher, {});
|
||||
|
|
|
@ -92,7 +92,11 @@ const getLocallyCreatedBeaconEventIds = (): string[] => {
|
|||
return ids;
|
||||
};
|
||||
export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||
private static internalInstance = new OwnBeaconStore();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new OwnBeaconStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
// users beacons, keyed by event type
|
||||
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
public readonly beaconsByRoomId = new Map<Room['roomId'], Set<BeaconIdentifier>>();
|
||||
|
|
|
@ -37,7 +37,11 @@ const KEY_DISPLAY_NAME = "mx_profile_displayname";
|
|||
const KEY_AVATAR_URL = "mx_profile_avatar_url";
|
||||
|
||||
export class OwnProfileStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new OwnProfileStore();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new OwnProfileStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
private monitoredUser: User;
|
||||
|
||||
|
|
|
@ -26,18 +26,19 @@ import { Action } from "../dispatcher/actions";
|
|||
|
||||
export abstract class ReadyWatchingStore extends EventEmitter implements IDestroyable {
|
||||
protected matrixClient: MatrixClient;
|
||||
private readonly dispatcherRef: string;
|
||||
private dispatcherRef: string | null = null;
|
||||
|
||||
constructor(protected readonly dispatcher: Dispatcher<ActionPayload>) {
|
||||
super();
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
this.dispatcherRef = this.dispatcher.register(this.onAction);
|
||||
|
||||
if (MatrixClientPeg.get()) {
|
||||
this.matrixClient = MatrixClientPeg.get();
|
||||
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.onReady();
|
||||
const matrixClient = MatrixClientPeg.get();
|
||||
if (matrixClient) {
|
||||
this.matrixClient = matrixClient;
|
||||
await this.onReady();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,7 +51,7 @@ export abstract class ReadyWatchingStore extends EventEmitter implements IDestro
|
|||
}
|
||||
|
||||
public destroy() {
|
||||
this.dispatcher.unregister(this.dispatcherRef);
|
||||
if (this.dispatcherRef !== null) this.dispatcher.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
protected async onReady() {
|
||||
|
|
|
@ -1,355 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { ClientWidgetApi, IWidgetApiRequest } from "matrix-widget-api";
|
||||
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import { ElementWidgetActions } from "./widgets/ElementWidgetActions";
|
||||
import { WidgetMessagingStore, WidgetMessagingStoreEvent } from "./widgets/WidgetMessagingStore";
|
||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "./ActiveWidgetStore";
|
||||
import { STUCK_DEVICE_TIMEOUT_MS, getVideoChannel, addOurDevice, removeOurDevice } from "../utils/VideoChannelUtils";
|
||||
import { timeout } from "../utils/promise";
|
||||
import WidgetUtils from "../utils/WidgetUtils";
|
||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||
|
||||
export enum VideoChannelEvent {
|
||||
StartConnect = "start_connect",
|
||||
Connect = "connect",
|
||||
Disconnect = "disconnect",
|
||||
Participants = "participants",
|
||||
}
|
||||
|
||||
export interface IJitsiParticipant {
|
||||
avatarURL: string;
|
||||
displayName: string;
|
||||
formattedDisplayName: string;
|
||||
participantId: string;
|
||||
}
|
||||
|
||||
const TIMEOUT_MS = 16000;
|
||||
|
||||
// Wait until an event is emitted satisfying the given predicate
|
||||
const waitForEvent = async (emitter: EventEmitter, event: string, pred: (...args) => boolean = () => true) => {
|
||||
let listener;
|
||||
const wait = new Promise<void>(resolve => {
|
||||
listener = (...args) => { if (pred(...args)) resolve(); };
|
||||
emitter.on(event, listener);
|
||||
});
|
||||
|
||||
const timedOut = await timeout(wait, false, TIMEOUT_MS) === false;
|
||||
emitter.off(event, listener);
|
||||
if (timedOut) throw new Error("Timed out");
|
||||
};
|
||||
|
||||
/*
|
||||
* Holds information about the currently active video channel.
|
||||
*/
|
||||
export default class VideoChannelStore extends AsyncStoreWithClient<null> {
|
||||
private static _instance: VideoChannelStore;
|
||||
|
||||
public static get instance(): VideoChannelStore {
|
||||
if (!VideoChannelStore._instance) {
|
||||
VideoChannelStore._instance = new VideoChannelStore();
|
||||
}
|
||||
return VideoChannelStore._instance;
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
super(defaultDispatcher);
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload): Promise<void> {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
private activeChannel: ClientWidgetApi;
|
||||
private resendDevicesTimer: number;
|
||||
|
||||
// This is persisted to settings so we can detect unclean disconnects
|
||||
public get roomId(): string | null { return SettingsStore.getValue("videoChannelRoomId"); }
|
||||
private set roomId(value: string | null) {
|
||||
SettingsStore.setValue("videoChannelRoomId", null, SettingLevel.DEVICE, value);
|
||||
}
|
||||
|
||||
private get room(): Room { return this.matrixClient.getRoom(this.roomId); }
|
||||
|
||||
private _connected = false;
|
||||
public get connected(): boolean { return this._connected; }
|
||||
private set connected(value: boolean) { this._connected = value; }
|
||||
|
||||
private _participants: IJitsiParticipant[] = [];
|
||||
public get participants(): IJitsiParticipant[] { return this._participants; }
|
||||
private set participants(value: IJitsiParticipant[]) { this._participants = value; }
|
||||
|
||||
public get audioMuted(): boolean { return SettingsStore.getValue("audioInputMuted"); }
|
||||
public set audioMuted(value: boolean) {
|
||||
SettingsStore.setValue("audioInputMuted", null, SettingLevel.DEVICE, value);
|
||||
}
|
||||
|
||||
public get videoMuted(): boolean { return SettingsStore.getValue("videoInputMuted"); }
|
||||
public set videoMuted(value: boolean) {
|
||||
SettingsStore.setValue("videoInputMuted", null, SettingLevel.DEVICE, value);
|
||||
}
|
||||
|
||||
public connect = async (
|
||||
roomId: string,
|
||||
audioDevice: MediaDeviceInfo | null,
|
||||
videoDevice: MediaDeviceInfo | null,
|
||||
) => {
|
||||
if (this.activeChannel) await this.disconnect();
|
||||
|
||||
const jitsi = getVideoChannel(roomId);
|
||||
if (!jitsi) throw new Error(`No video channel in room ${roomId}`);
|
||||
|
||||
const jitsiUid = WidgetUtils.getWidgetUid(jitsi);
|
||||
const messagingStore = WidgetMessagingStore.instance;
|
||||
|
||||
let messaging = messagingStore.getMessagingForUid(jitsiUid);
|
||||
if (!messaging) {
|
||||
// The widget might still be initializing, so wait for it
|
||||
try {
|
||||
await waitForEvent(
|
||||
messagingStore,
|
||||
WidgetMessagingStoreEvent.StoreMessaging,
|
||||
(uid: string, widgetApi: ClientWidgetApi) => {
|
||||
if (uid === jitsiUid) {
|
||||
messaging = widgetApi;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to bind video channel in room ${roomId}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we got the messaging, we need a way to ensure that it doesn't get stopped
|
||||
const dontStopMessaging = new Promise<void>((resolve, reject) => {
|
||||
const listener = (uid: string) => {
|
||||
if (uid === jitsiUid) {
|
||||
cleanup();
|
||||
reject(new Error("Messaging stopped"));
|
||||
}
|
||||
};
|
||||
const done = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const cleanup = () => {
|
||||
messagingStore.off(WidgetMessagingStoreEvent.StopMessaging, listener);
|
||||
this.off(VideoChannelEvent.Connect, done);
|
||||
this.off(VideoChannelEvent.Disconnect, done);
|
||||
};
|
||||
|
||||
messagingStore.on(WidgetMessagingStoreEvent.StopMessaging, listener);
|
||||
this.on(VideoChannelEvent.Connect, done);
|
||||
this.on(VideoChannelEvent.Disconnect, done);
|
||||
});
|
||||
|
||||
if (!messagingStore.isWidgetReady(jitsiUid)) {
|
||||
// Wait for the widget to be ready to receive our join event
|
||||
try {
|
||||
await Promise.race([
|
||||
waitForEvent(
|
||||
messagingStore,
|
||||
WidgetMessagingStoreEvent.WidgetReady,
|
||||
(uid: string) => uid === jitsiUid,
|
||||
),
|
||||
dontStopMessaging,
|
||||
]);
|
||||
} catch (e) {
|
||||
throw new Error(`Video channel in room ${roomId} never became ready: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Participant data and mute state will come down the event pipeline quickly, so prepare in advance
|
||||
this.activeChannel = messaging;
|
||||
this.roomId = roomId;
|
||||
messaging.on(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
|
||||
messaging.on(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio);
|
||||
messaging.on(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
|
||||
messaging.on(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
|
||||
messaging.on(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
|
||||
// Empirically, it's possible for Jitsi Meet to crash instantly at startup,
|
||||
// sending a hangup event that races with the rest of this method, so we also
|
||||
// need to add the hangup listener now rather than later
|
||||
messaging.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
|
||||
this.emit(VideoChannelEvent.StartConnect, roomId);
|
||||
|
||||
// Actually perform the join
|
||||
const waitForJoin = waitForEvent(
|
||||
messaging,
|
||||
`action:${ElementWidgetActions.JoinCall}`,
|
||||
(ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.ack(ev);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
messaging.transport.send(ElementWidgetActions.JoinCall, {
|
||||
audioDevice: audioDevice?.label ?? null,
|
||||
videoDevice: videoDevice?.label ?? null,
|
||||
});
|
||||
try {
|
||||
await Promise.race([waitForJoin, dontStopMessaging]);
|
||||
} catch (e) {
|
||||
// If it timed out, clean up our advance preparations
|
||||
this.activeChannel = null;
|
||||
this.roomId = null;
|
||||
|
||||
messaging.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
|
||||
messaging.off(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio);
|
||||
messaging.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
|
||||
messaging.off(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
|
||||
messaging.off(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
|
||||
messaging.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
|
||||
if (messaging.transport.ready) {
|
||||
// The messaging still exists, which means Jitsi might still be going in the background
|
||||
messaging.transport.send(ElementWidgetActions.ForceHangupCall, {});
|
||||
}
|
||||
|
||||
this.emit(VideoChannelEvent.Disconnect, roomId);
|
||||
|
||||
throw new Error(`Failed to join call in room ${roomId}: ${e}`);
|
||||
}
|
||||
|
||||
this.connected = true;
|
||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Dock, this.onDock);
|
||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Undock, this.onUndock);
|
||||
this.room.on(RoomEvent.MyMembership, this.onMyMembership);
|
||||
window.addEventListener("beforeunload", this.setDisconnected);
|
||||
|
||||
this.emit(VideoChannelEvent.Connect, roomId);
|
||||
|
||||
// Tell others that we're connected, by adding our device to room state
|
||||
await addOurDevice(this.room);
|
||||
// Re-add this device every so often so our video member event doesn't become stale
|
||||
this.resendDevicesTimer = setInterval(async () => {
|
||||
logger.log(`Resending video member event for ${this.roomId}`);
|
||||
await addOurDevice(this.room);
|
||||
}, (STUCK_DEVICE_TIMEOUT_MS * 3) / 4);
|
||||
};
|
||||
|
||||
public disconnect = async () => {
|
||||
if (!this.activeChannel) throw new Error("Not connected to any video channel");
|
||||
|
||||
const waitForDisconnect = waitForEvent(this, VideoChannelEvent.Disconnect);
|
||||
this.activeChannel.transport.send(ElementWidgetActions.HangupCall, {});
|
||||
try {
|
||||
await waitForDisconnect; // onHangup cleans up for us
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to hangup call in room ${this.roomId}: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
public setDisconnected = async () => {
|
||||
const roomId = this.roomId;
|
||||
const room = this.room;
|
||||
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio);
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
|
||||
this.activeChannel.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Dock, this.onDock);
|
||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onUndock);
|
||||
room.off(RoomEvent.MyMembership, this.onMyMembership);
|
||||
window.removeEventListener("beforeunload", this.setDisconnected);
|
||||
clearInterval(this.resendDevicesTimer);
|
||||
|
||||
this.activeChannel = null;
|
||||
this.roomId = null;
|
||||
this.connected = false;
|
||||
this.participants = [];
|
||||
|
||||
this.emit(VideoChannelEvent.Disconnect, roomId);
|
||||
|
||||
// Tell others that we're disconnected, by removing our device from room state
|
||||
await removeOurDevice(room);
|
||||
};
|
||||
|
||||
private ack = (ev: CustomEvent<IWidgetApiRequest>, messaging = this.activeChannel) => {
|
||||
// Even if we don't have a reply to a given widget action, we still need
|
||||
// to give the widget API something to acknowledge receipt
|
||||
messaging.transport.reply(ev.detail, {});
|
||||
};
|
||||
|
||||
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
const messaging = this.activeChannel;
|
||||
// In case this hangup is caused by Jitsi Meet crashing at startup,
|
||||
// wait for the connection event in order to avoid racing
|
||||
if (!this.connected) await waitForEvent(this, VideoChannelEvent.Connect);
|
||||
await this.setDisconnected();
|
||||
this.ack(ev, messaging);
|
||||
};
|
||||
|
||||
private onParticipants = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.participants = ev.detail.data.participants as IJitsiParticipant[];
|
||||
this.emit(VideoChannelEvent.Participants, this.roomId, ev.detail.data.participants);
|
||||
this.ack(ev);
|
||||
};
|
||||
|
||||
private onMuteAudio = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.audioMuted = true;
|
||||
this.ack(ev);
|
||||
};
|
||||
|
||||
private onUnmuteAudio = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.audioMuted = false;
|
||||
this.ack(ev);
|
||||
};
|
||||
|
||||
private onMuteVideo = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.videoMuted = true;
|
||||
this.ack(ev);
|
||||
};
|
||||
|
||||
private onUnmuteVideo = (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.videoMuted = false;
|
||||
this.ack(ev);
|
||||
};
|
||||
|
||||
private onMyMembership = (room: Room, membership: string) => {
|
||||
if (membership !== "join") this.setDisconnected();
|
||||
};
|
||||
|
||||
private onDock = async () => {
|
||||
// The widget is no longer a PiP, so let's restore the default layout
|
||||
await this.activeChannel.transport.send(ElementWidgetActions.TileLayout, {});
|
||||
};
|
||||
|
||||
private onUndock = async () => {
|
||||
// The widget has become a PiP, so let's switch Jitsi to spotlight mode
|
||||
// to only show the active speaker and economize on space
|
||||
await this.activeChannel.transport.send(ElementWidgetActions.SpotlightLayout, {});
|
||||
};
|
||||
}
|
|
@ -33,10 +33,11 @@ export class VoiceRecordingStore extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
public static get instance(): VoiceRecordingStore {
|
||||
if (!VoiceRecordingStore.internalInstance) {
|
||||
VoiceRecordingStore.internalInstance = new VoiceRecordingStore();
|
||||
if (!this.internalInstance) {
|
||||
this.internalInstance = new VoiceRecordingStore();
|
||||
this.internalInstance.start();
|
||||
}
|
||||
return VoiceRecordingStore.internalInstance;
|
||||
return this.internalInstance;
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload): Promise<void> {
|
||||
|
|
|
@ -36,7 +36,7 @@ export interface IApp extends IWidget {
|
|||
roomId: string;
|
||||
eventId: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
avatar_url: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
|
||||
avatar_url?: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
|
||||
}
|
||||
|
||||
interface IRoomWidgets {
|
||||
|
@ -46,7 +46,11 @@ interface IRoomWidgets {
|
|||
// TODO consolidate WidgetEchoStore into this
|
||||
// TODO consolidate ActiveWidgetStore into this
|
||||
export default class WidgetStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new WidgetStore();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new WidgetStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
private widgetMap = new Map<string, IApp>(); // Key is widget Unique ID (UID)
|
||||
private roomMap = new Map<string, IRoomWidgets>(); // Key is room ID
|
||||
|
|
|
@ -44,10 +44,11 @@ export class EchoStore extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
public static get instance(): EchoStore {
|
||||
if (!EchoStore._instance) {
|
||||
EchoStore._instance = new EchoStore();
|
||||
if (!this._instance) {
|
||||
this._instance = new EchoStore();
|
||||
this._instance.start();
|
||||
}
|
||||
return EchoStore._instance;
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
public get contexts(): EchoContext[] {
|
||||
|
|
|
@ -34,7 +34,11 @@ interface IState {}
|
|||
export const UPDATE_STATUS_INDICATOR = Symbol("update-status-indicator");
|
||||
|
||||
export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new RoomNotificationStateStore();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new RoomNotificationStateStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
private roomMap = new Map<Room, RoomNotificationState>();
|
||||
private roomThreadsMap = new Map<Room, ThreadsRoomNotificationState>();
|
||||
|
|
|
@ -394,10 +394,11 @@ export default class RightPanelStore extends ReadyWatchingStore {
|
|||
}
|
||||
|
||||
public static get instance(): RightPanelStore {
|
||||
if (!RightPanelStore.internalInstance) {
|
||||
RightPanelStore.internalInstance = new RightPanelStore();
|
||||
if (!this.internalInstance) {
|
||||
this.internalInstance = new RightPanelStore();
|
||||
this.internalInstance.start();
|
||||
}
|
||||
return RightPanelStore.internalInstance;
|
||||
return this.internalInstance;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,9 +25,9 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
|
|||
import { MessageEventPreview } from "./previews/MessageEventPreview";
|
||||
import { PollStartEventPreview } from "./previews/PollStartEventPreview";
|
||||
import { TagID } from "./models";
|
||||
import { CallInviteEventPreview } from "./previews/CallInviteEventPreview";
|
||||
import { CallAnswerEventPreview } from "./previews/CallAnswerEventPreview";
|
||||
import { CallHangupEvent } from "./previews/CallHangupEvent";
|
||||
import { LegacyCallInviteEventPreview } from "./previews/LegacyCallInviteEventPreview";
|
||||
import { LegacyCallAnswerEventPreview } from "./previews/LegacyCallAnswerEventPreview";
|
||||
import { LegacyCallHangupEvent } from "./previews/LegacyCallHangupEvent";
|
||||
import { StickerEventPreview } from "./previews/StickerEventPreview";
|
||||
import { ReactionEventPreview } from "./previews/ReactionEventPreview";
|
||||
import { UPDATE_EVENT } from "../AsyncStore";
|
||||
|
@ -47,15 +47,15 @@ const PREVIEWS: Record<string, {
|
|||
},
|
||||
'm.call.invite': {
|
||||
isState: false,
|
||||
previewer: new CallInviteEventPreview(),
|
||||
previewer: new LegacyCallInviteEventPreview(),
|
||||
},
|
||||
'm.call.answer': {
|
||||
isState: false,
|
||||
previewer: new CallAnswerEventPreview(),
|
||||
previewer: new LegacyCallAnswerEventPreview(),
|
||||
},
|
||||
'm.call.hangup': {
|
||||
isState: false,
|
||||
previewer: new CallHangupEvent(),
|
||||
previewer: new LegacyCallHangupEvent(),
|
||||
},
|
||||
'm.sticker': {
|
||||
isState: false,
|
||||
|
@ -87,7 +87,11 @@ interface IState {
|
|||
}
|
||||
|
||||
export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance = new MessagePreviewStore();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new MessagePreviewStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
// null indicates the preview is empty / irrelevant
|
||||
private previews = new Map<string, Map<TagID|TAG_ANY, string|null>>();
|
||||
|
|
|
@ -34,8 +34,9 @@ export default class RoomListLayoutStore extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
public static get instance(): RoomListLayoutStore {
|
||||
if (!RoomListLayoutStore.internalInstance) {
|
||||
RoomListLayoutStore.internalInstance = new RoomListLayoutStore();
|
||||
if (!this.internalInstance) {
|
||||
this.internalInstance = new RoomListLayoutStore();
|
||||
this.internalInstance.start();
|
||||
}
|
||||
return RoomListLayoutStore.internalInstance;
|
||||
}
|
||||
|
|
|
@ -602,11 +602,13 @@ export default class RoomListStore {
|
|||
private static internalInstance: Interface;
|
||||
|
||||
public static get instance(): Interface {
|
||||
if (!RoomListStore.internalInstance) {
|
||||
RoomListStore.internalInstance = new RoomListStoreClass();
|
||||
if (!this.internalInstance) {
|
||||
const instance = new RoomListStoreClass();
|
||||
instance.start();
|
||||
this.internalInstance = instance;
|
||||
}
|
||||
|
||||
return RoomListStore.internalInstance;
|
||||
return this.internalInstance;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } f
|
|||
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
|
||||
import { getListAlgorithmInstance } from "./list-ordering";
|
||||
import { VisibilityProvider } from "../filters/VisibilityProvider";
|
||||
import VideoChannelStore, { VideoChannelEvent } from "../../VideoChannelStore";
|
||||
import { CallStore, CallStoreEvent } from "../../CallStore";
|
||||
|
||||
/**
|
||||
* Fired when the Algorithm has determined a list has been updated.
|
||||
|
@ -82,13 +82,11 @@ export class Algorithm extends EventEmitter {
|
|||
public updatesInhibited = false;
|
||||
|
||||
public start() {
|
||||
VideoChannelStore.instance.on(VideoChannelEvent.Connect, this.updateVideoRoom);
|
||||
VideoChannelStore.instance.on(VideoChannelEvent.Disconnect, this.updateVideoRoom);
|
||||
CallStore.instance.on(CallStoreEvent.ActiveCalls, this.onActiveCalls);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
VideoChannelStore.instance.off(VideoChannelEvent.Connect, this.updateVideoRoom);
|
||||
VideoChannelStore.instance.off(VideoChannelEvent.Disconnect, this.updateVideoRoom);
|
||||
CallStore.instance.off(CallStoreEvent.ActiveCalls, this.onActiveCalls);
|
||||
}
|
||||
|
||||
public get stickyRoom(): Room {
|
||||
|
@ -106,7 +104,7 @@ export class Algorithm extends EventEmitter {
|
|||
protected set cachedRooms(val: ITagMap) {
|
||||
this._cachedRooms = val;
|
||||
this.recalculateStickyRoom();
|
||||
this.recalculateVideoRoom();
|
||||
this.recalculateActiveCallRooms();
|
||||
}
|
||||
|
||||
protected get cachedRooms(): ITagMap {
|
||||
|
@ -143,7 +141,7 @@ export class Algorithm extends EventEmitter {
|
|||
algorithm.setSortAlgorithm(sort);
|
||||
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
||||
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
||||
this.recalculateVideoRoom(tagId);
|
||||
this.recalculateActiveCallRooms(tagId);
|
||||
}
|
||||
|
||||
public getListOrdering(tagId: TagID): ListAlgorithm {
|
||||
|
@ -162,7 +160,7 @@ export class Algorithm extends EventEmitter {
|
|||
algorithm.setRooms(this._cachedRooms[tagId]);
|
||||
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
||||
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
||||
this.recalculateVideoRoom(tagId);
|
||||
this.recalculateActiveCallRooms(tagId);
|
||||
}
|
||||
|
||||
private updateStickyRoom(val: Room) {
|
||||
|
@ -279,22 +277,20 @@ export class Algorithm extends EventEmitter {
|
|||
// a room while filtering and it'll disappear. We don't update the filter earlier in
|
||||
// this function simply because we don't have to.
|
||||
this.recalculateStickyRoom();
|
||||
this.recalculateVideoRoom(tag);
|
||||
if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateVideoRoom(lastStickyRoom.tag);
|
||||
this.recalculateActiveCallRooms(tag);
|
||||
if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateActiveCallRooms(lastStickyRoom.tag);
|
||||
|
||||
// Finally, trigger an update
|
||||
if (this.updatesInhibited) return;
|
||||
this.emit(LIST_UPDATED_EVENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the stickiness of video rooms.
|
||||
*/
|
||||
public updateVideoRoom = () => {
|
||||
// In case we're unsticking a video room, sort it back into natural order
|
||||
private onActiveCalls = () => {
|
||||
// In case we're unsticking a room, sort it back into natural order
|
||||
this.recalculateStickyRoom();
|
||||
|
||||
this.recalculateVideoRoom();
|
||||
// Update the stickiness of rooms with calls
|
||||
this.recalculateActiveCallRooms();
|
||||
|
||||
if (this.updatesInhibited) return;
|
||||
// This isn't in response to any particular RoomListStore update,
|
||||
|
@ -358,16 +354,16 @@ export class Algorithm extends EventEmitter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Recalculate the position of any video rooms. If this is being called in relation to
|
||||
* a specific tag being updated, it should be given to this function to optimize
|
||||
* the call.
|
||||
* Recalculate the position of any rooms with calls. If this is being called in
|
||||
* relation to a specific tag being updated, it should be given to this function to
|
||||
* optimize the call.
|
||||
*
|
||||
* This expects to be called *after* the sticky rooms are updated, and sticks the
|
||||
* currently connected video room to the top of its tag.
|
||||
* room with the currently active call to the top of its tag.
|
||||
*
|
||||
* @param updatedTag The tag that was updated, if possible.
|
||||
*/
|
||||
protected recalculateVideoRoom(updatedTag: TagID = null): void {
|
||||
protected recalculateActiveCallRooms(updatedTag: TagID = null): void {
|
||||
if (!updatedTag) {
|
||||
// Assume all tags need updating
|
||||
// We're not modifying the map here, so can safely rely on the cached values
|
||||
|
@ -376,24 +372,26 @@ export class Algorithm extends EventEmitter {
|
|||
if (!tagId) {
|
||||
throw new Error("Unexpected recursion: falsy tag");
|
||||
}
|
||||
this.recalculateVideoRoom(tagId);
|
||||
this.recalculateActiveCallRooms(tagId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const videoRoomId = VideoChannelStore.instance.connected ? VideoChannelStore.instance.roomId : null;
|
||||
|
||||
if (videoRoomId) {
|
||||
// We operate directly on the sticky rooms map
|
||||
if (CallStore.instance.activeCalls.size) {
|
||||
// We operate on the sticky rooms map
|
||||
if (!this._cachedStickyRooms) this.initCachedStickyRooms();
|
||||
const rooms = this._cachedStickyRooms[updatedTag];
|
||||
const videoRoomIdxInTag = rooms.findIndex(r => r.roomId === videoRoomId);
|
||||
if (videoRoomIdxInTag < 0) return; // no-op
|
||||
|
||||
const videoRoom = rooms[videoRoomIdxInTag];
|
||||
rooms.splice(videoRoomIdxInTag, 1);
|
||||
rooms.unshift(videoRoom);
|
||||
this._cachedStickyRooms[updatedTag] = rooms; // re-set because references aren't always safe
|
||||
const activeRoomIds = new Set([...CallStore.instance.activeCalls].map(call => call.roomId));
|
||||
const activeRooms: Room[] = [];
|
||||
const inactiveRooms: Room[] = [];
|
||||
|
||||
for (const room of rooms) {
|
||||
(activeRoomIds.has(room.roomId) ? activeRooms : inactiveRooms).push(room);
|
||||
}
|
||||
|
||||
// Stick rooms with active calls to the top
|
||||
this._cachedStickyRooms[updatedTag] = [...activeRooms, ...inactiveRooms];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -666,7 +664,7 @@ export class Algorithm extends EventEmitter {
|
|||
algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
|
||||
this._cachedRooms[rmTag] = algorithm.orderedRooms;
|
||||
this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed
|
||||
this.recalculateVideoRoom(rmTag);
|
||||
this.recalculateActiveCallRooms(rmTag);
|
||||
}
|
||||
for (const addTag of diff.added) {
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
|
||||
|
@ -742,7 +740,7 @@ export class Algorithm extends EventEmitter {
|
|||
|
||||
// Flag that we've done something
|
||||
this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed
|
||||
this.recalculateVideoRoom(tag);
|
||||
this.recalculateActiveCallRooms(tag);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||
import { RoomListCustomisations } from "../../../customisations/RoomList";
|
||||
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
||||
import VoipUserMapper from "../../../VoipUserMapper";
|
||||
|
@ -44,7 +44,7 @@ export class VisibilityProvider {
|
|||
}
|
||||
|
||||
if (
|
||||
CallHandler.instance.getSupportsVirtualRooms() &&
|
||||
LegacyCallHandler.instance.getSupportsVirtualRooms() &&
|
||||
VoipUserMapper.sharedInstance().isVirtualRoom(room)
|
||||
) {
|
||||
return false;
|
||||
|
|
|
@ -21,7 +21,7 @@ import { TagID } from "../models";
|
|||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class CallAnswerEventPreview implements IPreview {
|
||||
export class LegacyCallAnswerEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
if (isSelf(event)) {
|
|
@ -21,7 +21,7 @@ import { TagID } from "../models";
|
|||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class CallHangupEvent implements IPreview {
|
||||
export class LegacyCallHangupEvent implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
if (isSelf(event)) {
|
|
@ -21,7 +21,7 @@ import { TagID } from "../models";
|
|||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
export class CallInviteEventPreview implements IPreview {
|
||||
export class LegacyCallInviteEventPreview implements IPreview {
|
||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||
if (isSelf(event)) {
|
|
@ -1284,7 +1284,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
|
||||
export default class SpaceStore {
|
||||
private static internalInstance = new SpaceStoreClass();
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new SpaceStoreClass();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
public static get instance(): SpaceStoreClass {
|
||||
return SpaceStore.internalInstance;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
* Copyright 2020-2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -17,13 +17,9 @@
|
|||
import { IWidgetApiRequest } from "matrix-widget-api";
|
||||
|
||||
export enum ElementWidgetActions {
|
||||
ClientReady = "im.vector.ready",
|
||||
WidgetReady = "io.element.widget_ready",
|
||||
|
||||
// All of these actions are currently specific to Jitsi
|
||||
JoinCall = "io.element.join",
|
||||
HangupCall = "im.vector.hangup",
|
||||
ForceHangupCall = "io.element.force_hangup",
|
||||
CallParticipants = "io.element.participants",
|
||||
MuteAudio = "io.element.mute_audio",
|
||||
UnmuteAudio = "io.element.unmute_audio",
|
||||
|
|
|
@ -114,10 +114,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
|||
}
|
||||
|
||||
public static get instance(): WidgetLayoutStore {
|
||||
if (!WidgetLayoutStore.internalInstance) {
|
||||
WidgetLayoutStore.internalInstance = new WidgetLayoutStore();
|
||||
if (!this.internalInstance) {
|
||||
this.internalInstance = new WidgetLayoutStore();
|
||||
this.internalInstance.start();
|
||||
}
|
||||
return WidgetLayoutStore.internalInstance;
|
||||
return this.internalInstance;
|
||||
}
|
||||
|
||||
public static emissionForRoom(room: Room): string {
|
||||
|
|
|
@ -14,9 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ClientWidgetApi, Widget, IWidgetApiRequest } from "matrix-widget-api";
|
||||
import { ClientWidgetApi, Widget } from "matrix-widget-api";
|
||||
|
||||
import { ElementWidgetActions } from "./ElementWidgetActions";
|
||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
|
@ -26,7 +25,6 @@ import WidgetUtils from "../../utils/WidgetUtils";
|
|||
export enum WidgetMessagingStoreEvent {
|
||||
StoreMessaging = "store_messaging",
|
||||
StopMessaging = "stop_messaging",
|
||||
WidgetReady = "widget_ready",
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -34,11 +32,14 @@ export enum WidgetMessagingStoreEvent {
|
|||
* going to be merged with a more complete WidgetStore, but for now it's
|
||||
* easiest to split this into a single place.
|
||||
*/
|
||||
export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
||||
private static internalInstance = new WidgetMessagingStore();
|
||||
export class WidgetMessagingStore extends AsyncStoreWithClient<{}> {
|
||||
private static readonly internalInstance = (() => {
|
||||
const instance = new WidgetMessagingStore();
|
||||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
private widgetMap = new EnhancedMap<string, ClientWidgetApi>(); // <widget UID, ClientWidgetAPi>
|
||||
private readyWidgets = new Set<string>(); // widgets that have sent a WidgetReady event
|
||||
|
||||
public constructor() {
|
||||
super(defaultDispatcher);
|
||||
|
@ -62,12 +63,6 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
|||
const uid = WidgetUtils.calcWidgetUid(widget.id, roomId);
|
||||
this.widgetMap.set(uid, widgetApi);
|
||||
|
||||
widgetApi.once(`action:${ElementWidgetActions.WidgetReady}`, (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
this.readyWidgets.add(uid);
|
||||
this.emit(WidgetMessagingStoreEvent.WidgetReady, uid);
|
||||
widgetApi.transport.reply(ev.detail, {}); // ack
|
||||
});
|
||||
|
||||
this.emit(WidgetMessagingStoreEvent.StoreMessaging, uid, widgetApi);
|
||||
}
|
||||
|
||||
|
@ -85,7 +80,6 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
|||
*/
|
||||
public stopMessagingByUid(widgetUid: string) {
|
||||
this.widgetMap.remove(widgetUid)?.stop();
|
||||
this.readyWidgets.delete(widgetUid);
|
||||
this.emit(WidgetMessagingStoreEvent.StopMessaging, widgetUid);
|
||||
}
|
||||
|
||||
|
@ -97,12 +91,4 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
|||
public getMessagingForUid(widgetUid: string): ClientWidgetApi {
|
||||
return this.widgetMap.get(widgetUid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} widgetUid The widget UID.
|
||||
* @returns {boolean} Whether the widget has issued an ElementWidgetActions.WidgetReady event.
|
||||
*/
|
||||
public isWidgetReady(widgetUid: string): boolean {
|
||||
return this.readyWidgets.has(widgetUid);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,14 +21,14 @@ import React from 'react';
|
|||
import { CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import CallHandler, { CallHandlerEvent } from '../CallHandler';
|
||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../LegacyCallHandler';
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import { _t } from '../languageHandler';
|
||||
import RoomAvatar from '../components/views/avatars/RoomAvatar';
|
||||
import AccessibleTooltipButton from '../components/views/elements/AccessibleTooltipButton';
|
||||
import AccessibleButton from '../components/views/elements/AccessibleButton';
|
||||
|
||||
export const getIncomingCallToastKey = (callId: string) => `call_${callId}`;
|
||||
export const getIncomingLegacyCallToastKey = (callId: string) => `call_${callId}`;
|
||||
|
||||
interface IProps {
|
||||
call: MatrixCall;
|
||||
|
@ -38,83 +38,87 @@ interface IState {
|
|||
silenced: boolean;
|
||||
}
|
||||
|
||||
export default class IncomingCallToast extends React.Component<IProps, IState> {
|
||||
export default class IncomingLegacyCallToast extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
silenced: CallHandler.instance.isCallSilenced(this.props.call.callId),
|
||||
silenced: LegacyCallHandler.instance.isCallSilenced(this.props.call.callId),
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount = (): void => {
|
||||
CallHandler.instance.addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
||||
LegacyCallHandler.instance.addListener(
|
||||
LegacyCallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged,
|
||||
);
|
||||
};
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
CallHandler.instance.removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
||||
LegacyCallHandler.instance.removeListener(
|
||||
LegacyCallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged,
|
||||
);
|
||||
}
|
||||
|
||||
private onSilencedCallsChanged = (): void => {
|
||||
this.setState({ silenced: CallHandler.instance.isCallSilenced(this.props.call.callId) });
|
||||
this.setState({ silenced: LegacyCallHandler.instance.isCallSilenced(this.props.call.callId) });
|
||||
};
|
||||
|
||||
private onAnswerClick = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
CallHandler.instance.answerCall(CallHandler.instance.roomIdForCall(this.props.call));
|
||||
LegacyCallHandler.instance.answerCall(LegacyCallHandler.instance.roomIdForCall(this.props.call));
|
||||
};
|
||||
|
||||
private onRejectClick= (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
CallHandler.instance.hangupOrReject(CallHandler.instance.roomIdForCall(this.props.call), true);
|
||||
LegacyCallHandler.instance.hangupOrReject(LegacyCallHandler.instance.roomIdForCall(this.props.call), true);
|
||||
};
|
||||
|
||||
private onSilenceClick = (e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
const callId = this.props.call.callId;
|
||||
this.state.silenced ?
|
||||
CallHandler.instance.unSilenceCall(callId) :
|
||||
CallHandler.instance.silenceCall(callId);
|
||||
LegacyCallHandler.instance.unSilenceCall(callId) :
|
||||
LegacyCallHandler.instance.silenceCall(callId);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const call = this.props.call;
|
||||
const room = MatrixClientPeg.get().getRoom(CallHandler.instance.roomIdForCall(call));
|
||||
const room = MatrixClientPeg.get().getRoom(LegacyCallHandler.instance.roomIdForCall(call));
|
||||
const isVoice = call.type === CallType.Voice;
|
||||
|
||||
const contentClass = classNames("mx_IncomingCallToast_content", {
|
||||
"mx_IncomingCallToast_content_voice": isVoice,
|
||||
"mx_IncomingCallToast_content_video": !isVoice,
|
||||
const contentClass = classNames("mx_IncomingLegacyCallToast_content", {
|
||||
"mx_IncomingLegacyCallToast_content_voice": isVoice,
|
||||
"mx_IncomingLegacyCallToast_content_video": !isVoice,
|
||||
});
|
||||
const silenceClass = classNames("mx_IncomingCallToast_iconButton", {
|
||||
"mx_IncomingCallToast_unSilence": this.state.silenced,
|
||||
"mx_IncomingCallToast_silence": !this.state.silenced,
|
||||
const silenceClass = classNames("mx_IncomingLegacyCallToast_iconButton", {
|
||||
"mx_IncomingLegacyCallToast_unSilence": this.state.silenced,
|
||||
"mx_IncomingLegacyCallToast_silence": !this.state.silenced,
|
||||
});
|
||||
|
||||
return <React.Fragment>
|
||||
<RoomAvatar
|
||||
room={room}
|
||||
room={room ?? undefined}
|
||||
height={32}
|
||||
width={32}
|
||||
/>
|
||||
<div className={contentClass}>
|
||||
<span className="mx_CallEvent_caller">
|
||||
<span className="mx_LegacyCallEvent_caller">
|
||||
{ room ? room.name : _t("Unknown caller") }
|
||||
</span>
|
||||
<div className="mx_CallEvent_type">
|
||||
<div className="mx_CallEvent_type_icon" />
|
||||
<div className="mx_LegacyCallEvent_type">
|
||||
<div className="mx_LegacyCallEvent_type_icon" />
|
||||
{ isVoice ? _t("Voice call") : _t("Video call") }
|
||||
</div>
|
||||
<div className="mx_IncomingCallToast_buttons">
|
||||
<div className="mx_IncomingLegacyCallToast_buttons">
|
||||
<AccessibleButton
|
||||
className="mx_IncomingCallToast_button mx_IncomingCallToast_button_decline"
|
||||
className="mx_IncomingLegacyCallToast_button mx_IncomingLegacyCallToast_button_decline"
|
||||
onClick={this.onRejectClick}
|
||||
kind="danger"
|
||||
>
|
||||
<span> { _t("Decline") } </span>
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className="mx_IncomingCallToast_button mx_IncomingCallToast_button_accept"
|
||||
className="mx_IncomingLegacyCallToast_button mx_IncomingLegacyCallToast_button_accept"
|
||||
onClick={this.onAnswerClick}
|
||||
kind="primary"
|
||||
>
|
|
@ -1,204 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { throttle } from "lodash";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { IMyDevice } from "matrix-js-sdk/src/client";
|
||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import { useEventEmitter, useTypedEventEmitter } from "../hooks/useEventEmitter";
|
||||
import WidgetStore, { IApp } from "../stores/WidgetStore";
|
||||
import { WidgetType } from "../widgets/WidgetType";
|
||||
import WidgetUtils from "./WidgetUtils";
|
||||
import VideoChannelStore, { VideoChannelEvent, IJitsiParticipant } from "../stores/VideoChannelStore";
|
||||
|
||||
interface IVideoChannelMemberContent {
|
||||
// Connected device IDs
|
||||
devices: string[];
|
||||
// Time at which this state event should be considered stale
|
||||
expires_ts: number;
|
||||
}
|
||||
|
||||
export const VIDEO_CHANNEL_MEMBER = "io.element.video.member";
|
||||
export const STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour
|
||||
|
||||
export enum ConnectionState {
|
||||
Disconnected = "disconnected",
|
||||
Connecting = "connecting",
|
||||
Connected = "connected",
|
||||
}
|
||||
|
||||
export const getVideoChannel = (roomId: string): IApp => {
|
||||
const apps = WidgetStore.instance.getApps(roomId);
|
||||
return apps.find(app => WidgetType.JITSI.matches(app.type) && app.data.isVideoChannel);
|
||||
};
|
||||
|
||||
export const addVideoChannel = async (roomId: string, roomName: string) => {
|
||||
await WidgetUtils.addJitsiWidget(roomId, CallType.Video, "Video channel", true, roomName);
|
||||
};
|
||||
|
||||
// Gets the members connected to a given video room, along with a timestamp
|
||||
// indicating when this data should be considered stale
|
||||
const getConnectedMembers = (room: Room, connectedLocalEcho: boolean): [Set<RoomMember>, number] => {
|
||||
const members = new Set<RoomMember>();
|
||||
const now = Date.now();
|
||||
let allExpireAt = Infinity;
|
||||
|
||||
for (const e of room.currentState.getStateEvents(VIDEO_CHANNEL_MEMBER)) {
|
||||
const member = room.getMember(e.getStateKey());
|
||||
const content = e.getContent<IVideoChannelMemberContent>();
|
||||
let devices = Array.isArray(content.devices) ? content.devices : [];
|
||||
const expiresAt = typeof content.expires_ts === "number" ? content.expires_ts : -Infinity;
|
||||
|
||||
// Ignore events with a timeout that's way off in the future
|
||||
const inTheFuture = (expiresAt - ((STUCK_DEVICE_TIMEOUT_MS * 5) / 4)) > now;
|
||||
const expired = expiresAt <= now || inTheFuture;
|
||||
|
||||
// Apply local echo for the disconnected case
|
||||
if (!connectedLocalEcho && member?.userId === room.client.getUserId()) {
|
||||
devices = devices.filter(d => d !== room.client.getDeviceId());
|
||||
}
|
||||
// Must have a device connected, be unexpired, and still be joined to the room
|
||||
if (devices.length && !expired && member?.membership === "join") {
|
||||
members.add(member);
|
||||
if (expiresAt < allExpireAt) allExpireAt = expiresAt;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply local echo for the connected case
|
||||
if (connectedLocalEcho) members.add(room.getMember(room.client.getUserId()));
|
||||
return [members, allExpireAt];
|
||||
};
|
||||
|
||||
export const useConnectedMembers = (
|
||||
room: Room, connectedLocalEcho: boolean, throttleMs = 100,
|
||||
): Set<RoomMember> => {
|
||||
const [[members, expiresAt], setState] = useState(() => getConnectedMembers(room, connectedLocalEcho));
|
||||
const updateState = useMemo(() => throttle(() => {
|
||||
setState(getConnectedMembers(room, connectedLocalEcho));
|
||||
}, throttleMs, { leading: true, trailing: true }), [setState, room, connectedLocalEcho, throttleMs]);
|
||||
|
||||
useTypedEventEmitter(room.currentState, RoomStateEvent.Update, updateState);
|
||||
useEffect(() => {
|
||||
if (expiresAt < Infinity) {
|
||||
const timer = setTimeout(() => {
|
||||
logger.log(`Refreshing video members for ${room.roomId}`);
|
||||
updateState();
|
||||
}, expiresAt - Date.now());
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [expiresAt, updateState, room.roomId]);
|
||||
|
||||
return members;
|
||||
};
|
||||
|
||||
export const useJitsiParticipants = (room: Room): IJitsiParticipant[] => {
|
||||
const store = VideoChannelStore.instance;
|
||||
const [participants, setParticipants] = useState(() =>
|
||||
store.connected && store.roomId === room.roomId ? store.participants : [],
|
||||
);
|
||||
|
||||
useEventEmitter(store, VideoChannelEvent.Disconnect, (roomId: string) => {
|
||||
if (roomId === room.roomId) setParticipants([]);
|
||||
});
|
||||
useEventEmitter(store, VideoChannelEvent.Participants, (roomId: string, participants: IJitsiParticipant[]) => {
|
||||
if (roomId === room.roomId) setParticipants(participants);
|
||||
});
|
||||
|
||||
return participants;
|
||||
};
|
||||
|
||||
const updateDevices = async (room: Optional<Room>, fn: (devices: string[] | null) => string[]) => {
|
||||
if (room?.getMyMembership() !== "join") return;
|
||||
|
||||
const devicesState = room.currentState.getStateEvents(VIDEO_CHANNEL_MEMBER, room.client.getUserId());
|
||||
const devices = devicesState?.getContent<IVideoChannelMemberContent>()?.devices ?? [];
|
||||
const newDevices = fn(devices);
|
||||
|
||||
if (newDevices) {
|
||||
const content: IVideoChannelMemberContent = {
|
||||
devices: newDevices,
|
||||
expires_ts: Date.now() + STUCK_DEVICE_TIMEOUT_MS,
|
||||
};
|
||||
|
||||
await room.client.sendStateEvent(room.roomId, VIDEO_CHANNEL_MEMBER, content, room.client.getUserId());
|
||||
}
|
||||
};
|
||||
|
||||
export const addOurDevice = async (room: Room) => {
|
||||
await updateDevices(room, devices => Array.from(new Set(devices).add(room.client.getDeviceId())));
|
||||
};
|
||||
|
||||
export const removeOurDevice = async (room: Room) => {
|
||||
await updateDevices(room, devices => {
|
||||
const devicesSet = new Set(devices);
|
||||
devicesSet.delete(room.client.getDeviceId());
|
||||
return Array.from(devicesSet);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fixes devices that may have gotten stuck in video channel member state after
|
||||
* an unclean disconnection, by filtering out logged out devices, inactive
|
||||
* devices, and our own device (if we're disconnected).
|
||||
* @param {Room} room The room to fix
|
||||
* @param {boolean} connectedLocalEcho Local echo of whether this device is connected
|
||||
*/
|
||||
export const fixStuckDevices = async (room: Room, connectedLocalEcho: boolean) => {
|
||||
const now = Date.now();
|
||||
const { devices: myDevices } = await room.client.getDevices();
|
||||
const deviceMap = new Map<string, IMyDevice>(myDevices.map(d => [d.device_id, d]));
|
||||
|
||||
await updateDevices(room, devices => {
|
||||
const newDevices = devices.filter(d => {
|
||||
const device = deviceMap.get(d);
|
||||
return device?.last_seen_ts
|
||||
&& !(d === room.client.getDeviceId() && !connectedLocalEcho)
|
||||
&& (now - device.last_seen_ts) < STUCK_DEVICE_TIMEOUT_MS;
|
||||
});
|
||||
|
||||
// Skip the update if the devices are unchanged
|
||||
return newDevices.length === devices.length ? null : newDevices;
|
||||
});
|
||||
};
|
||||
|
||||
export const useConnectionState = (room: Room): ConnectionState => {
|
||||
const store = VideoChannelStore.instance;
|
||||
const [state, setState] = useState(() =>
|
||||
store.roomId === room.roomId
|
||||
? store.connected
|
||||
? ConnectionState.Connected
|
||||
: ConnectionState.Connecting
|
||||
: ConnectionState.Disconnected,
|
||||
);
|
||||
|
||||
useEventEmitter(store, VideoChannelEvent.Disconnect, (roomId: string) => {
|
||||
if (roomId === room.roomId) setState(ConnectionState.Disconnected);
|
||||
});
|
||||
useEventEmitter(store, VideoChannelEvent.StartConnect, (roomId: string) => {
|
||||
if (roomId === room.roomId) setState(ConnectionState.Connecting);
|
||||
});
|
||||
useEventEmitter(store, VideoChannelEvent.Connect, (roomId: string) => {
|
||||
if (roomId === room.roomId) setState(ConnectionState.Connected);
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
|
@ -19,9 +19,9 @@ import { CallEvent, CallState, CallType } from 'matrix-js-sdk/src/webrtc/call';
|
|||
import EventEmitter from 'events';
|
||||
import { mocked } from 'jest-mock';
|
||||
|
||||
import CallHandler, {
|
||||
CallHandlerEvent, PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED, PROTOCOL_SIP_NATIVE, PROTOCOL_SIP_VIRTUAL,
|
||||
} from '../src/CallHandler';
|
||||
import LegacyCallHandler, {
|
||||
LegacyCallHandlerEvent, PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED, PROTOCOL_SIP_NATIVE, PROTOCOL_SIP_VIRTUAL,
|
||||
} from '../src/LegacyCallHandler';
|
||||
import { stubClient, mkStubRoom, untilDispatch } from './test-utils';
|
||||
import { MatrixClientPeg } from '../src/MatrixClientPeg';
|
||||
import DMRoomMap from '../src/utils/DMRoomMap';
|
||||
|
@ -109,7 +109,7 @@ class FakeCall extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
function untilCallHandlerEvent(callHandler: CallHandler, event: CallHandlerEvent): Promise<void> {
|
||||
function untilCallHandlerEvent(callHandler: LegacyCallHandler, event: LegacyCallHandlerEvent): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
callHandler.addListener(event, () => {
|
||||
resolve();
|
||||
|
@ -117,7 +117,7 @@ function untilCallHandlerEvent(callHandler: CallHandler, event: CallHandlerEvent
|
|||
});
|
||||
}
|
||||
|
||||
describe('CallHandler', () => {
|
||||
describe('LegacyCallHandler', () => {
|
||||
let dmRoomMap;
|
||||
let callHandler;
|
||||
let audioElement: HTMLAudioElement;
|
||||
|
@ -145,7 +145,7 @@ describe('CallHandler', () => {
|
|||
});
|
||||
};
|
||||
|
||||
callHandler = new CallHandler();
|
||||
callHandler = new LegacyCallHandler();
|
||||
callHandler.start();
|
||||
|
||||
mocked(getFunctionalMembers).mockReturnValue([
|
||||
|
@ -251,7 +251,7 @@ describe('CallHandler', () => {
|
|||
callHandler.stop();
|
||||
DMRoomMap.setShared(null);
|
||||
// @ts-ignore
|
||||
window.mxCallHandler = null;
|
||||
window.mxLegacyCallHandler = null;
|
||||
fakeCall = null;
|
||||
MatrixClientPeg.unset();
|
||||
|
||||
|
@ -295,14 +295,14 @@ describe('CallHandler', () => {
|
|||
it('should move calls between rooms when remote asserted identity changes', async () => {
|
||||
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
|
||||
|
||||
await untilCallHandlerEvent(callHandler, CallHandlerEvent.CallState);
|
||||
await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState);
|
||||
|
||||
// We placed the call in Alice's room so it should start off there
|
||||
expect(callHandler.getCallForRoom(NATIVE_ROOM_ALICE)).toBe(fakeCall);
|
||||
|
||||
let callRoomChangeEventCount = 0;
|
||||
const roomChangePromise = new Promise<void>(resolve => {
|
||||
callHandler.addListener(CallHandlerEvent.CallChangeRoom, () => {
|
||||
callHandler.addListener(LegacyCallHandlerEvent.CallChangeRoom, () => {
|
||||
++callRoomChangeEventCount;
|
||||
resolve();
|
||||
});
|
||||
|
@ -343,9 +343,9 @@ describe('CallHandler', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('CallHandler without third party protocols', () => {
|
||||
describe('LegacyCallHandler without third party protocols', () => {
|
||||
let dmRoomMap;
|
||||
let callHandler: CallHandler;
|
||||
let callHandler: LegacyCallHandler;
|
||||
let audioElement: HTMLAudioElement;
|
||||
let fakeCall;
|
||||
|
||||
|
@ -363,7 +363,7 @@ describe('CallHandler without third party protocols', () => {
|
|||
throw new Error("Endpoint unsupported.");
|
||||
};
|
||||
|
||||
callHandler = new CallHandler();
|
||||
callHandler = new LegacyCallHandler();
|
||||
callHandler.start();
|
||||
|
||||
const nativeRoomAlice = mkStubDM(NATIVE_ROOM_ALICE, NATIVE_ALICE);
|
||||
|
@ -406,7 +406,7 @@ describe('CallHandler without third party protocols', () => {
|
|||
callHandler.stop();
|
||||
DMRoomMap.setShared(null);
|
||||
// @ts-ignore
|
||||
window.mxCallHandler = null;
|
||||
window.mxLegacyCallHandler = null;
|
||||
fakeCall = null;
|
||||
MatrixClientPeg.unset();
|
||||
|
||||
|
@ -417,7 +417,7 @@ describe('CallHandler without third party protocols', () => {
|
|||
it('should still start a native call', async () => {
|
||||
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
|
||||
|
||||
await untilCallHandlerEvent(callHandler, CallHandlerEvent.CallState);
|
||||
await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState);
|
||||
|
||||
// Check that a call was started: its room on the protocol level
|
||||
// should be the virtual room
|
|
@ -23,7 +23,7 @@ import { MatrixClientPeg } from '../src/MatrixClientPeg';
|
|||
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from '../src/models/LocalRoom';
|
||||
import { RoomViewStore } from '../src/stores/RoomViewStore';
|
||||
import SettingsStore from '../src/settings/SettingsStore';
|
||||
import CallHandler from '../src/CallHandler';
|
||||
import LegacyCallHandler from '../src/LegacyCallHandler';
|
||||
|
||||
describe('SlashCommands', () => {
|
||||
let client: MatrixClient;
|
||||
|
@ -120,7 +120,7 @@ describe('SlashCommands', () => {
|
|||
describe("isEnabled", () => {
|
||||
describe("when virtual rooms are supported", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(CallHandler.instance, "getSupportsVirtualRooms").mockReturnValue(true);
|
||||
jest.spyOn(LegacyCallHandler.instance, "getSupportsVirtualRooms").mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("should return true for Room", () => {
|
||||
|
@ -136,7 +136,7 @@ describe('SlashCommands', () => {
|
|||
|
||||
describe("when virtual rooms are not supported", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(CallHandler.instance, "getSupportsVirtualRooms").mockReturnValue(false);
|
||||
jest.spyOn(LegacyCallHandler.instance, "getSupportsVirtualRooms").mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should return false for Room", () => {
|
||||
|
|
|
@ -20,14 +20,14 @@ import { CallState } from "matrix-js-sdk/src/webrtc/call";
|
|||
|
||||
import { stubClient } from '../../test-utils';
|
||||
import { MatrixClientPeg } from '../../../src/MatrixClientPeg';
|
||||
import CallEventGrouper, { CustomCallState } from "../../../src/components/structures/CallEventGrouper";
|
||||
import LegacyCallEventGrouper, { CustomCallState } from "../../../src/components/structures/LegacyCallEventGrouper";
|
||||
|
||||
const MY_USER_ID = "@me:here";
|
||||
const THEIR_USER_ID = "@they:here";
|
||||
|
||||
let client: MatrixClient;
|
||||
|
||||
describe('CallEventGrouper', () => {
|
||||
describe('LegacyCallEventGrouper', () => {
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
client = MatrixClientPeg.get();
|
||||
|
@ -37,7 +37,7 @@ describe('CallEventGrouper', () => {
|
|||
});
|
||||
|
||||
it("detects a missed call", () => {
|
||||
const grouper = new CallEventGrouper();
|
||||
const grouper = new LegacyCallEventGrouper();
|
||||
|
||||
grouper.add({
|
||||
getContent: () => {
|
||||
|
@ -57,8 +57,8 @@ describe('CallEventGrouper', () => {
|
|||
});
|
||||
|
||||
it("detects an ended call", () => {
|
||||
const grouperHangup = new CallEventGrouper();
|
||||
const grouperReject = new CallEventGrouper();
|
||||
const grouperHangup = new LegacyCallEventGrouper();
|
||||
const grouperReject = new LegacyCallEventGrouper();
|
||||
|
||||
grouperHangup.add({
|
||||
getContent: () => {
|
||||
|
@ -119,7 +119,7 @@ describe('CallEventGrouper', () => {
|
|||
});
|
||||
|
||||
it("detects call type", () => {
|
||||
const grouper = new CallEventGrouper();
|
||||
const grouper = new LegacyCallEventGrouper();
|
||||
|
||||
grouper.add({
|
||||
getContent: () => {
|
|
@ -15,110 +15,103 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
// eslint-disable-next-line deprecate/import
|
||||
import { mount } from "enzyme";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, IMyDevice } from "matrix-js-sdk/src/client";
|
||||
import { render, screen, act, fireEvent, waitFor, cleanup } from "@testing-library/react";
|
||||
import { mocked, Mocked } from "jest-mock";
|
||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixWidgetType } from "matrix-widget-api";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { Widget } from "matrix-widget-api";
|
||||
|
||||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||
import type { Call } from "../../../src/models/Call";
|
||||
import {
|
||||
stubClient,
|
||||
stubVideoChannelStore,
|
||||
StubVideoChannelStore,
|
||||
mkRoom,
|
||||
mkRoomMember,
|
||||
wrapInMatrixClientContext,
|
||||
mockStateEventImplementation,
|
||||
mkVideoChannelMember,
|
||||
useMockedCalls,
|
||||
MockedCall,
|
||||
setupAsyncStoreWithClient,
|
||||
} from "../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import { VIDEO_CHANNEL_MEMBER } from "../../../src/utils/VideoChannelUtils";
|
||||
import WidgetStore from "../../../src/stores/WidgetStore";
|
||||
import _VideoRoomView from "../../../src/components/structures/VideoRoomView";
|
||||
import VideoLobby from "../../../src/components/views/voip/VideoLobby";
|
||||
import AppTile from "../../../src/components/views/elements/AppTile";
|
||||
import { VideoRoomView as UnwrappedVideoRoomView } from "../../../src/components/structures/VideoRoomView";
|
||||
import { WidgetMessagingStore } from "../../../src/stores/widgets/WidgetMessagingStore";
|
||||
import { CallStore } from "../../../src/stores/CallStore";
|
||||
import { ConnectionState } from "../../../src/models/Call";
|
||||
|
||||
const VideoRoomView = wrapInMatrixClientContext(_VideoRoomView);
|
||||
const VideoRoomView = wrapInMatrixClientContext(UnwrappedVideoRoomView);
|
||||
|
||||
describe("VideoRoomView", () => {
|
||||
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([{
|
||||
id: "1",
|
||||
eventId: "$1:example.org",
|
||||
roomId: "!1:example.org",
|
||||
type: MatrixWidgetType.JitsiMeet,
|
||||
url: "https://example.org",
|
||||
name: "Video channel",
|
||||
creatorUserId: "@alice:example.org",
|
||||
avatar_url: null,
|
||||
data: { isVideoChannel: true },
|
||||
}]);
|
||||
useMockedCalls();
|
||||
Object.defineProperty(navigator, "mediaDevices", {
|
||||
value: { enumerateDevices: () => [] },
|
||||
value: {
|
||||
enumerateDevices: async () => [],
|
||||
getUserMedia: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
let cli: MatrixClient;
|
||||
let client: Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
let store: StubVideoChannelStore;
|
||||
let call: Call;
|
||||
let widget: Widget;
|
||||
let alice: RoomMember;
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.get();
|
||||
jest.spyOn(WidgetStore.instance, "matrixClient", "get").mockReturnValue(cli);
|
||||
store = stubVideoChannelStore();
|
||||
room = mkRoom(cli, "!1:example.org");
|
||||
client = mocked(MatrixClientPeg.get());
|
||||
|
||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
alice = mkRoomMember(room.roomId, "@alice:example.org");
|
||||
jest.spyOn(room, "getMember").mockImplementation(userId => userId === alice.userId ? alice : null);
|
||||
|
||||
client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
|
||||
client.getRooms.mockReturnValue([room]);
|
||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||
|
||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||
|
||||
MockedCall.create(room, "1");
|
||||
call = CallStore.instance.get(room.roomId);
|
||||
if (call === null) throw new Error("Failed to create call");
|
||||
|
||||
widget = new Widget(call.widget);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
});
|
||||
|
||||
it("removes stuck devices on mount", async () => {
|
||||
// Simulate an unclean disconnect
|
||||
store.roomId = "!1:example.org";
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
call.destroy();
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
});
|
||||
|
||||
const devices: IMyDevice[] = [
|
||||
{
|
||||
device_id: cli.getDeviceId(),
|
||||
last_seen_ts: new Date().valueOf(),
|
||||
},
|
||||
{
|
||||
device_id: "went offline 2 hours ago",
|
||||
last_seen_ts: new Date().valueOf() - 1000 * 60 * 60 * 2,
|
||||
},
|
||||
];
|
||||
mocked(cli).getDevices.mockResolvedValue({ devices });
|
||||
const renderView = async (): Promise<void> => {
|
||||
render(<VideoRoomView room={room} resizing={false} />);
|
||||
await act(() => Promise.resolve()); // Let effects settle
|
||||
};
|
||||
|
||||
// Make both devices be stuck
|
||||
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
|
||||
mkVideoChannelMember(cli.getUserId(), devices.map(d => d.device_id)),
|
||||
]));
|
||||
|
||||
mount(<VideoRoomView room={room} resizing={false} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
|
||||
// All devices should have been removed
|
||||
expect(cli.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
"!1:example.org",
|
||||
VIDEO_CHANNEL_MEMBER,
|
||||
{ devices: [], expires_ts: expect.any(Number) },
|
||||
cli.getUserId(),
|
||||
);
|
||||
it("calls clean on mount", async () => {
|
||||
const cleanSpy = jest.spyOn(call, "clean");
|
||||
await renderView();
|
||||
expect(cleanSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows lobby and keeps widget loaded when disconnected", async () => {
|
||||
const view = mount(<VideoRoomView room={room} resizing={false} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
|
||||
expect(view.find(VideoLobby).exists()).toEqual(true);
|
||||
expect(view.find(AppTile).exists()).toEqual(true);
|
||||
await renderView();
|
||||
screen.getByRole("button", { name: "Join" });
|
||||
screen.getAllByText(/\bwidget\b/i);
|
||||
});
|
||||
|
||||
it("only shows widget when connected", async () => {
|
||||
store.connect("!1:example.org");
|
||||
const view = mount(<VideoRoomView room={room} resizing={false} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
|
||||
expect(view.find(VideoLobby).exists()).toEqual(false);
|
||||
expect(view.find(AppTile).exists()).toEqual(true);
|
||||
await renderView();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Join" }));
|
||||
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Connected));
|
||||
expect(screen.queryByRole("button", { name: "Join" })).toBe(null);
|
||||
screen.getAllByText(/\bwidget\b/i);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -93,7 +93,7 @@ describe("AppTile", () => {
|
|||
url: "https://example.com",
|
||||
name: "Example 1",
|
||||
creatorUserId: cli.getUserId(),
|
||||
avatar_url: null,
|
||||
avatar_url: undefined,
|
||||
};
|
||||
app2 = {
|
||||
id: "1",
|
||||
|
@ -103,7 +103,7 @@ describe("AppTile", () => {
|
|||
url: "https://example.com",
|
||||
name: "Example 2",
|
||||
creatorUserId: cli.getUserId(),
|
||||
avatar_url: null,
|
||||
avatar_url: undefined,
|
||||
};
|
||||
jest.spyOn(WidgetStore.instance, "getApps").mockImplementation(roomId => {
|
||||
if (roomId === "r1") return [app1];
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
exports[`<TooltipTarget /> displays Bottom aligned tooltip on mouseover 1`] = `
|
||||
<div
|
||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||
role="tooltip"
|
||||
style="display: block; top: 6px; left: 0px; transform: translate(-50%);"
|
||||
>
|
||||
<div
|
||||
|
@ -15,6 +16,7 @@ exports[`<TooltipTarget /> displays Bottom aligned tooltip on mouseover 1`] = `
|
|||
exports[`<TooltipTarget /> displays InnerBottom aligned tooltip on mouseover 1`] = `
|
||||
<div
|
||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||
role="tooltip"
|
||||
style="display: block; top: -50px; left: 0px; transform: translate(-50%);"
|
||||
>
|
||||
<div
|
||||
|
@ -27,6 +29,7 @@ exports[`<TooltipTarget /> displays InnerBottom aligned tooltip on mouseover 1`]
|
|||
exports[`<TooltipTarget /> displays Left aligned tooltip on mouseover 1`] = `
|
||||
<div
|
||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||
role="tooltip"
|
||||
style="display: block; right: 1030px; top: 0px; transform: translateY(-50%);"
|
||||
>
|
||||
<div
|
||||
|
@ -39,6 +42,7 @@ exports[`<TooltipTarget /> displays Left aligned tooltip on mouseover 1`] = `
|
|||
exports[`<TooltipTarget /> displays Natural aligned tooltip on mouseover 1`] = `
|
||||
<div
|
||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||
role="tooltip"
|
||||
style="display: block; left: 6px; top: 0px; transform: translateY(-50%);"
|
||||
>
|
||||
<div
|
||||
|
@ -51,6 +55,7 @@ exports[`<TooltipTarget /> displays Natural aligned tooltip on mouseover 1`] = `
|
|||
exports[`<TooltipTarget /> displays Right aligned tooltip on mouseover 1`] = `
|
||||
<div
|
||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||
role="tooltip"
|
||||
style="display: block; left: 6px; top: 0px; transform: translateY(-50%);"
|
||||
>
|
||||
<div
|
||||
|
@ -63,6 +68,7 @@ exports[`<TooltipTarget /> displays Right aligned tooltip on mouseover 1`] = `
|
|||
exports[`<TooltipTarget /> displays Top aligned tooltip on mouseover 1`] = `
|
||||
<div
|
||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||
role="tooltip"
|
||||
style="display: block; top: -6px; left: 0px; transform: translate(-50%, -100%);"
|
||||
>
|
||||
<div
|
||||
|
@ -75,6 +81,7 @@ exports[`<TooltipTarget /> displays Top aligned tooltip on mouseover 1`] = `
|
|||
exports[`<TooltipTarget /> displays TopRight aligned tooltip on mouseover 1`] = `
|
||||
<div
|
||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||
role="tooltip"
|
||||
style="display: block; top: -6px; right: 1024px; transform: translateY(-100%);"
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -15,148 +15,131 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
// eslint-disable-next-line deprecate/import
|
||||
import { mount } from "enzyme";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { render, screen, act } from "@testing-library/react";
|
||||
import { mocked, Mocked } from "jest-mock";
|
||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { Widget } from "matrix-widget-api";
|
||||
|
||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||
import {
|
||||
stubClient,
|
||||
mockStateEventImplementation,
|
||||
mkRoom,
|
||||
mkVideoChannelMember,
|
||||
stubVideoChannelStore,
|
||||
StubVideoChannelStore,
|
||||
mkRoomMember,
|
||||
MockedCall,
|
||||
useMockedCalls,
|
||||
setupAsyncStoreWithClient,
|
||||
} from "../../../test-utils";
|
||||
import { STUCK_DEVICE_TIMEOUT_MS } from "../../../../src/utils/VideoChannelUtils";
|
||||
import { CallStore } from "../../../../src/stores/CallStore";
|
||||
import RoomTile from "../../../../src/components/views/rooms/RoomTile";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import PlatformPeg from "../../../../src/PlatformPeg";
|
||||
import BasePlatform from "../../../../src/BasePlatform";
|
||||
|
||||
const mockGetMember = (room: Room, getMembership: (userId: string) => string = () => "join") => {
|
||||
mocked(room).getMember.mockImplementation(userId => ({
|
||||
userId,
|
||||
membership: getMembership(userId),
|
||||
name: userId,
|
||||
rawDisplayName: userId,
|
||||
roomId: "!1:example.org",
|
||||
getAvatarUrl: () => {},
|
||||
getMxcAvatarUrl: () => {},
|
||||
}) as unknown as RoomMember);
|
||||
};
|
||||
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
|
||||
|
||||
describe("RoomTile", () => {
|
||||
jest.spyOn(PlatformPeg, 'get')
|
||||
jest.spyOn(PlatformPeg, "get")
|
||||
.mockReturnValue({ overrideBrowserShortcuts: () => false } as unknown as BasePlatform);
|
||||
useMockedCalls();
|
||||
Object.defineProperty(navigator, "mediaDevices", {
|
||||
value: { enumerateDevices: async () => [] },
|
||||
});
|
||||
|
||||
let client: Mocked<MatrixClient>;
|
||||
|
||||
let cli: MatrixClient;
|
||||
let store: StubVideoChannelStore;
|
||||
beforeEach(() => {
|
||||
const realGetValue = SettingsStore.getValue;
|
||||
SettingsStore.getValue = <T, >(name: string, roomId?: string): T => {
|
||||
if (name === "feature_video_rooms") {
|
||||
return true as unknown as T;
|
||||
}
|
||||
return realGetValue(name, roomId);
|
||||
};
|
||||
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.get();
|
||||
store = stubVideoChannelStore();
|
||||
client = mocked(MatrixClientPeg.get());
|
||||
DMRoomMap.makeShared();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe("video rooms", () => {
|
||||
describe("call subtitle", () => {
|
||||
let room: Room;
|
||||
let call: MockedCall;
|
||||
let widget: Widget;
|
||||
|
||||
beforeEach(() => {
|
||||
room = mkRoom(cli, "!1:example.org");
|
||||
mocked(room.isElementVideoRoom).mockReturnValue(true);
|
||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
|
||||
client.getRooms.mockReturnValue([room]);
|
||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||
|
||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||
|
||||
MockedCall.create(room, "1");
|
||||
call = CallStore.instance.get(room.roomId) as MockedCall;
|
||||
|
||||
widget = new Widget(call.widget);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
|
||||
render(
|
||||
<RoomTile
|
||||
room={room}
|
||||
showMessagePreview={false}
|
||||
isMinimized={false}
|
||||
tag={DefaultTagID.Untagged}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const mountTile = () => mount(
|
||||
<RoomTile
|
||||
room={room}
|
||||
showMessagePreview={false}
|
||||
isMinimized={false}
|
||||
tag={DefaultTagID.Untagged}
|
||||
/>,
|
||||
);
|
||||
|
||||
it("tracks connection state", () => {
|
||||
const tile = mountTile();
|
||||
expect(tile.find(".mx_VideoRoomSummary_indicator").text()).toEqual("Video");
|
||||
|
||||
act(() => { store.startConnect("!1:example.org"); });
|
||||
tile.update();
|
||||
expect(tile.find(".mx_VideoRoomSummary_indicator").text()).toEqual("Joining…");
|
||||
|
||||
act(() => { store.connect("!1:example.org"); });
|
||||
tile.update();
|
||||
expect(tile.find(".mx_VideoRoomSummary_indicator").text()).toEqual("Joined");
|
||||
|
||||
act(() => { store.disconnect(); });
|
||||
tile.update();
|
||||
expect(tile.find(".mx_VideoRoomSummary_indicator").text()).toEqual("Video");
|
||||
afterEach(() => {
|
||||
call.destroy();
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
});
|
||||
|
||||
it("displays connected members", () => {
|
||||
mockGetMember(room, userId => userId === "@chris:example.org" ? "leave" : "join");
|
||||
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
|
||||
// A user connected from 2 devices
|
||||
mkVideoChannelMember("@alice:example.org", ["device 1", "device 2"]),
|
||||
// A disconnected user
|
||||
mkVideoChannelMember("@bob:example.org", []),
|
||||
// A user that claims to have a connected device, but has left the room
|
||||
mkVideoChannelMember("@chris:example.org", ["device 1"]),
|
||||
]));
|
||||
it("tracks connection state", async () => {
|
||||
screen.getByText("Video");
|
||||
|
||||
const tile = mountTile();
|
||||
// Insert an await point in the connection method so we can inspect
|
||||
// the intermediate connecting state
|
||||
let completeConnection: () => void;
|
||||
const connectionCompleted = new Promise<void>(resolve => completeConnection = resolve);
|
||||
jest.spyOn(call, "performConnection").mockReturnValue(connectionCompleted);
|
||||
|
||||
// Only Alice should display as connected
|
||||
expect(tile.find(".mx_VideoRoomSummary_participants").text()).toEqual("1");
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
await screen.findByText("Joining…");
|
||||
const joinedFound = screen.findByText("Joined");
|
||||
completeConnection();
|
||||
await joinedFound;
|
||||
})(),
|
||||
call.connect(),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
screen.findByText("Video"),
|
||||
call.disconnect(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("reflects local echo in connected members", () => {
|
||||
mockGetMember(room);
|
||||
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
|
||||
// Make the remote echo claim that we're connected, while leaving the store disconnected
|
||||
mkVideoChannelMember(cli.getUserId(), [cli.getDeviceId()]),
|
||||
]));
|
||||
it("tracks participants", () => {
|
||||
const alice = mkRoomMember(room.roomId, "@alice:example.org");
|
||||
const bob = mkRoomMember(room.roomId, "@bob:example.org");
|
||||
const carol = mkRoomMember(room.roomId, "@carol:example.org");
|
||||
|
||||
const tile = mountTile();
|
||||
expect(screen.queryByLabelText(/participant/)).toBe(null);
|
||||
|
||||
// Because of our local echo, we should still appear as disconnected
|
||||
expect(tile.find(".mx_VideoRoomSummary_participants").exists()).toEqual(false);
|
||||
});
|
||||
act(() => { call.participants = new Set([alice]); });
|
||||
expect(screen.getByLabelText("1 participant").textContent).toBe("1");
|
||||
|
||||
it("doesn't count members whose device data has expired", () => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
act(() => { call.participants = new Set([alice, bob, carol]); });
|
||||
expect(screen.getByLabelText("3 participants").textContent).toBe("3");
|
||||
|
||||
mockGetMember(room);
|
||||
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
|
||||
mkVideoChannelMember("@alice:example.org", ["device 1"], STUCK_DEVICE_TIMEOUT_MS),
|
||||
]));
|
||||
|
||||
const tile = mountTile();
|
||||
|
||||
expect(tile.find(".mx_VideoRoomSummary_participants").text()).toEqual("1");
|
||||
// Expire Alice's device data
|
||||
act(() => { jest.advanceTimersByTime(STUCK_DEVICE_TIMEOUT_MS); });
|
||||
tile.update();
|
||||
expect(tile.find(".mx_VideoRoomSummary_participants").exists()).toEqual(false);
|
||||
act(() => { call.participants = new Set(); });
|
||||
expect(screen.queryByLabelText(/participant/)).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
181
test/components/views/voip/CallLobby-test.tsx
Normal file
181
test/components/views/voip/CallLobby-test.tsx
Normal file
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { zip } from "lodash";
|
||||
import { render, screen, act, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { mocked, Mocked } from "jest-mock";
|
||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { Widget } from "matrix-widget-api";
|
||||
|
||||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||
import {
|
||||
stubClient,
|
||||
mkRoomMember,
|
||||
MockedCall,
|
||||
useMockedCalls,
|
||||
setupAsyncStoreWithClient,
|
||||
} from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { CallLobby } from "../../../../src/components/views/voip/CallLobby";
|
||||
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
|
||||
import { CallStore } from "../../../../src/stores/CallStore";
|
||||
|
||||
describe("CallLobby", () => {
|
||||
useMockedCalls();
|
||||
Object.defineProperty(navigator, "mediaDevices", {
|
||||
value: {
|
||||
enumerateDevices: jest.fn(),
|
||||
getUserMedia: () => null,
|
||||
},
|
||||
});
|
||||
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});
|
||||
|
||||
let client: Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
let call: MockedCall;
|
||||
let widget: Widget;
|
||||
let alice: RoomMember;
|
||||
|
||||
beforeEach(() => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([]);
|
||||
|
||||
stubClient();
|
||||
client = mocked(MatrixClientPeg.get());
|
||||
|
||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
alice = mkRoomMember(room.roomId, "@alice:example.org");
|
||||
jest.spyOn(room, "getMember").mockImplementation(userId => userId === alice.userId ? alice : null);
|
||||
|
||||
client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
|
||||
client.getRooms.mockReturnValue([room]);
|
||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||
|
||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||
|
||||
MockedCall.create(room, "1");
|
||||
call = CallStore.instance.get(room.roomId) as MockedCall;
|
||||
|
||||
widget = new Widget(call.widget);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
call.destroy();
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
});
|
||||
|
||||
const renderLobby = async (): Promise<void> => {
|
||||
render(<CallLobby room={room} call={call} />);
|
||||
await act(() => Promise.resolve()); // Let effects settle
|
||||
};
|
||||
|
||||
it("tracks participants", async () => {
|
||||
const bob = mkRoomMember(room.roomId, "@bob:example.org");
|
||||
const carol = mkRoomMember(room.roomId, "@carol:example.org");
|
||||
|
||||
const expectAvatars = (userIds: string[]) => {
|
||||
const avatars = screen.queryAllByRole("button", { name: "Avatar" });
|
||||
expect(userIds.length).toBe(avatars.length);
|
||||
|
||||
for (const [userId, avatar] of zip(userIds, avatars)) {
|
||||
fireEvent.focus(avatar!);
|
||||
screen.getByRole("tooltip", { name: userId });
|
||||
}
|
||||
};
|
||||
|
||||
await renderLobby();
|
||||
expect(screen.queryByLabelText(/joined/)).toBe(null);
|
||||
expectAvatars([]);
|
||||
|
||||
act(() => { call.participants = new Set([alice]); });
|
||||
screen.getByText("1 person joined");
|
||||
expectAvatars([alice.userId]);
|
||||
|
||||
act(() => { call.participants = new Set([alice, bob, carol]); });
|
||||
screen.getByText("3 people joined");
|
||||
expectAvatars([alice.userId, bob.userId, carol.userId]);
|
||||
|
||||
act(() => { call.participants = new Set(); });
|
||||
expect(screen.queryByLabelText(/joined/)).toBe(null);
|
||||
expectAvatars([]);
|
||||
});
|
||||
|
||||
describe("device buttons", () => {
|
||||
it("hide when no devices are available", async () => {
|
||||
await renderLobby();
|
||||
expect(screen.queryByRole("button", { name: /microphone/ })).toBe(null);
|
||||
expect(screen.queryByRole("button", { name: /camera/ })).toBe(null);
|
||||
});
|
||||
|
||||
it("show without dropdown when only one device is available", async () => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([{
|
||||
deviceId: "1",
|
||||
groupId: "1",
|
||||
label: "Webcam",
|
||||
kind: "videoinput",
|
||||
toJSON: () => {},
|
||||
}]);
|
||||
|
||||
await renderLobby();
|
||||
screen.getByRole("button", { name: /camera/ });
|
||||
expect(screen.queryByRole("button", { name: "Video devices" })).toBe(null);
|
||||
});
|
||||
|
||||
it("show with dropdown when multiple devices are available", async () => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([
|
||||
{
|
||||
deviceId: "1",
|
||||
groupId: "1",
|
||||
label: "Headphones",
|
||||
kind: "audioinput",
|
||||
toJSON: () => {},
|
||||
},
|
||||
{
|
||||
deviceId: "2",
|
||||
groupId: "1",
|
||||
label: "", // Should fall back to "Audio input 2"
|
||||
kind: "audioinput",
|
||||
toJSON: () => {},
|
||||
},
|
||||
]);
|
||||
|
||||
await renderLobby();
|
||||
screen.getByRole("button", { name: /microphone/ });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Audio devices" }));
|
||||
screen.getByRole("menuitem", { name: "Headphones" });
|
||||
screen.getByRole("menuitem", { name: "Audio input 2" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("join button", () => {
|
||||
it("works", async () => {
|
||||
await renderLobby();
|
||||
const connectSpy = jest.spyOn(call, "connect");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Join" }));
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled(), { interval: 1 });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,193 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
// eslint-disable-next-line deprecate/import
|
||||
import { mount } from "enzyme";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import {
|
||||
stubClient,
|
||||
stubVideoChannelStore,
|
||||
StubVideoChannelStore,
|
||||
mkRoom,
|
||||
mkVideoChannelMember,
|
||||
mockStateEventImplementation,
|
||||
} from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import FacePile from "../../../../src/components/views/elements/FacePile";
|
||||
import MemberAvatar from "../../../../src/components/views/avatars/MemberAvatar";
|
||||
import VideoLobby from "../../../../src/components/views/voip/VideoLobby";
|
||||
|
||||
describe("VideoLobby", () => {
|
||||
Object.defineProperty(navigator, "mediaDevices", {
|
||||
value: {
|
||||
enumerateDevices: jest.fn(),
|
||||
getUserMedia: () => null,
|
||||
},
|
||||
});
|
||||
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});
|
||||
|
||||
let cli: MatrixClient;
|
||||
let store: StubVideoChannelStore;
|
||||
let room: Room;
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.get();
|
||||
store = stubVideoChannelStore();
|
||||
room = mkRoom(cli, "!1:example.org");
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
describe("connected members", () => {
|
||||
it("hides when no one is connected", async () => {
|
||||
const lobby = mount(<VideoLobby room={room} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
lobby.update();
|
||||
|
||||
expect(lobby.find(".mx_VideoLobby_connectedMembers").exists()).toEqual(false);
|
||||
});
|
||||
|
||||
it("is shown when someone is connected", async () => {
|
||||
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
|
||||
// A user connected from 2 devices
|
||||
mkVideoChannelMember("@alice:example.org", ["device 1", "device 2"]),
|
||||
// A disconnected user
|
||||
mkVideoChannelMember("@bob:example.org", []),
|
||||
// A user that claims to have a connected device, but has left the room
|
||||
mkVideoChannelMember("@chris:example.org", ["device 1"]),
|
||||
]));
|
||||
|
||||
mocked(room).getMember.mockImplementation(userId => ({
|
||||
userId,
|
||||
membership: userId === "@chris:example.org" ? "leave" : "join",
|
||||
name: userId,
|
||||
rawDisplayName: userId,
|
||||
roomId: "!1:example.org",
|
||||
getAvatarUrl: () => {},
|
||||
getMxcAvatarUrl: () => {},
|
||||
}) as unknown as RoomMember);
|
||||
|
||||
const lobby = mount(<VideoLobby room={room} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
lobby.update();
|
||||
|
||||
// Only Alice should display as connected
|
||||
const memberText = lobby.find(".mx_VideoLobby_connectedMembers").children().at(0).text();
|
||||
expect(memberText).toEqual("1 person joined");
|
||||
expect(lobby.find(FacePile).find(MemberAvatar).props().member.userId).toEqual("@alice:example.org");
|
||||
});
|
||||
|
||||
it("doesn't include remote echo of this device being connected", async () => {
|
||||
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
|
||||
// Make the remote echo claim that we're connected, while leaving the store disconnected
|
||||
mkVideoChannelMember(cli.getUserId(), [cli.getDeviceId()]),
|
||||
]));
|
||||
|
||||
mocked(room).getMember.mockImplementation(userId => ({
|
||||
userId,
|
||||
membership: "join",
|
||||
name: userId,
|
||||
rawDisplayName: userId,
|
||||
roomId: "!1:example.org",
|
||||
getAvatarUrl: () => {},
|
||||
getMxcAvatarUrl: () => {},
|
||||
}) as unknown as RoomMember);
|
||||
|
||||
const lobby = mount(<VideoLobby room={room} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
lobby.update();
|
||||
|
||||
// Because of our local echo, we should still appear as disconnected
|
||||
expect(lobby.find(".mx_VideoLobby_connectedMembers").exists()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("device buttons", () => {
|
||||
it("hides when no devices are available", async () => {
|
||||
const lobby = mount(<VideoLobby room={room} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
lobby.update();
|
||||
|
||||
expect(lobby.find("DeviceButton").children().exists()).toEqual(false);
|
||||
});
|
||||
|
||||
it("hides device list when only one device is available", async () => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([{
|
||||
deviceId: "1",
|
||||
groupId: "1",
|
||||
label: "Webcam",
|
||||
kind: "videoinput",
|
||||
toJSON: () => {},
|
||||
}]);
|
||||
|
||||
const lobby = mount(<VideoLobby room={room} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
lobby.update();
|
||||
|
||||
expect(lobby.find(".mx_VideoLobby_deviceListButton").exists()).toEqual(false);
|
||||
});
|
||||
|
||||
it("shows device list when multiple devices are available", async () => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([
|
||||
{
|
||||
deviceId: "1",
|
||||
groupId: "1",
|
||||
label: "Front camera",
|
||||
kind: "videoinput",
|
||||
toJSON: () => {},
|
||||
},
|
||||
{
|
||||
deviceId: "2",
|
||||
groupId: "1",
|
||||
label: "Back camera",
|
||||
kind: "videoinput",
|
||||
toJSON: () => {},
|
||||
},
|
||||
]);
|
||||
|
||||
const lobby = mount(<VideoLobby room={room} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
lobby.update();
|
||||
|
||||
expect(lobby.find(".mx_VideoLobby_deviceListButton").exists()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("join button", () => {
|
||||
it("works", async () => {
|
||||
const lobby = mount(<VideoLobby room={room} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
lobby.update();
|
||||
|
||||
act(() => {
|
||||
lobby.find("AccessibleButton.mx_VideoLobby_joinButton").simulate("click");
|
||||
});
|
||||
expect(store.connect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -23,7 +23,7 @@ import { stubClient, setupAsyncStoreWithClient, mockPlatformPeg } from "./test-u
|
|||
import { MatrixClientPeg } from "../src/MatrixClientPeg";
|
||||
import WidgetStore from "../src/stores/WidgetStore";
|
||||
import WidgetUtils from "../src/utils/WidgetUtils";
|
||||
import { VIDEO_CHANNEL_MEMBER } from "../src/utils/VideoChannelUtils";
|
||||
import { JitsiCall } from "../src/models/Call";
|
||||
import createRoom, { canEncryptToAllUsers } from '../src/createRoom';
|
||||
|
||||
describe("createRoom", () => {
|
||||
|
@ -51,7 +51,7 @@ describe("createRoom", () => {
|
|||
},
|
||||
events: {
|
||||
"im.vector.modular.widgets": widgetPower,
|
||||
[VIDEO_CHANNEL_MEMBER]: videoMemberPower,
|
||||
[JitsiCall.MEMBER_EVENT_TYPE]: jitsiMemberPower,
|
||||
},
|
||||
},
|
||||
}]] = mocked(client.createRoom).mock.calls as any; // no good type
|
||||
|
@ -64,7 +64,7 @@ describe("createRoom", () => {
|
|||
expect(widgetStateKey).toEqual("im.vector.modular.widgets");
|
||||
|
||||
// All members should be able to update their connected devices
|
||||
expect(videoMemberPower).toEqual(0);
|
||||
expect(jitsiMemberPower).toEqual(0);
|
||||
// Jitsi widget should be immutable for admins
|
||||
expect(widgetPower).toBeGreaterThan(100);
|
||||
// and we should have been reset back to admin
|
||||
|
|
339
test/models/Call-test.ts
Normal file
339
test/models/Call-test.ts
Normal file
|
@ -0,0 +1,339 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
import { isEqual } from "lodash";
|
||||
import { mocked } from "jest-mock";
|
||||
import { waitFor } from "@testing-library/react";
|
||||
import { PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { Widget } from "matrix-widget-api";
|
||||
|
||||
import type { Mocked } from "jest-mock";
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||
import type { Call } from "../../src/models/Call";
|
||||
import { stubClient, mkEvent, mkRoomMember, setupAsyncStoreWithClient, mockPlatformPeg } from "../test-utils";
|
||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../src/MediaDeviceHandler";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import { CallEvent, ConnectionState, JitsiCall } from "../../src/models/Call";
|
||||
import WidgetStore from "../../src/stores/WidgetStore";
|
||||
import { WidgetMessagingStore } from "../../src/stores/widgets/WidgetMessagingStore";
|
||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../src/stores/ActiveWidgetStore";
|
||||
import { ElementWidgetActions } from "../../src/stores/widgets/ElementWidgetActions";
|
||||
|
||||
describe("JitsiCall", () => {
|
||||
mockPlatformPeg({ supportsJitsiScreensharing: () => true });
|
||||
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
|
||||
[MediaDeviceKindEnum.AudioInput]: [
|
||||
{ deviceId: "1", groupId: "1", kind: "audioinput", label: "Headphones", toJSON: () => {} },
|
||||
],
|
||||
[MediaDeviceKindEnum.VideoInput]: [
|
||||
{ deviceId: "2", groupId: "2", kind: "videoinput", label: "Built-in webcam", toJSON: () => {} },
|
||||
],
|
||||
[MediaDeviceKindEnum.AudioOutput]: [],
|
||||
});
|
||||
jest.spyOn(MediaDeviceHandler, "getAudioInput").mockReturnValue("1");
|
||||
jest.spyOn(MediaDeviceHandler, "getVideoInput").mockReturnValue("2");
|
||||
|
||||
let client: Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
let alice: RoomMember;
|
||||
let bob: RoomMember;
|
||||
let carol: RoomMember;
|
||||
let call: Call;
|
||||
let widget: Widget;
|
||||
let messaging: Mocked<ClientWidgetApi>;
|
||||
let audioMutedSpy: jest.SpyInstance<boolean, []>;
|
||||
let videoMutedSpy: jest.SpyInstance<boolean, []>;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
|
||||
stubClient();
|
||||
client = mocked(MatrixClientPeg.get());
|
||||
|
||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
alice = mkRoomMember(room.roomId, "@alice:example.org");
|
||||
bob = mkRoomMember(room.roomId, "@bob:example.org");
|
||||
carol = mkRoomMember(room.roomId, "@carol:example.org");
|
||||
jest.spyOn(room, "getMember").mockImplementation(userId => {
|
||||
switch (userId) {
|
||||
case alice.userId: return alice;
|
||||
case bob.userId: return bob;
|
||||
case carol.userId: return carol;
|
||||
default: return null;
|
||||
}
|
||||
});
|
||||
jest.spyOn(room, "getMyMembership").mockReturnValue("join");
|
||||
|
||||
client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
|
||||
client.getRooms.mockReturnValue([room]);
|
||||
client.getUserId.mockReturnValue(alice.userId);
|
||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||
client.sendStateEvent.mockImplementation(async (roomId, eventType, content, stateKey = "") => {
|
||||
if (roomId !== room.roomId) throw new Error("Unknown room");
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
type: eventType,
|
||||
room: roomId,
|
||||
user: alice.userId,
|
||||
skey: stateKey,
|
||||
content,
|
||||
});
|
||||
room.addLiveEvents([event]);
|
||||
return { event_id: event.getId() };
|
||||
});
|
||||
|
||||
setupAsyncStoreWithClient(WidgetStore.instance, client);
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||
|
||||
await JitsiCall.create(room);
|
||||
call = JitsiCall.get(room);
|
||||
if (call === null) throw new Error("Failed to create call");
|
||||
|
||||
widget = new Widget(call.widget);
|
||||
|
||||
const eventEmitter = new EventEmitter();
|
||||
messaging = {
|
||||
on: eventEmitter.on.bind(eventEmitter),
|
||||
off: eventEmitter.off.bind(eventEmitter),
|
||||
once: eventEmitter.once.bind(eventEmitter),
|
||||
emit: eventEmitter.emit.bind(eventEmitter),
|
||||
stop: jest.fn(),
|
||||
transport: {
|
||||
send: jest.fn(async action => {
|
||||
if (action === ElementWidgetActions.JoinCall) {
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.JoinCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
} else if (action === ElementWidgetActions.HangupCall) {
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.HangupCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
reply: jest.fn(),
|
||||
},
|
||||
} as unknown as Mocked<ClientWidgetApi>;
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, messaging);
|
||||
|
||||
audioMutedSpy = jest.spyOn(MediaDeviceHandler, "startWithAudioMuted", "get");
|
||||
videoMutedSpy = jest.spyOn(MediaDeviceHandler, "startWithVideoMuted", "get");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
call.destroy();
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
jest.clearAllMocks();
|
||||
audioMutedSpy.mockRestore();
|
||||
videoMutedSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("connects muted", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
audioMutedSpy.mockReturnValue(true);
|
||||
videoMutedSpy.mockReturnValue(true);
|
||||
|
||||
await call.connect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.JoinCall, {
|
||||
audioInput: null,
|
||||
videoInput: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("connects unmuted", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
audioMutedSpy.mockReturnValue(false);
|
||||
videoMutedSpy.mockReturnValue(false);
|
||||
|
||||
await call.connect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.JoinCall, {
|
||||
audioInput: "Headphones",
|
||||
videoInput: "Built-in webcam",
|
||||
});
|
||||
});
|
||||
|
||||
it("waits for messaging when connecting", async () => {
|
||||
// Temporarily remove the messaging to simulate connecting while the
|
||||
// widget is still initializing
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
|
||||
const connect = call.connect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Connecting);
|
||||
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, messaging);
|
||||
await connect;
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
});
|
||||
|
||||
it("handles remote disconnection", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
|
||||
await call.connect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.HangupCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Disconnected), { interval: 5 });
|
||||
});
|
||||
|
||||
it("handles instant remote disconnection when connecting", async () => {
|
||||
mocked(messaging.transport).send.mockImplementation(async action => {
|
||||
if (action === ElementWidgetActions.JoinCall) {
|
||||
// Emit the hangup event *before* the join event to fully
|
||||
// exercise the race condition
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.HangupCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.JoinCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
}
|
||||
return {};
|
||||
});
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
await call.connect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
// Should disconnect on its own almost instantly
|
||||
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Disconnected), { interval: 5 });
|
||||
});
|
||||
|
||||
it("disconnects", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
await call.connect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
await call.disconnect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
});
|
||||
|
||||
it("tracks participants in room state", async () => {
|
||||
expect([...call.participants]).toEqual([]);
|
||||
|
||||
// A participant with multiple devices (should only show up once)
|
||||
await client.sendStateEvent(
|
||||
room.roomId,
|
||||
JitsiCall.MEMBER_EVENT_TYPE,
|
||||
{ devices: ["bobweb", "bobdesktop"], expires_ts: 1000 * 60 * 10 },
|
||||
bob.userId,
|
||||
);
|
||||
// A participant with an expired device (should not show up)
|
||||
await client.sendStateEvent(
|
||||
room.roomId,
|
||||
JitsiCall.MEMBER_EVENT_TYPE,
|
||||
{ devices: ["carolandroid"], expires_ts: -1000 * 60 },
|
||||
carol.userId,
|
||||
);
|
||||
|
||||
// Now, stub out client.sendStateEvent so we can test our local echo
|
||||
client.sendStateEvent.mockReset();
|
||||
await call.connect();
|
||||
expect([...call.participants]).toEqual([bob, alice]);
|
||||
|
||||
await call.disconnect();
|
||||
expect([...call.participants]).toEqual([bob]);
|
||||
});
|
||||
|
||||
it("updates room state when connecting and disconnecting", async () => {
|
||||
const now1 = Date.now();
|
||||
await call.connect();
|
||||
await waitFor(() => expect(
|
||||
room.currentState.getStateEvents(JitsiCall.MEMBER_EVENT_TYPE, alice.userId).getContent(),
|
||||
).toEqual({
|
||||
devices: [client.getDeviceId()],
|
||||
expires_ts: now1 + JitsiCall.STUCK_DEVICE_TIMEOUT_MS,
|
||||
}), { interval: 5 });
|
||||
|
||||
const now2 = Date.now();
|
||||
await call.disconnect();
|
||||
await waitFor(() => expect(
|
||||
room.currentState.getStateEvents(JitsiCall.MEMBER_EVENT_TYPE, alice.userId).getContent(),
|
||||
).toEqual({
|
||||
devices: [],
|
||||
expires_ts: now2 + JitsiCall.STUCK_DEVICE_TIMEOUT_MS,
|
||||
}), { interval: 5 });
|
||||
});
|
||||
|
||||
it("repeatedly updates room state while connected", async () => {
|
||||
await call.connect();
|
||||
await waitFor(() => expect(client.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
room.roomId,
|
||||
JitsiCall.MEMBER_EVENT_TYPE,
|
||||
{ devices: [client.getDeviceId()], expires_ts: expect.any(Number) },
|
||||
alice.userId,
|
||||
), { interval: 5 });
|
||||
|
||||
client.sendStateEvent.mockClear();
|
||||
jest.advanceTimersByTime(JitsiCall.STUCK_DEVICE_TIMEOUT_MS);
|
||||
await waitFor(() => expect(client.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
room.roomId,
|
||||
JitsiCall.MEMBER_EVENT_TYPE,
|
||||
{ devices: [client.getDeviceId()], expires_ts: expect.any(Number) },
|
||||
alice.userId,
|
||||
), { interval: 5 });
|
||||
});
|
||||
|
||||
it("emits events when connection state changes", async () => {
|
||||
const events: ConnectionState[] = [];
|
||||
const onConnectionState = (state: ConnectionState) => events.push(state);
|
||||
call.on(CallEvent.ConnectionState, onConnectionState);
|
||||
|
||||
await call.connect();
|
||||
await call.disconnect();
|
||||
expect(events).toEqual([
|
||||
ConnectionState.Connecting,
|
||||
ConnectionState.Connected,
|
||||
ConnectionState.Disconnecting,
|
||||
ConnectionState.Disconnected,
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits events when participants change", async () => {
|
||||
const events: Set<RoomMember>[] = [];
|
||||
const onParticipants = (participants: Set<RoomMember>) => {
|
||||
if (!isEqual(participants, events[events.length - 1])) events.push(participants);
|
||||
};
|
||||
call.on(CallEvent.Participants, onParticipants);
|
||||
|
||||
await call.connect();
|
||||
await call.disconnect();
|
||||
expect(events).toEqual([new Set([alice]), new Set()]);
|
||||
});
|
||||
|
||||
it("switches to spotlight layout when the widget becomes a PiP", async () => {
|
||||
await call.connect();
|
||||
ActiveWidgetStore.instance.emit(ActiveWidgetStoreEvent.Undock);
|
||||
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.SpotlightLayout, {});
|
||||
ActiveWidgetStore.instance.emit(ActiveWidgetStoreEvent.Dock);
|
||||
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.TileLayout, {});
|
||||
});
|
||||
});
|
|
@ -1,225 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked, Mocked } from "jest-mock";
|
||||
import {
|
||||
Widget,
|
||||
ClientWidgetApi,
|
||||
MatrixWidgetType,
|
||||
WidgetApiAction,
|
||||
IWidgetApiRequest,
|
||||
IWidgetApiRequestData,
|
||||
} from "matrix-widget-api";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { stubClient, setupAsyncStoreWithClient, mkRoom } from "../test-utils";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import WidgetStore, { IApp } from "../../src/stores/WidgetStore";
|
||||
import { WidgetMessagingStore } from "../../src/stores/widgets/WidgetMessagingStore";
|
||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../src/stores/ActiveWidgetStore";
|
||||
import { ElementWidgetActions } from "../../src/stores/widgets/ElementWidgetActions";
|
||||
import { VIDEO_CHANNEL_MEMBER, STUCK_DEVICE_TIMEOUT_MS } from "../../src/utils/VideoChannelUtils";
|
||||
import VideoChannelStore, { VideoChannelEvent } from "../../src/stores/VideoChannelStore";
|
||||
|
||||
describe("VideoChannelStore", () => {
|
||||
const store = VideoChannelStore.instance;
|
||||
|
||||
const widget = { id: "1" } as unknown as Widget;
|
||||
const app = {
|
||||
id: "1",
|
||||
eventId: "$1:example.org",
|
||||
roomId: "!1:example.org",
|
||||
type: MatrixWidgetType.JitsiMeet,
|
||||
url: "",
|
||||
name: "Video channel",
|
||||
creatorUserId: "@alice:example.org",
|
||||
avatar_url: null,
|
||||
data: { isVideoChannel: true },
|
||||
} as IApp;
|
||||
|
||||
// Set up mocks to simulate the remote end of the widget API
|
||||
let sendMock: (action: WidgetApiAction, data: IWidgetApiRequestData) => void;
|
||||
let onMock: (action: string, listener: (ev: CustomEvent<IWidgetApiRequest>) => void) => void;
|
||||
let onceMock: (action: string, listener: (ev: CustomEvent<IWidgetApiRequest>) => void) => void;
|
||||
let messaging: ClientWidgetApi;
|
||||
let cli: Mocked<MatrixClient>;
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = mocked(MatrixClientPeg.get());
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, cli);
|
||||
setupAsyncStoreWithClient(store, cli);
|
||||
cli.getRoom.mockReturnValue(mkRoom(cli, "!1:example.org"));
|
||||
|
||||
sendMock = jest.fn();
|
||||
onMock = jest.fn();
|
||||
onceMock = jest.fn();
|
||||
|
||||
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([app]);
|
||||
messaging = {
|
||||
on: onMock,
|
||||
off: () => {},
|
||||
stop: () => {},
|
||||
once: onceMock,
|
||||
transport: {
|
||||
send: sendMock,
|
||||
reply: () => {},
|
||||
},
|
||||
} as unknown as ClientWidgetApi;
|
||||
});
|
||||
|
||||
afterEach(() => jest.useRealTimers());
|
||||
|
||||
const getRequest = <T extends IWidgetApiRequestData>(): Promise<[WidgetApiAction, T]> =>
|
||||
new Promise<[WidgetApiAction, T]>(resolve => {
|
||||
mocked(sendMock).mockImplementationOnce((action, data) => resolve([action, data as T]));
|
||||
});
|
||||
|
||||
const widgetReady = () => {
|
||||
// Tell the WidgetStore that the widget is ready
|
||||
const [, ready] = mocked(onceMock).mock.calls.find(([action]) =>
|
||||
action === `action:${ElementWidgetActions.WidgetReady}`,
|
||||
);
|
||||
ready({ detail: {} } as unknown as CustomEvent<IWidgetApiRequest>);
|
||||
};
|
||||
|
||||
const confirmConnect = async () => {
|
||||
// Wait for the store to contact the widget API
|
||||
await getRequest();
|
||||
// Then, locate the callback that will confirm the join
|
||||
const [, join] = mocked(onMock).mock.calls.find(([action]) =>
|
||||
action === `action:${ElementWidgetActions.JoinCall}`,
|
||||
);
|
||||
// Confirm the join, and wait for the store to update
|
||||
const waitForConnect = new Promise<void>(resolve =>
|
||||
store.once(VideoChannelEvent.Connect, resolve),
|
||||
);
|
||||
join(new CustomEvent("widgetapirequest", { detail: {} }) as CustomEvent<IWidgetApiRequest>);
|
||||
await waitForConnect;
|
||||
};
|
||||
|
||||
const confirmDisconnect = async () => {
|
||||
// Locate the callback that will perform the hangup
|
||||
const [, hangup] = mocked(onceMock).mock.calls.find(([action]) =>
|
||||
action === `action:${ElementWidgetActions.HangupCall}`,
|
||||
);
|
||||
// Hangup and wait for the store, once again
|
||||
const waitForHangup = new Promise<void>(resolve =>
|
||||
store.once(VideoChannelEvent.Disconnect, resolve),
|
||||
);
|
||||
hangup(new CustomEvent("widgetapirequest", { detail: {} }) as CustomEvent<IWidgetApiRequest>);
|
||||
await waitForHangup;
|
||||
};
|
||||
|
||||
it("connects and disconnects", async () => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, "!1:example.org", messaging);
|
||||
widgetReady();
|
||||
expect(store.roomId).toBeFalsy();
|
||||
expect(store.connected).toEqual(false);
|
||||
|
||||
const connectConfirmed = confirmConnect();
|
||||
const connectPromise = store.connect("!1:example.org", null, null);
|
||||
await connectConfirmed;
|
||||
await expect(connectPromise).resolves.toBeUndefined();
|
||||
expect(store.roomId).toEqual("!1:example.org");
|
||||
expect(store.connected).toEqual(true);
|
||||
|
||||
// Our device should now appear as connected
|
||||
expect(cli.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
"!1:example.org",
|
||||
VIDEO_CHANNEL_MEMBER,
|
||||
{ devices: [cli.getDeviceId()], expires_ts: expect.any(Number) },
|
||||
cli.getUserId(),
|
||||
);
|
||||
cli.sendStateEvent.mockClear();
|
||||
|
||||
// Our devices should be resent within the timeout period to prevent
|
||||
// the data from becoming stale
|
||||
jest.advanceTimersByTime(STUCK_DEVICE_TIMEOUT_MS);
|
||||
expect(cli.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
"!1:example.org",
|
||||
VIDEO_CHANNEL_MEMBER,
|
||||
{ devices: [cli.getDeviceId()], expires_ts: expect.any(Number) },
|
||||
cli.getUserId(),
|
||||
);
|
||||
cli.sendStateEvent.mockClear();
|
||||
|
||||
const disconnectPromise = store.disconnect();
|
||||
await confirmDisconnect();
|
||||
await expect(disconnectPromise).resolves.toBeUndefined();
|
||||
expect(store.roomId).toBeFalsy();
|
||||
expect(store.connected).toEqual(false);
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, "!1:example.org");
|
||||
|
||||
// Our device should now be marked as disconnected
|
||||
expect(cli.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
"!1:example.org",
|
||||
VIDEO_CHANNEL_MEMBER,
|
||||
{ devices: [], expires_ts: expect.any(Number) },
|
||||
cli.getUserId(),
|
||||
);
|
||||
});
|
||||
|
||||
it("waits for messaging when connecting", async () => {
|
||||
const connectConfirmed = confirmConnect();
|
||||
const connectPromise = store.connect("!1:example.org", null, null);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, "!1:example.org", messaging);
|
||||
widgetReady();
|
||||
await connectConfirmed;
|
||||
await expect(connectPromise).resolves.toBeUndefined();
|
||||
expect(store.roomId).toEqual("!1:example.org");
|
||||
expect(store.connected).toEqual(true);
|
||||
|
||||
store.disconnect();
|
||||
await confirmDisconnect();
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, "!1:example.org");
|
||||
});
|
||||
|
||||
it("rejects if the widget's messaging gets stopped mid-connect", async () => {
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, "!1:example.org", messaging);
|
||||
widgetReady();
|
||||
expect(store.roomId).toBeFalsy();
|
||||
expect(store.connected).toEqual(false);
|
||||
|
||||
const requestPromise = getRequest();
|
||||
const connectPromise = store.connect("!1:example.org", null, null);
|
||||
// Wait for the store to contact the widget API, then stop the messaging
|
||||
await requestPromise;
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, "!1:example.org");
|
||||
await expect(connectPromise).rejects.toBeDefined();
|
||||
expect(store.roomId).toBeFalsy();
|
||||
expect(store.connected).toEqual(false);
|
||||
});
|
||||
|
||||
it("switches to spotlight mode when the widget becomes a PiP", async () => {
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, "!1:example.org", messaging);
|
||||
widgetReady();
|
||||
confirmConnect();
|
||||
await store.connect("!1:example.org", null, null);
|
||||
|
||||
const request = getRequest<IWidgetApiRequestData>();
|
||||
ActiveWidgetStore.instance.emit(ActiveWidgetStoreEvent.Undock);
|
||||
const [action, data] = await request;
|
||||
expect(action).toEqual(ElementWidgetActions.SpotlightLayout);
|
||||
expect(data).toEqual({});
|
||||
|
||||
store.disconnect();
|
||||
await confirmDisconnect();
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, "!1:example.org");
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue