/* * 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 {msg} from '@lingui/core/macro'; import {clsx} from 'clsx'; import {observer} from 'mobx-react-lite'; import type React from 'react'; import * as ContextMenuActionCreators from '~/actions/ContextMenuActionCreators'; import * as ModalActionCreators from '~/actions/ModalActionCreators'; import {modal} from '~/actions/ModalActionCreators'; import {ChannelTypes, Permissions} from '~/Constants'; import {GenericErrorModal} from '~/components/alerts/GenericErrorModal'; import {PreloadableUserPopout} from '~/components/channel/PreloadableUserPopout'; import {ChannelContextMenu} from '~/components/uikit/ContextMenu/ChannelContextMenu'; import FocusRing from '~/components/uikit/FocusRing/FocusRing'; import {Routes} from '~/Routes'; import ChannelStore from '~/stores/ChannelStore'; import GuildStore from '~/stores/GuildStore'; import PermissionStore from '~/stores/PermissionStore'; import SelectedGuildStore from '~/stores/SelectedGuildStore'; import UserStore from '~/stores/UserStore'; import markupStyles from '~/styles/Markup.module.css'; import mentionRendererStyles from '~/styles/MentionRenderer.module.css'; import * as ChannelUtils from '~/utils/ChannelUtils'; import * as ColorUtils from '~/utils/ColorUtils'; import * as NicknameUtils from '~/utils/NicknameUtils'; import * as RouterUtils from '~/utils/RouterUtils'; import {GuildNavKind, MentionKind} from '../parser/types/enums'; import type {MentionNode} from '../parser/types/nodes'; import type {RendererProps} from '.'; interface UserInfo { userId: string | null; name: string | null; } function getUserInfo(userId: string, channelId?: string): UserInfo { if (!userId) { return {userId: null, name: null}; } const user = UserStore.getUser(userId); if (!user) { return {userId, name: userId}; } let name = user.displayName; if (channelId) { const channel = ChannelStore.getChannel(channelId); if (channel?.guildId) { name = NicknameUtils.getNickname(user, channel.guildId) || name; } } return {userId: user.id, name}; } export const MentionRenderer = observer(function MentionRenderer({ node, id, options, }: RendererProps): React.ReactElement { const {kind} = node; const {channelId} = options; const i18n = options.i18n!; switch (kind.kind) { case MentionKind.User: { const {userId, name} = getUserInfo(kind.id, channelId); const genericMention = ( @{name || kind.id} ); if (!userId) { return genericMention; } const user = UserStore.getUser(userId); if (!user) { return genericMention; } const channel = channelId ? ChannelStore.getChannel(channelId) : undefined; const guildId = channel?.guildId || ''; return ( e.stopPropagation()} onKeyDown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); } }} > @{name || user.displayName} ); } case MentionKind.Role: { const selectedGuildId = SelectedGuildStore.selectedGuildId; const guild = selectedGuildId != null ? GuildStore.getGuild(selectedGuildId) : null; const role = guild?.roles[kind.id]; if (!role) { return ( @{i18n._(msg`Unknown Role`)} ); } const roleColor = role.color ? ColorUtils.int2rgb(role.color) : undefined; const roleBgColor = role.color ? ColorUtils.int2rgba(role.color, 0.1) : undefined; const roleBgHoverColor = role.color ? ColorUtils.int2rgba(role.color, 0.2) : undefined; const style = { color: roleColor, backgroundColor: roleBgColor, transition: 'background-color var(--transition-fast)', '--hover-bg': roleBgHoverColor, } as React.CSSProperties; return ( @{role.name} ); } case MentionKind.Channel: { const unknownMention = ( {ChannelUtils.getIcon({type: ChannelTypes.GUILD_TEXT}, {className: mentionRendererStyles.channelIcon})} {i18n._(msg`unknown-channel`)} ); const channel = ChannelStore.getChannel(kind.id); if (!channel) { return unknownMention; } if (channel.type === ChannelTypes.GUILD_CATEGORY) { return #{channel.name}; } if ( channel.type !== ChannelTypes.GUILD_TEXT && channel.type !== ChannelTypes.GUILD_VOICE && channel.type !== ChannelTypes.GUILD_LINK ) { return unknownMention; } return ( ); } case MentionKind.Everyone: { return ( @everyone ); } case MentionKind.Here: { return ( @here ); } case MentionKind.Command: { const {name} = kind; return ( {name} ); } case MentionKind.GuildNavigation: { const {navigationType} = kind; let content: string; switch (navigationType) { case GuildNavKind.Customize: content = ''; break; case GuildNavKind.Browse: content = ''; break; case GuildNavKind.Guide: content = ''; break; case GuildNavKind.LinkedRoles: { const linkedRolesId = (kind as {navigationType: typeof GuildNavKind.LinkedRoles; id?: string}).id; content = linkedRolesId ? `` : ''; break; } default: content = ``; break; } return ( {content} ); } default: return {''}; } });