/* * 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 {useLingui} from '@lingui/react/macro'; import {ArrowsClockwiseIcon} from '@phosphor-icons/react'; import React from 'react'; import {MessageStates} from '~/Constants'; import { createMessageActionHandlers, isEmbedsSuppressed, useMessagePermissions, } from '~/components/channel/messageActionUtils'; import { AddReactionIcon, BookmarkIcon, CopyIdIcon, CopyLinkIcon, CopyTextIcon, DeleteIcon, EditIcon, ForwardIcon, MarkAsUnreadIcon, PinIcon, ReplyIcon, SuppressEmbedsIcon, } from '~/components/uikit/ContextMenu/ContextMenuIcons'; import type {MenuGroupType, MenuItemType} from '~/components/uikit/MenuBottomSheet/MenuBottomSheet'; import type {MessageRecord} from '~/records/MessageRecord'; import ChannelStore from '~/stores/ChannelStore'; import EmojiPickerStore from '~/stores/EmojiPickerStore'; import type {Emoji} from '~/stores/EmojiStore'; import EmojiStore from '~/stores/EmojiStore'; import SavedMessagesStore from '~/stores/SavedMessagesStore'; interface MessageActionMenuOptions { onOpenEmojiPicker?: () => void; onClose?: () => void; onDelete?: () => void; quickReactionCount?: number; } export interface MessageActionMenuData { handlers: ReturnType; permissions: ReturnType; groups: Array; quickReactionEmojis: Array; quickReactionRowVisible: boolean; isFailed: boolean; isSaved: boolean; } export const useMessageActionMenuData = ( message: MessageRecord, options: MessageActionMenuOptions = {}, ): MessageActionMenuData => { const {t} = useLingui(); const {onOpenEmojiPicker, onClose, onDelete, quickReactionCount = 5} = options; const permissions = useMessagePermissions(message); const handlers = React.useMemo(() => createMessageActionHandlers(message, {onClose}), [message, onClose]); const isSaved = React.useMemo(() => SavedMessagesStore.isSaved(message.id), [message.id]); const channel = React.useMemo(() => ChannelStore.getChannel(message.channelId) ?? null, [message.channelId]); const allEmojis = React.useMemo(() => EmojiStore.search(channel, ''), [channel]); const quickReactionEmojis = React.useMemo( () => EmojiPickerStore.getQuickReactionEmojis(allEmojis, quickReactionCount), [allEmojis, quickReactionCount], ); const groups = React.useMemo(() => { const interactionActions: Array = []; const managementActions: Array = []; const utilityActions: Array = []; if (message.state === MessageStates.SENT) { if (permissions.canAddReactions && onOpenEmojiPicker) { interactionActions.push({ id: 'add-reaction', icon: , label: t`Add Reaction`, onClick: onOpenEmojiPicker, }); } interactionActions.push({ icon: , label: t`Mark as Unread`, onClick: handlers.handleMarkAsUnread, }); if (message.isUserMessage() && permissions.canSendMessages) { interactionActions.push({ id: 'reply', icon: , label: t`Reply`, onClick: handlers.handleReply, }); } if (message.isUserMessage()) { interactionActions.push({ icon: , label: t`Forward`, onClick: handlers.handleForward, }); } if (message.isCurrentUserAuthor() && message.isUserMessage() && !message.messageSnapshots) { interactionActions.push({ id: 'edit', icon: , label: t`Edit Message`, onClick: handlers.handleEditMessage, }); } if (message.isUserMessage() && permissions.canPinMessage) { managementActions.push({ icon: , label: message.pinned ? t`Unpin Message` : t`Pin Message`, onClick: handlers.handlePinMessage, }); } if (message.isUserMessage()) { managementActions.push({ icon: , label: isSaved ? t`Remove Bookmark` : t`Bookmark Message`, onClick: handlers.handleSaveMessage(isSaved), }); } if (permissions.shouldRenderSuppressEmbeds) { managementActions.push({ icon: , label: isEmbedsSuppressed(message) ? t`Unsuppress Embeds` : t`Suppress Embeds`, onClick: handlers.handleToggleSuppressEmbeds, }); } if (permissions.canDeleteMessage && onDelete) { managementActions.push({ icon: , label: t`Delete Message`, onClick: () => { onClose?.(); onDelete(); }, danger: true, }); } utilityActions.push({ icon: , label: t`Copy Message Link`, onClick: handlers.handleCopyMessageLink, }); if (message.content) { utilityActions.push({ icon: , label: t`Copy Message`, onClick: handlers.handleCopyMessage, }); } utilityActions.push({ icon: , label: t`Copy Message ID`, onClick: handlers.handleCopyMessageId, }); } else if (message.state === MessageStates.FAILED) { interactionActions.push({ icon: , label: t`Retry`, onClick: handlers.handleRetryMessage, }); managementActions.push({ icon: , label: t`Delete Message`, onClick: handlers.handleFailedMessageDelete, danger: true, }); } const groups: Array = []; if (interactionActions.length > 0) groups.push({items: interactionActions}); if (managementActions.length > 0) groups.push({items: managementActions}); if (utilityActions.length > 0) groups.push({items: utilityActions}); return groups; }, [message, handlers, isSaved, onClose, onDelete, onOpenEmojiPicker, permissions]); const quickReactionRowVisible = permissions.canAddReactions && message.state === MessageStates.SENT && quickReactionEmojis.length > 0; return { handlers, permissions, groups, quickReactionEmojis, quickReactionRowVisible, isFailed: message.state === MessageStates.FAILED, isSaved, }; };