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

Commit d569ba0

Browse files
authored
Allow managing room knocks (#11404)
* Allow managing room knocks Signed-off-by: Charly Nguyen <[email protected]> * Apply PR feedback Signed-off-by: Charly Nguyen <[email protected]> * Apply Sonar feedback Signed-off-by: Charly Nguyen <[email protected]> --------- Signed-off-by: Charly Nguyen <[email protected]>
1 parent 4f138ed commit d569ba0

File tree

13 files changed

+711
-7
lines changed

13 files changed

+711
-7
lines changed

res/css/_components.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@
341341
@import "./views/settings/tabs/_SettingsSection.pcss";
342342
@import "./views/settings/tabs/_SettingsTab.pcss";
343343
@import "./views/settings/tabs/room/_NotificationSettingsTab.pcss";
344+
@import "./views/settings/tabs/room/_PeopleRoomSettingsTab.pcss";
344345
@import "./views/settings/tabs/room/_RolesRoomSettingsTab.pcss";
345346
@import "./views/settings/tabs/room/_SecurityRoomSettingsTab.pcss";
346347
@import "./views/settings/tabs/user/_AppearanceUserSettingsTab.pcss";

res/css/views/dialogs/_RoomSettingsDialog.pcss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ limitations under the License.
5050
mask-image: url("$(res)/img/element-icons/room/settings/advanced.svg");
5151
}
5252

53+
.mx_RoomSettingsDialog_peopleIcon::before {
54+
mask-image: url("$(res)/img/element-icons/group-members.svg");
55+
}
56+
5357
.mx_RoomSettingsDialog .mx_Dialog_title {
5458
-ms-text-overflow: ellipsis;
5559
text-overflow: ellipsis;

res/css/views/elements/_AccessibleButton.pcss

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ limitations under the License.
2020
&.mx_AccessibleButton_disabled {
2121
cursor: not-allowed;
2222

23+
&.mx_AccessibleButton_kind_icon_primary,
24+
&.mx_AccessibleButton_kind_icon_primary_outline,
2325
&.mx_AccessibleButton_kind_primary,
2426
&.mx_AccessibleButton_kind_primary_outline,
2527
&.mx_AccessibleButton_kind_primary_sm,
@@ -80,29 +82,37 @@ limitations under the License.
8082
}
8183
}
8284

83-
&.mx_AccessibleButton_kind_icon {
85+
&.mx_AccessibleButton_kind_icon,
86+
&.mx_AccessibleButton_kind_icon_primary,
87+
&.mx_AccessibleButton_kind_icon_primary_outline {
8488
padding: 0;
8589
height: 32px;
8690
width: 32px;
8791
}
8892
}
8993

94+
&.mx_AccessibleButton_kind_icon_primary,
95+
&.mx_AccessibleButton_kind_icon_primary_outline,
9096
&.mx_AccessibleButton_kind_primary,
9197
&.mx_AccessibleButton_kind_primary_outline,
9298
&.mx_AccessibleButton_kind_secondary {
9399
font-weight: var(--cpd-font-weight-semibold);
94100
}
95101

102+
&.mx_AccessibleButton_kind_icon_primary,
103+
&.mx_AccessibleButton_kind_icon_primary_outline,
96104
&.mx_AccessibleButton_kind_primary,
97105
&.mx_AccessibleButton_kind_primary_outline {
98106
border: 1px solid $accent;
99107
}
100108

109+
&.mx_AccessibleButton_kind_icon_primary,
101110
&.mx_AccessibleButton_kind_primary {
102111
color: $button-primary-fg-color;
103112
background-color: $accent;
104113
}
105114

115+
&.mx_AccessibleButton_kind_icon_primary_outline,
106116
&.mx_AccessibleButton_kind_primary_outline {
107117
color: $accent;
108118
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
Copyright 2023 Nordeck IT + Consulting GmbH
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_PeopleRoomSettingsTab_knock {
18+
display: flex;
19+
margin-top: var(--cpd-space-2x);
20+
}
21+
22+
.mx_PeopleRoomSettingsTab_content {
23+
flex-grow: 1;
24+
margin: 0 var(--cpd-space-4x);
25+
}
26+
27+
.mx_PeopleRoomSettingsTab_name {
28+
font-weight: var(--cpd-font-weight-semibold);
29+
}
30+
31+
.mx_PeopleRoomSettingsTab_timestamp {
32+
color: $secondary-content;
33+
margin-left: var(--cpd-space-1x);
34+
}
35+
36+
.mx_PeopleRoomSettingsTab_userId {
37+
color: $secondary-content;
38+
display: block;
39+
font-size: var(--cpd-font-size-body-sm);
40+
}
41+
42+
.mx_PeopleRoomSettingsTab_seeMoreOrLess {
43+
margin: var(--cpd-space-3x) 0 0;
44+
}
45+
46+
.mx_PeopleRoomSettingsTab_action {
47+
flex-shrink: 0;
48+
49+
+ .mx_PeopleRoomSettingsTab_action {
50+
margin-left: var(--cpd-space-3x);
51+
}
52+
}
53+
54+
.mx_PeopleRoomSettingsTab_paragraph {
55+
margin: 0;
56+
}

res/img/feather-customised/check.svg

Lines changed: 1 addition & 1 deletion
Loading

res/img/feather-customised/x.svg

Lines changed: 1 addition & 1 deletion
Loading

src/components/views/dialogs/RoomSettingsDialog.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ limitations under the License.
1818
*/
1919

2020
import React from "react";
21-
import { RoomEvent, Room } from "matrix-js-sdk/src/matrix";
21+
import { RoomEvent, Room, RoomStateEvent, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
2222

2323
import TabbedView, { Tab } from "../../structures/TabbedView";
2424
import { _t, _td } from "../../../languageHandler";
@@ -39,9 +39,11 @@ import { ActionPayload } from "../../../dispatcher/payloads";
3939
import { NonEmptyArray } from "../../../@types/common";
4040
import { PollHistoryTab } from "../settings/tabs/room/PollHistoryTab";
4141
import ErrorBoundary from "../elements/ErrorBoundary";
42+
import { PeopleRoomSettingsTab } from "../settings/tabs/room/PeopleRoomSettingsTab";
4243

4344
export const enum RoomSettingsTab {
4445
General = "ROOM_GENERAL_TAB",
46+
People = "ROOM_PEOPLE_TAB",
4547
Voip = "ROOM_VOIP_TAB",
4648
Security = "ROOM_SECURITY_TAB",
4749
Roles = "ROOM_ROLES_TAB",
@@ -74,6 +76,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
7476
public componentDidMount(): void {
7577
this.dispatcherRef = dis.register(this.onAction);
7678
MatrixClientPeg.safeGet().on(RoomEvent.Name, this.onRoomName);
79+
MatrixClientPeg.safeGet().on(RoomStateEvent.Events, this.onStateEvent);
7780
this.onRoomName();
7881
}
7982

@@ -90,6 +93,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
9093
}
9194

9295
MatrixClientPeg.get()?.removeListener(RoomEvent.Name, this.onRoomName);
96+
MatrixClientPeg.get()?.removeListener(RoomStateEvent.Events, this.onStateEvent);
9397
}
9498

9599
/**
@@ -120,6 +124,10 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
120124
this.forceUpdate();
121125
};
122126

127+
private onStateEvent = (event: MatrixEvent): void => {
128+
if (event.getType() === EventType.RoomJoinRules) this.forceUpdate();
129+
};
130+
123131
private getTabs(): NonEmptyArray<Tab<RoomSettingsTab>> {
124132
const tabs: Tab<RoomSettingsTab>[] = [];
125133

@@ -132,6 +140,16 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
132140
"RoomSettingsGeneral",
133141
),
134142
);
143+
if (SettingsStore.getValue("feature_ask_to_join") && this.state.room.getJoinRule() === "knock") {
144+
tabs.push(
145+
new Tab(
146+
RoomSettingsTab.People,
147+
_td("People"),
148+
"mx_RoomSettingsDialog_peopleIcon",
149+
<PeopleRoomSettingsTab room={this.state.room} />,
150+
),
151+
);
152+
}
135153
if (SettingsStore.getValue("feature_group_calls")) {
136154
tabs.push(
137155
new Tab(

src/components/views/elements/AccessibleButton.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ type AccessibleButtonKind =
3838
| "link_sm"
3939
| "confirm_sm"
4040
| "cancel_sm"
41-
| "icon";
41+
| "icon"
42+
| "icon_primary"
43+
| "icon_primary_outline";
4244

4345
/**
4446
* This type construct allows us to specifically pass those props down to the element we’re creating that the element
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
Copyright 2023 Nordeck IT + Consulting GmbH
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 { EventTimeline, MatrixError, Room, RoomMember, RoomStateEvent } from "matrix-js-sdk/src/matrix";
18+
import React, { useCallback, useState, VFC } from "react";
19+
20+
import { Icon as CheckIcon } from "../../../../../../res/img/feather-customised/check.svg";
21+
import { Icon as XIcon } from "../../../../../../res/img/feather-customised/x.svg";
22+
import { formatRelativeTime } from "../../../../../DateUtils";
23+
import { useTypedEventEmitterState } from "../../../../../hooks/useEventEmitter";
24+
import { _t } from "../../../../../languageHandler";
25+
import Modal, { IHandle } from "../../../../../Modal";
26+
import MemberAvatar from "../../../avatars/MemberAvatar";
27+
import ErrorDialog from "../../../dialogs/ErrorDialog";
28+
import AccessibleButton from "../../../elements/AccessibleButton";
29+
import SettingsFieldset from "../../SettingsFieldset";
30+
import { SettingsSection } from "../../shared/SettingsSection";
31+
import SettingsTab from "../SettingsTab";
32+
33+
const Timestamp: VFC<{ roomMember: RoomMember }> = ({ roomMember }) => {
34+
const timestamp = roomMember.events.member?.event.origin_server_ts;
35+
if (!timestamp) return null;
36+
return <time className="mx_PeopleRoomSettingsTab_timestamp">{formatRelativeTime(new Date(timestamp))}</time>;
37+
};
38+
39+
const SeeMoreOrLess: VFC<{ roomMember: RoomMember }> = ({ roomMember }) => {
40+
const [seeMore, setSeeMore] = useState(false);
41+
const reason = roomMember.events.member?.getContent().reason;
42+
43+
if (!reason) return null;
44+
45+
const truncateAt = 120;
46+
const shouldTruncate = reason.length > truncateAt;
47+
48+
return (
49+
<>
50+
<p className="mx_PeopleRoomSettingsTab_seeMoreOrLess">
51+
{seeMore || !shouldTruncate ? reason : `${reason.substring(0, truncateAt)}…`}
52+
</p>
53+
{shouldTruncate && (
54+
<AccessibleButton kind="link" onClick={() => setSeeMore(!seeMore)}>
55+
{seeMore ? _t("See less") : _t("See more")}
56+
</AccessibleButton>
57+
)}
58+
</>
59+
);
60+
};
61+
62+
const Knock: VFC<{
63+
canInvite: boolean;
64+
canKick: boolean;
65+
onApprove: (userId: string) => Promise<void>;
66+
onDeny: (userId: string) => Promise<void>;
67+
roomMember: RoomMember;
68+
}> = ({ canKick, canInvite, onApprove, onDeny, roomMember }) => {
69+
const [disabled, setDisabled] = useState(false);
70+
71+
const handleApprove = (userId: string): void => {
72+
setDisabled(true);
73+
onApprove(userId).catch(onError);
74+
};
75+
76+
const handleDeny = (userId: string): void => {
77+
setDisabled(true);
78+
onDeny(userId).catch(onError);
79+
};
80+
81+
const onError = (): void => setDisabled(false);
82+
83+
return (
84+
<div className="mx_PeopleRoomSettingsTab_knock">
85+
<MemberAvatar height={42} member={roomMember} width={42} />
86+
<div className="mx_PeopleRoomSettingsTab_content">
87+
<span className="mx_PeopleRoomSettingsTab_name">{roomMember.name}</span>
88+
<Timestamp roomMember={roomMember} />
89+
<span className="mx_PeopleRoomSettingsTab_userId">{roomMember.userId}</span>
90+
<SeeMoreOrLess roomMember={roomMember} />
91+
</div>
92+
<AccessibleButton
93+
className="mx_PeopleRoomSettingsTab_action"
94+
disabled={!canKick || disabled}
95+
kind="icon_primary_outline"
96+
onClick={() => handleDeny(roomMember.userId)}
97+
title={_t("Deny")}
98+
>
99+
<XIcon width={18} height={18} />
100+
</AccessibleButton>
101+
<AccessibleButton
102+
className="mx_PeopleRoomSettingsTab_action"
103+
disabled={!canInvite || disabled}
104+
kind="icon_primary"
105+
onClick={() => handleApprove(roomMember.userId)}
106+
title={_t("Approve")}
107+
>
108+
<CheckIcon width={18} height={18} />
109+
</AccessibleButton>
110+
</div>
111+
);
112+
};
113+
114+
export const PeopleRoomSettingsTab: VFC<{ room: Room }> = ({ room }) => {
115+
const client = room.client;
116+
const userId = client.getUserId() || "";
117+
const canInvite = room.canInvite(userId);
118+
const member = room.getMember(userId);
119+
const state = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
120+
const canKick = member && state ? state.hasSufficientPowerLevelFor("kick", member.powerLevel) : false;
121+
const roomId = room.roomId;
122+
123+
const handleApprove = (userId: string): Promise<void> =>
124+
new Promise((_, reject) =>
125+
client.invite(roomId, userId).catch((error) => {
126+
onError(error);
127+
reject(error);
128+
}),
129+
);
130+
131+
const handleDeny = (userId: string): Promise<void> =>
132+
new Promise((_, reject) =>
133+
client.kick(roomId, userId).catch((error) => {
134+
onError(error);
135+
reject(error);
136+
}),
137+
);
138+
139+
const onError = (error: MatrixError): IHandle<typeof ErrorDialog> =>
140+
Modal.createDialog(ErrorDialog, {
141+
title: error.name,
142+
description: error.message,
143+
});
144+
145+
const knockMembers = useTypedEventEmitterState(
146+
room,
147+
RoomStateEvent.Members,
148+
useCallback(() => room.getMembersWithMembership("knock"), [room]),
149+
);
150+
151+
return (
152+
<SettingsTab>
153+
<SettingsSection heading={_t("People")}>
154+
<SettingsFieldset legend={_t("Asking to join")}>
155+
{knockMembers.length ? (
156+
knockMembers.map((knockMember) => (
157+
<Knock
158+
canInvite={canInvite}
159+
canKick={canKick}
160+
key={knockMember.userId}
161+
onApprove={handleApprove}
162+
onDeny={handleDeny}
163+
roomMember={knockMember}
164+
/>
165+
))
166+
) : (
167+
<p className="mx_PeopleRoomSettingsTab_paragraph">{_t("No requests")}</p>
168+
)}
169+
</SettingsFieldset>
170+
</SettingsSection>
171+
</SettingsTab>
172+
);
173+
};

0 commit comments

Comments
 (0)