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

Commit 90419bd

Browse files
Implement new unreachable state and fix broken string ref (#11748)
* Fix string ref issue * Implement unreachable state * Fix eslint failure * Fix i18n * Fix i18n again * Write cypress test * Write jest test * Write more jest tests * Update method name * Use unstable prefix * Always use prefix This is never to going to be in the spec so always use the io.element prefix * Update tests * Remove redundant code from cypress test * Use unstable prefix * Refactor code * Remove supressOnHover prop * Remove sub-text label * Join lines * Remove blank line * Add jsdoc
1 parent 6849afd commit 90419bd

File tree

9 files changed

+234
-133
lines changed

9 files changed

+234
-133
lines changed

cypress/e2e/presence/presence.spec.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
Copyright 2023 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+
/// <reference types="cypress" />
18+
import { HomeserverInstance } from "../../plugins/utils/homeserver";
19+
20+
describe("Presence tests", () => {
21+
let homeserver: HomeserverInstance;
22+
23+
beforeEach(() => {
24+
cy.startHomeserver("default").then((data) => {
25+
homeserver = data;
26+
});
27+
});
28+
29+
afterEach(() => {
30+
cy.stopHomeserver(homeserver);
31+
});
32+
33+
it("renders unreachable presence state correctly", () => {
34+
cy.initTestUser(homeserver, "Janet");
35+
cy.getBot(homeserver, { displayName: "Bob" }).then((bob) => {
36+
cy.intercept("GET", "**/sync*", (req) => {
37+
req.continue((res) => {
38+
res.body.presence = {
39+
events: [
40+
{
41+
type: "m.presence",
42+
sender: bob.getUserId(),
43+
content: {
44+
presence: "io.element.unreachable",
45+
currently_active: false,
46+
},
47+
},
48+
],
49+
};
50+
});
51+
});
52+
cy.createRoom({ name: "My Room", invite: [bob.getUserId()] }).then((roomId) => {
53+
cy.viewRoomById(roomId);
54+
});
55+
cy.findByRole("button", { name: "Room info" }).click();
56+
cy.get(".mx_RightPanel").within(() => {
57+
cy.contains("People").click();
58+
});
59+
cy.get(".mx_EntityTile_unreachable")
60+
.should("contain.text", "Bob")
61+
.should("contain.text", "User's server unreachable");
62+
});
63+
});
64+
});

res/css/views/rooms/_EntityTile.pcss

+5-3
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ limitations under the License.
4646
background-color: $header-panel-text-primary-color;
4747
}
4848

49-
.mx_EntityTile .mx_PresenceLabel {
49+
.mx_EntityTile:not(.mx_EntityTile_unreachable) .mx_PresenceLabel {
5050
display: none;
5151
}
5252

53-
.mx_EntityTile:not(.mx_EntityTile_noHover):hover .mx_PresenceLabel {
53+
.mx_EntityTile:hover .mx_PresenceLabel {
5454
display: block;
5555
}
5656

@@ -106,7 +106,9 @@ limitations under the License.
106106
}
107107

108108
.mx_EntityTile_unknown .mx_EntityTile_avatar,
109-
.mx_EntityTile_unknown .mx_EntityTile_name {
109+
.mx_EntityTile_unknown .mx_EntityTile_name,
110+
.mx_EntityTile_unreachable .mx_EntityTile_avatar,
111+
.mx_EntityTile_unreachable .mx_EntityTile_name {
110112
opacity: 0.25;
111113
}
112114

src/components/views/dialogs/ForwardDialog.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
264264
<BaseAvatar url={require("../../../../res/img/ellipsis.svg").default} name="..." size="36px" />
265265
}
266266
name={text}
267-
presenceState="online"
268-
suppressOnHover={true}
267+
showPresence={false}
269268
onClick={() => setTruncateAt(totalCount)}
270269
/>
271270
);

src/components/views/rooms/EntityTile.tsx

+30-44
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ const PowerLabel: Record<PowerStatus, TranslationKey> = {
3535
[PowerStatus.Moderator]: _td("power_level|mod"),
3636
};
3737

38-
export type PresenceState = "offline" | "online" | "unavailable";
38+
export type PresenceState = "offline" | "online" | "unavailable" | "io.element.unreachable";
3939

4040
const PRESENCE_CLASS: Record<PresenceState, string> = {
41-
offline: "mx_EntityTile_offline",
42-
online: "mx_EntityTile_online",
43-
unavailable: "mx_EntityTile_unavailable",
41+
"offline": "mx_EntityTile_offline",
42+
"online": "mx_EntityTile_online",
43+
"unavailable": "mx_EntityTile_unavailable",
44+
"io.element.unreachable": "mx_EntityTile_unreachable",
4445
};
4546

4647
function presenceClassForMember(presenceState?: PresenceState, lastActiveAgo?: number, showPresence?: boolean): string {
@@ -75,7 +76,6 @@ interface IProps {
7576
presenceCurrentlyActive?: boolean;
7677
showInviteButton: boolean;
7778
onClick(): void;
78-
suppressOnHover: boolean;
7979
showPresence: boolean;
8080
subtextLabel?: string;
8181
e2eStatus?: E2EState;
@@ -93,7 +93,6 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
9393
presenceLastActiveAgo: 0,
9494
presenceLastTs: 0,
9595
showInviteButton: false,
96-
suppressOnHover: false,
9796
showPresence: true,
9897
};
9998

@@ -105,10 +104,27 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
105104
};
106105
}
107106

107+
/**
108+
* Creates the PresenceLabel component if needed
109+
* @returns The PresenceLabel component if we need to render it, undefined otherwise
110+
*/
111+
private getPresenceLabel(): JSX.Element | undefined {
112+
if (!this.props.showPresence) return;
113+
const activeAgo = this.props.presenceLastActiveAgo
114+
? Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)
115+
: -1;
116+
return (
117+
<PresenceLabel
118+
activeAgo={activeAgo}
119+
currentlyActive={this.props.presenceCurrentlyActive}
120+
presenceState={this.props.presenceState}
121+
/>
122+
);
123+
}
124+
108125
public render(): React.ReactNode {
109126
const mainClassNames: Record<string, boolean> = {
110127
mx_EntityTile: true,
111-
mx_EntityTile_noHover: !!this.props.suppressOnHover,
112128
};
113129
if (this.props.className) mainClassNames[this.props.className] = true;
114130

@@ -119,43 +135,13 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
119135
);
120136
mainClassNames[presenceClass] = true;
121137

122-
let nameEl;
123138
const name = this.props.nameJSX || this.props.name;
124-
125-
if (!this.props.suppressOnHover) {
126-
const activeAgo = this.props.presenceLastActiveAgo
127-
? Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)
128-
: -1;
129-
130-
let presenceLabel: JSX.Element | undefined;
131-
if (this.props.showPresence) {
132-
presenceLabel = (
133-
<PresenceLabel
134-
activeAgo={activeAgo}
135-
currentlyActive={this.props.presenceCurrentlyActive}
136-
presenceState={this.props.presenceState}
137-
/>
138-
);
139-
}
140-
if (this.props.subtextLabel) {
141-
presenceLabel = <span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>;
142-
}
143-
nameEl = (
144-
<div className="mx_EntityTile_details">
145-
<div className="mx_EntityTile_name">{name}</div>
146-
{presenceLabel}
147-
</div>
148-
);
149-
} else if (this.props.subtextLabel) {
150-
nameEl = (
151-
<div className="mx_EntityTile_details">
152-
<div className="mx_EntityTile_name">{name}</div>
153-
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
154-
</div>
155-
);
156-
} else {
157-
nameEl = <div className="mx_EntityTile_name">{name}</div>;
158-
}
139+
const nameAndPresence = (
140+
<div className="mx_EntityTile_details">
141+
<div className="mx_EntityTile_name">{name}</div>
142+
{this.getPresenceLabel()}
143+
</div>
144+
);
159145

160146
let inviteButton;
161147
if (this.props.showInviteButton) {
@@ -198,7 +184,7 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
198184
{av}
199185
{e2eIcon}
200186
</div>
201-
{nameEl}
187+
{nameAndPresence}
202188
{powerLabel}
203189
{inviteButton}
204190
</AccessibleButton>

src/components/views/rooms/MemberList.tsx

+15-5
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export default class MemberList extends React.Component<IProps, IState> {
8181

8282
public static contextType = SDKContext;
8383
public context!: React.ContextType<typeof SDKContext>;
84+
private tiles: Map<string, MemberTile> = new Map();
8485

8586
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
8687
super(props);
@@ -154,7 +155,7 @@ export default class MemberList extends React.Component<IProps, IState> {
154155
// Attach a SINGLE listener for global presence changes then locate the
155156
// member tile and re-render it. This is more efficient than every tile
156157
// ever attaching their own listener.
157-
const tile = this.refs[user.userId];
158+
const tile = this.tiles.get(user.userId);
158159
if (tile) {
159160
this.updateList(); // reorder the membership list
160161
}
@@ -245,8 +246,7 @@ export default class MemberList extends React.Component<IProps, IState> {
245246
<BaseAvatar url={require("../../../../res/img/ellipsis.svg").default} name="..." size="36px" />
246247
}
247248
name={text}
248-
presenceState="online"
249-
suppressOnHover={true}
249+
showPresence={false}
250250
onClick={onClick}
251251
/>
252252
);
@@ -307,14 +307,24 @@ export default class MemberList extends React.Component<IProps, IState> {
307307
return members.map((m) => {
308308
if (m instanceof RoomMember) {
309309
// Is a Matrix invite
310-
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this.showPresence} />;
310+
return (
311+
<MemberTile
312+
key={m.userId}
313+
member={m}
314+
ref={(tile) => {
315+
if (tile) this.tiles.set(m.userId, tile);
316+
else this.tiles.delete(m.userId);
317+
}}
318+
showPresence={this.showPresence}
319+
/>
320+
);
311321
} else {
312322
// Is a 3pid invite
313323
return (
314324
<EntityTile
315325
key={m.getStateKey()}
316326
name={m.getContent().display_name}
317-
suppressOnHover={true}
327+
showPresence={false}
318328
onClick={() => this.onPending3pidInviteClick(m)}
319329
/>
320330
);

src/components/views/rooms/PresenceLabel.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export default class PresenceLabel extends React.Component<IProps> {
4444
// the 'active ago' ends up being 0.
4545
if (presence && BUSY_PRESENCE_NAME.matches(presence)) return _t("presence|busy");
4646

47+
if (presence === "io.element.unreachable") return _t("presence|unreachable");
48+
4749
if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) {
4850
const duration = formatDuration(activeAgo);
4951
if (presence === "online") return _t("presence|online_for", { duration: duration });

src/i18n/strings/en_EN.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1740,7 +1740,8 @@
17401740
"online": "Online",
17411741
"online_for": "Online for %(duration)s",
17421742
"unknown": "Unknown",
1743-
"unknown_for": "Unknown for %(duration)s"
1743+
"unknown_for": "Unknown for %(duration)s",
1744+
"unreachable": "User's server unreachable"
17441745
},
17451746
"quick_settings": {
17461747
"all_settings": "All settings",

0 commit comments

Comments
 (0)