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

Commit 3fbb412

Browse files
committed
Add call tiles
1 parent a704a2f commit 3fbb412

File tree

17 files changed

+552
-51
lines changed

17 files changed

+552
-51
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: '';

src/components/views/elements/AccessibleButton.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ type IProps<T extends keyof JSX.IntrinsicElements> = DynamicHtmlElementProps<T>
7272
disabled?: boolean;
7373
className?: string;
7474
triggerOnMouseDown?: boolean;
75-
onClick(e?: ButtonEvent): void | Promise<void>;
75+
onClick: ((e: ButtonEvent) => void | Promise<void>) | null;
7676
};
7777

7878
interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {
@@ -106,9 +106,9 @@ export default function AccessibleButton<T extends keyof JSX.IntrinsicElements>(
106106
newProps["disabled"] = true;
107107
} else {
108108
if (triggerOnMouseDown) {
109-
newProps.onMouseDown = onClick;
109+
newProps.onMouseDown = onClick ?? undefined;
110110
} else {
111-
newProps.onClick = onClick;
111+
newProps.onClick = onClick ?? undefined;
112112
}
113113
// We need to consume enter onKeyDown and space onKeyUp
114114
// otherwise we are risking also activating other keyboard focusable elements
@@ -124,7 +124,7 @@ export default function AccessibleButton<T extends keyof JSX.IntrinsicElements>(
124124
case KeyBindingAction.Enter:
125125
e.stopPropagation();
126126
e.preventDefault();
127-
return onClick(e);
127+
return onClick?.(e);
128128
case KeyBindingAction.Space:
129129
e.stopPropagation();
130130
e.preventDefault();
@@ -144,7 +144,7 @@ export default function AccessibleButton<T extends keyof JSX.IntrinsicElements>(
144144
case KeyBindingAction.Space:
145145
e.stopPropagation();
146146
e.preventDefault();
147-
return onClick(e);
147+
return onClick?.(e);
148148
default:
149149
onKeyUp?.(e);
150150
break;
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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, { FC, forwardRef, useCallback, useContext, useEffect, useMemo, useState } from "react";
18+
19+
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
20+
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
21+
import { Call, ConnectionState } from "../../../models/Call";
22+
import { _t } from "../../../languageHandler";
23+
import { useCall, useConnectionState, useParticipants } from "../../../hooks/useCall";
24+
import defaultDispatcher from "../../../dispatcher/dispatcher";
25+
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
26+
import { Action } from "../../../dispatcher/actions";
27+
import type { ButtonEvent } from "../elements/AccessibleButton";
28+
import MemberAvatar from "../avatars/MemberAvatar";
29+
import { LiveContentSummary, LiveContentType } from "../rooms/LiveContentSummary";
30+
import FacePile from "../elements/FacePile";
31+
import { formatCallTime } from "../../../DateUtils";
32+
import AccessibleButton from "../elements/AccessibleButton";
33+
import MatrixClientContext from "../../../contexts/MatrixClientContext";
34+
35+
interface CallEventDurationProps {
36+
delta: number;
37+
}
38+
39+
const CallEventDuration: FC<CallEventDurationProps> = ({ delta }) => {
40+
// Clock desync could lead to a negative duration, so just hide it if that happens
41+
if (delta <= 0) return null;
42+
return <div className="mx_CallEvent_duration">{ formatCallTime(new Date(delta)) }</div>;
43+
};
44+
45+
const MAX_FACES = 8;
46+
47+
interface ActiveCallEventProps {
48+
mxEvent: MatrixEvent;
49+
participants: Set<RoomMember>;
50+
buttonText: string;
51+
buttonKind: string;
52+
onButtonClick: ((ev: ButtonEvent) => void) | null;
53+
}
54+
55+
const ActiveCallEvent = forwardRef<any, ActiveCallEventProps>(
56+
(
57+
{
58+
mxEvent,
59+
participants,
60+
buttonText,
61+
buttonKind,
62+
onButtonClick,
63+
},
64+
ref,
65+
) => {
66+
const senderName = useMemo(() => mxEvent.sender?.name ?? mxEvent.getSender(), [mxEvent]);
67+
68+
const facePileMembers = useMemo(() => [...participants].slice(0, MAX_FACES), [participants]);
69+
const facePileOverflow = participants.size > facePileMembers.length;
70+
71+
const [now, setNow] = useState(() => Date.now());
72+
useEffect(() => {
73+
const timer = setInterval(() => setNow(Date.now()), 1000);
74+
return () => clearInterval(timer);
75+
}, []);
76+
77+
return <div className="mx_CallEvent_wrapper" ref={ref}>
78+
<div className="mx_CallEvent mx_CallEvent_active">
79+
<MemberAvatar
80+
member={mxEvent.sender}
81+
fallbackUserId={mxEvent.getSender()}
82+
viewUserOnClick
83+
width={24}
84+
height={24}
85+
/>
86+
<div className="mx_CallEvent_infoRows">
87+
<span className="mx_CallEvent_title">
88+
{ _t("%(name)s started a video call", { name: senderName }) }
89+
</span>
90+
<LiveContentSummary
91+
type={LiveContentType.Video}
92+
text={_t("Video call")}
93+
active={false}
94+
participantCount={participants.size}
95+
/>
96+
<FacePile members={facePileMembers} faceSize={24} overflow={facePileOverflow} />
97+
</div>
98+
<CallEventDuration delta={now - mxEvent.getTs()} />
99+
<AccessibleButton
100+
className="mx_CallEvent_button"
101+
kind={buttonKind}
102+
disabled={onButtonClick === null}
103+
onClick={onButtonClick}
104+
>
105+
{ buttonText }
106+
</AccessibleButton>
107+
</div>
108+
</div>;
109+
},
110+
);
111+
112+
interface ActiveLoadedCallEventProps {
113+
mxEvent: MatrixEvent;
114+
call: Call;
115+
}
116+
117+
const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxEvent, call }, ref) => {
118+
const connectionState = useConnectionState(call);
119+
const participants = useParticipants(call);
120+
121+
const connect = useCallback((ev: ButtonEvent) => {
122+
ev.preventDefault();
123+
defaultDispatcher.dispatch<ViewRoomPayload>({
124+
action: Action.ViewRoom,
125+
room_id: mxEvent.getRoomId()!,
126+
view_call: true,
127+
metricsTrigger: undefined,
128+
});
129+
}, [mxEvent]);
130+
131+
const disconnect = useCallback((ev: ButtonEvent) => {
132+
ev.preventDefault();
133+
call.disconnect();
134+
}, [call]);
135+
136+
const [buttonText, buttonKind, onButtonClick] = useMemo(() => {
137+
switch (connectionState) {
138+
case ConnectionState.Disconnected: return [_t("Join"), "primary", connect];
139+
case ConnectionState.Connecting: return [_t("Join"), "primary", null];
140+
case ConnectionState.Connected: return [_t("Leave"), "danger", disconnect];
141+
case ConnectionState.Disconnecting: return [_t("Leave"), "danger", null];
142+
}
143+
}, [connectionState, connect, disconnect]);
144+
145+
return <ActiveCallEvent
146+
ref={ref}
147+
mxEvent={mxEvent}
148+
participants={participants}
149+
buttonText={buttonText}
150+
buttonKind={buttonKind}
151+
onButtonClick={onButtonClick}
152+
/>;
153+
});
154+
155+
interface CallEventProps {
156+
mxEvent: MatrixEvent;
157+
}
158+
159+
/**
160+
* An event tile representing an active or historical Element call.
161+
*/
162+
export const CallEvent = forwardRef<any, CallEventProps>(({ mxEvent }, ref) => {
163+
const noParticipants = useMemo(() => new Set<RoomMember>(), []);
164+
const client = useContext(MatrixClientContext);
165+
const call = useCall(mxEvent.getRoomId()!);
166+
const latestEvent = client.getRoom(mxEvent.getRoomId())!.currentState
167+
.getStateEvents(mxEvent.getType(), mxEvent.getStateKey()!);
168+
169+
if ("m.terminated" in latestEvent.getContent()) {
170+
// The call is terminated
171+
return <div className="mx_CallEvent_wrapper" ref={ref}>
172+
<div className="mx_CallEvent mx_CallEvent_inactive">
173+
<span className="mx_CallEvent_title">{ _t("Video call ended") }</span>
174+
<CallEventDuration delta={latestEvent.getTs() - mxEvent.getTs()} />
175+
</div>
176+
</div>;
177+
}
178+
179+
if (call === null) {
180+
// There should be a call, but it hasn't loaded yet
181+
return <ActiveCallEvent
182+
ref={ref}
183+
mxEvent={mxEvent}
184+
participants={noParticipants}
185+
buttonText={_t("Join")}
186+
buttonKind="primary"
187+
onButtonClick={null}
188+
/>;
189+
}
190+
191+
return <ActiveLoadedCallEvent mxEvent={mxEvent} call={call} ref={ref} />;
192+
});

0 commit comments

Comments
 (0)