fix(app): request avatar and banner sizes the media proxy serves (#1842)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-24 12:29:16 +02:00
1 parent c3e747aaa4
commit bd381ccc74
8 files changed
+177 -559

No files matched your search

@@ -131,13 +131,10 @@ export const InviteAcceptModal = observer(function InviteAcceptModal({code}: Inv
if (!guild.id || !guild.splash) { if (!guild.id || !guild.splash) {
return null; return null;
} }
return AvatarUtils.getGuildSplashURL( return AvatarUtils.getGuildSplashURL({
{
id: guild.id, id: guild.id,
splash: guild.splash, splash: guild.splash,
}, });
4096,
);
}, [invite]); }, [invite]);
const isJoinDisabled = isGuildInviteActionDisabled(guildActionState); const isJoinDisabled = isGuildInviteActionDisabled(guildActionState);
const primaryActionType = getGuildInvitePrimaryAction(guildActionState); const primaryActionType = getGuildInvitePrimaryAction(guildActionState);
@@ -57,7 +57,7 @@ export const InvitePagePreviewModal: React.FC<InvitePagePreviewModalProps> = obs
const splashUrl = useMemo(() => { const splashUrl = useMemo(() => {
if (previewSplashUrl) return previewSplashUrl; if (previewSplashUrl) return previewSplashUrl;
if (guild?.splash) { if (guild?.splash) {
return AvatarUtils.getGuildSplashURL({id: guild.id, splash: guild.splash}, 4096); return AvatarUtils.getGuildSplashURL({id: guild.id, splash: guild.splash});
} }
return null; return null;
}, [previewSplashUrl, guild?.id, guild?.splash]); }, [previewSplashUrl, guild?.id, guild?.splash]);
@@ -170,7 +170,7 @@ const InviteLoginPageContainer = observer(() => {
} }
const guild = guildInvite.guild; const guild = guildInvite.guild;
if (guild?.splash && guild.id) { if (guild?.splash && guild.id) {
const splashUrl = getGuildSplashURL({id: guild.id, splash: guild.splash}, 4096) ?? null; const splashUrl = getGuildSplashURL({id: guild.id, splash: guild.splash}) ?? null;
setSplashUrl(splashUrl); setSplashUrl(splashUrl);
} }
}, [guildInvite?.guild?.splash, guildInvite?.guild?.id, setSplashUrl]); }, [guildInvite?.guild?.splash, guildInvite?.guild?.id, setSplashUrl]);
@@ -66,13 +66,10 @@ const InviteRegisterPage = observer(function InviteRegisterPage() {
} }
const guild = guildInvite.guild; const guild = guildInvite.guild;
if (guild?.splash) { if (guild?.splash) {
const splashUrl = AvatarUtils.getGuildSplashURL( const splashUrl = AvatarUtils.getGuildSplashURL({
{
id: guild.id, id: guild.id,
splash: guild.splash, splash: guild.splash,
}, });
4096,
);
setSplashUrl(splashUrl); setSplashUrl(splashUrl);
} }
}, [guildInvite?.guild?.splash, guildInvite?.guild?.id, setSplashUrl]); }, [guildInvite?.guild?.splash, guildInvite?.guild?.id, setSplashUrl]);
@@ -79,7 +79,7 @@ import {hasActiveDirectCallWithUser} from '@app/features/voice/utils/PrivateCall
import {ME} from '@fluxer/constants/src/AppConstants'; import {ME} from '@fluxer/constants/src/AppConstants';
import {Permissions} from '@fluxer/constants/src/ChannelConstants'; import {Permissions} from '@fluxer/constants/src/ChannelConstants';
import { import {
MEDIA_PROXY_AVATAR_SIZE_PROFILE, MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
MEDIA_PROXY_PROFILE_BANNER_SIZE_MODAL, MEDIA_PROXY_PROFILE_BANNER_SIZE_MODAL,
} from '@fluxer/constants/src/MediaProxyAssetSizes'; } from '@fluxer/constants/src/MediaProxyAssetSizes';
import {PublicUserFlags, RelationshipTypes} from '@fluxer/constants/src/UserConstants'; import {PublicUserFlags, RelationshipTypes} from '@fluxer/constants/src/UserConstants';
@@ -282,7 +282,7 @@ const UserProfileMobileSheetContent: React.FC<UserProfileMobileSheetContentProps
); );
const shouldAutoplayProfileAnimations = useAutoplayExpandedProfileAnimations(); const shouldAutoplayProfileAnimations = useAutoplayExpandedProfileAnimations();
const {avatarUrl, hoverAvatarUrl} = useMemo( const {avatarUrl, hoverAvatarUrl} = useMemo(
() => ProfileDisplayUtils.getProfileAvatarUrls(profileContext, undefined, MEDIA_PROXY_AVATAR_SIZE_PROFILE), () => ProfileDisplayUtils.getProfileAvatarUrls(profileContext, undefined, MEDIA_PROXY_AVATAR_SIZE_DEFAULT),
[profileContext], [profileContext],
); );
const {bannerUrl: staticBannerUrl, hoverBannerUrl} = useMemo( const {bannerUrl: staticBannerUrl, hoverBannerUrl} = useMemo(
@@ -6,13 +6,13 @@ import {SettingsTabSection} from '@app/features/app/components/dialogs/shared/Se
import {ChannelItemCore} from '@app/features/app/components/layout/ChannelItem'; import {ChannelItemCore} from '@app/features/app/components/layout/ChannelItem';
import channelItemSurfaceStyles from '@app/features/app/components/layout/ChannelItemSurface.module.css'; import channelItemSurfaceStyles from '@app/features/app/components/layout/ChannelItemSurface.module.css';
import {Typing} from '@app/features/channel/components/ChannelTyping'; import {Typing} from '@app/features/channel/components/ChannelTyping';
import {cdnUrl} from '@app/features/messaging/utils/MessagingUrlUtils';
import {AvatarStack} from '@app/features/ui/avatars/AvatarStack'; import {AvatarStack} from '@app/features/ui/avatars/AvatarStack';
import {MockAvatar} from '@app/features/ui/components/MockAvatar'; import {MockAvatar} from '@app/features/ui/components/MockAvatar';
import type {RadioOption} from '@app/features/ui/radio_group/RadioGroup'; import type {RadioOption} from '@app/features/ui/radio_group/RadioGroup';
import {RadioGroup} from '@app/features/ui/radio_group/RadioGroup'; import {RadioGroup} from '@app/features/ui/radio_group/RadioGroup';
import {Tooltip} from '@app/features/ui/tooltip/Tooltip'; import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
import styles from '@app/features/user/components/modals/tabs/appearance_tab/InterfaceTab.module.css'; import styles from '@app/features/user/components/modals/tabs/appearance_tab/InterfaceTab.module.css';
import {getDefaultAvatarURLForIndex} from '@app/features/user/utils/AvatarUtils';
import {msg} from '@lingui/core/macro'; import {msg} from '@lingui/core/macro';
import {Trans, useLingui} from '@lingui/react/macro'; import {Trans, useLingui} from '@lingui/react/macro';
import {clsx} from 'clsx'; import {clsx} from 'clsx';
@@ -92,7 +92,7 @@ const ChannelListPreview = observer(({mode}: {mode: ChannelTypingIndicatorMode})
key={index} key={index}
size={12} size={12}
userTag={`User ${index}`} userTag={`User ${index}`}
avatarUrl={cdnUrl(`avatars/${index}.png`)} avatarUrl={getDefaultAvatarURLForIndex(index)}
data-flx="user.appearance-tab.interface-tab.channel-list-preview.mock-avatar" data-flx="user.appearance-tab.interface-tab.channel-list-preview.mock-avatar"
/> />
))} ))}
@@ -1,482 +1,3 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import RuntimeConfig from '@app/features/app/state/RuntimeConfig'; export * from '@app/features/user/utils/AvatarUtils';
import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions';
import {buildMediaProxyURL} from '@app/features/messaging/utils/MediaProxyUtils';
import {cdnUrl, mediaUrl, setPathQueryParams} from '@app/features/messaging/utils/MessagingUrlUtils';
import type {User} from '@app/features/user/models/User';
import {
getDefaultAvatarIndex,
getDefaultAvatarPrimaryColor as getSharedDefaultAvatarPrimaryColor,
normalizeEndpoint,
parseAvatarHash,
} from '@app/features/user/utils/AvatarMediaUtils';
import {
MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
MEDIA_PROXY_ICON_SIZE_DEFAULT,
} from '@fluxer/constants/src/MediaProxyAssetSizes';
import type {MediaProxyImageSize} from '@fluxer/constants/src/MediaProxyImageSizes';
import {SOUNDBOARD_SOUND_PATH_PREFIX} from '@fluxer/constants/src/SoundboardConstants';
const getDefaultAvatar = (index: number): string => cdnUrl(`avatars/${index}.png`);
export function getDefaultAvatarPrimaryColor(id: string) {
return getSharedDefaultAvatarPrimaryColor(id);
}
export function getDefaultAvatarURL(id: string) {
return getDefaultAvatar(getDefaultAvatarIndex(id));
}
type AvatarOptions = Pick<User, 'id' | 'avatar'>;
type BannerOptions = Pick<User, 'id' | 'banner'>;
interface IconOptions {
id: string;
icon: string | null;
}
type MediaURLParams = {
path: string;
id: string;
hash: string;
size?: MediaProxyImageSize;
format: string;
animated?: boolean;
endpoint?: string;
};
const parseMediaHashForRequest = (value: string, animated = false) => {
const {animated: isAnimated} = parseAvatarHash(value);
return {
hash: value,
animated: isAnimated ? animated : undefined,
};
};
const getMediaURL = ({path, id, hash, size, format, animated, endpoint}: MediaURLParams) => {
if (DeveloperOptions.forceRenderPlaceholders) {
return '';
}
const baseEndpoint = endpoint === undefined ? RuntimeConfig.mediaEndpoint : endpoint;
if (!baseEndpoint) {
return '';
}
const basePath = `${path}/${id}/${hash}.${format}`;
const url = size ? setPathQueryParams(basePath, {size}) : basePath;
const proxyOptions = animated === undefined ? undefined : {animated};
return buildMediaProxyURL(`${normalizeEndpoint(baseEndpoint)}/${url}`, proxyOptions);
};
type GuildMemberMediaURLParams = {
path: string;
guildId: string;
userId: string;
hash: string;
size?: MediaProxyImageSize;
format: string;
animated?: boolean;
};
const getGuildMemberMediaURL = ({path, guildId, userId, hash, size, format, animated}: GuildMemberMediaURLParams) => {
if (DeveloperOptions.forceRenderPlaceholders) {
return '';
}
const baseEndpoint = RuntimeConfig.mediaEndpoint;
if (!baseEndpoint) {
return '';
}
const basePath = `guilds/${guildId}/users/${userId}/${path}/${hash}.${format}`;
const url = size ? setPathQueryParams(basePath, {size}) : basePath;
const proxyOptions = animated === undefined ? undefined : {animated};
return buildMediaProxyURL(`${normalizeEndpoint(baseEndpoint)}/${url}`, proxyOptions);
};
const buildWebpMediaUrl = (params: Omit<MediaURLParams, 'format'>) => getMediaURL({...params, format: 'webp'});
const buildPngMediaUrl = (params: Omit<MediaURLParams, 'format'>) => getMediaURL({...params, format: 'png'});
const buildGuildMemberWebpUrl = (params: Omit<GuildMemberMediaURLParams, 'format'>) =>
getGuildMemberMediaURL({...params, format: 'webp'});
export function getUserAvatarURL(
{id, avatar}: AvatarOptions,
animated = false,
size: MediaProxyImageSize = MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
) {
if (!avatar) {
return getDefaultAvatar(getDefaultAvatarIndex(id));
}
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated);
return buildWebpMediaUrl({
path: 'avatars',
id,
hash,
size,
animated: shouldAnimate,
});
}
export function getUserNotificationAvatarURL(
{id, avatar}: AvatarOptions,
size: MediaProxyImageSize = MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
) {
if (!avatar) {
return getDefaultAvatar(getDefaultAvatarIndex(id));
}
const {hash, animated} = parseMediaHashForRequest(avatar, false);
return buildPngMediaUrl({
path: 'avatars',
id,
hash,
size,
animated,
});
}
export function getUserBannerURL({id, banner}: BannerOptions, animated = false, size: MediaProxyImageSize = 1024) {
if (!banner) {
return '';
}
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(banner, animated);
return buildWebpMediaUrl({
path: 'banners',
id,
hash,
size,
animated: shouldAnimate,
});
}
const SOUNDBOARD_SOUND_URL_CACHE = new Map<string, string>();
const SOUNDBOARD_SOUND_URL_CACHE_LIMIT = 4096;
export function getSoundboardSoundURL(soundId: string): string {
if (DeveloperOptions.forceRenderPlaceholders) {
return '';
}
const key = `${RuntimeConfig.mediaEndpoint}:${soundId}`;
const cached = SOUNDBOARD_SOUND_URL_CACHE.get(key);
if (cached !== undefined) return cached;
const result = mediaUrl(`${SOUNDBOARD_SOUND_PATH_PREFIX}/${soundId}`);
if (SOUNDBOARD_SOUND_URL_CACHE.size >= SOUNDBOARD_SOUND_URL_CACHE_LIMIT) SOUNDBOARD_SOUND_URL_CACHE.clear();
SOUNDBOARD_SOUND_URL_CACHE.set(key, result);
return result;
}
export function getGuildIconURL({id, icon}: IconOptions, animated = false) {
if (!icon) {
return '';
}
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(icon, animated);
return buildWebpMediaUrl({
path: 'icons',
id,
hash,
size: MEDIA_PROXY_ICON_SIZE_DEFAULT,
animated: shouldAnimate,
});
}
export function getGuildIconURLWithProxy({id, icon}: IconOptions, endpoint: string, animated = false) {
if (!endpoint || !icon) {
return '';
}
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(icon, animated);
return buildWebpMediaUrl({
path: 'icons',
id,
hash,
size: MEDIA_PROXY_ICON_SIZE_DEFAULT,
animated: shouldAnimate,
endpoint,
});
}
export function getGuildSplashURL(
{
id,
splash,
}: {
id: string;
splash: string | null;
},
size: MediaProxyImageSize = 1024,
) {
if (!splash) {
return '';
}
return buildWebpMediaUrl({
path: 'splashes',
id,
hash: splash,
size,
});
}
export function getGuildDiscoverySplashURL({id, splash}: {id: string; splash: string | null}) {
if (!splash) {
return '';
}
return buildWebpMediaUrl({
path: 'discovery-splashes',
id,
hash: splash,
size: 1024,
});
}
export function getGuildBannerURL(
{
id,
banner,
}: {
id: string;
banner: string | null;
},
animated = false,
) {
if (!banner) {
return '';
}
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(banner, animated);
return buildWebpMediaUrl({
path: 'banners',
id,
hash,
size: 1024,
animated: shouldAnimate,
});
}
export function getGuildMemberAvatarURL({
guildId,
userId,
avatar,
memberAvatar,
animated = false,
size = MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
}: {
guildId: string;
userId: string;
avatar: string | null;
memberAvatar?: string | null;
animated?: boolean;
size?: MediaProxyImageSize;
}) {
if (memberAvatar) {
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(memberAvatar, animated);
return buildGuildMemberWebpUrl({
path: 'avatars',
guildId,
userId,
hash,
size,
animated: shouldAnimate,
});
}
if (avatar) {
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated);
return buildWebpMediaUrl({
path: 'avatars',
id: userId,
hash,
size,
animated: shouldAnimate,
});
}
return getDefaultAvatar(getDefaultAvatarIndex(userId));
}
export function getGuildMemberDisplayAvatarURL({
guildId,
user,
memberAvatar,
avatarUnset = false,
animated = false,
size = MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
}: {
guildId: string;
user: AvatarOptions;
memberAvatar?: string | null;
avatarUnset?: boolean;
animated?: boolean;
size?: MediaProxyImageSize;
}) {
if (avatarUnset) {
return getUserAvatarURL({id: user.id, avatar: null}, animated, size);
}
return getGuildMemberAvatarURL({
guildId,
userId: user.id,
avatar: user.avatar,
memberAvatar,
animated,
size,
});
}
export function getGuildMemberBannerURL({
guildId,
userId,
banner,
memberBanner,
animated = false,
size = 1024,
}: {
guildId: string;
userId: string;
banner: string | null;
memberBanner?: string | null;
animated?: boolean;
size?: MediaProxyImageSize;
}) {
if (memberBanner) {
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(memberBanner, animated);
return buildGuildMemberWebpUrl({
path: 'banners',
guildId,
userId,
hash,
size,
animated: shouldAnimate,
});
}
if (banner) {
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(banner, animated);
return buildWebpMediaUrl({
path: 'banners',
id: userId,
hash,
size,
animated: shouldAnimate,
});
}
return '';
}
export function getUserAvatarURLWithProxy(
options: AvatarOptions,
endpoint: string,
animated = false,
size: MediaProxyImageSize = MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
) {
const {id, avatar} = options;
if (!endpoint || !avatar) {
return getDefaultAvatar(getDefaultAvatarIndex(id));
}
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated);
return buildWebpMediaUrl({
path: 'avatars',
id,
hash,
size,
animated: shouldAnimate,
endpoint,
});
}
export function getGuildEmbedSplashURL(
{
id,
embedSplash,
}: {
id: string;
embedSplash: string | null;
},
size: MediaProxyImageSize = 1024,
) {
if (!embedSplash) {
return '';
}
return buildWebpMediaUrl({
path: 'embed-splashes',
id,
hash: embedSplash,
size,
});
}
export function getChannelIconURL(
{id, icon}: IconOptions,
size: MediaProxyImageSize = MEDIA_PROXY_ICON_SIZE_DEFAULT,
animated = false,
) {
if (!icon) {
return '';
}
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(icon, animated);
return buildWebpMediaUrl({
path: 'icons',
id,
hash,
size,
animated: shouldAnimate,
});
}
export function getChannelIconURLWithProxy(
{id, icon}: IconOptions,
endpoint: string,
size: MediaProxyImageSize = MEDIA_PROXY_ICON_SIZE_DEFAULT,
animated = false,
) {
if (!endpoint || !icon) {
return '';
}
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(icon, animated);
return buildWebpMediaUrl({
path: 'icons',
id,
hash,
size,
animated: shouldAnimate,
endpoint,
});
}
export function getWebhookAvatarURL({id, avatar}: AvatarOptions, animated = false) {
if (!avatar) {
return getDefaultAvatar(getDefaultAvatarIndex(id));
}
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated);
return buildWebpMediaUrl({
path: 'avatars',
id,
hash,
size: MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
animated: shouldAnimate,
});
}
const EMOJI_URL_CACHE = new Map<string, string>();
const EMOJI_URL_CACHE_LIMIT = 4096;
export function getEmojiURL({id, animated}: {id: string; animated?: boolean}) {
if (DeveloperOptions.forceRenderPlaceholders) {
return '';
}
const animatedFlag = Boolean(animated);
const key = `${RuntimeConfig.mediaEndpoint}:${animatedFlag ? 'a' : 's'}:${id}`;
const cached = EMOJI_URL_CACHE.get(key);
if (cached !== undefined) return cached;
const result = mediaUrl(setPathQueryParams(`emojis/${id}.webp`, {v: 5}), {animated: animatedFlag});
if (EMOJI_URL_CACHE.size >= EMOJI_URL_CACHE_LIMIT) EMOJI_URL_CACHE.clear();
EMOJI_URL_CACHE.set(key, result);
return result;
}
type StickerSize = 160 | 320;
export function getStickerURL({id, animated, size = 320}: {id: string; animated?: boolean; size?: StickerSize}) {
if (DeveloperOptions.forceRenderPlaceholders) {
return '';
}
const safeSize: StickerSize = size === 320 ? 320 : 160;
return mediaUrl(setPathQueryParams(`stickers/${id}.webp`, {size: safeSize}), {animated: Boolean(animated)});
}
export function fileToBase64(file: File) {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
+166 -63
View File
@@ -2,7 +2,15 @@
import RuntimeConfig from '@app/features/app/state/RuntimeConfig'; import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions'; import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions';
import {buildMediaProxyURL} from '@app/features/messaging/utils/MediaProxyUtils'; import {
buildMediaProxyURL,
LARGEST_MEDIA_PROXY_IMAGE_SIZE,
MEDIA_PROXY_IMAGE_SIZE_LADDER,
snapMediaProxyImageSize,
} from '@app/features/messaging/utils/MediaProxyUtils';
export {MEDIA_PROXY_IMAGE_SIZE_LADDER, snapMediaProxyImageSize};
import {cdnUrl, mediaUrl, setPathQueryParams} from '@app/features/messaging/utils/MessagingUrlUtils'; import {cdnUrl, mediaUrl, setPathQueryParams} from '@app/features/messaging/utils/MessagingUrlUtils';
import type {User} from '@app/features/user/models/User'; import type {User} from '@app/features/user/models/User';
import { import {
@@ -16,13 +24,68 @@ import {
MEDIA_PROXY_ICON_SIZE_DEFAULT, MEDIA_PROXY_ICON_SIZE_DEFAULT,
} from '@fluxer/constants/src/MediaProxyAssetSizes'; } from '@fluxer/constants/src/MediaProxyAssetSizes';
import type {MediaProxyImageSize} from '@fluxer/constants/src/MediaProxyImageSizes'; import type {MediaProxyImageSize} from '@fluxer/constants/src/MediaProxyImageSizes';
import {SOUNDBOARD_SOUND_PATH_PREFIX} from '@fluxer/constants/src/SoundboardConstants';
const getDefaultAvatar = (index: number): string => cdnUrl(`avatars/${index}.png`); const GUILD_BANNER_CSS_WIDTH = 360;
const GUILD_EMBED_SPLASH_CSS_WIDTH = 360;
const GUILD_SPLASH_FALLBACK_CSS_WIDTH = 1024;
const MEDIA_PROXY_WIDE_ASSET_SERVER_MIN_DIMENSION = 480;
const MEDIA_PROXY_WIDE_ASSET_SERVER_MAX_DIMENSION = 2400;
const MEDIA_PROXY_ICON_SERVER_MIN_DIMENSION = 128;
const MEDIA_PROXY_ICON_SERVER_MAX_DIMENSION = 1024;
function snapImageSizeWithinServerRange(
cssPixels: number,
minDimension: number,
maxDimension: number,
): MediaProxyImageSize {
const floor = MEDIA_PROXY_IMAGE_SIZE_LADDER.find((rung) => rung >= minDimension) ?? LARGEST_MEDIA_PROXY_IMAGE_SIZE;
const ceiling = MEDIA_PROXY_IMAGE_SIZE_LADDER.reduce(
(largest, rung) => (rung <= maxDimension ? rung : largest),
MEDIA_PROXY_IMAGE_SIZE_LADDER[0],
);
const snapped = snapMediaProxyImageSize(cssPixels);
if (snapped < floor) return floor;
if (snapped > ceiling) return ceiling;
return snapped;
}
function snapWideAssetImageSize(cssPixels: number): MediaProxyImageSize {
return snapImageSizeWithinServerRange(
cssPixels,
MEDIA_PROXY_WIDE_ASSET_SERVER_MIN_DIMENSION,
MEDIA_PROXY_WIDE_ASSET_SERVER_MAX_DIMENSION,
);
}
function snapIconImageSize(cssPixels: number): MediaProxyImageSize {
return snapImageSizeWithinServerRange(
cssPixels,
MEDIA_PROXY_ICON_SERVER_MIN_DIMENSION,
MEDIA_PROXY_ICON_SERVER_MAX_DIMENSION,
);
}
const getViewportSplashSize = (): MediaProxyImageSize => {
const screenWidth = typeof window === 'undefined' ? Number.NaN : window.screen.width;
const cssWidth = Number.isFinite(screenWidth) && screenWidth > 0 ? screenWidth : GUILD_SPLASH_FALLBACK_CSS_WIDTH;
return snapWideAssetImageSize(cssWidth);
};
const DEFAULT_AVATAR_ASSET_VERSION = '1';
export const getDefaultAvatarURLForIndex = (index: number): string =>
cdnUrl(`avatars/${index}.png?v=${DEFAULT_AVATAR_ASSET_VERSION}`);
export function getDefaultAvatarPrimaryColor(id: string) { export function getDefaultAvatarPrimaryColor(id: string) {
return getSharedDefaultAvatarPrimaryColor(id); return getSharedDefaultAvatarPrimaryColor(id);
} }
export function getDefaultAvatarURL(id: string) {
return getDefaultAvatarURLForIndex(getDefaultAvatarIndex(id));
}
type AvatarOptions = Pick<User, 'id' | 'avatar'>; type AvatarOptions = Pick<User, 'id' | 'avatar'>;
type BannerOptions = Pick<User, 'id' | 'banner'>; type BannerOptions = Pick<User, 'id' | 'banner'>;
@@ -42,17 +105,18 @@ type MediaURLParams = {
}; };
const parseMediaHashForRequest = (value: string, animated = false) => { const parseMediaHashForRequest = (value: string, animated = false) => {
const {animated: isAnimated} = parseAvatarHash(value); const {animated: isAnimated, hash} = parseAvatarHash(value);
const shouldAnimate = isAnimated && animated;
return { return {
hash: value, hash: shouldAnimate ? value : hash,
animated: isAnimated ? animated : undefined, animated: shouldAnimate ? true : undefined,
}; };
}; };
const getMediaURL = ({path, id, hash, size, format, animated, endpoint}: MediaURLParams) => { const getMediaURL = ({path, id, hash, size, format, animated, endpoint}: MediaURLParams) => {
if (DeveloperOptions.forceRenderPlaceholders) { if (DeveloperOptions.forceRenderPlaceholders) {
return ''; return '';
} }
const baseEndpoint = endpoint ?? RuntimeConfig.mediaEndpoint; const baseEndpoint = endpoint === undefined ? RuntimeConfig.mediaEndpoint : endpoint;
if (!baseEndpoint) { if (!baseEndpoint) {
return ''; return '';
} }
@@ -89,6 +153,8 @@ const buildWebpMediaUrl = (params: Omit<MediaURLParams, 'format'>) => getMediaUR
const buildPngMediaUrl = (params: Omit<MediaURLParams, 'format'>) => getMediaURL({...params, format: 'png'}); const buildPngMediaUrl = (params: Omit<MediaURLParams, 'format'>) => getMediaURL({...params, format: 'png'});
const buildGuildMemberWebpUrl = (params: Omit<GuildMemberMediaURLParams, 'format'>) => const buildGuildMemberWebpUrl = (params: Omit<GuildMemberMediaURLParams, 'format'>) =>
getGuildMemberMediaURL({...params, format: 'webp'}); getGuildMemberMediaURL({...params, format: 'webp'});
const buildGuildMemberPngUrl = (params: Omit<GuildMemberMediaURLParams, 'format'>) =>
getGuildMemberMediaURL({...params, format: 'png'});
export function getUserAvatarURL( export function getUserAvatarURL(
{id, avatar}: AvatarOptions, {id, avatar}: AvatarOptions,
@@ -96,14 +162,14 @@ export function getUserAvatarURL(
size: MediaProxyImageSize = MEDIA_PROXY_AVATAR_SIZE_DEFAULT, size: MediaProxyImageSize = MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
) { ) {
if (!avatar) { if (!avatar) {
return getDefaultAvatar(getDefaultAvatarIndex(id)); return getDefaultAvatarURLForIndex(getDefaultAvatarIndex(id));
} }
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated); const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated);
return buildWebpMediaUrl({ return buildWebpMediaUrl({
path: 'avatars', path: 'avatars',
id, id,
hash, hash,
size, size: snapIconImageSize(size),
animated: shouldAnimate, animated: shouldAnimate,
}); });
} }
@@ -113,18 +179,49 @@ export function getUserNotificationAvatarURL(
size: MediaProxyImageSize = MEDIA_PROXY_AVATAR_SIZE_DEFAULT, size: MediaProxyImageSize = MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
) { ) {
if (!avatar) { if (!avatar) {
return getDefaultAvatar(getDefaultAvatarIndex(id)); return getDefaultAvatarURLForIndex(getDefaultAvatarIndex(id));
} }
const {hash, animated} = parseMediaHashForRequest(avatar, false); const {hash, animated} = parseMediaHashForRequest(avatar, false);
return buildPngMediaUrl({ return buildPngMediaUrl({
path: 'avatars', path: 'avatars',
id, id,
hash, hash,
size, size: snapIconImageSize(size),
animated, animated,
}); });
} }
export function getGuildMemberNotificationAvatarURL({
guildId,
userId,
avatar,
memberAvatar,
avatarUnset = false,
size = MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
}: {
guildId: string;
userId: string;
avatar: string | null;
memberAvatar?: string | null;
avatarUnset?: boolean;
size?: MediaProxyImageSize;
}) {
if (avatarUnset) {
return getUserNotificationAvatarURL({id: userId, avatar: null}, size);
}
if (memberAvatar) {
const {hash} = parseMediaHashForRequest(memberAvatar, false);
return buildGuildMemberPngUrl({
path: 'avatars',
guildId,
userId,
hash,
size: snapIconImageSize(size),
});
}
return getUserNotificationAvatarURL({id: userId, avatar}, size);
}
export function getUserBannerURL({id, banner}: BannerOptions, animated = false, size: MediaProxyImageSize = 1024) { export function getUserBannerURL({id, banner}: BannerOptions, animated = false, size: MediaProxyImageSize = 1024) {
if (!banner) { if (!banner) {
return ''; return '';
@@ -134,11 +231,27 @@ export function getUserBannerURL({id, banner}: BannerOptions, animated = false,
path: 'banners', path: 'banners',
id, id,
hash, hash,
size, size: snapWideAssetImageSize(size),
animated: shouldAnimate, animated: shouldAnimate,
}); });
} }
const SOUNDBOARD_SOUND_URL_CACHE = new Map<string, string>();
const SOUNDBOARD_SOUND_URL_CACHE_LIMIT = 4096;
export function getSoundboardSoundURL(soundId: string): string {
if (DeveloperOptions.forceRenderPlaceholders) {
return '';
}
const key = `${RuntimeConfig.mediaEndpoint}:${soundId}`;
const cached = SOUNDBOARD_SOUND_URL_CACHE.get(key);
if (cached !== undefined) return cached;
const result = mediaUrl(`${SOUNDBOARD_SOUND_PATH_PREFIX}/${soundId}`);
if (SOUNDBOARD_SOUND_URL_CACHE.size >= SOUNDBOARD_SOUND_URL_CACHE_LIMIT) SOUNDBOARD_SOUND_URL_CACHE.clear();
SOUNDBOARD_SOUND_URL_CACHE.set(key, result);
return result;
}
export function getGuildIconURL({id, icon}: IconOptions, animated = false) { export function getGuildIconURL({id, icon}: IconOptions, animated = false) {
if (!icon) { if (!icon) {
return ''; return '';
@@ -148,21 +261,12 @@ export function getGuildIconURL({id, icon}: IconOptions, animated = false) {
path: 'icons', path: 'icons',
id, id,
hash, hash,
size: MEDIA_PROXY_ICON_SIZE_DEFAULT, size: snapIconImageSize(MEDIA_PROXY_ICON_SIZE_DEFAULT),
animated: shouldAnimate, animated: shouldAnimate,
}); });
} }
export function getGuildSplashURL( export function getGuildSplashURL({id, splash}: {id: string; splash: string | null}) {
{
id,
splash,
}: {
id: string;
splash: string | null;
},
size: MediaProxyImageSize = 1024,
) {
if (!splash) { if (!splash) {
return ''; return '';
} }
@@ -170,19 +274,7 @@ export function getGuildSplashURL(
path: 'splashes', path: 'splashes',
id, id,
hash: splash, hash: splash,
size, size: getViewportSplashSize(),
});
}
export function getGuildDiscoverySplashURL({id, splash}: {id: string; splash: string | null}) {
if (!splash) {
return '';
}
return buildWebpMediaUrl({
path: 'discovery-splashes',
id,
hash: splash,
size: 1024,
}); });
} }
@@ -204,7 +296,7 @@ export function getGuildBannerURL(
path: 'banners', path: 'banners',
id, id,
hash, hash,
size: 1024, size: snapWideAssetImageSize(GUILD_BANNER_CSS_WIDTH),
animated: shouldAnimate, animated: shouldAnimate,
}); });
} }
@@ -231,7 +323,7 @@ export function getGuildMemberAvatarURL({
guildId, guildId,
userId, userId,
hash, hash,
size, size: snapIconImageSize(size),
animated: shouldAnimate, animated: shouldAnimate,
}); });
} }
@@ -241,11 +333,11 @@ export function getGuildMemberAvatarURL({
path: 'avatars', path: 'avatars',
id: userId, id: userId,
hash, hash,
size, size: snapIconImageSize(size),
animated: shouldAnimate, animated: shouldAnimate,
}); });
} }
return getDefaultAvatar(getDefaultAvatarIndex(userId)); return getDefaultAvatarURLForIndex(getDefaultAvatarIndex(userId));
} }
export function getGuildMemberDisplayAvatarURL({ export function getGuildMemberDisplayAvatarURL({
@@ -298,7 +390,7 @@ export function getGuildMemberBannerURL({
guildId, guildId,
userId, userId,
hash, hash,
size, size: snapWideAssetImageSize(size),
animated: shouldAnimate, animated: shouldAnimate,
}); });
} }
@@ -308,7 +400,7 @@ export function getGuildMemberBannerURL({
path: 'banners', path: 'banners',
id: userId, id: userId,
hash, hash,
size, size: snapWideAssetImageSize(size),
animated: shouldAnimate, animated: shouldAnimate,
}); });
} }
@@ -326,29 +418,20 @@ export function getUserAvatarURLWithProxy(
} }
const {id, avatar} = options; const {id, avatar} = options;
if (!avatar) { if (!avatar) {
return getDefaultAvatar(getDefaultAvatarIndex(id)); return getDefaultAvatarURLForIndex(getDefaultAvatarIndex(id));
} }
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated); const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated);
return buildWebpMediaUrl({ return buildWebpMediaUrl({
path: 'avatars', path: 'avatars',
id, id,
hash, hash,
size, size: snapIconImageSize(size),
animated: shouldAnimate, animated: shouldAnimate,
endpoint, endpoint,
}); });
} }
export function getGuildEmbedSplashURL( export function getGuildEmbedSplashURL({id, embedSplash}: {id: string; embedSplash: string | null}) {
{
id,
embedSplash,
}: {
id: string;
embedSplash: string | null;
},
size: MediaProxyImageSize = 1024,
) {
if (!embedSplash) { if (!embedSplash) {
return ''; return '';
} }
@@ -356,7 +439,7 @@ export function getGuildEmbedSplashURL(
path: 'embed-splashes', path: 'embed-splashes',
id, id,
hash: embedSplash, hash: embedSplash,
size, size: snapWideAssetImageSize(GUILD_EMBED_SPLASH_CSS_WIDTH),
}); });
} }
@@ -373,21 +456,21 @@ export function getChannelIconURL(
path: 'icons', path: 'icons',
id, id,
hash, hash,
size, size: snapIconImageSize(size),
animated: shouldAnimate, animated: shouldAnimate,
}); });
} }
export function getWebhookAvatarURL({id, avatar}: AvatarOptions, animated = false) { export function getWebhookAvatarURL({id, avatar}: AvatarOptions, animated = false) {
if (!avatar) { if (!avatar) {
return getDefaultAvatar(getDefaultAvatarIndex(id)); return getDefaultAvatarURLForIndex(getDefaultAvatarIndex(id));
} }
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated); const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated);
return buildWebpMediaUrl({ return buildWebpMediaUrl({
path: 'avatars', path: 'avatars',
id, id,
hash, hash,
size: MEDIA_PROXY_AVATAR_SIZE_DEFAULT, size: snapIconImageSize(MEDIA_PROXY_AVATAR_SIZE_DEFAULT),
animated: shouldAnimate, animated: shouldAnimate,
}); });
} }
@@ -395,15 +478,15 @@ export function getWebhookAvatarURL({id, avatar}: AvatarOptions, animated = fals
const EMOJI_URL_CACHE = new Map<string, string>(); const EMOJI_URL_CACHE = new Map<string, string>();
const EMOJI_URL_CACHE_LIMIT = 4096; const EMOJI_URL_CACHE_LIMIT = 4096;
export function getEmojiURL({id, animated}: {id: string; animated?: boolean}) { export function getEmojiURL({id, animated, isAnimatable}: {id: string; animated?: boolean; isAnimatable?: boolean}) {
if (DeveloperOptions.forceRenderPlaceholders) { if (DeveloperOptions.forceRenderPlaceholders) {
return ''; return '';
} }
const animatedFlag = Boolean(animated); const animatedFlag = isAnimatable === false ? false : animated === true;
const key = animatedFlag ? `a:${id}` : `s:${id}`; const key = `${RuntimeConfig.mediaEndpoint}:${animatedFlag ? 'a' : 's'}:${id}`;
const cached = EMOJI_URL_CACHE.get(key); const cached = EMOJI_URL_CACHE.get(key);
if (cached !== undefined) return cached; if (cached !== undefined) return cached;
const result = mediaUrl(setPathQueryParams(`emojis/${id}.webp`, {v: 5}), {animated: animatedFlag}); const result = mediaUrl(`emojis/${id}.webp`, {animated: animatedFlag});
if (EMOJI_URL_CACHE.size >= EMOJI_URL_CACHE_LIMIT) EMOJI_URL_CACHE.clear(); if (EMOJI_URL_CACHE.size >= EMOJI_URL_CACHE_LIMIT) EMOJI_URL_CACHE.clear();
EMOJI_URL_CACHE.set(key, result); EMOJI_URL_CACHE.set(key, result);
return result; return result;
@@ -411,12 +494,32 @@ export function getEmojiURL({id, animated}: {id: string; animated?: boolean}) {
type StickerSize = 160 | 320; type StickerSize = 160 | 320;
export function getStickerURL({id, animated, size = 320}: {id: string; animated?: boolean; size?: StickerSize}) { const STICKER_URL_CACHE = new Map<string, string>();
const STICKER_URL_CACHE_LIMIT = 4096;
export function getStickerURL({
id,
animated,
isAnimatable,
size = 320,
}: {
id: string;
animated?: boolean;
isAnimatable?: boolean;
size?: StickerSize;
}) {
if (DeveloperOptions.forceRenderPlaceholders) { if (DeveloperOptions.forceRenderPlaceholders) {
return ''; return '';
} }
const animatedFlag = isAnimatable === false ? false : animated === true;
const safeSize: StickerSize = size === 320 ? 320 : 160; const safeSize: StickerSize = size === 320 ? 320 : 160;
return mediaUrl(setPathQueryParams(`stickers/${id}.webp`, {size: safeSize}), {animated: Boolean(animated)}); const key = `${RuntimeConfig.mediaEndpoint}:${animatedFlag ? 'a' : 's'}:${safeSize}:${id}`;
const cached = STICKER_URL_CACHE.get(key);
if (cached !== undefined) return cached;
const result = mediaUrl(setPathQueryParams(`stickers/${id}.webp`, {size: safeSize}), {animated: animatedFlag});
if (STICKER_URL_CACHE.size >= STICKER_URL_CACHE_LIMIT) STICKER_URL_CACHE.clear();
STICKER_URL_CACHE.set(key, result);
return result;
} }
export function fileToBase64(file: File) { export function fileToBase64(file: File) {