Files
fluxer/fluxer_app/src/components/modals/UserProfileActionsSheet.tsx
2026-02-17 12:22:36 +00:00

318 lines
9.3 KiB
TypeScript

/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
import {modal} from '@app/actions/ModalActionCreators';
import * as RelationshipActionCreators from '@app/actions/RelationshipActionCreators';
import * as TextCopyActionCreators from '@app/actions/TextCopyActionCreators';
import {BanMemberModal} from '@app/components/modals/BanMemberModal';
import {ChangeNicknameModal} from '@app/components/modals/ChangeNicknameModal';
import {ConfirmModal} from '@app/components/modals/ConfirmModal';
import {KickMemberModal} from '@app/components/modals/KickMemberModal';
import {TransferOwnershipModal} from '@app/components/modals/TransferOwnershipModal';
import styles from '@app/components/modals/UserProfileActionsSheet.module.css';
import {MenuBottomSheet, type MenuGroupType} from '@app/components/uikit/menu_bottom_sheet/MenuBottomSheet';
import {useRoleHierarchy} from '@app/hooks/useRoleHierarchy';
import type {GuildMemberRecord} from '@app/records/GuildMemberRecord';
import type {UserRecord} from '@app/records/UserRecord';
import AuthenticationStore from '@app/stores/AuthenticationStore';
import GuildMemberStore from '@app/stores/GuildMemberStore';
import GuildStore from '@app/stores/GuildStore';
import PermissionStore from '@app/stores/PermissionStore';
import RelationshipStore from '@app/stores/RelationshipStore';
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
import {RelationshipTypes} from '@fluxer/constants/src/UserConstants';
import {useLingui} from '@lingui/react/macro';
import {
CopyIcon,
CrownIcon,
FlagIcon,
GavelIcon,
GlobeIcon,
IdentificationCardIcon,
PencilIcon,
ProhibitIcon,
SignOutIcon,
UserMinusIcon,
} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
interface UserProfileActionsSheetProps {
isOpen: boolean;
onClose: () => void;
user: UserRecord;
isCurrentUser?: boolean;
hasGuildProfile?: boolean;
showGlobalProfile?: boolean;
onToggleProfileView?: () => void;
guildId?: string;
guildMember?: GuildMemberRecord | null;
}
export const UserProfileActionsSheet: React.FC<UserProfileActionsSheetProps> = observer(
({
isOpen,
onClose,
user,
isCurrentUser = false,
hasGuildProfile = false,
showGlobalProfile = false,
onToggleProfileView,
guildId,
guildMember,
}) => {
const {t, i18n} = useLingui();
const relationshipType = RelationshipStore.getRelationship(user.id)?.type;
const guild = guildId ? GuildStore.getGuild(guildId) : null;
const member = guildMember ?? (guildId ? GuildMemberStore.getMember(guildId, user.id) : null);
const currentUserId = AuthenticationStore.currentUserId;
const canKickMembers = guildId ? PermissionStore.can(Permissions.KICK_MEMBERS, {guildId}) : false;
const canBanMembers = guildId ? PermissionStore.can(Permissions.BAN_MEMBERS, {guildId}) : false;
const hasChangeNicknamePermission = guildId ? PermissionStore.can(Permissions.CHANGE_NICKNAME, {guildId}) : false;
const hasManageNicknamesPermission = guildId ? PermissionStore.can(Permissions.MANAGE_NICKNAMES, {guildId}) : false;
const isOwner = guild?.ownerId === currentUserId;
const {canManageTarget} = useRoleHierarchy(guild);
const canKick = !isCurrentUser && canKickMembers && member && canManageTarget(user.id);
const canBan = !isCurrentUser && canBanMembers && member && canManageTarget(user.id);
const canTransfer = !isCurrentUser && isOwner && member;
const canManageNicknames =
member &&
((isCurrentUser && hasChangeNicknamePermission) || (hasManageNicknamesPermission && canManageTarget(user.id)));
const handleCopyFluxerTag = () => {
TextCopyActionCreators.copy(i18n, `${user.username}#${user.discriminator}`, true);
onClose();
};
const handleCopyUserId = () => {
TextCopyActionCreators.copy(i18n, user.id, true);
onClose();
};
const handleRemoveFriend = () => {
onClose();
ModalActionCreators.push(
modal(() => (
<ConfirmModal
title={t`Remove Friend`}
description={t`Are you sure you want to remove ${user.username} as a friend?`}
primaryText={t`Remove Friend`}
primaryVariant="danger-primary"
onPrimary={async () => {
RelationshipActionCreators.removeRelationship(user.id);
}}
/>
)),
);
};
const handleBlockUser = () => {
onClose();
ModalActionCreators.push(
modal(() => (
<ConfirmModal
title={t`Block User`}
description={t`Are you sure you want to block ${user.username}? They won't be able to message you or send you friend requests.`}
primaryText={t`Block`}
primaryVariant="danger-primary"
onPrimary={async () => {
RelationshipActionCreators.blockUser(user.id);
}}
/>
)),
);
};
const handleUnblockUser = () => {
onClose();
ModalActionCreators.push(
modal(() => (
<ConfirmModal
title={t`Unblock User`}
description={t`Are you sure you want to unblock ${user.username}?`}
primaryText={t`Unblock`}
primaryVariant="primary"
onPrimary={async () => {
RelationshipActionCreators.removeRelationship(user.id);
}}
/>
)),
);
};
const handleChangeNickname = () => {
if (!guildId || !member) return;
onClose();
ModalActionCreators.push(modal(() => <ChangeNicknameModal guildId={guildId} user={user} member={member} />));
};
const handleKickMember = () => {
if (!guildId) return;
onClose();
ModalActionCreators.push(modal(() => <KickMemberModal guildId={guildId} targetUser={user} />));
};
const handleBanMember = () => {
if (!guildId) return;
onClose();
ModalActionCreators.push(modal(() => <BanMemberModal guildId={guildId} targetUser={user} />));
};
const handleTransferOwnership = () => {
if (!guildId || !member) return;
onClose();
ModalActionCreators.push(
modal(() => <TransferOwnershipModal guildId={guildId} targetUser={user} targetMember={member} />),
);
};
const menuGroups: Array<MenuGroupType> = [];
if (hasGuildProfile && onToggleProfileView) {
menuGroups.push({
items: [
{
icon: <GlobeIcon className={styles.icon} />,
label: showGlobalProfile ? t`View Community Profile` : t`View Global Profile`,
onClick: () => {
onToggleProfileView();
onClose();
},
},
],
});
}
menuGroups.push({
items: [
{
icon: <CopyIcon className={styles.icon} />,
label: t`Copy FluxerTag`,
onClick: handleCopyFluxerTag,
},
{
icon: <IdentificationCardIcon className={styles.icon} />,
label: t`Copy User ID`,
onClick: handleCopyUserId,
},
],
});
if (guildId && member) {
const guildItems = [];
if (canManageNicknames) {
guildItems.push({
icon: <PencilIcon className={styles.icon} />,
label: isCurrentUser ? t`Change Nickname` : t`Change Nickname`,
onClick: handleChangeNickname,
});
}
if (guildItems.length > 0) {
menuGroups.push({items: guildItems});
}
if (canTransfer) {
menuGroups.push({
items: [
{
icon: <CrownIcon className={styles.icon} />,
label: t`Transfer Ownership`,
onClick: handleTransferOwnership,
danger: true,
},
],
});
}
if (canKick || canBan) {
const moderationItems = [];
if (canKick) {
moderationItems.push({
icon: <SignOutIcon className={styles.icon} />,
label: t`Kick`,
onClick: handleKickMember,
danger: true,
});
}
if (canBan) {
moderationItems.push({
icon: <GavelIcon className={styles.icon} />,
label: t`Ban`,
onClick: handleBanMember,
danger: true,
});
}
menuGroups.push({items: moderationItems});
}
}
if (!isCurrentUser) {
if (relationshipType === RelationshipTypes.FRIEND) {
menuGroups.push({
items: [
{
icon: <UserMinusIcon className={styles.icon} />,
label: t`Remove Friend`,
onClick: handleRemoveFriend,
danger: true,
},
],
});
}
const reportBlockItems = [
{
icon: <FlagIcon className={styles.icon} />,
label: t`Report User`,
onClick: () => {},
danger: true,
},
];
if (relationshipType !== RelationshipTypes.BLOCKED) {
reportBlockItems.push({
icon: <ProhibitIcon className={styles.icon} />,
label: t`Block`,
onClick: handleBlockUser,
danger: true,
});
} else {
reportBlockItems.push({
icon: <ProhibitIcon className={styles.icon} />,
label: t`Unblock`,
onClick: handleUnblockUser,
danger: false,
});
}
menuGroups.push({items: reportBlockItems});
}
return <MenuBottomSheet isOpen={isOpen} onClose={onClose} groups={menuGroups} />;
},
);