Add member list in space settings

Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
Ajay Bura 2022-01-30 18:47:19 +05:30
parent e85a869733
commit abe03811f1
8 changed files with 251 additions and 7 deletions

View file

@ -1,14 +1,16 @@
@use '../../partials/text'; @use '../../partials/text';
@use '../../partials/dir';
.people-selector { .people-selector {
width: 100%; width: 100%;
padding: var(--sp-extra-tight); padding: var(--sp-extra-tight) var(--sp-normal);
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
display: flex; display: flex;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
&__container {
display: flex;
}
@media (hover: hover) { @media (hover: hover) {
&:hover { &:hover {
background-color: var(--bg-surface-hover); background-color: var(--bg-surface-hover);

View file

@ -0,0 +1,191 @@
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import './RoomMembers.scss';
import initMatrix from '../../../client/initMatrix';
import colorMXID from '../../../util/colorMXID';
import { openProfileViewer } from '../../../client/action/navigation';
import { getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil';
import AsyncSearch from '../../../util/AsyncSearch';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import SegmentedControls from '../../atoms/segmented-controls/SegmentedControls';
import PeopleSelector from '../people-selector/PeopleSelector';
const PER_PAGE_MEMBER = 50;
function AtoZ(m1, m2) {
const aName = m1.name;
const bName = m2.name;
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
}
if (aName.toLowerCase() > bName.toLowerCase()) {
return 1;
}
return 0;
}
function sortByPowerLevel(m1, m2) {
const pl1 = m1.powerLevel;
const pl2 = m2.powerLevel;
if (pl1 > pl2) return -1;
if (pl1 < pl2) return 1;
return 0;
}
function normalizeMembers(members) {
const mx = initMatrix.matrixClient;
return members.map((member) => ({
userId: member.userId,
name: getUsernameOfRoomMember(member),
username: member.userId.slice(1, member.userId.indexOf(':')),
avatarSrc: member.getAvatarUrl(mx.baseUrl, 24, 24, 'crop'),
peopleRole: getPowerLabel(member.powerLevel),
powerLevel: members.powerLevel,
}));
}
function useMemberOfMembership(roomId, membership) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
const [members, setMembers] = useState([]);
useEffect(() => {
let isMounted = true;
const updateMemberList = (event) => {
if (event && event?.getRoomId() !== roomId) return;
const memberOfMembership = normalizeMembers(
room.getMembersWithMembership(membership)
.sort(AtoZ).sort(sortByPowerLevel),
);
setMembers(memberOfMembership);
};
updateMemberList();
room.loadMembersIfNeeded().then(() => {
if (!isMounted) return;
updateMemberList();
});
mx.on('RoomMember.membership', updateMemberList);
mx.on('RoomMember.powerLevel', updateMemberList);
return () => {
isMounted = false;
mx.removeListener('RoomMember.membership', updateMemberList);
mx.removeListener('RoomMember.powerLevel', updateMemberList);
};
}, [membership]);
return [members];
}
const asyncSearch = new AsyncSearch();
function useSearchMembers(members) {
const [searchMembers, setSearchMembers] = useState(null);
const reSearch = useCallback(() => {
if (searchMembers) {
asyncSearch.search(searchMembers.term);
}
}, [searchMembers]);
useEffect(() => {
asyncSearch.setup(members, {
keys: ['name', 'username', 'userId'],
limit: PER_PAGE_MEMBER,
});
reSearch();
}, [members]);
useEffect(() => {
const handleSearchData = (data, term) => setSearchMembers({ data, term });
asyncSearch.on(asyncSearch.RESULT_SENT, handleSearchData);
return () => {
asyncSearch.removeListener(asyncSearch.RESULT_SENT, handleSearchData);
};
}, []);
const handleSearch = (e) => {
const term = e.target.value;
if (term === '' || term === undefined) {
setSearchMembers(null);
} else asyncSearch.search(term);
};
return [searchMembers, handleSearch];
}
function RoomMembers({ roomId }) {
const [itemCount, setItemCount] = useState(PER_PAGE_MEMBER);
const [membership, setMembership] = useState('join');
const [members] = useMemberOfMembership(roomId, membership);
const [searchMembers, handleSearch] = useSearchMembers(members);
useEffect(() => {
setItemCount(PER_PAGE_MEMBER);
}, [searchMembers]);
const loadMorePeople = () => {
setItemCount(itemCount + PER_PAGE_MEMBER);
};
const mList = searchMembers ? searchMembers.data : members.slice(0, itemCount);
return (
<div className="room-members">
<SegmentedControls
selected={
(() => {
const getSegmentIndex = { join: 0, invite: 1, ban: 2 };
return getSegmentIndex[membership];
})()
}
segments={[{ text: 'Joined' }, { text: 'Invited' }, { text: 'Banned' }]}
onSelect={(index) => {
const memberships = ['join', 'invite', 'ban'];
setMembership(memberships[index]);
}}
/>
<Input onChange={handleSearch} label="Search member" placeholder="name" />
<div className="room-members__list">
<MenuHeader>{`${searchMembers ? `Found — ${mList.length}` : members.length} members`}</MenuHeader>
{mList.map((member) => (
<PeopleSelector
key={member.userId}
onClick={() => openProfileViewer(member.userId, roomId)}
avatarSrc={member.avatarSrc}
name={member.name}
color={colorMXID(member.userId)}
peopleRole={member.peopleRole}
/>
))}
{
(searchMembers?.data.length === 0 || members.length === 0)
&& (
<div className="room-members__status">
<Text variant="b2">
{searchMembers ? `No results found for "${searchMembers.term}"` : 'No members to display'}
</Text>
</div>
)
}
{
mList.length !== 0
&& members.length > itemCount
&& searchMembers === null
&& <Button onClick={loadMorePeople}>View more</Button>
}
</div>
</div>
);
}
RoomMembers.propTypes = {
roomId: PropTypes.string.isRequired,
};
export default RoomMembers;

View file

@ -0,0 +1,27 @@
.room-members {
& .input-container {
margin: var(--sp-normal);
}
& .segmented-controls {
margin: var(--sp-normal);
display: flex;
& > * {
flex: 1;
}
}
&__list {
& .people-selector__container:last-child {
margin-bottom: var(--sp-extra-tight);
}
& > .btn-surface {
width: calc(100% - 32px);
margin: var(--sp-normal);
}
}
&__status {
margin: var(--sp-normal);
}
}

View file

@ -30,7 +30,7 @@
--search-input-height: 40px; --search-input-height: 40px;
min-height: var(--search-input-height); min-height: var(--search-input-height);
margin: 0 var(--sp-normal); margin: 0 var(--sp-extra-tight);
position: relative; position: relative;
bottom: var(--sp-normal); bottom: var(--sp-normal);
@ -54,7 +54,7 @@
flex: 1; flex: 1;
} }
& .input { & .input {
padding: 0 calc(var(--sp-loose) + var(--sp-normal)); padding: 0 44px;
height: var(--search-input-height); height: var(--search-input-height);
} }
} }
@ -65,6 +65,12 @@
padding-top: var(--sp-extra-tight); padding-top: var(--sp-extra-tight);
padding-bottom: calc(2 * var(--sp-normal)); padding-bottom: calc(2 * var(--sp-normal));
& .people-selector {
padding: var(--sp-extra-tight);
border-radius: var(--bo-radius);
@include dir.side(margin, var(--sp-extra-tight), 0);
}
& .segmented-controls { & .segmented-controls {
display: flex; display: flex;
margin-bottom: var(--sp-extra-tight); margin-bottom: var(--sp-extra-tight);

View file

@ -24,7 +24,9 @@ import RoomAliases from '../../molecules/room-aliases/RoomAliases';
import RoomHistoryVisibility from '../../molecules/room-history-visibility/RoomHistoryVisibility'; import RoomHistoryVisibility from '../../molecules/room-history-visibility/RoomHistoryVisibility';
import RoomEncryption from '../../molecules/room-encryption/RoomEncryption'; import RoomEncryption from '../../molecules/room-encryption/RoomEncryption';
import RoomPermissions from '../../molecules/room-permissions/RoomPermissions'; import RoomPermissions from '../../molecules/room-permissions/RoomPermissions';
import RoomMembers from '../../molecules/room-members/RoomMembers';
import UserIC from '../../../../public/res/ic/outlined/user.svg';
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg'; import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
import SearchIC from '../../../../public/res/ic/outlined/search.svg'; import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg'; import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
@ -38,6 +40,7 @@ import { useForceUpdate } from '../../hooks/useForceUpdate';
const tabText = { const tabText = {
GENERAL: 'General', GENERAL: 'General',
SEARCH: 'Search', SEARCH: 'Search',
MEMBERS: 'Members',
PERMISSIONS: 'Permissions', PERMISSIONS: 'Permissions',
SECURITY: 'Security', SECURITY: 'Security',
}; };
@ -50,6 +53,10 @@ const tabItems = [{
iconSrc: SearchIC, iconSrc: SearchIC,
text: tabText.SEARCH, text: tabText.SEARCH,
disabled: false, disabled: false,
}, {
iconSrc: UserIC,
text: tabText.MEMBERS,
disabled: false,
}, { }, {
iconSrc: ShieldUserIC, iconSrc: ShieldUserIC,
text: tabText.PERMISSIONS, text: tabText.PERMISSIONS,
@ -182,6 +189,7 @@ function RoomSettings({ roomId }) {
<div className="room-settings__cards-wrapper"> <div className="room-settings__cards-wrapper">
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />} {selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
{selectedTab.text === tabText.SEARCH && <RoomSearch roomId={roomId} />} {selectedTab.text === tabText.SEARCH && <RoomSearch roomId={roomId} />}
{selectedTab.text === tabText.MEMBERS && <RoomMembers roomId={roomId} />}
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />} {selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
{selectedTab.text === tabText.SECURITY && <SecuritySettings roomId={roomId} />} {selectedTab.text === tabText.SECURITY && <SecuritySettings roomId={roomId} />}
</div> </div>

View file

@ -75,6 +75,7 @@
.room-settings .room-permissions__card, .room-settings .room-permissions__card,
.room-settings .room-search__form, .room-settings .room-search__form,
.room-settings .room-search__result-item { .room-settings .room-search__result-item ,
.room-settings .room-members {
@extend .room-settings__card; @extend .room-settings__card;
} }

View file

@ -18,7 +18,9 @@ import RoomProfile from '../../molecules/room-profile/RoomProfile';
import RoomVisibility from '../../molecules/room-visibility/RoomVisibility'; import RoomVisibility from '../../molecules/room-visibility/RoomVisibility';
import RoomAliases from '../../molecules/room-aliases/RoomAliases'; import RoomAliases from '../../molecules/room-aliases/RoomAliases';
import RoomPermissions from '../../molecules/room-permissions/RoomPermissions'; import RoomPermissions from '../../molecules/room-permissions/RoomPermissions';
import RoomMembers from '../../molecules/room-members/RoomMembers';
import UserIC from '../../../../public/res/ic/outlined/user.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg'; import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg'; import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
@ -30,6 +32,7 @@ import { useForceUpdate } from '../../hooks/useForceUpdate';
const tabText = { const tabText = {
GENERAL: 'General', GENERAL: 'General',
MEMBERS: 'Members',
PERMISSIONS: 'Permissions', PERMISSIONS: 'Permissions',
}; };
@ -37,6 +40,10 @@ const tabItems = [{
iconSrc: SettingsIC, iconSrc: SettingsIC,
text: tabText.GENERAL, text: tabText.GENERAL,
disabled: false, disabled: false,
}, {
iconSrc: UserIC,
text: tabText.MEMBERS,
disabled: false,
}, { }, {
iconSrc: ShieldUserIC, iconSrc: ShieldUserIC,
text: tabText.PERMISSIONS, text: tabText.PERMISSIONS,
@ -144,6 +151,7 @@ function SpaceSettings() {
/> />
<div className="space-settings__cards-wrapper"> <div className="space-settings__cards-wrapper">
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />} {selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
{selectedTab.text === tabText.MEMBERS && <RoomMembers roomId={roomId} />}
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />} {selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
</div> </div>
</div> </div>

View file

@ -35,6 +35,7 @@
} }
} }
.space-settings .room-permissions__card { .space-settings .room-permissions__card,
.space-settings .room-members {
@extend .space-settings__card; @extend .space-settings__card;
} }