/*
* 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 {CrownIcon, PencilSimpleIcon, UserMinusIcon, XIcon} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
import React from 'react';
import * as ChannelActionCreators from '~/actions/ChannelActionCreators';
import * as ModalActionCreators from '~/actions/ModalActionCreators';
import {modal} from '~/actions/ModalActionCreators';
import * as PrivateChannelActionCreators from '~/actions/PrivateChannelActionCreators';
import * as ToastActionCreators from '~/actions/ToastActionCreators';
import {ME, Permissions, RelationshipTypes} from '~/Constants';
import {DMCloseFailedModal} from '~/components/alerts/DMCloseFailedModal';
import {ChangeGroupDMNicknameModal} from '~/components/modals/ChangeGroupDMNicknameModal';
import {ConfirmModal} from '~/components/modals/ConfirmModal';
import {Routes} from '~/Routes';
import type {UserRecord} from '~/records/UserRecord';
import AuthenticationStore from '~/stores/AuthenticationStore';
import CallStateStore from '~/stores/CallStateStore';
import ChannelStore from '~/stores/ChannelStore';
import PermissionStore from '~/stores/PermissionStore';
import RelationshipStore from '~/stores/RelationshipStore';
import SelectedChannelStore from '~/stores/SelectedChannelStore';
import UserSettingsStore from '~/stores/UserSettingsStore';
import UserStore from '~/stores/UserStore';
import * as RouterUtils from '~/utils/RouterUtils';
import {RingUserMenuItem, StartVoiceCallMenuItem} from './items/CallMenuItems';
import {FavoriteChannelMenuItem} from './items/ChannelMenuItems';
import {CopyUserIdMenuItem} from './items/CopyMenuItems';
import {DebugUserMenuItem} from './items/DebugMenuItems';
import {MarkDMAsReadMenuItem} from './items/DMMenuItems';
import {InviteToCommunityMenuItem} from './items/InviteMenuItems';
import {MentionUserMenuItem} from './items/MentionUserMenuItem';
import {MessageUserMenuItem} from './items/MessageUserMenuItem';
import {
BlockUserMenuItem,
ChangeFriendNicknameMenuItem,
RelationshipActionMenuItem,
UnblockUserMenuItem,
} from './items/RelationshipMenuItems';
import {AddNoteMenuItem} from './items/UserNoteMenuItems';
import {UserProfileMenuItem} from './items/UserProfileMenuItem';
import {LocalMuteParticipantMenuItem, ParticipantVolumeSlider} from './items/VoiceParticipantMenuItems';
import {MenuGroup} from './MenuGroup';
import {MenuItem} from './MenuItem';
interface UserContextMenuProps {
user: UserRecord;
onClose: () => void;
guildId?: string;
channelId?: string;
isCallContext?: boolean;
}
export const UserContextMenu: React.FC = observer(
({user, onClose, guildId, channelId, isCallContext = false}) => {
const {t} = useLingui();
const channel = channelId ? ChannelStore.getChannel(channelId) : null;
const canSendMessages = channel
? channel.isPrivate() || PermissionStore.can(Permissions.SEND_MESSAGES, {channelId, guildId})
: true;
const canMention = channel !== null && canSendMessages;
const isCurrentUser = user.id === AuthenticationStore.currentUserId;
const relationship = RelationshipStore.getRelationship(user.id);
const relationshipType = relationship?.type;
const developerMode = UserSettingsStore.developerMode;
const currentUserId = AuthenticationStore.currentUserId;
const dmPartnerId = channel?.isDM()
? (channel.recipientIds.find((id) => id !== currentUserId) ?? channel.recipientIds[0])
: null;
const dmPartner = dmPartnerId ? UserStore.getUser(dmPartnerId) : null;
const isGroupDM = channel?.isGroupDM();
const isOwner = channel?.ownerId === currentUserId;
const isRecipient = channel?.recipientIds.includes(user.id);
const isBot = user.bot;
const call = channelId ? CallStateStore.getCall(channelId) : null;
const showCallItems = isCallContext && call && !isCurrentUser;
const handleChangeGroupNickname = React.useCallback(() => {
if (!channel) return;
onClose();
ModalActionCreators.push(modal(() => ));
}, [channel, onClose, user]);
const handleRemoveFromGroup = React.useCallback(() => {
if (!channel) return;
onClose();
ModalActionCreators.push(
modal(() => (
PrivateChannelActionCreators.removeRecipient(channel.id, user.id)}
/>
)),
);
}, [channel, onClose, t, user.id, user.username]);
const handleMakeGroupOwner = React.useCallback(() => {
if (!channel) return;
onClose();
ModalActionCreators.push(
modal(() => (
{
ChannelActionCreators.update(channel.id, {owner_id: user.id});
}}
/>
)),
);
}, [channel, onClose, t, user.id, user.username]);
const handleCloseDM = React.useCallback(() => {
if (!channel || !channel.isDM()) return;
onClose();
const displayName = dmPartner?.username ?? user.username;
ModalActionCreators.push(
modal(() => (
{
try {
await ChannelActionCreators.remove(channel.id);
const selectedChannel = SelectedChannelStore.selectedChannelIds.get(ME);
if (selectedChannel === channel.id) {
RouterUtils.transitionTo(Routes.ME);
}
ToastActionCreators.createToast({
type: 'success',
children: t`DM closed`,
});
} catch (error) {
console.error('Failed to close DM:', error);
ModalActionCreators.push(modal(() => ));
}
}}
/>
)),
);
}, [channel, dmPartner?.username, onClose, t, user.username]);
const renderDmSelfMenu = () => {
if (!channel) return renderDefaultMenu();
return (
<>
} onClick={handleCloseDM}>
{t`Close DM`}
{developerMode && }
>
);
};
const renderDmOtherMenu = () => {
if (!channel) return renderDefaultMenu();
return (
<>
{showCallItems && channelId && (
)}
{!isBot && }
} onClick={handleCloseDM}>
{t`Close DM`}
{!isBot && }
{!isBot && }
{relationshipType === RelationshipTypes.BLOCKED ? (
) : (
)}
{developerMode && (
)}
>
);
};
const renderDefaultMenu = () => (
<>
{isGroupDM && isCurrentUser && (
} onClick={handleChangeGroupNickname}>
{t`Change Group Nickname`}
)}
{canMention && }
{!isCurrentUser && }
{showCallItems && channelId && }
{!isCurrentUser && !isBot && !isCallContext && }
{!isCurrentUser && }
{showCallItems && (
)}
{isGroupDM && isOwner && isRecipient && !isCurrentUser && (
} onClick={handleRemoveFromGroup} danger>
{t`Remove from Group`}
} onClick={handleMakeGroupOwner} danger>
{t`Make Group Owner`}
} onClick={handleChangeGroupNickname}>
{t`Change Group Nickname`}
)}
{showCallItems && (
)}
{!isCurrentUser && !isBot && }
{!isCurrentUser && !isBot && }
{!isCurrentUser &&
(relationshipType === RelationshipTypes.BLOCKED ? (
) : (
))}
{developerMode && (
)}
>
);
if (channel?.isDM()) {
return isCurrentUser ? renderDmSelfMenu() : renderDmOtherMenu();
}
return renderDefaultMenu();
},
);