Files
cinny/src/app/components/user-profile/UserModeration.tsx
Ajay Bura 4d1ae4eafd Redesign user profile view (#2396)
* WIP - new profile view

* render common rooms in user profile

* add presence component

* WIP - room user profile

* temp hide profile button

* show mutual rooms in spaces, rooms and direct messages categories

* add message button

* add option to change user powers in profile

* improve ban info and option to unban

* add share user button in user profile

* add option to block user in user profile

* improve blocked user alert body

* add moderation tool in user profile

* open profile view on left side in member drawer

* open new user profile in all places
2025-08-09 22:16:10 +10:00

350 lines
11 KiB
TypeScript

import { Box, Button, color, config, Icon, Icons, Spinner, Text, Input } from 'folds';
import React, { useCallback, useRef } from 'react';
import { useRoom } from '../../hooks/useRoom';
import { CutoutCard } from '../cutout-card';
import { SettingTile } from '../setting-tile';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { BreakWord } from '../../styles/Text.css';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { timeDayMonYear, timeHourMinute } from '../../utils/time';
type UserKickAlertProps = {
reason?: string;
kickedBy?: string;
ts?: number;
};
export function UserKickAlert({ reason, kickedBy, ts }: UserKickAlertProps) {
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
const time = ts ? timeHourMinute(ts, hour24Clock) : undefined;
const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined;
return (
<CutoutCard style={{ padding: config.space.S200 }} variant="Critical">
<SettingTile>
<Box direction="Column" gap="200">
<Box gap="200" justifyContent="SpaceBetween">
<Text size="L400">Kicked User</Text>
{time && date && (
<Text size="T200">
{date} {time}
</Text>
)}
</Box>
<Box direction="Column">
{kickedBy && (
<Text size="T200">
Kicked by: <b>{kickedBy}</b>
</Text>
)}
<Text size="T200">
{reason ? (
<>
Reason: <b>{reason}</b>
</>
) : (
<i>No Reason Provided.</i>
)}
</Text>
</Box>
</Box>
</SettingTile>
</CutoutCard>
);
}
type UserBanAlertProps = {
userId: string;
reason?: string;
canUnban?: boolean;
bannedBy?: string;
ts?: number;
};
export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBanAlertProps) {
const mx = useMatrixClient();
const room = useRoom();
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
const time = ts ? timeHourMinute(ts, hour24Clock) : undefined;
const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined;
const [unbanState, unban] = useAsyncCallback<undefined, Error, []>(
useCallback(async () => {
await mx.unban(room.roomId, userId);
}, [mx, room, userId])
);
const banning = unbanState.status === AsyncStatus.Loading;
const error = unbanState.status === AsyncStatus.Error;
return (
<CutoutCard style={{ padding: config.space.S200 }} variant="Critical">
<SettingTile>
<Box direction="Column" gap="200">
<Box gap="200" justifyContent="SpaceBetween">
<Text size="L400">Banned User</Text>
{time && date && (
<Text size="T200">
{date} {time}
</Text>
)}
</Box>
<Box direction="Column">
{bannedBy && (
<Text size="T200">
Banned by: <b>{bannedBy}</b>
</Text>
)}
<Text size="T200">
{reason ? (
<>
Reason: <b>{reason}</b>
</>
) : (
<i>No Reason Provided.</i>
)}
</Text>
</Box>
{error && (
<Text className={BreakWord} size="T200" style={{ color: color.Critical.Main }}>
<b>{unbanState.error.message}</b>
</Text>
)}
{canUnban && (
<Button
size="300"
variant="Critical"
radii="300"
onClick={unban}
before={banning && <Spinner size="100" variant="Critical" fill="Solid" />}
disabled={banning}
>
<Text size="B300">Unban</Text>
</Button>
)}
</Box>
</SettingTile>
</CutoutCard>
);
}
type UserInviteAlertProps = {
userId: string;
reason?: string;
canKick?: boolean;
invitedBy?: string;
ts?: number;
};
export function UserInviteAlert({ userId, reason, canKick, invitedBy, ts }: UserInviteAlertProps) {
const mx = useMatrixClient();
const room = useRoom();
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
const time = ts ? timeHourMinute(ts, hour24Clock) : undefined;
const date = ts ? timeDayMonYear(ts, dateFormatString) : undefined;
const [kickState, kick] = useAsyncCallback<undefined, Error, []>(
useCallback(async () => {
await mx.kick(room.roomId, userId);
}, [mx, room, userId])
);
const kicking = kickState.status === AsyncStatus.Loading;
const error = kickState.status === AsyncStatus.Error;
return (
<CutoutCard style={{ padding: config.space.S200 }} variant="Success">
<SettingTile>
<Box direction="Column" gap="200">
<Box gap="200" justifyContent="SpaceBetween">
<Text size="L400">Invited User</Text>
{time && date && (
<Text size="T200">
{date} {time}
</Text>
)}
</Box>
<Box direction="Column">
{invitedBy && (
<Text size="T200">
Invited by: <b>{invitedBy}</b>
</Text>
)}
<Text size="T200">
{reason ? (
<>
Reason: <b>{reason}</b>
</>
) : (
<i>No Reason Provided.</i>
)}
</Text>
</Box>
{error && (
<Text className={BreakWord} size="T200" style={{ color: color.Critical.Main }}>
<b>{kickState.error.message}</b>
</Text>
)}
{canKick && (
<Button
size="300"
variant="Success"
fill="Soft"
outlined
radii="300"
onClick={kick}
before={kicking && <Spinner size="100" variant="Success" fill="Soft" />}
disabled={kicking}
>
<Text size="B300">Cancel Invite</Text>
</Button>
)}
</Box>
</SettingTile>
</CutoutCard>
);
}
type UserModerationProps = {
userId: string;
canKick: boolean;
canBan: boolean;
canInvite: boolean;
};
export function UserModeration({ userId, canKick, canBan, canInvite }: UserModerationProps) {
const mx = useMatrixClient();
const room = useRoom();
const reasonInputRef = useRef<HTMLInputElement>(null);
const getReason = useCallback((): string | undefined => {
const reason = reasonInputRef.current?.value.trim() || undefined;
if (reasonInputRef.current) {
reasonInputRef.current.value = '';
}
return reason;
}, []);
const [kickState, kick] = useAsyncCallback<undefined, Error, []>(
useCallback(async () => {
await mx.kick(room.roomId, userId, getReason());
}, [mx, room, userId, getReason])
);
const [banState, ban] = useAsyncCallback<undefined, Error, []>(
useCallback(async () => {
await mx.ban(room.roomId, userId, getReason());
}, [mx, room, userId, getReason])
);
const [inviteState, invite] = useAsyncCallback<undefined, Error, []>(
useCallback(async () => {
await mx.invite(room.roomId, userId, getReason());
}, [mx, room, userId, getReason])
);
const disabled =
kickState.status === AsyncStatus.Loading ||
banState.status === AsyncStatus.Loading ||
inviteState.status === AsyncStatus.Loading;
if (!canBan && !canKick && !canInvite) return null;
return (
<Box direction="Column" gap="400">
<Box direction="Column" gap="200">
<Box grow="Yes" direction="Column" gap="100">
<Text size="L400">Moderation</Text>
<Input
ref={reasonInputRef}
placeholder="Reason"
size="300"
variant="Background"
radii="300"
disabled={disabled}
/>
{kickState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} className={BreakWord} size="T200">
<b>{kickState.error.message}</b>
</Text>
)}
{banState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} className={BreakWord} size="T200">
<b>{banState.error.message}</b>
</Text>
)}
{inviteState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} className={BreakWord} size="T200">
<b>{inviteState.error.message}</b>
</Text>
)}
</Box>
<Box shrink="No" gap="200">
{canInvite && (
<Button
style={{ flexGrow: 1 }}
size="300"
variant="Secondary"
fill="Soft"
radii="300"
before={
inviteState.status === AsyncStatus.Loading ? (
<Spinner size="50" variant="Secondary" fill="Soft" />
) : (
<Icon size="50" src={Icons.ArrowRight} />
)
}
onClick={invite}
disabled={disabled}
>
<Text size="B300">Invite</Text>
</Button>
)}
{canKick && (
<Button
style={{ flexGrow: 1 }}
size="300"
variant="Critical"
fill="Soft"
radii="300"
before={
kickState.status === AsyncStatus.Loading ? (
<Spinner size="50" variant="Critical" fill="Soft" />
) : (
<Icon size="50" src={Icons.ArrowLeft} />
)
}
onClick={kick}
disabled={disabled}
>
<Text size="B300">Kick</Text>
</Button>
)}
{canBan && (
<Button
style={{ flexGrow: 1 }}
size="300"
variant="Critical"
fill="Solid"
radii="300"
before={
banState.status === AsyncStatus.Loading ? (
<Spinner size="50" variant="Critical" fill="Solid" />
) : (
<Icon size="50" src={Icons.Prohibited} />
)
}
onClick={ban}
disabled={disabled}
>
<Text size="B300">Ban</Text>
</Button>
)}
</Box>
</Box>
</Box>
);
}