Add member list in space settings
Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
parent
e85a869733
commit
abe03811f1
8 changed files with 251 additions and 7 deletions
|
@ -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);
|
||||||
|
|
191
src/app/molecules/room-members/RoomMembers.jsx
Normal file
191
src/app/molecules/room-members/RoomMembers.jsx
Normal 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;
|
27
src/app/molecules/room-members/RoomMembers.scss
Normal file
27
src/app/molecules/room-members/RoomMembers.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
Loading…
Reference in a new issue