Allow managing room knocks (#11404)

* Allow managing room knocks

Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>

* Apply PR feedback

Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>

* Apply Sonar feedback

Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>

---------

Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>
This commit is contained in:
Charly Nguyen 2023-08-16 10:16:19 +02:00 committed by GitHub
parent 4f138ed041
commit d569ba0cfe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 711 additions and 7 deletions

View file

@ -18,7 +18,7 @@ limitations under the License.
*/
import React from "react";
import { RoomEvent, Room } from "matrix-js-sdk/src/matrix";
import { RoomEvent, Room, RoomStateEvent, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
import TabbedView, { Tab } from "../../structures/TabbedView";
import { _t, _td } from "../../../languageHandler";
@ -39,9 +39,11 @@ import { ActionPayload } from "../../../dispatcher/payloads";
import { NonEmptyArray } from "../../../@types/common";
import { PollHistoryTab } from "../settings/tabs/room/PollHistoryTab";
import ErrorBoundary from "../elements/ErrorBoundary";
import { PeopleRoomSettingsTab } from "../settings/tabs/room/PeopleRoomSettingsTab";
export const enum RoomSettingsTab {
General = "ROOM_GENERAL_TAB",
People = "ROOM_PEOPLE_TAB",
Voip = "ROOM_VOIP_TAB",
Security = "ROOM_SECURITY_TAB",
Roles = "ROOM_ROLES_TAB",
@ -74,6 +76,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
public componentDidMount(): void {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.safeGet().on(RoomEvent.Name, this.onRoomName);
MatrixClientPeg.safeGet().on(RoomStateEvent.Events, this.onStateEvent);
this.onRoomName();
}
@ -90,6 +93,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
}
MatrixClientPeg.get()?.removeListener(RoomEvent.Name, this.onRoomName);
MatrixClientPeg.get()?.removeListener(RoomStateEvent.Events, this.onStateEvent);
}
/**
@ -120,6 +124,10 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
this.forceUpdate();
};
private onStateEvent = (event: MatrixEvent): void => {
if (event.getType() === EventType.RoomJoinRules) this.forceUpdate();
};
private getTabs(): NonEmptyArray<Tab<RoomSettingsTab>> {
const tabs: Tab<RoomSettingsTab>[] = [];
@ -132,6 +140,16 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
"RoomSettingsGeneral",
),
);
if (SettingsStore.getValue("feature_ask_to_join") && this.state.room.getJoinRule() === "knock") {
tabs.push(
new Tab(
RoomSettingsTab.People,
_td("People"),
"mx_RoomSettingsDialog_peopleIcon",
<PeopleRoomSettingsTab room={this.state.room} />,
),
);
}
if (SettingsStore.getValue("feature_group_calls")) {
tabs.push(
new Tab(

View file

@ -38,7 +38,9 @@ type AccessibleButtonKind =
| "link_sm"
| "confirm_sm"
| "cancel_sm"
| "icon";
| "icon"
| "icon_primary"
| "icon_primary_outline";
/**
* This type construct allows us to specifically pass those props down to the element were creating that the element

View file

@ -0,0 +1,173 @@
/*
Copyright 2023 Nordeck IT + Consulting GmbH
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 { EventTimeline, MatrixError, Room, RoomMember, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import React, { useCallback, useState, VFC } from "react";
import { Icon as CheckIcon } from "../../../../../../res/img/feather-customised/check.svg";
import { Icon as XIcon } from "../../../../../../res/img/feather-customised/x.svg";
import { formatRelativeTime } from "../../../../../DateUtils";
import { useTypedEventEmitterState } from "../../../../../hooks/useEventEmitter";
import { _t } from "../../../../../languageHandler";
import Modal, { IHandle } from "../../../../../Modal";
import MemberAvatar from "../../../avatars/MemberAvatar";
import ErrorDialog from "../../../dialogs/ErrorDialog";
import AccessibleButton from "../../../elements/AccessibleButton";
import SettingsFieldset from "../../SettingsFieldset";
import { SettingsSection } from "../../shared/SettingsSection";
import SettingsTab from "../SettingsTab";
const Timestamp: VFC<{ roomMember: RoomMember }> = ({ roomMember }) => {
const timestamp = roomMember.events.member?.event.origin_server_ts;
if (!timestamp) return null;
return <time className="mx_PeopleRoomSettingsTab_timestamp">{formatRelativeTime(new Date(timestamp))}</time>;
};
const SeeMoreOrLess: VFC<{ roomMember: RoomMember }> = ({ roomMember }) => {
const [seeMore, setSeeMore] = useState(false);
const reason = roomMember.events.member?.getContent().reason;
if (!reason) return null;
const truncateAt = 120;
const shouldTruncate = reason.length > truncateAt;
return (
<>
<p className="mx_PeopleRoomSettingsTab_seeMoreOrLess">
{seeMore || !shouldTruncate ? reason : `${reason.substring(0, truncateAt)}`}
</p>
{shouldTruncate && (
<AccessibleButton kind="link" onClick={() => setSeeMore(!seeMore)}>
{seeMore ? _t("See less") : _t("See more")}
</AccessibleButton>
)}
</>
);
};
const Knock: VFC<{
canInvite: boolean;
canKick: boolean;
onApprove: (userId: string) => Promise<void>;
onDeny: (userId: string) => Promise<void>;
roomMember: RoomMember;
}> = ({ canKick, canInvite, onApprove, onDeny, roomMember }) => {
const [disabled, setDisabled] = useState(false);
const handleApprove = (userId: string): void => {
setDisabled(true);
onApprove(userId).catch(onError);
};
const handleDeny = (userId: string): void => {
setDisabled(true);
onDeny(userId).catch(onError);
};
const onError = (): void => setDisabled(false);
return (
<div className="mx_PeopleRoomSettingsTab_knock">
<MemberAvatar height={42} member={roomMember} width={42} />
<div className="mx_PeopleRoomSettingsTab_content">
<span className="mx_PeopleRoomSettingsTab_name">{roomMember.name}</span>
<Timestamp roomMember={roomMember} />
<span className="mx_PeopleRoomSettingsTab_userId">{roomMember.userId}</span>
<SeeMoreOrLess roomMember={roomMember} />
</div>
<AccessibleButton
className="mx_PeopleRoomSettingsTab_action"
disabled={!canKick || disabled}
kind="icon_primary_outline"
onClick={() => handleDeny(roomMember.userId)}
title={_t("Deny")}
>
<XIcon width={18} height={18} />
</AccessibleButton>
<AccessibleButton
className="mx_PeopleRoomSettingsTab_action"
disabled={!canInvite || disabled}
kind="icon_primary"
onClick={() => handleApprove(roomMember.userId)}
title={_t("Approve")}
>
<CheckIcon width={18} height={18} />
</AccessibleButton>
</div>
);
};
export const PeopleRoomSettingsTab: VFC<{ room: Room }> = ({ room }) => {
const client = room.client;
const userId = client.getUserId() || "";
const canInvite = room.canInvite(userId);
const member = room.getMember(userId);
const state = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
const canKick = member && state ? state.hasSufficientPowerLevelFor("kick", member.powerLevel) : false;
const roomId = room.roomId;
const handleApprove = (userId: string): Promise<void> =>
new Promise((_, reject) =>
client.invite(roomId, userId).catch((error) => {
onError(error);
reject(error);
}),
);
const handleDeny = (userId: string): Promise<void> =>
new Promise((_, reject) =>
client.kick(roomId, userId).catch((error) => {
onError(error);
reject(error);
}),
);
const onError = (error: MatrixError): IHandle<typeof ErrorDialog> =>
Modal.createDialog(ErrorDialog, {
title: error.name,
description: error.message,
});
const knockMembers = useTypedEventEmitterState(
room,
RoomStateEvent.Members,
useCallback(() => room.getMembersWithMembership("knock"), [room]),
);
return (
<SettingsTab>
<SettingsSection heading={_t("People")}>
<SettingsFieldset legend={_t("Asking to join")}>
{knockMembers.length ? (
knockMembers.map((knockMember) => (
<Knock
canInvite={canInvite}
canKick={canKick}
key={knockMember.userId}
onApprove={handleApprove}
onDeny={handleDeny}
roomMember={knockMember}
/>
))
) : (
<p className="mx_PeopleRoomSettingsTab_paragraph">{_t("No requests")}</p>
)}
</SettingsFieldset>
</SettingsSection>
</SettingsTab>
);
};

View file

@ -1698,6 +1698,12 @@
"Set a new custom sound": "Set a new custom sound",
"Upload custom sound": "Upload custom sound",
"Browse": "Browse",
"See less": "See less",
"See more": "See more",
"Deny": "Deny",
"Approve": "Approve",
"Asking to join": "Asking to join",
"No requests": "No requests",
"Failed to unban": "Failed to unban",
"Banned by %(displayName)s": "Banned by %(displayName)s",
"Reason": "Reason",
@ -3123,7 +3129,6 @@
"Verification Request": "Verification Request",
"Approve widget permissions": "Approve widget permissions",
"This widget would like to:": "This widget would like to:",
"Approve": "Approve",
"Decline All": "Decline All",
"Remember my selection for this widget": "Remember my selection for this widget",
"Allow this widget to verify your identity": "Allow this widget to verify your identity",