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