From 3cc3d03ace88d83c33637109ddff8b60715c2987 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Fri, 8 Aug 2025 14:28:32 +0530 Subject: [PATCH] add option to change user powers in profile --- src/app/components/user-profile/PowerChip.tsx | 344 ++++++++++++++++++ .../user-profile/UserRoomProfile.tsx | 154 +------- 2 files changed, 347 insertions(+), 151 deletions(-) create mode 100644 src/app/components/user-profile/PowerChip.tsx diff --git a/src/app/components/user-profile/PowerChip.tsx b/src/app/components/user-profile/PowerChip.tsx new file mode 100644 index 00000000..78f539a1 --- /dev/null +++ b/src/app/components/user-profile/PowerChip.tsx @@ -0,0 +1,344 @@ +import { + Box, + Button, + Chip, + config, + Dialog, + Header, + Icon, + IconButton, + Icons, + Line, + Menu, + MenuItem, + Overlay, + OverlayBackdrop, + OverlayCenter, + PopOut, + RectCords, + Spinner, + Text, + toRem, +} from 'folds'; +import React, { MouseEventHandler, useCallback, useState } from 'react'; +import FocusTrap from 'focus-trap-react'; +import { isKeyHotkey } from 'is-hotkey'; +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 { stopPropagation } from '../../utils/keyboard'; +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 { CutoutCard } from '../cutout-card'; +import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings'; +import { SpaceSettingsPage } from '../../state/spaceSettings'; +import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; +import { BreakWord } from '../../styles/Text.css'; + +type SelfDemoteAlertProps = { + power: number; + onCancel: () => void; + onChange: (power: number) => void; +}; +function SelfDemoteAlert({ power, onCancel, onChange }: SelfDemoteAlertProps) { + return ( + }> + + + +
+ + Self Demotion + + + + +
+ + + + You are about to demote yourself! You will not be able to regain this power + yourself. Are you sure? + + + + + + +
+
+
+
+ ); +} + +type SharedPowerAlertProps = { + power: number; + onCancel: () => void; + onChange: (power: number) => void; +}; +function SharedPowerAlert({ power, onCancel, onChange }: SharedPowerAlertProps) { + return ( + }> + + + +
+ + Shared Power + + + + +
+ + + + You are promoting the user to have the same power as yourself! You will not be + able to change their power afterward. Are you sure? + + + + + + +
+
+
+
+ ); +} + +export function PowerChip({ userId }: { userId: string }) { + const mx = useMatrixClient(); + const room = useRoom(); + const space = useSpaceOptionally(); + const useAuthentication = useMediaAuthentication(); + const openRoomSettings = useOpenRoomSettings(); + const openSpaceSettings = useOpenSpaceSettings(); + + const powerLevels = usePowerLevels(room); + const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels); + const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels); + const myPower = getPowerLevel(mx.getSafeUserId()); + const userPower = getPowerLevel(userId); + const canChangePowers = + canSendStateEvent(StateEvent.RoomPowerLevels, myPower) && + (mx.getSafeUserId() === userId ? true : myPower > userPower); + + const tag = getPowerLevelTag(userPower); + const tagIconSrc = tag.icon && getTagIconSrc(mx, useAuthentication, tag.icon); + + const [cords, setCords] = useState(); + + const open: MouseEventHandler = (evt) => { + setCords(evt.currentTarget.getBoundingClientRect()); + }; + + const close = () => setCords(undefined); + + const [powerState, changePower] = useAsyncCallback( + useCallback( + async (power: number) => { + await mx.setPowerLevel(room.roomId, userId, power); + }, + [mx, userId, room] + ) + ); + const changing = powerState.status === AsyncStatus.Loading; + const error = powerState.status === AsyncStatus.Error; + const [selfDemote, setSelfDemote] = useState(); + const [sharedPower, setSharedPower] = useState(); + + const handlePowerSelect = (power: number): void => { + close(); + if (!canChangePowers) return; + if (power === userPower) return; + + if (userId === mx.getSafeUserId()) { + setSelfDemote(power); + return; + } + if (power === myPower) { + setSharedPower(power); + return; + } + + changePower(power); + }; + + const handleSelfDemote = (power: number) => { + setSelfDemote(undefined); + changePower(power); + }; + const handleSharedPower = (power: number) => { + setSharedPower(undefined); + changePower(power); + }; + + return ( + <> + isKeyHotkey('arrowdown', evt), + isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), + }} + > + + + {error && ( + + Error: {powerState.error.name} + + {powerState.error.message} + + + )} + {getPowers(powerLevelTags).map((power) => { + const powerTag = powerLevelTags[power]; + const powerTagIconSrc = + powerTag.icon && getTagIconSrc(mx, useAuthentication, powerTag.icon); + + const canAssignPower = power <= myPower; + + return ( + } + after={ + powerTagIconSrc ? ( + + ) : undefined + } + onClick={ + canChangePowers && canAssignPower + ? () => handlePowerSelect(power) + : undefined + } + > + {powerTag.name} + + ); + })} + + +
+ { + if (room.isSpaceRoom()) { + openSpaceSettings( + room.roomId, + space?.roomId, + SpaceSettingsPage.PermissionsPage + ); + } else { + openRoomSettings( + room.roomId, + space?.roomId, + RoomSettingsPage.PermissionsPage + ); + } + close(); + }} + > + Manage Powers + +
+
+ + } + > + + ) : ( + <> + {!changing && } + {changing && } + + ) + } + after={tagIconSrc ? : undefined} + onClick={open} + aria-pressed={!!cords} + > + + {tag.name} + + +
+ {typeof selfDemote === 'number' ? ( + setSelfDemote(undefined)} + onChange={handleSelfDemote} + /> + ) : null} + {typeof sharedPower === 'number' ? ( + setSharedPower(undefined)} + onChange={handleSharedPower} + /> + ) : null} + + ); +} diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index fca93f01..00ed31bc 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -1,172 +1,24 @@ -import { - Box, - Button, - Chip, - color, - config, - Icon, - Icons, - Line, - Menu, - MenuItem, - PopOut, - RectCords, - Spinner, - Text, -} from 'folds'; -import React, { MouseEventHandler, useCallback, useState } from 'react'; -import FocusTrap from 'focus-trap-react'; -import { isKeyHotkey } from 'is-hotkey'; +import { Box, Button, color, config, Icon, Icons, MenuItem, Spinner, Text } from 'folds'; +import React, { useCallback } from 'react'; import { UserHero, UserHeroName } from './UserHero'; import { getDMRoomFor, getMxIdServer, mxcUrlToHttp } from '../../utils/matrix'; 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 { stopPropagation } from '../../utils/keyboard'; -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 { 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'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { createDM } from '../../../client/action/room'; import { hasDevices } from '../../../util/matrixUtil'; import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useAlive } from '../../hooks/useAlive'; import { useCloseUserRoomProfile } from '../../state/hooks/userRoomProfile'; - -function PowerChip({ userId }: { userId: string }) { - const mx = useMatrixClient(); - const room = useRoom(); - const space = useSpaceOptionally(); - const useAuthentication = useMediaAuthentication(); - const openRoomSettings = useOpenRoomSettings(); - const openSpaceSettings = useOpenSpaceSettings(); - - const powerLevels = usePowerLevels(room); - const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels); - const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels); - const myPower = getPowerLevel(mx.getSafeUserId()); - const canChangePowers = canSendStateEvent(StateEvent.RoomPowerLevels, myPower); - - const userPower = getPowerLevel(userId); - const tag = getPowerLevelTag(userPower); - const tagIconSrc = tag.icon && getTagIconSrc(mx, useAuthentication, tag.icon); - - 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), - }} - > - -
- {getPowers(powerLevelTags).map((power) => { - const powerTag = powerLevelTags[power]; - const powerTagIconSrc = - powerTag.icon && getTagIconSrc(mx, useAuthentication, powerTag.icon); - - return ( - } - after={ - powerTagIconSrc ? ( - - ) : undefined - } - onClick={canChangePowers ? undefined : undefined} - > - {powerTag.name} - - ); - })} -
- - -
- { - 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(); - }} - > - Manage Powers - -
-
- - } - > - - ) : ( - - ) - } - after={tagIconSrc ? : undefined} - onClick={open} - aria-pressed={!!cords} - > - - {tag.name} - - -
- ); -} +import { PowerChip } from './PowerChip'; type UserBanAlertProps = { userId: string;