Add member list in space settings
authorAjay Bura <ajbura@gmail.com>
Sun, 30 Jan 2022 13:17:19 +0000 (18:47 +0530)
committerAjay Bura <ajbura@gmail.com>
Sun, 30 Jan 2022 13:17:19 +0000 (18:47 +0530)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
src/app/molecules/people-selector/PeopleSelector.scss
src/app/molecules/room-members/RoomMembers.jsx [new file with mode: 0644]
src/app/molecules/room-members/RoomMembers.scss [new file with mode: 0644]
src/app/organisms/room/PeopleDrawer.scss
src/app/organisms/room/RoomSettings.jsx
src/app/organisms/room/RoomSettings.scss
src/app/organisms/space-settings/SpaceSettings.jsx
src/app/organisms/space-settings/SpaceSettings.scss

index 1f1af5649c2f905a747a65671d68d32502239b7b..65907e6c61f32f4ad22143c8eb73eca2543ad3c1 100644 (file)
@@ -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 (file)
index 0000000..cd49f9b
--- /dev/null
@@ -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;
diff --git a/src/app/molecules/room-members/RoomMembers.scss b/src/app/molecules/room-members/RoomMembers.scss
new file mode 100644 (file)
index 0000000..d74c08a
--- /dev/null
@@ -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
index 281f66d66c3e1c4e6a7b2c1c8e5139fe0916afd8..1cd8fb07c4510ef054c46a5eb4f88f5a1f200011 100644 (file)
@@ -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);
       }
     }
   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);
index d5e5c165c08e6ddfca8eb25db98176ba80f87b91..beab5cbc5dbf01259242a9e3004e20bc4fe980e1 100644 (file)
@@ -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 }) {
           <div className="room-settings__cards-wrapper">
             {selectedTab.text === tabText.GENERAL && <GeneralSettings 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.SECURITY && <SecuritySettings roomId={roomId} />}
           </div>
index 3df776d449cf5007539edeaf451a2cc72c003612..ab7fca5c89b3f8e89d2a7413bc194bc0404cc152 100644 (file)
@@ -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
index 8042580f5bcc4462ab769de2ac0fe7a3d1a73a7d..7f0e0d24532e1fdd39c0930c0e3e78d09b53cb5f 100644 (file)
@@ -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() {
           />
           <div className="space-settings__cards-wrapper">
             {selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
+            {selectedTab.text === tabText.MEMBERS && <RoomMembers roomId={roomId} />}
             {selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
           </div>
         </div>
index d695dace9b1882dc6be47c8cb30808dd16fec8cb..501deedb22fba0d45f1ec6846af6452891097f05 100644 (file)
@@ -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