Implement new memberlist header view
parent
4f50174aa9
commit
6eb51b34bf
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_MemberListHeaderView {
|
||||
border-bottom: var(--cpd-border-width-1) solid var(--cpd-color-gray-400);
|
||||
max-height: 112px;
|
||||
|
||||
.mx_MemberListHeaderView_container {
|
||||
margin-top: var(--cpd-space-6x);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx_MemberListHeaderView_invite_small {
|
||||
margin-left: var(--cpd-space-3x);
|
||||
}
|
||||
|
||||
.mx_MemberListHeaderView_invite_large {
|
||||
width: 288px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.mx_MemberListHeaderView_label {
|
||||
padding: var(--cpd-space-6x) 0 var(--cpd-space-2x) var(--cpd-space-4x);
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
color: var(--cpd-color-text-secondary);
|
||||
font: var(--cpd-font-body-sm-semibold);
|
||||
}
|
||||
|
||||
.mx_MemberListHeaderView_search {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Search, Text, Button, Tooltip, InlineSpinner } from "@vector-im/compound-web";
|
||||
import React from "react";
|
||||
import InviteIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add-solid";
|
||||
import { UserAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import { MemberListViewState } from "../../../viewmodels/memberlist/MemberListViewModel";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
|
||||
interface TooltipProps {
|
||||
canInvite: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const OptionalTooltip: React.FC<TooltipProps> = ({ canInvite, children }) => {
|
||||
if (canInvite) return children;
|
||||
// If the user isn't allowed to invite others to this room, wrap with a relevant tooltip.
|
||||
return <Tooltip description={_t("member_list|invite_button_no_perms_tooltip")}>{children}</Tooltip>;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
vm: MemberListViewState;
|
||||
}
|
||||
|
||||
const InviteButton: React.FC<Props> = ({ vm }) => {
|
||||
const shouldShowInvite = vm.shouldShowInvite;
|
||||
const shouldShowSearch = vm.shouldShowSearch;
|
||||
const disabled = !vm.canInvite;
|
||||
|
||||
if (!shouldShowInvite) {
|
||||
// In this case, invite button should not be rendered.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (shouldShowSearch) {
|
||||
/// When rendered alongside a search box, the invite button is just an icon.
|
||||
return (
|
||||
<OptionalTooltip canInvite={vm.canInvite}>
|
||||
<Button
|
||||
className="mx_MemberListHeaderView_invite_small"
|
||||
kind="primary"
|
||||
onClick={vm.onInviteButtonClick}
|
||||
size="sm"
|
||||
iconOnly={true}
|
||||
Icon={InviteIcon}
|
||||
disabled={disabled}
|
||||
aria-label={_t("action|invite")}
|
||||
/>
|
||||
</OptionalTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// Without a search box, invite button is a full size button.
|
||||
return (
|
||||
<OptionalTooltip canInvite={vm.canInvite}>
|
||||
<Button
|
||||
kind="secondary"
|
||||
size="sm"
|
||||
Icon={UserAddIcon}
|
||||
className="mx_MemberListHeaderView_invite_large"
|
||||
disabled={!vm.canInvite}
|
||||
onClick={vm.onInviteButtonClick}
|
||||
>
|
||||
{_t("action|invite")}
|
||||
</Button>
|
||||
</OptionalTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be:
|
||||
* A loading text with spinner while the memberlist loads.
|
||||
* Member count of the room when there's nothing in the search field.
|
||||
* Number of matching members during search or 'No result' if search found nothing.
|
||||
*/
|
||||
function getHeaderLabelJSX(vm: MemberListViewState): React.ReactNode {
|
||||
if (vm.isLoading) {
|
||||
return (
|
||||
<Flex align="center" gap="8px">
|
||||
<InlineSpinner /> {_t("common|loading")}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const filteredMemberCount = vm.members.length;
|
||||
if (filteredMemberCount === 0) {
|
||||
return _t("member_list|no_matches");
|
||||
}
|
||||
return _t("member_list|count", { count: filteredMemberCount });
|
||||
}
|
||||
|
||||
export const MemberListHeaderView: React.FC<Props> = (props: Props) => {
|
||||
const vm = props.vm;
|
||||
|
||||
let contentJSX: React.ReactNode;
|
||||
|
||||
if (vm.shouldShowSearch) {
|
||||
// When we need to show the search box
|
||||
contentJSX = (
|
||||
<Flex justify="center" className="mx_MemberListHeaderView_container">
|
||||
<Search
|
||||
className="mx_MemberListHeaderView_search mx_no_textinput"
|
||||
name="searchMembers"
|
||||
placeholder={_t("member_list|filter_placeholder")}
|
||||
onChange={(e) => vm.search((e as React.ChangeEvent<HTMLInputElement>).target.value)}
|
||||
/>
|
||||
<InviteButton vm={vm} />
|
||||
</Flex>
|
||||
);
|
||||
} else if (!vm.shouldShowSearch && vm.shouldShowInvite) {
|
||||
// When we don't need to show the search box but still need an invite button
|
||||
contentJSX = (
|
||||
<Flex justify="center" className="mx_MemberListHeaderView_container">
|
||||
<InviteButton vm={vm} />
|
||||
</Flex>
|
||||
);
|
||||
} else {
|
||||
// No search box and no invite icon, so nothing to render!
|
||||
contentJSX = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex className="mx_MemberListHeaderView" as="header" align="center" justify="space-between" direction="column">
|
||||
{!vm.isLoading && contentJSX}
|
||||
<Text as="div" size="sm" weight="semibold" className="mx_MemberListHeaderView_label">
|
||||
{getHeaderLabelJSX(vm)}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue