diff --git a/src/app/molecules/people-selector/PeopleSelector.scss b/src/app/molecules/people-selector/PeopleSelector.scss index 1f1af564..65907e6c 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 00000000..cd49f9bb --- /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 00000000..d74c08a5 --- /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 281f66d6..1cd8fb07 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 d5e5c165..beab5cbc 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 3df776d4..ab7fca5c 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 8042580f..7f0e0d24 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 d695dace..501deedb 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