fix(media): stream large files and honour size settings (#3387)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-11 13:28:24 +02:00
1 parent 6acd7f3d22
commit 45b78a13c2
97 files changed
+2547 -409

No files matched your search

@@ -35,8 +35,13 @@
padding: 1.25rem;
}
.contentScroller > .contentFrame {
flex: 1 0 auto;
}
.content {
display: flex;
flex: 1 0 auto;
flex-direction: column;
justify-content: center;
min-height: min(33rem, calc(100svh - 12.5rem));
@@ -1004,7 +1004,11 @@ export const SelfHostedSetupWizardGate = observer(() => {
hideCloseButton
data-flx="app.self-hosted-setup-wizard-gate.header"
/>
<Modal.Content className={styles.contentScroller} data-flx="app.self-hosted-setup-wizard-gate.content">
<Modal.Content
className={styles.contentScroller}
contentClassName={styles.contentFrame}
data-flx="app.self-hosted-setup-wizard-gate.content"
>
<div className={styles.content} data-flx="app.self-hosted-setup-wizard-gate.content-inner">
{loadError ? (
<div className={styles.centeredStep} data-flx="app.self-hosted-setup-wizard-gate.load-error-wrap">
@@ -67,7 +67,6 @@ export const STATIC_IMAGE_WITH_AVIF_FORMATS = formatAssetUploadExtensions('splas
export const ANIMATED_IMAGE_FORMATS = formatAssetUploadExtensions('avatar');
export const ANIMATED_AVATAR_FORMATS = formatKnownAnimatedAssetExtensions('avatar');
export const AVIF_FORMAT_LABEL = 'AVIF';
export const IMAGE_MAX_SIZE_BYTES = 10 * 1024 * 1024;
export const BACKGROUND_MEDIA_MAX_SIZE_BYTES = 10 * 1024 * 1024;
export const CUSTOM_SOUND_MAX_SIZE_BYTES = 2 * 1024 * 1024;
export const AVATAR_RECOMMENDED_SIZE_LABEL = '512×512px';
@@ -1,25 +1,14 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {LimitResolver} from '@app/features/app/utils/LimitResolverAdapter';
const FALLBACKS = {
emoji_max_size: 384 * 1024,
sticker_max_size: 512 * 1024,
avatar_max_size: 10 * 1024 * 1024,
} as const;
import {AVATAR_MAX_SIZE} from '@fluxer/constants/src/LimitConstants';
class GlobalLimitsClass {
getEmojiMaxSize(): number {
getAvatarMaxSize(): number {
return LimitResolver.resolve({
key: 'emoji_max_size',
fallback: FALLBACKS.emoji_max_size,
});
}
getStickerMaxSize(): number {
return LimitResolver.resolve({
key: 'sticker_max_size',
fallback: FALLBACKS.sticker_max_size,
key: 'avatar_max_size',
fallback: AVATAR_MAX_SIZE,
context: {traits: [], guildFeatures: []},
});
}
}
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {AVATAR_MAX_SIZE, EMOJI_MAX_SIZE, STICKER_MAX_SIZE} from '@fluxer/constants/src/LimitConstants';
import type {LimitConfigSnapshot} from '@fluxer/limits/src/LimitTypes';
import type {Guild as WireGuild} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
import {beforeEach, describe, expect, it, vi} from 'vitest';
const RAISED = 5 * 1024 * 1024;
const state: {limits: LimitConfigSnapshot} = {limits: {traitDefinitions: [], rules: []}};
vi.mock('@app/features/app/state/RuntimeConfig', () => ({
default: {
localInstanceDomain: 'fluxer.test',
get limits() {
return state.limits;
},
getSnapshotOrNull: () => ({limits: state.limits}),
},
}));
vi.mock('@app/features/app/state/InstanceSnapshotStore', () => ({default: {getLimitsForInstance: () => null}}));
vi.mock('@app/features/user/state/Users', () => ({default: {getCurrentUser: () => ({traits: ['premium']})}}));
vi.mock('@app/features/channel/state/Channels', () => ({default: {getChannel: () => null}}));
vi.mock('@app/features/guild/state/Guilds', () => ({default: {getGuild: () => null}}));
vi.mock('@app/features/navigation/state/SelectedChannel', () => ({default: {currentChannelId: null}}));
vi.mock('@app/features/navigation/state/SelectedGuild', () => ({default: {selectedGuildId: null}}));
vi.mock('@app/features/theme/fonts/ScriptFontLoader', () => ({noteText: () => {}}));
const {Guild} = await import('@app/features/guild/models/Guild');
const {GlobalLimits} = await import('@app/features/app/utils/GlobalLimits');
function guild(features: Array<string> = []) {
return new Guild({id: '1', name: 'guild', owner_id: '2', features, roles: []} as unknown as WireGuild);
}
beforeEach(() => {
state.limits = {traitDefinitions: ['premium'], rules: [{id: 'default', limits: {}}]};
});
describe('configured image size limits', () => {
it('falls back to the stock sizes when nothing is configured', () => {
expect(guild().maxEmojiSize).toBe(EMOJI_MAX_SIZE);
expect(guild().maxStickerSize).toBe(STICKER_MAX_SIZE);
expect(GlobalLimits.getAvatarMaxSize()).toBe(AVATAR_MAX_SIZE);
});
it('follows a raised default rule', () => {
state.limits.rules[0].limits = {emoji_max_size: RAISED, sticker_max_size: RAISED, avatar_max_size: RAISED * 6};
expect(guild().maxEmojiSize).toBe(RAISED);
expect(guild().maxStickerSize).toBe(RAISED);
expect(GlobalLimits.getAvatarMaxSize()).toBe(RAISED * 6);
});
it('applies guild feature rules to emoji and sticker sizes', () => {
state.limits.rules.push({
id: 'boosted',
filters: {guildFeatures: ['MORE_EMOJI']},
limits: {emoji_max_size: RAISED, sticker_max_size: RAISED},
});
expect(guild().maxEmojiSize).toBe(EMOJI_MAX_SIZE);
expect(guild(['MORE_EMOJI']).maxEmojiSize).toBe(RAISED);
expect(guild(['MORE_EMOJI']).maxStickerSize).toBe(RAISED);
});
it('ignores trait rules the server does not apply', () => {
state.limits.rules.push({
id: 'premium',
filters: {traits: ['premium']},
limits: {emoji_max_size: RAISED, sticker_max_size: RAISED, avatar_max_size: RAISED * 6},
});
expect(guild().maxEmojiSize).toBe(EMOJI_MAX_SIZE);
expect(guild().maxStickerSize).toBe(STICKER_MAX_SIZE);
expect(GlobalLimits.getAvatarMaxSize()).toBe(AVATAR_MAX_SIZE);
});
});
@@ -1,11 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {
AVATAR_RECOMMENDED_SIZE_LABEL,
IMAGE_MAX_SIZE_BYTES,
STATIC_IMAGE_FORMATS,
} from '@app/features/app/config/I18nDisplayConstants';
import {AVATAR_RECOMMENDED_SIZE_LABEL, STATIC_IMAGE_FORMATS} from '@app/features/app/config/I18nDisplayConstants';
import {useFormSubmit} from '@app/features/app/hooks/useFormSubmit';
import {GlobalLimits} from '@app/features/app/utils/GlobalLimits';
import Authentication from '@app/features/auth/state/Authentication';
import * as ChannelCommands from '@app/features/channel/commands/ChannelCommands';
import {showChannelErrorModal} from '@app/features/channel/components/alerts/ChannelErrorModalUtils';
@@ -85,11 +82,11 @@ export function useEditGroupForm({
async (file: File | null) => {
try {
if (!file) return;
if (file.size > 10 * 1024 * 1024) {
if (file.size > GlobalLimits.getAvatarMaxSize()) {
showChannelErrorModal({
title: i18n._(messages.iconFileTooLargeTitle),
message: i18n._(messages.iconFileTooLarge, {
imageMaxSizeLabel: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
imageMaxSizeLabel: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
}),
dataFlx: `${dataFlx}.icon-file-too-large.generic-error-modal`,
});
@@ -166,7 +163,7 @@ export function useEditGroupForm({
title: i18n._(messages.changeIcon),
uploadHint: formatImageUploadRecommendedHint(i18n, {
formats: STATIC_IMAGE_FORMATS,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
recommendedSize: AVATAR_RECOMMENDED_SIZE_LABEL,
}),
onPickUpload: handleIconUploadClick,
@@ -4,12 +4,12 @@ import {GenericErrorModal} from '@app/features/app/components/alerts/GenericErro
import {ConfirmModal} from '@app/features/app/components/dialogs/ConfirmModal';
import * as Modal from '@app/features/app/components/dialogs/Modal';
import {useFormSubmit} from '@app/features/app/hooks/useFormSubmit';
import {GlobalLimits} from '@app/features/app/utils/GlobalLimits';
import * as GuildStickerCommands from '@app/features/expressions/commands/GuildStickerCommands';
import styles from '@app/features/expressions/components/modals/AddGuildStickerModal.module.css';
import {StickerFormFields} from '@app/features/expressions/components/modals/sticker_form/StickerFormFields';
import {StickerPreview} from '@app/features/expressions/components/modals/sticker_form/StickerPreview';
import * as ImageCropUtils from '@app/features/expressions/utils/ImageCropUtils';
import Guilds from '@app/features/guild/state/Guilds';
import {CREATE_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {formatFileSize} from '@app/features/messaging/utils/FileUtils';
import {HttpError} from '@app/features/platform/types/EndpointError';
@@ -20,6 +20,7 @@ import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {Form} from '@app/features/ui/components/form/Form';
import * as FormUtils from '@app/lib/forms';
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
import {STICKER_MAX_SIZE} from '@fluxer/constants/src/LimitConstants';
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
import type {I18n} from '@lingui/core';
import {msg} from '@lingui/core/macro';
@@ -143,7 +144,7 @@ export const AddGuildStickerModal = observer(function AddGuildStickerModal({
}, [file]);
const onSubmit = useCallback(
async (data: FormInputs) => {
const maxStickerSize = GlobalLimits.getStickerMaxSize();
const maxStickerSize = Guilds.getGuild(guildId)?.maxStickerSize ?? STICKER_MAX_SIZE;
setIsProcessing(true);
try {
const base64Image = await ImageCropUtils.optimizeStickerImage(file, maxStickerSize, 320);
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {GlobalLimits} from '@app/features/app/utils/GlobalLimits';
import {
getAspectRatioRange,
type ImageDimensions,
@@ -134,7 +135,6 @@ interface AssetConfig {
maxHeight: number;
minWidth: number;
minHeight: number;
sizeLimitBytes: number;
minHeightRatio?: number;
maxHeightRatio?: number;
}
@@ -147,7 +147,6 @@ const ASSET_CONFIGS: Record<AssetType, AssetConfig> = {
maxHeight: 1024,
minWidth: 256,
minHeight: 256,
sizeLimitBytes: 10 * 1024 * 1024,
},
[AssetType.GUILD_ICON]: {
aspectRatio: 1,
@@ -156,7 +155,6 @@ const ASSET_CONFIGS: Record<AssetType, AssetConfig> = {
maxHeight: 1024,
minWidth: 256,
minHeight: 256,
sizeLimitBytes: 10 * 1024 * 1024,
},
[AssetType.CHANNEL_ICON]: {
aspectRatio: 1,
@@ -165,7 +163,6 @@ const ASSET_CONFIGS: Record<AssetType, AssetConfig> = {
maxHeight: 1024,
minWidth: 256,
minHeight: 256,
sizeLimitBytes: 10 * 1024 * 1024,
},
[AssetType.GUILD_BANNER]: {
aspectRatio: 16 / 9,
@@ -174,7 +171,6 @@ const ASSET_CONFIGS: Record<AssetType, AssetConfig> = {
maxHeight: 1152,
minWidth: 960,
minHeight: 540,
sizeLimitBytes: 10 * 1024 * 1024,
minHeightRatio: 0.5,
maxHeightRatio: 1,
},
@@ -185,7 +181,6 @@ const ASSET_CONFIGS: Record<AssetType, AssetConfig> = {
maxHeight: 723,
minWidth: 680,
minHeight: 240,
sizeLimitBytes: 10 * 1024 * 1024,
},
[AssetType.SPLASH]: {
aspectRatio: 16 / 9,
@@ -194,7 +189,6 @@ const ASSET_CONFIGS: Record<AssetType, AssetConfig> = {
maxHeight: 1152,
minWidth: 960,
minHeight: 540,
sizeLimitBytes: 10 * 1024 * 1024,
minHeightRatio: 0.5,
maxHeightRatio: 1,
},
@@ -205,7 +199,6 @@ const ASSET_CONFIGS: Record<AssetType, AssetConfig> = {
maxHeight: 1152,
minWidth: 960,
minHeight: 540,
sizeLimitBytes: 10 * 1024 * 1024,
minHeightRatio: 0.5,
maxHeightRatio: 1,
},
@@ -317,7 +310,7 @@ export const AssetCropModal: React.FC<AssetCropModalProps> = observer(
cropShape={config.cropShape}
maxWidth={config.maxWidth}
maxHeight={config.maxHeight}
sizeLimitBytes={config.sizeLimitBytes}
sizeLimitBytes={GlobalLimits.getAvatarMaxSize()}
minHeightRatio={config.minHeightRatio}
maxHeightRatio={config.maxHeightRatio}
canSkipOriginal={(dimensions) => canSkipOriginalAssetImage(assetType, dimensions)}
@@ -3,12 +3,9 @@
import {Routes} from '@app/app/Routes';
import {UserSettingsModal} from '@app/features/app/components/dialogs/LoadableSettingsModals';
import {ExternalLink} from '@app/features/app/components/shared/ExternalLink';
import {
AVATAR_RECOMMENDED_SIZE_LABEL,
IMAGE_MAX_SIZE_BYTES,
STATIC_IMAGE_FORMATS,
} from '@app/features/app/config/I18nDisplayConstants';
import {AVATAR_RECOMMENDED_SIZE_LABEL, STATIC_IMAGE_FORMATS} from '@app/features/app/config/I18nDisplayConstants';
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
import {GlobalLimits} from '@app/features/app/utils/GlobalLimits';
import {openClaimAccountModal} from '@app/features/auth/components/modals/ClaimAccountModal';
import {AssetCropModal, AssetType} from '@app/features/expressions/components/modals/AssetCropModal';
import {openAssetSourceModal} from '@app/features/expressions/components/modals/AssetSourceModal';
@@ -95,10 +92,10 @@ export function useGuildCreateIconField({
try {
const [file] = await openFilePicker({accept: getAcceptStringFiltered('guild_icon', false)});
if (!file) return;
if (file.size > 10 * 1024 * 1024) {
if (file.size > GlobalLimits.getAvatarMaxSize()) {
showIconUploadErrorModal(
i18n._(ICON_FILE_IS_TOO_LARGE_PLEASE_CHOOSE_A_DESCRIPTOR, {
imageMaxSizeLabel: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
imageMaxSizeLabel: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
}),
);
return;
@@ -150,7 +147,7 @@ export function useGuildCreateIconField({
title: i18n._(messages.changeIcon),
uploadHint: formatImageUploadRecommendedHint(i18n, {
formats: STATIC_IMAGE_FORMATS,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
recommendedSize: AVATAR_RECOMMENDED_SIZE_LABEL,
}),
onPickUpload: handleIconUpload,
@@ -2,7 +2,6 @@
import {GenericErrorModal} from '@app/features/app/components/alerts/GenericErrorModal';
import {ConfirmModal} from '@app/features/app/components/dialogs/ConfirmModal';
import {GlobalLimits} from '@app/features/app/utils/GlobalLimits';
import {EmojiListHeader, EmojiListItem} from '@app/features/emoji/components/emojis/EmojiListItem';
import {EmojiUploadModal} from '@app/features/emoji/components/modals/EmojiUploadModal';
import EmojiStickerLayout from '@app/features/emoji/state/EmojiStickerLayout';
@@ -33,7 +32,7 @@ import {Spinner} from '@app/features/ui/components/Spinner';
import Users from '@app/features/user/state/Users';
import {canCropFormat} from '@app/features/voice/utils/MediaCapabilities';
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
import {MAX_GUILD_EMOJIS} from '@fluxer/constants/src/LimitConstants';
import {EMOJI_MAX_SIZE, MAX_GUILD_EMOJIS} from '@fluxer/constants/src/LimitConstants';
import type {GuildEmojiWithUser} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
import {sortBySnowflakeDesc} from '@fluxer/snowflake/src/SnowflakeUtils';
import {msg} from '@lingui/core/macro';
@@ -179,7 +178,8 @@ const GuildEmojiTab: React.FC<{guildId: string}> = observer(function GuildEmojiT
}, [filteredEmojis]);
const maxEmojis = guild?.maxEmojis ?? MAX_GUILD_EMOJIS;
const currentEmojiCount = emojis.length;
const emojiMaxSizeLabel = formatFileSize(i18n.locale, GlobalLimits.getEmojiMaxSize());
const maxEmojiSize = guild?.maxEmojiSize ?? EMOJI_MAX_SIZE;
const emojiMaxSizeLabel = formatFileSize(i18n.locale, maxEmojiSize);
const canModifyEmoji = useCallback(
(emoji: GuildEmojiWithUser): boolean => {
if (canManageExpressions) return true;
@@ -291,7 +291,6 @@ const GuildEmojiTab: React.FC<{guildId: string}> = observer(function GuildEmojiT
);
const preparedEmojis: Array<{name: string; image: string; file: File}> = [];
const preparationFailures: Array<EmojiUploadFailure> = [];
const maxEmojiSize = GlobalLimits.getEmojiMaxSize();
const maxEmojiSizeLabel = formatFileSize(i18n.locale, maxEmojiSize);
for (const file of filesWithinSlots) {
try {
@@ -399,7 +398,7 @@ const GuildEmojiTab: React.FC<{guildId: string}> = observer(function GuildEmojiT
);
}
},
[guildId, fetchEmojis, maxEmojis, currentEmojiCount, i18n, renderEmojiFailureDescription],
[guildId, fetchEmojis, maxEmojis, maxEmojiSize, currentEmojiCount, i18n, renderEmojiFailureDescription],
);
const handleDrop = useCallback(
async (files: Array<File>) => {
@@ -3,7 +3,6 @@
import {GenericErrorModal} from '@app/features/app/components/alerts/GenericErrorModal';
import {ConfirmModal} from '@app/features/app/components/dialogs/ConfirmModal';
import {StatusSlate} from '@app/features/app/components/dialogs/shared/StatusSlate';
import {GlobalLimits} from '@app/features/app/utils/GlobalLimits';
import {StickerGridItem} from '@app/features/emoji/components/stickers/StickerGridItem';
import EmojiStickerLayout from '@app/features/emoji/state/EmojiStickerLayout';
import * as GuildStickerCommands from '@app/features/expressions/commands/GuildStickerCommands';
@@ -34,7 +33,7 @@ import {Spinner} from '@app/features/ui/components/Spinner';
import Users from '@app/features/user/state/Users';
import {canCropFormat} from '@app/features/voice/utils/MediaCapabilities';
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
import {MAX_GUILD_STICKERS} from '@fluxer/constants/src/LimitConstants';
import {MAX_GUILD_STICKERS, STICKER_MAX_SIZE} from '@fluxer/constants/src/LimitConstants';
import type {GuildStickerWithUser} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
import {sortBySnowflakeDesc} from '@fluxer/snowflake/src/SnowflakeUtils';
import {msg} from '@lingui/core/macro';
@@ -227,7 +226,7 @@ const GuildStickersTab: React.FC<{guildId: string}> = observer(function GuildSti
},
[canManageExpressions, canCreateExpressions, currentUserId],
);
const stickerMaxSizeLabel = formatFileSize(i18n.locale, GlobalLimits.getStickerMaxSize());
const stickerMaxSizeLabel = formatFileSize(i18n.locale, guild?.maxStickerSize ?? STICKER_MAX_SIZE);
return (
<div className={styles.container} data-flx="guild.guild-tabs.guild-stickers-tab.container">
<CloneAllowedToggle
@@ -4,10 +4,10 @@ import {ImagePreviewField} from '@app/features/app/components/shared/ImagePrevie
import {
ANIMATED_BANNER_FEATURE,
ANIMATED_IMAGE_FORMATS,
IMAGE_MAX_SIZE_BYTES,
STATIC_IMAGE_FORMATS,
WIDE_IMAGE_ASPECT_RATIO_LABEL,
} from '@app/features/app/config/I18nDisplayConstants';
import {GlobalLimits} from '@app/features/app/utils/GlobalLimits';
import {AssetType, getAssetConfig} from '@app/features/expressions/components/modals/AssetCropModal';
import {getAcceptStringFiltered} from '@app/features/expressions/utils/AssetFormatCopy';
import {getAspectRatioFromDimensions} from '@app/features/expressions/utils/AssetImageGeometry';
@@ -97,7 +97,7 @@ export const GuildBannerUploadField: React.FC<{
sourceModalTitle: i18n._(CHANGE_BANNER_DESCRIPTOR),
uploadHint: formatImageUploadMinimumHint(i18n, {
formats: bannerFormats,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
minimumSize: bannerMinimumSize,
aspectRatio: WIDE_IMAGE_ASPECT_RATIO_LABEL,
}),
@@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {
IMAGE_MAX_SIZE_BYTES,
STATIC_IMAGE_WITH_AVIF_FORMATS,
WIDE_IMAGE_ASPECT_RATIO_LABEL,
} from '@app/features/app/config/I18nDisplayConstants';
import {GlobalLimits} from '@app/features/app/utils/GlobalLimits';
import {GuildInviteEmbedPreview} from '@app/features/channel/components/InviteEmbed';
import {AssetType, getAssetConfig} from '@app/features/expressions/components/modals/AssetCropModal';
import {getAcceptString} from '@app/features/expressions/utils/AssetFormatCopy';
@@ -76,7 +76,7 @@ export const GuildEmbedSplashUploadField: React.FC<{
sourceModalTitle: i18n._(CHANGE_CHAT_EMBED_BACKGROUND_DESCRIPTOR),
uploadHint: formatImageUploadMinimumHintWithNote(i18n, {
formats: STATIC_IMAGE_WITH_AVIF_FORMATS,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
minimumSize: embedSplashMinimumSize,
aspectRatio: WIDE_IMAGE_ASPECT_RATIO_LABEL,
note: i18n._(SHOWN_IN_INVITE_EMBEDS_IN_CHAT_DESCRIPTOR),
@@ -4,9 +4,9 @@ import {
ANIMATED_ICON_FEATURE,
ANIMATED_IMAGE_FORMATS,
AVATAR_RECOMMENDED_SIZE_LABEL,
IMAGE_MAX_SIZE_BYTES,
STATIC_IMAGE_FORMATS,
} from '@app/features/app/config/I18nDisplayConstants';
import {GlobalLimits} from '@app/features/app/utils/GlobalLimits';
import {AssetType} from '@app/features/expressions/components/modals/AssetCropModal';
import {getAcceptStringFiltered} from '@app/features/expressions/utils/AssetFormatCopy';
import {formatImageUploadRecommendedHint} from '@app/features/expressions/utils/AssetUploadHintCopy';
@@ -75,7 +75,7 @@ export const GuildIconUploadField: React.FC<{
sourceModalTitle: i18n._(CHANGE_ICON_DESCRIPTOR),
uploadHint: formatImageUploadRecommendedHint(i18n, {
formats: iconFormats,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
recommendedSize: AVATAR_RECOMMENDED_SIZE_LABEL,
}),
});
@@ -2,10 +2,10 @@
import {ImagePreviewField} from '@app/features/app/components/shared/ImagePreviewField';
import {
IMAGE_MAX_SIZE_BYTES,
STATIC_IMAGE_WITH_AVIF_FORMATS,
WIDE_IMAGE_ASPECT_RATIO_LABEL,
} from '@app/features/app/config/I18nDisplayConstants';
import {GlobalLimits} from '@app/features/app/utils/GlobalLimits';
import {AssetType, getAssetConfig} from '@app/features/expressions/components/modals/AssetCropModal';
import {getAcceptString} from '@app/features/expressions/utils/AssetFormatCopy';
import {getAspectRatioFromDimensions} from '@app/features/expressions/utils/AssetImageGeometry';
@@ -82,7 +82,7 @@ export const GuildInviteSplashUploadField: React.FC<{
sourceModalTitle: i18n._(CHANGE_INVITE_BACKGROUND_DESCRIPTOR),
uploadHint: formatImageUploadMinimumHint(i18n, {
formats: STATIC_IMAGE_WITH_AVIF_FORMATS,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
minimumSize: splashMinimumSize,
aspectRatio: WIDE_IMAGE_ASPECT_RATIO_LABEL,
}),
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {GlobalLimits} from '@app/features/app/utils/GlobalLimits';
import type {Gif} from '@app/features/expressions/commands/GifCommands';
import {AssetCropModal, type AssetType} from '@app/features/expressions/components/modals/AssetCropModal';
import {openAssetSourceModal} from '@app/features/expressions/components/modals/AssetSourceModal';
@@ -10,7 +11,6 @@ import {isSvgFile, readImageFileAsUploadDataUrl} from '@app/features/expressions
import {showGuildErrorModal} from '@app/features/guild/components/alerts/GuildErrorModalUtils';
import {
blobToDataUrl,
MAX_IMAGE_BYTES,
revokeObjectUrl,
} from '@app/features/guild/components/modals/guild_tabs/guild_overview_tab/utils/ImageAsset';
import type {FormInputs} from '@app/features/guild/utils/guild_tabs/GuildOverviewTabUtils';
@@ -78,7 +78,6 @@ export interface UseGuildImageAssetFieldArgs {
previewUrl: string | null;
setPreviewUrl: React.Dispatch<React.SetStateAction<string | null>>;
setHasCleared: React.Dispatch<React.SetStateAction<boolean>>;
maxBytes?: number;
labelForMessages: string;
gif?: GifPolicy;
aspectRatio?: AspectRatioConfig;
@@ -107,7 +106,6 @@ export function useGuildImageAssetField({
previewUrl,
setPreviewUrl,
setHasCleared,
maxBytes = MAX_IMAGE_BYTES,
labelForMessages,
gif,
aspectRatio,
@@ -115,6 +113,7 @@ export function useGuildImageAssetField({
uploadHint,
}: UseGuildImageAssetFieldArgs): ImageAssetFieldController {
const {i18n} = useLingui();
const maxBytes = GlobalLimits.getAvatarMaxSize();
const label = labelForMessages;
const requestIdRef = useRef(0);
const [isProcessing, setIsProcessing] = useState(false);
@@ -1,7 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
export function revokeObjectUrl(url: string | null | undefined): void {
if (!url) return;
if (!url.startsWith('blob:')) return;
+14 -1
View File
@@ -8,7 +8,12 @@ import {LARGE_GUILD_THRESHOLD} from '@fluxer/constants/src/GatewayConstants';
import type {GuildSplashCardAlignmentValue} from '@fluxer/constants/src/GuildConstants';
import {GuildFeatures, GuildNSFWLevel, GuildSplashCardAlignment} from '@fluxer/constants/src/GuildConstants';
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
import {MAX_GUILD_EMOJIS, MAX_GUILD_STICKERS} from '@fluxer/constants/src/LimitConstants';
import {
EMOJI_MAX_SIZE,
MAX_GUILD_EMOJIS,
MAX_GUILD_STICKERS,
STICKER_MAX_SIZE,
} from '@fluxer/constants/src/LimitConstants';
import {MessageNotifications} from '@fluxer/constants/src/NotificationConstants';
import {resolveLimit} from '@fluxer/limits/src/LimitResolver';
import type {Guild as WireGuild} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
@@ -348,6 +353,14 @@ export class Guild {
return this.resolveGuildLimit('max_guild_stickers', MAX_GUILD_STICKERS);
}
get maxEmojiSize(): number {
return this.resolveGuildLimit('emoji_max_size', EMOJI_MAX_SIZE);
}
get maxStickerSize(): number {
return this.resolveGuildLimit('sticker_max_size', STICKER_MAX_SIZE);
}
private resolveGuildLimit(key: LimitKey, fallback: number): number {
const resolved = resolveLimit(
RuntimeConfig.limits,
@@ -6,10 +6,10 @@ import {
ANIMATED_AVATAR_FORMATS,
ANIMATED_IMAGE_FORMATS,
AVATAR_RECOMMENDED_SIZE_LABEL,
IMAGE_MAX_SIZE_BYTES,
STATIC_IMAGE_FORMATS,
} from '@app/features/app/config/I18nDisplayConstants';
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
import {GlobalLimits} from '@app/features/app/utils/GlobalLimits';
import {LimitResolver} from '@app/features/app/utils/LimitResolverAdapter';
import {isLimitToggleEnabled} from '@app/features/app/utils/LimitUtils';
import type {Gif} from '@app/features/expressions/commands/GifCommands';
@@ -163,11 +163,11 @@ export const AvatarUploader = observer(
);
const processAvatarFile = useCallback(
async (file: File) => {
if (file.size > 10 * 1024 * 1024) {
if (file.size > GlobalLimits.getAvatarMaxSize()) {
showUserErrorModal(
i18n._(COULDN_T_UPLOAD_AVATAR_DESCRIPTOR),
i18n._(AVATAR_FILE_IS_TOO_LARGE_PLEASE_CHOOSE_A_DESCRIPTOR, {
imageMaxSizeLabel: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
imageMaxSizeLabel: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
}),
);
return;
@@ -308,13 +308,13 @@ export const AvatarUploader = observer(
const uploadHint = canUploadAnimatedAvatar
? formatImageUploadRecommendedHint(i18n, {
formats: ANIMATED_IMAGE_FORMATS,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
recommendedSize: AVATAR_RECOMMENDED_SIZE_LABEL,
})
: shouldShowPremiumFeatures()
? formatImageUploadRecommendedHintWithNote(i18n, {
formats: STATIC_IMAGE_FORMATS,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
recommendedSize: AVATAR_RECOMMENDED_SIZE_LABEL,
note: i18n._(ANIMATED_AVATARS_REQUIRE_PREMIUM_NOTE_DESCRIPTOR, {
animatedAvatarFormats: ANIMATED_AVATAR_FORMATS,
@@ -323,7 +323,7 @@ export const AvatarUploader = observer(
})
: formatImageUploadRecommendedHint(i18n, {
formats: STATIC_IMAGE_FORMATS,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
recommendedSize: AVATAR_RECOMMENDED_SIZE_LABEL,
});
openAssetSourceModal({
@@ -5,8 +5,8 @@ import {
ANIMATED_IMAGE_FORMATS,
BANNER_ASPECT_RATIO_LABEL,
BANNER_MINIMUM_SIZE_LABEL,
IMAGE_MAX_SIZE_BYTES,
} from '@app/features/app/config/I18nDisplayConstants';
import {GlobalLimits} from '@app/features/app/utils/GlobalLimits';
import {LimitResolver} from '@app/features/app/utils/LimitResolverAdapter';
import {isLimitToggleEnabled} from '@app/features/app/utils/LimitUtils';
import type {Gif} from '@app/features/expressions/commands/GifCommands';
@@ -183,11 +183,11 @@ export const BannerUploader = observer(
}, [i18n]);
const processBannerFile = useCallback(
async (file: File) => {
if (file.size > 10 * 1024 * 1024) {
if (file.size > GlobalLimits.getAvatarMaxSize()) {
showUserErrorModal(
i18n._(COULDN_T_UPLOAD_BANNER_DESCRIPTOR),
i18n._(BANNER_FILE_IS_TOO_LARGE_PLEASE_CHOOSE_A_DESCRIPTOR, {
imageMaxSizeLabel: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
imageMaxSizeLabel: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
}),
);
return;
@@ -264,7 +264,7 @@ export const BannerUploader = observer(
title: i18n._(CHANGE_BANNER_DESCRIPTOR),
uploadHint: formatImageUploadMinimumHint(i18n, {
formats: ANIMATED_IMAGE_FORMATS,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
minimumSize: BANNER_MINIMUM_SIZE_LABEL,
aspectRatio: BANNER_ASPECT_RATIO_LABEL,
}),
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {showGenericErrorModal} from '@app/features/app/components/alerts/GenericErrorModalCommands';
import {IMAGE_MAX_SIZE_BYTES} from '@app/features/app/config/I18nDisplayConstants';
import {GlobalLimits} from '@app/features/app/utils/GlobalLimits';
import {getAcceptString} from '@app/features/expressions/utils/AssetFormatCopy';
import {isSvgFile, readImageFileAsUploadDataUrl} from '@app/features/expressions/utils/ImageUploadFileUtils';
import {
@@ -159,12 +159,12 @@ export const WebhookListItem: React.FC<WebhookListItemProps> = observer(
const handleAvatarUpload = useCallback(async () => {
const [file] = await openFilePicker({accept: getAcceptString('avatar')});
if (!file) return;
if (file.size > 10 * 1024 * 1024) {
if (file.size > GlobalLimits.getAvatarMaxSize()) {
showGenericErrorModal({
title: () => i18n._(SOMETHING_WENT_WRONG_DESCRIPTOR),
message: () =>
i18n._(AVATAR_FILE_IS_TOO_LARGE_PLEASE_CHOOSE_A_DESCRIPTOR, {
imageMaxSizeLabel: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
imageMaxSizeLabel: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
}),
dataFlx: 'webhook.webhook-list-item.avatar-too-large-error-modal',
});