/* * 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 {Plural, Trans, useLingui} from '@lingui/react/macro'; import { CaretDownIcon, ChatTeardropIcon, CheckCircleIcon, ClockCounterClockwiseIcon, DotsThreeIcon, PencilIcon, ProhibitIcon, UserMinusIcon, UserPlusIcon, UsersThreeIcon, } from '@phosphor-icons/react'; import {clsx} from 'clsx'; import {autorun} from 'mobx'; import {observer} from 'mobx-react-lite'; import React, {useId} from 'react'; import type {PressEvent} from 'react-aria-components'; import * as ContextMenuActionCreators from '~/actions/ContextMenuActionCreators'; import * as ModalActionCreators from '~/actions/ModalActionCreators'; import {modal} from '~/actions/ModalActionCreators'; import * as NavigationActionCreators from '~/actions/NavigationActionCreators'; import * as PrivateChannelActionCreators from '~/actions/PrivateChannelActionCreators'; import * as TextCopyActionCreators from '~/actions/TextCopyActionCreators'; import * as UserNoteActionCreators from '~/actions/UserNoteActionCreators'; import * as UserProfileActionCreators from '~/actions/UserProfileActionCreators'; import {DEFAULT_ACCENT_COLOR, Permissions, RelationshipTypes} from '~/Constants'; import {UserTag} from '~/components/channel/UserTag'; import {CustomStatusDisplay} from '~/components/common/CustomStatusDisplay/CustomStatusDisplay'; import {GroupDMAvatar} from '~/components/common/GroupDMAvatar'; import {CustomStatusModal} from '~/components/modals/CustomStatusModal'; import type {IARContext} from '~/components/modals/IARModal'; import {IARModal} from '~/components/modals/IARModal'; import * as Modal from '~/components/modals/Modal'; import {UserSettingsModal} from '~/components/modals/UserSettingsModal'; import {GuildIcon} from '~/components/popouts/GuildIcon'; import {UserProfileBadges} from '~/components/popouts/UserProfileBadges'; import {UserProfileDataWarning} from '~/components/popouts/UserProfileDataWarning'; import {UserProfileBio, UserProfileMembershipInfo, UserProfileRoles} from '~/components/popouts/UserProfileShared'; import {Button} from '~/components/uikit/Button/Button'; import { BlockUserIcon, CopyFluxerTagIcon, CopyUserIdIcon, ReportUserIcon, VideoCallIcon, ViewGlobalProfileIcon, VoiceCallIcon, } from '~/components/uikit/ContextMenu/ContextMenuIcons'; import {GroupDMContextMenu} from '~/components/uikit/ContextMenu/GroupDMContextMenu'; import {GuildContextMenu} from '~/components/uikit/ContextMenu/GuildContextMenu'; import {GuildMemberContextMenu} from '~/components/uikit/ContextMenu/GuildMemberContextMenu'; import {MenuGroup} from '~/components/uikit/ContextMenu/MenuGroup'; import {MenuItem} from '~/components/uikit/ContextMenu/MenuItem'; import {MenuItemRadio} from '~/components/uikit/ContextMenu/MenuItemRadio'; import {UserContextMenu} from '~/components/uikit/ContextMenu/UserContextMenu'; import {Scroller} from '~/components/uikit/Scroller'; import {Spinner} from '~/components/uikit/Spinner'; import {StatusAwareAvatar} from '~/components/uikit/StatusAwareAvatar'; import {Tabs} from '~/components/uikit/Tabs/Tabs'; import {Tooltip} from '~/components/uikit/Tooltip/Tooltip'; import {useAutoplayExpandedProfileAnimations} from '~/hooks/useAutoplayExpandedProfileAnimations'; import {TextareaAutosize} from '~/lib/TextareaAutosize'; import {Routes} from '~/Routes'; import type {ChannelRecord} from '~/records/ChannelRecord'; import type {GuildRecord} from '~/records/GuildRecord'; import type {ProfileRecord} from '~/records/ProfileRecord'; import {type UserPartial, UserRecord} from '~/records/UserRecord'; import AuthenticationStore from '~/stores/AuthenticationStore'; import ChannelStore from '~/stores/ChannelStore'; import type {ContextMenuTargetElement} from '~/stores/ContextMenuStore'; import ContextMenuStore, {isContextMenuNodeTarget} from '~/stores/ContextMenuStore'; import DeveloperOptionsStore from '~/stores/DeveloperOptionsStore'; import GuildMemberStore from '~/stores/GuildMemberStore'; import MemberPresenceSubscriptionStore from '~/stores/MemberPresenceSubscriptionStore'; import ModalStore from '~/stores/ModalStore'; import PermissionStore from '~/stores/PermissionStore'; import RelationshipStore from '~/stores/RelationshipStore'; import SelectedChannelStore from '~/stores/SelectedChannelStore'; import UserNoteStore from '~/stores/UserNoteStore'; import UserProfileStore from '~/stores/UserProfileStore'; import UserStore from '~/stores/UserStore'; import * as CallUtils from '~/utils/CallUtils'; import * as ChannelUtils from '~/utils/ChannelUtils'; import * as ColorUtils from '~/utils/ColorUtils'; import * as NicknameUtils from '~/utils/NicknameUtils'; import * as ProfileDisplayUtils from '~/utils/ProfileDisplayUtils'; import {createMockProfile} from '~/utils/ProfileUtils'; import * as RelationshipActionUtils from '~/utils/RelationshipActionUtils'; import * as RouterUtils from '~/utils/RouterUtils'; import modalRootStyles from './Modal.module.css'; import userProfileModalStyles from './UserProfileModal.module.css'; export interface UserProfileModalProps { userId: string; guildId?: string; autoFocusNote?: boolean; disableEditProfile?: boolean; previewOverrides?: ProfileDisplayUtils.ProfilePreviewOverrides; previewUser?: UserRecord; } interface UserInfoProps { user: UserRecord; profile: ProfileRecord; guildId?: string; warningIndicator?: React.ReactNode; isCurrentUser?: boolean; onEditCustomStatus?: () => void; } interface UserNoteEditorProps { userId: string; initialNote: string | null; autoFocus?: boolean; noteRef?: React.RefObject; } interface ProfileContentProps { profile: ProfileRecord; user: UserRecord; userNote: string | null; autoFocusNote?: boolean; noteRef?: React.RefObject; } type UserProfileModalComponent = React.FC; interface ProfileModalContentProps { profile: ProfileRecord; user: UserRecord; userNote: string | null; autoFocusNote?: boolean; noteRef?: React.RefObject; renderActionButtons: () => React.ReactNode; previewOverrides?: ProfileDisplayUtils.ProfilePreviewOverrides; warningIndicator?: React.ReactNode; } const UserInfo: React.FC = observer( ({user, profile, guildId, warningIndicator, isCurrentUser, onEditCustomStatus}) => { const displayName = NicknameUtils.getNickname(user, guildId); const effectiveProfile = profile?.getEffectiveProfile() ?? null; const shouldAutoplayProfileAnimations = useAutoplayExpandedProfileAnimations(); return (
{displayName} {user.bot && }
{user.tag}
{effectiveProfile?.pronouns && (
{effectiveProfile.pronouns}
)}
); }, ); const UserNoteEditor: React.FC = observer(({userId, initialNote, autoFocus, noteRef}) => { const {t} = useLingui(); const [isEditing, setIsEditing] = React.useState(false); const [localNote, setLocalNote] = React.useState(null); const internalNoteRef = React.useRef(null); const textareaRef = noteRef || internalNoteRef; React.useEffect(() => { if (autoFocus && textareaRef.current) { setIsEditing(true); } }, [autoFocus, textareaRef]); const handleBlur = () => { if (localNote != null && localNote !== initialNote) { UserNoteActionCreators.update(userId, localNote); } setIsEditing(false); }; const handleFocus = () => { setIsEditing(true); if (textareaRef.current) { const length = textareaRef.current.value.length; textareaRef.current.setSelectionRange(length, length); } }; return (
Note setLocalNote(event.target.value)} onFocus={handleFocus} placeholder={isEditing ? undefined : t`Click to add a note`} />
); }); const ProfileContent: React.FC = observer(({profile, user, userNote, autoFocusNote, noteRef}) => { const guildMember = GuildMemberStore.getMember(profile?.guildId ?? '', user.id); const memberRoles = profile?.guildId && guildMember ? guildMember.getSortedRoles() : []; const canManageRoles = PermissionStore.can(Permissions.MANAGE_ROLES, {guildId: profile?.guild?.id}); return (
); }); const ProfileModalContent: React.FC = observer( ({profile, user, userNote, autoFocusNote, noteRef, renderActionButtons, previewOverrides, warningIndicator}) => { const {t} = useLingui(); const effectiveProfile = profile?.getEffectiveProfile() ?? null; const rawAccentColor = effectiveProfile?.accent_color; const accentColorHex = typeof rawAccentColor === 'number' ? ColorUtils.int2hex(rawAccentColor) : rawAccentColor; const bannerColor = accentColorHex || DEFAULT_ACCENT_COLOR; const guildMember = GuildMemberStore.getMember(profile?.guildId ?? '', user.id); const profileContext = React.useMemo( () => ({ user, profile, guildId: profile?.guildId, guildMember, guildMemberProfile: profile?.guildMemberProfile, }), [user, profile, guildMember], ); const shouldAutoplayProfileAnimations = useAutoplayExpandedProfileAnimations(); const {avatarUrl, hoverAvatarUrl} = React.useMemo( () => ProfileDisplayUtils.getProfileAvatarUrls(profileContext, previewOverrides), [profileContext, previewOverrides], ); const bannerUrl = React.useMemo( () => ProfileDisplayUtils.getProfileBannerUrl(profileContext, previewOverrides, shouldAutoplayProfileAnimations), [profileContext, previewOverrides, shouldAutoplayProfileAnimations], ); type MutualView = 'mutual_friends' | 'mutual_communities' | 'mutual_groups'; const [activeTab, setActiveTab] = React.useState<'overview' | 'mutual'>('overview'); const handleTabChange = React.useCallback((tab: 'overview' | 'mutual') => { setActiveTab(tab); }, []); const showMutualFriendsTab = !user.bot; const mutualFriendsCount = profile?.mutualFriends?.length ?? 0; const isCurrentUser = user.id === AuthenticationStore.currentUserId; const openCustomStatus = React.useCallback(() => { ModalActionCreators.push(modal(() => )); }, []); const mutualGuilds = React.useMemo(() => GuildMemberStore.getMutualGuilds(user.id), [user.id]); const mutualGroups = ChannelStore.dmChannels.filter( (channel) => channel.isGroupDM() && channel.recipientIds.includes(user.id), ); const [mutualView, setMutualView] = React.useState( showMutualFriendsTab ? 'mutual_friends' : 'mutual_communities', ); const mutualMenuButtonRef = React.useRef(null); const [isMutualMenuOpen, setIsMutualMenuOpen] = React.useState(false); const getMutualViewLabel = React.useCallback( (view: MutualView) => { switch (view) { case 'mutual_friends': { const count = mutualFriendsCount; return t`Mutual Friends (${count})`; } case 'mutual_groups': { const count = mutualGroups.length; return t`Mutual Groups (${count})`; } default: { const count = mutualGuilds.length; return t`Mutual Communities (${count})`; } } }, [t, mutualFriendsCount, mutualGroups.length, mutualGuilds.length], ); const openMutualMenu = React.useCallback( (event: React.MouseEvent) => { const contextMenu = ContextMenuStore.contextMenu; const isOpen = !!contextMenu && contextMenu.target.target === event.currentTarget; if (isOpen) { return; } setActiveTab('mutual'); ContextMenuActionCreators.openFromEvent(event, () => ( {showMutualFriendsTab && ( setMutualView('mutual_friends')} > {getMutualViewLabel('mutual_friends')} )} setMutualView('mutual_communities')} > {getMutualViewLabel('mutual_communities')} setMutualView('mutual_groups')} > {getMutualViewLabel('mutual_groups')} )); }, [getMutualViewLabel, mutualView, showMutualFriendsTab], ); const mutualTabLabelText = React.useMemo(() => getMutualViewLabel(mutualView), [getMutualViewLabel, mutualView]); const tabs = React.useMemo( () => [ {key: 'overview', label: t`Overview`}, {key: 'mutual', label: mutualTabLabelText}, ] as Array<{key: 'overview' | 'mutual'; label: React.ReactNode}>, [t, mutualTabLabelText], ); React.useEffect(() => { setMutualView(showMutualFriendsTab ? 'mutual_friends' : 'mutual_communities'); }, [showMutualFriendsTab, user.id]); React.useEffect(() => { if (!showMutualFriendsTab && mutualView === 'mutual_friends') { setMutualView('mutual_communities'); } }, [mutualView, showMutualFriendsTab]); const handleMutualFriendClick = (friendId: string) => { const currentModal = ModalStore.getModal(); if (currentModal) { ModalActionCreators.update(currentModal.key, () => modal(() => ), ); } }; const handleMutualFriendContextMenu = (event: React.MouseEvent, friend: UserRecord) => { event.preventDefault(); event.stopPropagation(); ContextMenuActionCreators.openFromEvent(event, ({onClose}) => ( <> {profile?.guildId ? ( ) : ( )} )); }; const handleGuildClick = (guild: GuildRecord) => { ModalActionCreators.pop(); const selectedChannel = SelectedChannelStore.selectedChannelIds.get(guild.id); RouterUtils.transitionTo(Routes.guildChannel(guild.id, selectedChannel)); NavigationActionCreators.selectGuild(guild.id); }; const handleGuildContextMenu = (event: React.MouseEvent, guild: GuildRecord) => { event.preventDefault(); event.stopPropagation(); ContextMenuActionCreators.openFromEvent(event, (props) => ( )); }; const handleGroupClick = (group: ChannelRecord) => { ModalActionCreators.pop(); RouterUtils.transitionTo(Routes.dmChannel(group.id)); }; const handleGroupContextMenu = (event: React.MouseEvent, group: ChannelRecord) => { event.preventDefault(); event.stopPropagation(); ContextMenuActionCreators.openFromEvent(event, ({onClose}) => ( )); }; const [contextMenuTarget, setContextMenuTarget] = React.useState(null); React.useEffect(() => { const disposer = autorun(() => { const contextMenu = ContextMenuStore.contextMenu; setContextMenuTarget(contextMenu?.target.target ?? null); }); return () => { disposer(); }; }, []); const isContextMenuOpenFor = (target: EventTarget | null) => { if (!contextMenuTarget || !target) { return false; } if (target === contextMenuTarget) { return true; } if (target instanceof Node && isContextMenuNodeTarget(contextMenuTarget)) { return target.contains(contextMenuTarget); } return false; }; React.useEffect(() => { const disposer = autorun(() => { const contextMenu = ContextMenuStore.contextMenu; const isOpen = !!contextMenu && !!mutualMenuButtonRef.current && contextMenu.target.target === mutualMenuButtonRef.current; setIsMutualMenuOpen(isOpen); }); return () => { disposer(); }; }, []); const renderMutualFriendsList = React.useCallback(() => { const friends = profile?.mutualFriends ?? []; return (
{friends.map((friend: UserPartial) => { const friendRecord = new UserRecord(friend); return ( handleMutualFriendClick(friendRecord.id)} onContextMenu={(e) => handleMutualFriendContextMenu(e, friendRecord)} isContextMenuOpen={isContextMenuOpenFor} /> ); })} {friends.length === 0 && (
No mutual friends found.
)}
); }, [handleMutualFriendClick, profile, isContextMenuOpenFor]); const renderMutualGroupsList = React.useCallback(() => { return (
{mutualGroups.map((group) => ( handleGroupClick(group)} onContextMenu={(e) => handleGroupContextMenu(e, group)} isContextMenuOpen={isContextMenuOpenFor} /> ))} {mutualGroups.length === 0 && (
No mutual groups found.
)}
); }, [handleGroupClick, handleGroupContextMenu, isContextMenuOpenFor, mutualGroups]); const renderMutualGuildsList = React.useCallback(() => { return (
{mutualGuilds.map((guild) => ( handleGuildClick(guild)} onContextMenu={(e) => handleGuildContextMenu(e, guild)} isContextMenuOpen={isContextMenuOpenFor} /> ))} {mutualGuilds.length === 0 && (
No mutual communities found.
)}
); }, [handleGuildClick, handleGuildContextMenu, isContextMenuOpenFor, mutualGuilds]); const renderMutualTabContent = React.useCallback(() => { switch (mutualView) { case 'mutual_friends': return showMutualFriendsTab ? renderMutualFriendsList() : renderMutualGuildsList(); case 'mutual_groups': return renderMutualGroupsList(); default: return renderMutualGuildsList(); } }, [mutualView, renderMutualFriendsList, renderMutualGroupsList, renderMutualGuildsList, showMutualFriendsTab]); const renderActiveTabContent = React.useCallback(() => { switch (activeTab) { case 'overview': return ( ); default: return renderMutualTabContent(); } }, [activeTab, autoFocusNote, noteRef, profile, renderMutualTabContent, user, userNote]); const reactId = useId(); const safeId = reactId.replace(/[^a-zA-Z0-9_-]/g, ''); const maskId = `uid_${safeId}`; return ( <>
{/* biome-ignore lint/a11y/noSvgWithoutTitle: this is fine */} {bannerUrl ? (
) : (
)}
{renderActionButtons()}
{!isCurrentUser ? (
tab === 'mutual' ? ( ) : null } />
) : (
)}
{renderActiveTabContent()}
); }, ); const MutualFriendItem = ({ user, profile, onClick, onContextMenu, isContextMenuOpen, }: { user: UserRecord; profile: ProfileRecord | null; onClick: () => void; onContextMenu: (e: React.MouseEvent) => void; isContextMenuOpen: (target: EventTarget | null) => boolean; }) => { const itemRef = React.useRef(null); const isActive = isContextMenuOpen(itemRef.current); return (
(e.key === 'Enter' || e.key === ' ') && onClick()} onContextMenu={onContextMenu} role="button" tabIndex={0} >
{NicknameUtils.getNickname(user, profile?.guildId ?? undefined)} {user.tag}
); }; const MutualGuildItem = ({ guild, onClick, onContextMenu, isContextMenuOpen, }: { guild: GuildRecord; onClick: () => void; onContextMenu: (e: React.MouseEvent) => void; isContextMenuOpen: (target: EventTarget | null) => boolean; }) => { const itemRef = React.useRef(null); const isActive = isContextMenuOpen(itemRef.current); return (
(e.key === 'Enter' || e.key === ' ') && onClick()} onContextMenu={onContextMenu} role="button" tabIndex={0} >
{guild.name}
); }; const MutualGroupItem = ({ group, onClick, onContextMenu, isContextMenuOpen, }: { group: ChannelRecord; onClick: () => void; onContextMenu: (e: React.MouseEvent) => void; isContextMenuOpen: (target: EventTarget | null) => boolean; }) => { const itemRef = React.useRef(null); const isActive = isContextMenuOpen(itemRef.current); return (
(e.key === 'Enter' || e.key === ' ') && onClick()} onContextMenu={onContextMenu} role="button" tabIndex={0} >
{ChannelUtils.getDMDisplayName(group)}
); }; export const UserProfileModal: UserProfileModalComponent = observer( ({userId, guildId, autoFocusNote, disableEditProfile, previewOverrides, previewUser}) => { const {t, i18n} = useLingui(); const storeUser = UserStore.getUser(userId); const user = previewUser ?? storeUser; const fallbackUser = React.useMemo( () => new UserRecord({ id: userId, username: userId, discriminator: '0000', avatar: null, flags: 0, }), [userId], ); const displayUser = user ?? fallbackUser; const fallbackProfile = React.useMemo(() => createMockProfile(fallbackUser), [fallbackUser]); const mockProfile = React.useMemo(() => (user ? createMockProfile(user) : null), [user]); const initialProfile = React.useMemo(() => UserProfileStore.getProfile(userId, guildId), [userId, guildId]); const [profile, setProfile] = React.useState(initialProfile); const [profileLoadError, setProfileLoadError] = React.useState(false); const [showGlobalProfile, setShowGlobalProfile] = React.useState(false); const [isProfileLoading, setIsProfileLoading] = React.useState(() => !previewUser && !initialProfile); const userNote = UserNoteStore.getUserNote(userId); const isCurrentUser = user?.id === AuthenticationStore.currentUserId; const relationship = RelationshipStore.getRelationship(userId); const relationshipType = relationship?.type; const isUserBot = user?.bot ?? false; const noteRef = React.useRef(null); const moreOptionsButtonRef = React.useRef(null); const [isMoreMenuOpen, setIsMoreMenuOpen] = React.useState(false); React.useEffect(() => { setProfile(initialProfile); setIsProfileLoading(!previewUser && !initialProfile); }, [initialProfile, previewUser]); React.useEffect(() => { if (previewUser || profile) { setIsProfileLoading(false); setProfileLoadError(false); return; } let cancelled = false; setIsProfileLoading(true); setProfileLoadError(false); UserProfileActionCreators.fetch(userId, guildId) .then(() => { if (cancelled) return; const fetchedProfile = UserProfileStore.getProfile(userId, guildId); if (fetchedProfile) { setProfile(fetchedProfile); } setProfileLoadError(false); }) .catch((error) => { if (cancelled) return; console.error('Failed to fetch user profile:', error); setProfileLoadError(true); }) .finally(() => { if (cancelled) return; setIsProfileLoading(false); }); return () => { cancelled = true; }; }, [userId, guildId, previewUser, profile]); React.useEffect(() => { const handleContextMenuChange = () => { const contextMenu = ContextMenuStore.contextMenu; const isOpen = !!contextMenu && !!moreOptionsButtonRef.current && contextMenu.target.target === moreOptionsButtonRef.current; setIsMoreMenuOpen(isOpen); }; const disposer = autorun(handleContextMenuChange); return () => disposer(); }, []); React.useEffect(() => { if (!guildId || !userId || previewUser) { return; } const hasMember = GuildMemberStore.getMember(guildId, userId); if (!hasMember) { MemberPresenceSubscriptionStore.touchMember(guildId, userId); GuildMemberStore.fetchMembers(guildId, {userIds: [userId]}); } else { MemberPresenceSubscriptionStore.touchMember(guildId, userId); } return () => { MemberPresenceSubscriptionStore.unsubscribe(guildId, userId); }; }, [guildId, userId, previewUser]); const hasGuildProfile = !!(profile?.guildId && profile?.guildMemberProfile); const shouldShowProfileDataWarning = profileLoadError || DeveloperOptionsStore.forceProfileDataWarning; const displayProfile = React.useMemo((): ProfileRecord | null => { if (!profile) return null; if (showGlobalProfile && hasGuildProfile) { return profile.withUpdates({guild_member_profile: null}).withGuildId(null); } return profile; }, [profile, showGlobalProfile, hasGuildProfile]); const screenReaderLabel = React.useMemo(() => { if (!displayUser) return t`User Profile`; const tag = displayUser.tag; return t`User Profile: ${tag}`; }, [displayUser, t]); const shouldShowSpinner = isProfileLoading || !user; const effectiveProfile: ProfileRecord | null = displayProfile ?? profile ?? mockProfile; const resolvedProfile: ProfileRecord = effectiveProfile ?? fallbackProfile; const handleEditProfile = () => { ModalActionCreators.pop(); ModalActionCreators.push(modal(() => )); }; const handleMessage = async () => { try { ModalActionCreators.pop(); await PrivateChannelActionCreators.openDMChannel(userId); } catch (error) { console.error('Failed to open DM channel:', error); } }; const handleSendFriendRequest = () => { RelationshipActionUtils.sendFriendRequest(i18n, userId); }; const handleAcceptFriendRequest = () => { RelationshipActionUtils.acceptFriendRequest(i18n, userId); }; const handleRemoveFriend = () => { RelationshipActionUtils.showRemoveFriendConfirmation(i18n, displayUser); }; const handleBlockUser = () => { RelationshipActionUtils.showBlockUserConfirmation(i18n, displayUser); }; const handleUnblockUser = () => { RelationshipActionUtils.showUnblockUserConfirmation(i18n, displayUser); }; const handleCancelFriendRequest = () => { RelationshipActionUtils.cancelFriendRequest(i18n, userId); }; const handleStartVoiceCall = async (event?: PressEvent) => { try { const channelId = await PrivateChannelActionCreators.ensureDMChannel(userId); await CallUtils.checkAndStartCall(channelId, event?.shiftKey ?? false); } catch (error) { console.error('Failed to start voice call:', error); } }; const handleStartVideoCall = async (event?: PressEvent) => { try { const channelId = await PrivateChannelActionCreators.ensureDMChannel(userId); await CallUtils.checkAndStartCall(channelId, event?.shiftKey ?? false); } catch (error) { console.error('Failed to start video call:', error); } }; const handleReportUser = () => { const context: IARContext = { type: 'user', user: displayUser, guildId, }; ModalActionCreators.push(modal(() => )); }; const handleCopyFluxerTag = () => { TextCopyActionCreators.copy(i18n, `${displayUser.username}#${displayUser.discriminator}`, true); }; const handleCopyUserId = () => { TextCopyActionCreators.copy(i18n, displayUser.id, true); }; const handleMoreOptionsPointerDown = (event: React.PointerEvent) => { const contextMenu = ContextMenuStore.contextMenu; const isOpen = !!contextMenu && contextMenu.target.target === moreOptionsButtonRef.current; if (isOpen) { event.stopPropagation(); event.preventDefault(); ContextMenuActionCreators.close(); } }; const renderBlockMenuItem = (onClose: () => void) => { switch (relationshipType) { case RelationshipTypes.BLOCKED: return ( } onClick={() => { handleUnblockUser(); onClose(); }} > {t`Unblock`} ); default: return ( } onClick={() => { handleBlockUser(); onClose(); }} danger > {t`Block`} ); } }; const openMoreOptionsMenu = (event: React.MouseEvent) => { const contextMenu = ContextMenuStore.contextMenu; const isOpen = !!contextMenu && contextMenu.target.target === event.currentTarget; if (isOpen) { return; } ContextMenuActionCreators.openFromEvent(event, (props) => ( <> {hasGuildProfile && ( } onClick={() => { setShowGlobalProfile(!showGlobalProfile); props.onClose(); }} > {showGlobalProfile ? t`View Community Profile` : t`View Global Profile`} )} {!isCurrentUser && !isUserBot && relationshipType === RelationshipTypes.FRIEND && ( } onClick={(pressEvent: PressEvent) => { handleStartVoiceCall(pressEvent); props.onClose(); }} > {t`Start Voice Call`} } onClick={(pressEvent: PressEvent) => { handleStartVideoCall(pressEvent); props.onClose(); }} > {t`Start Video Call`} )} } onClick={() => { handleCopyFluxerTag(); props.onClose(); }} > {t`Copy FluxerTag`} } onClick={() => { handleCopyUserId(); props.onClose(); }} > {t`Copy User ID`} {!isCurrentUser && relationshipType === RelationshipTypes.FRIEND && ( } onClick={() => { handleRemoveFriend(); props.onClose(); }} danger > {t`Remove Friend`} )} {!isCurrentUser && ( } onClick={() => { handleReportUser(); props.onClose(); }} danger > {t`Report User`} {renderBlockMenuItem(props.onClose)} )} )); }; const renderActionButtons = () => { const currentUserUnclaimed = !(UserStore.currentUser?.isClaimed() ?? true); if (isCurrentUser && disableEditProfile) { return (
); } if (isCurrentUser && !disableEditProfile) { return (
); } const renderPrimaryActionButton = () => { if (isUserBot) { return null; } if (relationshipType === RelationshipTypes.FRIEND) { return (
); } if (relationshipType === RelationshipTypes.BLOCKED) { return (
); } if (relationshipType === RelationshipTypes.INCOMING_REQUEST) { return (
); } if (relationshipType === RelationshipTypes.OUTGOING_REQUEST) { return (
); } if (relationshipType === undefined && !isUserBot) { const tooltipText = currentUserUnclaimed ? t`Claim your account to send friend requests.` : t`Send Friend Request`; return (
); } return null; }; return (
{renderPrimaryActionButton()}
); }; const borderProfile = displayProfile?.getEffectiveProfile() ?? null; const rawAccentColor = borderProfile?.accent_color; const accentColorHex = typeof rawAccentColor === 'number' ? ColorUtils.int2hex(rawAccentColor) : rawAccentColor; const borderColor = accentColorHex || DEFAULT_ACCENT_COLOR; return (
{shouldShowSpinner ? (
) : ( : undefined} previewOverrides={previewOverrides} /> )}
); }, );