mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(media): stream large files and honour size settings (#3387)
This commit is contained in:
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)}
|
||||
|
||||
+5
-8
@@ -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
|
||||
|
||||
+2
-2
@@ -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,
|
||||
}),
|
||||
|
||||
+2
-2
@@ -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),
|
||||
|
||||
+2
-2
@@ -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
-2
@@ -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,
|
||||
}),
|
||||
|
||||
+2
-3
@@ -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);
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
@@ -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
-6
@@ -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({
|
||||
|
||||
+4
-4
@@ -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',
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user