/*
* 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 type {I18n} from '@lingui/core';
import {msg} from '@lingui/core/macro';
import {useLingui} from '@lingui/react/macro';
import {AnimatePresence, motion, type PanInfo} from 'framer-motion';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import {useEffect} from 'react';
import {MessageTypes} from '~/Constants';
import {SafeMarkdown} from '~/lib/markdown';
import {MarkdownContext} from '~/lib/markdown/renderers';
import type {MessageRecord} from '~/records/MessageRecord';
import ChannelStore from '~/stores/ChannelStore';
import GuildStore from '~/stores/GuildStore';
import MobileMentionToastStore from '~/stores/MobileMentionToastStore';
import * as ChannelUtils from '~/utils/ChannelUtils';
import {isMobileExperienceEnabled} from '~/utils/mobileExperience';
import {SystemMessageUtils} from '~/utils/SystemMessageUtils';
import styles from './MobileMentionToast.module.css';
const DISPLAY_DURATION_MS = 3000;
const getChannelLabel = (channelId: string, i18n: I18n): string => {
const channel = ChannelStore.getChannel(channelId);
if (!channel) {
return i18n._(msg`Unknown channel`);
}
if (channel.isGuildText()) {
const channelName = channel.name?.trim();
const fallback = i18n._(msg`Unknown channel`);
return channelName ? `#${channelName}` : fallback;
}
return ChannelUtils.getDMDisplayName(channel);
};
const getLocationLabel = (message: MessageRecord, i18n: I18n): string => {
const channel = ChannelStore.getChannel(message.channelId);
const channelLabel = getChannelLabel(message.channelId, i18n);
if (channel?.guildId) {
const guild = GuildStore.getGuild(channel.guildId);
if (guild && channel.isGuildText()) {
return `${guild.name} • ${channelLabel}`;
}
}
return channelLabel;
};
const renderMessageContent = (message: MessageRecord, i18n: I18n): React.ReactNode => {
if (message.type !== MessageTypes.DEFAULT && message.type !== MessageTypes.REPLY) {
const systemText = SystemMessageUtils.stringify(message, i18n);
if (systemText) {
return {systemText.replace(/\.$/, '')};
}
return null;
}
if (message.content) {
return (
);
}
if (message.attachments.length > 0) {
return {i18n._(msg`Sent an attachment`)};
}
return null;
};
export const MobileMentionToast = observer(() => {
const {i18n} = useLingui();
const current = MobileMentionToastStore.current;
const isMobile = isMobileExperienceEnabled();
useEffect(() => {
if (!current || !isMobile) return;
const timer = setTimeout(() => {
MobileMentionToastStore.dequeue(current.id);
}, DISPLAY_DURATION_MS);
return () => clearTimeout(timer);
}, [current?.id, isMobile]);
if (!isMobile || !current) {
return null;
}
const handleDragEnd = (_event: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => {
if (Math.abs(info.offset.x) > 60) {
MobileMentionToastStore.dequeue(current.id);
}
};
const locationLabel = getLocationLabel(current, i18n);
return (
{current.author.displayName}
•
{locationLabel}
{i18n._(msg`Mentioned you`)}
{renderMessageContent(current, i18n)}
);
});