Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 3d54059

Browse files
committed
Add call tiles
1 parent 7a33818 commit 3d54059

File tree

16 files changed

+539
-46
lines changed

16 files changed

+539
-46
lines changed

res/css/_components.pcss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@
210210
@import "./views/elements/_Validation.pcss";
211211
@import "./views/emojipicker/_EmojiPicker.pcss";
212212
@import "./views/location/_LocationPicker.pcss";
213+
@import "./views/messages/_CallEvent.pcss";
213214
@import "./views/messages/_CreateEvent.pcss";
214215
@import "./views/messages/_DateSeparator.pcss";
215216
@import "./views/messages/_DisambiguatedProfile.pcss";
@@ -264,6 +265,7 @@
264265
@import "./views/rooms/_JumpToBottomButton.pcss";
265266
@import "./views/rooms/_LinkPreviewGroup.pcss";
266267
@import "./views/rooms/_LinkPreviewWidget.pcss";
268+
@import "./views/rooms/_LiveContentSummary.pcss";
267269
@import "./views/rooms/_MemberInfo.pcss";
268270
@import "./views/rooms/_MemberList.pcss";
269271
@import "./views/rooms/_MessageComposer.pcss";
@@ -285,7 +287,6 @@
285287
@import "./views/rooms/_RoomPreviewCard.pcss";
286288
@import "./views/rooms/_RoomSublist.pcss";
287289
@import "./views/rooms/_RoomTile.pcss";
288-
@import "./views/rooms/_RoomTileCallSummary.pcss";
289290
@import "./views/rooms/_RoomUpgradeWarningBar.pcss";
290291
@import "./views/rooms/_SearchBar.pcss";
291292
@import "./views/rooms/_SendMessageComposer.pcss";

res/css/views/elements/_FacePile.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ limitations under the License.
2222
display: inline-flex;
2323
flex-direction: row-reverse;
2424
vertical-align: middle;
25+
margin: 0 -1px; /* to cancel out the border on the edges */
2526

2627
/* Overlap the children */
2728
> * + * {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_CallEvent_wrapper {
18+
display: flex;
19+
width: 100%;
20+
}
21+
22+
.mx_CallEvent {
23+
padding: 12px;
24+
box-sizing: border-box;
25+
min-height: 60px;
26+
max-width: 600px;
27+
width: 100%;
28+
background-color: $system;
29+
border-radius: 8px;
30+
31+
display: flex;
32+
align-items: center;
33+
justify-content: space-between;
34+
gap: $spacing-8;
35+
36+
.mx_CallEvent_title {
37+
font-size: $font-15px;
38+
line-height: 24px; /* in px to match the avatar */
39+
}
40+
41+
&.mx_CallEvent_inactive .mx_CallEvent_title::before {
42+
display: inline-block;
43+
vertical-align: middle;
44+
content: '';
45+
background-color: $secondary-content;
46+
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
47+
mask-size: 16px;
48+
width: 16px;
49+
height: 16px;
50+
margin-right: 8px;
51+
}
52+
53+
&.mx_CallEvent_active .mx_CallEvent_title {
54+
font-weight: 600;
55+
}
56+
57+
> .mx_BaseAvatar {
58+
align-self: flex-start;
59+
}
60+
61+
> .mx_CallEvent_infoRows {
62+
flex-grow: 1;
63+
64+
display: flex;
65+
flex-direction: column;
66+
gap: $spacing-4;
67+
}
68+
69+
> .mx_CallEvent_duration {
70+
padding: $spacing-4;
71+
color: $secondary-content;
72+
font-size: $font-12px;
73+
}
74+
75+
> .mx_CallEvent_button {
76+
box-sizing: border-box;
77+
min-width: 120px;
78+
}
79+
}

res/css/views/rooms/_EventBubbleTile.pcss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,8 @@ limitations under the License.
523523
max-width: 100%;
524524
}
525525

526-
.mx_LegacyCallEvent_wrapper {
526+
.mx_LegacyCallEvent_wrapper,
527+
.mx_CallEvent_wrapper {
527528
justify-content: center;
528529
}
529530
}

res/css/views/rooms/_RoomTileCallSummary.pcss renamed to res/css/views/rooms/_LiveContentSummary.pcss

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,26 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
.mx_RoomTileCallSummary {
18-
.mx_RoomTileCallSummary_text {
17+
.mx_LiveContentSummary {
18+
color: $secondary-content;
19+
20+
.mx_LiveContentSummary_text {
1921
&::before {
2022
display: inline-block;
2123
vertical-align: text-bottom;
2224
content: '';
2325
background-color: $secondary-content;
24-
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
2526
mask-size: 16px;
2627
width: 16px;
2728
height: 16px;
2829
margin-right: 4px;
2930
}
3031

31-
&.mx_RoomTileCallSummary_text_active {
32+
&.mx_LiveContentSummary_text_video::before {
33+
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
34+
}
35+
36+
&.mx_LiveContentSummary_text_active {
3237
color: $accent;
3338

3439
&::before {
@@ -37,7 +42,7 @@ limitations under the License.
3742
}
3843
}
3944

40-
.mx_RoomTileCallSummary_participants::before {
45+
.mx_LiveContentSummary_participants::before {
4146
display: inline-block;
4247
vertical-align: text-bottom;
4348
content: '';
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React, { forwardRef, useCallback, useContext, useEffect, useMemo, useState } from "react";
18+
19+
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
20+
import { Call, ConnectionState } from "../../../models/Call";
21+
import { _t } from "../../../languageHandler";
22+
import { useCall, useConnectionState, useParticipants } from "../../../hooks/useCall";
23+
import defaultDispatcher from "../../../dispatcher/dispatcher";
24+
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
25+
import { Action } from "../../../dispatcher/actions";
26+
import type { ButtonEvent } from "../elements/AccessibleButton";
27+
import MemberAvatar from "../avatars/MemberAvatar";
28+
import { LiveContentSummary, LiveContentType } from "../rooms/LiveContentSummary";
29+
import FacePile from "../elements/FacePile";
30+
import { formatCallTime } from "../../../DateUtils";
31+
import AccessibleButton from "../elements/AccessibleButton";
32+
import MatrixClientContext from "../../../contexts/MatrixClientContext";
33+
34+
interface ActiveCallEventProps {
35+
mxEvent: MatrixEvent;
36+
call: Call;
37+
}
38+
39+
const MAX_FACES = 8;
40+
41+
const ActiveCallEvent = forwardRef<any, ActiveCallEventProps>(({ mxEvent, call }, ref) => {
42+
const connectionState = useConnectionState(call);
43+
const participants = useParticipants(call);
44+
45+
const connect = useCallback((ev: ButtonEvent) => {
46+
ev.preventDefault();
47+
defaultDispatcher.dispatch<ViewRoomPayload>({
48+
action: Action.ViewRoom,
49+
room_id: mxEvent.getRoomId()!,
50+
view_call: true,
51+
metricsTrigger: undefined,
52+
});
53+
}, [mxEvent]);
54+
55+
const disconnect = useCallback((ev: ButtonEvent) => {
56+
ev.preventDefault();
57+
call.disconnect();
58+
}, [call]);
59+
60+
const [buttonText, buttonKind, buttonDisabled, onButtonClick] = useMemo(() => {
61+
switch (connectionState) {
62+
case ConnectionState.Disconnected: return [_t("Join"), "primary", false, connect];
63+
case ConnectionState.Connecting: return [_t("Join"), "primary", true, connect];
64+
case ConnectionState.Connected: return [_t("Leave"), "danger", false, disconnect];
65+
case ConnectionState.Disconnecting: return [_t("Leave"), "danger", true, disconnect];
66+
}
67+
}, [connectionState, connect, disconnect]);
68+
69+
const [now, setNow] = useState(() => Date.now());
70+
const duration = now - mxEvent.getTs();
71+
72+
useEffect(() => {
73+
const timer = setInterval(() => setNow(Date.now()), 1000);
74+
return () => clearInterval(timer);
75+
}, []);
76+
77+
const facePileMembers = useMemo(() => [...participants].slice(0, MAX_FACES), [participants]);
78+
const facePileOverflow = participants.size > facePileMembers.length;
79+
80+
const senderName = mxEvent.sender?.name ?? mxEvent.getSender();
81+
82+
return <div className="mx_CallEvent_wrapper" ref={ref}>
83+
<div className="mx_CallEvent mx_CallEvent_active">
84+
<MemberAvatar
85+
member={mxEvent.sender}
86+
fallbackUserId={mxEvent.getSender()}
87+
viewUserOnClick
88+
width={24}
89+
height={24}
90+
/>
91+
<div className="mx_CallEvent_infoRows">
92+
<span className="mx_CallEvent_title">
93+
{ _t("%(name)s started a video call", { name: senderName }) }
94+
</span>
95+
<LiveContentSummary
96+
type={LiveContentType.Video}
97+
text={_t("Video call")}
98+
active={false}
99+
participantCount={participants.size}
100+
/>
101+
<FacePile members={facePileMembers} faceSize={24} overflow={facePileOverflow} />
102+
</div>
103+
{ /* Clock desync could lead to a negative duration, so just hide it if that happens */ }
104+
{ duration > 0 && <div className="mx_CallEvent_duration">
105+
{ formatCallTime(new Date(duration)) }
106+
</div> }
107+
<AccessibleButton
108+
className="mx_CallEvent_button"
109+
kind={buttonKind}
110+
disabled={buttonDisabled}
111+
onClick={onButtonClick}
112+
>
113+
{ buttonText }
114+
</AccessibleButton>
115+
</div>
116+
</div>;
117+
});
118+
119+
interface CallEventProps {
120+
mxEvent: MatrixEvent;
121+
}
122+
123+
/**
124+
* An event tile representing an active or historical Element call.
125+
*/
126+
export const CallEvent = forwardRef<any, CallEventProps>(({ mxEvent }, ref) => {
127+
const client = useContext(MatrixClientContext);
128+
const call = useCall(mxEvent.getRoomId()!);
129+
const latestEvent = client.getRoom(mxEvent.getRoomId())!.currentState
130+
.getStateEvents(mxEvent.getType(), mxEvent.getStateKey()!);
131+
132+
if ("m.terminated" in latestEvent.getContent()) {
133+
const duration = latestEvent.getTs() - mxEvent.getTs();
134+
135+
// The call is terminated
136+
return <div className="mx_CallEvent_wrapper" ref={ref}>
137+
<div className="mx_CallEvent mx_CallEvent_inactive">
138+
<span className="mx_CallEvent_title">{ _t("Video call ended") }</span>
139+
{ /* Clock desync could lead to a negative duration, so just hide it if that happens */ }
140+
{ duration > 0 && <div className="mx_CallEvent_duration">
141+
{ formatCallTime(new Date(duration)) }
142+
</div> }
143+
</div>
144+
</div>;
145+
}
146+
147+
if (call === null) {
148+
const senderName = mxEvent.sender?.name ?? mxEvent.getSender();
149+
150+
// There should be a call, but it hasn't loaded yet
151+
return <div className="mx_CallEvent_wrapper" ref={ref}>
152+
<div className="mx_CallEvent mx_CallEvent_active">
153+
<MemberAvatar
154+
member={mxEvent.sender}
155+
fallbackUserId={mxEvent.getSender()}
156+
viewUserOnClick
157+
width={24}
158+
height={24}
159+
/>
160+
<div className="mx_CallEvent_infoRows">
161+
<span className="mx_CallEvent_title">
162+
{ _t("%(name)s started a video call", { name: senderName }) }
163+
</span>
164+
<LiveContentSummary
165+
type={LiveContentType.Video}
166+
text={_t("Video call")}
167+
active={false}
168+
participantCount={0}
169+
/>
170+
</div>
171+
<AccessibleButton
172+
className="mx_CallEvent_button"
173+
kind="primary"
174+
disabled={true}
175+
onClick={() => {}}
176+
>
177+
{ _t("Join") }
178+
</AccessibleButton>
179+
</div>
180+
</div>;
181+
}
182+
183+
return <ActiveCallEvent mxEvent={mxEvent} call={call} ref={ref} />;
184+
});

src/components/views/rooms/EventTile.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import { ReadReceiptGroup } from './ReadReceiptGroup';
8383
import { useTooltip } from "../../../utils/useTooltip";
8484
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
8585
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
86+
import { ElementCall } from "../../../models/Call";
8687

8788
export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations;
8889

@@ -937,7 +938,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
937938

938939
public render() {
939940
const msgtype = this.props.mxEvent.getContent().msgtype;
940-
const eventType = this.props.mxEvent.getType() as EventType;
941+
const eventType = this.props.mxEvent.getType();
941942
const {
942943
hasRenderer,
943944
isBubbleMessage,
@@ -999,7 +1000,9 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
9991000
mx_EventTile_sending: !isEditing && isSending,
10001001
mx_EventTile_highlight: this.shouldHighlight(),
10011002
mx_EventTile_selected: this.props.isSelectedEvent || this.state.contextMenu,
1002-
mx_EventTile_continuation: isContinuation || eventType === EventType.CallInvite,
1003+
mx_EventTile_continuation: isContinuation
1004+
|| eventType === EventType.CallInvite
1005+
|| ElementCall.CALL_EVENT_TYPE.matches(eventType),
10031006
mx_EventTile_last: this.props.last,
10041007
mx_EventTile_lastInSection: this.props.lastInSection,
10051008
mx_EventTile_contextual: this.props.contextual,
@@ -1053,8 +1056,9 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
10531056
avatarSize = 14;
10541057
needsSenderProfile = true;
10551058
} else if (
1056-
(this.props.continuation && this.context.timelineRenderingType !== TimelineRenderingType.File) ||
1057-
eventType === EventType.CallInvite
1059+
(this.props.continuation && this.context.timelineRenderingType !== TimelineRenderingType.File)
1060+
|| eventType === EventType.CallInvite
1061+
|| ElementCall.CALL_EVENT_TYPE.matches(eventType)
10581062
) {
10591063
// no avatar or sender profile for continuation messages and call tiles
10601064
avatarSize = 0;

0 commit comments

Comments
 (0)