/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import {useLingui} from '@lingui/react/macro'; import {ClockIcon, CrownIcon, PencilIcon, UserListIcon, UsersIcon} from '@phosphor-icons/react'; import {observer} from 'mobx-react-lite'; import React from 'react'; import * as GuildMemberActionCreators from '~/actions/GuildMemberActionCreators'; import * as ModalActionCreators from '~/actions/ModalActionCreators'; import {modal} from '~/actions/ModalActionCreators'; import {Permissions} from '~/Constants'; import {BanMemberModal} from '~/components/modals/BanMemberModal'; import {ChangeNicknameModal} from '~/components/modals/ChangeNicknameModal'; import {KickMemberModal} from '~/components/modals/KickMemberModal'; import {RemoveTimeoutModal} from '~/components/modals/RemoveTimeoutModal'; import {TimeoutMemberModal} from '~/components/modals/TimeoutMemberModal'; import {TransferOwnershipModal} from '~/components/modals/TransferOwnershipModal'; import {useRoleHierarchy} from '~/hooks/useRoleHierarchy'; import type {GuildMemberRecord} from '~/records/GuildMemberRecord'; import type {UserRecord} from '~/records/UserRecord'; import AuthenticationStore from '~/stores/AuthenticationStore'; import GuildMemberStore from '~/stores/GuildMemberStore'; import GuildStore from '~/stores/GuildStore'; import PermissionStore from '~/stores/PermissionStore'; import * as ColorUtils from '~/utils/ColorUtils'; import * as PermissionUtils from '~/utils/PermissionUtils'; import {MenuGroup} from '../MenuGroup'; import {MenuItem} from '../MenuItem'; import {MenuItemCheckbox} from '../MenuItemCheckbox'; import {MenuItemSubmenu} from '../MenuItemSubmenu'; import itemStyles from './MenuItems.module.css'; interface TransferOwnershipMenuItemProps { guildId: string; user: UserRecord; member: GuildMemberRecord; onClose: () => void; } export const TransferOwnershipMenuItem: React.FC = observer( function TransferOwnershipMenuItem({guildId, user, member, onClose}) { const {t} = useLingui(); const handleTransferOwnership = React.useCallback(() => { onClose(); ModalActionCreators.push( modal(() => ), ); }, [guildId, user, member, onClose]); return ( } onClick={handleTransferOwnership}> {t`Transfer Ownership`} ); }, ); interface KickMemberMenuItemProps { guildId: string; user: UserRecord; onClose: () => void; } export const KickMemberMenuItem: React.FC = observer(function KickMemberMenuItem({ guildId, user, onClose, }) { const {t} = useLingui(); const handleKickMember = React.useCallback(() => { onClose(); ModalActionCreators.push(modal(() => )); }, [guildId, user, onClose]); return ( } onClick={handleKickMember} danger> {t`Kick Member`} ); }); interface BanMemberMenuItemProps { guildId: string; user: UserRecord; onClose: () => void; } export const BanMemberMenuItem: React.FC = observer(function BanMemberMenuItem({ guildId, user, onClose, }) { const {t} = useLingui(); const handleBanMember = React.useCallback(() => { onClose(); ModalActionCreators.push(modal(() => )); }, [guildId, user, onClose]); return ( } onClick={handleBanMember} danger> {t`Ban Member`} ); }); interface ManageRolesMenuItemProps { guildId: string; member: GuildMemberRecord; } export const ManageRolesMenuItem: React.FC = observer(function ManageRolesMenuItem({ guildId, member, }) { const {t} = useLingui(); const guild = GuildStore.getGuild(guildId); const currentMember = GuildMemberStore.getMember(guildId, member.user.id); const {canManageRole} = useRoleHierarchy(guild); const canManageRoles = PermissionStore.can(Permissions.MANAGE_ROLES, {guildId}); const allRoles = React.useMemo(() => { if (!guild) return []; return Object.values(guild.roles) .filter((role) => !role.isEveryone) .sort((a, b) => b.position - a.position) .map((role) => ({ role, canManage: canManageRole({id: role.id, position: role.position, permissions: role.permissions}), })); }, [guild, canManageRole]); const handleToggleRole = React.useCallback( async (roleId: string, hasRole: boolean, canToggle: boolean) => { if (!canToggle) return; if (hasRole) { await GuildMemberActionCreators.removeRole(guildId, member.user.id, roleId); } else { await GuildMemberActionCreators.addRole(guildId, member.user.id, roleId); } }, [guildId, member.user.id], ); if (allRoles.length === 0) return null; return ( } selectionMode="multiple" render={() => ( {allRoles.map(({role, canManage}) => { const hasRole = currentMember?.roles.has(role.id) ?? false; const canToggle = canManageRoles && canManage; return ( handleToggleRole(role.id, hasRole, canToggle)} closeOnChange={false} >
{role.name}
); })} )} /> ); }); interface ChangeNicknameMenuItemProps { guildId: string; user: UserRecord; member: GuildMemberRecord; onClose: () => void; } export const ChangeNicknameMenuItem: React.FC = observer(function ChangeNicknameMenuItem({ guildId, user, member, onClose, }) { const {t} = useLingui(); const currentUserId = AuthenticationStore.currentUserId; const isCurrentUser = user.id === currentUserId; const guild = GuildStore.getGuild(guildId); const {canManageTarget} = useRoleHierarchy(guild); const hasChangeNicknamePermission = PermissionStore.can(Permissions.CHANGE_NICKNAME, {guildId}); const hasManageNicknamesPermission = PermissionStore.can(Permissions.MANAGE_NICKNAMES, {guildId}); const canManageNicknames = (isCurrentUser && hasChangeNicknamePermission) || (hasManageNicknamesPermission && canManageTarget(user.id)); const handleChangeNickname = React.useCallback(() => { onClose(); ModalActionCreators.push(modal(() => )); }, [guildId, user, member, onClose]); if (!canManageNicknames) return null; return ( } onClick={handleChangeNickname}> {isCurrentUser ? t`Change Nickname` : t`Change Nickname`} ); }); interface TimeoutMemberMenuItemProps { guildId: string; user: UserRecord; member: GuildMemberRecord; onClose: () => void; } export const TimeoutMemberMenuItem: React.FC = observer(function TimeoutMemberMenuItem({ guildId, user, member, onClose, }) { const {t} = useLingui(); const guild = GuildStore.getGuild(guildId); const guildSnapshot = guild?.toJSON(); const targetHasModerateMembersPermission = guildSnapshot !== undefined && PermissionUtils.can(Permissions.MODERATE_MEMBERS, user.id, guildSnapshot); const handleTimeoutMember = React.useCallback(() => { onClose(); ModalActionCreators.push(modal(() => )); }, [guildId, user, onClose]); const handleRemoveTimeout = React.useCallback(() => { onClose(); ModalActionCreators.push(modal(() => )); }, [guildId, user, onClose]); if (targetHasModerateMembersPermission) { return null; } const isTimedOut = member.isTimedOut(); const handleClick = isTimedOut ? handleRemoveTimeout : handleTimeoutMember; return ( } onClick={handleClick} danger={!isTimedOut}> {isTimedOut ? t`Remove Timeout` : t`Timeout`} ); });