mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(api): distinguish a deleted reply target from no reply (#1884)
This commit is contained in:
1 parent
7b646f891e
commit
735ecc1cca
8 files changed
+250
-181
No files matched your search
@@ -57,7 +57,7 @@ export const ReplyPreview = observer(
|
||||
messageDisplayCompact: boolean;
|
||||
}) => {
|
||||
const {i18n} = useLingui();
|
||||
const {message: referencedMessage, state: messageState} = MessageReferences.getMessageReference(
|
||||
const resolution = MessageReferences.getMessageReference(
|
||||
message.messageReference?.channel_id ?? '',
|
||||
message.messageReference?.message_id ?? '',
|
||||
);
|
||||
@@ -72,12 +72,14 @@ export const ReplyPreview = observer(
|
||||
}
|
||||
}, [message.channelId, message.id, message.messageReference]);
|
||||
if (!message.messageReference) return null;
|
||||
if (messageState !== MessageReferenceState.LOADED || !referencedMessage) {
|
||||
if (resolution.state !== MessageReferenceState.LOADED) {
|
||||
const isDeleted = resolution.state === MessageReferenceState.DELETED;
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.repliedMessage, messageDisplayCompact && styles.repliedMessageCompact)}
|
||||
data-message-copy-hidden="true"
|
||||
data-flx="channel.reply-preview.replied-message"
|
||||
data-flx-reference-state={resolution.state}
|
||||
>
|
||||
<div className={styles.repliedIconContainer} data-flx="channel.reply-preview.replied-icon-container">
|
||||
<ArrowBendUpLeftIcon
|
||||
@@ -86,26 +88,40 @@ export const ReplyPreview = observer(
|
||||
data-flx="channel.reply-preview.replied-icon"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className={clsx(styles.repliedTextPreview, styles.unstyled)}
|
||||
tabIndex={-1}
|
||||
data-flx="channel.reply-preview.replied-text-preview.button"
|
||||
>
|
||||
{messageState === MessageReferenceState.DELETED ? (
|
||||
{isDeleted ? (
|
||||
<div
|
||||
className={clsx(styles.repliedTextPreview, styles.unstyled)}
|
||||
data-flx="channel.reply-preview.replied-text-preview.deleted"
|
||||
>
|
||||
<span className={styles.repliedItalic} data-flx="channel.reply-preview.replied-italic">
|
||||
{i18n._(ORIGINAL_MESSAGE_WAS_DELETED_DESCRIPTOR)}
|
||||
</span>
|
||||
) : (
|
||||
<span className={styles.repliedItalic} data-flx="channel.reply-preview.replied-italic--2">
|
||||
{i18n._(ORIGINAL_MESSAGE_FAILED_TO_LOAD_DESCRIPTOR)}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<FocusRing offset={-2} data-flx="channel.reply-preview.focus-ring--not-loaded">
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(styles.repliedTextPreview, styles.unstyled)}
|
||||
onClick={jumpToRepliedMessage}
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
jumpToRepliedMessage();
|
||||
}
|
||||
}}
|
||||
data-flx="channel.reply-preview.replied-text-preview.not-loaded"
|
||||
>
|
||||
<span className={styles.repliedItalic} data-flx="channel.reply-preview.replied-italic--2">
|
||||
{i18n._(ORIGINAL_MESSAGE_FAILED_TO_LOAD_DESCRIPTOR)}
|
||||
</span>
|
||||
</button>
|
||||
</FocusRing>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const referencedMessage = resolution.message;
|
||||
const isSpammerReply =
|
||||
referencedMessage.author.id !== Authentication.currentUserId &&
|
||||
LocalUserSpamOverride.isUserMarkedAsSpammer(referencedMessage.author.id, referencedMessage.author.flags);
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {Endpoints} from '@app/features/app/constants/Endpoints';
|
||||
import Channels from '@app/features/channel/state/Channels';
|
||||
import GuildMatureContentAgree from '@app/features/guild/state/GuildMatureContentAgree';
|
||||
import {Message as MessageRecord} from '@app/features/messaging/models/MessagingMessage';
|
||||
import Messages from '@app/features/messaging/state/MessagingMessages';
|
||||
import {http} from '@app/features/platform/transport/RestTransport';
|
||||
import {HttpError} from '@app/features/platform/types/EndpointError';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {MessageReferenceTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import type {ValueOf} from '@fluxer/constants/src/ValueOf';
|
||||
import type {Message as WireMessage} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import {makeAutoObservable} from 'mobx';
|
||||
|
||||
const logger = new Logger('MessageReferences');
|
||||
export const MessageReferenceState = {
|
||||
LOADED: 'LOADED',
|
||||
NOT_LOADED: 'NOT_LOADED',
|
||||
@@ -20,6 +14,20 @@ export const MessageReferenceState = {
|
||||
} as const;
|
||||
|
||||
export type MessageReferenceState = ValueOf<typeof MessageReferenceState>;
|
||||
|
||||
export type MessageReferenceResolution =
|
||||
| {readonly state: typeof MessageReferenceState.LOADED; readonly message: MessageRecord}
|
||||
| {readonly state: typeof MessageReferenceState.NOT_LOADED}
|
||||
| {readonly state: typeof MessageReferenceState.DELETED};
|
||||
|
||||
const NOT_LOADED_RESOLUTION: MessageReferenceResolution = Object.freeze({
|
||||
state: MessageReferenceState.NOT_LOADED,
|
||||
});
|
||||
|
||||
const DELETED_RESOLUTION: MessageReferenceResolution = Object.freeze({
|
||||
state: MessageReferenceState.DELETED,
|
||||
});
|
||||
|
||||
type MessageInput = WireMessage | MessageRecord;
|
||||
|
||||
const toWireMessage = (message: MessageInput): WireMessage =>
|
||||
@@ -59,7 +67,9 @@ class MessageReferences {
|
||||
const key = this.getKey(refChannelId, refMessageId);
|
||||
const nextMessage = new MessageRecord(toWireMessage(message), {missingReactions: 'preserve'});
|
||||
const currentMessage = this.cachedMessages.get(key);
|
||||
if (currentMessage?.equals(nextMessage)) {
|
||||
if (this.deletedMessageIds.has(key)) {
|
||||
this.deletedMessageIds.delete(key);
|
||||
} else if (currentMessage?.equals(nextMessage)) {
|
||||
return false;
|
||||
}
|
||||
this.cachedMessages.set(key, nextMessage);
|
||||
@@ -67,6 +77,17 @@ class MessageReferences {
|
||||
return true;
|
||||
}
|
||||
|
||||
private markReferenceDeleted(refChannelId: string, refMessageId: string): boolean {
|
||||
const key = this.getKey(refChannelId, refMessageId);
|
||||
if (this.deletedMessageIds.has(key) && !this.cachedMessages.has(key)) {
|
||||
return false;
|
||||
}
|
||||
this.deletedMessageIds.add(key);
|
||||
this.cachedMessages.delete(key);
|
||||
this.bumpReferenceVersion(refChannelId, refMessageId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private updateCachedMessage(refChannelId: string, refMessageId: string, updates: Partial<WireMessage>): boolean {
|
||||
const key = this.getKey(refChannelId, refMessageId);
|
||||
const currentMessage = this.cachedMessages.get(key);
|
||||
@@ -116,13 +137,26 @@ class MessageReferences {
|
||||
this.referencingMessages.delete(referencingMessageId);
|
||||
}
|
||||
|
||||
handleMessageCreate(message: WireMessage, _optimistic: boolean): void {
|
||||
if (message.referenced_message) {
|
||||
const refChannelId = message.message_reference?.channel_id ?? message.channel_id;
|
||||
const refMessageId = message.referenced_message.id;
|
||||
this.setCachedMessage(refChannelId, refMessageId, message.referenced_message);
|
||||
this.addReference(refChannelId, refMessageId, message.id);
|
||||
private resolveReferenceTarget(message: WireMessage, fallbackChannelId: string): boolean {
|
||||
const reference = message.message_reference;
|
||||
if (!reference || reference.type !== MessageReferenceTypes.DEFAULT) {
|
||||
return false;
|
||||
}
|
||||
const refChannelId = reference.channel_id ?? fallbackChannelId;
|
||||
const refMessageId = reference.message_id;
|
||||
this.addReference(refChannelId, refMessageId, message.id);
|
||||
if (!('referenced_message' in message)) {
|
||||
return false;
|
||||
}
|
||||
const referenced = message.referenced_message;
|
||||
if (referenced == null) {
|
||||
return this.markReferenceDeleted(refChannelId, refMessageId);
|
||||
}
|
||||
return this.setCachedMessage(refChannelId, refMessageId, referenced);
|
||||
}
|
||||
|
||||
handleMessageCreate(message: WireMessage, _optimistic: boolean): void {
|
||||
this.resolveReferenceTarget(message, message.channel_id);
|
||||
}
|
||||
|
||||
handleMessageDelete(channelId: string, messageId: string): void {
|
||||
@@ -139,45 +173,13 @@ class MessageReferences {
|
||||
|
||||
handleMessageDeleteBulk(channelId: string, messageIds: Array<string>): void {
|
||||
for (const messageId of messageIds) {
|
||||
const key = this.getKey(channelId, messageId);
|
||||
this.deletedMessageIds.add(key);
|
||||
this.cachedMessages.delete(key);
|
||||
this.referenceVersions.delete(key);
|
||||
this.referenceCount.delete(key);
|
||||
const referencedBy = this.referencingMessages.get(messageId);
|
||||
if (referencedBy) {
|
||||
this.removeReference(referencedBy.channelId, referencedBy.messageId, messageId);
|
||||
}
|
||||
this.handleMessageDelete(channelId, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
handleMessagesFetchSuccess(channelId: string, messages: Array<WireMessage>): void {
|
||||
for (const message of messages) {
|
||||
if (message.referenced_message) {
|
||||
const refChannelId = message.message_reference?.channel_id ?? channelId;
|
||||
const refMessageId = message.referenced_message.id;
|
||||
this.setCachedMessage(refChannelId, refMessageId, message.referenced_message);
|
||||
this.addReference(refChannelId, refMessageId, message.id);
|
||||
}
|
||||
}
|
||||
const potentiallyMissingMessageIds = messages
|
||||
.filter((message) => message.message_reference && !message.referenced_message)
|
||||
.map((message) => ({
|
||||
channelId: message.message_reference!.channel_id ?? channelId,
|
||||
messageId: message.message_reference!.message_id,
|
||||
referencingMessageId: message.id,
|
||||
}))
|
||||
.filter(
|
||||
({channelId: refChannelId, messageId}) =>
|
||||
!Messages.getMessage(refChannelId, messageId) &&
|
||||
!this.deletedMessageIds.has(this.getKey(refChannelId, messageId)) &&
|
||||
!this.cachedMessages.has(this.getKey(refChannelId, messageId)),
|
||||
);
|
||||
for (const {channelId: refChannelId, messageId, referencingMessageId} of potentiallyMissingMessageIds) {
|
||||
this.addReference(refChannelId, messageId, referencingMessageId);
|
||||
}
|
||||
if (potentiallyMissingMessageIds.length > 0) {
|
||||
this.fetchMissingMessages(potentiallyMissingMessageIds.map(({channelId, messageId}) => ({channelId, messageId})));
|
||||
this.resolveReferenceTarget(message, channelId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,9 +200,11 @@ class MessageReferences {
|
||||
if (!('message_reference' in message) && !('referenced_message' in message)) {
|
||||
return;
|
||||
}
|
||||
const reference = message.message_reference;
|
||||
const isReferenceBearing = reference != null && reference.type === MessageReferenceTypes.DEFAULT;
|
||||
const previousRef = this.referencingMessages.get(message.id);
|
||||
const newRefChannelId = message.message_reference?.channel_id ?? message.channel_id;
|
||||
const newRefMessageId = message.referenced_message?.id ?? message.message_reference?.message_id;
|
||||
const newRefChannelId = reference?.channel_id ?? message.channel_id;
|
||||
const newRefMessageId = isReferenceBearing ? reference.message_id : undefined;
|
||||
if (previousRef) {
|
||||
const previousKey = this.getKey(previousRef.channelId, previousRef.messageId);
|
||||
const newKey = newRefMessageId ? this.getKey(newRefChannelId, newRefMessageId) : null;
|
||||
@@ -209,61 +213,7 @@ class MessageReferences {
|
||||
}
|
||||
}
|
||||
if (newRefMessageId) {
|
||||
if (message.referenced_message) {
|
||||
this.setCachedMessage(newRefChannelId, newRefMessageId, message.referenced_message);
|
||||
}
|
||||
this.addReference(newRefChannelId, newRefMessageId, message.id);
|
||||
}
|
||||
}
|
||||
|
||||
private fetchMissingMessages(
|
||||
refs: Array<{
|
||||
channelId: string;
|
||||
messageId: string;
|
||||
}>,
|
||||
): void {
|
||||
const allowedRefs = refs.filter(({channelId}) => {
|
||||
const channel = Channels.getChannel(channelId);
|
||||
if (!channel) {
|
||||
return false;
|
||||
}
|
||||
if (channel.isPrivate()) {
|
||||
return true;
|
||||
}
|
||||
return !GuildMatureContentAgree.shouldShowGate({channelId: channel.id, guildId: channel.guildId ?? null});
|
||||
});
|
||||
if (allowedRefs.length === 0) {
|
||||
return;
|
||||
}
|
||||
Promise.allSettled(
|
||||
allowedRefs.map(({channelId, messageId}) =>
|
||||
http
|
||||
.get<WireMessage>(Endpoints.CHANNEL_MESSAGE(channelId, messageId))
|
||||
.then((response) => {
|
||||
if (response.body) {
|
||||
this.handleMessageFetchSuccess(channelId, messageId, response.body);
|
||||
}
|
||||
})
|
||||
.catch((error) => this.handleMessageFetchError(channelId, messageId, error)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private handleMessageFetchSuccess(channelId: string, messageId: string, message: WireMessage): void {
|
||||
const messageRecord = new MessageRecord(message);
|
||||
const key = this.getKey(channelId, messageId);
|
||||
this.cachedMessages.set(key, messageRecord);
|
||||
this.bumpReferenceVersion(channelId, messageId);
|
||||
}
|
||||
|
||||
private handleMessageFetchError(channelId: string, messageId: string, error: unknown): void {
|
||||
const key = this.getKey(channelId, messageId);
|
||||
if (error instanceof HttpError && error.status === 404) {
|
||||
this.deletedMessageIds.add(key);
|
||||
this.cachedMessages.delete(key);
|
||||
this.referenceVersions.delete(key);
|
||||
} else {
|
||||
logger.error(`Failed to fetch message ${messageId}`, error);
|
||||
this.resolveReferenceTarget(message, message.channel_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,48 +246,21 @@ class MessageReferences {
|
||||
}
|
||||
}
|
||||
|
||||
getMessage(channelId: string, messageId: string): MessageRecord | null {
|
||||
getMessageReference(channelId: string, messageId: string): MessageReferenceResolution {
|
||||
const key = this.getKey(channelId, messageId);
|
||||
this.readReferenceVersion(channelId, messageId);
|
||||
if (this.deletedMessageIds.has(key)) {
|
||||
return null;
|
||||
}
|
||||
return Messages.getMessage(channelId, messageId) || this.cachedMessages.get(key) || null;
|
||||
}
|
||||
|
||||
getMessageReference(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
): {
|
||||
message: MessageRecord | null;
|
||||
state: MessageReferenceState;
|
||||
} {
|
||||
const key = this.getKey(channelId, messageId);
|
||||
this.readReferenceVersion(channelId, messageId);
|
||||
if (this.deletedMessageIds.has(key)) {
|
||||
return {
|
||||
message: null,
|
||||
state: MessageReferenceState.DELETED,
|
||||
};
|
||||
}
|
||||
const message = Messages.getMessage(channelId, messageId);
|
||||
if (message) {
|
||||
return {
|
||||
message,
|
||||
state: MessageReferenceState.LOADED,
|
||||
};
|
||||
return DELETED_RESOLUTION;
|
||||
}
|
||||
const cachedMessage = this.cachedMessages.get(key);
|
||||
if (cachedMessage) {
|
||||
return {
|
||||
message: cachedMessage,
|
||||
state: MessageReferenceState.LOADED,
|
||||
};
|
||||
return {state: MessageReferenceState.LOADED, message: cachedMessage};
|
||||
}
|
||||
return {
|
||||
message: null,
|
||||
state: MessageReferenceState.NOT_LOADED,
|
||||
};
|
||||
const message = Messages.getMessage(channelId, messageId);
|
||||
if (message) {
|
||||
return {state: MessageReferenceState.LOADED, message};
|
||||
}
|
||||
return NOT_LOADED_RESOLUTION;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Accessibility from '@app/features/accessibility/state/Accessibility';
|
||||
import Authentication from '@app/features/auth/state/Authentication';
|
||||
import Channels from '@app/features/channel/state/Channels';
|
||||
import MessageReferences from '@app/features/messaging/state/MessageReferences';
|
||||
import MessageReferences, {MessageReferenceState} from '@app/features/messaging/state/MessageReferences';
|
||||
import {SystemMessageUtils} from '@app/features/messaging/utils/SystemMessageUtils';
|
||||
import SelectedChannel from '@app/features/navigation/state/SelectedChannel';
|
||||
import Relationships from '@app/features/relationship/state/Relationships';
|
||||
@@ -454,7 +454,7 @@ function handleIncomingTtsMessage(message: Message): void {
|
||||
const refChannelId = message.message_reference.channel_id ?? message.channel_id;
|
||||
const refMessageId = message.message_reference.message_id;
|
||||
const ref = MessageReferences.getMessageReference(refChannelId, refMessageId);
|
||||
if (ref.message) {
|
||||
if (ref.state === MessageReferenceState.LOADED) {
|
||||
const replyAuthor = Users.getUser(ref.message.author.id);
|
||||
if (replyAuthor) {
|
||||
replyAuthorName = NicknameUtils.getNickname(replyAuthor, channel.guildId ?? null);
|
||||
|
||||
Reference in new issue
Block a user