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 && (
+ }
+ disabled={kicking}
+ >
+ Cancel Invite
+
+ )}
+
+
+
+ );
+}
+
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 && (
+
+ ) : (
+
+ )
+ }
+ onClick={invite}
+ disabled={disabled}
+ >
+ Invite
+
+ )}
+ {canKick && (
+
+ ) : (
+
+ )
+ }
+ onClick={kick}
+ disabled={disabled}
+ >
+ Kick
+
+ )}
+ {canBan && (
+
+ ) : (
+
+ )
+ }
+ onClick={ban}
+ disabled={disabled}
+ >
+ Ban
+
+ )}
+
+
);
}
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;
+};