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

Introduce room knocks bar #11475

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@
@import "./views/rooms/_RoomCallBanner.pcss";
@import "./views/rooms/_RoomHeader.pcss";
@import "./views/rooms/_RoomInfoLine.pcss";
@import "./views/rooms/_RoomKnocksBar.pcss";
@import "./views/rooms/_RoomList.pcss";
@import "./views/rooms/_RoomListHeader.pcss";
@import "./views/rooms/_RoomPreviewBar.pcss";
Expand Down
50 changes: 50 additions & 0 deletions res/css/views/rooms/_RoomKnocksBar.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
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.
*/

.mx_RoomKnocksBar {
background-color: var(--cpd-color-bg-subtle-secondary);
display: flex;
padding: var(--cpd-space-2x) var(--cpd-space-4x);
}

.mx_RoomKnocksBar_content {
flex-grow: 1;
margin: 0 var(--cpd-space-3x);
}

.mx_RoomKnocksBar_paragraph {
color: $secondary-content;
font-size: var(--cpd-font-size-body-sm);
margin: 0;
}

.mx_RoomKnocksBar_link {
margin-left: var(--cpd-space-3x);
}

.mx_RoomKnocksBar_action,
.mx_RoomKnocksBar_avatar {
align-self: center;
flex-shrink: 0;
}

.mx_RoomKnocksBar_action + .mx_RoomKnocksBar_action {
margin-left: var(--cpd-space-3x);
}

.mx_RoomKnocksBar_avatar + .mx_RoomKnocksBar_avatar {
margin-left: calc(var(--cpd-space-4x) * -1);
}
2 changes: 2 additions & 0 deletions src/components/views/rooms/LegacyRoomHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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>
);
}
Expand Down
159 changes: 159 additions & 0 deletions src/components/views/rooms/RoomKnocksBar.tsx
Original file line number Diff line number Diff line change
@@ -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(", ");
andybalaam marked this conversation as resolved.
Show resolved Hide resolved
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>
);
};
12 changes: 11 additions & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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"
andybalaam marked this conversation as resolved.
Show resolved Hide resolved
},
"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",
Expand Down
Loading