diff --git a/src/app/molecules/people-selector/PeopleSelector.scss b/src/app/molecules/people-selector/PeopleSelector.scss
index 1f1af56..65907e6 100644
--- a/src/app/molecules/people-selector/PeopleSelector.scss
+++ b/src/app/molecules/people-selector/PeopleSelector.scss
@@ -1,14 +1,16 @@
@use '../../partials/text';
-@use '../../partials/dir';
.people-selector {
width: 100%;
- padding: var(--sp-extra-tight);
- @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
+ padding: var(--sp-extra-tight) var(--sp-normal);
display: flex;
align-items: center;
cursor: pointer;
+ &__container {
+ display: flex;
+ }
+
@media (hover: hover) {
&:hover {
background-color: var(--bg-surface-hover);
diff --git a/src/app/molecules/room-members/RoomMembers.jsx b/src/app/molecules/room-members/RoomMembers.jsx
new file mode 100644
index 0000000..cd49f9b
--- /dev/null
+++ b/src/app/molecules/room-members/RoomMembers.jsx
@@ -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 (
+
+
{
+ 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]);
+ }}
+ />
+
+
+
{`${searchMembers ? `Found — ${mList.length}` : members.length} members`}
+ {mList.map((member) => (
+
openProfileViewer(member.userId, roomId)}
+ avatarSrc={member.avatarSrc}
+ name={member.name}
+ color={colorMXID(member.userId)}
+ peopleRole={member.peopleRole}
+ />
+ ))}
+ {
+ (searchMembers?.data.length === 0 || members.length === 0)
+ && (
+
+
+ {searchMembers ? `No results found for "${searchMembers.term}"` : 'No members to display'}
+
+
+ )
+ }
+ {
+ mList.length !== 0
+ && members.length > itemCount
+ && searchMembers === null
+ &&
+ }
+
+
+ );
+}
+
+RoomMembers.propTypes = {
+ roomId: PropTypes.string.isRequired,
+};
+
+export default RoomMembers;
diff --git a/src/app/molecules/room-members/RoomMembers.scss b/src/app/molecules/room-members/RoomMembers.scss
new file mode 100644
index 0000000..d74c08a
--- /dev/null
+++ b/src/app/molecules/room-members/RoomMembers.scss
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/src/app/organisms/room/PeopleDrawer.scss b/src/app/organisms/room/PeopleDrawer.scss
index 281f66d..1cd8fb0 100644
--- a/src/app/organisms/room/PeopleDrawer.scss
+++ b/src/app/organisms/room/PeopleDrawer.scss
@@ -30,7 +30,7 @@
--search-input-height: 40px;
min-height: var(--search-input-height);
- margin: 0 var(--sp-normal);
+ margin: 0 var(--sp-extra-tight);
position: relative;
bottom: var(--sp-normal);
@@ -54,7 +54,7 @@
flex: 1;
}
& .input {
- padding: 0 calc(var(--sp-loose) + var(--sp-normal));
+ padding: 0 44px;
height: var(--search-input-height);
}
}
@@ -65,6 +65,12 @@
padding-top: var(--sp-extra-tight);
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 {
display: flex;
margin-bottom: var(--sp-extra-tight);
diff --git a/src/app/organisms/room/RoomSettings.jsx b/src/app/organisms/room/RoomSettings.jsx
index d5e5c16..beab5cb 100644
--- a/src/app/organisms/room/RoomSettings.jsx
+++ b/src/app/organisms/room/RoomSettings.jsx
@@ -24,7 +24,9 @@ import RoomAliases from '../../molecules/room-aliases/RoomAliases';
import RoomHistoryVisibility from '../../molecules/room-history-visibility/RoomHistoryVisibility';
import RoomEncryption from '../../molecules/room-encryption/RoomEncryption';
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 SearchIC from '../../../../public/res/ic/outlined/search.svg';
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
@@ -38,6 +40,7 @@ import { useForceUpdate } from '../../hooks/useForceUpdate';
const tabText = {
GENERAL: 'General',
SEARCH: 'Search',
+ MEMBERS: 'Members',
PERMISSIONS: 'Permissions',
SECURITY: 'Security',
};
@@ -50,6 +53,10 @@ const tabItems = [{
iconSrc: SearchIC,
text: tabText.SEARCH,
disabled: false,
+}, {
+ iconSrc: UserIC,
+ text: tabText.MEMBERS,
+ disabled: false,
}, {
iconSrc: ShieldUserIC,
text: tabText.PERMISSIONS,
@@ -182,6 +189,7 @@ function RoomSettings({ roomId }) {
{selectedTab.text === tabText.GENERAL && }
{selectedTab.text === tabText.SEARCH && }
+ {selectedTab.text === tabText.MEMBERS && }
{selectedTab.text === tabText.PERMISSIONS && }
{selectedTab.text === tabText.SECURITY && }
diff --git a/src/app/organisms/room/RoomSettings.scss b/src/app/organisms/room/RoomSettings.scss
index 3df776d..ab7fca5 100644
--- a/src/app/organisms/room/RoomSettings.scss
+++ b/src/app/organisms/room/RoomSettings.scss
@@ -75,6 +75,7 @@
.room-settings .room-permissions__card,
.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;
}
\ No newline at end of file
diff --git a/src/app/organisms/space-settings/SpaceSettings.jsx b/src/app/organisms/space-settings/SpaceSettings.jsx
index 8042580..7f0e0d2 100644
--- a/src/app/organisms/space-settings/SpaceSettings.jsx
+++ b/src/app/organisms/space-settings/SpaceSettings.jsx
@@ -18,7 +18,9 @@ import RoomProfile from '../../molecules/room-profile/RoomProfile';
import RoomVisibility from '../../molecules/room-visibility/RoomVisibility';
import RoomAliases from '../../molecules/room-aliases/RoomAliases';
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 SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
@@ -30,6 +32,7 @@ import { useForceUpdate } from '../../hooks/useForceUpdate';
const tabText = {
GENERAL: 'General',
+ MEMBERS: 'Members',
PERMISSIONS: 'Permissions',
};
@@ -37,6 +40,10 @@ const tabItems = [{
iconSrc: SettingsIC,
text: tabText.GENERAL,
disabled: false,
+}, {
+ iconSrc: UserIC,
+ text: tabText.MEMBERS,
+ disabled: false,
}, {
iconSrc: ShieldUserIC,
text: tabText.PERMISSIONS,
@@ -144,6 +151,7 @@ function SpaceSettings() {
/>
{selectedTab.text === tabText.GENERAL && }
+ {selectedTab.text === tabText.MEMBERS && }
{selectedTab.text === tabText.PERMISSIONS && }
diff --git a/src/app/organisms/space-settings/SpaceSettings.scss b/src/app/organisms/space-settings/SpaceSettings.scss
index d695dac..501deed 100644
--- a/src/app/organisms/space-settings/SpaceSettings.scss
+++ b/src/app/organisms/space-settings/SpaceSettings.scss
@@ -35,6 +35,7 @@
}
}
-.space-settings .room-permissions__card {
+.space-settings .room-permissions__card,
+.space-settings .room-members {
@extend .space-settings__card;
}
\ No newline at end of file