diff --git a/src/app/components/UserRoomProfileRenderer.tsx b/src/app/components/UserRoomProfileRenderer.tsx index 81d4bf6e..c12a4bfc 100644 --- a/src/app/components/UserRoomProfileRenderer.tsx +++ b/src/app/components/UserRoomProfileRenderer.tsx @@ -23,7 +23,7 @@ function UserRoomProfileContextMenu({ state }: { state: UserRoomProfileState }) return ( (); + + const open: MouseEventHandler = (evt) => { + setCords(evt.currentTarget.getBoundingClientRect()); + }; + + const close = () => setCords(undefined); + + return ( + isKeyHotkey('arrowdown', evt), + isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), + }} + > + +
+ { + copyToClipboard(server); + close(); + }} + > + Copy Server + + { + navigate(getExploreServerPath(server)); + closeProfile(); + }} + > + Explore Community + +
+ +
+ { + window.open(`https://${server}`, '_blank'); + close(); + }} + > + Open in Browser + +
+
+ + } + > + + ) : ( + + ) + } + onClick={open} + aria-pressed={!!cords} + > + + {server} + + +
+ ); +} + +export function MutualRoomsChip({ userId }: { userId: string }) { + const mx = useMatrixClient(); + const mutualRoomSupported = useMutualRoomsSupport(); + const mutualRoomsState = useMutualRooms(userId); + const { navigateRoom, navigateSpace } = useRoomNavigate(); + const closeUserRoomProfile = useCloseUserRoomProfile(); + const directs = useDirectRooms(); + const useAuthentication = useMediaAuthentication(); + + const allJoinedRooms = useAllJoinedRoomsSet(); + const getRoom = useGetRoom(allJoinedRooms); + + const [cords, setCords] = useState(); + + const open: MouseEventHandler = (evt) => { + setCords(evt.currentTarget.getBoundingClientRect()); + }; + + const close = () => setCords(undefined); + + if ( + userId === mx.getSafeUserId() || + !mutualRoomSupported || + mutualRoomsState.status === AsyncStatus.Error + ) { + return null; + } + + return ( + isKeyHotkey('arrowdown', evt), + isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), + }} + > + + + + + {mutualRoomsState.data + .sort(factoryRoomIdByAtoZ(mx)) + .map((roomId) => getRoom(roomId)) + .map((room) => { + if (!room) return null; + const { roomId } = room; + const dm = directs.includes(roomId); + + return ( + { + if (room.isSpaceRoom()) { + navigateSpace(roomId); + } else { + navigateRoom(roomId); + } + closeUserRoomProfile(); + }} + before={ + + {dm || room.isSpaceRoom() ? ( + ( + + {nameInitials(room.name)} + + )} + /> + ) : ( + + )} + + } + > + + {room.name} + + + ); + })} + + + + + + ) : null + } + > + } + disabled={mutualRoomsState.status !== AsyncStatus.Success} + onClick={open} + aria-pressed={!!cords} + > + + {mutualRoomsState.status === AsyncStatus.Success && + `${mutualRoomsState.data.length} Mutual Rooms`} + {mutualRoomsState.status === AsyncStatus.Loading && 'Mutual Rooms'} + + + + ); +} diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx index 3376ff37..cf4c815f 100644 --- a/src/app/components/user-profile/UserHero.tsx +++ b/src/app/components/user-profile/UserHero.tsx @@ -6,26 +6,42 @@ import { UserAvatar } from '../user-avatar'; import colorMXID from '../../../util/colorMXID'; import { getMxIdLocalPart } from '../../utils/matrix'; import { BreakWord, LineClamp3 } from '../../styles/Text.css'; +import { UserPresence } from '../../hooks/useUserPresence'; +import { AvatarPresence, PresenceBadge } from '../presence'; type UserHeroProps = { userId: string; avatarUrl?: string; + presence?: UserPresence; }; -export function UserHero({ userId, avatarUrl }: UserHeroProps) { +export function UserHero({ userId, avatarUrl, presence }: UserHeroProps) { return ( -
+
{avatarUrl && {userId}}
- - } - /> - + + } + > + + } + /> + +
); @@ -42,7 +58,7 @@ export function UserHeroName({ displayName, userId }: UserHeroNameProps) { diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index f0be38fc..0cc3d5e8 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -1,5 +1,4 @@ import { - Avatar, Box, Button, Chip, @@ -11,141 +10,32 @@ import { MenuItem, PopOut, RectCords, - Scroll, - Spinner, Text, - toRem, } from 'folds'; import React, { MouseEventHandler, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; import FocusTrap from 'focus-trap-react'; import { isKeyHotkey } from 'is-hotkey'; import { UserHero, UserHeroName } from './UserHero'; import { getMxIdServer, mxcUrlToHttp } from '../../utils/matrix'; -import { - getDirectRoomAvatarUrl, - getMemberAvatarMxc, - getMemberDisplayName, - getRoomAvatarUrl, - joinRuleToIconSrc, -} from '../../utils/room'; +import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { PowerColorBadge, PowerIcon } from '../power'; import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels'; import { getPowers, getTagIconSrc, usePowerLevelTags } from '../../hooks/usePowerLevelTags'; -import { useMutualRooms, useMutualRoomsSupport } from '../../hooks/useMutualRooms'; -import { AsyncStatus } from '../../hooks/useAsyncCallback'; import { stopPropagation } from '../../utils/keyboard'; -import { getExploreServerPath } from '../../pages/pathUtils'; -import { useCloseUserRoomProfile } from '../../state/hooks/userRoomProfile'; -import { copyToClipboard } from '../../../util/common'; import { StateEvent } from '../../../types/matrix/room'; import { useOpenRoomSettings } from '../../state/hooks/roomSettings'; import { RoomSettingsPage } from '../../state/roomSettings'; import { useRoom } from '../../hooks/useRoom'; import { useSpaceOptionally } from '../../hooks/useSpace'; -import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom'; -import { useRoomNavigate } from '../../hooks/useRoomNavigate'; -import { factoryRoomIdByAtoZ } from '../../utils/sort'; -import { useDirectRooms } from '../../pages/client/direct/useDirectRooms'; -import { RoomAvatar, RoomIcon } from '../room-avatar'; -import { nameInitials } from '../../utils/common'; - -function ServerChip({ server }: { server: string }) { - const mx = useMatrixClient(); - const myServer = getMxIdServer(mx.getSafeUserId()); - const navigate = useNavigate(); - const closeProfile = useCloseUserRoomProfile(); - - const [cords, setCords] = useState(); - - const open: MouseEventHandler = (evt) => { - setCords(evt.currentTarget.getBoundingClientRect()); - }; - - const close = () => setCords(undefined); - - return ( - isKeyHotkey('arrowdown', evt), - isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), - }} - > - -
- { - copyToClipboard(server); - close(); - }} - > - Copy Server - - { - navigate(getExploreServerPath(server)); - closeProfile(); - }} - > - Explore Community - -
- -
- window.open(`https://${server}`, '_blank')} - > - Open in Browser - -
-
- - } - > - - ) : ( - - ) - } - onClick={open} - aria-pressed={!!cords} - > - - {server} - - -
- ); -} +import { useUserPresence } from '../../hooks/useUserPresence'; +import { SequenceCard } from '../sequence-card'; +import { MutualRoomsChip, ServerChip } from './UserChips'; +import { CutoutCard } from '../cutout-card'; +import { SettingTile } from '../setting-tile'; +import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings'; +import { SpaceSettingsPage } from '../../state/spaceSettings'; function PowerChip({ userId }: { userId: string }) { const mx = useMatrixClient(); @@ -153,6 +43,7 @@ function PowerChip({ userId }: { userId: string }) { const space = useSpaceOptionally(); const useAuthentication = useMediaAuthentication(); const openRoomSettings = useOpenRoomSettings(); + const openSpaceSettings = useOpenSpaceSettings(); const powerLevels = usePowerLevels(room); const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels); @@ -227,7 +118,16 @@ function PowerChip({ userId }: { userId: string }) { size="300" radii="300" onClick={() => { - openRoomSettings(room.roomId, space?.roomId, RoomSettingsPage.PermissionsPage); + console.log(room.roomId, space?.roomId); + if (room.isSpaceRoom()) { + openSpaceSettings( + room.roomId, + space?.roomId, + SpaceSettingsPage.PermissionsPage + ); + } else { + openRoomSettings(room.roomId, space?.roomId, RoomSettingsPage.PermissionsPage); + } close(); }} > @@ -260,145 +160,66 @@ function PowerChip({ userId }: { userId: string }) { ); } -function MutualRoomsChip({ userId }: { userId: string }) { - const mx = useMatrixClient(); - const mutualRoomSupported = useMutualRoomsSupport(); - const mutualRoomsState = useMutualRooms(userId); - const { navigateRoom, navigateSpace } = useRoomNavigate(); - const closeUserRoomProfile = useCloseUserRoomProfile(); - const directs = useDirectRooms(); - const useAuthentication = useMediaAuthentication(); +type UserBanAlertProps = { + userId: string; + reason?: string; +}; +function UserBanAlert({ userId, reason }: UserBanAlertProps) { + return ( + + + Unban + + } + > + + Banned User + This user has been banned!{reason ? ` Reason: ${reason}` : ''} + + + + ); +} - const allJoinedRooms = useAllJoinedRoomsSet(); - const getRoom = useGetRoom(allJoinedRooms); - - const [cords, setCords] = useState(); - - const open: MouseEventHandler = (evt) => { - setCords(evt.currentTarget.getBoundingClientRect()); - }; - - const close = () => setCords(undefined); - - if ( - userId === mx.getSafeUserId() || - !mutualRoomSupported || - mutualRoomsState.status === AsyncStatus.Error - ) { - return null; - } +type UserModerationProps = { + userId: string; + canKick: boolean; + canBan: boolean; +}; +function UserModeration({ userId, canKick, canBan }: UserModerationProps) { + const room = useRoom(); return ( - isKeyHotkey('arrowdown', evt), - isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), - }} - > - - - - - {mutualRoomsState.data - .sort(factoryRoomIdByAtoZ(mx)) - .map((roomId) => getRoom(roomId)) - .map((room) => { - if (!room) return null; - const { roomId } = room; - const dm = directs.includes(roomId); - - return ( - { - if (room.isSpaceRoom()) { - navigateSpace(roomId); - } else { - navigateRoom(roomId); - } - closeUserRoomProfile(); - }} - before={ - - {dm || room.isSpaceRoom() ? ( - ( - - {nameInitials(room.name)} - - )} - /> - ) : ( - - )} - - } - > - - {room.name} - - - ); - })} - - - - - - ) : null - } - > - + Moderation + } - disabled={mutualRoomsState.status !== AsyncStatus.Success} - onClick={open} - aria-pressed={!!cords} + style={{ padding: config.space.S100 }} + direction="Column" > - - {mutualRoomsState.status === AsyncStatus.Success && - `${mutualRoomsState.data.length} Mutual Rooms`} - {mutualRoomsState.status === AsyncStatus.Loading && 'Mutual Rooms'} - - - + } + disabled={!canKick} + > + Kick + + } + disabled={!canBan} + > + Ban + + +
); } @@ -409,22 +230,42 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const room = useRoom(); + const powerlevels = usePowerLevels(room); + const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerlevels); + const myPowerLevel = getPowerLevel(mx.getSafeUserId()); + const userPowerLevel = getPowerLevel(userId); + const canKick = canDoAction('kick', myPowerLevel) && myPowerLevel > userPowerLevel; + const canBan = canDoAction('ban', myPowerLevel) && myPowerLevel > userPowerLevel; + + const member = room.getMember(userId); const server = getMxIdServer(userId); const displayName = getMemberDisplayName(room, userId); const avatarMxc = getMemberAvatarMxc(room, userId); const avatarUrl = (avatarMxc && mxcUrlToHttp(mx, avatarMxc, useAuthentication)) ?? undefined; + const presence = useUserPresence(userId); + return ( - + - @@ -434,6 +275,10 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) { + {member?.membership === 'ban' && } + {(canKick || canBan) && ( + + )}
); diff --git a/src/app/components/user-profile/styles.css.ts b/src/app/components/user-profile/styles.css.ts index ac3ef416..ad6d5a95 100644 --- a/src/app/components/user-profile/styles.css.ts +++ b/src/app/components/user-profile/styles.css.ts @@ -30,12 +30,13 @@ export const UserHeroAvatarContainer = style({ position: 'relative', height: toRem(29), }); -export const UserHeroAvatar = style({ +export const UserAvatarContainer = style({ position: 'absolute', left: config.space.S400, top: 0, transform: 'translateY(-50%)', - background: color.Surface.Container, - - outline: `${config.borderWidth.B500} solid ${color.Surface.Container}`, + backgroundColor: color.Surface.Container, +}); +export const UserHeroAvatar = style({ + outline: `${config.borderWidth.B600} solid ${color.Surface.Container}`, }); diff --git a/src/app/features/common-settings/members/Members.tsx b/src/app/features/common-settings/members/Members.tsx index 8d7f89fd..dc802a1c 100644 --- a/src/app/features/common-settings/members/Members.tsx +++ b/src/app/features/common-settings/members/Members.tsx @@ -37,7 +37,6 @@ import { MemberTile } from '../../../components/member-tile'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { getMxIdLocalPart, getMxIdServer } from '../../../utils/matrix'; import { ServerBadge } from '../../../components/server-badge'; -import { openProfileViewer } from '../../../../client/action/navigation'; import { useDebounce } from '../../../hooks/useDebounce'; import { SearchItemStrGetter, @@ -53,6 +52,11 @@ import { UseStateProvider } from '../../../components/UseStateProvider'; import { MembershipFilterMenu } from '../../../components/MembershipFilterMenu'; import { MemberSortMenu } from '../../../components/MemberSortMenu'; import { ScrollTopContainer } from '../../../components/scroll-top-container'; +import { + useOpenUserRoomProfile, + useUserRoomProfileState, +} from '../../../state/hooks/userRoomProfile'; +import { useSpaceOptionally } from '../../../hooks/useSpace'; const SEARCH_OPTIONS: UseAsyncSearchOptions = { limit: 1000, @@ -77,6 +81,9 @@ export function Members({ requestClose }: MembersProps) { const room = useRoom(); const members = useRoomMembers(mx, room.roomId); const fetchingMembers = members.length < room.getJoinedMemberCount(); + const openProfile = useOpenUserRoomProfile(); + const profileUser = useUserRoomProfileState(); + const space = useSpaceOptionally(); const powerLevels = usePowerLevels(room); const { getPowerLevel } = usePowerLevelsAPI(powerLevels); @@ -142,8 +149,9 @@ export function Members({ requestClose }: MembersProps) { const handleMemberClick: MouseEventHandler = (evt) => { const btn = evt.currentTarget as HTMLButtonElement; const userId = btn.getAttribute('data-user-id'); - openProfileViewer(userId, room.roomId); - requestClose(); + if (userId) { + openProfile(room.roomId, space?.roomId, userId, btn.getBoundingClientRect()); + } }; return ( @@ -317,6 +325,7 @@ export function Members({ requestClose }: MembersProps) {