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 { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import classNames from 'classnames';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { MobileTouchTarget } from '../../styles/mobile.css';
|
||||
import { Membership } from '../../../types/matrix/room';
|
||||
|
||||
@@ -71,6 +72,7 @@ import { MemberVerificationBadge } from '../../components/MemberVerificationBadg
|
||||
import { useUserPresence } from '../../hooks/useUserPresence';
|
||||
import { PresenceBadge, PresenceRingAvatar } from '../../components/presence';
|
||||
import { AvatarDecoration } from '../../components/avatar-decoration/AvatarDecoration';
|
||||
import { createErrorToast, toastQueueAtom } from '../../state/toast';
|
||||
|
||||
type MemberDrawerHeaderProps = {
|
||||
room: Room;
|
||||
@@ -239,11 +241,55 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
|
||||
const myUserId = mx.getUserId();
|
||||
const myPowerLevel = readPowerLevel.user(powerLevels, myUserId ?? undefined);
|
||||
const invitePowerLevel = readPowerLevel.action(powerLevels, 'invite');
|
||||
const kickPowerLevel = readPowerLevel.action(powerLevels, 'kick');
|
||||
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(
|
||||
() => (canApproveKnock ? room.getMembersWithMembership(Membership.Knock) : []),
|
||||
[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(
|
||||
() => members.filter(membershipFilter.filterFn).sort(memberSort.sortFn).sort(memberPowerSort),
|
||||
@@ -462,20 +508,24 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
|
||||
radii="300"
|
||||
fill="Soft"
|
||||
className={MobileTouchTarget}
|
||||
onClick={() => mx.invite(room.roomId, knockMember.userId)}
|
||||
disabled={pendingKnockAction === knockMember.userId}
|
||||
onClick={() => handleApproveKnock(knockMember.userId)}
|
||||
>
|
||||
<Text size="B300">Approve</Text>
|
||||
</Button>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Critical"
|
||||
radii="300"
|
||||
fill="Soft"
|
||||
className={MobileTouchTarget}
|
||||
onClick={() => mx.kick(room.roomId, knockMember.userId)}
|
||||
>
|
||||
<Text size="B300">Deny</Text>
|
||||
</Button>
|
||||
{canDenyKnock && (
|
||||
<Button
|
||||
size="300"
|
||||
variant="Critical"
|
||||
radii="300"
|
||||
fill="Soft"
|
||||
className={MobileTouchTarget}
|
||||
disabled={pendingKnockAction === knockMember.userId}
|
||||
onClick={() => handleDenyKnock(knockMember.userId)}
|
||||
>
|
||||
<Text size="B300">Deny</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user