diff --git a/src/app/components/user-profile/UserModeration.tsx b/src/app/components/user-profile/UserModeration.tsx index 97837085..814bb5ba 100644 --- a/src/app/components/user-profile/UserModeration.tsx +++ b/src/app/components/user-profile/UserModeration.tsx @@ -1,7 +1,6 @@ -import { Box, Button, color, config, Icon, Icons, MenuItem, Spinner, Text } from 'folds'; -import React, { useCallback } from 'react'; +import { Box, Button, color, config, Icon, Icons, Spinner, Text, Input } from 'folds'; +import React, { useCallback, useRef } from 'react'; import { useRoom } from '../../hooks/useRoom'; -import { SequenceCard } from '../sequence-card'; import { CutoutCard } from '../cutout-card'; import { SettingTile } from '../setting-tile'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; @@ -11,6 +10,52 @@ 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 ( + + + + + Kicked User + {time && date && ( + + {date} {time} + + )} + + + {kickedBy && ( + + Kicked by: {kickedBy} + + )} + + {reason ? ( + <> + Reason: {reason} + + ) : ( + No Reason Provided. + )} + + + + + + ); +} + type UserBanAlertProps = { userId: string; reason?: string; @@ -86,42 +131,219 @@ export function UserBanAlert({ userId, reason, canUnban, bannedBy, ts }: UserBan ); } +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( + useCallback(async () => { + await mx.kick(room.roomId, userId); + }, [mx, room, userId]) + ); + const kicking = kickState.status === AsyncStatus.Loading; + const error = kickState.status === AsyncStatus.Error; + + return ( + + + + + Invited User + {time && date && ( + + {date} {time} + + )} + + + {invitedBy && ( + + Invited by: {invitedBy} + + )} + + {reason ? ( + <> + Reason: {reason} + + ) : ( + No Reason Provided. + )} + + + {error && ( + + {kickState.error.message} + + )} + {canKick && ( + + )} + + + + ); +} + type UserModerationProps = { userId: string; canKick: boolean; canBan: boolean; + canInvite: boolean; }; -export function UserModeration({ userId, canKick, canBan }: UserModerationProps) { +export function UserModeration({ userId, canKick, canBan, canInvite }: UserModerationProps) { + const mx = useMatrixClient(); const room = useRoom(); + const reasonInputRef = useRef(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( + useCallback(async () => { + await mx.kick(room.roomId, userId, getReason()); + }, [mx, room, userId, getReason]) + ); + + const [banState, ban] = useAsyncCallback( + useCallback(async () => { + await mx.ban(room.roomId, userId, getReason()); + }, [mx, room, userId, getReason]) + ); + + const [inviteState, invite] = useAsyncCallback( + 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 ( - - Moderation - - } - disabled={!canKick} - > - Kick - - } - disabled={!canBan} - > - Ban - - + + + + Moderation + + {kickState.status === AsyncStatus.Error && ( + + {kickState.error.message} + + )} + {banState.status === AsyncStatus.Error && ( + + {banState.error.message} + + )} + {inviteState.status === AsyncStatus.Error && ( + + {inviteState.error.message} + + )} + + + {canInvite && ( + + )} + {canKick && ( + + )} + {canBan && ( + + )} + + ); } diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index ab31fc2e..ad23fef1 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -16,8 +16,10 @@ import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useAlive } from '../../hooks/useAlive'; import { useCloseUserRoomProfile } from '../../state/hooks/userRoomProfile'; import { PowerChip } from './PowerChip'; -import { UserBanAlert, UserModeration } from './UserModeration'; +import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration'; import { useIgnoredUsers } from '../../hooks/useIgnoredUsers'; +import { useMembership } from '../../hooks/useMembership'; +import { Membership } from '../../../types/matrix/room'; type UserRoomProfileProps = { userId: string; @@ -38,8 +40,10 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) { const userPowerLevel = getPowerLevel(userId); const canKick = canDoAction('kick', myPowerLevel) && myPowerLevel > userPowerLevel; const canBan = canDoAction('ban', myPowerLevel) && myPowerLevel > userPowerLevel; + const canInvite = canDoAction('invite', myPowerLevel); const member = room.getMember(userId); + const membership = useMembership(room, userId); const server = getMxIdServer(userId); const displayName = getMemberDisplayName(room, userId); @@ -115,7 +119,7 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) { {ignored && } - {member?.membership === 'ban' && ( + {member && membership === Membership.Ban && ( )} - {(canKick || canBan) && ( - + {member && + membership === Membership.Leave && + member.events.member && + member.events.member.getSender() !== userId && ( + + )} + {member && membership === Membership.Invite && ( + )} + ); diff --git a/src/app/hooks/useMembership.ts b/src/app/hooks/useMembership.ts new file mode 100644 index 00000000..dbdd527e --- /dev/null +++ b/src/app/hooks/useMembership.ts @@ -0,0 +1,28 @@ +import { useEffect, useState } from 'react'; +import { Room, RoomMemberEvent, RoomMemberEventHandlerMap } from 'matrix-js-sdk'; +import { Membership } from '../../types/matrix/room'; + +export const useMembership = (room: Room, userId: string): Membership => { + const member = room.getMember(userId); + + const [membership, setMembership] = useState( + () => (member?.membership as Membership | undefined) ?? Membership.Leave + ); + + useEffect(() => { + const handleMembershipChange: RoomMemberEventHandlerMap[RoomMemberEvent.Membership] = ( + event, + m + ) => { + if (event.getRoomId() === room.roomId && m.userId === userId) { + setMembership((m.membership as Membership | undefined) ?? Membership.Leave); + } + }; + member?.on(RoomMemberEvent.Membership, handleMembershipChange); + return () => { + member?.removeListener(RoomMemberEvent.Membership, handleMembershipChange); + }; + }, [room, member, userId]); + + return membership; +};