// SPDX-License-Identifier: AGPL-3.0-or-later import Accessibility from '@app/features/accessibility/state/Accessibility'; import {SILENT_MENTION} from '@app/features/app/config/I18nDisplayConstants'; import {UserTag} from '@app/features/channel/components/ChannelUserTag'; import {CompactAuthorPrefix, CompactMessageLayout} from '@app/features/channel/components/CompactMessageLayout'; import {EditingMessageInput} from '@app/features/channel/components/EditingMessageInput'; import {isMediaOnlyEmbed} from '@app/features/channel/components/embeds/EmbedRenderUtils'; import {MessageAttachments} from '@app/features/channel/components/MessageAttachments'; import {MessageAuthorInfo} from '@app/features/channel/components/MessageAuthorInfo'; import {MessageAvatar} from '@app/features/channel/components/MessageAvatar'; import {MessageTimeoutIndicator} from '@app/features/channel/components/MessageTimeoutIndicator'; import {MessageUsername} from '@app/features/channel/components/MessageUsername'; import {useMessageViewContext} from '@app/features/channel/components/MessageViewContext'; import {ReplyPreview} from '@app/features/channel/components/ReplyPreview'; import {TimestampWithTooltip} from '@app/features/channel/components/TimestampWithTooltip'; import {createSystemMessage} from '@app/features/devtools/utils/CommandUtils'; import Emoji from '@app/features/emoji/state/Emoji'; import {checkEmojiAvailability} from '@app/features/expressions/utils/ExpressionPermissionUtils'; import Guilds from '@app/features/guild/state/Guilds'; import {TRY_AGAIN_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; import GuildMembers from '@app/features/member/state/GuildMembers'; import * as MessageCommands from '@app/features/messaging/commands/MessageCommands'; import {SafeMarkdown} from '@app/features/messaging/components/markdown'; import {parse} from '@app/features/messaging/components/markdown/renderers'; import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes'; import MessageEdit from '@app/features/messaging/state/MessageEdit'; import {hasStyleableMessageText} from '@app/features/messaging/utils/FailedMessageDisplayUtils'; import { buildExistingAttachmentEditReferences, canSubmitEmptyMessageEdit, } from '@app/features/messaging/utils/MessageEditContentUtils'; import {retryFailedMessage} from '@app/features/messaging/utils/MessageRetryUtils'; import {NodeType} from '@app/features/messaging/utils/markdown/parser/Enums'; import {SpoilerSyncProvider} from '@app/features/messaging/utils/SpoilerUtils'; import {compactMarkdownProps} from '@app/features/theme/layout/MessageLayoutAttributes'; import markupStyles from '@app/features/theme/styles/Markup.module.css'; import styles from '@app/features/theme/styles/Message.module.css'; import MobileLayout from '@app/features/ui/state/MobileLayout'; import {Tooltip} from '@app/features/ui/tooltip/Tooltip'; import UserSettings from '@app/features/user/state/UserSettings'; import Users from '@app/features/user/state/Users'; import * as DateUtils from '@app/features/user/utils/DateFormatting'; import * as NicknameUtils from '@app/features/user/utils/NicknameUtils'; import {FLUXERBOT_ID} from '@fluxer/constants/src/AppConstants'; import {MessageFlags, MessageStates, MessageTypes} from '@fluxer/constants/src/ChannelConstants'; import {msg} from '@lingui/core/macro'; import {Trans, useLingui} from '@lingui/react/macro'; import {ArrowsClockwiseIcon, BellSlashIcon, EyeIcon, WarningCircleIcon} from '@phosphor-icons/react'; import {clsx} from 'clsx'; import {observer} from 'mobx-react-lite'; import {type MouseEvent, useCallback, useMemo} from 'react'; const JUMP_TO_MESSAGE_FROM_SENT_DESCRIPTOR = msg({ message: 'Jump to message from {displayName}, sent {formattedDate}', comment: 'Label in the channel and chat user message. Preserve {displayName}, {formattedDate}; they are inserted by code.', }); const EDITED_DESCRIPTOR = msg({ message: '(edited)', comment: 'Button or menu action label in the channel and chat user message. Keep it concise.', }); const FAILED_TO_SEND_MESSAGE_HOLD_FOR_OPTIONS_DESCRIPTOR = msg({ message: 'Failed to send message. Hold for options.', comment: 'Error message in the channel and chat user message.', }); const FAILED_TO_SEND_MESSAGE_DESCRIPTOR = msg({ message: 'Failed to send message.', comment: 'Error message in the channel and chat user message.', }); const THIS_WAS_A_MESSAGE_DESCRIPTOR = msg({ message: 'This was a {silentMention} message.', comment: 'Description text in the channel and chat user message. Preserve {silentMention}; it is inserted by code.', }); const MessageStateToClassName: Record = { [MessageStates.SENT]: styles.messageSent, [MessageStates.SENDING]: styles.messageSending, [MessageStates.FAILED]: styles.messageFailed, }; const CUSTOM_EMOJI_MARKDOWN_PATTERN = //g; function messageContentCopyBlockProps(content: string): { 'data-message-copy-block'?: 'true'; 'data-message-copy-text'?: string; } { return content ? {'data-message-copy-block': 'true', 'data-message-copy-text': content} : {}; } export const UserMessage = observer(() => { const {i18n} = useLingui(); const { message, channel, handleDelete, isHovering, shouldGroup, messageDisplayCompact, previewContext, previewOverrides, onHeadingActivate, } = useMessageViewContext(); const isEditing = MessageEdit.isEditing(message.channelId, message.id); const userAuthor = Users.getUser(message.author.id); const author = message.webhookId != null ? message.author : (userAuthor ?? message.author); const formattedDate = DateUtils.getRelativeDateString(message.timestamp, i18n); const jumpHeadingLabel = useMemo(() => { const displayName = previewOverrides?.displayName ?? NicknameUtils.getDisplayName(message.author); return i18n._(JUMP_TO_MESSAGE_FROM_SENT_DESCRIPTOR, {displayName, formattedDate}); }, [previewOverrides?.displayName, message.author, formattedDate, i18n.locale]); const jumpHeading = onHeadingActivate ? (

) : null; const AuthorHeading = (onHeadingActivate ? 'div' : 'h3') as 'div' | 'h3'; const showUserAvatarsInCompactMode = Accessibility.showUserAvatarsInCompactMode; const {nodes: astNodes} = useMemo( () => parse({ content: message.content, context: MarkdownContext.STANDARD_WITH_JUMBO, }), [message.content], ); const markdownOptions = useMemo( () => ({ context: MarkdownContext.STANDARD_WITH_JUMBO, messageId: message.id, channelId: message.channelId, mentionChannels: message.mentionChannels, }), [message.id, message.channelId, message.mentionChannels], ); const shouldHideContent = UserSettings.getRenderEmbeds() && message.embeds.length > 0 && message.embeds.every(isMediaOnlyEmbed) && astNodes.length === 1 && astNodes[0].type === NodeType.Link && !message.suppressEmbeds; const hasStyleableTextContent = useMemo(() => hasStyleableMessageText(astNodes), [astNodes]); const guild = Guilds.getGuild(channel.guildId ?? ''); const member = GuildMembers.getMember(guild?.id ?? '', author?.id ?? ''); const shouldAppearAuthorless = false; const mobileLayout = MobileLayout; const shouldShowFailedFooter = message.state === MessageStates.FAILED && (mobileLayout.enabled || shouldHideContent || !hasStyleableTextContent); const shouldApplyFailedTextStyling = message.state === MessageStates.FAILED && !shouldHideContent && hasStyleableTextContent; const messageStateClassName = message.state === MessageStates.FAILED ? shouldApplyFailedTextStyling ? styles.messageFailed : undefined : MessageStateToClassName[message.state]; const checkCustomEmojiAvailability = useCallback( (content: string): boolean => { CUSTOM_EMOJI_MARKDOWN_PATTERN.lastIndex = 0; let match: RegExpExecArray | null = null; while ((match = CUSTOM_EMOJI_MARKDOWN_PATTERN.exec(content))) { const emojiId = match[1]; const emoji = Emoji.getEmojiById(emojiId); if (!emoji) { continue; } const availability = checkEmojiAvailability(i18n, emoji, channel); if (availability.canUse) { continue; } if (availability.lockReason) { const errorMessage = createSystemMessage(channel.id, availability.lockReason); MessageCommands.createOptimistic(channel.id, errorMessage.toJSON()); } return true; } return false; }, [channel, i18n], ); const finishEditing = useCallback(() => { MessageEdit.clearDraftContent(message.id); MessageCommands.stopEdit(channel.id); }, [channel.id, message.id]); const onSubmit = useCallback( (actualContent?: string) => { if (message.messageSnapshots) { return; } const content = (actualContent ?? '').trim(); if (!content) { if (canSubmitEmptyMessageEdit(message)) { if (message.content.length === 0) { finishEditing(); return; } finishEditing(); void MessageCommands.edit( channel.id, message.id, '', undefined, message._allowedMentions, buildExistingAttachmentEditReferences(message), ); return; } handleDelete(); return; } if (checkCustomEmojiAvailability(content)) { return; } finishEditing(); void MessageCommands.edit(channel.id, message.id, content, undefined, message._allowedMentions); }, [ channel.id, handleDelete, message, message.id, message.messageSnapshots, message._allowedMentions, message.content, checkCustomEmojiAvailability, finishEditing, ], ); const cancelEditing = useCallback(() => { MessageCommands.stopEdit(message.channelId); }, [message.channelId]); const handleDismissSystemMessage = useCallback(() => { MessageCommands.deleteOptimistic(message.channelId, message.id); }, [message.channelId, message.id]); const handleRetryFailedMessage = useCallback( (event: MouseEvent) => { event.preventDefault(); event.stopPropagation(); retryFailedMessage(message); }, [message], ); const shouldShowEditingInput = isEditing && !previewContext && !mobileLayout.enabled; const compactAuthorPrefix = ( ); const renderMessageContent = useCallback(() => { if (shouldShowEditingInput) { return ( ); } if (shouldHideContent) return null; return (
{(message.editedTimestamp || message.isEditing) && (message.isEditing ? ( {' '} {i18n._(EDITED_DESCRIPTOR)} ) : ( {' '} {i18n._(EDITED_DESCRIPTOR)} ))}
); }, [ shouldShowEditingInput, shouldHideContent, markdownOptions, message, message.content, message.id, message.channelId, message.editedTimestamp, message.isEditing, channel, cancelEditing, onSubmit, i18n, ]); const renderFailedFooter = useCallback(() => { if (!shouldShowFailedFooter) { return null; } const failedLabel = mobileLayout.enabled ? i18n._(FAILED_TO_SEND_MESSAGE_HOLD_FOR_OPTIONS_DESCRIPTOR) : i18n._(FAILED_TO_SEND_MESSAGE_DESCRIPTOR); return (
{failedLabel}
{!mobileLayout.enabled && !previewContext && message.nonce && ( )}
); }, [handleRetryFailedMessage, message.nonce, mobileLayout.enabled, previewContext, shouldShowFailedFooter, i18n]); if (message.type === MessageTypes.CLIENT_SYSTEM && message.author.id === FLUXERBOT_ID) { return (
{jumpHeading} {formattedDate}
Only you can see this message.{' '}
); } if (messageDisplayCompact) { return ( {message.messageReference && message.messageReference.type === 0 && ( )} {(showMetadata) => isEditing && !previewContext && !mobileLayout.enabled ? (
{showMetadata && compactAuthorPrefix}
) : (
{showMetadata && compactAuthorPrefix} {!shouldHideContent && ( )} {(message.editedTimestamp || message.isEditing) && (message.isEditing ? ( {' '} {i18n._(EDITED_DESCRIPTOR)} ) : ( {' '} {i18n._(EDITED_DESCRIPTOR)} ))}
) }
{renderFailedFooter()}
); } return ( {message.messageReference && message.messageReference.type === 0 && ( )} {(message.content || isEditing) && (!shouldHideContent || isEditing) && (
{!shouldGroup && jumpHeading} {!shouldGroup && ( {author.bot && ( )} {formattedDate} {(message.flags & MessageFlags.SUPPRESS_NOTIFICATIONS) !== 0 && ( )} )}
{renderMessageContent()}
)} {shouldGroup && ( )} {!shouldGroup && ( <>
)}
{((!message.content && !isEditing) || (shouldHideContent && !isEditing)) && !shouldGroup && jumpHeading} {((!message.content && !isEditing) || (shouldHideContent && !isEditing)) && !shouldGroup && ( {author.bot && ( )} {formattedDate} {(message.flags & MessageFlags.SUPPRESS_NOTIFICATIONS) !== 0 && ( )} )} {shouldHideContent && !isEditing && message.content.length > 0 && (message.editedTimestamp || message.isEditing) && (message.isEditing ? ( {' '} {i18n._(EDITED_DESCRIPTOR)} ) : ( {' '} {i18n._(EDITED_DESCRIPTOR)} ))} {message.content.length === 0 && !isEditing && (message.editedTimestamp || message.isEditing) && (message.isEditing ? ( {' '} {i18n._(EDITED_DESCRIPTOR)} ) : ( {' '} {i18n._(EDITED_DESCRIPTOR)} ))} {renderFailedFooter()}
); });