fix: various fixes to sentry-reported errors
This commit is contained in:
parent
eb194ae5be
commit
24e9a1529d
@ -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);
|
||||
})();
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>) => {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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',
|
||||
);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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',
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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={() => {
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
@ -89,7 +89,7 @@ export class GuildRoleRepository extends IGuildRoleRepository {
|
||||
}),
|
||||
(current) => ({
|
||||
pk: {guild_id: guildId, role_id: roleId},
|
||||
patch: buildPatchFromData(data, current, GUILD_ROLE_COLUMNS, ['guild_id', 'role_id']),
|
||||
patch: buildPatchFromData(data, oldData ?? current, GUILD_ROLE_COLUMNS, ['guild_id', 'role_id']),
|
||||
}),
|
||||
GuildRoles,
|
||||
{initialData: oldData},
|
||||
|
||||
@ -26,6 +26,6 @@ export abstract class IGuildRoleRepository {
|
||||
abstract listRoles(guildId: GuildID): Promise<Array<GuildRole>>;
|
||||
abstract listRolesByIds(roleIds: Array<RoleID>, guildId: GuildID): Promise<Array<GuildRole>>;
|
||||
abstract countRoles(guildId: GuildID): Promise<number>;
|
||||
abstract upsertRole(data: GuildRoleRow): Promise<GuildRole>;
|
||||
abstract upsertRole(data: GuildRoleRow, oldData?: GuildRoleRow | null): Promise<GuildRole>;
|
||||
abstract deleteRole(guildId: GuildID, roleId: RoleID): Promise<void>;
|
||||
}
|
||||
|
||||
@ -257,7 +257,7 @@ export class GuildRoleService {
|
||||
mentionable: updateData.mentionable ?? role.isMentionable,
|
||||
};
|
||||
|
||||
const updatedRole = await this.guildRoleRepository.upsertRole(updatedRoleData);
|
||||
const updatedRole = await this.guildRoleRepository.upsertRole(updatedRoleData, role.toRow());
|
||||
|
||||
await this.dispatchGuildRoleUpdate({guildId, role: updatedRole});
|
||||
|
||||
@ -424,10 +424,14 @@ export class GuildRoleService {
|
||||
const role = roleMap.get(update.roleId)!;
|
||||
if (role.hoistPosition === update.hoistPosition) continue;
|
||||
|
||||
const updatedRole = await this.guildRoleRepository.upsertRole({
|
||||
...role.toRow(),
|
||||
hoist_position: update.hoistPosition,
|
||||
});
|
||||
const roleRow = role.toRow();
|
||||
const updatedRole = await this.guildRoleRepository.upsertRole(
|
||||
{
|
||||
...roleRow,
|
||||
hoist_position: update.hoistPosition,
|
||||
},
|
||||
roleRow,
|
||||
);
|
||||
changedRoles.push(updatedRole);
|
||||
}
|
||||
|
||||
@ -460,10 +464,14 @@ export class GuildRoleService {
|
||||
for (const role of allRoles) {
|
||||
if (role.hoistPosition === null) continue;
|
||||
|
||||
const updatedRole = await this.guildRoleRepository.upsertRole({
|
||||
...role.toRow(),
|
||||
hoist_position: null,
|
||||
});
|
||||
const roleRow = role.toRow();
|
||||
const updatedRole = await this.guildRoleRepository.upsertRole(
|
||||
{
|
||||
...roleRow,
|
||||
hoist_position: null,
|
||||
},
|
||||
roleRow,
|
||||
);
|
||||
changedRoles.push(updatedRole);
|
||||
}
|
||||
|
||||
@ -649,7 +657,11 @@ export class GuildRoleService {
|
||||
const reorderedIds = targetOrder.map((r) => r.id);
|
||||
const reorderedRoles = this.reorderRolePositions({allRoles, reorderedIds, guildId});
|
||||
|
||||
const updatePromises = reorderedRoles.map((role) => this.guildRoleRepository.upsertRole(role.toRow()));
|
||||
const updatePromises = reorderedRoles.map((role) => {
|
||||
const roleRow = role.toRow();
|
||||
const oldRole = roleMap.get(role.id);
|
||||
return this.guildRoleRepository.upsertRole(roleRow, oldRole ? oldRole.toRow() : undefined);
|
||||
});
|
||||
await Promise.all(updatePromises);
|
||||
|
||||
const updatedRoles = await this.guildRoleRepository.listRoles(guildId);
|
||||
|
||||
@ -82,6 +82,7 @@ export class ChannelHelpers {
|
||||
static validateChannelVoicePlacement(
|
||||
finalChannels: Array<Channel>,
|
||||
parentMap: Map<ChannelID, ChannelID | null>,
|
||||
parentIdsToValidate?: ReadonlySet<ChannelID>,
|
||||
): void {
|
||||
const orderedByParent = new Map<ChannelID | null, Array<Channel>>();
|
||||
|
||||
@ -95,6 +96,9 @@ export class ChannelHelpers {
|
||||
|
||||
for (const [parentId, siblings] of orderedByParent.entries()) {
|
||||
if (parentId === null) continue;
|
||||
if (parentIdsToValidate && !parentIdsToValidate.has(parentId)) {
|
||||
continue;
|
||||
}
|
||||
let encounteredVoice = false;
|
||||
for (const sibling of siblings) {
|
||||
if (sibling.type === ChannelTypes.GUILD_VOICE) {
|
||||
|
||||
@ -570,11 +570,18 @@ export class ChannelOperationsService {
|
||||
const desiredParentId = plan.desiredParentById.get(operation.channelId) ?? null;
|
||||
const targetChannel = allChannels.find((ch) => ch.id === operation.channelId);
|
||||
const currentParentId = targetChannel?.parentId ?? null;
|
||||
const parentIdsToValidate = new Set<ChannelID>();
|
||||
if (currentParentId) {
|
||||
parentIdsToValidate.add(currentParentId);
|
||||
}
|
||||
if (desiredParentId) {
|
||||
parentIdsToValidate.add(desiredParentId);
|
||||
}
|
||||
if (desiredParentId && desiredParentId !== currentParentId) {
|
||||
await this.ensureCategoryHasCapacity({guildId, categoryId: desiredParentId});
|
||||
}
|
||||
|
||||
ChannelHelpers.validateChannelVoicePlacement(plan.finalChannels, plan.desiredParentById);
|
||||
ChannelHelpers.validateChannelVoicePlacement(plan.finalChannels, plan.desiredParentById, parentIdsToValidate);
|
||||
|
||||
if (plan.orderUnchanged) {
|
||||
return;
|
||||
|
||||
@ -18,6 +18,8 @@
|
||||
*/
|
||||
|
||||
import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {createGuildID} from '@fluxer/api/src/BrandedTypes';
|
||||
import {ChannelRepository} from '@fluxer/api/src/channel/ChannelRepository';
|
||||
import {
|
||||
acceptInvite,
|
||||
addMemberRole,
|
||||
@ -385,6 +387,60 @@ describe('Guild Channel Positions', () => {
|
||||
expect(response.errors[0]?.code).toBe(ValidationErrorCodes.CANNOT_POSITION_CHANNEL_RELATIVE_TO_ITSELF);
|
||||
});
|
||||
|
||||
test('should reorder top-level categories despite unrelated legacy voice and text ordering', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Test Guild');
|
||||
|
||||
const milsims = await createChannel(harness, account.token, guild.id, 'MILSIMS', ChannelTypes.GUILD_CATEGORY);
|
||||
const coopGames = await createChannel(harness, account.token, guild.id, 'COOP GAMES', ChannelTypes.GUILD_CATEGORY);
|
||||
const frontDoor = await createChannel(harness, account.token, guild.id, 'FRONT DOOR', ChannelTypes.GUILD_CATEGORY);
|
||||
const coopText = await createChannel(harness, account.token, guild.id, 'coop-text', ChannelTypes.GUILD_TEXT);
|
||||
const coopVoice = await createChannel(harness, account.token, guild.id, 'coop-voice', ChannelTypes.GUILD_VOICE);
|
||||
|
||||
await updateChannelPositions(harness, account.token, guild.id, [
|
||||
{id: coopText.id, parent_id: coopGames.id},
|
||||
{id: coopVoice.id, parent_id: coopGames.id},
|
||||
]);
|
||||
|
||||
const channelRepository = new ChannelRepository();
|
||||
const channelsBeforeMove = await channelRepository.listGuildChannels(createGuildID(BigInt(guild.id)));
|
||||
const storedCoopText = channelsBeforeMove.find((channel) => channel.id.toString() === coopText.id);
|
||||
const storedCoopVoice = channelsBeforeMove.find((channel) => channel.id.toString() === coopVoice.id);
|
||||
|
||||
expect(storedCoopText).toBeDefined();
|
||||
expect(storedCoopVoice).toBeDefined();
|
||||
if (!storedCoopText || !storedCoopVoice) {
|
||||
return;
|
||||
}
|
||||
|
||||
await channelRepository.upsert({...storedCoopVoice.toRow(), position: 1});
|
||||
await channelRepository.upsert({...storedCoopText.toRow(), position: 2});
|
||||
|
||||
await updateChannelPositions(harness, account.token, guild.id, [
|
||||
{
|
||||
id: frontDoor.id,
|
||||
parent_id: null,
|
||||
preceding_sibling_id: milsims.id,
|
||||
},
|
||||
]);
|
||||
|
||||
const channels = await getGuildChannels(harness, account.token, guild.id);
|
||||
const orderedRootCategories = channels
|
||||
.filter((channel) => channel.type === ChannelTypes.GUILD_CATEGORY && channel.parent_id == null)
|
||||
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
||||
.map((channel) => channel.id);
|
||||
|
||||
const milsimsIndex = orderedRootCategories.indexOf(milsims.id);
|
||||
const frontDoorIndex = orderedRootCategories.indexOf(frontDoor.id);
|
||||
const coopGamesIndex = orderedRootCategories.indexOf(coopGames.id);
|
||||
|
||||
expect(milsimsIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(frontDoorIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(coopGamesIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(frontDoorIndex).toBeGreaterThan(milsimsIndex);
|
||||
expect(frontDoorIndex).toBeLessThan(coopGamesIndex);
|
||||
});
|
||||
|
||||
test('should reject text channels being positioned below voice channels via preceding_sibling_id', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Test Guild');
|
||||
|
||||
@ -18,6 +18,8 @@
|
||||
*/
|
||||
|
||||
import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {createGuildID, createRoleID} from '@fluxer/api/src/BrandedTypes';
|
||||
import {GuildRoleRepository} from '@fluxer/api/src/guild/repositories/GuildRoleRepository';
|
||||
import {
|
||||
acceptInvite,
|
||||
addMemberRole,
|
||||
@ -175,4 +177,43 @@ describe('Guild Role Operations', () => {
|
||||
|
||||
expect(newRole.name).toBe('Member Created Role');
|
||||
});
|
||||
|
||||
test('should preserve concurrent position updates when applying stale role snapshot', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Test Guild');
|
||||
|
||||
const role = await createRole(harness, account.token, guild.id, {
|
||||
name: 'Role',
|
||||
});
|
||||
|
||||
const guildId = createGuildID(BigInt(guild.id));
|
||||
const roleId = createRoleID(BigInt(role.id));
|
||||
const roleRepository = new GuildRoleRepository();
|
||||
const staleRole = await roleRepository.getRole(roleId, guildId);
|
||||
expect(staleRole).toBeDefined();
|
||||
if (!staleRole) {
|
||||
return;
|
||||
}
|
||||
|
||||
const staleRoleRow = staleRole.toRow();
|
||||
const movedRole = await roleRepository.upsertRole(
|
||||
{
|
||||
...staleRoleRow,
|
||||
position: staleRoleRow.position + 5,
|
||||
},
|
||||
staleRoleRow,
|
||||
);
|
||||
|
||||
await roleRepository.upsertRole(
|
||||
{
|
||||
...staleRoleRow,
|
||||
name: 'Renamed Role',
|
||||
},
|
||||
staleRoleRow,
|
||||
);
|
||||
|
||||
const finalRole = await roleRepository.getRole(roleId, guildId);
|
||||
expect(finalRole?.name).toBe('Renamed Role');
|
||||
expect(finalRole?.position).toBe(movedRole.position);
|
||||
});
|
||||
});
|
||||
|
||||
@ -191,6 +191,7 @@ export async function initializeWorkerDependencies(snowflakeService: SnowflakeSe
|
||||
const limitConfigSubscriber = getKVClient();
|
||||
const limitConfigService = new LimitConfigService(instanceConfigRepository, cacheService, limitConfigSubscriber);
|
||||
await limitConfigService.initialize();
|
||||
limitConfigService.setAsGlobalInstance();
|
||||
const userCacheService = new UserCacheService(cacheService, userRepository);
|
||||
const storageService = createStorageService({s3Service: getInjectedS3Service()});
|
||||
const csamEvidenceRetentionService = new CsamEvidenceRetentionService(storageService);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user