Introduce room knocks bar (#11475)

* Introduce room knocks bar

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

* Apply PR 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-31 15:43:38 +02:00 committed by GitHub
parent f948a8f798
commit 45094bda7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 522 additions and 1 deletions

View file

@ -36,6 +36,7 @@ import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName";
import { E2EStatus } from "../../../utils/ShieldUtils";
import { IOOBData } from "../../../stores/ThreepidInviteStore";
import { RoomKnocksBar } from "./RoomKnocksBar";
import { SearchScope } from "./SearchBar";
import { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
import RoomContextMenu from "../context_menus/RoomContextMenu";
@ -820,6 +821,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
</div>
{!isVideoRoom && <RoomCallBanner roomId={this.props.room.roomId} />}
<RoomLiveShareWarning roomId={this.props.room.roomId} />
<RoomKnocksBar room={this.props.room} />
</header>
);
}

View file

@ -0,0 +1,159 @@
/*
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, JoinRule, MatrixError, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import React, { ReactElement, ReactNode, 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 dis from "../../../dispatcher/dispatcher";
import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import MemberAvatar from "../avatars/MemberAvatar";
import ErrorDialog from "../dialogs/ErrorDialog";
import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog";
import AccessibleButton from "../elements/AccessibleButton";
import Heading from "../typography/Heading";
export const RoomKnocksBar: VFC<{ room: Room }> = ({ room }) => {
const [disabled, setDisabled] = useState(false);
const knockMembers = useTypedEventEmitterState(
room,
RoomStateEvent.Members,
useCallback(() => room.getMembersWithMembership("knock"), [room]),
);
const knockMembersCount = knockMembers.length;
if (room.getJoinRule() !== JoinRule.Knock || knockMembersCount === 0) return null;
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;
if (!canInvite && !canKick) return null;
const onError = (error: MatrixError): void => {
Modal.createDialog(ErrorDialog, { title: error.name, description: error.message });
};
const handleApprove = (userId: string): void => {
setDisabled(true);
client
.invite(room.roomId, userId)
.catch(onError)
.finally(() => setDisabled(false));
};
const handleDeny = (userId: string): void => {
setDisabled(true);
client
.kick(room.roomId, userId)
.catch(onError)
.finally(() => setDisabled(false));
};
const handleOpenRoomSettings = (): void =>
dis.dispatch({ action: "open_room_settings", room_id: room.roomId, initial_tab_id: RoomSettingsTab.People });
let buttons: ReactElement = (
<AccessibleButton
className="mx_RoomKnocksBar_action"
kind="primary"
onClick={handleOpenRoomSettings}
title={_t("action|view")}
>
{_t("action|view")}
</AccessibleButton>
);
let names: string = knockMembers
.slice(0, 2)
.map((knockMember) => knockMember.name)
.join(", ");
let link: ReactNode = null;
switch (knockMembersCount) {
case 1: {
buttons = (
<>
<AccessibleButton
className="mx_RoomKnocksBar_action"
disabled={!canKick || disabled}
kind="icon_primary_outline"
onClick={() => handleDeny(knockMembers[0].userId)}
title={_t("action|deny")}
>
<XIcon width={18} height={18} />
</AccessibleButton>
<AccessibleButton
className="mx_RoomKnocksBar_action"
disabled={!canInvite || disabled}
kind="icon_primary"
onClick={() => handleApprove(knockMembers[0].userId)}
title={_t("action|approve")}
>
<CheckIcon width={18} height={18} />
</AccessibleButton>
</>
);
names = `${knockMembers[0].name} (${knockMembers[0].userId})`;
link = (
<AccessibleButton
className="mx_RoomKnocksBar_link"
element="a"
kind="link_inline"
onClick={handleOpenRoomSettings}
>
{_t("action|view_message")}
</AccessibleButton>
);
break;
}
case 2: {
names = _t("%(names)s and %(name)s", { names: knockMembers[0].name, name: knockMembers[1].name });
break;
}
case 3: {
names = _t("%(names)s and %(name)s", { names, name: knockMembers[2].name });
break;
}
default:
names = _t("%(names)s and %(count)s others", { names, count: knockMembersCount - 2 });
}
return (
<div className="mx_RoomKnocksBar">
{knockMembers.slice(0, 2).map((knockMember) => (
<MemberAvatar
className="mx_RoomKnocksBar_avatar"
key={knockMember.userId}
member={knockMember}
size="32px"
/>
))}
<div className="mx_RoomKnocksBar_content">
<Heading size="4">{_t("%(count)s people asking to join", { count: knockMembersCount })}</Heading>
<p className="mx_RoomKnocksBar_paragraph">
{names}
{link}
</p>
</div>
{buttons}
</div>
);
};

View file

@ -67,6 +67,8 @@
"search": "Search",
"quote": "Quote",
"unpin": "Unpin",
"view": "View",
"view_message": "View message",
"start_chat": "Start chat",
"invites_list": "Invites",
"reject": "Reject",
@ -87,7 +89,6 @@
"report_content": "Report Content",
"resend": "Resend",
"next": "Next",
"view": "View",
"ask_to_join": "Ask to join",
"forward": "Forward",
"copy_link": "Copy link",
@ -1861,6 +1862,15 @@
"Public room": "Public room",
"Private space": "Private space",
"Private room": "Private room",
"%(names)s and %(name)s": "%(names)s and %(name)s",
"%(names)s and %(count)s others": {
"other": "%(names)s and %(count)s others",
"one": "%(names)s and %(count)s other"
},
"%(count)s people asking to join": {
"other": "%(count)s people asking to join",
"one": "Asking to join"
},
"Start new chat": "Start new chat",
"Invite to space": "Invite to space",
"You do not have permissions to invite people to this space": "You do not have permissions to invite people to this space",