chore: bug fix cleanup (#4)

This commit is contained in:
hampus-fluxer
2026-01-03 06:44:40 +01:00
committed by GitHub
parent 275126d61b
commit c9c5dceb47
80 changed files with 4639 additions and 3709 deletions

View File

@@ -147,7 +147,7 @@ class MessageQueue extends Queue<MessageQueuePayload, HttpResponse<Message> | un
drain(
message: MessageQueuePayload,
completed: (err: RetryError | null, result?: HttpResponse<Message>) => void,
completed: (err: RetryError | null, result?: HttpResponse<Message>, error?: unknown) => void,
): void {
if (isSendPayload(message)) {
this.handleSend(message, completed);
@@ -155,7 +155,7 @@ class MessageQueue extends Queue<MessageQueuePayload, HttpResponse<Message> | un
this.handleEdit(message, completed);
} else {
logger.error('Unknown message type, completing with null');
completed(null);
completed(null, undefined, new Error('Unknown message queue payload'));
}
}
@@ -188,7 +188,7 @@ class MessageQueue extends Queue<MessageQueuePayload, HttpResponse<Message> | un
private async handleSend(
payload: SendMessagePayload,
completed: (err: RetryError | null, result?: HttpResponse<Message>) => void,
completed: (err: RetryError | null, result?: HttpResponse<Message>, error?: unknown) => void,
): Promise<void> {
const {channelId, nonce, hasAttachments} = payload;
@@ -239,7 +239,7 @@ class MessageQueue extends Queue<MessageQueuePayload, HttpResponse<Message> | un
this.handleSendRateLimit(httpError, completed);
} else {
this.handleSendError(channelId, nonce, httpError, i18n, payload.hasAttachments);
completed(null);
completed(null, undefined, httpError);
}
}
}
@@ -309,12 +309,12 @@ class MessageQueue extends Queue<MessageQueuePayload, HttpResponse<Message> | un
private handleSendRateLimit(
error: HttpError,
completed: (err: RetryError | null, result?: HttpResponse<Message>) => void,
completed: (err: RetryError | null, result?: HttpResponse<Message>, error?: unknown) => void,
): void {
const retryAfterSeconds = getApiErrorBody(error)?.retry_after ?? 0;
const retryAfterMs = retryAfterSeconds > 0 ? retryAfterSeconds * 1000 : undefined;
completed({retryAfter: retryAfterMs});
completed({retryAfter: retryAfterMs}, undefined, error);
this.handleRateLimitError(retryAfterSeconds);
}
@@ -399,7 +399,7 @@ class MessageQueue extends Queue<MessageQueuePayload, HttpResponse<Message> | un
private async handleEdit(
payload: EditMessagePayload,
completed: (err: RetryError | null, result?: HttpResponse<Message>) => void,
completed: (err: RetryError | null, result?: HttpResponse<Message>, error?: unknown) => void,
): Promise<void> {
const {channelId, messageId, content, flags} = payload;
@@ -428,7 +428,7 @@ class MessageQueue extends Queue<MessageQueuePayload, HttpResponse<Message> | un
this.handleEditRateLimit(httpError, completed);
} else {
this.showEditErrorModal(httpError);
completed(null);
completed(null, undefined, httpError);
}
} finally {
this.abortControllers.delete(messageId);
@@ -451,12 +451,12 @@ class MessageQueue extends Queue<MessageQueuePayload, HttpResponse<Message> | un
private handleEditRateLimit(
error: HttpError,
completed: (err: RetryError | null, result?: HttpResponse<Message>) => void,
completed: (err: RetryError | null, result?: HttpResponse<Message>, error?: unknown) => void,
): void {
const retryAfterSeconds = getApiErrorBody(error)?.retry_after ?? 0;
const retryAfterMs = retryAfterSeconds > 0 ? retryAfterSeconds * 1000 : undefined;
completed({retryAfter: retryAfterMs});
completed({retryAfter: retryAfterMs}, undefined, error);
this.handleEditRateLimitError(retryAfterSeconds);
}

View File

@@ -21,7 +21,7 @@ import {Logger} from '~/lib/Logger';
export interface QueueEntry<TMessage, TResult = void> {
message: TMessage;
success: (result?: TResult) => void;
success: (result?: TResult, error?: unknown) => void;
}
interface RetryInfo {
@@ -49,9 +49,12 @@ export abstract class Queue<TMessage, TResult = void> {
this.isDraining = false;
}
protected abstract drain(message: TMessage, complete: (retry: RetryInfo | null, result?: TResult) => void): void;
protected abstract drain(
message: TMessage,
complete: (retry: RetryInfo | null, result?: TResult, error?: unknown) => void,
): void;
enqueue(message: TMessage, success: (result?: TResult) => void): void {
enqueue(message: TMessage, success: (result?: TResult, error?: unknown) => void): void {
this.queue.push({message, success});
this.maybeProcessNext();
}
@@ -90,7 +93,7 @@ export abstract class Queue<TMessage, TResult = void> {
let hasCompleted = false;
const complete = (retry: RetryInfo | null, result?: TResult): void => {
const complete = (retry: RetryInfo | null, result?: TResult, error?: unknown): void => {
if (hasCompleted) {
this.logger.warn('Queue completion callback invoked more than once; ignoring extra call');
return;
@@ -105,9 +108,9 @@ export abstract class Queue<TMessage, TResult = void> {
setTimeout(() => this.maybeProcessNext(), 0);
try {
success(result);
} catch (error) {
this.logger.error('Error in queue success callback', error);
success(result, error);
} catch (callbackError) {
this.logger.error('Error in queue success callback', callbackError);
}
return;
}
@@ -132,7 +135,7 @@ export abstract class Queue<TMessage, TResult = void> {
} catch (error) {
this.logger.error('Unhandled error while draining queue item', error);
if (!hasCompleted) {
complete(null);
complete(null, undefined, error);
}
}
}

View File

@@ -1023,6 +1023,27 @@ export class ScrollManager {
}
}
applyLayoutShift(heightDelta: number): boolean {
if (this.isDisposed || !this.isInitialized()) return false;
if (heightDelta === 0) return false;
const scrollerNode = this.ref.current?.getScrollerNode();
if (!scrollerNode) return false;
const state = this.getScrollerState();
const distanceFromBottom = Math.max(state.scrollHeight - state.offsetHeight - state.scrollTop, 0);
const stickThreshold = Math.max(Math.abs(heightDelta) + BOTTOM_LOCK_TOLERANCE, 32);
const shouldStick = this.isPinned() || distanceFromBottom <= stickThreshold;
if (!shouldStick) return false;
const maxScrollTop = Math.max(0, scrollerNode.scrollHeight - scrollerNode.offsetHeight);
const targetScrollTop = Math.max(0, Math.min(state.scrollTop + heightDelta, maxScrollTop));
this.mergeTo(targetScrollTop, this.handleScroll);
return true;
}
private updateStoreDimensions(callback?: () => void): void {
if (this.isDisposed) return;
if (this.isJumping() || !this.isInitialized()) return;

View File

@@ -58,6 +58,7 @@
.jumpLinkGuildIcon {
width: 1rem;
height: 1rem;
--guild-icon-size: 1rem;
flex-shrink: 0;
display: inline-flex;
align-items: center;

View File

@@ -129,7 +129,10 @@ const EmojiRendererInner = observer(function EmojiRendererInner({
const tooltipQualitySuffix = `?size=${tooltipEmojiSize}&quality=lossless`;
const isCustomEmoji = node.kind.kind === EmojiKind.Custom;
const emojiRecord = isCustomEmoji && emojiData.id ? EmojiStore.getEmojiById(emojiData.id) : null;
const emojiRecord: Emoji | null =
isCustomEmoji && emojiData.id ? (EmojiStore.getEmojiById(emojiData.id) ?? null) : null;
const fallbackGuildId = emojiRecord?.guildId;
const fallbackAnimated = emojiRecord?.animated ?? emojiData.isAnimated;
const handleOpenBottomSheet = React.useCallback(() => {
if (!isMobile) return;
@@ -160,12 +163,16 @@ const EmojiRendererInner = observer(function EmojiRendererInner({
if (emojiRecord) {
return emojiRecord;
}
return {
id: emojiData.id,
guildId: fallbackGuildId,
animated: fallbackAnimated,
name: node.kind.name,
allNamesString: node.kind.name,
uniqueName: node.kind.name,
};
}, [emojiRecord, node.kind.name]);
}, [emojiData.id, emojiData.isAnimated, emojiRecord, node.kind.name]);
const getTooltipData = React.useCallback(() => {
const emojiUrl =

View File

@@ -62,11 +62,21 @@ interface JumpLinkMentionProps {
guild: GuildRecord | null;
messageId?: string;
i18n: I18n;
interactive?: boolean;
}
const JumpLinkMention = observer(function JumpLinkMention({channel, guild, messageId, i18n}: JumpLinkMentionProps) {
const INLINE_REPLY_CONTEXT = 1;
const JumpLinkMention = observer(function JumpLinkMention({
channel,
guild,
messageId,
i18n,
interactive = true,
}: JumpLinkMentionProps) {
const handleClick = React.useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return;
event.preventDefault();
event.stopPropagation();
@@ -98,17 +108,26 @@ const JumpLinkMention = observer(function JumpLinkMention({channel, guild, messa
? i18n._(msg`Jump to ${labelText}`)
: i18n._(msg`Jump to the linked channel`);
const Component = interactive ? 'button' : 'span';
return (
<button
type="button"
className={clsx(markupStyles.mention, markupStyles.interactive, jumpLinkStyles.jumpLinkButton)}
<Component
{...(interactive ? {type: 'button'} : {})}
className={clsx(markupStyles.mention, interactive && markupStyles.interactive, jumpLinkStyles.jumpLinkButton)}
onClick={handleClick}
aria-label={ariaLabel}
tabIndex={interactive ? 0 : -1}
>
<span className={jumpLinkStyles.jumpLinkInfo}>
{guild ? (
<span className={jumpLinkStyles.jumpLinkGuild}>
<GuildIcon id={guild.id} name={guild.name} icon={guild.icon} className={jumpLinkStyles.jumpLinkGuildIcon} />
<GuildIcon
id={guild.id}
name={guild.name}
icon={guild.icon}
className={jumpLinkStyles.jumpLinkGuildIcon}
containerProps={{'data-jump-link-guild-icon': ''}}
/>
<span className={jumpLinkStyles.jumpLinkGuildName}>{guild.name}</span>
</span>
) : shouldShowDMIconLabel ? (
@@ -137,7 +156,7 @@ const JumpLinkMention = observer(function JumpLinkMention({channel, guild, messa
</span>
)}
</span>
</button>
</Component>
);
});
@@ -156,13 +175,20 @@ export const LinkRenderer = observer(function LinkRenderer({
const jumpTarget = messageJumpTarget ?? parseChannelJumpLink(url);
const jumpChannel = jumpTarget ? (ChannelStore.getChannel(jumpTarget.channelId) ?? null) : null;
const jumpGuild = jumpChannel?.guildId ? (GuildStore.getGuild(jumpChannel.guildId) ?? null) : null;
const isInlineReplyContext = options.context === INLINE_REPLY_CONTEXT;
if (jumpTarget && jumpChannel) {
return (
<FocusRing key={id}>
<JumpLinkMention channel={jumpChannel} guild={jumpGuild} messageId={messageJumpTarget?.messageId} i18n={i18n} />
</FocusRing>
const mention = (
<JumpLinkMention
channel={jumpChannel}
guild={jumpGuild}
messageId={messageJumpTarget?.messageId}
i18n={i18n}
interactive={!isInlineReplyContext}
/>
);
return isInlineReplyContext ? mention : <FocusRing key={id}>{mention}</FocusRing>;
}
const shouldShowAccessDeniedModal = Boolean(jumpTarget && !jumpChannel);