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);
});
});