refactor progress
This commit is contained in:
527
fluxer_app/src/components/channel/MessageActionUtils.tsx
Normal file
527
fluxer_app/src/components/channel/MessageActionUtils.tsx
Normal file
@@ -0,0 +1,527 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as ChannelPinActionCreators from '@app/actions/ChannelPinsActionCreators';
|
||||
import * as MessageActionCreators from '@app/actions/MessageActionCreators';
|
||||
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
|
||||
import {modal} from '@app/actions/ModalActionCreators';
|
||||
import * as ReactionActionCreators from '@app/actions/ReactionActionCreators';
|
||||
import * as ReadStateActionCreators from '@app/actions/ReadStateActionCreators';
|
||||
import * as SavedMessageActionCreators from '@app/actions/SavedMessageActionCreators';
|
||||
import * as TextCopyActionCreators from '@app/actions/TextCopyActionCreators';
|
||||
import * as ToastActionCreators from '@app/actions/ToastActionCreators';
|
||||
import {useMaybeMessageViewContext} from '@app/components/channel/MessageViewContext';
|
||||
import {ConfirmModal} from '@app/components/modals/ConfirmModal';
|
||||
import {ForwardModal} from '@app/components/modals/ForwardModal';
|
||||
import {CloudUpload} from '@app/lib/CloudUpload';
|
||||
import {ComponentDispatch} from '@app/lib/ComponentDispatch';
|
||||
import type {ChannelRecord} from '@app/records/ChannelRecord';
|
||||
import type {MessageRecord} from '@app/records/MessageRecord';
|
||||
import AuthenticationStore from '@app/stores/AuthenticationStore';
|
||||
import ChannelStore from '@app/stores/ChannelStore';
|
||||
import GuildMemberStore from '@app/stores/GuildMemberStore';
|
||||
import GuildStore from '@app/stores/GuildStore';
|
||||
import GuildVerificationStore from '@app/stores/GuildVerificationStore';
|
||||
import MobileLayoutStore from '@app/stores/MobileLayoutStore';
|
||||
import PermissionStore from '@app/stores/PermissionStore';
|
||||
import RelationshipStore from '@app/stores/RelationshipStore';
|
||||
import SavedMessagesStore from '@app/stores/SavedMessagesStore';
|
||||
import type {UnicodeEmoji} from '@app/types/EmojiTypes';
|
||||
import {isSystemDmChannel} from '@app/utils/ChannelUtils';
|
||||
import {buildMessageJumpLink} from '@app/utils/MessageLinkUtils';
|
||||
import {type ReactionEmoji, toReactionEmoji} from '@app/utils/ReactionUtils';
|
||||
import TtsUtils from '@app/utils/TtsUtils';
|
||||
import {
|
||||
isMessageTypeDeletable,
|
||||
MessageFlags,
|
||||
MessageStates,
|
||||
MessageTypes,
|
||||
Permissions,
|
||||
} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {GuildOperations} from '@fluxer/constants/src/GuildConstants';
|
||||
import type {Message} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
|
||||
export function isEmbedsSuppressed(message: MessageRecord): boolean {
|
||||
return (message.flags & MessageFlags.SUPPRESS_EMBEDS) !== 0;
|
||||
}
|
||||
|
||||
export function isClientSystemMessage(message: MessageRecord): boolean {
|
||||
return message.isClientSystemMessage();
|
||||
}
|
||||
|
||||
export function canReportMessage(message: MessageRecord): boolean {
|
||||
if (message.state !== MessageStates.SENT) {
|
||||
return false;
|
||||
}
|
||||
if (message.isCurrentUserAuthor()) {
|
||||
return false;
|
||||
}
|
||||
if (message.author.system) {
|
||||
return false;
|
||||
}
|
||||
return message.type === MessageTypes.DEFAULT || message.type === MessageTypes.REPLY;
|
||||
}
|
||||
|
||||
export function canDeleteAttachmentUtil(message: MessageRecord | undefined): boolean {
|
||||
if (!message?.isCurrentUserAuthor()) return false;
|
||||
const channel = ChannelStore.getChannel(message.channelId);
|
||||
const guild = channel?.guildId ? GuildStore.getGuild(channel.guildId) : null;
|
||||
const sendMessageDisabled = guild ? (guild.disabledOperations & GuildOperations.SEND_MESSAGE) !== 0 : false;
|
||||
return !sendMessageDisabled;
|
||||
}
|
||||
|
||||
export function requestOpenReactionPicker(messageId: string): void {
|
||||
ComponentDispatch.dispatch('EMOJI_PICKER_OPEN', {messageId});
|
||||
}
|
||||
|
||||
export function triggerAddReaction(message: MessageRecord): boolean {
|
||||
if (isClientSystemMessage(message)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const messageElement = document.querySelector<HTMLElement>(`[data-message-id="${message.id}"]`);
|
||||
if (!messageElement) {
|
||||
ComponentDispatch.dispatch('EMOJI_PICKER_OPEN', {messageId: message.id});
|
||||
return false;
|
||||
}
|
||||
|
||||
const addReactionButton = messageElement.querySelector<HTMLButtonElement>(
|
||||
'[data-action="message-add-reaction-button"]',
|
||||
);
|
||||
if (!addReactionButton) {
|
||||
ComponentDispatch.dispatch('EMOJI_PICKER_OPEN', {messageId: message.id});
|
||||
return false;
|
||||
}
|
||||
|
||||
addReactionButton.click();
|
||||
return true;
|
||||
}
|
||||
|
||||
export interface MessagePermissions {
|
||||
channel: ChannelRecord;
|
||||
isDM: boolean;
|
||||
canSendMessages: boolean;
|
||||
canAddReactions: boolean;
|
||||
canEditMessage: boolean;
|
||||
canDeleteMessage: boolean;
|
||||
canDeleteAttachment: boolean;
|
||||
canPinMessage: boolean;
|
||||
canSuppressEmbeds: boolean;
|
||||
shouldRenderSuppressEmbeds: boolean;
|
||||
}
|
||||
|
||||
function getMessagePermissionsFromStores(message: MessageRecord): MessagePermissions | null {
|
||||
const channel = ChannelStore.getChannel(message.channelId);
|
||||
if (!channel) {
|
||||
return null;
|
||||
}
|
||||
const isDM = !channel.guildId;
|
||||
const isAuthorBlocked = RelationshipStore.isBlocked(message.author.id);
|
||||
const interactionsBlocked = isSystemDmChannel(channel);
|
||||
const isClientSystem = isClientSystemMessage(message);
|
||||
|
||||
const passesVerification = isDM || GuildVerificationStore.canAccessGuild(channel.guildId || '');
|
||||
|
||||
const guild = channel.guildId ? GuildStore.getGuild(channel.guildId) : null;
|
||||
const sendMessageDisabled = guild ? (guild.disabledOperations & GuildOperations.SEND_MESSAGE) !== 0 : false;
|
||||
const reactionsDisabled = guild ? (guild.disabledOperations & GuildOperations.REACTIONS) !== 0 : false;
|
||||
const messageTypeDeletable = isMessageTypeDeletable(message.type);
|
||||
const currentUserId = AuthenticationStore.currentUserId;
|
||||
const isCurrentUserTimedOut =
|
||||
guild && currentUserId ? GuildMemberStore.isUserTimedOut(guild.id, currentUserId) : false;
|
||||
|
||||
const canSendMessages =
|
||||
!isClientSystem &&
|
||||
!interactionsBlocked &&
|
||||
(isDM ||
|
||||
(!sendMessageDisabled &&
|
||||
PermissionStore.can(Permissions.SEND_MESSAGES, {channelId: message.channelId}) &&
|
||||
passesVerification));
|
||||
const canAddReactions =
|
||||
!isClientSystem &&
|
||||
!interactionsBlocked &&
|
||||
!isAuthorBlocked &&
|
||||
(isDM ||
|
||||
(!reactionsDisabled &&
|
||||
PermissionStore.can(Permissions.ADD_REACTIONS, {channelId: message.channelId}) &&
|
||||
passesVerification &&
|
||||
!isCurrentUserTimedOut));
|
||||
|
||||
const canEditMessage = !interactionsBlocked && !sendMessageDisabled && message.isCurrentUserAuthor();
|
||||
|
||||
const canDeleteMessage =
|
||||
!interactionsBlocked &&
|
||||
messageTypeDeletable &&
|
||||
!sendMessageDisabled &&
|
||||
(message.isCurrentUserAuthor() ||
|
||||
(isDM ? false : PermissionStore.can(Permissions.MANAGE_MESSAGES, {channelId: message.channelId})));
|
||||
|
||||
const canDeleteAttachment = !interactionsBlocked && !sendMessageDisabled && message.isCurrentUserAuthor();
|
||||
|
||||
const canPinMessage =
|
||||
!interactionsBlocked &&
|
||||
!sendMessageDisabled &&
|
||||
(isDM ? true : PermissionStore.can(Permissions.PIN_MESSAGES, {channelId: message.channelId}));
|
||||
const canSuppressEmbeds =
|
||||
!interactionsBlocked &&
|
||||
!sendMessageDisabled &&
|
||||
message.isUserMessage() &&
|
||||
(message.isCurrentUserAuthor() || (!isDM && canDeleteMessage));
|
||||
|
||||
const shouldRenderSuppressEmbeds =
|
||||
message.isUserMessage() && canSuppressEmbeds && (isEmbedsSuppressed(message) || message.embeds.length > 0);
|
||||
|
||||
return {
|
||||
channel,
|
||||
isDM,
|
||||
canSendMessages,
|
||||
canAddReactions,
|
||||
canEditMessage,
|
||||
canDeleteMessage,
|
||||
canDeleteAttachment,
|
||||
canPinMessage,
|
||||
canSuppressEmbeds,
|
||||
shouldRenderSuppressEmbeds,
|
||||
};
|
||||
}
|
||||
|
||||
export function useMessagePermissions(message: MessageRecord): MessagePermissions | null {
|
||||
const context = useMaybeMessageViewContext();
|
||||
|
||||
if (context?.previewPermissions) {
|
||||
return {
|
||||
channel: context.channel,
|
||||
...context.previewPermissions,
|
||||
};
|
||||
}
|
||||
|
||||
return getMessagePermissionsFromStores(message);
|
||||
}
|
||||
|
||||
interface MessageActionHandlersOptions {
|
||||
i18n: I18n;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export interface MessageActionHandlers {
|
||||
handleEmojiSelect: (emoji: UnicodeEmoji | ReactionEmoji) => void;
|
||||
handleCopyMessageId: () => void;
|
||||
handleCopyMessage: () => void;
|
||||
handleCopyMessageLink: () => void;
|
||||
handleSaveMessage: (isSaved: boolean) => (event?: React.MouseEvent | React.KeyboardEvent) => void;
|
||||
handleToggleSuppressEmbeds: () => void;
|
||||
handleReply: (event?: React.MouseEvent | React.KeyboardEvent) => void;
|
||||
handlePinMessage: (event?: React.MouseEvent | React.KeyboardEvent) => void;
|
||||
handleEditMessage: () => void;
|
||||
handleRetryMessage: () => void;
|
||||
handleFailedMessageDelete: () => void;
|
||||
handleForward: () => void;
|
||||
handleRemoveAllReactions: () => void;
|
||||
handleMarkAsUnread: () => void;
|
||||
}
|
||||
|
||||
export function createMessageActionHandlers(
|
||||
message: MessageRecord,
|
||||
options: MessageActionHandlersOptions,
|
||||
): MessageActionHandlers {
|
||||
const {i18n, onClose} = options;
|
||||
|
||||
const handleEmojiSelect = (emoji: UnicodeEmoji | ReactionEmoji) => {
|
||||
if (isClientSystemMessage(message)) {
|
||||
return;
|
||||
}
|
||||
ReactionActionCreators.addReaction(i18n, message.channelId, message.id, toReactionEmoji(emoji));
|
||||
};
|
||||
|
||||
const handleCopyMessageId = () => {
|
||||
requestCopyMessageId(message, i18n);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleCopyMessage = () => {
|
||||
if (message.content) {
|
||||
TextCopyActionCreators.copy(i18n, message.content);
|
||||
onClose?.();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyMessageLink = () => {
|
||||
requestCopyMessageLink(message, i18n);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleSaveMessage = (isSaved: boolean) => (event?: React.MouseEvent | React.KeyboardEvent) => {
|
||||
if (isClientSystemMessage(message)) {
|
||||
onClose?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSaved) {
|
||||
SavedMessageActionCreators.remove(i18n, message.id);
|
||||
} else {
|
||||
SavedMessageActionCreators.create(i18n, message.channelId, message.id).then(() => {
|
||||
if (!event?.shiftKey) {
|
||||
ComponentDispatch.dispatch('SAVED_MESSAGES_OPEN');
|
||||
}
|
||||
});
|
||||
}
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleToggleSuppressEmbeds = () => {
|
||||
requestToggleSuppressEmbeds(message, i18n);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleReply = (event?: React.MouseEvent | React.KeyboardEvent) => {
|
||||
const channel = ChannelStore.getChannel(message.channelId)!;
|
||||
requestMessageReply(message, {
|
||||
mention: !event?.shiftKey && !message.isCurrentUserAuthor() && channel.guildId != null,
|
||||
});
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handlePinMessage = (event?: React.MouseEvent | React.KeyboardEvent) => {
|
||||
onClose?.();
|
||||
requestMessagePin(message, i18n, {shiftKey: Boolean(event?.shiftKey)});
|
||||
};
|
||||
|
||||
const handleEditMessage = () => {
|
||||
startMessageEdit(message);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleRetryMessage = () => {
|
||||
if (!message.nonce) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageUpload = CloudUpload.getMessageUpload(message.nonce);
|
||||
const hasAttachments = messageUpload !== null;
|
||||
const optimisticMessage: Message = {
|
||||
...message.toJSON(),
|
||||
state: MessageStates.SENDING,
|
||||
edited_timestamp: undefined,
|
||||
attachments: message.attachments ? [...message.attachments] : undefined,
|
||||
reactions: [],
|
||||
};
|
||||
MessageActionCreators.retryLocal(message.channelId, message.id);
|
||||
MessageActionCreators.createOptimistic(message.channelId, optimisticMessage);
|
||||
|
||||
MessageActionCreators.send(message.channelId, {
|
||||
content: message.content,
|
||||
nonce: message.nonce,
|
||||
hasAttachments,
|
||||
allowedMentions: message._allowedMentions,
|
||||
messageReference: message.messageReference,
|
||||
flags: message.flags,
|
||||
favoriteMemeId: message._favoriteMemeId,
|
||||
stickers: [...(message.stickers ?? [])],
|
||||
});
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleFailedMessageDelete = () => {
|
||||
MessageActionCreators.deleteLocal(message.channelId, message.id);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleForward = () => {
|
||||
onClose?.();
|
||||
requestMessageForward(message);
|
||||
};
|
||||
|
||||
const handleRemoveAllReactions = () => {
|
||||
ReactionActionCreators.removeAllReactions(i18n, message.channelId, message.id);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleMarkAsUnread = () => {
|
||||
requestMarkMessageUnread(message);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return {
|
||||
handleEmojiSelect,
|
||||
handleCopyMessageId,
|
||||
handleCopyMessage,
|
||||
handleCopyMessageLink,
|
||||
handleSaveMessage,
|
||||
handleToggleSuppressEmbeds,
|
||||
handleReply,
|
||||
handlePinMessage,
|
||||
handleEditMessage,
|
||||
handleRetryMessage,
|
||||
handleFailedMessageDelete,
|
||||
handleForward,
|
||||
handleRemoveAllReactions,
|
||||
handleMarkAsUnread,
|
||||
};
|
||||
}
|
||||
|
||||
export function startMessageEdit(message: MessageRecord): void {
|
||||
if (message.messageSnapshots) {
|
||||
return;
|
||||
}
|
||||
if (MobileLayoutStore.isEnabled()) {
|
||||
MessageActionCreators.startEditMobile(message.channelId, message.id);
|
||||
} else {
|
||||
MessageActionCreators.startEdit(message.channelId, message.id, message.content);
|
||||
}
|
||||
}
|
||||
|
||||
export function requestDeleteMessage(message: MessageRecord, i18n: I18n, bypassConfirm = false): void {
|
||||
if (bypassConfirm) {
|
||||
MessageActionCreators.remove(message.channelId, message.id);
|
||||
return;
|
||||
}
|
||||
|
||||
MessageActionCreators.showDeleteConfirmation(i18n, {message});
|
||||
}
|
||||
|
||||
export function requestMessagePin(message: MessageRecord, i18n: I18n, options: {shiftKey?: boolean} = {}): void {
|
||||
if (message.pinned) {
|
||||
if (options.shiftKey) {
|
||||
ChannelPinActionCreators.unpin(message.channelId, message.id);
|
||||
return;
|
||||
}
|
||||
ModalActionCreators.push(
|
||||
modal(() => (
|
||||
<ConfirmModal
|
||||
title={i18n._(msg`Unpin Message`)}
|
||||
description={i18n._(msg`Do you want to send this pin back to the future?`)}
|
||||
message={message}
|
||||
primaryText={i18n._(msg`Unpin it`)}
|
||||
onPrimary={() => ChannelPinActionCreators.unpin(message.channelId, message.id)}
|
||||
/>
|
||||
)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (options.shiftKey) {
|
||||
ChannelPinActionCreators.pin(message.channelId, message.id);
|
||||
return;
|
||||
}
|
||||
ModalActionCreators.push(
|
||||
modal(() => (
|
||||
<ConfirmModal
|
||||
title={i18n._(msg`Pin it. Pin it good.`)}
|
||||
description={i18n._(msg`Pin this message to the channel for all to see. Unless ... you're chicken.`)}
|
||||
message={message}
|
||||
primaryText={i18n._(msg`Pin it real good`)}
|
||||
primaryVariant="primary"
|
||||
onPrimary={() => ChannelPinActionCreators.pin(message.channelId, message.id)}
|
||||
/>
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
export function requestMessageReply(message: MessageRecord, options?: {mention?: boolean}): void {
|
||||
if (isClientSystemMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = ChannelStore.getChannel(message.channelId);
|
||||
if (!channel) return;
|
||||
const shouldMention = options?.mention ?? (!message.isCurrentUserAuthor() && Boolean(channel.guildId));
|
||||
MessageActionCreators.startReply(message.channelId, message.id, shouldMention);
|
||||
}
|
||||
|
||||
export function requestMessageForward(message: MessageRecord): void {
|
||||
if (isClientSystemMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ModalActionCreators.push(modal(() => <ForwardModal message={message} />));
|
||||
}
|
||||
|
||||
export function requestCopyMessageText(message: MessageRecord, i18n: I18n): void {
|
||||
if (!message.content) return;
|
||||
void TextCopyActionCreators.copy(i18n, message.content);
|
||||
}
|
||||
|
||||
export function requestMarkMessageUnread(message: MessageRecord): void {
|
||||
ReadStateActionCreators.markAsUnread(message.channelId, message.id);
|
||||
}
|
||||
|
||||
export function requestCopyMessageLink(message: MessageRecord, i18n: I18n): void {
|
||||
if (isClientSystemMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = ChannelStore.getChannel(message.channelId);
|
||||
if (!channel) return;
|
||||
const jumpLink = buildMessageJumpLink({
|
||||
guildId: channel.guildId,
|
||||
channelId: message.channelId,
|
||||
messageId: message.id,
|
||||
});
|
||||
void TextCopyActionCreators.copy(i18n, jumpLink);
|
||||
}
|
||||
|
||||
export function requestCopyMessageId(message: MessageRecord, i18n: I18n): void {
|
||||
void TextCopyActionCreators.copy(i18n, message.id);
|
||||
}
|
||||
|
||||
export function requestToggleBookmark(message: MessageRecord, i18n: I18n): void {
|
||||
if (isClientSystemMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SavedMessagesStore.isSaved(message.id)) {
|
||||
SavedMessageActionCreators.remove(i18n, message.id);
|
||||
return;
|
||||
}
|
||||
void SavedMessageActionCreators.create(i18n, message.channelId, message.id);
|
||||
}
|
||||
|
||||
export function requestToggleSuppressEmbeds(message: MessageRecord, i18n: I18n): void {
|
||||
if (isEmbedsSuppressed(message)) {
|
||||
MessageActionCreators.edit(
|
||||
message.channelId,
|
||||
message.id,
|
||||
undefined,
|
||||
message.flags & ~MessageFlags.SUPPRESS_EMBEDS,
|
||||
).then(() => {
|
||||
ToastActionCreators.createToast({
|
||||
type: 'success',
|
||||
children: i18n._(msg`Embeds unsuppressed`),
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
MessageActionCreators.edit(
|
||||
message.channelId,
|
||||
message.id,
|
||||
undefined,
|
||||
message.flags | MessageFlags.SUPPRESS_EMBEDS,
|
||||
).then(() => {
|
||||
ToastActionCreators.createToast({
|
||||
type: 'success',
|
||||
children: i18n._(msg`Embeds suppressed`),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function requestSpeakMessage(message: MessageRecord): void {
|
||||
TtsUtils.speakMessage(message.content);
|
||||
}
|
||||
Reference in New Issue
Block a user