fix: various fixes to sentry-reported errors

This commit is contained in:
Hampus Kraft
2026-02-21 01:32:04 +00:00
parent eb194ae5be
commit 24e9a1529d
29 changed files with 290 additions and 75 deletions

View File

@@ -894,7 +894,7 @@ export const ChannelDetailsBottomSheet: React.FC<ChannelDetailsBottomSheetProps>
}
}
const users = memberIds.map((id) => UserStore.getUser(id)).filter((u): u is UserRecord => u !== null);
const users = memberIds.map((id) => UserStore.getUser(id)).filter((u): u is UserRecord => u != null);
return MemberListUtils.getGroupDMMemberGroups(users);
})();

View File

@@ -39,15 +39,15 @@ export const ChannelIndexPage = observer(() => {
messageId?: string;
};
if (!channelId) {
return null;
}
const channel = ChannelStore.getChannel(channelId);
const channel = channelId ? ChannelStore.getChannel(channelId) : undefined;
const isInFavorites = location.pathname.startsWith('/channels/@favorites');
const derivedGuildId = isInFavorites ? channel?.guildId : routeGuildId || channel?.guildId;
useEffect(() => {
if (!channelId) {
return;
}
if (!channel) {
return;
}
@@ -62,7 +62,11 @@ export const ChannelIndexPage = observer(() => {
}
NavigationActionCreators.selectChannel(fallbackGuildId, undefined, undefined, 'replace');
}, [channel, routeGuildId, isInFavorites]);
}, [channelId, channel, routeGuildId, isInFavorites]);
if (!channelId) {
return null;
}
if (channel && (channel.type === ChannelTypes.GUILD_CATEGORY || channel.type === ChannelTypes.GUILD_LINK)) {
return null;

View File

@@ -328,7 +328,7 @@ export const ChannelMembers = observer(function ChannelMembers({guild = null, ch
if (channel.type === ChannelTypes.GROUP_DM) {
const currentUserId = AuthenticationStore.currentUserId;
const allUserIds = currentUserId ? [currentUserId, ...channel.recipientIds] : channel.recipientIds;
const users = allUserIds.map((id) => UserStore.getUser(id)).filter((user): user is UserRecord => user !== null);
const users = allUserIds.map((id) => UserStore.getUser(id)).filter((user): user is UserRecord => user != null);
const memberGroups = MemberListUtils.getGroupDMMemberGroups(users);
return (

View File

@@ -42,6 +42,7 @@ 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 UserStore from '@app/stores/UserStore';
import type {UnicodeEmoji} from '@app/types/EmojiTypes';
import {isSystemDmChannel} from '@app/utils/ChannelUtils';
import {buildMessageJumpLink} from '@app/utils/MessageLinkUtils';
@@ -450,7 +451,12 @@ export function requestMessageForward(message: MessageRecord): void {
return;
}
ModalActionCreators.push(modal(() => <ForwardModal message={message} />));
const currentUser = UserStore.currentUser;
if (!currentUser) {
return;
}
ModalActionCreators.push(modal(() => <ForwardModal message={message} user={currentUser} />));
}
export function requestCopyMessageText(message: MessageRecord, i18n: I18n): void {

View File

@@ -186,7 +186,7 @@ const DMListItem = observer(({channel, isSelected}: {channel: ChannelRecord; isS
}, []);
const contextMenuOpen = useContextMenuHoverState(scrollTargetRef);
const closeAllSheets = useCallback(() => {
closeAllSheets();
setMenuOpen(false);
setNestedSheet(null);
}, []);
const openNestedSheet = useCallback((title: string, groups: Array<MenuGroupType>) => {

View File

@@ -408,7 +408,7 @@ export const MessageSearchBar = observer(
useEffect(() => {
const context = MemberSearchStore.getSearchContext((results) => {
const users = results.map((result) => UserStore.getUser(result.id)).filter((u): u is UserRecord => u !== null);
const users = results.map((result) => UserStore.getUser(result.id)).filter((u): u is UserRecord => u != null);
setMemberSearchResults(users);
}, 25);
@@ -660,7 +660,7 @@ export const MessageSearchBar = observer(
if (channel) {
const users = channel.recipientIds
.map((id) => UserStore.getUser(id))
.filter((u): u is UserRecord => u !== null);
.filter((u): u is UserRecord => u != null);
return matchSorter(users, searchTerm, {keys: ['username', 'tag']}).slice(0, 12);
}

View File

@@ -150,6 +150,41 @@ describe('ChannelMoveOperation', () => {
});
});
it('moves a top-level category above another category with root-level preceding sibling', () => {
const milsims = createChannel({id: 'milsims', type: ChannelTypes.GUILD_CATEGORY, position: 3});
const coopGames = createChannel({id: 'coop-games', type: ChannelTypes.GUILD_CATEGORY, position: 11});
const frontDoor = createChannel({id: 'front-door', type: ChannelTypes.GUILD_CATEGORY, position: 30});
const coopVoice = createChannel({
id: 'coop-voice',
type: ChannelTypes.GUILD_VOICE,
position: 12,
parentId: coopGames.id,
});
const coopText = createChannel({
id: 'coop-text',
type: ChannelTypes.GUILD_TEXT,
position: 13,
parentId: coopGames.id,
});
const operation = createChannelMoveOperation({
channels: [milsims, coopGames, frontDoor, coopVoice, coopText],
dragItem: createDragItem(frontDoor),
dropResult: {
targetId: coopGames.id,
position: 'before',
targetParentId: null,
},
});
expect(operation).toEqual({
channelId: frontDoor.id,
newParentId: null,
precedingSiblingId: milsims.id,
position: 1,
});
});
it('returns null when dropping to the same effective placement', () => {
const category = createChannel({id: 'category', type: ChannelTypes.GUILD_CATEGORY, position: 0});
const textOne = createChannel({

View File

@@ -24,7 +24,7 @@ import styles from '@app/components/modals/BackupCodesModal.module.css';
import {BackupCodesRegenerateModal} from '@app/components/modals/BackupCodesRegenerateModal';
import * as Modal from '@app/components/modals/Modal';
import {Button} from '@app/components/uikit/button/Button';
import UserStore from '@app/stores/UserStore';
import type {UserRecord} from '@app/records/UserRecord';
import type {BackupCode} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import {Trans, useLingui} from '@lingui/react/macro';
import {CheckIcon, ClipboardIcon, DownloadIcon} from '@phosphor-icons/react';
@@ -32,11 +32,11 @@ import {observer} from 'mobx-react-lite';
interface BackupCodesModalProps {
backupCodes: ReadonlyArray<BackupCode>;
user: UserRecord;
}
export const BackupCodesModal = observer(({backupCodes}: BackupCodesModalProps) => {
export const BackupCodesModal = observer(({backupCodes, user}: BackupCodesModalProps) => {
const {t, i18n} = useLingui();
const user = UserStore.getCurrentUser()!;
return (
<Modal.Root size="small" centered>
@@ -89,7 +89,7 @@ export const BackupCodesModal = observer(({backupCodes}: BackupCodesModalProps)
<Button
variant="danger-secondary"
small={true}
onClick={() => ModalActionCreators.push(modal(() => <BackupCodesRegenerateModal />))}
onClick={() => ModalActionCreators.push(modal(() => <BackupCodesRegenerateModal user={user} />))}
>
<Trans>Regenerate</Trans>
</Button>

View File

@@ -27,6 +27,7 @@ import {BackupCodesModal} from '@app/components/modals/BackupCodesModal';
import * as Modal from '@app/components/modals/Modal';
import {Button} from '@app/components/uikit/button/Button';
import {useFormSubmit} from '@app/hooks/useFormSubmit';
import type {UserRecord} from '@app/records/UserRecord';
import {Trans, useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import {useForm} from 'react-hook-form';
@@ -35,14 +36,20 @@ interface FormInputs {
form: string;
}
export const BackupCodesRegenerateModal = observer(() => {
interface BackupCodesRegenerateModalProps {
user: UserRecord;
}
export const BackupCodesRegenerateModal = observer(({user}: BackupCodesRegenerateModalProps) => {
const {t} = useLingui();
const form = useForm<FormInputs>();
const onSubmit = async () => {
const backupCodes = await MfaActionCreators.getBackupCodes(true);
ModalActionCreators.pop();
ModalActionCreators.update('backup-codes', () => modal(() => <BackupCodesModal backupCodes={backupCodes} />));
ModalActionCreators.update('backup-codes', () =>
modal(() => <BackupCodesModal backupCodes={backupCodes} user={user} />),
);
ToastActionCreators.createToast({
type: 'success',
children: t`Backup codes regenerated`,

View File

@@ -26,6 +26,7 @@ import {BackupCodesModal} from '@app/components/modals/BackupCodesModal';
import * as Modal from '@app/components/modals/Modal';
import {Button} from '@app/components/uikit/button/Button';
import {useFormSubmit} from '@app/hooks/useFormSubmit';
import type {UserRecord} from '@app/records/UserRecord';
import {Trans, useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import {useForm} from 'react-hook-form';
@@ -34,7 +35,11 @@ interface FormInputs {
form: string;
}
export const BackupCodesViewModal = observer(() => {
interface BackupCodesViewModalProps {
user: UserRecord;
}
export const BackupCodesViewModal = observer(({user}: BackupCodesViewModalProps) => {
const {t} = useLingui();
const form = useForm<FormInputs>();
@@ -42,7 +47,7 @@ export const BackupCodesViewModal = observer(() => {
const backupCodes = await MfaActionCreators.getBackupCodes();
ModalActionCreators.pop();
ModalActionCreators.pushWithKey(
modal(() => <BackupCodesModal backupCodes={backupCodes} />),
modal(() => <BackupCodesModal backupCodes={backupCodes} user={user} />),
'backup-codes',
);
};

View File

@@ -26,7 +26,7 @@ import styles from '@app/components/modals/EmailChangeModal.module.css';
import * as Modal from '@app/components/modals/Modal';
import {Button} from '@app/components/uikit/button/Button';
import {useFormSubmit} from '@app/hooks/useFormSubmit';
import UserStore from '@app/stores/UserStore';
import type {UserRecord} from '@app/records/UserRecord';
import {Trans, useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import {useEffect, useMemo, useState} from 'react';
@@ -38,9 +38,12 @@ interface NewEmailForm {
email: string;
}
export const EmailChangeModal = observer(() => {
interface EmailChangeModalProps {
user: UserRecord;
}
export const EmailChangeModal = observer(({user}: EmailChangeModalProps) => {
const {t} = useLingui();
const user = UserStore.getCurrentUser()!;
const newEmailForm = useForm<NewEmailForm>({defaultValues: {email: ''}});
const [stage, setStage] = useState<Stage>('intro');
const [ticket, setTicket] = useState<string | null>(null);

View File

@@ -33,7 +33,7 @@ import FocusRing from '@app/components/uikit/focus_ring/FocusRing';
import {PlutoniumUpsell} from '@app/components/uikit/plutonium_upsell/PlutoniumUpsell';
import {Tooltip} from '@app/components/uikit/tooltip/Tooltip';
import {useFormSubmit} from '@app/hooks/useFormSubmit';
import UserStore from '@app/stores/UserStore';
import type {UserRecord} from '@app/records/UserRecord';
import {LimitResolver} from '@app/utils/limits/LimitResolverAdapter';
import {isLimitToggleEnabled} from '@app/utils/limits/LimitUtils';
import {shouldShowPremiumFeatures} from '@app/utils/PremiumUtils';
@@ -47,9 +47,12 @@ interface FormInputs {
discriminator: string;
}
export const FluxerTagChangeModal = observer(() => {
interface FluxerTagChangeModalProps {
user: UserRecord;
}
export const FluxerTagChangeModal = observer(({user}: FluxerTagChangeModalProps) => {
const {t} = useLingui();
const user = UserStore.getCurrentUser()!;
const usernameRef = useRef<HTMLInputElement>(null);
const hasCustomDiscriminator = isLimitToggleEnabled(
{feature_custom_discriminator: LimitResolver.resolve({key: 'feature_custom_discriminator', fallback: 0})},
@@ -143,7 +146,7 @@ export const FluxerTagChangeModal = observer(() => {
ModalActionCreators.pop();
ToastActionCreators.createToast({type: 'success', children: t`FluxerTag updated`});
},
[hasCustomDiscriminator, user.username, user.discriminator],
[hasCustomDiscriminator],
);
const {handleSubmit, isSubmitting} = useFormSubmit({

View File

@@ -52,6 +52,7 @@ import {Logger} from '@app/lib/Logger';
import {TextareaAutosize} from '@app/lib/TextareaAutosize';
import type {ChannelRecord} from '@app/records/ChannelRecord';
import type {MessageRecord} from '@app/records/MessageRecord';
import type {UserRecord} from '@app/records/UserRecord';
import ChannelStore from '@app/stores/ChannelStore';
import MobileLayoutStore from '@app/stores/MobileLayoutStore';
import UserStore from '@app/stores/UserStore';
@@ -66,7 +67,12 @@ import {useCallback, useMemo, useRef, useState} from 'react';
const logger = new Logger('ForwardModal');
export const ForwardModal = observer(({message}: {message: MessageRecord}) => {
interface ForwardModalProps {
message: MessageRecord;
user: UserRecord;
}
export const ForwardModal = observer(({message, user}: ForwardModalProps) => {
const {t} = useLingui();
const {filteredChannels, handleToggleChannel, isChannelDisabled, searchQuery, selectedChannelIds, setSearchQuery} =
useForwardChannelSelection({excludedChannelId: message.channelId});
@@ -75,7 +81,6 @@ export const ForwardModal = observer(({message}: {message: MessageRecord}) => {
const [expressionPickerOpen, setExpressionPickerOpen] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const currentUser = UserStore.currentUser!;
const premiumMaxLength = Limits.getPremiumValue('max_message_length', MAX_MESSAGE_LENGTH_PREMIUM);
const mobileLayout = MobileLayoutStore;
@@ -88,7 +93,7 @@ export const ForwardModal = observer(({message}: {message: MessageRecord}) => {
textareaRef,
segmentManagerRef,
previousValueRef,
maxActualLength: currentUser.maxMessageLength,
maxActualLength: user.maxMessageLength,
onExceedMaxLength: handleOptionalMessageExceedsLimit,
});
@@ -109,7 +114,7 @@ export const ForwardModal = observer(({message}: {message: MessageRecord}) => {
textareaRef,
segmentManagerRef,
previousValueRef,
maxActualLength: currentUser.maxMessageLength,
maxActualLength: user.maxMessageLength,
onExceedMaxLength: handleOptionalMessageExceedsLimit,
});
@@ -119,14 +124,14 @@ export const ForwardModal = observer(({message}: {message: MessageRecord}) => {
segmentManagerRef,
setValue: setOptionalMessage,
previousValueRef,
maxMessageLength: currentUser.maxMessageLength,
maxMessageLength: user.maxMessageLength,
onPasteExceedsLimit: () => handleOptionalMessageExceedsLimit(),
});
const actualOptionalMessage = useMemo(() => displayToActual(optionalMessage), [displayToActual, optionalMessage]);
const optionalMessageDisplayMaxLength = useMemo(() => {
return Math.max(0, optionalMessage.length + (currentUser.maxMessageLength - actualOptionalMessage.length));
}, [actualOptionalMessage.length, currentUser.maxMessageLength, optionalMessage.length]);
return Math.max(0, optionalMessage.length + (user.maxMessageLength - actualOptionalMessage.length));
}, [actualOptionalMessage.length, user.maxMessageLength, optionalMessage.length]);
const handleForward = async () => {
if (selectedChannelIds.size === 0 || isForwarding) return;
@@ -310,8 +315,8 @@ export const ForwardModal = observer(({message}: {message: MessageRecord}) => {
/>
<MessageCharacterCounter
currentLength={actualOptionalMessage.length}
maxLength={currentUser.maxMessageLength}
canUpgrade={currentUser.maxMessageLength < premiumMaxLength}
maxLength={user.maxMessageLength}
canUpgrade={user.maxMessageLength < premiumMaxLength}
premiumMaxLength={premiumMaxLength}
/>
<div className={modalStyles.messageInputActions}>

View File

@@ -29,7 +29,7 @@ import * as Modal from '@app/components/modals/Modal';
import {Button} from '@app/components/uikit/button/Button';
import {QRCodeCanvas} from '@app/components/uikit/QRCodeCanvas';
import {useFormSubmit} from '@app/hooks/useFormSubmit';
import UserStore from '@app/stores/UserStore';
import type {UserRecord} from '@app/records/UserRecord';
import * as MfaUtils from '@app/utils/MfaUtils';
import {Trans, useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
@@ -40,9 +40,12 @@ interface FormInputs {
code: string;
}
export const MfaTotpEnableModal = observer(() => {
interface MfaTotpEnableModalProps {
user: UserRecord;
}
export const MfaTotpEnableModal = observer(({user}: MfaTotpEnableModalProps) => {
const {t} = useLingui();
const user = UserStore.getCurrentUser()!;
const form = useForm<FormInputs>();
const [secret] = useState(() => MfaUtils.generateTotpSecret());
@@ -54,7 +57,7 @@ export const MfaTotpEnableModal = observer(() => {
ModalActionCreators.pop();
ToastActionCreators.createToast({type: 'success', children: <Trans>Two-factor authentication enabled</Trans>});
ModalActionCreators.pushWithKey(
modal(() => <BackupCodesModal backupCodes={backupCodes} />),
modal(() => <BackupCodesModal backupCodes={backupCodes} user={user} />),
'backup-codes',
);
};

View File

@@ -61,6 +61,11 @@ export const MobileVideoViewer = observer(function MobileVideoViewer({
const hudTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [hasInitialized, setHasInitialized] = useState(false);
const safelyPlayVideo = useCallback((video: HTMLVideoElement) => {
const playPromise = video.play();
void playPromise?.catch(() => {});
}, []);
const progress = duration > 0 ? currentTime / duration : 0;
const scheduleHudHide = useCallback(() => {
@@ -86,18 +91,21 @@ export const MobileVideoViewer = observer(function MobileVideoViewer({
setZoomScale(state.scale);
}, []);
const handlePlayPause = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
const video = videoRef.current;
if (!video) return;
const handlePlayPause = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
const video = videoRef.current;
if (!video) return;
if (video.paused) {
video.play();
transformRef.current?.resetTransform();
} else {
video.pause();
}
}, []);
if (video.paused) {
safelyPlayVideo(video);
transformRef.current?.resetTransform();
} else {
video.pause();
}
},
[safelyPlayVideo],
);
const handleToggleMute = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
@@ -139,7 +147,7 @@ export const MobileVideoViewer = observer(function MobileVideoViewer({
if (initialTime && !hasInitialized) {
video.currentTime = initialTime;
setHasInitialized(true);
video.play();
safelyPlayVideo(video);
}
};
const handleDurationChange = () => setDuration(video.duration);
@@ -157,7 +165,7 @@ export const MobileVideoViewer = observer(function MobileVideoViewer({
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
video.removeEventListener('durationchange', handleDurationChange);
};
}, [initialTime, hasInitialized, scheduleHudHide]);
}, [initialTime, hasInitialized, scheduleHudHide, safelyPlayVideo]);
useEffect(() => {
const video = videoRef.current;

View File

@@ -672,7 +672,7 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
) : (
<div className={styles.contentLayout}>
<div className={styles.formColumn}>
{!isPerGuildProfile && <UsernameSection isClaimed={isClaimed} discriminator={user.discriminator} />}
{!isPerGuildProfile && <UsernameSection isClaimed={isClaimed} user={user} />}
{isPerGuildProfile && (
<div>

View File

@@ -77,7 +77,10 @@ export const AccountTabContent: React.FC<AccountTabProps> = observer(
</button>
</div>
</div>
<Button small={true} onClick={() => ModalActionCreators.push(modal(() => <EmailChangeModal />))}>
<Button
small={true}
onClick={() => ModalActionCreators.push(modal(() => <EmailChangeModal user={user} />))}
>
<Trans>Change Email</Trans>
</Button>
</div>

View File

@@ -240,7 +240,10 @@ export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
<Trans>Disable</Trans>
</Button>
) : (
<Button small={true} onClick={() => ModalActionCreators.push(modal(() => <MfaTotpEnableModal />))}>
<Button
small={true}
onClick={() => ModalActionCreators.push(modal(() => <MfaTotpEnableModal user={user} />))}
>
<Trans>Enable</Trans>
</Button>
)}
@@ -260,7 +263,7 @@ export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
<Button
variant="secondary"
small={true}
onClick={() => ModalActionCreators.push(modal(() => <BackupCodesViewModal />))}
onClick={() => ModalActionCreators.push(modal(() => <BackupCodesViewModal user={user} />))}
>
<Trans>View Codes</Trans>
</Button>

View File

@@ -24,6 +24,7 @@ import {FluxerTagChangeModal} from '@app/components/modals/FluxerTagChangeModal'
import styles from '@app/components/modals/tabs/my_profile_tab/UsernameSection.module.css';
import {Button} from '@app/components/uikit/button/Button';
import {Tooltip} from '@app/components/uikit/tooltip/Tooltip';
import type {UserRecord} from '@app/records/UserRecord';
import {LimitResolver} from '@app/utils/limits/LimitResolverAdapter';
import {isLimitToggleEnabled} from '@app/utils/limits/LimitUtils';
import {shouldShowPremiumFeatures} from '@app/utils/PremiumUtils';
@@ -34,10 +35,10 @@ import {observer} from 'mobx-react-lite';
interface UsernameSectionProps {
isClaimed: boolean;
discriminator: string;
user: UserRecord;
}
export const UsernameSection = observer(({isClaimed, discriminator}: UsernameSectionProps) => {
export const UsernameSection = observer(({isClaimed, user}: UsernameSectionProps) => {
const {t} = useLingui();
const hasCustomDiscriminator = isLimitToggleEnabled(
@@ -64,14 +65,14 @@ export const UsernameSection = observer(({isClaimed, discriminator}: UsernameSec
<Button
variant="primary"
small
onClick={() => ModalActionCreators.push(modal(() => <FluxerTagChangeModal />))}
onClick={() => ModalActionCreators.push(modal(() => <FluxerTagChangeModal user={user} />))}
>
<Trans>Change FluxerTag</Trans>
</Button>
)}
{!hasCustomDiscriminator && shouldShowPremiumFeatures() && (
<Tooltip text={t(msg`Customize your 4-digit tag (#${discriminator}) to your liking with Plutonium`)}>
<Tooltip text={t(msg`Customize your 4-digit tag (#${user.discriminator}) to your liking with Plutonium`)}>
<button
type="button"
onClick={() => {

View File

@@ -62,7 +62,7 @@ export const UserFilterSheet: React.FC<UserFilterSheetProps> = observer(
const members = GuildMemberStore.getMembers(channel.guildId);
return members.map((m) => m.user);
}
return channel.recipientIds.map((id) => UserStore.getUser(id)).filter((u): u is UserRecord => u !== null);
return channel.recipientIds.map((id) => UserStore.getUser(id)).filter((u): u is UserRecord => u != null);
}, [channel.guildId, channel.recipientIds]);
const filteredUsers = useMemo(() => {

View File

@@ -139,6 +139,14 @@ function initSentry(): void {
if (error.name === 'HTTPResponseError' || error.name === 'TimeoutError') {
return null;
}
const isBlobWorkerImportScriptsFailure =
error.name === 'NetworkError' &&
error.message.includes("Failed to execute 'importScripts' on 'WorkerGlobalScope'") &&
error.message.includes('blob:');
if (isBlobWorkerImportScriptsFailure) {
return null;
}
}
return event;
},