mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(app): request avatar and banner sizes the media proxy serves (#1842)
This commit is contained in:
1 parent
c3e747aaa4
commit
bd381ccc74
8 files changed
+181
-563
No files matched your search
@@ -2,7 +2,15 @@
|
||||
|
||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
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 type {User} from '@app/features/user/models/User';
|
||||
import {
|
||||
@@ -16,13 +24,68 @@ import {
|
||||
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`);
|
||||
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) {
|
||||
return getSharedDefaultAvatarPrimaryColor(id);
|
||||
}
|
||||
|
||||
export function getDefaultAvatarURL(id: string) {
|
||||
return getDefaultAvatarURLForIndex(getDefaultAvatarIndex(id));
|
||||
}
|
||||
|
||||
type AvatarOptions = Pick<User, 'id' | 'avatar'>;
|
||||
type BannerOptions = Pick<User, 'id' | 'banner'>;
|
||||
|
||||
@@ -42,17 +105,18 @@ type MediaURLParams = {
|
||||
};
|
||||
|
||||
const parseMediaHashForRequest = (value: string, animated = false) => {
|
||||
const {animated: isAnimated} = parseAvatarHash(value);
|
||||
const {animated: isAnimated, hash} = parseAvatarHash(value);
|
||||
const shouldAnimate = isAnimated && animated;
|
||||
return {
|
||||
hash: value,
|
||||
animated: isAnimated ? animated : undefined,
|
||||
hash: shouldAnimate ? value : hash,
|
||||
animated: shouldAnimate ? true : undefined,
|
||||
};
|
||||
};
|
||||
const getMediaURL = ({path, id, hash, size, format, animated, endpoint}: MediaURLParams) => {
|
||||
if (DeveloperOptions.forceRenderPlaceholders) {
|
||||
return '';
|
||||
}
|
||||
const baseEndpoint = endpoint ?? RuntimeConfig.mediaEndpoint;
|
||||
const baseEndpoint = endpoint === undefined ? RuntimeConfig.mediaEndpoint : endpoint;
|
||||
if (!baseEndpoint) {
|
||||
return '';
|
||||
}
|
||||
@@ -89,6 +153,8 @@ const buildWebpMediaUrl = (params: Omit<MediaURLParams, 'format'>) => getMediaUR
|
||||
const buildPngMediaUrl = (params: Omit<MediaURLParams, 'format'>) => getMediaURL({...params, format: 'png'});
|
||||
const buildGuildMemberWebpUrl = (params: Omit<GuildMemberMediaURLParams, 'format'>) =>
|
||||
getGuildMemberMediaURL({...params, format: 'webp'});
|
||||
const buildGuildMemberPngUrl = (params: Omit<GuildMemberMediaURLParams, 'format'>) =>
|
||||
getGuildMemberMediaURL({...params, format: 'png'});
|
||||
|
||||
export function getUserAvatarURL(
|
||||
{id, avatar}: AvatarOptions,
|
||||
@@ -96,14 +162,14 @@ export function getUserAvatarURL(
|
||||
size: MediaProxyImageSize = MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
|
||||
) {
|
||||
if (!avatar) {
|
||||
return getDefaultAvatar(getDefaultAvatarIndex(id));
|
||||
return getDefaultAvatarURLForIndex(getDefaultAvatarIndex(id));
|
||||
}
|
||||
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated);
|
||||
return buildWebpMediaUrl({
|
||||
path: 'avatars',
|
||||
id,
|
||||
hash,
|
||||
size,
|
||||
size: snapIconImageSize(size),
|
||||
animated: shouldAnimate,
|
||||
});
|
||||
}
|
||||
@@ -113,18 +179,49 @@ export function getUserNotificationAvatarURL(
|
||||
size: MediaProxyImageSize = MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
|
||||
) {
|
||||
if (!avatar) {
|
||||
return getDefaultAvatar(getDefaultAvatarIndex(id));
|
||||
return getDefaultAvatarURLForIndex(getDefaultAvatarIndex(id));
|
||||
}
|
||||
const {hash, animated} = parseMediaHashForRequest(avatar, false);
|
||||
return buildPngMediaUrl({
|
||||
path: 'avatars',
|
||||
id,
|
||||
hash,
|
||||
size,
|
||||
size: snapIconImageSize(size),
|
||||
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) {
|
||||
if (!banner) {
|
||||
return '';
|
||||
@@ -134,11 +231,27 @@ export function getUserBannerURL({id, banner}: BannerOptions, animated = false,
|
||||
path: 'banners',
|
||||
id,
|
||||
hash,
|
||||
size,
|
||||
size: snapWideAssetImageSize(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 '';
|
||||
@@ -148,21 +261,12 @@ export function getGuildIconURL({id, icon}: IconOptions, animated = false) {
|
||||
path: 'icons',
|
||||
id,
|
||||
hash,
|
||||
size: MEDIA_PROXY_ICON_SIZE_DEFAULT,
|
||||
size: snapIconImageSize(MEDIA_PROXY_ICON_SIZE_DEFAULT),
|
||||
animated: shouldAnimate,
|
||||
});
|
||||
}
|
||||
|
||||
export function getGuildSplashURL(
|
||||
{
|
||||
id,
|
||||
splash,
|
||||
}: {
|
||||
id: string;
|
||||
splash: string | null;
|
||||
},
|
||||
size: MediaProxyImageSize = 1024,
|
||||
) {
|
||||
export function getGuildSplashURL({id, splash}: {id: string; splash: string | null}) {
|
||||
if (!splash) {
|
||||
return '';
|
||||
}
|
||||
@@ -170,19 +274,7 @@ export function getGuildSplashURL(
|
||||
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,
|
||||
size: getViewportSplashSize(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -204,7 +296,7 @@ export function getGuildBannerURL(
|
||||
path: 'banners',
|
||||
id,
|
||||
hash,
|
||||
size: 1024,
|
||||
size: snapWideAssetImageSize(GUILD_BANNER_CSS_WIDTH),
|
||||
animated: shouldAnimate,
|
||||
});
|
||||
}
|
||||
@@ -231,7 +323,7 @@ export function getGuildMemberAvatarURL({
|
||||
guildId,
|
||||
userId,
|
||||
hash,
|
||||
size,
|
||||
size: snapIconImageSize(size),
|
||||
animated: shouldAnimate,
|
||||
});
|
||||
}
|
||||
@@ -241,11 +333,11 @@ export function getGuildMemberAvatarURL({
|
||||
path: 'avatars',
|
||||
id: userId,
|
||||
hash,
|
||||
size,
|
||||
size: snapIconImageSize(size),
|
||||
animated: shouldAnimate,
|
||||
});
|
||||
}
|
||||
return getDefaultAvatar(getDefaultAvatarIndex(userId));
|
||||
return getDefaultAvatarURLForIndex(getDefaultAvatarIndex(userId));
|
||||
}
|
||||
|
||||
export function getGuildMemberDisplayAvatarURL({
|
||||
@@ -298,7 +390,7 @@ export function getGuildMemberBannerURL({
|
||||
guildId,
|
||||
userId,
|
||||
hash,
|
||||
size,
|
||||
size: snapWideAssetImageSize(size),
|
||||
animated: shouldAnimate,
|
||||
});
|
||||
}
|
||||
@@ -308,7 +400,7 @@ export function getGuildMemberBannerURL({
|
||||
path: 'banners',
|
||||
id: userId,
|
||||
hash,
|
||||
size,
|
||||
size: snapWideAssetImageSize(size),
|
||||
animated: shouldAnimate,
|
||||
});
|
||||
}
|
||||
@@ -326,29 +418,20 @@ export function getUserAvatarURLWithProxy(
|
||||
}
|
||||
const {id, avatar} = options;
|
||||
if (!avatar) {
|
||||
return getDefaultAvatar(getDefaultAvatarIndex(id));
|
||||
return getDefaultAvatarURLForIndex(getDefaultAvatarIndex(id));
|
||||
}
|
||||
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated);
|
||||
return buildWebpMediaUrl({
|
||||
path: 'avatars',
|
||||
id,
|
||||
hash,
|
||||
size,
|
||||
size: snapIconImageSize(size),
|
||||
animated: shouldAnimate,
|
||||
endpoint,
|
||||
});
|
||||
}
|
||||
|
||||
export function getGuildEmbedSplashURL(
|
||||
{
|
||||
id,
|
||||
embedSplash,
|
||||
}: {
|
||||
id: string;
|
||||
embedSplash: string | null;
|
||||
},
|
||||
size: MediaProxyImageSize = 1024,
|
||||
) {
|
||||
export function getGuildEmbedSplashURL({id, embedSplash}: {id: string; embedSplash: string | null}) {
|
||||
if (!embedSplash) {
|
||||
return '';
|
||||
}
|
||||
@@ -356,7 +439,7 @@ export function getGuildEmbedSplashURL(
|
||||
path: 'embed-splashes',
|
||||
id,
|
||||
hash: embedSplash,
|
||||
size,
|
||||
size: snapWideAssetImageSize(GUILD_EMBED_SPLASH_CSS_WIDTH),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -373,21 +456,21 @@ export function getChannelIconURL(
|
||||
path: 'icons',
|
||||
id,
|
||||
hash,
|
||||
size,
|
||||
size: snapIconImageSize(size),
|
||||
animated: shouldAnimate,
|
||||
});
|
||||
}
|
||||
|
||||
export function getWebhookAvatarURL({id, avatar}: AvatarOptions, animated = false) {
|
||||
if (!avatar) {
|
||||
return getDefaultAvatar(getDefaultAvatarIndex(id));
|
||||
return getDefaultAvatarURLForIndex(getDefaultAvatarIndex(id));
|
||||
}
|
||||
const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated);
|
||||
return buildWebpMediaUrl({
|
||||
path: 'avatars',
|
||||
id,
|
||||
hash,
|
||||
size: MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
|
||||
size: snapIconImageSize(MEDIA_PROXY_AVATAR_SIZE_DEFAULT),
|
||||
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_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) {
|
||||
return '';
|
||||
}
|
||||
const animatedFlag = Boolean(animated);
|
||||
const key = animatedFlag ? `a:${id}` : `s:${id}`;
|
||||
const animatedFlag = isAnimatable === false ? false : animated === true;
|
||||
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});
|
||||
const result = mediaUrl(`emojis/${id}.webp`, {animated: animatedFlag});
|
||||
if (EMOJI_URL_CACHE.size >= EMOJI_URL_CACHE_LIMIT) EMOJI_URL_CACHE.clear();
|
||||
EMOJI_URL_CACHE.set(key, result);
|
||||
return result;
|
||||
@@ -411,12 +494,32 @@ export function getEmojiURL({id, animated}: {id: string; animated?: boolean}) {
|
||||
|
||||
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) {
|
||||
return '';
|
||||
}
|
||||
const animatedFlag = isAnimatable === false ? false : animated === true;
|
||||
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) {
|
||||
|
||||
Reference in new issue
Block a user