fix(moderation): knock Deny gated on kick power; approve/deny errors surfaced
Fixes #49 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -30,6 +30,7 @@ import {
|
|||||||
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
|
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
import { MobileTouchTarget } from '../../styles/mobile.css';
|
import { MobileTouchTarget } from '../../styles/mobile.css';
|
||||||
import { Membership } from '../../../types/matrix/room';
|
import { Membership } from '../../../types/matrix/room';
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ import { MemberVerificationBadge } from '../../components/MemberVerificationBadg
|
|||||||
import { useUserPresence } from '../../hooks/useUserPresence';
|
import { useUserPresence } from '../../hooks/useUserPresence';
|
||||||
import { PresenceBadge, PresenceRingAvatar } from '../../components/presence';
|
import { PresenceBadge, PresenceRingAvatar } from '../../components/presence';
|
||||||
import { AvatarDecoration } from '../../components/avatar-decoration/AvatarDecoration';
|
import { AvatarDecoration } from '../../components/avatar-decoration/AvatarDecoration';
|
||||||
|
import { createErrorToast, toastQueueAtom } from '../../state/toast';
|
||||||
|
|
||||||
type MemberDrawerHeaderProps = {
|
type MemberDrawerHeaderProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
@@ -239,11 +241,55 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
|
|||||||
const myUserId = mx.getUserId();
|
const myUserId = mx.getUserId();
|
||||||
const myPowerLevel = readPowerLevel.user(powerLevels, myUserId ?? undefined);
|
const myPowerLevel = readPowerLevel.user(powerLevels, myUserId ?? undefined);
|
||||||
const invitePowerLevel = readPowerLevel.action(powerLevels, 'invite');
|
const invitePowerLevel = readPowerLevel.action(powerLevels, 'invite');
|
||||||
|
const kickPowerLevel = readPowerLevel.action(powerLevels, 'kick');
|
||||||
const canApproveKnock = myPowerLevel >= invitePowerLevel;
|
const canApproveKnock = myPowerLevel >= invitePowerLevel;
|
||||||
|
// Deny uses mx.kick, which is gated by the room's kick power level, not invite -
|
||||||
|
// these can differ (e.g. moderators can invite but only admins can kick).
|
||||||
|
const canDenyKnock = myPowerLevel >= kickPowerLevel;
|
||||||
const knockMembers = useMemo(
|
const knockMembers = useMemo(
|
||||||
() => (canApproveKnock ? room.getMembersWithMembership(Membership.Knock) : []),
|
() => (canApproveKnock ? room.getMembersWithMembership(Membership.Knock) : []),
|
||||||
[room, canApproveKnock],
|
[room, canApproveKnock],
|
||||||
);
|
);
|
||||||
|
const setToast = useSetAtom(toastQueueAtom);
|
||||||
|
const [pendingKnockAction, setPendingKnockAction] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const handleApproveKnock = useCallback(
|
||||||
|
(userId: string) => {
|
||||||
|
setPendingKnockAction(userId);
|
||||||
|
mx.invite(room.roomId, userId)
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
console.error('Failed to approve knock request:', err);
|
||||||
|
setToast(
|
||||||
|
createErrorToast(
|
||||||
|
'Could not approve this request. Please try again.',
|
||||||
|
Icons.Warning,
|
||||||
|
'Failed',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(() => setPendingKnockAction(undefined));
|
||||||
|
},
|
||||||
|
[mx, room.roomId, setToast],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDenyKnock = useCallback(
|
||||||
|
(userId: string) => {
|
||||||
|
setPendingKnockAction(userId);
|
||||||
|
mx.kick(room.roomId, userId)
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
console.error('Failed to deny knock request:', err);
|
||||||
|
setToast(
|
||||||
|
createErrorToast(
|
||||||
|
'Could not deny this request. Please try again.',
|
||||||
|
Icons.Warning,
|
||||||
|
'Failed',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(() => setPendingKnockAction(undefined));
|
||||||
|
},
|
||||||
|
[mx, room.roomId, setToast],
|
||||||
|
);
|
||||||
|
|
||||||
const filteredMembers = useMemo(
|
const filteredMembers = useMemo(
|
||||||
() => members.filter(membershipFilter.filterFn).sort(memberSort.sortFn).sort(memberPowerSort),
|
() => members.filter(membershipFilter.filterFn).sort(memberSort.sortFn).sort(memberPowerSort),
|
||||||
@@ -462,20 +508,24 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
|
|||||||
radii="300"
|
radii="300"
|
||||||
fill="Soft"
|
fill="Soft"
|
||||||
className={MobileTouchTarget}
|
className={MobileTouchTarget}
|
||||||
onClick={() => mx.invite(room.roomId, knockMember.userId)}
|
disabled={pendingKnockAction === knockMember.userId}
|
||||||
|
onClick={() => handleApproveKnock(knockMember.userId)}
|
||||||
>
|
>
|
||||||
<Text size="B300">Approve</Text>
|
<Text size="B300">Approve</Text>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{canDenyKnock && (
|
||||||
size="300"
|
<Button
|
||||||
variant="Critical"
|
size="300"
|
||||||
radii="300"
|
variant="Critical"
|
||||||
fill="Soft"
|
radii="300"
|
||||||
className={MobileTouchTarget}
|
fill="Soft"
|
||||||
onClick={() => mx.kick(room.roomId, knockMember.userId)}
|
className={MobileTouchTarget}
|
||||||
>
|
disabled={pendingKnockAction === knockMember.userId}
|
||||||
<Text size="B300">Deny</Text>
|
onClick={() => handleDenyKnock(knockMember.userId)}
|
||||||
</Button>
|
>
|
||||||
|
<Text size="B300">Deny</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user