/*
* 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 {''};
}
});