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",
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
// We're okay with assertion errors when we ask for them
|
// We're okay with assertion errors when we ask for them
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
"@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
|
// temporary override for offending icon require files
|
||||||
|
|
|
@ -752,7 +752,7 @@ legend {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@define-mixin CallButton {
|
@define-mixin LegacyCallButton {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
height: $font-24px;
|
height: $font-24px;
|
||||||
|
|
|
@ -97,9 +97,9 @@
|
||||||
@import "./views/avatars/_DecoratedRoomAvatar.pcss";
|
@import "./views/avatars/_DecoratedRoomAvatar.pcss";
|
||||||
@import "./views/avatars/_WidgetAvatar.pcss";
|
@import "./views/avatars/_WidgetAvatar.pcss";
|
||||||
@import "./views/beta/_BetaCard.pcss";
|
@import "./views/beta/_BetaCard.pcss";
|
||||||
@import "./views/context_menus/_CallContextMenu.pcss";
|
|
||||||
@import "./views/context_menus/_DeviceContextMenu.pcss";
|
@import "./views/context_menus/_DeviceContextMenu.pcss";
|
||||||
@import "./views/context_menus/_IconizedContextMenu.pcss";
|
@import "./views/context_menus/_IconizedContextMenu.pcss";
|
||||||
|
@import "./views/context_menus/_LegacyCallContextMenu.pcss";
|
||||||
@import "./views/context_menus/_MessageContextMenu.pcss";
|
@import "./views/context_menus/_MessageContextMenu.pcss";
|
||||||
@import "./views/context_menus/_RoomGeneralContextMenu.pcss";
|
@import "./views/context_menus/_RoomGeneralContextMenu.pcss";
|
||||||
@import "./views/context_menus/_RoomNotificationContextMenu.pcss";
|
@import "./views/context_menus/_RoomNotificationContextMenu.pcss";
|
||||||
|
@ -207,13 +207,13 @@
|
||||||
@import "./views/elements/_Validation.pcss";
|
@import "./views/elements/_Validation.pcss";
|
||||||
@import "./views/emojipicker/_EmojiPicker.pcss";
|
@import "./views/emojipicker/_EmojiPicker.pcss";
|
||||||
@import "./views/location/_LocationPicker.pcss";
|
@import "./views/location/_LocationPicker.pcss";
|
||||||
@import "./views/messages/_CallEvent.pcss";
|
|
||||||
@import "./views/messages/_CreateEvent.pcss";
|
@import "./views/messages/_CreateEvent.pcss";
|
||||||
@import "./views/messages/_DateSeparator.pcss";
|
@import "./views/messages/_DateSeparator.pcss";
|
||||||
@import "./views/messages/_DisambiguatedProfile.pcss";
|
@import "./views/messages/_DisambiguatedProfile.pcss";
|
||||||
@import "./views/messages/_EventTileBubble.pcss";
|
@import "./views/messages/_EventTileBubble.pcss";
|
||||||
@import "./views/messages/_HiddenBody.pcss";
|
@import "./views/messages/_HiddenBody.pcss";
|
||||||
@import "./views/messages/_JumpToDatePicker.pcss";
|
@import "./views/messages/_JumpToDatePicker.pcss";
|
||||||
|
@import "./views/messages/_LegacyCallEvent.pcss";
|
||||||
@import "./views/messages/_MEmoteBody.pcss";
|
@import "./views/messages/_MEmoteBody.pcss";
|
||||||
@import "./views/messages/_MFileBody.pcss";
|
@import "./views/messages/_MFileBody.pcss";
|
||||||
@import "./views/messages/_MImageBody.pcss";
|
@import "./views/messages/_MImageBody.pcss";
|
||||||
|
@ -282,13 +282,13 @@
|
||||||
@import "./views/rooms/_RoomPreviewCard.pcss";
|
@import "./views/rooms/_RoomPreviewCard.pcss";
|
||||||
@import "./views/rooms/_RoomSublist.pcss";
|
@import "./views/rooms/_RoomSublist.pcss";
|
||||||
@import "./views/rooms/_RoomTile.pcss";
|
@import "./views/rooms/_RoomTile.pcss";
|
||||||
|
@import "./views/rooms/_RoomTileCallSummary.pcss";
|
||||||
@import "./views/rooms/_RoomUpgradeWarningBar.pcss";
|
@import "./views/rooms/_RoomUpgradeWarningBar.pcss";
|
||||||
@import "./views/rooms/_SearchBar.pcss";
|
@import "./views/rooms/_SearchBar.pcss";
|
||||||
@import "./views/rooms/_SendMessageComposer.pcss";
|
@import "./views/rooms/_SendMessageComposer.pcss";
|
||||||
@import "./views/rooms/_Stickers.pcss";
|
@import "./views/rooms/_Stickers.pcss";
|
||||||
@import "./views/rooms/_ThreadSummary.pcss";
|
@import "./views/rooms/_ThreadSummary.pcss";
|
||||||
@import "./views/rooms/_TopUnreadMessagesBar.pcss";
|
@import "./views/rooms/_TopUnreadMessagesBar.pcss";
|
||||||
@import "./views/rooms/_VideoRoomSummary.pcss";
|
|
||||||
@import "./views/rooms/_VoiceRecordComposerTile.pcss";
|
@import "./views/rooms/_VoiceRecordComposerTile.pcss";
|
||||||
@import "./views/rooms/_WhoIsTypingTile.pcss";
|
@import "./views/rooms/_WhoIsTypingTile.pcss";
|
||||||
@import "./views/settings/_AvatarSetting.pcss";
|
@import "./views/settings/_AvatarSetting.pcss";
|
||||||
|
@ -333,7 +333,7 @@
|
||||||
@import "./views/spaces/_SpacePublicShare.pcss";
|
@import "./views/spaces/_SpacePublicShare.pcss";
|
||||||
@import "./views/terms/_InlineTermsAgreement.pcss";
|
@import "./views/terms/_InlineTermsAgreement.pcss";
|
||||||
@import "./views/toasts/_AnalyticsToast.pcss";
|
@import "./views/toasts/_AnalyticsToast.pcss";
|
||||||
@import "./views/toasts/_IncomingCallToast.pcss";
|
@import "./views/toasts/_IncomingLegacyCallToast.pcss";
|
||||||
@import "./views/toasts/_NonUrgentEchoFailureToast.pcss";
|
@import "./views/toasts/_NonUrgentEchoFailureToast.pcss";
|
||||||
@import "./views/typography/_Heading.pcss";
|
@import "./views/typography/_Heading.pcss";
|
||||||
@import "./views/user-onboarding/_UserOnboardingButton.pcss";
|
@import "./views/user-onboarding/_UserOnboardingButton.pcss";
|
||||||
|
@ -343,15 +343,15 @@
|
||||||
@import "./views/user-onboarding/_UserOnboardingPage.pcss";
|
@import "./views/user-onboarding/_UserOnboardingPage.pcss";
|
||||||
@import "./views/user-onboarding/_UserOnboardingTask.pcss";
|
@import "./views/user-onboarding/_UserOnboardingTask.pcss";
|
||||||
@import "./views/verification/_VerificationShowSas.pcss";
|
@import "./views/verification/_VerificationShowSas.pcss";
|
||||||
@import "./views/voip/CallView/_CallViewButtons.pcss";
|
@import "./views/voip/LegacyCallView/_LegacyCallViewButtons.pcss";
|
||||||
@import "./views/voip/_CallPreview.pcss";
|
@import "./views/voip/_CallLobby.pcss";
|
||||||
@import "./views/voip/_CallView.pcss";
|
|
||||||
@import "./views/voip/_CallViewForRoom.pcss";
|
|
||||||
@import "./views/voip/_CallViewHeader.pcss";
|
|
||||||
@import "./views/voip/_CallViewSidebar.pcss";
|
|
||||||
@import "./views/voip/_DialPad.pcss";
|
@import "./views/voip/_DialPad.pcss";
|
||||||
@import "./views/voip/_DialPadContextMenu.pcss";
|
@import "./views/voip/_DialPadContextMenu.pcss";
|
||||||
@import "./views/voip/_DialPadModal.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/_PiPContainer.pcss";
|
||||||
@import "./views/voip/_VideoFeed.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 */
|
/* 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;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_CallContextMenu_item {
|
.mx_LegacyCallContextMenu_item {
|
||||||
width: 205px;
|
width: 205px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
|
@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_CallEvent_wrapper {
|
.mx_LegacyCallEvent_wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.mx_CallEvent {
|
.mx_LegacyCallEvent {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -35,7 +35,7 @@ limitations under the License.
|
||||||
width: 65%;
|
width: 65%;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
|
||||||
.mx_CallEvent_iconButton {
|
.mx_LegacyCallEvent_iconButton {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
&::before {
|
&::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');
|
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');
|
mask-image: url('$(res)/img/voip/un-silence.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallEvent_voice {
|
&.mx_LegacyCallEvent_voice {
|
||||||
.mx_CallEvent_type_icon::before,
|
.mx_LegacyCallEvent_type_icon::before,
|
||||||
.mx_CallEvent_content_button_callBack span::before,
|
.mx_LegacyCallEvent_content_button_callBack span::before,
|
||||||
.mx_CallEvent_content_button_answer span::before {
|
.mx_LegacyCallEvent_content_button_answer span::before {
|
||||||
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallEvent_rejected,
|
&.mx_LegacyCallEvent_rejected,
|
||||||
&.mx_CallEvent_noAnswer {
|
&.mx_LegacyCallEvent_noAnswer {
|
||||||
.mx_CallEvent_type_icon::before {
|
.mx_LegacyCallEvent_type_icon::before {
|
||||||
mask-image: url('$(res)/img/voip/declined-voice.svg');
|
mask-image: url('$(res)/img/voip/declined-voice.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallEvent_video {
|
&.mx_LegacyCallEvent_video {
|
||||||
.mx_CallEvent_type_icon::before,
|
.mx_LegacyCallEvent_type_icon::before,
|
||||||
.mx_CallEvent_content_button_callBack span::before,
|
.mx_LegacyCallEvent_content_button_callBack span::before,
|
||||||
.mx_CallEvent_content_button_answer span::before {
|
.mx_LegacyCallEvent_content_button_answer span::before {
|
||||||
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallEvent_rejected,
|
&.mx_LegacyCallEvent_rejected,
|
||||||
&.mx_CallEvent_noAnswer {
|
&.mx_LegacyCallEvent_noAnswer {
|
||||||
.mx_CallEvent_type_icon::before {
|
.mx_LegacyCallEvent_type_icon::before {
|
||||||
mask-image: url('$(res)/img/voip/declined-video.svg');
|
mask-image: url('$(res)/img/voip/declined-video.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallEvent_missed {
|
&.mx_LegacyCallEvent_missed {
|
||||||
&.mx_CallEvent_voice {
|
&.mx_LegacyCallEvent_voice {
|
||||||
.mx_CallEvent_type_icon::before {
|
.mx_LegacyCallEvent_type_icon::before {
|
||||||
mask-image: url('$(res)/img/voip/missed-voice.svg');
|
mask-image: url('$(res)/img/voip/missed-voice.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallEvent_video {
|
&.mx_LegacyCallEvent_video {
|
||||||
.mx_CallEvent_type_icon::before {
|
.mx_LegacyCallEvent_type_icon::before {
|
||||||
mask-image: url('$(res)/img/voip/missed-video.svg');
|
mask-image: url('$(res)/img/voip/missed-video.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallEvent_info {
|
.mx_LegacyCallEvent_info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
.mx_CallEvent_info_basic {
|
.mx_LegacyCallEvent_info_basic {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $spacing-4;
|
gap: $spacing-4;
|
||||||
margin-left: 10px; /* To match mx_CallEvent */
|
margin-left: 10px; /* To match mx_LegacyCallEvent */
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
.mx_CallEvent_sender {
|
.mx_LegacyCallEvent_sender {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 1.8rem;
|
line-height: 1.8rem;
|
||||||
|
@ -128,7 +128,7 @@ limitations under the License.
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallEvent_type {
|
.mx_LegacyCallEvent_type {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
@ -136,7 +136,7 @@ limitations under the License.
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
line-height: $font-13px;
|
line-height: $font-13px;
|
||||||
|
|
||||||
.mx_CallEvent_type_icon {
|
.mx_LegacyCallEvent_type_icon {
|
||||||
height: 13px;
|
height: 13px;
|
||||||
width: 13px;
|
width: 13px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
@ -155,18 +155,18 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallEvent_content {
|
.mx_LegacyCallEvent_content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: $secondary-content;
|
color: $secondary-content;
|
||||||
gap: $spacing-12; /* See mx_IncomingCallToast_buttons */
|
gap: $spacing-12; /* See mx_IncomingLegacyCallToast_buttons */
|
||||||
margin-inline-start: 42px; /* avatar (32px) + mx_CallEvent_info_basic margin (10px) */
|
margin-inline-start: 42px; /* avatar (32px) + mx_LegacyCallEvent_info_basic margin (10px) */
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
max-width: fit-content;
|
max-width: fit-content;
|
||||||
|
|
||||||
.mx_CallEvent_content_button {
|
.mx_LegacyCallEvent_content_button {
|
||||||
@mixin CallButton;
|
@mixin LegacyCallButton;
|
||||||
padding: 0 $spacing-12;
|
padding: 0 $spacing-12;
|
||||||
|
|
||||||
span::before {
|
span::before {
|
||||||
|
@ -177,25 +177,25 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallEvent_content_button_reject {
|
.mx_LegacyCallEvent_content_button_reject {
|
||||||
span::before {
|
span::before {
|
||||||
mask-image: url('$(res)/img/element-icons/call/hangup.svg');
|
mask-image: url('$(res)/img/element-icons/call/hangup.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallEvent_content_tooltip {
|
.mx_LegacyCallEvent_content_tooltip {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallEvent_narrow {
|
&.mx_LegacyCallEvent_narrow {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: unset;
|
align-items: unset;
|
||||||
gap: $spacing-4 $spacing-16;
|
gap: $spacing-4 $spacing-16;
|
||||||
height: unset;
|
height: unset;
|
||||||
min-width: 290px;
|
min-width: 290px;
|
||||||
|
|
||||||
.mx_CallEvent_iconButton {
|
.mx_LegacyCallEvent_iconButton {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
|
@ -205,7 +205,7 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallEvent_info {
|
.mx_LegacyCallEvent_info {
|
||||||
align-items: unset;
|
align-items: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,8 +213,8 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile[data-layout="bubble"] {
|
.mx_EventTile[data-layout="bubble"] {
|
||||||
.mx_EventTile_e2eIcon + .mx_CallEvent_wrapper {
|
.mx_EventTile_e2eIcon + .mx_LegacyCallEvent_wrapper {
|
||||||
.mx_CallEvent {
|
.mx_LegacyCallEvent {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
/* 5px (gap) + 14px (e2e icon size * mask-size) + 9px (margin-left of e2e icon) */
|
/* 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_EventTile_leftAlignedBubble {
|
||||||
.mx_CallEvent_wrapper {
|
.mx_LegacyCallEvent_wrapper {
|
||||||
.mx_CallEvent {
|
.mx_LegacyCallEvent {
|
||||||
&.mx_CallEvent_narrow {
|
&.mx_LegacyCallEvent_narrow {
|
||||||
gap: $spacing-8 $spacing-4;
|
gap: $spacing-8 $spacing-4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -234,8 +234,8 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_IRCLayout {
|
.mx_IRCLayout {
|
||||||
.mx_CallEvent_wrapper {
|
.mx_LegacyCallEvent_wrapper {
|
||||||
.mx_CallEvent {
|
.mx_LegacyCallEvent {
|
||||||
margin-inline-start: $spacing-4; /* display green line */
|
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_VideoRoomSummary {
|
.mx_RoomTileCallSummary {
|
||||||
.mx_VideoRoomSummary_indicator {
|
.mx_RoomTileCallSummary_text {
|
||||||
&::before {
|
&::before {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
|
@ -28,7 +28,7 @@ limitations under the License.
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_VideoRoomSummary_indicator_active {
|
&.mx_RoomTileCallSummary_text_active {
|
||||||
color: $accent;
|
color: $accent;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
@ -37,7 +37,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoRoomSummary_participants::before {
|
.mx_RoomTileCallSummary_participants::before {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
content: '';
|
content: '';
|
|
@ -15,17 +15,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_IncomingCallToast {
|
.mx_IncomingLegacyCallToast {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
pointer-events: initial; /* restore pointer events so the user can accept/decline */
|
pointer-events: initial; /* restore pointer events so the user can accept/decline */
|
||||||
|
|
||||||
.mx_IncomingCallToast_content {
|
.mx_IncomingLegacyCallToast_content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
|
||||||
.mx_CallEvent_caller {
|
.mx_LegacyCallEvent_caller {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
line-height: $font-18px;
|
line-height: $font-18px;
|
||||||
|
@ -40,7 +40,7 @@ limitations under the License.
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallEvent_type {
|
.mx_LegacyCallEvent_type {
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
line-height: $font-15px;
|
line-height: $font-15px;
|
||||||
color: $tertiary-content;
|
color: $tertiary-content;
|
||||||
|
@ -52,7 +52,7 @@ limitations under the License.
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.mx_CallEvent_type_icon {
|
.mx_LegacyCallEvent_type_icon {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
|
@ -69,28 +69,28 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_IncomingCallToast_content_voice {
|
&.mx_IncomingLegacyCallToast_content_voice {
|
||||||
.mx_CallEvent_type .mx_CallEvent_type_icon::before,
|
.mx_LegacyCallEvent_type .mx_LegacyCallEvent_type_icon::before,
|
||||||
.mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before {
|
.mx_IncomingLegacyCallToast_buttons .mx_IncomingLegacyCallToast_button_accept span::before {
|
||||||
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_IncomingCallToast_content_video {
|
&.mx_IncomingLegacyCallToast_content_video {
|
||||||
.mx_CallEvent_type .mx_CallEvent_type_icon::before,
|
.mx_LegacyCallEvent_type .mx_LegacyCallEvent_type_icon::before,
|
||||||
.mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before {
|
.mx_IncomingLegacyCallToast_buttons .mx_IncomingLegacyCallToast_button_accept span::before {
|
||||||
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_IncomingCallToast_buttons {
|
.mx_IncomingLegacyCallToast_buttons {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|
||||||
.mx_IncomingCallToast_button {
|
.mx_IncomingLegacyCallToast_button {
|
||||||
@mixin CallButton;
|
@mixin LegacyCallButton;
|
||||||
padding: 0px 8px;
|
padding: 0px 8px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -100,13 +100,13 @@ limitations under the License.
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_IncomingCallToast_button_accept span::before {
|
&.mx_IncomingLegacyCallToast_button_accept span::before {
|
||||||
mask-size: 13px;
|
mask-size: 13px;
|
||||||
width: 13px;
|
width: 13px;
|
||||||
height: 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-image: url('$(res)/img/element-icons/call/hangup.svg');
|
||||||
mask-size: 16px;
|
mask-size: 16px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
|
@ -116,7 +116,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_IncomingCallToast_iconButton {
|
.mx_IncomingLegacyCallToast_iconButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 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');
|
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');
|
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 */
|
/* 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. */
|
/* 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 */
|
opacity: 0.001; /* opacity 0 can cause a re-layout */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallViewButtons {
|
.mx_LegacyCallViewButtons {
|
||||||
--CallViewButtons_dropdownButton-size: 16px;
|
--LegacyCallViewButtons_dropdownButton-size: 16px;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -35,7 +35,7 @@ limitations under the License.
|
||||||
z-index: 200; /* To be above _all_ feeds */
|
z-index: 200; /* To be above _all_ feeds */
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
|
|
||||||
.mx_CallViewButtons_button {
|
.mx_LegacyCallViewButtons_button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
background-color: $call-view-button-on-background;
|
background-color: $call-view-button-on-background;
|
||||||
|
@ -66,9 +66,9 @@ limitations under the License.
|
||||||
width: 24px;
|
width: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallViewButtons_dropdownButton {
|
&.mx_LegacyCallViewButtons_dropdownButton {
|
||||||
width: var(--CallViewButtons_dropdownButton-size);
|
width: var(--LegacyCallViewButtons_dropdownButton-size);
|
||||||
height: var(--CallViewButtons_dropdownButton-size);
|
height: var(--LegacyCallViewButtons_dropdownButton-size);
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -80,28 +80,28 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/message/chevron-up.svg');
|
mask-image: url('$(res)/img/element-icons/message/chevron-up.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallViewButtons_dropdownButton_collapsed::before {
|
&.mx_LegacyCallViewButtons_dropdownButton_collapsed::before {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* State buttons */
|
/* State buttons */
|
||||||
&.mx_CallViewButtons_button_on {
|
&.mx_LegacyCallViewButtons_button_on {
|
||||||
background-color: $call-view-button-on-background;
|
background-color: $call-view-button-on-background;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
background-color: $call-view-button-on-foreground;
|
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');
|
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');
|
mask-image: url('$(res)/img/voip/call-view/cam-on.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallViewButtons_button_screensharing {
|
&.mx_LegacyCallViewButtons_button_screensharing {
|
||||||
background-color: $accent;
|
background-color: $accent;
|
||||||
|
|
||||||
&::before {
|
&::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');
|
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;
|
background-color: $call-view-button-off-background;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
background-color: $call-view-button-off-foreground;
|
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');
|
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');
|
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;
|
background-color: $call-view-button-on-background;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
@ -139,7 +139,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallViewButtons_button_sidebar {
|
&.mx_LegacyCallViewButtons_button_sidebar {
|
||||||
background-color: $call-view-button-on-background;
|
background-color: $call-view-button-on-background;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
@ -151,11 +151,11 @@ limitations under the License.
|
||||||
/* State buttons */
|
/* State buttons */
|
||||||
|
|
||||||
/* Stateless buttons */
|
/* Stateless buttons */
|
||||||
&.mx_CallViewButtons_dialpad::before {
|
&.mx_LegacyCallViewButtons_dialpad::before {
|
||||||
mask-image: url('$(res)/img/voip/call-view/dialpad.svg');
|
mask-image: url('$(res)/img/voip/call-view/dialpad.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallViewButtons_button_hangup {
|
&.mx_LegacyCallViewButtons_button_hangup {
|
||||||
background-color: $alert;
|
background-color: $alert;
|
||||||
|
|
||||||
&::before {
|
&::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');
|
mask-image: url('$(res)/img/voip/call-view/more.svg');
|
||||||
}
|
}
|
||||||
/* Stateless buttons */
|
/* Stateless buttons */
|
||||||
|
|
||||||
/* Invisible state */
|
/* Invisible state */
|
||||||
&.mx_CallViewButtons_button_invisible {
|
&.mx_LegacyCallViewButtons_button_invisible {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
|
@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_VideoLobby {
|
.mx_CallLobby {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: $spacing-12;
|
padding: $spacing-12;
|
||||||
color: $video-lobby-primary-content;
|
color: $call-lobby-primary-content;
|
||||||
background-color: $video-lobby-background;
|
background-color: $call-lobby-background;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -33,16 +33,16 @@ limitations under the License.
|
||||||
margin: $spacing-8 auto 0;
|
margin: $spacing-8 auto 0;
|
||||||
|
|
||||||
.mx_FacePile_faces .mx_BaseAvatar_image {
|
.mx_FacePile_faces .mx_BaseAvatar_image {
|
||||||
border-color: $video-lobby-background;
|
border-color: $call-lobby-background;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoLobby_preview {
|
.mx_CallLobby_preview {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
aspect-ratio: 1.5;
|
aspect-ratio: 1.5;
|
||||||
background-color: $video-lobby-system;
|
background-color: $call-lobby-system;
|
||||||
|
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -74,29 +74,29 @@ limitations under the License.
|
||||||
background-color: black;
|
background-color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoLobby_controls {
|
.mx_CallLobby_controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
||||||
background-color: rgba($video-lobby-background, 0.9);
|
background-color: rgba($call-lobby-background, 0.9);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: $spacing-24;
|
gap: $spacing-24;
|
||||||
|
|
||||||
.mx_VideoLobby_deviceButtonWrapper {
|
.mx_CallLobby_deviceButtonWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 6px 0 10px;
|
margin: 6px 0 10px;
|
||||||
|
|
||||||
.mx_VideoLobby_deviceButton {
|
.mx_CallLobby_deviceButton {
|
||||||
$size: 50px;
|
$size: 50px;
|
||||||
|
|
||||||
width: $size;
|
width: $size;
|
||||||
height: $size;
|
height: $size;
|
||||||
|
|
||||||
background-color: $video-lobby-primary-content;
|
background-color: $call-lobby-system;
|
||||||
border-radius: calc($size / 2);
|
border-radius: calc($size / 2);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
@ -105,21 +105,21 @@ limitations under the License.
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-size: 20px;
|
mask-size: 20px;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
background-color: $video-lobby-system;
|
background-color: $call-lobby-primary-content;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_VideoLobby_deviceButton_audio::before {
|
&.mx_CallLobby_deviceButton_audio::before {
|
||||||
mask-image: url('$(res)/img/voip/call-view/mic-off.svg');
|
mask-image: url('$(res)/img/voip/call-view/mic-on.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_VideoLobby_deviceButton_video::before {
|
&.mx_CallLobby_deviceButton_video::before {
|
||||||
mask-image: url('$(res)/img/voip/call-view/cam-off.svg');
|
mask-image: url('$(res)/img/voip/call-view/cam-on.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoLobby_deviceListButton {
|
.mx_CallLobby_deviceListButton {
|
||||||
$size: 15px;
|
$size: 15px;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -128,7 +128,7 @@ limitations under the License.
|
||||||
width: $size;
|
width: $size;
|
||||||
height: $size;
|
height: $size;
|
||||||
|
|
||||||
background-color: $video-lobby-primary-content;
|
background-color: $call-lobby-system;
|
||||||
border-radius: calc($size / 2);
|
border-radius: calc($size / 2);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
@ -137,29 +137,29 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||||
mask-size: $size;
|
mask-size: $size;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
background-color: $video-lobby-system;
|
background-color: $call-lobby-primary-content;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_VideoLobby_deviceButtonWrapper_active {
|
&.mx_CallLobby_deviceButtonWrapper_muted {
|
||||||
.mx_VideoLobby_deviceButton,
|
.mx_CallLobby_deviceButton,
|
||||||
.mx_VideoLobby_deviceListButton {
|
.mx_CallLobby_deviceListButton {
|
||||||
background-color: $video-lobby-system;
|
background-color: $call-lobby-primary-content;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
background-color: $video-lobby-primary-content;
|
background-color: $call-lobby-system;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoLobby_deviceButton {
|
.mx_CallLobby_deviceButton {
|
||||||
&.mx_VideoLobby_deviceButton_audio::before {
|
&.mx_CallLobby_deviceButton_audio::before {
|
||||||
mask-image: url('$(res)/img/voip/call-view/mic-on.svg');
|
mask-image: url('$(res)/img/voip/call-view/mic-off.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_VideoLobby_deviceButton_video::before {
|
&.mx_CallLobby_deviceButton_video::before {
|
||||||
mask-image: url('$(res)/img/voip/call-view/cam-on.svg');
|
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-left: 50px;
|
||||||
padding-right: 50px;
|
padding-right: 50px;
|
||||||
}
|
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_CallPreview {
|
.mx_LegacyCallPreview {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
|
@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_CallView {
|
.mx_LegacyCallView {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: $dark-panel-bg-color;
|
background-color: $dark-panel-bg-color;
|
||||||
padding-left: 8px;
|
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 */
|
/* XXX: PiPContainer sets pointer-events: none - should probably be set back in a better place */
|
||||||
pointer-events: initial;
|
pointer-events: initial;
|
||||||
|
|
||||||
.mx_CallView_toast {
|
.mx_LegacyCallView_toast {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 74px;
|
top: 74px;
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ limitations under the License.
|
||||||
background-color: #17191c;
|
background-color: #17191c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_content_wrapper {
|
.mx_LegacyCallView_content_wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ limitations under the License.
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.mx_CallView_content {
|
.mx_LegacyCallView_content {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -65,12 +65,12 @@ limitations under the License.
|
||||||
|
|
||||||
background-color: $call-view-content-background;
|
background-color: $call-view-content-background;
|
||||||
|
|
||||||
.mx_CallView_status {
|
.mx_LegacyCallView_status {
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
color: $accent-fg-color;
|
color: $accent-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_avatarsContainer {
|
.mx_LegacyCallView_avatarsContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -82,7 +82,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_holdBackground {
|
.mx_LegacyCallView_holdBackground {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 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;
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ limitations under the License.
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_pip &::before {
|
.mx_LegacyCallView_pip &::before {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 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;
|
padding: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -145,7 +145,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallView_pip {
|
&.mx_LegacyCallView_pip {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
|
|
||||||
|
@ -154,16 +154,16 @@ limitations under the License.
|
||||||
background-color: $system;
|
background-color: $system;
|
||||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2);
|
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
.mx_CallViewButtons {
|
.mx_LegacyCallViewButtons {
|
||||||
bottom: 13px;
|
bottom: 13px;
|
||||||
|
|
||||||
.mx_CallViewButtons_button {
|
.mx_LegacyCallViewButtons_button {
|
||||||
width: 34px;
|
width: 34px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
|
|
||||||
&.mx_CallViewButtons_dropdownButton {
|
&.mx_LegacyCallViewButtons_dropdownButton {
|
||||||
width: var(--CallViewButtons_dropdownButton-size);
|
width: var(--LegacyCallViewButtons_dropdownButton-size);
|
||||||
height: var(--CallViewButtons_dropdownButton-size);
|
height: var(--LegacyCallViewButtons_dropdownButton-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
@ -173,12 +173,12 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_content {
|
.mx_LegacyCallView_content {
|
||||||
min-height: 180px;
|
min-height: 180px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallView_large {
|
&.mx_LegacyCallView_large {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -193,7 +193,7 @@ limitations under the License.
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallView_belowWidget {
|
&.mx_LegacyCallView_belowWidget {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_CallViewForRoom {
|
.mx_LegacyCallViewForRoom {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.mx_CallViewForRoom_ResizeWrapper {
|
.mx_LegacyCallViewForRoom_ResizeWrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
&:hover .mx_CallViewForRoom_ResizeHandle {
|
&:hover .mx_LegacyCallViewForRoom_ResizeHandle {
|
||||||
/* Need to use important to override element style attributes */
|
/* Need to use important to override element style attributes */
|
||||||
/* set by re-resizable */
|
/* set by re-resizable */
|
||||||
width: 100% !important;
|
width: 100% !important;
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_CallViewHeader {
|
.mx_LegacyCallViewHeader {
|
||||||
height: 44px;
|
height: 44px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -24,18 +24,18 @@ limitations under the License.
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&.mx_CallViewHeader_pip {
|
&.mx_LegacyCallViewHeader_pip {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallViewHeader_text {
|
.mx_LegacyCallViewHeader_text {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallViewHeader_secondaryCallInfo {
|
.mx_LegacyCallViewHeader_secondaryCallInfo {
|
||||||
&::before {
|
&::before {
|
||||||
content: '·';
|
content: '·';
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
|
@ -43,13 +43,13 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallViewHeader_controls {
|
.mx_LegacyCallViewHeader_controls {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallViewHeader_button {
|
.mx_LegacyCallViewHeader_button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -66,32 +66,32 @@ limitations under the License.
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallViewHeader_button_fullscreen {
|
&.mx_LegacyCallViewHeader_button_fullscreen {
|
||||||
&::before {
|
&::before {
|
||||||
mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
|
mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallViewHeader_button_pin {
|
&.mx_LegacyCallViewHeader_button_pin {
|
||||||
&::before {
|
&::before {
|
||||||
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
|
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallViewHeader_button_expand {
|
&.mx_LegacyCallViewHeader_button_expand {
|
||||||
&::before {
|
&::before {
|
||||||
mask-image: url('$(res)/img/element-icons/call/expand.svg');
|
mask-image: url('$(res)/img/element-icons/call/expand.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallViewHeader_callInfo {
|
.mx_LegacyCallViewHeader_callInfo {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallViewHeader_roomName {
|
.mx_LegacyCallViewHeader_roomName {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: initial;
|
line-height: initial;
|
||||||
|
@ -102,11 +102,11 @@ limitations under the License.
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_secondaryCall_roomName {
|
.mx_LegacyCallView_secondaryCall_roomName {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallViewHeader_icon {
|
.mx_LegacyCallViewHeader_icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
height: 16px;
|
height: 16px;
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_CallViewSidebar {
|
.mx_LegacyCallViewSidebar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_CallViewSidebar_pipMode {
|
&.mx_LegacyCallViewSidebar_pipMode {
|
||||||
top: 16px;
|
top: 16px;
|
||||||
bottom: unset;
|
bottom: unset;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
|
@ -188,9 +188,9 @@ $call-view-content-background: $quinary-content;
|
||||||
|
|
||||||
$video-feed-secondary-background: $system;
|
$video-feed-secondary-background: $system;
|
||||||
|
|
||||||
$video-lobby-system: $system;
|
$call-lobby-system: $system;
|
||||||
$video-lobby-background: $background;
|
$call-lobby-background: $background;
|
||||||
$video-lobby-primary-content: $primary-content;
|
$call-lobby-primary-content: $primary-content;
|
||||||
/* ******************** */
|
/* ******************** */
|
||||||
|
|
||||||
/* Location sharing */
|
/* Location sharing */
|
||||||
|
|
|
@ -119,9 +119,9 @@ $call-view-content-background: $quinary-content;
|
||||||
|
|
||||||
$video-feed-secondary-background: $system;
|
$video-feed-secondary-background: $system;
|
||||||
|
|
||||||
$video-lobby-system: $system;
|
$call-lobby-system: $system;
|
||||||
$video-lobby-background: $background;
|
$call-lobby-background: $background;
|
||||||
$video-lobby-primary-content: $primary-content;
|
$call-lobby-primary-content: $primary-content;
|
||||||
|
|
||||||
$roomlist-filter-active-bg-color: $panel-actions;
|
$roomlist-filter-active-bg-color: $panel-actions;
|
||||||
$roomlist-bg-color: $header-panel-bg-color;
|
$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 */
|
$video-feed-secondary-background: #394049; /* XXX: Color from dark theme */
|
||||||
|
|
||||||
/* All of these are from dark theme */
|
/* All of these are from dark theme */
|
||||||
$video-lobby-system: #21262C;
|
$call-lobby-system: #21262C;
|
||||||
$video-lobby-background: #15191E;
|
$call-lobby-background: #15191E;
|
||||||
$video-lobby-primary-content: #FFFFFF;
|
$call-lobby-primary-content: #FFFFFF;
|
||||||
|
|
||||||
$username-variant1-color: #368bd6;
|
$username-variant1-color: #368bd6;
|
||||||
$username-variant2-color: #ac3ba8;
|
$username-variant2-color: #ac3ba8;
|
||||||
|
|
|
@ -287,9 +287,9 @@ $video-feed-secondary-background: #394049; /* XXX: Color from dark theme */
|
||||||
$voipcall-plinth-color: $system;
|
$voipcall-plinth-color: $system;
|
||||||
|
|
||||||
/* All of these are from dark theme */
|
/* All of these are from dark theme */
|
||||||
$video-lobby-system: #21262C;
|
$call-lobby-system: #21262C;
|
||||||
$video-lobby-background: #15191E;
|
$call-lobby-background: #15191E;
|
||||||
$video-lobby-primary-content: #FFFFFF;
|
$call-lobby-primary-content: #FFFFFF;
|
||||||
/* ******************** */
|
/* ******************** */
|
||||||
|
|
||||||
/* One-off colors */
|
/* 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 type { Renderer } from "react-dom";
|
||||||
import RightPanelStore from "../stores/right-panel/RightPanelStore";
|
import RightPanelStore from "../stores/right-panel/RightPanelStore";
|
||||||
import WidgetStore from "../stores/WidgetStore";
|
import WidgetStore from "../stores/WidgetStore";
|
||||||
import CallHandler from "../CallHandler";
|
import LegacyCallHandler from "../LegacyCallHandler";
|
||||||
import UserActivity from "../UserActivity";
|
import UserActivity from "../UserActivity";
|
||||||
import { ModalWidgetStore } from "../stores/ModalWidgetStore";
|
import { ModalWidgetStore } from "../stores/ModalWidgetStore";
|
||||||
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||||
|
@ -89,7 +89,7 @@ declare global {
|
||||||
mxRightPanelStore: RightPanelStore;
|
mxRightPanelStore: RightPanelStore;
|
||||||
mxWidgetStore: WidgetStore;
|
mxWidgetStore: WidgetStore;
|
||||||
mxWidgetLayoutStore: WidgetLayoutStore;
|
mxWidgetLayoutStore: WidgetLayoutStore;
|
||||||
mxCallHandler: CallHandler;
|
mxLegacyCallHandler: LegacyCallHandler;
|
||||||
mxUserActivity: UserActivity;
|
mxUserActivity: UserActivity;
|
||||||
mxModalWidgetStore: ModalWidgetStore;
|
mxModalWidgetStore: ModalWidgetStore;
|
||||||
mxVoipUserMapper: VoipUserMapper;
|
mxVoipUserMapper: VoipUserMapper;
|
||||||
|
|
|
@ -54,7 +54,7 @@ import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
import { ensureDMExists } from './createRoom';
|
import { ensureDMExists } from './createRoom';
|
||||||
import { Container, WidgetLayoutStore } from './stores/widgets/WidgetLayoutStore';
|
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 ToastStore from './stores/ToastStore';
|
||||||
import Resend from './Resend';
|
import Resend from './Resend';
|
||||||
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
||||||
|
@ -100,7 +100,7 @@ interface ThirdpartyLookupResponse {
|
||||||
fields: ThirdpartyLookupResponseFields;
|
fields: ThirdpartyLookupResponseFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CallHandlerEvent {
|
export enum LegacyCallHandlerEvent {
|
||||||
CallsChanged = "calls_changed",
|
CallsChanged = "calls_changed",
|
||||||
CallChangeRoom = "call_change_room",
|
CallChangeRoom = "call_change_room",
|
||||||
SilencedCallsChanged = "silenced_calls_changed",
|
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,
|
* placing, answering, rejecting and hanging up calls. It also handles ringing,
|
||||||
* PSTN support and other things.
|
* 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
|
private calls = new Map<string, MatrixCall>(); // roomId -> call
|
||||||
// Calls started as an attended transfer, ie. with the intention of transferring another
|
// Calls started as an attended transfer, ie. with the intention of transferring another
|
||||||
// call with a different party to this one.
|
// call with a different party to this one.
|
||||||
|
@ -130,11 +130,11 @@ export default class CallHandler extends EventEmitter {
|
||||||
private silencedCalls = new Set<string>(); // callIds
|
private silencedCalls = new Set<string>(); // callIds
|
||||||
|
|
||||||
public static get instance() {
|
public static get instance() {
|
||||||
if (!window.mxCallHandler) {
|
if (!window.mxLegacyCallHandler) {
|
||||||
window.mxCallHandler = new CallHandler();
|
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 {
|
public silenceCall(callId: string): void {
|
||||||
this.silencedCalls.add(callId);
|
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
|
// Don't pause audio if we have calls which are still ringing
|
||||||
if (this.areAnyCallsUnsilenced()) return;
|
if (this.areAnyCallsUnsilenced()) return;
|
||||||
|
@ -195,7 +195,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
|
|
||||||
public unSilenceCall(callId: string): void {
|
public unSilenceCall(callId: string): void {
|
||||||
this.silencedCalls.delete(callId);
|
this.silencedCalls.delete(callId);
|
||||||
this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
|
this.emit(LegacyCallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
|
||||||
this.play(AudioID.Ring);
|
this.play(AudioID.Ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,7 +311,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mappedRoomId = CallHandler.instance.roomIdForCall(call);
|
const mappedRoomId = LegacyCallHandler.instance.roomIdForCall(call);
|
||||||
if (this.getCallForRoom(mappedRoomId)) {
|
if (this.getCallForRoom(mappedRoomId)) {
|
||||||
logger.log(
|
logger.log(
|
||||||
"Got incoming call for room " + mappedRoomId +
|
"Got incoming call for room " + mappedRoomId +
|
||||||
|
@ -389,7 +389,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
public play(audioId: AudioID): void {
|
public play(audioId: AudioID): void {
|
||||||
const logPrefix = `CallHandler.play(${audioId}):`;
|
const logPrefix = `LegacyCallHandler.play(${audioId}):`;
|
||||||
logger.debug(`${logPrefix} beginning of function`);
|
logger.debug(`${logPrefix} beginning of function`);
|
||||||
// TODO: Attach an invisible element for this instead
|
// TODO: Attach an invisible element for this instead
|
||||||
// which listens?
|
// which listens?
|
||||||
|
@ -424,7 +424,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(audioId: AudioID): void {
|
public pause(audioId: AudioID): void {
|
||||||
const logPrefix = `CallHandler.pause(${audioId}):`;
|
const logPrefix = `LegacyCallHandler.pause(${audioId}):`;
|
||||||
logger.debug(`${logPrefix} beginning of function`);
|
logger.debug(`${logPrefix} beginning of function`);
|
||||||
// TODO: Attach an invisible element for this instead
|
// TODO: Attach an invisible element for this instead
|
||||||
// which listens?
|
// which listens?
|
||||||
|
@ -688,32 +688,32 @@ export default class CallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private setCallState(call: MatrixCall, status: CallState): void {
|
private setCallState(call: MatrixCall, status: CallState): void {
|
||||||
const mappedRoomId = CallHandler.instance.roomIdForCall(call);
|
const mappedRoomId = LegacyCallHandler.instance.roomIdForCall(call);
|
||||||
|
|
||||||
logger.log(
|
logger.log(
|
||||||
`Call state in ${mappedRoomId} changed to ${status}`,
|
`Call state in ${mappedRoomId} changed to ${status}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const toastKey = getIncomingCallToastKey(call.callId);
|
const toastKey = getIncomingLegacyCallToastKey(call.callId);
|
||||||
if (status === CallState.Ringing) {
|
if (status === CallState.Ringing) {
|
||||||
ToastStore.sharedInstance().addOrReplaceToast({
|
ToastStore.sharedInstance().addOrReplaceToast({
|
||||||
key: toastKey,
|
key: toastKey,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
component: IncomingCallToast,
|
component: IncomingLegacyCallToast,
|
||||||
bodyClassName: "mx_IncomingCallToast",
|
bodyClassName: "mx_IncomingLegacyCallToast",
|
||||||
props: { call },
|
props: { call },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ToastStore.sharedInstance().dismissToast(toastKey);
|
ToastStore.sharedInstance().dismissToast(toastKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit(CallHandlerEvent.CallState, mappedRoomId, status);
|
this.emit(LegacyCallHandlerEvent.CallState, mappedRoomId, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeCallForRoom(roomId: string): void {
|
private removeCallForRoom(roomId: string): void {
|
||||||
logger.log("Removing call for room ", roomId);
|
logger.log("Removing call for room ", roomId);
|
||||||
this.calls.delete(roomId);
|
this.calls.delete(roomId);
|
||||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
this.emit(LegacyCallHandlerEvent.CallsChanged, this.calls);
|
||||||
}
|
}
|
||||||
|
|
||||||
private showICEFallbackPrompt(): void {
|
private showICEFallbackPrompt(): void {
|
||||||
|
@ -1115,9 +1115,9 @@ export default class CallHandler extends EventEmitter {
|
||||||
|
|
||||||
// Should we always emit CallsChanged too?
|
// Should we always emit CallsChanged too?
|
||||||
if (changedRooms) {
|
if (changedRooms) {
|
||||||
this.emit(CallHandlerEvent.CallChangeRoom, call);
|
this.emit(LegacyCallHandlerEvent.CallChangeRoom, call);
|
||||||
} else {
|
} 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 { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY } from "./BasePlatform";
|
||||||
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
||||||
import { PosthogAnalytics } from "./PosthogAnalytics";
|
import { PosthogAnalytics } from "./PosthogAnalytics";
|
||||||
import CallHandler from './CallHandler';
|
import LegacyCallHandler from './LegacyCallHandler';
|
||||||
import LifecycleCustomisations from "./customisations/Lifecycle";
|
import LifecycleCustomisations from "./customisations/Lifecycle";
|
||||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||||
import { _t } from "./languageHandler";
|
import { _t } from "./languageHandler";
|
||||||
|
@ -59,8 +59,6 @@ import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialo
|
||||||
import { setSentryUser } from "./sentry";
|
import { setSentryUser } from "./sentry";
|
||||||
import SdkConfig from "./SdkConfig";
|
import SdkConfig from "./SdkConfig";
|
||||||
import { DialogOpener } from "./utils/DialogOpener";
|
import { DialogOpener } from "./utils/DialogOpener";
|
||||||
import VideoChannelStore from "./stores/VideoChannelStore";
|
|
||||||
import { fixStuckDevices } from "./utils/VideoChannelUtils";
|
|
||||||
import { Action } from "./dispatcher/actions";
|
import { Action } from "./dispatcher/actions";
|
||||||
import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLocalStorageSettingsHandler";
|
import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLocalStorageSettingsHandler";
|
||||||
import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload";
|
import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload";
|
||||||
|
@ -808,7 +806,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
|
||||||
DMRoomMap.makeShared().start();
|
DMRoomMap.makeShared().start();
|
||||||
IntegrationManagers.sharedInstance().startWatching();
|
IntegrationManagers.sharedInstance().startWatching();
|
||||||
ActiveWidgetStore.instance.start();
|
ActiveWidgetStore.instance.start();
|
||||||
CallHandler.instance.start();
|
LegacyCallHandler.instance.start();
|
||||||
|
|
||||||
// Start Mjolnir even though we haven't checked the feature flag yet. Starting
|
// 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
|
// 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
|
// Now that we have a MatrixClientPeg, update the Jitsi info
|
||||||
Jitsi.getInstance().start();
|
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
|
// dispatch that we finished starting up to wire up any other bits
|
||||||
// of the matrix client that cannot be set prior to starting up.
|
// of the matrix client that cannot be set prior to starting up.
|
||||||
dis.dispatch({ action: 'client_started' });
|
dis.dispatch({ action: 'client_started' });
|
||||||
|
@ -932,7 +925,7 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
|
||||||
*/
|
*/
|
||||||
export function stopMatrixClient(unsetClient = true): void {
|
export function stopMatrixClient(unsetClient = true): void {
|
||||||
Notifier.stop();
|
Notifier.stop();
|
||||||
CallHandler.instance.stop();
|
LegacyCallHandler.instance.stop();
|
||||||
UserActivity.sharedInstance().stop();
|
UserActivity.sharedInstance().stop();
|
||||||
TypingStore.sharedInstance().reset();
|
TypingStore.sharedInstance().reset();
|
||||||
Presence.stop();
|
Presence.stop();
|
||||||
|
|
|
@ -137,4 +137,18 @@ export default class MediaDeviceHandler extends EventEmitter {
|
||||||
case MediaDeviceKindEnum.VideoInput: return this.getVideoInput();
|
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 UserActivity from "./UserActivity";
|
||||||
import { mediaFromMxc } from "./customisations/Media";
|
import { mediaFromMxc } from "./customisations/Media";
|
||||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||||
import CallHandler from "./CallHandler";
|
import LegacyCallHandler from "./LegacyCallHandler";
|
||||||
import VoipUserMapper from "./VoipUserMapper";
|
import VoipUserMapper from "./VoipUserMapper";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -397,7 +397,7 @@ export const Notifier = {
|
||||||
|
|
||||||
_evaluateEvent: function(ev: MatrixEvent) {
|
_evaluateEvent: function(ev: MatrixEvent) {
|
||||||
let roomId = ev.getRoomId();
|
let roomId = ev.getRoomId();
|
||||||
if (CallHandler.instance.getSupportsVirtualRooms()) {
|
if (LegacyCallHandler.instance.getSupportsVirtualRooms()) {
|
||||||
// Attempt to translate a virtual room to a native one
|
// Attempt to translate a virtual room to a native one
|
||||||
const nativeRoomId = VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(roomId);
|
const nativeRoomId = VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(roomId);
|
||||||
if (nativeRoomId) {
|
if (nativeRoomId) {
|
||||||
|
|
|
@ -52,7 +52,7 @@ import SdkConfig from "./SdkConfig";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import { UIComponent, UIFeature } from "./settings/UIFeature";
|
import { UIComponent, UIFeature } from "./settings/UIFeature";
|
||||||
import { CHAT_EFFECTS } from "./effects";
|
import { CHAT_EFFECTS } from "./effects";
|
||||||
import CallHandler from "./CallHandler";
|
import LegacyCallHandler from "./LegacyCallHandler";
|
||||||
import { guessAndSetDMRoom } from "./Rooms";
|
import { guessAndSetDMRoom } from "./Rooms";
|
||||||
import { upgradeRoom } from './utils/RoomUpgrade';
|
import { upgradeRoom } from './utils/RoomUpgrade';
|
||||||
import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog';
|
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"),
|
description: _td("Switches to this room's virtual room, if it has one"),
|
||||||
category: CommandCategories.advanced,
|
category: CommandCategories.advanced,
|
||||||
isEnabled(): boolean {
|
isEnabled(): boolean {
|
||||||
return CallHandler.instance.getSupportsVirtualRooms() && !isCurrentLocalRoom();
|
return LegacyCallHandler.instance.getSupportsVirtualRooms() && !isCurrentLocalRoom();
|
||||||
},
|
},
|
||||||
runFn: (roomId) => {
|
runFn: (roomId) => {
|
||||||
return success((async () => {
|
return success((async () => {
|
||||||
|
@ -1212,7 +1212,7 @@ export const Commands = [
|
||||||
|
|
||||||
return success((async () => {
|
return success((async () => {
|
||||||
if (isPhoneNumber) {
|
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) {
|
if (!results || results.length === 0 || !results[0].userid) {
|
||||||
throw newTranslatableError("Unable to find Matrix ID for phone number");
|
throw newTranslatableError("Unable to find Matrix ID for phone number");
|
||||||
}
|
}
|
||||||
|
@ -1269,7 +1269,7 @@ export const Commands = [
|
||||||
category: CommandCategories.other,
|
category: CommandCategories.other,
|
||||||
isEnabled: () => !isCurrentLocalRoom(),
|
isEnabled: () => !isCurrentLocalRoom(),
|
||||||
runFn: function(roomId, args) {
|
runFn: function(roomId, args) {
|
||||||
const call = CallHandler.instance.getCallForRoom(roomId);
|
const call = LegacyCallHandler.instance.getCallForRoom(roomId);
|
||||||
if (!call) {
|
if (!call) {
|
||||||
return reject(newTranslatableError("No active call in this room"));
|
return reject(newTranslatableError("No active call in this room"));
|
||||||
}
|
}
|
||||||
|
@ -1284,7 +1284,7 @@ export const Commands = [
|
||||||
category: CommandCategories.other,
|
category: CommandCategories.other,
|
||||||
isEnabled: () => !isCurrentLocalRoom(),
|
isEnabled: () => !isCurrentLocalRoom(),
|
||||||
runFn: function(roomId, args) {
|
runFn: function(roomId, args) {
|
||||||
const call = CallHandler.instance.getCallForRoom(roomId);
|
const call = LegacyCallHandler.instance.getCallForRoom(roomId);
|
||||||
if (!call) {
|
if (!call) {
|
||||||
return reject(newTranslatableError("No active call in this room"));
|
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 { ensureVirtualRoomExists } from './createRoom';
|
||||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
import DMRoomMap from "./utils/DMRoomMap";
|
import DMRoomMap from "./utils/DMRoomMap";
|
||||||
import CallHandler from './CallHandler';
|
import LegacyCallHandler from './LegacyCallHandler';
|
||||||
import { VIRTUAL_ROOM_EVENT_TYPE } from "./call-types";
|
import { VIRTUAL_ROOM_EVENT_TYPE } from "./call-types";
|
||||||
import { findDMForUser } from './utils/dm/findDMForUser';
|
import { findDMForUser } from './utils/dm/findDMForUser';
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ export default class VoipUserMapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async userToVirtualUser(userId: string): Promise<string> {
|
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;
|
if (results.length === 0 || !results[0].fields.lookup_success) return null;
|
||||||
return results[0].userid;
|
return results[0].userid;
|
||||||
}
|
}
|
||||||
|
@ -118,11 +118,11 @@ export default class VoipUserMapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onNewInvitedRoom(invitedRoom: Room): Promise<void> {
|
public async onNewInvitedRoom(invitedRoom: Room): Promise<void> {
|
||||||
if (!CallHandler.instance.getSupportsVirtualRooms()) return;
|
if (!LegacyCallHandler.instance.getSupportsVirtualRooms()) return;
|
||||||
|
|
||||||
const inviterId = invitedRoom.getDMInviter();
|
const inviterId = invitedRoom.getDMInviter();
|
||||||
logger.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
|
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) {
|
if (result.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import classNames from "classnames";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import RoomList from "../views/rooms/RoomList";
|
import RoomList from "../views/rooms/RoomList";
|
||||||
import CallHandler from "../../CallHandler";
|
import LegacyCallHandler from "../../LegacyCallHandler";
|
||||||
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
|
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import RoomSearch from "./RoomSearch";
|
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
|
// If we have dialer support, show a button to bring up the dial pad
|
||||||
// to start a new call
|
// to start a new call
|
||||||
if (CallHandler.instance.getSupportsPstnProtocol()) {
|
if (LegacyCallHandler.instance.getSupportsPstnProtocol()) {
|
||||||
dialPadButton =
|
dialPadButton =
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className={classNames("mx_LeftPanel_dialPadButton", {})}
|
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 { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
|
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../LegacyCallHandler';
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
|
|
||||||
export enum CallEventGrouperEvent {
|
export enum LegacyCallEventGrouperEvent {
|
||||||
StateChanged = "state_changed",
|
StateChanged = "state_changed",
|
||||||
SilencedChanged = "silenced_changed",
|
SilencedChanged = "silenced_changed",
|
||||||
LengthChanged = "length_changed",
|
LengthChanged = "length_changed",
|
||||||
|
@ -44,10 +44,10 @@ export enum CustomCallState {
|
||||||
Missed = "missed",
|
Missed = "missed",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildCallEventGroupers(
|
export function buildLegacyCallEventGroupers(
|
||||||
callEventGroupers: Map<string, CallEventGrouper>,
|
callEventGroupers: Map<string, LegacyCallEventGrouper>,
|
||||||
events?: MatrixEvent[],
|
events?: MatrixEvent[],
|
||||||
): Map<string, CallEventGrouper> {
|
): Map<string, LegacyCallEventGrouper> {
|
||||||
const newCallEventGroupers = new Map();
|
const newCallEventGroupers = new Map();
|
||||||
events?.forEach(ev => {
|
events?.forEach(ev => {
|
||||||
if (!ev.getType().startsWith("m.call.") && !ev.getType().startsWith("org.matrix.call.")) {
|
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;
|
const callId = ev.getContent().call_id;
|
||||||
if (!newCallEventGroupers.has(callId)) {
|
if (!newCallEventGroupers.has(callId)) {
|
||||||
if (callEventGroupers.has(callId)) {
|
if (callEventGroupers.has(callId)) {
|
||||||
// reuse the CallEventGrouper object where possible
|
// reuse the LegacyCallEventGrouper object where possible
|
||||||
newCallEventGroupers.set(callId, callEventGroupers.get(callId));
|
newCallEventGroupers.set(callId, callEventGroupers.get(callId));
|
||||||
} else {
|
} else {
|
||||||
newCallEventGroupers.set(callId, new CallEventGrouper());
|
newCallEventGroupers.set(callId, new LegacyCallEventGrouper());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newCallEventGroupers.get(callId).add(ev);
|
newCallEventGroupers.get(callId).add(ev);
|
||||||
|
@ -68,7 +68,7 @@ export function buildCallEventGroupers(
|
||||||
return newCallEventGroupers;
|
return newCallEventGroupers;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CallEventGrouper extends EventEmitter {
|
export default class LegacyCallEventGrouper extends EventEmitter {
|
||||||
private events: Set<MatrixEvent> = new Set<MatrixEvent>();
|
private events: Set<MatrixEvent> = new Set<MatrixEvent>();
|
||||||
private call: MatrixCall;
|
private call: MatrixCall;
|
||||||
public state: CallState | CustomCallState;
|
public state: CallState | CustomCallState;
|
||||||
|
@ -76,8 +76,10 @@ export default class CallEventGrouper extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
CallHandler.instance.addListener(CallHandlerEvent.CallsChanged, this.setCall);
|
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallsChanged, this.setCall);
|
||||||
CallHandler.instance.addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
LegacyCallHandler.instance.addListener(
|
||||||
|
LegacyCallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get invite(): MatrixEvent {
|
private get invite(): MatrixEvent {
|
||||||
|
@ -138,31 +140,31 @@ export default class CallEventGrouper extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSilencedCallsChanged = () => {
|
private onSilencedCallsChanged = () => {
|
||||||
const newState = CallHandler.instance.isCallSilenced(this.callId);
|
const newState = LegacyCallHandler.instance.isCallSilenced(this.callId);
|
||||||
this.emit(CallEventGrouperEvent.SilencedChanged, newState);
|
this.emit(LegacyCallEventGrouperEvent.SilencedChanged, newState);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onLengthChanged = (length: number): void => {
|
private onLengthChanged = (length: number): void => {
|
||||||
this.emit(CallEventGrouperEvent.LengthChanged, length);
|
this.emit(LegacyCallEventGrouperEvent.LengthChanged, length);
|
||||||
};
|
};
|
||||||
|
|
||||||
public answerCall = (): void => {
|
public answerCall = (): void => {
|
||||||
CallHandler.instance.answerCall(this.roomId);
|
LegacyCallHandler.instance.answerCall(this.roomId);
|
||||||
};
|
};
|
||||||
|
|
||||||
public rejectCall = (): void => {
|
public rejectCall = (): void => {
|
||||||
CallHandler.instance.hangupOrReject(this.roomId, true);
|
LegacyCallHandler.instance.hangupOrReject(this.roomId, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
public callBack = (): void => {
|
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 = () => {
|
public toggleSilenced = () => {
|
||||||
const silenced = CallHandler.instance.isCallSilenced(this.callId);
|
const silenced = LegacyCallHandler.instance.isCallSilenced(this.callId);
|
||||||
silenced ?
|
silenced ?
|
||||||
CallHandler.instance.unSilenceCall(this.callId) :
|
LegacyCallHandler.instance.unSilenceCall(this.callId) :
|
||||||
CallHandler.instance.silenceCall(this.callId);
|
LegacyCallHandler.instance.silenceCall(this.callId);
|
||||||
};
|
};
|
||||||
|
|
||||||
private setCallListeners() {
|
private setCallListeners() {
|
||||||
|
@ -182,13 +184,13 @@ export default class CallEventGrouper extends EventEmitter {
|
||||||
else if (this.hangup) this.state = CallState.Ended;
|
else if (this.hangup) this.state = CallState.Ended;
|
||||||
else if (this.invite && this.call) this.state = CallState.Connecting;
|
else if (this.invite && this.call) this.state = CallState.Connecting;
|
||||||
}
|
}
|
||||||
this.emit(CallEventGrouperEvent.StateChanged, this.state);
|
this.emit(LegacyCallEventGrouperEvent.StateChanged, this.state);
|
||||||
};
|
};
|
||||||
|
|
||||||
private setCall = () => {
|
private setCall = () => {
|
||||||
if (this.call) return;
|
if (this.call) return;
|
||||||
|
|
||||||
this.call = CallHandler.instance.getCallById(this.callId);
|
this.call = LegacyCallHandler.instance.getCallById(this.callId);
|
||||||
this.setCallListeners();
|
this.setCallListeners();
|
||||||
this.setState();
|
this.setState();
|
||||||
};
|
};
|
|
@ -51,8 +51,8 @@ import HostSignupContainer from '../views/host_signup/HostSignupContainer';
|
||||||
import { getKeyBindingsManager } from '../../KeyBindingsManager';
|
import { getKeyBindingsManager } from '../../KeyBindingsManager';
|
||||||
import { IOpts } from "../../createRoom";
|
import { IOpts } from "../../createRoom";
|
||||||
import SpacePanel from "../views/spaces/SpacePanel";
|
import SpacePanel from "../views/spaces/SpacePanel";
|
||||||
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
|
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../LegacyCallHandler';
|
||||||
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
|
import AudioFeedArrayForLegacyCall from '../views/voip/AudioFeedArrayForLegacyCall';
|
||||||
import { OwnProfileStore } from '../../stores/OwnProfileStore';
|
import { OwnProfileStore } from '../../stores/OwnProfileStore';
|
||||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
import RoomView from './RoomView';
|
import RoomView from './RoomView';
|
||||||
|
@ -146,7 +146,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
// use compact timeline view
|
// use compact timeline view
|
||||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||||
usageLimitDismissed: false,
|
usageLimitDismissed: false,
|
||||||
activeCalls: CallHandler.instance.getAllActiveCalls(),
|
activeCalls: LegacyCallHandler.instance.getAllActiveCalls(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// stash the MatrixClient in case we log out before we are unmounted
|
// stash the MatrixClient in case we log out before we are unmounted
|
||||||
|
@ -163,7 +163,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.addEventListener('keydown', this.onNativeKeyDown, false);
|
document.addEventListener('keydown', this.onNativeKeyDown, false);
|
||||||
CallHandler.instance.addListener(CallHandlerEvent.CallState, this.onCallState);
|
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.onCallState);
|
||||||
|
|
||||||
this.updateServerNoticeEvents();
|
this.updateServerNoticeEvents();
|
||||||
|
|
||||||
|
@ -195,7 +195,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('keydown', this.onNativeKeyDown, false);
|
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.AccountData, this.onAccountData);
|
||||||
this._matrixClient.removeListener(ClientEvent.Sync, this.onSync);
|
this._matrixClient.removeListener(ClientEvent.Sync, this.onSync);
|
||||||
this._matrixClient.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
this._matrixClient.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||||
|
@ -207,7 +207,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onCallState = (): void => {
|
private onCallState = (): void => {
|
||||||
const activeCalls = CallHandler.instance.getAllActiveCalls();
|
const activeCalls = LegacyCallHandler.instance.getAllActiveCalls();
|
||||||
if (activeCalls === this.state.activeCalls) return;
|
if (activeCalls === this.state.activeCalls) return;
|
||||||
this.setState({ activeCalls });
|
this.setState({ activeCalls });
|
||||||
};
|
};
|
||||||
|
@ -658,7 +658,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const audioFeedArraysForCalls = this.state.activeCalls.map((call) => {
|
const audioFeedArraysForCalls = this.state.activeCalls.map((call) => {
|
||||||
return (
|
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 { copyPlaintext } from "../../utils/strings";
|
||||||
import { PosthogAnalytics } from '../../PosthogAnalytics';
|
import { PosthogAnalytics } from '../../PosthogAnalytics';
|
||||||
import { initSentry } from "../../sentry";
|
import { initSentry } from "../../sentry";
|
||||||
import CallHandler from "../../CallHandler";
|
import LegacyCallHandler from "../../LegacyCallHandler";
|
||||||
import { showSpaceInvite } from "../../utils/space";
|
import { showSpaceInvite } from "../../utils/space";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
@ -128,7 +128,7 @@ import { ViewStartChatOrReusePayload } from '../../dispatcher/payloads/ViewStart
|
||||||
import { IConfigOptions } from "../../IConfigOptions";
|
import { IConfigOptions } from "../../IConfigOptions";
|
||||||
import { SnakedObject } from "../../utils/SnakedObject";
|
import { SnakedObject } from "../../utils/SnakedObject";
|
||||||
import { leaveRoomBehaviour } from "../../utils/leave-behaviour";
|
import { leaveRoomBehaviour } from "../../utils/leave-behaviour";
|
||||||
import VideoChannelStore from "../../stores/VideoChannelStore";
|
import { CallStore } from "../../stores/CallStore";
|
||||||
import { IRoomStateEventsActionPayload } from "../../actions/MatrixActionCreators";
|
import { IRoomStateEventsActionPayload } from "../../actions/MatrixActionCreators";
|
||||||
import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";
|
import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";
|
||||||
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
|
||||||
|
@ -576,9 +576,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'logout':
|
case 'logout':
|
||||||
CallHandler.instance.hangupAllCalls();
|
LegacyCallHandler.instance.hangupAllCalls();
|
||||||
if (VideoChannelStore.instance.connected) VideoChannelStore.instance.setDisconnected();
|
Promise.all([...CallStore.instance.activeCalls].map(call => call.disconnect()))
|
||||||
Lifecycle.logout();
|
.finally(() => Lifecycle.logout());
|
||||||
break;
|
break;
|
||||||
case 'require_registration':
|
case 'require_registration':
|
||||||
startAnyRegistrationFlow(payload as any);
|
startAnyRegistrationFlow(payload as any);
|
||||||
|
|
|
@ -40,7 +40,7 @@ import DMRoomMap from "../../utils/DMRoomMap";
|
||||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||||
import HistoryTile from "../views/rooms/HistoryTile";
|
import HistoryTile from "../views/rooms/HistoryTile";
|
||||||
import defaultDispatcher from '../../dispatcher/dispatcher';
|
import defaultDispatcher from '../../dispatcher/dispatcher';
|
||||||
import CallEventGrouper from "./CallEventGrouper";
|
import LegacyCallEventGrouper from "./LegacyCallEventGrouper";
|
||||||
import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
|
import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
|
||||||
import ScrollPanel, { IScrollState } from "./ScrollPanel";
|
import ScrollPanel, { IScrollState } from "./ScrollPanel";
|
||||||
import GenericEventListSummary from '../views/elements/GenericEventListSummary';
|
import GenericEventListSummary from '../views/elements/GenericEventListSummary';
|
||||||
|
@ -188,7 +188,7 @@ interface IProps {
|
||||||
hideThreadedMessages?: boolean;
|
hideThreadedMessages?: boolean;
|
||||||
disableGrouping?: boolean;
|
disableGrouping?: boolean;
|
||||||
|
|
||||||
callEventGroupers: Map<string, CallEventGrouper>;
|
callEventGroupers: Map<string, LegacyCallEventGrouper>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
|
|
@ -45,7 +45,7 @@ import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
||||||
import ResizeNotifier from '../../utils/ResizeNotifier';
|
import ResizeNotifier from '../../utils/ResizeNotifier';
|
||||||
import ContentMessages from '../../ContentMessages';
|
import ContentMessages from '../../ContentMessages';
|
||||||
import Modal from '../../Modal';
|
import Modal from '../../Modal';
|
||||||
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
|
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../LegacyCallHandler';
|
||||||
import dis, { defaultDispatcher } from '../../dispatcher/dispatcher';
|
import dis, { defaultDispatcher } from '../../dispatcher/dispatcher';
|
||||||
import * as Rooms from '../../Rooms';
|
import * as Rooms from '../../Rooms';
|
||||||
import eventSearch, { searchPagination } from '../../Searching';
|
import eventSearch, { searchPagination } from '../../Searching';
|
||||||
|
@ -78,7 +78,7 @@ import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||||
import { containsEmoji } from '../../effects/utils';
|
import { containsEmoji } from '../../effects/utils';
|
||||||
import { CHAT_EFFECTS } from '../../effects';
|
import { CHAT_EFFECTS } from '../../effects';
|
||||||
import WidgetStore from "../../stores/WidgetStore";
|
import WidgetStore from "../../stores/WidgetStore";
|
||||||
import VideoRoomView from "./VideoRoomView";
|
import { VideoRoomView } from "./VideoRoomView";
|
||||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
import Notifier from "../../Notifier";
|
import Notifier from "../../Notifier";
|
||||||
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
|
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
|
||||||
|
@ -810,7 +810,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
callState: callState,
|
callState: callState,
|
||||||
});
|
});
|
||||||
|
|
||||||
CallHandler.instance.on(CallHandlerEvent.CallState, this.onCallState);
|
LegacyCallHandler.instance.on(LegacyCallHandlerEvent.CallState, this.onCallState);
|
||||||
window.addEventListener('beforeunload', this.onPageUnload);
|
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.)
|
// (We could use isMounted, but facebook have deprecated that.)
|
||||||
this.unmounted = true;
|
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
|
// update the scroll map before we get unmounted
|
||||||
if (this.state.roomId) {
|
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
|
// cancel any pending calls to the throttled updated
|
||||||
this.updateRoomMembers.cancel();
|
this.updateRoomMembers.cancel();
|
||||||
|
@ -1655,7 +1655,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onCallPlaced = (type: CallType): void => {
|
private onCallPlaced = (type: CallType): void => {
|
||||||
CallHandler.instance.placeCall(this.state.room?.roomId, type);
|
LegacyCallHandler.instance.placeCall(this.state.room?.roomId, type);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onAppsClick = () => {
|
private onAppsClick = () => {
|
||||||
|
@ -1872,7 +1872,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
if (!this.state.room) {
|
if (!this.state.room) {
|
||||||
return null;
|
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,
|
// 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 Spinner from "../views/elements/Spinner";
|
||||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
||||||
import ErrorDialog from '../views/dialogs/ErrorDialog';
|
import ErrorDialog from '../views/dialogs/ErrorDialog';
|
||||||
import CallEventGrouper, { buildCallEventGroupers } from "./CallEventGrouper";
|
import LegacyCallEventGrouper, { buildLegacyCallEventGroupers } from "./LegacyCallEventGrouper";
|
||||||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
import { getKeyBindingsManager } from "../../KeyBindingsManager";
|
||||||
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||||
|
@ -240,8 +240,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
private readReceiptActivityTimer: Timer;
|
private readReceiptActivityTimer: Timer;
|
||||||
private readMarkerActivityTimer: Timer;
|
private readMarkerActivityTimer: Timer;
|
||||||
|
|
||||||
// A map of <callId, CallEventGrouper>
|
// A map of <callId, LegacyCallEventGrouper>
|
||||||
private callEventGroupers = new Map<string, CallEventGrouper>();
|
private callEventGroupers = new Map<string, LegacyCallEventGrouper>();
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -493,7 +493,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
this.timelineWindow.unpaginate(count, backwards);
|
this.timelineWindow.unpaginate(count, backwards);
|
||||||
|
|
||||||
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
|
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
|
||||||
this.buildCallEventGroupers(events);
|
this.buildLegacyCallEventGroupers(events);
|
||||||
const newState: Partial<IState> = {
|
const newState: Partial<IState> = {
|
||||||
events,
|
events,
|
||||||
liveEvents,
|
liveEvents,
|
||||||
|
@ -555,7 +555,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
debuglog("paginate complete backwards:"+backwards+"; success:"+r);
|
debuglog("paginate complete backwards:"+backwards+"; success:"+r);
|
||||||
|
|
||||||
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
|
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
|
||||||
this.buildCallEventGroupers(events);
|
this.buildLegacyCallEventGroupers(events);
|
||||||
const newState: Partial<IState> = {
|
const newState: Partial<IState> = {
|
||||||
[paginatingKey]: false,
|
[paginatingKey]: false,
|
||||||
[canPaginateKey]: r,
|
[canPaginateKey]: r,
|
||||||
|
@ -686,7 +686,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
if (this.unmounted) { return; }
|
if (this.unmounted) { return; }
|
||||||
|
|
||||||
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
|
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
|
||||||
this.buildCallEventGroupers(events);
|
this.buildLegacyCallEventGroupers(events);
|
||||||
const lastLiveEvent = liveEvents[liveEvents.length - 1];
|
const lastLiveEvent = liveEvents[liveEvents.length - 1];
|
||||||
|
|
||||||
const updatedState: Partial<IState> = {
|
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,
|
// TODO: We should restrict this to only events in our timeline,
|
||||||
// but possibly the event tile itself should just update when this
|
// but possibly the event tile itself should just update when this
|
||||||
// happens to save us re-rendering the whole timeline.
|
// happens to save us re-rendering the whole timeline.
|
||||||
this.buildCallEventGroupers(this.state.events);
|
this.buildLegacyCallEventGroupers(this.state.events);
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1405,7 +1405,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
onLoaded();
|
onLoaded();
|
||||||
} else {
|
} else {
|
||||||
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE);
|
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE);
|
||||||
this.buildCallEventGroupers();
|
this.buildLegacyCallEventGroupers();
|
||||||
this.setState({
|
this.setState({
|
||||||
events: [],
|
events: [],
|
||||||
liveEvents: [],
|
liveEvents: [],
|
||||||
|
@ -1426,7 +1426,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
|
||||||
const state = this.getEvents();
|
const state = this.getEvents();
|
||||||
this.buildCallEventGroupers(state.events);
|
this.buildLegacyCallEventGroupers(state.events);
|
||||||
this.setState(state);
|
this.setState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1707,8 +1707,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
eventType: EventType | string,
|
eventType: EventType | string,
|
||||||
) => this.props.timelineSet.relations?.getChildEventsForEvent(eventId, relationType, eventType);
|
) => this.props.timelineSet.relations?.getChildEventsForEvent(eventId, relationType, eventType);
|
||||||
|
|
||||||
private buildCallEventGroupers(events?: MatrixEvent[]): void {
|
private buildLegacyCallEventGroupers(events?: MatrixEvent[]): void {
|
||||||
this.callEventGroupers = buildCallEventGroupers(this.callEventGroupers, events);
|
this.callEventGroupers = buildLegacyCallEventGroupers(this.callEventGroupers, events);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -14,79 +14,46 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, useContext, useState, useMemo, useEffect } from "react";
|
import React, { FC, useContext, useEffect } from "react";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
|
||||||
|
|
||||||
|
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 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 AppTile from "../views/elements/AppTile";
|
||||||
import VideoLobby from "../views/voip/VideoLobby";
|
import { CallLobby } from "../views/voip/CallLobby";
|
||||||
|
|
||||||
interface IProps {
|
interface Props {
|
||||||
room: Room;
|
room: Room;
|
||||||
resizing: boolean;
|
resizing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VideoRoomView: FC<IProps> = ({ room, resizing }) => {
|
const LoadedVideoRoomView: FC<Props & { call: Call }> = ({ room, resizing, call }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
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
|
// We'll take this opportunity to tidy up our room state
|
||||||
const [widgetStoreReady, setWidgetStoreReady] = useState(Boolean(WidgetStore.instance.matrixClient));
|
useEffect(() => { call?.clean(); }, [call]);
|
||||||
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)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const app: IApp = useMemo(() => {
|
if (!call) return null;
|
||||||
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;
|
|
||||||
|
|
||||||
return <div className="mx_VideoRoomView">
|
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 */ }
|
{ /* We render the widget even if we're disconnected, so it stays loaded */ }
|
||||||
<AppTile
|
<AppTile
|
||||||
app={app}
|
app={call.widget}
|
||||||
room={room}
|
room={room}
|
||||||
userId={cli.credentials.userId}
|
userId={cli.credentials.userId}
|
||||||
creatorUserId={app.creatorUserId}
|
creatorUserId={call.widget.creatorUserId}
|
||||||
waitForIframeLoad={app.waitForIframeLoad}
|
waitForIframeLoad={call.widget.waitForIframeLoad}
|
||||||
showMenubar={false}
|
showMenubar={false}
|
||||||
pointerEvents={resizing ? "none" : null}
|
pointerEvents={resizing ? "none" : undefined}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</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 { _t } from '../../../languageHandler';
|
||||||
import ContextMenu, { IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu';
|
import ContextMenu, { IProps as IContextMenuProps, MenuItem } from '../../structures/ContextMenu';
|
||||||
import CallHandler from '../../../CallHandler';
|
import LegacyCallHandler from '../../../LegacyCallHandler';
|
||||||
|
|
||||||
interface IProps extends IContextMenuProps {
|
interface IProps extends IContextMenuProps {
|
||||||
call: MatrixCall;
|
call: MatrixCall;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CallContextMenu extends React.Component<IProps> {
|
export default class LegacyCallContextMenu extends React.Component<IProps> {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
// js-sdk User object. Not required because it might not exist.
|
// js-sdk User object. Not required because it might not exist.
|
||||||
user: PropTypes.object,
|
user: PropTypes.object,
|
||||||
|
@ -42,13 +42,13 @@ export default class CallContextMenu extends React.Component<IProps> {
|
||||||
};
|
};
|
||||||
|
|
||||||
onUnholdClick = () => {
|
onUnholdClick = () => {
|
||||||
CallHandler.instance.setActiveCallRoomId(this.props.call.roomId);
|
LegacyCallHandler.instance.setActiveCallRoomId(this.props.call.roomId);
|
||||||
|
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
onTransferClick = () => {
|
onTransferClick = () => {
|
||||||
CallHandler.instance.showTransferDialog(this.props.call);
|
LegacyCallHandler.instance.showTransferDialog(this.props.call);
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -58,13 +58,13 @@ export default class CallContextMenu extends React.Component<IProps> {
|
||||||
|
|
||||||
let transferItem;
|
let transferItem;
|
||||||
if (this.props.call.opponentCanBeTransferred()) {
|
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") }
|
{ _t("Transfer") }
|
||||||
</MenuItem>;
|
</MenuItem>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ContextMenu {...this.props}>
|
return <ContextMenu {...this.props}>
|
||||||
<MenuItem className="mx_CallContextMenu_item" onClick={handler}>
|
<MenuItem className="mx_LegacyCallContextMenu_item" onClick={handler}>
|
||||||
{ holdUnholdCaption }
|
{ holdUnholdCaption }
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{ transferItem }
|
{ transferItem }
|
|
@ -56,7 +56,7 @@ import QuestionDialog from "./QuestionDialog";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
|
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
|
||||||
import CallHandler from "../../../CallHandler";
|
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||||
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
|
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
|
||||||
import CopyableText from "../elements/CopyableText";
|
import CopyableText from "../elements/CopyableText";
|
||||||
import { ScreenName } from '../../../PosthogTrackers';
|
import { ScreenName } from '../../../PosthogTrackers';
|
||||||
|
@ -510,13 +510,13 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CallHandler.instance.startTransferToMatrixID(
|
LegacyCallHandler.instance.startTransferToMatrixID(
|
||||||
this.props.call,
|
this.props.call,
|
||||||
targetIds[0],
|
targetIds[0],
|
||||||
this.state.consultFirst,
|
this.state.consultFirst,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
CallHandler.instance.startTransferToPhoneNumber(
|
LegacyCallHandler.instance.startTransferToPhoneNumber(
|
||||||
this.props.call,
|
this.props.call,
|
||||||
this.state.dialPadValue,
|
this.state.dialPadValue,
|
||||||
this.state.consultFirst,
|
this.state.consultFirst,
|
||||||
|
|
|
@ -36,10 +36,9 @@ import { aboveLeftOf, ContextMenuButton } from "../../structures/ContextMenu";
|
||||||
import PersistedElement, { getPersistKey } from "./PersistedElement";
|
import PersistedElement, { getPersistKey } from "./PersistedElement";
|
||||||
import { WidgetType } from "../../../widgets/WidgetType";
|
import { WidgetType } from "../../../widgets/WidgetType";
|
||||||
import { ElementWidget, StopGapWidget } from "../../../stores/widgets/StopGapWidget";
|
import { ElementWidget, StopGapWidget } from "../../../stores/widgets/StopGapWidget";
|
||||||
import { ElementWidgetActions } from "../../../stores/widgets/ElementWidgetActions";
|
|
||||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||||
import CallHandler from '../../../CallHandler';
|
import LegacyCallHandler from '../../../LegacyCallHandler';
|
||||||
import { IApp } from "../../../stores/WidgetStore";
|
import { IApp } from "../../../stores/WidgetStore";
|
||||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||||
import { OwnProfileStore } from '../../../stores/OwnProfileStore';
|
import { OwnProfileStore } from '../../../stores/OwnProfileStore';
|
||||||
|
@ -305,7 +304,6 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private setupSgListeners() {
|
private setupSgListeners() {
|
||||||
this.sgWidget.on("preparing", this.onWidgetPreparing);
|
this.sgWidget.on("preparing", this.onWidgetPreparing);
|
||||||
this.sgWidget.on("ready", this.onWidgetReady);
|
|
||||||
// emits when the capabilities have been set up or changed
|
// emits when the capabilities have been set up or changed
|
||||||
this.sgWidget.on("capabilitiesNotified", this.onWidgetCapabilitiesNotified);
|
this.sgWidget.on("capabilitiesNotified", this.onWidgetCapabilitiesNotified);
|
||||||
}
|
}
|
||||||
|
@ -313,7 +311,6 @@ export default class AppTile extends React.Component<IProps, IState> {
|
||||||
private stopSgListeners() {
|
private stopSgListeners() {
|
||||||
if (!this.sgWidget) return;
|
if (!this.sgWidget) return;
|
||||||
this.sgWidget.off("preparing", this.onWidgetPreparing);
|
this.sgWidget.off("preparing", this.onWidgetPreparing);
|
||||||
this.sgWidget.off("ready", this.onWidgetReady);
|
|
||||||
this.sgWidget.off("capabilitiesNotified", this.onWidgetCapabilitiesNotified);
|
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) {
|
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.
|
// 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 });
|
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 => {
|
private onWidgetCapabilitiesNotified = (): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
requiresClient: this.sgWidget.widgetApi.hasCapability(ElementWidgetCapabilities.RequiresClient),
|
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";
|
style.display = this.props.visible ? "block" : "none";
|
||||||
|
|
||||||
const tooltip = (
|
const tooltip = (
|
||||||
<div className={tooltipClasses} style={style}>
|
<div role="tooltip" className={tooltipClasses} style={style}>
|
||||||
<div className="mx_Tooltip_chevron" />
|
<div className="mx_Tooltip_chevron" />
|
||||||
{ this.props.label }
|
{ this.props.label }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,7 +21,10 @@ import classNames from 'classnames';
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
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 AccessibleButton from '../elements/AccessibleButton';
|
||||||
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
|
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
|
||||||
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||||
|
@ -32,7 +35,7 @@ const MAX_NON_NARROW_WIDTH = 450 / 70 * 100;
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
callEventGrouper: CallEventGrouper;
|
callEventGrouper: LegacyCallEventGrouper;
|
||||||
timestamp?: JSX.Element;
|
timestamp?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +46,7 @@ interface IState {
|
||||||
length: number;
|
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 wrapperElement = createRef<HTMLDivElement>();
|
||||||
private resizeObserver: ResizeObserver;
|
private resizeObserver: ResizeObserver;
|
||||||
|
|
||||||
|
@ -59,18 +62,18 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.callEventGrouper.addListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
|
this.props.callEventGrouper.addListener(LegacyCallEventGrouperEvent.StateChanged, this.onStateChanged);
|
||||||
this.props.callEventGrouper.addListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
this.props.callEventGrouper.addListener(LegacyCallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
||||||
this.props.callEventGrouper.addListener(CallEventGrouperEvent.LengthChanged, this.onLengthChanged);
|
this.props.callEventGrouper.addListener(LegacyCallEventGrouperEvent.LengthChanged, this.onLengthChanged);
|
||||||
|
|
||||||
this.resizeObserver = new ResizeObserver(this.resizeObserverCallback);
|
this.resizeObserver = new ResizeObserver(this.resizeObserverCallback);
|
||||||
this.wrapperElement.current && this.resizeObserver.observe(this.wrapperElement.current);
|
this.wrapperElement.current && this.resizeObserver.observe(this.wrapperElement.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
|
this.props.callEventGrouper.removeListener(LegacyCallEventGrouperEvent.StateChanged, this.onStateChanged);
|
||||||
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
this.props.callEventGrouper.removeListener(LegacyCallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
||||||
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.LengthChanged, this.onLengthChanged);
|
this.props.callEventGrouper.removeListener(LegacyCallEventGrouperEvent.LengthChanged, this.onLengthChanged);
|
||||||
|
|
||||||
this.resizeObserver.disconnect();
|
this.resizeObserver.disconnect();
|
||||||
}
|
}
|
||||||
|
@ -97,7 +100,7 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
private renderCallBackButton(text: string): JSX.Element {
|
private renderCallBackButton(text: string): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<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}
|
onClick={this.props.callEventGrouper.callBack}
|
||||||
kind="primary"
|
kind="primary"
|
||||||
>
|
>
|
||||||
|
@ -108,9 +111,9 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
private renderSilenceIcon(): JSX.Element {
|
private renderSilenceIcon(): JSX.Element {
|
||||||
const silenceClass = classNames({
|
const silenceClass = classNames({
|
||||||
"mx_CallEvent_iconButton": true,
|
"mx_LegacyCallEvent_iconButton": true,
|
||||||
"mx_CallEvent_unSilence": this.state.silenced,
|
"mx_LegacyCallEvent_unSilence": this.state.silenced,
|
||||||
"mx_CallEvent_silence": !this.state.silenced,
|
"mx_LegacyCallEvent_silence": !this.state.silenced,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -130,17 +133,17 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_LegacyCallEvent_content">
|
||||||
{ silenceIcon }
|
{ silenceIcon }
|
||||||
<AccessibleButton
|
<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}
|
onClick={this.props.callEventGrouper.rejectCall}
|
||||||
kind="danger"
|
kind="danger"
|
||||||
>
|
>
|
||||||
<span> { _t("Decline") } </span>
|
<span> { _t("Decline") } </span>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<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}
|
onClick={this.props.callEventGrouper.answerCall}
|
||||||
kind="primary"
|
kind="primary"
|
||||||
>
|
>
|
||||||
|
@ -156,7 +159,7 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
if (gotRejected) {
|
if (gotRejected) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_LegacyCallEvent_content">
|
||||||
{ _t("Call declined") }
|
{ _t("Call declined") }
|
||||||
{ this.renderCallBackButton(_t("Call back")) }
|
{ this.renderCallBackButton(_t("Call back")) }
|
||||||
{ this.props.timestamp }
|
{ this.props.timestamp }
|
||||||
|
@ -175,14 +178,14 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
text += " • " + formatCallTime(duration);
|
text += " • " + formatCallTime(duration);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_LegacyCallEvent_content">
|
||||||
{ text }
|
{ text }
|
||||||
{ this.props.timestamp }
|
{ this.props.timestamp }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (hangupReason === CallErrorCode.InviteTimeout) {
|
} else if (hangupReason === CallErrorCode.InviteTimeout) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_LegacyCallEvent_content">
|
||||||
{ _t("No answer") }
|
{ _t("No answer") }
|
||||||
{ this.renderCallBackButton(_t("Call back")) }
|
{ this.renderCallBackButton(_t("Call back")) }
|
||||||
{ this.props.timestamp }
|
{ this.props.timestamp }
|
||||||
|
@ -212,10 +215,10 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_LegacyCallEvent_content">
|
||||||
<InfoTooltip
|
<InfoTooltip
|
||||||
tooltip={reason}
|
tooltip={reason}
|
||||||
className="mx_CallEvent_content_tooltip"
|
className="mx_LegacyCallEvent_content_tooltip"
|
||||||
kind={InfoTooltipKind.Warning}
|
kind={InfoTooltipKind.Warning}
|
||||||
/>
|
/>
|
||||||
{ _t("Connection failed") }
|
{ _t("Connection failed") }
|
||||||
|
@ -226,7 +229,7 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
if (state === CallState.Connected) {
|
if (state === CallState.Connected) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_LegacyCallEvent_content">
|
||||||
<Clock seconds={this.state.length} aria-live="off" />
|
<Clock seconds={this.state.length} aria-live="off" />
|
||||||
{ this.props.timestamp }
|
{ this.props.timestamp }
|
||||||
</div>
|
</div>
|
||||||
|
@ -234,7 +237,7 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
if (state === CallState.Connecting) {
|
if (state === CallState.Connecting) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_LegacyCallEvent_content">
|
||||||
{ _t("Connecting") }
|
{ _t("Connecting") }
|
||||||
{ this.props.timestamp }
|
{ this.props.timestamp }
|
||||||
</div>
|
</div>
|
||||||
|
@ -242,7 +245,7 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
if (state === CustomCallState.Missed) {
|
if (state === CustomCallState.Missed) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_LegacyCallEvent_content">
|
||||||
{ _t("Missed call") }
|
{ _t("Missed call") }
|
||||||
{ this.renderCallBackButton(_t("Call back")) }
|
{ this.renderCallBackButton(_t("Call back")) }
|
||||||
{ this.props.timestamp }
|
{ this.props.timestamp }
|
||||||
|
@ -251,7 +254,7 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_LegacyCallEvent_content">
|
||||||
{ _t("The call is in an unknown state!") }
|
{ _t("The call is in an unknown state!") }
|
||||||
{ this.props.timestamp }
|
{ this.props.timestamp }
|
||||||
</div>
|
</div>
|
||||||
|
@ -266,13 +269,13 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
const callState = this.state.callState;
|
const callState = this.state.callState;
|
||||||
const hangupReason = this.props.callEventGrouper.hangupReason;
|
const hangupReason = this.props.callEventGrouper.hangupReason;
|
||||||
const content = this.renderContent(callState);
|
const content = this.renderContent(callState);
|
||||||
const className = classNames("mx_CallEvent", {
|
const className = classNames("mx_LegacyCallEvent", {
|
||||||
mx_CallEvent_voice: isVoice,
|
mx_LegacyCallEvent_voice: isVoice,
|
||||||
mx_CallEvent_video: !isVoice,
|
mx_LegacyCallEvent_video: !isVoice,
|
||||||
mx_CallEvent_narrow: this.state.narrow,
|
mx_LegacyCallEvent_narrow: this.state.narrow,
|
||||||
mx_CallEvent_missed: callState === CustomCallState.Missed,
|
mx_LegacyCallEvent_missed: callState === CustomCallState.Missed,
|
||||||
mx_CallEvent_noAnswer: callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout,
|
mx_LegacyCallEvent_noAnswer: callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout,
|
||||||
mx_CallEvent_rejected: callState === CallState.Ended && this.props.callEventGrouper.gotRejected,
|
mx_LegacyCallEvent_rejected: callState === CallState.Ended && this.props.callEventGrouper.gotRejected,
|
||||||
});
|
});
|
||||||
let silenceIcon;
|
let silenceIcon;
|
||||||
if (this.state.narrow && this.state.callState === CallState.Ringing) {
|
if (this.state.narrow && this.state.callState === CallState.Ringing) {
|
||||||
|
@ -280,21 +283,21 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_wrapper" ref={this.wrapperElement}>
|
<div className="mx_LegacyCallEvent_wrapper" ref={this.wrapperElement}>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{ silenceIcon }
|
{ silenceIcon }
|
||||||
<div className="mx_CallEvent_info">
|
<div className="mx_LegacyCallEvent_info">
|
||||||
<MemberAvatar
|
<MemberAvatar
|
||||||
member={event.sender}
|
member={event.sender}
|
||||||
width={32}
|
width={32}
|
||||||
height={32}
|
height={32}
|
||||||
/>
|
/>
|
||||||
<div className="mx_CallEvent_info_basic">
|
<div className="mx_LegacyCallEvent_info_basic">
|
||||||
<div className="mx_CallEvent_sender">
|
<div className="mx_LegacyCallEvent_sender">
|
||||||
{ sender }
|
{ sender }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_CallEvent_type">
|
<div className="mx_LegacyCallEvent_type">
|
||||||
<div className="mx_CallEvent_type_icon" />
|
<div className="mx_LegacyCallEvent_type_icon" />
|
||||||
{ callType }
|
{ callType }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -27,7 +27,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||||
import CallViewForRoom from '../voip/CallViewForRoom';
|
import LegacyCallViewForRoom from '../voip/LegacyCallViewForRoom';
|
||||||
import { objectHasDiff } from "../../../utils/objects";
|
import { objectHasDiff } from "../../../utils/objects";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
@ -123,7 +123,7 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const callView = (
|
const callView = (
|
||||||
<CallViewForRoom
|
<LegacyCallViewForRoom
|
||||||
roomId={this.props.room.roomId}
|
roomId={this.props.room.roomId}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
showApps={this.props.showApps}
|
showApps={this.props.showApps}
|
||||||
|
|
|
@ -47,7 +47,7 @@ import EditorStateTransfer from "../../../utils/EditorStateTransfer";
|
||||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||||
import NotificationBadge from "./NotificationBadge";
|
import NotificationBadge from "./NotificationBadge";
|
||||||
import CallEventGrouper from "../../structures/CallEventGrouper";
|
import LegacyCallEventGrouper from "../../structures/LegacyCallEventGrouper";
|
||||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
import { Action } from '../../../dispatcher/actions';
|
import { Action } from '../../../dispatcher/actions';
|
||||||
import PlatformPeg from '../../../PlatformPeg';
|
import PlatformPeg from '../../../PlatformPeg';
|
||||||
|
@ -200,8 +200,8 @@ interface IProps {
|
||||||
// Helper to build permalinks for the room
|
// Helper to build permalinks for the room
|
||||||
permalinkCreator?: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
|
|
||||||
// CallEventGrouper for this event
|
// LegacyCallEventGrouper for this event
|
||||||
callEventGrouper?: CallEventGrouper;
|
callEventGrouper?: LegacyCallEventGrouper;
|
||||||
|
|
||||||
// Symbol of the root node
|
// Symbol of the root node
|
||||||
as?: string;
|
as?: string;
|
||||||
|
|
|
@ -19,11 +19,11 @@ import React, { createRef } from "react";
|
||||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import type { Call } from "../../../models/Call";
|
||||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||||
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||||
import defaultDispatcher from '../../../dispatcher/dispatcher';
|
import defaultDispatcher from '../../../dispatcher/dispatcher';
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
|
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
|
||||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
|
@ -45,8 +45,9 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||||
import { RoomViewStore } from "../../../stores/RoomViewStore";
|
import { RoomViewStore } from "../../../stores/RoomViewStore";
|
||||||
import VideoRoomSummary from "./VideoRoomSummary";
|
import { RoomTileCallSummary } from "./RoomTileCallSummary";
|
||||||
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
|
import { RoomGeneralContextMenu } from "../context_menus/RoomGeneralContextMenu";
|
||||||
|
import { CallStore, CallStoreEvent } from "../../../stores/CallStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -61,6 +62,7 @@ interface IState {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
notificationsMenuPosition: PartialDOMRect;
|
notificationsMenuPosition: PartialDOMRect;
|
||||||
generalMenuPosition: PartialDOMRect;
|
generalMenuPosition: PartialDOMRect;
|
||||||
|
call: Call | null;
|
||||||
messagePreview?: string;
|
messagePreview?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +81,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
private roomTileRef = createRef<HTMLDivElement>();
|
private roomTileRef = createRef<HTMLDivElement>();
|
||||||
private notificationState: NotificationState;
|
private notificationState: NotificationState;
|
||||||
private roomProps: RoomEchoChamber;
|
private roomProps: RoomEchoChamber;
|
||||||
private isVideoRoom: boolean;
|
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -88,6 +89,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
selected: RoomViewStore.instance.getRoomId() === this.props.room.roomId,
|
selected: RoomViewStore.instance.getRoomId() === this.props.room.roomId,
|
||||||
notificationsMenuPosition: null,
|
notificationsMenuPosition: null,
|
||||||
generalMenuPosition: null,
|
generalMenuPosition: null,
|
||||||
|
call: CallStore.instance.get(this.props.room.roomId),
|
||||||
// generatePreview() will return nothing if the user has previews disabled
|
// generatePreview() will return nothing if the user has previews disabled
|
||||||
messagePreview: "",
|
messagePreview: "",
|
||||||
};
|
};
|
||||||
|
@ -95,7 +97,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||||
this.roomProps = EchoChamber.forRoom(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) => {
|
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.notificationState.on(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||||
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||||
this.props.room.on(RoomEvent.Name, this.onRoomNameUpdate);
|
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() {
|
public componentWillUnmount() {
|
||||||
|
@ -166,6 +172,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
defaultDispatcher.unregister(this.dispatcherRef);
|
defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate);
|
this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||||
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||||
|
CallStore.instance.off(CallStoreEvent.Call, this.onCallChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload) => {
|
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() {
|
private async generatePreview() {
|
||||||
if (!this.showMessagePreview) {
|
if (!this.showMessagePreview) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -362,10 +373,10 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let subtitle;
|
let subtitle;
|
||||||
if (this.isVideoRoom) {
|
if (this.state.call) {
|
||||||
subtitle = (
|
subtitle = (
|
||||||
<div className="mx_RoomTile_subtitle">
|
<div className="mx_RoomTile_subtitle">
|
||||||
<VideoRoomSummary room={this.props.room} />
|
<RoomTileCallSummary call={this.state.call} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (this.showMessagePreview && this.state.messagePreview) {
|
} else if (this.showMessagePreview && this.state.messagePreview) {
|
||||||
|
|
|
@ -16,66 +16,56 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import classNames from "classnames";
|
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 { _t, TranslatedString } from "../../../languageHandler";
|
||||||
import {
|
import { useConnectionState, useParticipants } from "../../../hooks/useCall";
|
||||||
ConnectionState,
|
import { ConnectionState } from "../../../models/Call";
|
||||||
useConnectionState,
|
|
||||||
useConnectedMembers,
|
|
||||||
useJitsiParticipants,
|
|
||||||
} from "../../../utils/VideoChannelUtils";
|
|
||||||
|
|
||||||
interface IProps {
|
interface Props {
|
||||||
room: Room;
|
call: Call;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VideoRoomSummary: FC<IProps> = ({ room }) => {
|
export const RoomTileCallSummary: FC<Props> = ({ call }) => {
|
||||||
const connectionState = useConnectionState(room);
|
const connectionState = useConnectionState(call);
|
||||||
const videoMembers = useConnectedMembers(room, connectionState === ConnectionState.Connected);
|
const participants = useParticipants(call);
|
||||||
const jitsiParticipants = useJitsiParticipants(room);
|
|
||||||
|
|
||||||
let indicator: TranslatedString;
|
let text: TranslatedString;
|
||||||
let active: boolean;
|
let active: boolean;
|
||||||
let participantCount: number;
|
|
||||||
|
|
||||||
switch (connectionState) {
|
switch (connectionState) {
|
||||||
case ConnectionState.Disconnected:
|
case ConnectionState.Disconnected:
|
||||||
indicator = _t("Video");
|
text = _t("Video");
|
||||||
active = false;
|
active = false;
|
||||||
participantCount = videoMembers.size;
|
|
||||||
break;
|
break;
|
||||||
case ConnectionState.Connecting:
|
case ConnectionState.Connecting:
|
||||||
indicator = _t("Joining…");
|
text = _t("Joining…");
|
||||||
active = true;
|
active = true;
|
||||||
participantCount = videoMembers.size;
|
|
||||||
break;
|
break;
|
||||||
case ConnectionState.Connected:
|
case ConnectionState.Connected:
|
||||||
indicator = _t("Joined");
|
case ConnectionState.Disconnecting:
|
||||||
|
text = _t("Joined");
|
||||||
active = true;
|
active = true;
|
||||||
participantCount = jitsiParticipants.length;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <span className="mx_VideoRoomSummary">
|
return <span className="mx_RoomTileCallSummary">
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mx_VideoRoomSummary_indicator",
|
"mx_RoomTileCallSummary_text",
|
||||||
{ "mx_VideoRoomSummary_indicator_active": active },
|
{ "mx_RoomTileCallSummary_text_active": active },
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{ indicator }
|
{ text }
|
||||||
</span>
|
</span>
|
||||||
{ participantCount ? <>
|
{ participants.size ? <>
|
||||||
{ " · " }
|
{ " · " }
|
||||||
<span
|
<span
|
||||||
className="mx_VideoRoomSummary_participants"
|
className="mx_RoomTileCallSummary_participants"
|
||||||
aria-label={_t("%(count)s participants", { count: participantCount })}
|
aria-label={_t("%(count)s participants", { count: participants.size })}
|
||||||
>
|
>
|
||||||
{ participantCount }
|
{ participants.size }
|
||||||
</span>
|
</span>
|
||||||
</> : null }
|
</> : null }
|
||||||
</span>;
|
</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VideoRoomSummary;
|
|
|
@ -26,7 +26,7 @@ import DateSeparator from "../messages/DateSeparator";
|
||||||
import EventTile from "./EventTile";
|
import EventTile from "./EventTile";
|
||||||
import { shouldFormContinuation } from "../../structures/MessagePanel";
|
import { shouldFormContinuation } from "../../structures/MessagePanel";
|
||||||
import { wantsDateSeparator } from "../../../DateUtils";
|
import { wantsDateSeparator } from "../../../DateUtils";
|
||||||
import CallEventGrouper, { buildCallEventGroupers } from "../../structures/CallEventGrouper";
|
import LegacyCallEventGrouper, { buildLegacyCallEventGroupers } from "../../structures/LegacyCallEventGrouper";
|
||||||
import { haveRendererForEvent } from "../../../events/EventTileFactory";
|
import { haveRendererForEvent } from "../../../events/EventTileFactory";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
@ -44,17 +44,17 @@ export default class SearchResultTile extends React.Component<IProps> {
|
||||||
static contextType = RoomContext;
|
static contextType = RoomContext;
|
||||||
public context!: React.ContextType<typeof RoomContext>;
|
public context!: React.ContextType<typeof RoomContext>;
|
||||||
|
|
||||||
// A map of <callId, CallEventGrouper>
|
// A map of <callId, LegacyCallEventGrouper>
|
||||||
private callEventGroupers = new Map<string, CallEventGrouper>();
|
private callEventGroupers = new Map<string, LegacyCallEventGrouper>();
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.buildCallEventGroupers(this.props.searchResult.context.getTimeline());
|
this.buildLegacyCallEventGroupers(this.props.searchResult.context.getTimeline());
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildCallEventGroupers(events?: MatrixEvent[]): void {
|
private buildLegacyCallEventGroupers(events?: MatrixEvent[]): void {
|
||||||
this.callEventGroupers = buildCallEventGroupers(this.callEventGroupers, events);
|
this.callEventGroupers = buildLegacyCallEventGroupers(this.callEventGroupers, events);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
|
|
@ -28,7 +28,7 @@ interface IState {
|
||||||
feeds: Array<CallFeed>;
|
feeds: Array<CallFeed>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AudioFeedArrayForCall extends React.Component<IProps, IState> {
|
export default class AudioFeedArrayForLegacyCall extends React.Component<IProps, IState> {
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
|
@ -14,15 +14,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 classNames from "classnames";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||||
import { useConnectedMembers } from "../../../utils/VideoChannelUtils";
|
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
|
||||||
import VideoChannelStore from "../../../stores/VideoChannelStore";
|
import { useParticipants } from "../../../hooks/useCall";
|
||||||
|
import { CallStore } from "../../../stores/CallStore";
|
||||||
|
import { Call } from "../../../models/Call";
|
||||||
import IconizedContextMenu, {
|
import IconizedContextMenu, {
|
||||||
IconizedContextMenuOption,
|
IconizedContextMenuOption,
|
||||||
IconizedContextMenuOptionList,
|
IconizedContextMenuOptionList,
|
||||||
|
@ -34,25 +36,22 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import FacePile from "../elements/FacePile";
|
import FacePile from "../elements/FacePile";
|
||||||
import MemberAvatar from "../avatars/MemberAvatar";
|
import MemberAvatar from "../avatars/MemberAvatar";
|
||||||
|
|
||||||
interface IDeviceButtonProps {
|
interface DeviceButtonProps {
|
||||||
kind: string;
|
kind: string;
|
||||||
devices: MediaDeviceInfo[];
|
devices: MediaDeviceInfo[];
|
||||||
setDevice: (device: MediaDeviceInfo) => void;
|
setDevice: (device: MediaDeviceInfo) => void;
|
||||||
deviceListLabel: string;
|
deviceListLabel: string;
|
||||||
active: boolean;
|
fallbackDeviceLabel: (n: number) => string;
|
||||||
|
muted: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
toggle: () => void;
|
toggle: () => void;
|
||||||
activeTitle: string;
|
unmutedTitle: string;
|
||||||
inactiveTitle: string;
|
mutedTitle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeviceButton: FC<IDeviceButtonProps> = ({
|
const DeviceButton: FC<DeviceButtonProps> = ({
|
||||||
kind, devices, setDevice, deviceListLabel, active, disabled, toggle, activeTitle, inactiveTitle,
|
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();
|
const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu();
|
||||||
let contextMenu;
|
let contextMenu;
|
||||||
if (menuDisplayed) {
|
if (menuDisplayed) {
|
||||||
|
@ -61,13 +60,13 @@ const DeviceButton: FC<IDeviceButtonProps> = ({
|
||||||
closeMenu();
|
closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonRect = buttonRef.current.getBoundingClientRect();
|
const buttonRect = buttonRef.current!.getBoundingClientRect();
|
||||||
contextMenu = <IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
contextMenu = <IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
||||||
<IconizedContextMenuOptionList>
|
<IconizedContextMenuOptionList>
|
||||||
{ labelledDevices.map(d =>
|
{ devices.map((d, index) =>
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
key={d.deviceId}
|
key={d.deviceId}
|
||||||
label={d.label}
|
label={d.label || fallbackDeviceLabel(index + 1)}
|
||||||
onClick={() => selectDevice(d)}
|
onClick={() => selectDevice(d)}
|
||||||
/>,
|
/>,
|
||||||
) }
|
) }
|
||||||
|
@ -78,21 +77,20 @@ const DeviceButton: FC<IDeviceButtonProps> = ({
|
||||||
if (!devices.length) return null;
|
if (!devices.length) return null;
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
className={classNames({
|
className={classNames("mx_CallLobby_deviceButtonWrapper", {
|
||||||
"mx_VideoLobby_deviceButtonWrapper": true,
|
"mx_CallLobby_deviceButtonWrapper_muted": muted,
|
||||||
"mx_VideoLobby_deviceButtonWrapper_active": active,
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className={`mx_VideoLobby_deviceButton mx_VideoLobby_deviceButton_${kind}`}
|
className={`mx_CallLobby_deviceButton mx_CallLobby_deviceButton_${kind}`}
|
||||||
title={active ? activeTitle : inactiveTitle}
|
title={muted ? mutedTitle : unmutedTitle}
|
||||||
alignment={Alignment.Top}
|
alignment={Alignment.Top}
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
{ labelledDevices.length > 1 ? (
|
{ devices.length > 1 ? (
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
className="mx_VideoLobby_deviceListButton"
|
className="mx_CallLobby_deviceListButton"
|
||||||
inputRef={buttonRef}
|
inputRef={buttonRef}
|
||||||
onClick={openMenu}
|
onClick={openMenu}
|
||||||
isExpanded={menuDisplayed}
|
isExpanded={menuDisplayed}
|
||||||
|
@ -106,57 +104,65 @@ const DeviceButton: FC<IDeviceButtonProps> = ({
|
||||||
|
|
||||||
const MAX_FACES = 8;
|
const MAX_FACES = 8;
|
||||||
|
|
||||||
const VideoLobby: FC<{ room: Room }> = ({ room }) => {
|
interface Props {
|
||||||
const store = VideoChannelStore.instance;
|
room: Room;
|
||||||
|
call: Call;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CallLobby: FC<Props> = ({ room, call }) => {
|
||||||
const [connecting, setConnecting] = useState(false);
|
const [connecting, setConnecting] = useState(false);
|
||||||
const me = useMemo(() => room.getMember(room.myUserId), [room]);
|
const me = useMemo(() => room.getMember(room.myUserId)!, [room]);
|
||||||
const connectedMembers = useConnectedMembers(room, false);
|
const participants = useParticipants(call);
|
||||||
const videoRef = useRef<HTMLVideoElement>();
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
const devices = useAsyncMemo(async () => {
|
const [audioInputs, videoInputs] = useAsyncMemo(async () => {
|
||||||
try {
|
try {
|
||||||
return await navigator.mediaDevices.enumerateDevices();
|
const devices = await MediaDeviceHandler.getDevices();
|
||||||
|
return [devices[MediaDeviceKindEnum.AudioInput], devices[MediaDeviceKindEnum.VideoInput]];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn(`Failed to get media device list: ${e}`);
|
logger.warn(`Failed to get media device list`, e);
|
||||||
return [];
|
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 [videoInputId, setVideoInputId] = useState<string>(() => MediaDeviceHandler.getVideoInput());
|
||||||
const [selectedVideoDevice, selectVideoDevice] = useState<MediaDeviceInfo>(null);
|
|
||||||
|
|
||||||
const audioDevice = selectedAudioDevice ?? audioDevices[0];
|
const setAudioInput = useCallback((device: MediaDeviceInfo) => {
|
||||||
const videoDevice = selectedVideoDevice ?? videoDevices[0];
|
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 [audioMuted, setAudioMuted] = useState(() => MediaDeviceHandler.startWithAudioMuted);
|
||||||
const [videoActive, setVideoActive] = useState(!store.videoMuted);
|
const [videoMuted, setVideoMuted] = useState(() => MediaDeviceHandler.startWithVideoMuted);
|
||||||
const toggleAudio = () => {
|
|
||||||
store.audioMuted = audioActive;
|
const toggleAudio = useCallback(() => {
|
||||||
setAudioActive(!audioActive);
|
MediaDeviceHandler.startWithAudioMuted = !audioMuted;
|
||||||
};
|
setAudioMuted(!audioMuted);
|
||||||
const toggleVideo = () => {
|
}, [audioMuted, setAudioMuted]);
|
||||||
store.videoMuted = videoActive;
|
const toggleVideo = useCallback(() => {
|
||||||
setVideoActive(!videoActive);
|
MediaDeviceHandler.startWithVideoMuted = !videoMuted;
|
||||||
};
|
setVideoMuted(!videoMuted);
|
||||||
|
}, [videoMuted, setVideoMuted]);
|
||||||
|
|
||||||
const videoStream = useAsyncMemo(async () => {
|
const videoStream = useAsyncMemo(async () => {
|
||||||
if (videoDevice && videoActive) {
|
if (videoInputId && !videoMuted) {
|
||||||
try {
|
try {
|
||||||
return await navigator.mediaDevices.getUserMedia({
|
return await navigator.mediaDevices.getUserMedia({
|
||||||
video: { deviceId: videoDevice.deviceId },
|
video: { deviceId: videoInputId },
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} 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;
|
return null;
|
||||||
}, [videoDevice, videoActive]);
|
}, [videoInputId, videoMuted]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (videoStream) {
|
if (videoStream) {
|
||||||
const videoElement = videoRef.current;
|
const videoElement = videoRef.current!;
|
||||||
videoElement.srcObject = videoStream;
|
videoElement.srcObject = videoStream;
|
||||||
videoElement.play();
|
videoElement.play();
|
||||||
|
|
||||||
|
@ -167,67 +173,69 @@ const VideoLobby: FC<{ room: Room }> = ({ room }) => {
|
||||||
}
|
}
|
||||||
}, [videoStream]);
|
}, [videoStream]);
|
||||||
|
|
||||||
const connect = async () => {
|
const connect = useCallback(async () => {
|
||||||
setConnecting(true);
|
setConnecting(true);
|
||||||
try {
|
try {
|
||||||
await store.connect(
|
// Disconnect from any other active calls first, since we don't yet support holding
|
||||||
room.roomId, audioActive ? audioDevice : null, videoActive ? videoDevice : null,
|
await Promise.all([...CallStore.instance.activeCalls].map(call => call.disconnect()));
|
||||||
);
|
await call.connect();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
setConnecting(false);
|
setConnecting(false);
|
||||||
}
|
}
|
||||||
};
|
}, [call, setConnecting]);
|
||||||
|
|
||||||
let facePile;
|
let facePile: JSX.Element | null = null;
|
||||||
if (connectedMembers.size) {
|
if (participants.size) {
|
||||||
const shownMembers = [...connectedMembers].slice(0, MAX_FACES);
|
const shownMembers = [...participants].slice(0, MAX_FACES);
|
||||||
const overflow = connectedMembers.size > shownMembers.length;
|
const overflow = participants.size > shownMembers.length;
|
||||||
|
|
||||||
facePile = <div className="mx_VideoLobby_connectedMembers">
|
facePile = <div className="mx_CallLobby_participants">
|
||||||
{ _t("%(count)s people joined", { count: connectedMembers.size }) }
|
{ _t("%(count)s people joined", { count: participants.size }) }
|
||||||
<FacePile members={shownMembers} faceSize={24} overflow={overflow} />
|
<FacePile members={shownMembers} faceSize={24} overflow={overflow} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="mx_VideoLobby">
|
return <div className="mx_CallLobby">
|
||||||
{ facePile }
|
{ facePile }
|
||||||
<div className="mx_VideoLobby_preview">
|
<div className="mx_CallLobby_preview">
|
||||||
<MemberAvatar key={me.userId} member={me} width={200} height={200} resizeMethod="scale" />
|
<MemberAvatar key={me.userId} member={me} width={200} height={200} resizeMethod="scale" />
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
style={{ visibility: videoActive ? null : "hidden" }}
|
style={{ visibility: videoMuted ? "hidden" : undefined }}
|
||||||
muted
|
muted
|
||||||
playsInline
|
playsInline
|
||||||
disablePictureInPicture
|
disablePictureInPicture
|
||||||
/>
|
/>
|
||||||
<div className="mx_VideoLobby_controls">
|
<div className="mx_CallLobby_controls">
|
||||||
<DeviceButton
|
<DeviceButton
|
||||||
kind="audio"
|
kind="audio"
|
||||||
devices={audioDevices}
|
devices={audioInputs}
|
||||||
setDevice={selectAudioDevice}
|
setDevice={setAudioInput}
|
||||||
deviceListLabel={_t("Audio devices")}
|
deviceListLabel={_t("Audio devices")}
|
||||||
active={audioActive}
|
fallbackDeviceLabel={n => _t("Audio input %(n)s", { n })}
|
||||||
|
muted={audioMuted}
|
||||||
disabled={connecting}
|
disabled={connecting}
|
||||||
toggle={toggleAudio}
|
toggle={toggleAudio}
|
||||||
activeTitle={_t("Mute microphone")}
|
unmutedTitle={_t("Mute microphone")}
|
||||||
inactiveTitle={_t("Unmute microphone")}
|
mutedTitle={_t("Unmute microphone")}
|
||||||
/>
|
/>
|
||||||
<DeviceButton
|
<DeviceButton
|
||||||
kind="video"
|
kind="video"
|
||||||
devices={videoDevices}
|
devices={videoInputs}
|
||||||
setDevice={selectVideoDevice}
|
setDevice={setVideoInput}
|
||||||
deviceListLabel={_t("Video devices")}
|
deviceListLabel={_t("Video devices")}
|
||||||
active={videoActive}
|
fallbackDeviceLabel={n => _t("Video input %(n)s", { n })}
|
||||||
|
muted={videoMuted}
|
||||||
disabled={connecting}
|
disabled={connecting}
|
||||||
toggle={toggleVideo}
|
toggle={toggleVideo}
|
||||||
activeTitle={_t("Turn off camera")}
|
unmutedTitle={_t("Turn off camera")}
|
||||||
inactiveTitle={_t("Turn on camera")}
|
mutedTitle={_t("Turn on camera")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_VideoLobby_joinButton"
|
className="mx_CallLobby_connectButton"
|
||||||
kind="primary"
|
kind="primary"
|
||||||
disabled={connecting}
|
disabled={connecting}
|
||||||
onClick={connect}
|
onClick={connect}
|
||||||
|
@ -236,5 +244,3 @@ const VideoLobby: FC<{ room: Room }> = ({ room }) => {
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VideoLobby;
|
|
|
@ -21,7 +21,7 @@ import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import DialPad from './DialPad';
|
import DialPad from './DialPad';
|
||||||
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
|
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
|
||||||
import CallHandler from "../../../CallHandler";
|
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onFinished: (boolean) => void;
|
onFinished: (boolean) => void;
|
||||||
|
@ -78,7 +78,7 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
onDialPress = async () => {
|
onDialPress = async () => {
|
||||||
CallHandler.instance.dialNumber(this.state.value);
|
LegacyCallHandler.instance.dialNumber(this.state.value);
|
||||||
this.props.onFinished(true);
|
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 { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
|
||||||
|
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import CallHandler from '../../../CallHandler';
|
import LegacyCallHandler from '../../../LegacyCallHandler';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import VideoFeed from './VideoFeed';
|
import VideoFeed from './VideoFeed';
|
||||||
|
@ -32,9 +32,9 @@ import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { avatarUrlForMember } from '../../../Avatar';
|
import { avatarUrlForMember } from '../../../Avatar';
|
||||||
import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker";
|
import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker";
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import CallViewSidebar from './CallViewSidebar';
|
import LegacyCallViewSidebar from './LegacyCallViewSidebar';
|
||||||
import CallViewHeader from './CallView/CallViewHeader';
|
import LegacyCallViewHeader from './LegacyCallView/LegacyCallViewHeader';
|
||||||
import CallViewButtons from "./CallView/CallViewButtons";
|
import LegacyCallViewButtons from "./LegacyCallView/LegacyCallViewButtons";
|
||||||
import PlatformPeg from "../../../PlatformPeg";
|
import PlatformPeg from "../../../PlatformPeg";
|
||||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||||
|
@ -47,7 +47,7 @@ interface IProps {
|
||||||
// Another ongoing call to display information about
|
// Another ongoing call to display information about
|
||||||
secondaryCall?: MatrixCall;
|
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.
|
// in a way that is likely to cause a resize.
|
||||||
onResize?: (event: Event) => void;
|
onResize?: (event: Event) => void;
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ interface IProps {
|
||||||
// need to control those things separately, so this is simpler.
|
// need to control those things separately, so this is simpler.
|
||||||
pipMode?: boolean;
|
pipMode?: boolean;
|
||||||
|
|
||||||
// Used for dragging the PiP CallView
|
// Used for dragging the PiP LegacyCallView
|
||||||
onMouseDownOnHeader?: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
onMouseDownOnHeader?: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
|
|
||||||
showApps?: boolean;
|
showApps?: boolean;
|
||||||
|
@ -104,15 +104,15 @@ function exitFullscreen() {
|
||||||
if (exitMethod) exitMethod.call(document);
|
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 dispatcherRef: string;
|
||||||
private contentWrapperRef = createRef<HTMLDivElement>();
|
private contentWrapperRef = createRef<HTMLDivElement>();
|
||||||
private buttonsRef = createRef<CallViewButtons>();
|
private buttonsRef = createRef<LegacyCallViewButtons>();
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { primary, secondary, sidebar } = CallView.getOrderedFeeds(this.props.call.getFeeds());
|
const { primary, secondary, sidebar } = LegacyCallView.getOrderedFeeds(this.props.call.getFeeds());
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isLocalOnHold: this.props.call.isLocalOnHold(),
|
isLocalOnHold: this.props.call.isLocalOnHold(),
|
||||||
|
@ -146,7 +146,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: IProps): Partial<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 {
|
return {
|
||||||
primaryFeed: primary,
|
primaryFeed: primary,
|
||||||
|
@ -209,7 +209,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onFeedsChanged = (newFeeds: Array<CallFeed>): void => {
|
private onFeedsChanged = (newFeeds: Array<CallFeed>): void => {
|
||||||
const { primary, secondary, sidebar } = CallView.getOrderedFeeds(newFeeds);
|
const { primary, secondary, sidebar } = LegacyCallView.getOrderedFeeds(newFeeds);
|
||||||
this.setState({
|
this.setState({
|
||||||
primaryFeed: primary,
|
primaryFeed: primary,
|
||||||
secondaryFeed: secondary,
|
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
|
// 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
|
// Note that this assumes we always have a LegacyCallView on screen at any given time
|
||||||
// CallHandler would probably be a better place for this
|
// LegacyCallHandler would probably be a better place for this
|
||||||
private onNativeKeyDown = (ev): void => {
|
private onNativeKeyDown = (ev): void => {
|
||||||
let handled = false;
|
let handled = false;
|
||||||
|
|
||||||
|
@ -339,17 +339,17 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onCallResumeClick = (): void => {
|
private onCallResumeClick = (): void => {
|
||||||
const userFacingRoomId = CallHandler.instance.roomIdForCall(this.props.call);
|
const userFacingRoomId = LegacyCallHandler.instance.roomIdForCall(this.props.call);
|
||||||
CallHandler.instance.setActiveCallRoomId(userFacingRoomId);
|
LegacyCallHandler.instance.setActiveCallRoomId(userFacingRoomId);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onTransferClick = (): void => {
|
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);
|
this.props.call.transferToCall(transfereeCall);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onHangupClick = (): void => {
|
private onHangupClick = (): void => {
|
||||||
CallHandler.instance.hangupOrReject(CallHandler.instance.roomIdForCall(this.props.call));
|
LegacyCallHandler.instance.hangupOrReject(LegacyCallHandler.instance.roomIdForCall(this.props.call));
|
||||||
};
|
};
|
||||||
|
|
||||||
private onToggleSidebar = (): void => {
|
private onToggleSidebar = (): void => {
|
||||||
|
@ -380,7 +380,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CallViewButtons
|
<LegacyCallViewButtons
|
||||||
ref={this.buttonsRef}
|
ref={this.buttonsRef}
|
||||||
call={call}
|
call={call}
|
||||||
pipMode={pipMode}
|
pipMode={pipMode}
|
||||||
|
@ -431,7 +431,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallView_toast">
|
<div className="mx_LegacyCallView_toast">
|
||||||
{ text }
|
{ text }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -443,7 +443,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const callRoom = MatrixClientPeg.get().getRoom(call.roomId);
|
const callRoom = MatrixClientPeg.get().getRoom(call.roomId);
|
||||||
const avatarSize = pipMode ? 76 : 160;
|
const avatarSize = pipMode ? 76 : 160;
|
||||||
const transfereeCall = CallHandler.instance.getTransfereeForCallId(call.callId);
|
const transfereeCall = LegacyCallHandler.instance.getTransfereeForCallId(call.callId);
|
||||||
const isOnHold = isLocalOnHold || isRemoteOnHold;
|
const isOnHold = isLocalOnHold || isRemoteOnHold;
|
||||||
|
|
||||||
let secondaryFeedElement: React.ReactNode;
|
let secondaryFeedElement: React.ReactNode;
|
||||||
|
@ -460,23 +460,23 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transfereeCall || isOnHold) {
|
if (transfereeCall || isOnHold) {
|
||||||
const containerClasses = classNames("mx_CallView_content", {
|
const containerClasses = classNames("mx_LegacyCallView_content", {
|
||||||
mx_CallView_content_hold: isOnHold,
|
mx_LegacyCallView_content_hold: isOnHold,
|
||||||
});
|
});
|
||||||
const backgroundAvatarUrl = avatarUrlForMember(call.getOpponentMember(), 1024, 1024, 'crop');
|
const backgroundAvatarUrl = avatarUrlForMember(call.getOpponentMember(), 1024, 1024, 'crop');
|
||||||
|
|
||||||
let holdTransferContent: React.ReactNode;
|
let holdTransferContent: React.ReactNode;
|
||||||
if (transfereeCall) {
|
if (transfereeCall) {
|
||||||
const transferTargetRoom = MatrixClientPeg.get().getRoom(
|
const transferTargetRoom = MatrixClientPeg.get().getRoom(
|
||||||
CallHandler.instance.roomIdForCall(call),
|
LegacyCallHandler.instance.roomIdForCall(call),
|
||||||
);
|
);
|
||||||
const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person");
|
const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person");
|
||||||
const transfereeRoom = MatrixClientPeg.get().getRoom(
|
const transfereeRoom = MatrixClientPeg.get().getRoom(
|
||||||
CallHandler.instance.roomIdForCall(transfereeCall),
|
LegacyCallHandler.instance.roomIdForCall(transfereeCall),
|
||||||
);
|
);
|
||||||
const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person");
|
const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person");
|
||||||
|
|
||||||
holdTransferContent = <div className="mx_CallView_status">
|
holdTransferContent = <div className="mx_LegacyCallView_status">
|
||||||
{ _t(
|
{ _t(
|
||||||
"Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
|
"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;
|
let onHoldText: React.ReactNode;
|
||||||
if (isRemoteOnHold) {
|
if (isRemoteOnHold) {
|
||||||
onHoldText = _t(
|
onHoldText = _t(
|
||||||
CallHandler.instance.hasAnyUnheldCall()
|
LegacyCallHandler.instance.hasAnyUnheldCall()
|
||||||
? _td("You held the call <a>Switch</a>")
|
? _td("You held the call <a>Switch</a>")
|
||||||
: _td("You held the call <a>Resume</a>"),
|
: _td("You held the call <a>Resume</a>"),
|
||||||
{},
|
{},
|
||||||
|
@ -511,7 +511,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
holdTransferContent = (
|
holdTransferContent = (
|
||||||
<div className="mx_CallView_status">
|
<div className="mx_LegacyCallView_status">
|
||||||
{ onHoldText }
|
{ onHoldText }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -519,16 +519,16 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses} onMouseMove={this.onMouseMove}>
|
<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 }
|
{ holdTransferContent }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (call.noIncomingFeeds()) {
|
} else if (call.noIncomingFeeds()) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallView_content" onMouseMove={this.onMouseMove}>
|
<div className="mx_LegacyCallView_content" onMouseMove={this.onMouseMove}>
|
||||||
<div className="mx_CallView_avatarsContainer">
|
<div className="mx_LegacyCallView_avatarsContainer">
|
||||||
<div
|
<div
|
||||||
className="mx_CallView_avatarContainer"
|
className="mx_LegacyCallView_avatarContainer"
|
||||||
style={{ width: avatarSize, height: avatarSize }}
|
style={{ width: avatarSize, height: avatarSize }}
|
||||||
>
|
>
|
||||||
<RoomAvatar
|
<RoomAvatar
|
||||||
|
@ -538,14 +538,14 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_CallView_status">{ _t("Connecting") }</div>
|
<div className="mx_LegacyCallView_status">{ _t("Connecting") }</div>
|
||||||
{ secondaryFeedElement }
|
{ secondaryFeedElement }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (pipMode) {
|
} else if (pipMode) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="mx_CallView_content"
|
className="mx_LegacyCallView_content"
|
||||||
onMouseMove={this.onMouseMove}
|
onMouseMove={this.onMouseMove}
|
||||||
>
|
>
|
||||||
<VideoFeed
|
<VideoFeed
|
||||||
|
@ -559,7 +559,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
} else if (secondaryFeed) {
|
} else if (secondaryFeed) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallView_content" onMouseMove={this.onMouseMove}>
|
<div className="mx_LegacyCallView_content" onMouseMove={this.onMouseMove}>
|
||||||
<VideoFeed
|
<VideoFeed
|
||||||
feed={primaryFeed}
|
feed={primaryFeed}
|
||||||
call={call}
|
call={call}
|
||||||
|
@ -572,7 +572,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallView_content" onMouseMove={this.onMouseMove}>
|
<div className="mx_LegacyCallView_content" onMouseMove={this.onMouseMove}>
|
||||||
<VideoFeed
|
<VideoFeed
|
||||||
feed={primaryFeed}
|
feed={primaryFeed}
|
||||||
call={call}
|
call={call}
|
||||||
|
@ -580,7 +580,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
onResize={onResize}
|
onResize={onResize}
|
||||||
primary={true}
|
primary={true}
|
||||||
/>
|
/>
|
||||||
{ sidebarShown && <CallViewSidebar
|
{ sidebarShown && <LegacyCallViewSidebar
|
||||||
feeds={sidebarFeeds}
|
feeds={sidebarFeeds}
|
||||||
call={call}
|
call={call}
|
||||||
pipMode={pipMode}
|
pipMode={pipMode}
|
||||||
|
@ -604,27 +604,27 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const callRoomId = CallHandler.instance.roomIdForCall(call);
|
const callRoomId = LegacyCallHandler.instance.roomIdForCall(call);
|
||||||
const secondaryCallRoomId = CallHandler.instance.roomIdForCall(secondaryCall);
|
const secondaryCallRoomId = LegacyCallHandler.instance.roomIdForCall(secondaryCall);
|
||||||
const callRoom = client.getRoom(callRoomId);
|
const callRoom = client.getRoom(callRoomId);
|
||||||
const secCallRoom = secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
|
const secCallRoom = secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
|
||||||
|
|
||||||
const callViewClasses = classNames({
|
const callViewClasses = classNames({
|
||||||
mx_CallView: true,
|
mx_LegacyCallView: true,
|
||||||
mx_CallView_pip: pipMode,
|
mx_LegacyCallView_pip: pipMode,
|
||||||
mx_CallView_large: !pipMode,
|
mx_LegacyCallView_large: !pipMode,
|
||||||
mx_CallView_sidebar: sidebarShown && sidebarFeeds.length !== 0 && !pipMode,
|
mx_LegacyCallView_sidebar: sidebarShown && sidebarFeeds.length !== 0 && !pipMode,
|
||||||
mx_CallView_belowWidget: showApps, // css to correct the margins if the call is below the AppsDrawer.
|
mx_LegacyCallView_belowWidget: showApps, // css to correct the margins if the call is below the AppsDrawer.
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className={callViewClasses}>
|
return <div className={callViewClasses}>
|
||||||
<CallViewHeader
|
<LegacyCallViewHeader
|
||||||
onPipMouseDown={onMouseDownOnHeader}
|
onPipMouseDown={onMouseDownOnHeader}
|
||||||
pipMode={pipMode}
|
pipMode={pipMode}
|
||||||
callRooms={[callRoom, secCallRoom]}
|
callRooms={[callRoom, secCallRoom]}
|
||||||
onMaximize={this.onMaximizeClick}
|
onMaximize={this.onMaximizeClick}
|
||||||
/>
|
/>
|
||||||
<div className="mx_CallView_content_wrapper" ref={this.contentWrapperRef}>
|
<div className="mx_LegacyCallView_content_wrapper" ref={this.contentWrapperRef}>
|
||||||
{ this.renderToast() }
|
{ this.renderToast() }
|
||||||
{ this.renderContent() }
|
{ this.renderContent() }
|
||||||
{ this.renderCallControls() }
|
{ this.renderCallControls() }
|
|
@ -21,7 +21,7 @@ import classNames from "classnames";
|
||||||
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
|
||||||
import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton";
|
||||||
import CallContextMenu from "../../context_menus/CallContextMenu";
|
import LegacyCallContextMenu from "../../context_menus/LegacyCallContextMenu";
|
||||||
import DialpadContextMenu from "../../context_menus/DialpadContextMenu";
|
import DialpadContextMenu from "../../context_menus/DialpadContextMenu";
|
||||||
import { Alignment } from "../../elements/Tooltip";
|
import { Alignment } from "../../elements/Tooltip";
|
||||||
import {
|
import {
|
||||||
|
@ -49,7 +49,7 @@ interface IButtonProps extends Omit<React.ComponentProps<typeof AccessibleToolti
|
||||||
onClick: (event: React.MouseEvent) => void;
|
onClick: (event: React.MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CallViewToggleButton: React.FC<IButtonProps> = ({
|
const LegacyCallViewToggleButton: React.FC<IButtonProps> = ({
|
||||||
children,
|
children,
|
||||||
state: isOn,
|
state: isOn,
|
||||||
className,
|
className,
|
||||||
|
@ -57,9 +57,9 @@ const CallViewToggleButton: React.FC<IButtonProps> = ({
|
||||||
offLabel,
|
offLabel,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const classes = classNames("mx_CallViewButtons_button", className, {
|
const classes = classNames("mx_LegacyCallViewButtons_button", className, {
|
||||||
mx_CallViewButtons_button_on: isOn,
|
mx_LegacyCallViewButtons_button_on: isOn,
|
||||||
mx_CallViewButtons_button_off: !isOn,
|
mx_LegacyCallViewButtons_button_off: !isOn,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -78,12 +78,12 @@ interface IDropdownButtonProps extends IButtonProps {
|
||||||
deviceKinds: MediaDeviceKindEnum[];
|
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 [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu();
|
||||||
const [hoveringDropdown, setHoveringDropdown] = useState(false);
|
const [hoveringDropdown, setHoveringDropdown] = useState(false);
|
||||||
|
|
||||||
const classes = classNames("mx_CallViewButtons_button", "mx_CallViewButtons_dropdownButton", {
|
const classes = classNames("mx_LegacyCallViewButtons_button", "mx_LegacyCallViewButtons_dropdownButton", {
|
||||||
mx_CallViewButtons_dropdownButton_collapsed: !menuDisplayed,
|
mx_LegacyCallViewButtons_dropdownButton_collapsed: !menuDisplayed,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onClick = (event: React.MouseEvent): void => {
|
const onClick = (event: React.MouseEvent): void => {
|
||||||
|
@ -92,8 +92,8 @@ const CallViewDropdownButton: React.FC<IDropdownButtonProps> = ({ state, deviceK
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CallViewToggleButton inputRef={buttonRef} forceHide={menuDisplayed || hoveringDropdown} state={state} {...props}>
|
<LegacyCallViewToggleButton inputRef={buttonRef} forceHide={menuDisplayed || hoveringDropdown} state={state} {...props}>
|
||||||
<CallViewToggleButton
|
<LegacyCallViewToggleButton
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onHover={(hovering) => setHoveringDropdown(hovering)}
|
onHover={(hovering) => setHoveringDropdown(hovering)}
|
||||||
|
@ -105,7 +105,7 @@ const CallViewDropdownButton: React.FC<IDropdownButtonProps> = ({ state, deviceK
|
||||||
onFinished={closeMenu}
|
onFinished={closeMenu}
|
||||||
deviceKinds={deviceKinds}
|
deviceKinds={deviceKinds}
|
||||||
/> }
|
/> }
|
||||||
</CallViewToggleButton>
|
</LegacyCallViewToggleButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ interface IState {
|
||||||
showMoreMenu: boolean;
|
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 dialpadButton = createRef<HTMLDivElement>();
|
||||||
private contextMenuButton = createRef<HTMLDivElement>();
|
private contextMenuButton = createRef<HTMLDivElement>();
|
||||||
private controlsHideTimer: number = null;
|
private controlsHideTimer: number = null;
|
||||||
|
@ -212,8 +212,8 @@ export default class CallViewButtons extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const callControlsClasses = classNames("mx_CallViewButtons", {
|
const callControlsClasses = classNames("mx_LegacyCallViewButtons", {
|
||||||
mx_CallViewButtons_hidden: !this.state.visible,
|
mx_LegacyCallViewButtons_hidden: !this.state.visible,
|
||||||
});
|
});
|
||||||
|
|
||||||
let dialPad;
|
let dialPad;
|
||||||
|
@ -236,7 +236,7 @@ export default class CallViewButtons extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let contextMenu;
|
let contextMenu;
|
||||||
if (this.state.showMoreMenu) {
|
if (this.state.showMoreMenu) {
|
||||||
contextMenu = <CallContextMenu
|
contextMenu = <LegacyCallContextMenu
|
||||||
{...alwaysAboveLeftOf(
|
{...alwaysAboveLeftOf(
|
||||||
this.contextMenuButton.current.getBoundingClientRect(),
|
this.contextMenuButton.current.getBoundingClientRect(),
|
||||||
ChevronFace.None,
|
ChevronFace.None,
|
||||||
|
@ -258,45 +258,45 @@ export default class CallViewButtons extends React.Component<IProps, IState> {
|
||||||
{ contextMenu }
|
{ contextMenu }
|
||||||
|
|
||||||
{ this.props.buttonsVisibility.dialpad && <ContextMenuTooltipButton
|
{ this.props.buttonsVisibility.dialpad && <ContextMenuTooltipButton
|
||||||
className="mx_CallViewButtons_button mx_CallViewButtons_dialpad"
|
className="mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_dialpad"
|
||||||
inputRef={this.dialpadButton}
|
inputRef={this.dialpadButton}
|
||||||
onClick={this.onDialpadClick}
|
onClick={this.onDialpadClick}
|
||||||
isExpanded={this.state.showDialpad}
|
isExpanded={this.state.showDialpad}
|
||||||
title={_t("Dialpad")}
|
title={_t("Dialpad")}
|
||||||
alignment={Alignment.Top}
|
alignment={Alignment.Top}
|
||||||
/> }
|
/> }
|
||||||
<CallViewDropdownButton
|
<LegacyCallViewDropdownButton
|
||||||
state={!this.props.buttonsState.micMuted}
|
state={!this.props.buttonsState.micMuted}
|
||||||
className="mx_CallViewButtons_button_mic"
|
className="mx_LegacyCallViewButtons_button_mic"
|
||||||
onLabel={_t("Mute the microphone")}
|
onLabel={_t("Mute the microphone")}
|
||||||
offLabel={_t("Unmute the microphone")}
|
offLabel={_t("Unmute the microphone")}
|
||||||
onClick={this.props.handlers.onMicMuteClick}
|
onClick={this.props.handlers.onMicMuteClick}
|
||||||
deviceKinds={[MediaDeviceKindEnum.AudioInput, MediaDeviceKindEnum.AudioOutput]}
|
deviceKinds={[MediaDeviceKindEnum.AudioInput, MediaDeviceKindEnum.AudioOutput]}
|
||||||
/>
|
/>
|
||||||
{ this.props.buttonsVisibility.vidMute && <CallViewDropdownButton
|
{ this.props.buttonsVisibility.vidMute && <LegacyCallViewDropdownButton
|
||||||
state={!this.props.buttonsState.vidMuted}
|
state={!this.props.buttonsState.vidMuted}
|
||||||
className="mx_CallViewButtons_button_vid"
|
className="mx_LegacyCallViewButtons_button_vid"
|
||||||
onLabel={_t("Stop the camera")}
|
onLabel={_t("Stop the camera")}
|
||||||
offLabel={_t("Start the camera")}
|
offLabel={_t("Start the camera")}
|
||||||
onClick={this.props.handlers.onVidMuteClick}
|
onClick={this.props.handlers.onVidMuteClick}
|
||||||
deviceKinds={[MediaDeviceKindEnum.VideoInput]}
|
deviceKinds={[MediaDeviceKindEnum.VideoInput]}
|
||||||
/> }
|
/> }
|
||||||
{ this.props.buttonsVisibility.screensharing && <CallViewToggleButton
|
{ this.props.buttonsVisibility.screensharing && <LegacyCallViewToggleButton
|
||||||
state={this.props.buttonsState.screensharing}
|
state={this.props.buttonsState.screensharing}
|
||||||
className="mx_CallViewButtons_button_screensharing"
|
className="mx_LegacyCallViewButtons_button_screensharing"
|
||||||
onLabel={_t("Stop sharing your screen")}
|
onLabel={_t("Stop sharing your screen")}
|
||||||
offLabel={_t("Start sharing your screen")}
|
offLabel={_t("Start sharing your screen")}
|
||||||
onClick={this.props.handlers.onScreenshareClick}
|
onClick={this.props.handlers.onScreenshareClick}
|
||||||
/> }
|
/> }
|
||||||
{ this.props.buttonsVisibility.sidebar && <CallViewToggleButton
|
{ this.props.buttonsVisibility.sidebar && <LegacyCallViewToggleButton
|
||||||
state={this.props.buttonsState.sidebarShown}
|
state={this.props.buttonsState.sidebarShown}
|
||||||
className="mx_CallViewButtons_button_sidebar"
|
className="mx_LegacyCallViewButtons_button_sidebar"
|
||||||
onLabel={_t("Hide sidebar")}
|
onLabel={_t("Hide sidebar")}
|
||||||
offLabel={_t("Show sidebar")}
|
offLabel={_t("Show sidebar")}
|
||||||
onClick={this.props.handlers.onToggleSidebarClick}
|
onClick={this.props.handlers.onToggleSidebarClick}
|
||||||
/> }
|
/> }
|
||||||
{ this.props.buttonsVisibility.contextMenu && <ContextMenuTooltipButton
|
{ this.props.buttonsVisibility.contextMenu && <ContextMenuTooltipButton
|
||||||
className="mx_CallViewButtons_button mx_CallViewButtons_button_more"
|
className="mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_more"
|
||||||
onClick={this.onMoreClick}
|
onClick={this.onMoreClick}
|
||||||
inputRef={this.contextMenuButton}
|
inputRef={this.contextMenuButton}
|
||||||
isExpanded={this.state.showMoreMenu}
|
isExpanded={this.state.showMoreMenu}
|
||||||
|
@ -304,7 +304,7 @@ export default class CallViewButtons extends React.Component<IProps, IState> {
|
||||||
alignment={Alignment.Top}
|
alignment={Alignment.Top}
|
||||||
/> }
|
/> }
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className="mx_CallViewButtons_button mx_CallViewButtons_button_hangup"
|
className="mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_hangup"
|
||||||
onClick={this.props.handlers.onHangupClick}
|
onClick={this.props.handlers.onHangupClick}
|
||||||
title={_t("Hangup")}
|
title={_t("Hangup")}
|
||||||
alignment={Alignment.Top}
|
alignment={Alignment.Top}
|
|
@ -21,26 +21,26 @@ import { _t } from '../../../../languageHandler';
|
||||||
import RoomAvatar from '../../avatars/RoomAvatar';
|
import RoomAvatar from '../../avatars/RoomAvatar';
|
||||||
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';
|
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';
|
||||||
|
|
||||||
interface CallControlsProps {
|
interface LegacyCallControlsProps {
|
||||||
onExpand?: () => void;
|
onExpand?: () => void;
|
||||||
onPin?: () => void;
|
onPin?: () => void;
|
||||||
onMaximize?: () => void;
|
onMaximize?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CallViewHeaderControls: React.FC<CallControlsProps> = ({ onExpand, onPin, onMaximize }) => {
|
const LegacyCallViewHeaderControls: React.FC<LegacyCallControlsProps> = ({ onExpand, onPin, onMaximize }) => {
|
||||||
return <div className="mx_CallViewHeader_controls">
|
return <div className="mx_LegacyCallViewHeader_controls">
|
||||||
{ onMaximize && <AccessibleTooltipButton
|
{ onMaximize && <AccessibleTooltipButton
|
||||||
className="mx_CallViewHeader_button mx_CallViewHeader_button_fullscreen"
|
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_fullscreen"
|
||||||
onClick={onMaximize}
|
onClick={onMaximize}
|
||||||
title={_t("Fill Screen")}
|
title={_t("Fill Screen")}
|
||||||
/> }
|
/> }
|
||||||
{ onPin && <AccessibleTooltipButton
|
{ onPin && <AccessibleTooltipButton
|
||||||
className="mx_CallViewHeader_button mx_CallViewHeader_button_pin"
|
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_pin"
|
||||||
onClick={onPin}
|
onClick={onPin}
|
||||||
title={_t("Pin")}
|
title={_t("Pin")}
|
||||||
/> }
|
/> }
|
||||||
{ onExpand && <AccessibleTooltipButton
|
{ onExpand && <AccessibleTooltipButton
|
||||||
className="mx_CallViewHeader_button mx_CallViewHeader_button_expand"
|
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_expand"
|
||||||
onClick={onExpand}
|
onClick={onExpand}
|
||||||
title={_t("Return to call")}
|
title={_t("Return to call")}
|
||||||
/> }
|
/> }
|
||||||
|
@ -52,15 +52,15 @@ interface ISecondaryCallInfoProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const SecondaryCallInfo: React.FC<ISecondaryCallInfoProps> = ({ callRoom }) => {
|
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} />
|
<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 }) }
|
{ _t("%(name)s on hold", { name: callRoom.name }) }
|
||||||
</span>
|
</span>
|
||||||
</span>;
|
</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CallViewHeaderProps {
|
interface LegacyCallViewHeaderProps {
|
||||||
pipMode: boolean;
|
pipMode: boolean;
|
||||||
callRooms?: Room[];
|
callRooms?: Room[];
|
||||||
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
|
@ -69,7 +69,7 @@ interface CallViewHeaderProps {
|
||||||
onMaximize?: () => void;
|
onMaximize?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CallViewHeader: React.FC<CallViewHeaderProps> = ({
|
const LegacyCallViewHeader: React.FC<LegacyCallViewHeaderProps> = ({
|
||||||
pipMode = false,
|
pipMode = false,
|
||||||
callRooms = [],
|
callRooms = [],
|
||||||
onPipMouseDown,
|
onPipMouseDown,
|
||||||
|
@ -81,25 +81,25 @@ const CallViewHeader: React.FC<CallViewHeaderProps> = ({
|
||||||
const callRoomName = callRoom.name;
|
const callRoomName = callRoom.name;
|
||||||
|
|
||||||
if (!pipMode) {
|
if (!pipMode) {
|
||||||
return <div className="mx_CallViewHeader">
|
return <div className="mx_LegacyCallViewHeader">
|
||||||
<div className="mx_CallViewHeader_icon" />
|
<div className="mx_LegacyCallViewHeader_icon" />
|
||||||
<span className="mx_CallViewHeader_text">{ _t("Call") }</span>
|
<span className="mx_LegacyCallViewHeader_text">{ _t("Call") }</span>
|
||||||
<CallViewHeaderControls onMaximize={onMaximize} />
|
<LegacyCallViewHeaderControls onMaximize={onMaximize} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="mx_CallViewHeader mx_CallViewHeader_pip"
|
className="mx_LegacyCallViewHeader mx_LegacyCallViewHeader_pip"
|
||||||
onMouseDown={onPipMouseDown}
|
onMouseDown={onPipMouseDown}
|
||||||
>
|
>
|
||||||
<RoomAvatar room={callRoom} height={32} width={32} />
|
<RoomAvatar room={callRoom} height={32} width={32} />
|
||||||
<div className="mx_CallViewHeader_callInfo">
|
<div className="mx_LegacyCallViewHeader_callInfo">
|
||||||
<div className="mx_CallViewHeader_roomName">{ callRoomName }</div>
|
<div className="mx_LegacyCallViewHeader_roomName">{ callRoomName }</div>
|
||||||
{ onHoldCallRoom && <SecondaryCallInfo callRoom={onHoldCallRoom} /> }
|
{ onHoldCallRoom && <SecondaryCallInfo callRoom={onHoldCallRoom} /> }
|
||||||
</div>
|
</div>
|
||||||
<CallViewHeaderControls onExpand={onExpand} onPin={onPin} onMaximize={onMaximize} />
|
<LegacyCallViewHeaderControls onExpand={onExpand} onPin={onPin} onMaximize={onMaximize} />
|
||||||
</div>
|
</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 React from 'react';
|
||||||
import { Resizable } from "re-resizable";
|
import { Resizable } from "re-resizable";
|
||||||
|
|
||||||
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
|
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../../LegacyCallHandler';
|
||||||
import CallView from './CallView';
|
import LegacyCallView from './LegacyCallView';
|
||||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
@ -32,14 +32,14 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
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.
|
* 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) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -48,13 +48,13 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
CallHandler.instance.addListener(CallHandlerEvent.CallState, this.updateCall);
|
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.updateCall);
|
||||||
CallHandler.instance.addListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
|
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCall);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.updateCall);
|
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCall);
|
||||||
CallHandler.instance.removeListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
|
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCall);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateCall = () => {
|
private updateCall = () => {
|
||||||
|
@ -64,8 +64,8 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private getCall(): MatrixCall {
|
private getCall(): MatrixCall | null {
|
||||||
const call = CallHandler.instance.getCallForRoom(this.props.roomId);
|
const call = LegacyCallHandler.instance.getCallForRoom(this.props.roomId);
|
||||||
|
|
||||||
if (call && [CallState.Ended, CallState.Ringing].includes(call.state)) return null;
|
if (call && [CallState.Ended, CallState.Ringing].includes(call.state)) return null;
|
||||||
return call;
|
return call;
|
||||||
|
@ -87,7 +87,7 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
|
||||||
if (!this.state.call) return null;
|
if (!this.state.call) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallViewForRoom">
|
<div className="mx_LegacyCallViewForRoom">
|
||||||
<Resizable
|
<Resizable
|
||||||
minHeight={380}
|
minHeight={380}
|
||||||
maxHeight="80vh"
|
maxHeight="80vh"
|
||||||
|
@ -104,10 +104,10 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
|
||||||
onResizeStart={this.onResizeStart}
|
onResizeStart={this.onResizeStart}
|
||||||
onResize={this.onResize}
|
onResize={this.onResize}
|
||||||
onResizeStop={this.onResizeStop}
|
onResizeStop={this.onResizeStop}
|
||||||
className="mx_CallViewForRoom_ResizeWrapper"
|
className="mx_LegacyCallViewForRoom_ResizeWrapper"
|
||||||
handleClasses={{ bottom: "mx_CallViewForRoom_ResizeHandle" }}
|
handleClasses={{ bottom: "mx_LegacyCallViewForRoom_ResizeHandle" }}
|
||||||
>
|
>
|
||||||
<CallView
|
<LegacyCallView
|
||||||
call={this.state.call}
|
call={this.state.call}
|
||||||
pipMode={false}
|
pipMode={false}
|
||||||
showApps={this.props.showApps}
|
showApps={this.props.showApps}
|
|
@ -27,7 +27,7 @@ interface IProps {
|
||||||
pipMode: boolean;
|
pipMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CallViewSidebar extends React.Component<IProps> {
|
export default class LegacyCallViewSidebar extends React.Component<IProps> {
|
||||||
render() {
|
render() {
|
||||||
const feeds = this.props.feeds.map((feed) => {
|
const feeds = this.props.feeds.map((feed) => {
|
||||||
return (
|
return (
|
||||||
|
@ -41,8 +41,8 @@ export default class CallViewSidebar extends React.Component<IProps> {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const className = classNames("mx_CallViewSidebar", {
|
const className = classNames("mx_LegacyCallViewSidebar", {
|
||||||
mx_CallViewSidebar_pipMode: this.props.pipMode,
|
mx_LegacyCallViewSidebar_pipMode: this.props.pipMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
|
@ -21,9 +21,9 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import CallView from "./CallView";
|
import LegacyCallView from "./LegacyCallView";
|
||||||
import { RoomViewStore } from '../../../stores/RoomViewStore';
|
import { RoomViewStore } from '../../../stores/RoomViewStore';
|
||||||
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
|
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../../LegacyCallHandler';
|
||||||
import PersistentApp from "../elements/PersistentApp";
|
import PersistentApp from "../elements/PersistentApp";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
|
@ -31,7 +31,7 @@ import PictureInPictureDragger from './PictureInPictureDragger';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { Container, WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
|
import { Container, WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
|
||||||
import CallViewHeader from './CallView/CallViewHeader';
|
import LegacyCallViewHeader from './LegacyCallView/LegacyCallViewHeader';
|
||||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
|
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
|
||||||
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
|
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
|
||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
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
|
// The primary will be the one not on hold, or an arbitrary one
|
||||||
// if they're all on hold)
|
// if they're all on hold)
|
||||||
function getPrimarySecondaryCallsForPip(roomId: string): [MatrixCall, MatrixCall[]] {
|
function getPrimarySecondaryCallsForPip(roomId: string): [MatrixCall, MatrixCall[]] {
|
||||||
const calls = CallHandler.instance.getAllActiveCallsForPip(roomId);
|
const calls = LegacyCallHandler.instance.getAllActiveCallsForPip(roomId);
|
||||||
|
|
||||||
let primary: MatrixCall = null;
|
let primary: MatrixCall = null;
|
||||||
let secondaries: MatrixCall[] = [];
|
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
|
* (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.
|
* 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() {
|
public componentDidMount() {
|
||||||
CallHandler.instance.addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||||
CallHandler.instance.addListener(CallHandlerEvent.CallState, this.updateCalls);
|
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
|
||||||
this.roomStoreToken = RoomViewStore.instance.addListener(this.onRoomViewStoreUpdate);
|
this.roomStoreToken = RoomViewStore.instance.addListener(this.onRoomViewStoreUpdate);
|
||||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||||
const room = MatrixClientPeg.get()?.getRoom(this.state.viewedRoomId);
|
const room = MatrixClientPeg.get()?.getRoom(this.state.viewedRoomId);
|
||||||
|
@ -154,8 +154,8 @@ export default class PipView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
CallHandler.instance.removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||||
CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.updateCalls);
|
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
|
||||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||||
this.roomStoreToken?.remove();
|
this.roomStoreToken?.remove();
|
||||||
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||||
|
@ -308,7 +308,7 @@ export default class PipView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
if (this.state.primaryCall) {
|
if (this.state.primaryCall) {
|
||||||
pipContent = ({ onStartMoving, onResize }) =>
|
pipContent = ({ onStartMoving, onResize }) =>
|
||||||
<CallView
|
<LegacyCallView
|
||||||
onMouseDownOnHeader={onStartMoving}
|
onMouseDownOnHeader={onStartMoving}
|
||||||
call={this.state.primaryCall}
|
call={this.state.primaryCall}
|
||||||
secondaryCall={this.state.secondaryCall}
|
secondaryCall={this.state.secondaryCall}
|
||||||
|
@ -329,7 +329,7 @@ export default class PipView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
pipContent = ({ onStartMoving, _onResize }) =>
|
pipContent = ({ onStartMoving, _onResize }) =>
|
||||||
<div className={pipViewClasses}>
|
<div className={pipViewClasses}>
|
||||||
<CallViewHeader
|
<LegacyCallViewHeader
|
||||||
onPipMouseDown={(event) => { onStartMoving(event); this.onStartMoving.bind(this)(); }}
|
onPipMouseDown={(event) => { onStartMoving(event); this.onStartMoving.bind(this)(); }}
|
||||||
pipMode={pipMode}
|
pipMode={pipMode}
|
||||||
callRooms={[roomForWidget]}
|
callRooms={[roomForWidget]}
|
||||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { EventType, RoomCreateTypeField, RoomType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, RoomCreateTypeField, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
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 { VIRTUAL_ROOM_EVENT_TYPE } from "./call-types";
|
||||||
import SpaceStore from "./stores/spaces/SpaceStore";
|
import SpaceStore from "./stores/spaces/SpaceStore";
|
||||||
import { makeSpaceParentEvent } from "./utils/space";
|
import { makeSpaceParentEvent } from "./utils/space";
|
||||||
import { VIDEO_CHANNEL_MEMBER, addVideoChannel } from "./utils/VideoChannelUtils";
|
import { JitsiCall } from "./models/Call";
|
||||||
import { Action } from "./dispatcher/actions";
|
import { Action } from "./dispatcher/actions";
|
||||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||||
import Spinner from "./components/views/elements/Spinner";
|
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) {
|
if (opts.roomType === RoomType.ElementVideo) {
|
||||||
createOpts.power_level_content_override = {
|
createOpts.power_level_content_override = {
|
||||||
events: {
|
events: {
|
||||||
// Allow all users to send video member updates
|
// Allow all users to send call membership updates
|
||||||
[VIDEO_CHANNEL_MEMBER]: 0,
|
[JitsiCall.MEMBER_EVENT_TYPE]: 0,
|
||||||
// Make widgets immutable, even to admins
|
// Make widgets immutable, even to admins
|
||||||
"im.vector.modular.widgets": 200,
|
"im.vector.modular.widgets": 200,
|
||||||
// Annoyingly, we have to reiterate all the defaults here
|
// 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;
|
let modal;
|
||||||
if (opts.spinner) modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
|
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) {
|
return client.createRoom(createOpts).catch(function(err) {
|
||||||
// NB This checks for the Synapse-specific error condition of a room creation
|
// 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,
|
// 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() {
|
}).finally(function() {
|
||||||
if (modal) modal.close();
|
if (modal) modal.close();
|
||||||
}).then(function(res) {
|
}).then(async res => {
|
||||||
roomId = res.room_id;
|
roomId = res.room_id;
|
||||||
if (opts.dmUserId) {
|
|
||||||
return Rooms.setDMRoom(roomId, opts.dmUserId);
|
room = new Promise(resolve => {
|
||||||
} else {
|
const storedRoom = client.getRoom(roomId);
|
||||||
return Promise.resolve();
|
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(() => {
|
}).then(() => {
|
||||||
if (opts.parentSpace) {
|
if (opts.parentSpace) {
|
||||||
return SpaceStore.instance.addRoomToSpace(opts.parentSpace, roomId, [client.getDomain()], opts.suggested);
|
return SpaceStore.instance.addRoomToSpace(opts.parentSpace, roomId, [client.getDomain()], opts.suggested);
|
||||||
}
|
}
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
if (opts.roomType === RoomType.ElementVideo) {
|
if (opts.roomType === RoomType.ElementVideo) {
|
||||||
// Set up video rooms with a Jitsi widget
|
// Set up video rooms with a Jitsi call
|
||||||
await addVideoChannel(roomId, createOpts.name);
|
await JitsiCall.create(await room);
|
||||||
|
|
||||||
// Reset our power level back to admin so that the widget becomes immutable
|
// Reset our power level back to admin so that the widget becomes immutable
|
||||||
const room = client.getRoom(roomId);
|
const plEvent = (await room)?.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||||
const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
await client.setPowerLevel(roomId, client.getUserId()!, 100, plEvent);
|
||||||
await client.setPowerLevel(roomId, client.getUserId(), 100, plEvent);
|
|
||||||
}
|
}
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
// NB createRoom doesn't block on the client seeing the echo that the
|
// NB we haven't necessarily blocked on the room promise, so we race
|
||||||
// room has been created, so we race here with the client knowing that
|
// here with the client knowing that the room exists, causing things
|
||||||
// the room exists, causing things like
|
// like https://github.com/vector-im/vector-web/issues/1813
|
||||||
// https://github.com/vector-im/vector-web/issues/1813
|
|
||||||
// Even if we were to block on the echo, servers tend to split the room
|
// 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
|
// state over multiple syncs so we can't atomically know when we have the
|
||||||
// entire thing.
|
// entire thing.
|
||||||
|
|
|
@ -22,12 +22,12 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
import EditorStateTransfer from "../utils/EditorStateTransfer";
|
import EditorStateTransfer from "../utils/EditorStateTransfer";
|
||||||
import { RoomPermalinkCreator } from "../utils/permalinks/Permalinks";
|
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 { GetRelationsForEvent } from "../components/views/rooms/EventTile";
|
||||||
import { TimelineRenderingType } from "../contexts/RoomContext";
|
import { TimelineRenderingType } from "../contexts/RoomContext";
|
||||||
import MessageEvent from "../components/views/messages/MessageEvent";
|
import MessageEvent from "../components/views/messages/MessageEvent";
|
||||||
import MKeyVerificationConclusion from "../components/views/messages/MKeyVerificationConclusion";
|
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 TextualEvent from "../components/views/messages/TextualEvent";
|
||||||
import EncryptionEvent from "../components/views/messages/EncryptionEvent";
|
import EncryptionEvent from "../components/views/messages/EncryptionEvent";
|
||||||
import RoomCreate from "../components/views/messages/RoomCreate";
|
import RoomCreate from "../components/views/messages/RoomCreate";
|
||||||
|
@ -57,7 +57,7 @@ export interface EventTileTypeProps {
|
||||||
editState?: EditorStateTransfer;
|
editState?: EditorStateTransfer;
|
||||||
replacingEventId?: string;
|
replacingEventId?: string;
|
||||||
permalinkCreator: RoomPermalinkCreator;
|
permalinkCreator: RoomPermalinkCreator;
|
||||||
callEventGrouper?: CallEventGrouper;
|
callEventGrouper?: LegacyCallEventGrouper;
|
||||||
isSeeingThroughMessageHiddenForModeration?: boolean;
|
isSeeingThroughMessageHiddenForModeration?: boolean;
|
||||||
timestamp?: JSX.Element;
|
timestamp?: JSX.Element;
|
||||||
maxImageHeight?: number; // pixels
|
maxImageHeight?: number; // pixels
|
||||||
|
@ -71,8 +71,8 @@ type FactoryMap = Record<string, Factory>;
|
||||||
|
|
||||||
const MessageEventFactory: Factory = (ref, props) => <MessageEvent ref={ref} {...props} />;
|
const MessageEventFactory: Factory = (ref, props) => <MessageEvent ref={ref} {...props} />;
|
||||||
const KeyVerificationConclFactory: Factory = (ref, props) => <MKeyVerificationConclusion ref={ref} {...props} />;
|
const KeyVerificationConclFactory: Factory = (ref, props) => <MKeyVerificationConclusion ref={ref} {...props} />;
|
||||||
const CallEventFactory: Factory<FactoryProps & { callEventGrouper: CallEventGrouper }> = (ref, props) => (
|
const LegacyCallEventFactory: Factory<FactoryProps & { callEventGrouper: LegacyCallEventGrouper }> = (ref, props) => (
|
||||||
<CallEvent ref={ref} {...props} />
|
<LegacyCallEvent ref={ref} {...props} />
|
||||||
);
|
);
|
||||||
const TextualEventFactory: Factory = (ref, props) => <TextualEvent ref={ref} {...props} />;
|
const TextualEventFactory: Factory = (ref, props) => <TextualEvent ref={ref} {...props} />;
|
||||||
const VerificationReqFactory: Factory = (ref, props) => <MKeyVerificationRequest 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,
|
[M_POLL_START.altName]: MessageEventFactory,
|
||||||
[EventType.KeyVerificationCancel]: KeyVerificationConclFactory,
|
[EventType.KeyVerificationCancel]: KeyVerificationConclFactory,
|
||||||
[EventType.KeyVerificationDone]: 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 = {
|
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",
|
"Error": "Error",
|
||||||
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
|
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
|
||||||
"Dismiss": "Dismiss",
|
"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' 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",
|
"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",
|
"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.",
|
"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.",
|
"Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.",
|
||||||
"Trust": "Trust",
|
"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 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.",
|
"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",
|
"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?",
|
"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.",
|
"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",
|
"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",
|
"You are presenting": "You are presenting",
|
||||||
"%(sharerName)s is presenting": "%(sharerName)s is presenting",
|
"%(sharerName)s is presenting": "%(sharerName)s is presenting",
|
||||||
"Your camera is turned off": "Your camera is turned off",
|
"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>",
|
"You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
|
||||||
"%(peerName)s held the call": "%(peerName)s held the call",
|
"%(peerName)s held the call": "%(peerName)s held the call",
|
||||||
"Connecting": "Connecting",
|
"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",
|
"Dialpad": "Dialpad",
|
||||||
"Mute the microphone": "Mute the microphone",
|
"Mute the microphone": "Mute the microphone",
|
||||||
"Unmute the microphone": "Unmute 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.|other": "%(count)s unread messages.",
|
||||||
"%(count)s unread messages.|one": "1 unread message.",
|
"%(count)s unread messages.|one": "1 unread message.",
|
||||||
"Unread messages.": "Unread messages.",
|
"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.",
|
"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 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>.",
|
"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",
|
"Open thread": "Open thread",
|
||||||
"Jump to first unread message.": "Jump to first unread message.",
|
"Jump to first unread message.": "Jump to first unread message.",
|
||||||
"Mark all as read": "Mark all as read",
|
"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",
|
"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.",
|
"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",
|
"No microphone found": "No microphone found",
|
||||||
|
@ -2155,17 +2157,6 @@
|
||||||
"%(displayName)s cancelled verification.": "%(displayName)s cancelled verification.",
|
"%(displayName)s cancelled verification.": "%(displayName)s cancelled verification.",
|
||||||
"You cancelled verification.": "You cancelled verification.",
|
"You cancelled verification.": "You cancelled verification.",
|
||||||
"Verification cancelled": "Verification cancelled",
|
"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",
|
"Sunday": "Sunday",
|
||||||
"Monday": "Monday",
|
"Monday": "Monday",
|
||||||
"Tuesday": "Tuesday",
|
"Tuesday": "Tuesday",
|
||||||
|
@ -2196,6 +2187,17 @@
|
||||||
"Message pending moderation": "Message pending moderation",
|
"Message pending moderation": "Message pending moderation",
|
||||||
"Pick a date to jump to": "Pick a date to jump to",
|
"Pick a date to jump to": "Pick a date to jump to",
|
||||||
"Go": "Go",
|
"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",
|
"Error processing audio message": "Error processing audio message",
|
||||||
"View live location": "View live location",
|
"View live location": "View live location",
|
||||||
"React": "React",
|
"React": "React",
|
||||||
|
@ -3018,11 +3020,11 @@
|
||||||
"Observe only": "Observe only",
|
"Observe only": "Observe only",
|
||||||
"No verification requests found": "No verification requests found",
|
"No verification requests found": "No verification requests found",
|
||||||
"There was an error finding this widget.": "There was an error finding this widget.",
|
"There was an error finding this widget.": "There was an error finding this widget.",
|
||||||
"Resume": "Resume",
|
|
||||||
"Hold": "Hold",
|
|
||||||
"Input devices": "Input devices",
|
"Input devices": "Input devices",
|
||||||
"Output devices": "Output devices",
|
"Output devices": "Output devices",
|
||||||
"Cameras": "Cameras",
|
"Cameras": "Cameras",
|
||||||
|
"Resume": "Resume",
|
||||||
|
"Hold": "Hold",
|
||||||
"Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)",
|
"Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)",
|
||||||
"Open in OpenStreetMap": "Open in OpenStreetMap",
|
"Open in OpenStreetMap": "Open in OpenStreetMap",
|
||||||
"Forward": "Forward",
|
"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,
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
"videoChannelRoomId": {
|
"activeCallRoomIds": {
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||||
default: null,
|
default: [],
|
||||||
},
|
},
|
||||||
[UIFeature.RoomHistorySettings]: {
|
[UIFeature.RoomHistorySettings]: {
|
||||||
supportedLevels: LEVELS_UI_FEATURE,
|
supportedLevels: LEVELS_UI_FEATURE,
|
||||||
|
|
|
@ -449,7 +449,12 @@ export default class SettingsStore {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-enable valid-jsdoc */
|
/* 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
|
// Verify that the setting is actually a setting
|
||||||
const setting = SETTINGS[settingName];
|
const setting = SETTINGS[settingName];
|
||||||
if (!setting) {
|
if (!setting) {
|
||||||
|
|
|
@ -44,6 +44,10 @@ export abstract class AsyncStoreWithClient<T extends Object> extends AsyncStore<
|
||||||
})(dispatcher);
|
})(dispatcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
await this.readyStore.start();
|
||||||
|
}
|
||||||
|
|
||||||
get matrixClient(): MatrixClient {
|
get matrixClient(): MatrixClient {
|
||||||
return this.readyStore.mxClient;
|
return this.readyStore.mxClient;
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,11 @@ interface IState {
|
||||||
* reported.
|
* reported.
|
||||||
*/
|
*/
|
||||||
export default class AutoRageshakeStore extends AsyncStoreWithClient<IState> {
|
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() {
|
private constructor() {
|
||||||
super(defaultDispatcher, {
|
super(defaultDispatcher, {
|
||||||
|
|
|
@ -37,7 +37,11 @@ interface IState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BreadcrumbsStore extends AsyncStoreWithClient<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 }[] = [];
|
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> {
|
export class ModalWidgetStore extends AsyncStoreWithClient<IState> {
|
||||||
private static internalInstance = new ModalWidgetStore();
|
private static readonly internalInstance = (() => {
|
||||||
private modalInstance: IHandle<void[]> = null;
|
const instance = new ModalWidgetStore();
|
||||||
private openSourceWidgetId: string = null;
|
instance.start();
|
||||||
private openSourceWidgetRoomId: string = null;
|
return instance;
|
||||||
|
})();
|
||||||
|
private modalInstance: IHandle<void[]> | null = null;
|
||||||
|
private openSourceWidgetId: string | null = null;
|
||||||
|
private openSourceWidgetRoomId: string | null = null;
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
super(defaultDispatcher, {});
|
super(defaultDispatcher, {});
|
||||||
|
|
|
@ -92,7 +92,11 @@ const getLocallyCreatedBeaconEventIds = (): string[] => {
|
||||||
return ids;
|
return ids;
|
||||||
};
|
};
|
||||||
export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
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
|
// users beacons, keyed by event type
|
||||||
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
|
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
|
||||||
public readonly beaconsByRoomId = new Map<Room['roomId'], Set<BeaconIdentifier>>();
|
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";
|
const KEY_AVATAR_URL = "mx_profile_avatar_url";
|
||||||
|
|
||||||
export class OwnProfileStore extends AsyncStoreWithClient<IState> {
|
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;
|
private monitoredUser: User;
|
||||||
|
|
||||||
|
|
|
@ -26,18 +26,19 @@ import { Action } from "../dispatcher/actions";
|
||||||
|
|
||||||
export abstract class ReadyWatchingStore extends EventEmitter implements IDestroyable {
|
export abstract class ReadyWatchingStore extends EventEmitter implements IDestroyable {
|
||||||
protected matrixClient: MatrixClient;
|
protected matrixClient: MatrixClient;
|
||||||
private readonly dispatcherRef: string;
|
private dispatcherRef: string | null = null;
|
||||||
|
|
||||||
constructor(protected readonly dispatcher: Dispatcher<ActionPayload>) {
|
constructor(protected readonly dispatcher: Dispatcher<ActionPayload>) {
|
||||||
super();
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start(): Promise<void> {
|
||||||
this.dispatcherRef = this.dispatcher.register(this.onAction);
|
this.dispatcherRef = this.dispatcher.register(this.onAction);
|
||||||
|
|
||||||
if (MatrixClientPeg.get()) {
|
const matrixClient = MatrixClientPeg.get();
|
||||||
this.matrixClient = MatrixClientPeg.get();
|
if (matrixClient) {
|
||||||
|
this.matrixClient = matrixClient;
|
||||||
// noinspection JSIgnoredPromiseFromCall
|
await this.onReady();
|
||||||
this.onReady();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +51,7 @@ export abstract class ReadyWatchingStore extends EventEmitter implements IDestro
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
this.dispatcher.unregister(this.dispatcherRef);
|
if (this.dispatcherRef !== null) this.dispatcher.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onReady() {
|
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 {
|
public static get instance(): VoiceRecordingStore {
|
||||||
if (!VoiceRecordingStore.internalInstance) {
|
if (!this.internalInstance) {
|
||||||
VoiceRecordingStore.internalInstance = new VoiceRecordingStore();
|
this.internalInstance = new VoiceRecordingStore();
|
||||||
|
this.internalInstance.start();
|
||||||
}
|
}
|
||||||
return VoiceRecordingStore.internalInstance;
|
return this.internalInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onAction(payload: ActionPayload): Promise<void> {
|
protected async onAction(payload: ActionPayload): Promise<void> {
|
||||||
|
|
|
@ -36,7 +36,7 @@ export interface IApp extends IWidget {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
eventId: string;
|
eventId: string;
|
||||||
// eslint-disable-next-line camelcase
|
// 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 {
|
interface IRoomWidgets {
|
||||||
|
@ -46,7 +46,11 @@ interface IRoomWidgets {
|
||||||
// TODO consolidate WidgetEchoStore into this
|
// TODO consolidate WidgetEchoStore into this
|
||||||
// TODO consolidate ActiveWidgetStore into this
|
// TODO consolidate ActiveWidgetStore into this
|
||||||
export default class WidgetStore extends AsyncStoreWithClient<IState> {
|
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 widgetMap = new Map<string, IApp>(); // Key is widget Unique ID (UID)
|
||||||
private roomMap = new Map<string, IRoomWidgets>(); // Key is room ID
|
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 {
|
public static get instance(): EchoStore {
|
||||||
if (!EchoStore._instance) {
|
if (!this._instance) {
|
||||||
EchoStore._instance = new EchoStore();
|
this._instance = new EchoStore();
|
||||||
|
this._instance.start();
|
||||||
}
|
}
|
||||||
return EchoStore._instance;
|
return this._instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get contexts(): EchoContext[] {
|
public get contexts(): EchoContext[] {
|
||||||
|
|
|
@ -34,7 +34,11 @@ interface IState {}
|
||||||
export const UPDATE_STATUS_INDICATOR = Symbol("update-status-indicator");
|
export const UPDATE_STATUS_INDICATOR = Symbol("update-status-indicator");
|
||||||
|
|
||||||
export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
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 roomMap = new Map<Room, RoomNotificationState>();
|
||||||
private roomThreadsMap = new Map<Room, ThreadsRoomNotificationState>();
|
private roomThreadsMap = new Map<Room, ThreadsRoomNotificationState>();
|
||||||
|
|
|
@ -394,10 +394,11 @@ export default class RightPanelStore extends ReadyWatchingStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get instance(): RightPanelStore {
|
public static get instance(): RightPanelStore {
|
||||||
if (!RightPanelStore.internalInstance) {
|
if (!this.internalInstance) {
|
||||||
RightPanelStore.internalInstance = new RightPanelStore();
|
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 { MessageEventPreview } from "./previews/MessageEventPreview";
|
||||||
import { PollStartEventPreview } from "./previews/PollStartEventPreview";
|
import { PollStartEventPreview } from "./previews/PollStartEventPreview";
|
||||||
import { TagID } from "./models";
|
import { TagID } from "./models";
|
||||||
import { CallInviteEventPreview } from "./previews/CallInviteEventPreview";
|
import { LegacyCallInviteEventPreview } from "./previews/LegacyCallInviteEventPreview";
|
||||||
import { CallAnswerEventPreview } from "./previews/CallAnswerEventPreview";
|
import { LegacyCallAnswerEventPreview } from "./previews/LegacyCallAnswerEventPreview";
|
||||||
import { CallHangupEvent } from "./previews/CallHangupEvent";
|
import { LegacyCallHangupEvent } from "./previews/LegacyCallHangupEvent";
|
||||||
import { StickerEventPreview } from "./previews/StickerEventPreview";
|
import { StickerEventPreview } from "./previews/StickerEventPreview";
|
||||||
import { ReactionEventPreview } from "./previews/ReactionEventPreview";
|
import { ReactionEventPreview } from "./previews/ReactionEventPreview";
|
||||||
import { UPDATE_EVENT } from "../AsyncStore";
|
import { UPDATE_EVENT } from "../AsyncStore";
|
||||||
|
@ -47,15 +47,15 @@ const PREVIEWS: Record<string, {
|
||||||
},
|
},
|
||||||
'm.call.invite': {
|
'm.call.invite': {
|
||||||
isState: false,
|
isState: false,
|
||||||
previewer: new CallInviteEventPreview(),
|
previewer: new LegacyCallInviteEventPreview(),
|
||||||
},
|
},
|
||||||
'm.call.answer': {
|
'm.call.answer': {
|
||||||
isState: false,
|
isState: false,
|
||||||
previewer: new CallAnswerEventPreview(),
|
previewer: new LegacyCallAnswerEventPreview(),
|
||||||
},
|
},
|
||||||
'm.call.hangup': {
|
'm.call.hangup': {
|
||||||
isState: false,
|
isState: false,
|
||||||
previewer: new CallHangupEvent(),
|
previewer: new LegacyCallHangupEvent(),
|
||||||
},
|
},
|
||||||
'm.sticker': {
|
'm.sticker': {
|
||||||
isState: false,
|
isState: false,
|
||||||
|
@ -87,7 +87,11 @@ interface IState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MessagePreviewStore extends AsyncStoreWithClient<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
|
// null indicates the preview is empty / irrelevant
|
||||||
private previews = new Map<string, Map<TagID|TAG_ANY, string|null>>();
|
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 {
|
public static get instance(): RoomListLayoutStore {
|
||||||
if (!RoomListLayoutStore.internalInstance) {
|
if (!this.internalInstance) {
|
||||||
RoomListLayoutStore.internalInstance = new RoomListLayoutStore();
|
this.internalInstance = new RoomListLayoutStore();
|
||||||
|
this.internalInstance.start();
|
||||||
}
|
}
|
||||||
return RoomListLayoutStore.internalInstance;
|
return RoomListLayoutStore.internalInstance;
|
||||||
}
|
}
|
||||||
|
|
|
@ -602,11 +602,13 @@ export default class RoomListStore {
|
||||||
private static internalInstance: Interface;
|
private static internalInstance: Interface;
|
||||||
|
|
||||||
public static get instance(): Interface {
|
public static get instance(): Interface {
|
||||||
if (!RoomListStore.internalInstance) {
|
if (!this.internalInstance) {
|
||||||
RoomListStore.internalInstance = new RoomListStoreClass();
|
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 { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
|
||||||
import { getListAlgorithmInstance } from "./list-ordering";
|
import { getListAlgorithmInstance } from "./list-ordering";
|
||||||
import { VisibilityProvider } from "../filters/VisibilityProvider";
|
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.
|
* Fired when the Algorithm has determined a list has been updated.
|
||||||
|
@ -82,13 +82,11 @@ export class Algorithm extends EventEmitter {
|
||||||
public updatesInhibited = false;
|
public updatesInhibited = false;
|
||||||
|
|
||||||
public start() {
|
public start() {
|
||||||
VideoChannelStore.instance.on(VideoChannelEvent.Connect, this.updateVideoRoom);
|
CallStore.instance.on(CallStoreEvent.ActiveCalls, this.onActiveCalls);
|
||||||
VideoChannelStore.instance.on(VideoChannelEvent.Disconnect, this.updateVideoRoom);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop() {
|
public stop() {
|
||||||
VideoChannelStore.instance.off(VideoChannelEvent.Connect, this.updateVideoRoom);
|
CallStore.instance.off(CallStoreEvent.ActiveCalls, this.onActiveCalls);
|
||||||
VideoChannelStore.instance.off(VideoChannelEvent.Disconnect, this.updateVideoRoom);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get stickyRoom(): Room {
|
public get stickyRoom(): Room {
|
||||||
|
@ -106,7 +104,7 @@ export class Algorithm extends EventEmitter {
|
||||||
protected set cachedRooms(val: ITagMap) {
|
protected set cachedRooms(val: ITagMap) {
|
||||||
this._cachedRooms = val;
|
this._cachedRooms = val;
|
||||||
this.recalculateStickyRoom();
|
this.recalculateStickyRoom();
|
||||||
this.recalculateVideoRoom();
|
this.recalculateActiveCallRooms();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get cachedRooms(): ITagMap {
|
protected get cachedRooms(): ITagMap {
|
||||||
|
@ -143,7 +141,7 @@ export class Algorithm extends EventEmitter {
|
||||||
algorithm.setSortAlgorithm(sort);
|
algorithm.setSortAlgorithm(sort);
|
||||||
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
||||||
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
||||||
this.recalculateVideoRoom(tagId);
|
this.recalculateActiveCallRooms(tagId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getListOrdering(tagId: TagID): ListAlgorithm {
|
public getListOrdering(tagId: TagID): ListAlgorithm {
|
||||||
|
@ -162,7 +160,7 @@ export class Algorithm extends EventEmitter {
|
||||||
algorithm.setRooms(this._cachedRooms[tagId]);
|
algorithm.setRooms(this._cachedRooms[tagId]);
|
||||||
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
||||||
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
||||||
this.recalculateVideoRoom(tagId);
|
this.recalculateActiveCallRooms(tagId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateStickyRoom(val: Room) {
|
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
|
// 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 function simply because we don't have to.
|
||||||
this.recalculateStickyRoom();
|
this.recalculateStickyRoom();
|
||||||
this.recalculateVideoRoom(tag);
|
this.recalculateActiveCallRooms(tag);
|
||||||
if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateVideoRoom(lastStickyRoom.tag);
|
if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateActiveCallRooms(lastStickyRoom.tag);
|
||||||
|
|
||||||
// Finally, trigger an update
|
// Finally, trigger an update
|
||||||
if (this.updatesInhibited) return;
|
if (this.updatesInhibited) return;
|
||||||
this.emit(LIST_UPDATED_EVENT);
|
this.emit(LIST_UPDATED_EVENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private onActiveCalls = () => {
|
||||||
* Update the stickiness of video rooms.
|
// In case we're unsticking a room, sort it back into natural order
|
||||||
*/
|
|
||||||
public updateVideoRoom = () => {
|
|
||||||
// In case we're unsticking a video room, sort it back into natural order
|
|
||||||
this.recalculateStickyRoom();
|
this.recalculateStickyRoom();
|
||||||
|
|
||||||
this.recalculateVideoRoom();
|
// Update the stickiness of rooms with calls
|
||||||
|
this.recalculateActiveCallRooms();
|
||||||
|
|
||||||
if (this.updatesInhibited) return;
|
if (this.updatesInhibited) return;
|
||||||
// This isn't in response to any particular RoomListStore update,
|
// 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
|
* Recalculate the position of any rooms with calls. If this is being called in
|
||||||
* a specific tag being updated, it should be given to this function to optimize
|
* relation to a specific tag being updated, it should be given to this function to
|
||||||
* the call.
|
* optimize the call.
|
||||||
*
|
*
|
||||||
* This expects to be called *after* the sticky rooms are updated, and sticks the
|
* 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.
|
* @param updatedTag The tag that was updated, if possible.
|
||||||
*/
|
*/
|
||||||
protected recalculateVideoRoom(updatedTag: TagID = null): void {
|
protected recalculateActiveCallRooms(updatedTag: TagID = null): void {
|
||||||
if (!updatedTag) {
|
if (!updatedTag) {
|
||||||
// Assume all tags need updating
|
// Assume all tags need updating
|
||||||
// We're not modifying the map here, so can safely rely on the cached values
|
// 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) {
|
if (!tagId) {
|
||||||
throw new Error("Unexpected recursion: falsy tag");
|
throw new Error("Unexpected recursion: falsy tag");
|
||||||
}
|
}
|
||||||
this.recalculateVideoRoom(tagId);
|
this.recalculateActiveCallRooms(tagId);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoRoomId = VideoChannelStore.instance.connected ? VideoChannelStore.instance.roomId : null;
|
if (CallStore.instance.activeCalls.size) {
|
||||||
|
// We operate on the sticky rooms map
|
||||||
if (videoRoomId) {
|
|
||||||
// We operate directly on the sticky rooms map
|
|
||||||
if (!this._cachedStickyRooms) this.initCachedStickyRooms();
|
if (!this._cachedStickyRooms) this.initCachedStickyRooms();
|
||||||
const rooms = this._cachedStickyRooms[updatedTag];
|
const rooms = this._cachedStickyRooms[updatedTag];
|
||||||
const videoRoomIdxInTag = rooms.findIndex(r => r.roomId === videoRoomId);
|
|
||||||
if (videoRoomIdxInTag < 0) return; // no-op
|
|
||||||
|
|
||||||
const videoRoom = rooms[videoRoomIdxInTag];
|
const activeRoomIds = new Set([...CallStore.instance.activeCalls].map(call => call.roomId));
|
||||||
rooms.splice(videoRoomIdxInTag, 1);
|
const activeRooms: Room[] = [];
|
||||||
rooms.unshift(videoRoom);
|
const inactiveRooms: Room[] = [];
|
||||||
this._cachedStickyRooms[updatedTag] = rooms; // re-set because references aren't always safe
|
|
||||||
|
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);
|
algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
|
||||||
this._cachedRooms[rmTag] = algorithm.orderedRooms;
|
this._cachedRooms[rmTag] = algorithm.orderedRooms;
|
||||||
this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed
|
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) {
|
for (const addTag of diff.added) {
|
||||||
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
|
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
|
||||||
|
@ -742,7 +740,7 @@ export class Algorithm extends EventEmitter {
|
||||||
|
|
||||||
// Flag that we've done something
|
// Flag that we've done something
|
||||||
this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed
|
this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed
|
||||||
this.recalculateVideoRoom(tag);
|
this.recalculateActiveCallRooms(tag);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import CallHandler from "../../../CallHandler";
|
import LegacyCallHandler from "../../../LegacyCallHandler";
|
||||||
import { RoomListCustomisations } from "../../../customisations/RoomList";
|
import { RoomListCustomisations } from "../../../customisations/RoomList";
|
||||||
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
||||||
import VoipUserMapper from "../../../VoipUserMapper";
|
import VoipUserMapper from "../../../VoipUserMapper";
|
||||||
|
@ -44,7 +44,7 @@ export class VisibilityProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
CallHandler.instance.getSupportsVirtualRooms() &&
|
LegacyCallHandler.instance.getSupportsVirtualRooms() &&
|
||||||
VoipUserMapper.sharedInstance().isVirtualRoom(room)
|
VoipUserMapper.sharedInstance().isVirtualRoom(room)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { TagID } from "../models";
|
||||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
|
||||||
export class CallAnswerEventPreview implements IPreview {
|
export class LegacyCallAnswerEventPreview implements IPreview {
|
||||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||||
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||||
if (isSelf(event)) {
|
if (isSelf(event)) {
|
|
@ -21,7 +21,7 @@ import { TagID } from "../models";
|
||||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
|
||||||
export class CallHangupEvent implements IPreview {
|
export class LegacyCallHangupEvent implements IPreview {
|
||||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||||
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||||
if (isSelf(event)) {
|
if (isSelf(event)) {
|
|
@ -21,7 +21,7 @@ import { TagID } from "../models";
|
||||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
|
||||||
export class CallInviteEventPreview implements IPreview {
|
export class LegacyCallInviteEventPreview implements IPreview {
|
||||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||||
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||||
if (isSelf(event)) {
|
if (isSelf(event)) {
|
|
@ -1284,7 +1284,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class SpaceStore {
|
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 {
|
public static get instance(): SpaceStoreClass {
|
||||||
return SpaceStore.internalInstance;
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -17,13 +17,9 @@
|
||||||
import { IWidgetApiRequest } from "matrix-widget-api";
|
import { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
|
|
||||||
export enum ElementWidgetActions {
|
export enum ElementWidgetActions {
|
||||||
ClientReady = "im.vector.ready",
|
|
||||||
WidgetReady = "io.element.widget_ready",
|
|
||||||
|
|
||||||
// All of these actions are currently specific to Jitsi
|
// All of these actions are currently specific to Jitsi
|
||||||
JoinCall = "io.element.join",
|
JoinCall = "io.element.join",
|
||||||
HangupCall = "im.vector.hangup",
|
HangupCall = "im.vector.hangup",
|
||||||
ForceHangupCall = "io.element.force_hangup",
|
|
||||||
CallParticipants = "io.element.participants",
|
CallParticipants = "io.element.participants",
|
||||||
MuteAudio = "io.element.mute_audio",
|
MuteAudio = "io.element.mute_audio",
|
||||||
UnmuteAudio = "io.element.unmute_audio",
|
UnmuteAudio = "io.element.unmute_audio",
|
||||||
|
|
|
@ -114,10 +114,11 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get instance(): WidgetLayoutStore {
|
public static get instance(): WidgetLayoutStore {
|
||||||
if (!WidgetLayoutStore.internalInstance) {
|
if (!this.internalInstance) {
|
||||||
WidgetLayoutStore.internalInstance = new WidgetLayoutStore();
|
this.internalInstance = new WidgetLayoutStore();
|
||||||
|
this.internalInstance.start();
|
||||||
}
|
}
|
||||||
return WidgetLayoutStore.internalInstance;
|
return this.internalInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static emissionForRoom(room: Room): string {
|
public static emissionForRoom(room: Room): string {
|
||||||
|
|
|
@ -14,9 +14,8 @@
|
||||||
* limitations under the License.
|
* 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 { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
@ -26,7 +25,6 @@ import WidgetUtils from "../../utils/WidgetUtils";
|
||||||
export enum WidgetMessagingStoreEvent {
|
export enum WidgetMessagingStoreEvent {
|
||||||
StoreMessaging = "store_messaging",
|
StoreMessaging = "store_messaging",
|
||||||
StopMessaging = "stop_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
|
* going to be merged with a more complete WidgetStore, but for now it's
|
||||||
* easiest to split this into a single place.
|
* easiest to split this into a single place.
|
||||||
*/
|
*/
|
||||||
export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
export class WidgetMessagingStore extends AsyncStoreWithClient<{}> {
|
||||||
private static internalInstance = new WidgetMessagingStore();
|
private static readonly internalInstance = (() => {
|
||||||
|
const instance = new WidgetMessagingStore();
|
||||||
|
instance.start();
|
||||||
|
return instance;
|
||||||
|
})();
|
||||||
|
|
||||||
private widgetMap = new EnhancedMap<string, ClientWidgetApi>(); // <widget UID, ClientWidgetAPi>
|
private widgetMap = new EnhancedMap<string, ClientWidgetApi>(); // <widget UID, ClientWidgetAPi>
|
||||||
private readyWidgets = new Set<string>(); // widgets that have sent a WidgetReady event
|
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
super(defaultDispatcher);
|
super(defaultDispatcher);
|
||||||
|
@ -62,12 +63,6 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
||||||
const uid = WidgetUtils.calcWidgetUid(widget.id, roomId);
|
const uid = WidgetUtils.calcWidgetUid(widget.id, roomId);
|
||||||
this.widgetMap.set(uid, widgetApi);
|
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);
|
this.emit(WidgetMessagingStoreEvent.StoreMessaging, uid, widgetApi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +80,6 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
||||||
*/
|
*/
|
||||||
public stopMessagingByUid(widgetUid: string) {
|
public stopMessagingByUid(widgetUid: string) {
|
||||||
this.widgetMap.remove(widgetUid)?.stop();
|
this.widgetMap.remove(widgetUid)?.stop();
|
||||||
this.readyWidgets.delete(widgetUid);
|
|
||||||
this.emit(WidgetMessagingStoreEvent.StopMessaging, widgetUid);
|
this.emit(WidgetMessagingStoreEvent.StopMessaging, widgetUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,12 +91,4 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
||||||
public getMessagingForUid(widgetUid: string): ClientWidgetApi {
|
public getMessagingForUid(widgetUid: string): ClientWidgetApi {
|
||||||
return this.widgetMap.get(widgetUid);
|
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 { CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import CallHandler, { CallHandlerEvent } from '../CallHandler';
|
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../LegacyCallHandler';
|
||||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import RoomAvatar from '../components/views/avatars/RoomAvatar';
|
import RoomAvatar from '../components/views/avatars/RoomAvatar';
|
||||||
import AccessibleTooltipButton from '../components/views/elements/AccessibleTooltipButton';
|
import AccessibleTooltipButton from '../components/views/elements/AccessibleTooltipButton';
|
||||||
import AccessibleButton from '../components/views/elements/AccessibleButton';
|
import AccessibleButton from '../components/views/elements/AccessibleButton';
|
||||||
|
|
||||||
export const getIncomingCallToastKey = (callId: string) => `call_${callId}`;
|
export const getIncomingLegacyCallToastKey = (callId: string) => `call_${callId}`;
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
call: MatrixCall;
|
call: MatrixCall;
|
||||||
|
@ -38,83 +38,87 @@ interface IState {
|
||||||
silenced: boolean;
|
silenced: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class IncomingCallToast extends React.Component<IProps, IState> {
|
export default class IncomingLegacyCallToast extends React.Component<IProps, IState> {
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
silenced: CallHandler.instance.isCallSilenced(this.props.call.callId),
|
silenced: LegacyCallHandler.instance.isCallSilenced(this.props.call.callId),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount = (): void => {
|
public componentDidMount = (): void => {
|
||||||
CallHandler.instance.addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
LegacyCallHandler.instance.addListener(
|
||||||
|
LegacyCallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
public componentWillUnmount(): void {
|
||||||
CallHandler.instance.removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
|
LegacyCallHandler.instance.removeListener(
|
||||||
|
LegacyCallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSilencedCallsChanged = (): void => {
|
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 => {
|
private onAnswerClick = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
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 => {
|
private onRejectClick= (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
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 => {
|
private onSilenceClick = (e: React.MouseEvent): void => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const callId = this.props.call.callId;
|
const callId = this.props.call.callId;
|
||||||
this.state.silenced ?
|
this.state.silenced ?
|
||||||
CallHandler.instance.unSilenceCall(callId) :
|
LegacyCallHandler.instance.unSilenceCall(callId) :
|
||||||
CallHandler.instance.silenceCall(callId);
|
LegacyCallHandler.instance.silenceCall(callId);
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const call = this.props.call;
|
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 isVoice = call.type === CallType.Voice;
|
||||||
|
|
||||||
const contentClass = classNames("mx_IncomingCallToast_content", {
|
const contentClass = classNames("mx_IncomingLegacyCallToast_content", {
|
||||||
"mx_IncomingCallToast_content_voice": isVoice,
|
"mx_IncomingLegacyCallToast_content_voice": isVoice,
|
||||||
"mx_IncomingCallToast_content_video": !isVoice,
|
"mx_IncomingLegacyCallToast_content_video": !isVoice,
|
||||||
});
|
});
|
||||||
const silenceClass = classNames("mx_IncomingCallToast_iconButton", {
|
const silenceClass = classNames("mx_IncomingLegacyCallToast_iconButton", {
|
||||||
"mx_IncomingCallToast_unSilence": this.state.silenced,
|
"mx_IncomingLegacyCallToast_unSilence": this.state.silenced,
|
||||||
"mx_IncomingCallToast_silence": !this.state.silenced,
|
"mx_IncomingLegacyCallToast_silence": !this.state.silenced,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
<RoomAvatar
|
<RoomAvatar
|
||||||
room={room}
|
room={room ?? undefined}
|
||||||
height={32}
|
height={32}
|
||||||
width={32}
|
width={32}
|
||||||
/>
|
/>
|
||||||
<div className={contentClass}>
|
<div className={contentClass}>
|
||||||
<span className="mx_CallEvent_caller">
|
<span className="mx_LegacyCallEvent_caller">
|
||||||
{ room ? room.name : _t("Unknown caller") }
|
{ room ? room.name : _t("Unknown caller") }
|
||||||
</span>
|
</span>
|
||||||
<div className="mx_CallEvent_type">
|
<div className="mx_LegacyCallEvent_type">
|
||||||
<div className="mx_CallEvent_type_icon" />
|
<div className="mx_LegacyCallEvent_type_icon" />
|
||||||
{ isVoice ? _t("Voice call") : _t("Video call") }
|
{ isVoice ? _t("Voice call") : _t("Video call") }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_IncomingCallToast_buttons">
|
<div className="mx_IncomingLegacyCallToast_buttons">
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_IncomingCallToast_button mx_IncomingCallToast_button_decline"
|
className="mx_IncomingLegacyCallToast_button mx_IncomingLegacyCallToast_button_decline"
|
||||||
onClick={this.onRejectClick}
|
onClick={this.onRejectClick}
|
||||||
kind="danger"
|
kind="danger"
|
||||||
>
|
>
|
||||||
<span> { _t("Decline") } </span>
|
<span> { _t("Decline") } </span>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_IncomingCallToast_button mx_IncomingCallToast_button_accept"
|
className="mx_IncomingLegacyCallToast_button mx_IncomingLegacyCallToast_button_accept"
|
||||||
onClick={this.onAnswerClick}
|
onClick={this.onAnswerClick}
|
||||||
kind="primary"
|
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 EventEmitter from 'events';
|
||||||
import { mocked } from 'jest-mock';
|
import { mocked } from 'jest-mock';
|
||||||
|
|
||||||
import CallHandler, {
|
import LegacyCallHandler, {
|
||||||
CallHandlerEvent, PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED, PROTOCOL_SIP_NATIVE, PROTOCOL_SIP_VIRTUAL,
|
LegacyCallHandlerEvent, PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED, PROTOCOL_SIP_NATIVE, PROTOCOL_SIP_VIRTUAL,
|
||||||
} from '../src/CallHandler';
|
} from '../src/LegacyCallHandler';
|
||||||
import { stubClient, mkStubRoom, untilDispatch } from './test-utils';
|
import { stubClient, mkStubRoom, untilDispatch } from './test-utils';
|
||||||
import { MatrixClientPeg } from '../src/MatrixClientPeg';
|
import { MatrixClientPeg } from '../src/MatrixClientPeg';
|
||||||
import DMRoomMap from '../src/utils/DMRoomMap';
|
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) => {
|
return new Promise<void>((resolve) => {
|
||||||
callHandler.addListener(event, () => {
|
callHandler.addListener(event, () => {
|
||||||
resolve();
|
resolve();
|
||||||
|
@ -117,7 +117,7 @@ function untilCallHandlerEvent(callHandler: CallHandler, event: CallHandlerEvent
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('CallHandler', () => {
|
describe('LegacyCallHandler', () => {
|
||||||
let dmRoomMap;
|
let dmRoomMap;
|
||||||
let callHandler;
|
let callHandler;
|
||||||
let audioElement: HTMLAudioElement;
|
let audioElement: HTMLAudioElement;
|
||||||
|
@ -145,7 +145,7 @@ describe('CallHandler', () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
callHandler = new CallHandler();
|
callHandler = new LegacyCallHandler();
|
||||||
callHandler.start();
|
callHandler.start();
|
||||||
|
|
||||||
mocked(getFunctionalMembers).mockReturnValue([
|
mocked(getFunctionalMembers).mockReturnValue([
|
||||||
|
@ -251,7 +251,7 @@ describe('CallHandler', () => {
|
||||||
callHandler.stop();
|
callHandler.stop();
|
||||||
DMRoomMap.setShared(null);
|
DMRoomMap.setShared(null);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.mxCallHandler = null;
|
window.mxLegacyCallHandler = null;
|
||||||
fakeCall = null;
|
fakeCall = null;
|
||||||
MatrixClientPeg.unset();
|
MatrixClientPeg.unset();
|
||||||
|
|
||||||
|
@ -295,14 +295,14 @@ describe('CallHandler', () => {
|
||||||
it('should move calls between rooms when remote asserted identity changes', async () => {
|
it('should move calls between rooms when remote asserted identity changes', async () => {
|
||||||
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
|
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
|
// We placed the call in Alice's room so it should start off there
|
||||||
expect(callHandler.getCallForRoom(NATIVE_ROOM_ALICE)).toBe(fakeCall);
|
expect(callHandler.getCallForRoom(NATIVE_ROOM_ALICE)).toBe(fakeCall);
|
||||||
|
|
||||||
let callRoomChangeEventCount = 0;
|
let callRoomChangeEventCount = 0;
|
||||||
const roomChangePromise = new Promise<void>(resolve => {
|
const roomChangePromise = new Promise<void>(resolve => {
|
||||||
callHandler.addListener(CallHandlerEvent.CallChangeRoom, () => {
|
callHandler.addListener(LegacyCallHandlerEvent.CallChangeRoom, () => {
|
||||||
++callRoomChangeEventCount;
|
++callRoomChangeEventCount;
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
@ -343,9 +343,9 @@ describe('CallHandler', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CallHandler without third party protocols', () => {
|
describe('LegacyCallHandler without third party protocols', () => {
|
||||||
let dmRoomMap;
|
let dmRoomMap;
|
||||||
let callHandler: CallHandler;
|
let callHandler: LegacyCallHandler;
|
||||||
let audioElement: HTMLAudioElement;
|
let audioElement: HTMLAudioElement;
|
||||||
let fakeCall;
|
let fakeCall;
|
||||||
|
|
||||||
|
@ -363,7 +363,7 @@ describe('CallHandler without third party protocols', () => {
|
||||||
throw new Error("Endpoint unsupported.");
|
throw new Error("Endpoint unsupported.");
|
||||||
};
|
};
|
||||||
|
|
||||||
callHandler = new CallHandler();
|
callHandler = new LegacyCallHandler();
|
||||||
callHandler.start();
|
callHandler.start();
|
||||||
|
|
||||||
const nativeRoomAlice = mkStubDM(NATIVE_ROOM_ALICE, NATIVE_ALICE);
|
const nativeRoomAlice = mkStubDM(NATIVE_ROOM_ALICE, NATIVE_ALICE);
|
||||||
|
@ -406,7 +406,7 @@ describe('CallHandler without third party protocols', () => {
|
||||||
callHandler.stop();
|
callHandler.stop();
|
||||||
DMRoomMap.setShared(null);
|
DMRoomMap.setShared(null);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.mxCallHandler = null;
|
window.mxLegacyCallHandler = null;
|
||||||
fakeCall = null;
|
fakeCall = null;
|
||||||
MatrixClientPeg.unset();
|
MatrixClientPeg.unset();
|
||||||
|
|
||||||
|
@ -417,7 +417,7 @@ describe('CallHandler without third party protocols', () => {
|
||||||
it('should still start a native call', async () => {
|
it('should still start a native call', async () => {
|
||||||
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
|
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
|
// Check that a call was started: its room on the protocol level
|
||||||
// should be the virtual room
|
// 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 { LocalRoom, LOCAL_ROOM_ID_PREFIX } from '../src/models/LocalRoom';
|
||||||
import { RoomViewStore } from '../src/stores/RoomViewStore';
|
import { RoomViewStore } from '../src/stores/RoomViewStore';
|
||||||
import SettingsStore from '../src/settings/SettingsStore';
|
import SettingsStore from '../src/settings/SettingsStore';
|
||||||
import CallHandler from '../src/CallHandler';
|
import LegacyCallHandler from '../src/LegacyCallHandler';
|
||||||
|
|
||||||
describe('SlashCommands', () => {
|
describe('SlashCommands', () => {
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
|
@ -120,7 +120,7 @@ describe('SlashCommands', () => {
|
||||||
describe("isEnabled", () => {
|
describe("isEnabled", () => {
|
||||||
describe("when virtual rooms are supported", () => {
|
describe("when virtual rooms are supported", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(CallHandler.instance, "getSupportsVirtualRooms").mockReturnValue(true);
|
jest.spyOn(LegacyCallHandler.instance, "getSupportsVirtualRooms").mockReturnValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return true for Room", () => {
|
it("should return true for Room", () => {
|
||||||
|
@ -136,7 +136,7 @@ describe('SlashCommands', () => {
|
||||||
|
|
||||||
describe("when virtual rooms are not supported", () => {
|
describe("when virtual rooms are not supported", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(CallHandler.instance, "getSupportsVirtualRooms").mockReturnValue(false);
|
jest.spyOn(LegacyCallHandler.instance, "getSupportsVirtualRooms").mockReturnValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false for Room", () => {
|
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 { stubClient } from '../../test-utils';
|
||||||
import { MatrixClientPeg } from '../../../src/MatrixClientPeg';
|
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 MY_USER_ID = "@me:here";
|
||||||
const THEIR_USER_ID = "@they:here";
|
const THEIR_USER_ID = "@they:here";
|
||||||
|
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
|
|
||||||
describe('CallEventGrouper', () => {
|
describe('LegacyCallEventGrouper', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
stubClient();
|
stubClient();
|
||||||
client = MatrixClientPeg.get();
|
client = MatrixClientPeg.get();
|
||||||
|
@ -37,7 +37,7 @@ describe('CallEventGrouper', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects a missed call", () => {
|
it("detects a missed call", () => {
|
||||||
const grouper = new CallEventGrouper();
|
const grouper = new LegacyCallEventGrouper();
|
||||||
|
|
||||||
grouper.add({
|
grouper.add({
|
||||||
getContent: () => {
|
getContent: () => {
|
||||||
|
@ -57,8 +57,8 @@ describe('CallEventGrouper', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects an ended call", () => {
|
it("detects an ended call", () => {
|
||||||
const grouperHangup = new CallEventGrouper();
|
const grouperHangup = new LegacyCallEventGrouper();
|
||||||
const grouperReject = new CallEventGrouper();
|
const grouperReject = new LegacyCallEventGrouper();
|
||||||
|
|
||||||
grouperHangup.add({
|
grouperHangup.add({
|
||||||
getContent: () => {
|
getContent: () => {
|
||||||
|
@ -119,7 +119,7 @@ describe('CallEventGrouper', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects call type", () => {
|
it("detects call type", () => {
|
||||||
const grouper = new CallEventGrouper();
|
const grouper = new LegacyCallEventGrouper();
|
||||||
|
|
||||||
grouper.add({
|
grouper.add({
|
||||||
getContent: () => {
|
getContent: () => {
|
|
@ -15,110 +15,103 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
// eslint-disable-next-line deprecate/import
|
import { render, screen, act, fireEvent, waitFor, cleanup } from "@testing-library/react";
|
||||||
import { mount } from "enzyme";
|
import { mocked, Mocked } from "jest-mock";
|
||||||
import { act } from "react-dom/test-utils";
|
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||||
import { mocked } from "jest-mock";
|
|
||||||
import { MatrixClient, IMyDevice } from "matrix-js-sdk/src/client";
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
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 {
|
import {
|
||||||
stubClient,
|
stubClient,
|
||||||
stubVideoChannelStore,
|
mkRoomMember,
|
||||||
StubVideoChannelStore,
|
|
||||||
mkRoom,
|
|
||||||
wrapInMatrixClientContext,
|
wrapInMatrixClientContext,
|
||||||
mockStateEventImplementation,
|
useMockedCalls,
|
||||||
mkVideoChannelMember,
|
MockedCall,
|
||||||
|
setupAsyncStoreWithClient,
|
||||||
} from "../../test-utils";
|
} from "../../test-utils";
|
||||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||||
import { VIDEO_CHANNEL_MEMBER } from "../../../src/utils/VideoChannelUtils";
|
import { VideoRoomView as UnwrappedVideoRoomView } from "../../../src/components/structures/VideoRoomView";
|
||||||
import WidgetStore from "../../../src/stores/WidgetStore";
|
import { WidgetMessagingStore } from "../../../src/stores/widgets/WidgetMessagingStore";
|
||||||
import _VideoRoomView from "../../../src/components/structures/VideoRoomView";
|
import { CallStore } from "../../../src/stores/CallStore";
|
||||||
import VideoLobby from "../../../src/components/views/voip/VideoLobby";
|
import { ConnectionState } from "../../../src/models/Call";
|
||||||
import AppTile from "../../../src/components/views/elements/AppTile";
|
|
||||||
|
|
||||||
const VideoRoomView = wrapInMatrixClientContext(_VideoRoomView);
|
const VideoRoomView = wrapInMatrixClientContext(UnwrappedVideoRoomView);
|
||||||
|
|
||||||
describe("VideoRoomView", () => {
|
describe("VideoRoomView", () => {
|
||||||
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([{
|
useMockedCalls();
|
||||||
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 },
|
|
||||||
}]);
|
|
||||||
Object.defineProperty(navigator, "mediaDevices", {
|
Object.defineProperty(navigator, "mediaDevices", {
|
||||||
value: { enumerateDevices: () => [] },
|
value: {
|
||||||
|
enumerateDevices: async () => [],
|
||||||
|
getUserMedia: () => null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let cli: MatrixClient;
|
let client: Mocked<MatrixClient>;
|
||||||
let room: Room;
|
let room: Room;
|
||||||
let store: StubVideoChannelStore;
|
let call: Call;
|
||||||
|
let widget: Widget;
|
||||||
|
let alice: RoomMember;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
stubClient();
|
stubClient();
|
||||||
cli = MatrixClientPeg.get();
|
client = mocked(MatrixClientPeg.get());
|
||||||
jest.spyOn(WidgetStore.instance, "matrixClient", "get").mockReturnValue(cli);
|
|
||||||
store = stubVideoChannelStore();
|
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||||
room = mkRoom(cli, "!1: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 () => {
|
afterEach(() => {
|
||||||
// Simulate an unclean disconnect
|
cleanup();
|
||||||
store.roomId = "!1:example.org";
|
call.destroy();
|
||||||
|
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||||
|
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||||
|
});
|
||||||
|
|
||||||
const devices: IMyDevice[] = [
|
const renderView = async (): Promise<void> => {
|
||||||
{
|
render(<VideoRoomView room={room} resizing={false} />);
|
||||||
device_id: cli.getDeviceId(),
|
await act(() => Promise.resolve()); // Let effects settle
|
||||||
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 });
|
|
||||||
|
|
||||||
// Make both devices be stuck
|
it("calls clean on mount", async () => {
|
||||||
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
|
const cleanSpy = jest.spyOn(call, "clean");
|
||||||
mkVideoChannelMember(cli.getUserId(), devices.map(d => d.device_id)),
|
await renderView();
|
||||||
]));
|
expect(cleanSpy).toHaveBeenCalled();
|
||||||
|
|
||||||
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("shows lobby and keeps widget loaded when disconnected", async () => {
|
it("shows lobby and keeps widget loaded when disconnected", async () => {
|
||||||
const view = mount(<VideoRoomView room={room} resizing={false} />);
|
await renderView();
|
||||||
// Wait for state to settle
|
screen.getByRole("button", { name: "Join" });
|
||||||
await act(() => Promise.resolve());
|
screen.getAllByText(/\bwidget\b/i);
|
||||||
|
|
||||||
expect(view.find(VideoLobby).exists()).toEqual(true);
|
|
||||||
expect(view.find(AppTile).exists()).toEqual(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("only shows widget when connected", async () => {
|
it("only shows widget when connected", async () => {
|
||||||
store.connect("!1:example.org");
|
await renderView();
|
||||||
const view = mount(<VideoRoomView room={room} resizing={false} />);
|
fireEvent.click(screen.getByRole("button", { name: "Join" }));
|
||||||
// Wait for state to settle
|
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Connected));
|
||||||
await act(() => Promise.resolve());
|
expect(screen.queryByRole("button", { name: "Join" })).toBe(null);
|
||||||
|
screen.getAllByText(/\bwidget\b/i);
|
||||||
expect(view.find(VideoLobby).exists()).toEqual(false);
|
|
||||||
expect(view.find(AppTile).exists()).toEqual(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -93,7 +93,7 @@ describe("AppTile", () => {
|
||||||
url: "https://example.com",
|
url: "https://example.com",
|
||||||
name: "Example 1",
|
name: "Example 1",
|
||||||
creatorUserId: cli.getUserId(),
|
creatorUserId: cli.getUserId(),
|
||||||
avatar_url: null,
|
avatar_url: undefined,
|
||||||
};
|
};
|
||||||
app2 = {
|
app2 = {
|
||||||
id: "1",
|
id: "1",
|
||||||
|
@ -103,7 +103,7 @@ describe("AppTile", () => {
|
||||||
url: "https://example.com",
|
url: "https://example.com",
|
||||||
name: "Example 2",
|
name: "Example 2",
|
||||||
creatorUserId: cli.getUserId(),
|
creatorUserId: cli.getUserId(),
|
||||||
avatar_url: null,
|
avatar_url: undefined,
|
||||||
};
|
};
|
||||||
jest.spyOn(WidgetStore.instance, "getApps").mockImplementation(roomId => {
|
jest.spyOn(WidgetStore.instance, "getApps").mockImplementation(roomId => {
|
||||||
if (roomId === "r1") return [app1];
|
if (roomId === "r1") return [app1];
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
exports[`<TooltipTarget /> displays Bottom aligned tooltip on mouseover 1`] = `
|
exports[`<TooltipTarget /> displays Bottom aligned tooltip on mouseover 1`] = `
|
||||||
<div
|
<div
|
||||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||||
|
role="tooltip"
|
||||||
style="display: block; top: 6px; left: 0px; transform: translate(-50%);"
|
style="display: block; top: 6px; left: 0px; transform: translate(-50%);"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -15,6 +16,7 @@ exports[`<TooltipTarget /> displays Bottom aligned tooltip on mouseover 1`] = `
|
||||||
exports[`<TooltipTarget /> displays InnerBottom aligned tooltip on mouseover 1`] = `
|
exports[`<TooltipTarget /> displays InnerBottom aligned tooltip on mouseover 1`] = `
|
||||||
<div
|
<div
|
||||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||||
|
role="tooltip"
|
||||||
style="display: block; top: -50px; left: 0px; transform: translate(-50%);"
|
style="display: block; top: -50px; left: 0px; transform: translate(-50%);"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -27,6 +29,7 @@ exports[`<TooltipTarget /> displays InnerBottom aligned tooltip on mouseover 1`]
|
||||||
exports[`<TooltipTarget /> displays Left aligned tooltip on mouseover 1`] = `
|
exports[`<TooltipTarget /> displays Left aligned tooltip on mouseover 1`] = `
|
||||||
<div
|
<div
|
||||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||||
|
role="tooltip"
|
||||||
style="display: block; right: 1030px; top: 0px; transform: translateY(-50%);"
|
style="display: block; right: 1030px; top: 0px; transform: translateY(-50%);"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -39,6 +42,7 @@ exports[`<TooltipTarget /> displays Left aligned tooltip on mouseover 1`] = `
|
||||||
exports[`<TooltipTarget /> displays Natural aligned tooltip on mouseover 1`] = `
|
exports[`<TooltipTarget /> displays Natural aligned tooltip on mouseover 1`] = `
|
||||||
<div
|
<div
|
||||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||||
|
role="tooltip"
|
||||||
style="display: block; left: 6px; top: 0px; transform: translateY(-50%);"
|
style="display: block; left: 6px; top: 0px; transform: translateY(-50%);"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -51,6 +55,7 @@ exports[`<TooltipTarget /> displays Natural aligned tooltip on mouseover 1`] = `
|
||||||
exports[`<TooltipTarget /> displays Right aligned tooltip on mouseover 1`] = `
|
exports[`<TooltipTarget /> displays Right aligned tooltip on mouseover 1`] = `
|
||||||
<div
|
<div
|
||||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||||
|
role="tooltip"
|
||||||
style="display: block; left: 6px; top: 0px; transform: translateY(-50%);"
|
style="display: block; left: 6px; top: 0px; transform: translateY(-50%);"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -63,6 +68,7 @@ exports[`<TooltipTarget /> displays Right aligned tooltip on mouseover 1`] = `
|
||||||
exports[`<TooltipTarget /> displays Top aligned tooltip on mouseover 1`] = `
|
exports[`<TooltipTarget /> displays Top aligned tooltip on mouseover 1`] = `
|
||||||
<div
|
<div
|
||||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||||
|
role="tooltip"
|
||||||
style="display: block; top: -6px; left: 0px; transform: translate(-50%, -100%);"
|
style="display: block; top: -6px; left: 0px; transform: translate(-50%, -100%);"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -75,6 +81,7 @@ exports[`<TooltipTarget /> displays Top aligned tooltip on mouseover 1`] = `
|
||||||
exports[`<TooltipTarget /> displays TopRight aligned tooltip on mouseover 1`] = `
|
exports[`<TooltipTarget /> displays TopRight aligned tooltip on mouseover 1`] = `
|
||||||
<div
|
<div
|
||||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||||
|
role="tooltip"
|
||||||
style="display: block; top: -6px; right: 1024px; transform: translateY(-100%);"
|
style="display: block; top: -6px; right: 1024px; transform: translateY(-100%);"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -15,148 +15,131 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
// eslint-disable-next-line deprecate/import
|
import { render, screen, act } from "@testing-library/react";
|
||||||
import { mount } from "enzyme";
|
import { mocked, Mocked } from "jest-mock";
|
||||||
import { act } from "react-dom/test-utils";
|
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||||
import { mocked } from "jest-mock";
|
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
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 {
|
import {
|
||||||
stubClient,
|
stubClient,
|
||||||
mockStateEventImplementation,
|
mkRoomMember,
|
||||||
mkRoom,
|
MockedCall,
|
||||||
mkVideoChannelMember,
|
useMockedCalls,
|
||||||
stubVideoChannelStore,
|
setupAsyncStoreWithClient,
|
||||||
StubVideoChannelStore,
|
|
||||||
} from "../../../test-utils";
|
} 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 RoomTile from "../../../../src/components/views/rooms/RoomTile";
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
|
||||||
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
||||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
import PlatformPeg from "../../../../src/PlatformPeg";
|
import PlatformPeg from "../../../../src/PlatformPeg";
|
||||||
import BasePlatform from "../../../../src/BasePlatform";
|
import BasePlatform from "../../../../src/BasePlatform";
|
||||||
|
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("RoomTile", () => {
|
describe("RoomTile", () => {
|
||||||
jest.spyOn(PlatformPeg, 'get')
|
jest.spyOn(PlatformPeg, "get")
|
||||||
.mockReturnValue({ overrideBrowserShortcuts: () => false } as unknown as BasePlatform);
|
.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(() => {
|
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();
|
stubClient();
|
||||||
cli = MatrixClientPeg.get();
|
client = mocked(MatrixClientPeg.get());
|
||||||
store = stubVideoChannelStore();
|
|
||||||
DMRoomMap.makeShared();
|
DMRoomMap.makeShared();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("video rooms", () => {
|
describe("call subtitle", () => {
|
||||||
let room: Room;
|
let room: Room;
|
||||||
|
let call: MockedCall;
|
||||||
|
let widget: Widget;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
room = mkRoom(cli, "!1:example.org");
|
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||||
mocked(room.isElementVideoRoom).mockReturnValue(true);
|
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(
|
afterEach(() => {
|
||||||
<RoomTile
|
call.destroy();
|
||||||
room={room}
|
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||||
showMessagePreview={false}
|
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||||
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");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("displays connected members", () => {
|
it("tracks connection state", async () => {
|
||||||
mockGetMember(room, userId => userId === "@chris:example.org" ? "leave" : "join");
|
screen.getByText("Video");
|
||||||
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"]),
|
|
||||||
]));
|
|
||||||
|
|
||||||
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
|
await Promise.all([
|
||||||
expect(tile.find(".mx_VideoRoomSummary_participants").text()).toEqual("1");
|
(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", () => {
|
it("tracks participants", () => {
|
||||||
mockGetMember(room);
|
const alice = mkRoomMember(room.roomId, "@alice:example.org");
|
||||||
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
|
const bob = mkRoomMember(room.roomId, "@bob:example.org");
|
||||||
// Make the remote echo claim that we're connected, while leaving the store disconnected
|
const carol = mkRoomMember(room.roomId, "@carol:example.org");
|
||||||
mkVideoChannelMember(cli.getUserId(), [cli.getDeviceId()]),
|
|
||||||
]));
|
|
||||||
|
|
||||||
const tile = mountTile();
|
expect(screen.queryByLabelText(/participant/)).toBe(null);
|
||||||
|
|
||||||
// Because of our local echo, we should still appear as disconnected
|
act(() => { call.participants = new Set([alice]); });
|
||||||
expect(tile.find(".mx_VideoRoomSummary_participants").exists()).toEqual(false);
|
expect(screen.getByLabelText("1 participant").textContent).toBe("1");
|
||||||
});
|
|
||||||
|
|
||||||
it("doesn't count members whose device data has expired", () => {
|
act(() => { call.participants = new Set([alice, bob, carol]); });
|
||||||
jest.useFakeTimers();
|
expect(screen.getByLabelText("3 participants").textContent).toBe("3");
|
||||||
jest.setSystemTime(0);
|
|
||||||
|
|
||||||
mockGetMember(room);
|
act(() => { call.participants = new Set(); });
|
||||||
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
|
expect(screen.queryByLabelText(/participant/)).toBe(null);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
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 { MatrixClientPeg } from "../src/MatrixClientPeg";
|
||||||
import WidgetStore from "../src/stores/WidgetStore";
|
import WidgetStore from "../src/stores/WidgetStore";
|
||||||
import WidgetUtils from "../src/utils/WidgetUtils";
|
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';
|
import createRoom, { canEncryptToAllUsers } from '../src/createRoom';
|
||||||
|
|
||||||
describe("createRoom", () => {
|
describe("createRoom", () => {
|
||||||
|
@ -51,7 +51,7 @@ describe("createRoom", () => {
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
"im.vector.modular.widgets": widgetPower,
|
"im.vector.modular.widgets": widgetPower,
|
||||||
[VIDEO_CHANNEL_MEMBER]: videoMemberPower,
|
[JitsiCall.MEMBER_EVENT_TYPE]: jitsiMemberPower,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}]] = mocked(client.createRoom).mock.calls as any; // no good type
|
}]] = mocked(client.createRoom).mock.calls as any; // no good type
|
||||||
|
@ -64,7 +64,7 @@ describe("createRoom", () => {
|
||||||
expect(widgetStateKey).toEqual("im.vector.modular.widgets");
|
expect(widgetStateKey).toEqual("im.vector.modular.widgets");
|
||||||
|
|
||||||
// All members should be able to update their connected devices
|
// 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
|
// Jitsi widget should be immutable for admins
|
||||||
expect(widgetPower).toBeGreaterThan(100);
|
expect(widgetPower).toBeGreaterThan(100);
|
||||||
// and we should have been reset back to admin
|
// 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