mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +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
@@ -131,13 +131,10 @@ export const InviteAcceptModal = observer(function InviteAcceptModal({code}: Inv
|
||||
if (!guild.id || !guild.splash) {
|
||||
return null;
|
||||
}
|
||||
return AvatarUtils.getGuildSplashURL(
|
||||
{
|
||||
id: guild.id,
|
||||
splash: guild.splash,
|
||||
},
|
||||
4096,
|
||||
);
|
||||
return AvatarUtils.getGuildSplashURL({
|
||||
id: guild.id,
|
||||
splash: guild.splash,
|
||||
});
|
||||
}, [invite]);
|
||||
const isJoinDisabled = isGuildInviteActionDisabled(guildActionState);
|
||||
const primaryActionType = getGuildInvitePrimaryAction(guildActionState);
|
||||
|
||||
@@ -57,7 +57,7 @@ export const InvitePagePreviewModal: React.FC<InvitePagePreviewModalProps> = obs
|
||||
const splashUrl = useMemo(() => {
|
||||
if (previewSplashUrl) return previewSplashUrl;
|
||||
if (guild?.splash) {
|
||||
return AvatarUtils.getGuildSplashURL({id: guild.id, splash: guild.splash}, 4096);
|
||||
return AvatarUtils.getGuildSplashURL({id: guild.id, splash: guild.splash});
|
||||
}
|
||||
return null;
|
||||
}, [previewSplashUrl, guild?.id, guild?.splash]);
|
||||
|
||||
@@ -170,7 +170,7 @@ const InviteLoginPageContainer = observer(() => {
|
||||
}
|
||||
const guild = guildInvite.guild;
|
||||
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);
|
||||
}
|
||||
}, [guildInvite?.guild?.splash, guildInvite?.guild?.id, setSplashUrl]);
|
||||
|
||||
@@ -66,13 +66,10 @@ const InviteRegisterPage = observer(function InviteRegisterPage() {
|
||||
}
|
||||
const guild = guildInvite.guild;
|
||||
if (guild?.splash) {
|
||||
const splashUrl = AvatarUtils.getGuildSplashURL(
|
||||
{
|
||||
id: guild.id,
|
||||
splash: guild.splash,
|
||||
},
|
||||
4096,
|
||||
);
|
||||
const splashUrl = AvatarUtils.getGuildSplashURL({
|
||||
id: guild.id,
|
||||
splash: guild.splash,
|
||||
});
|
||||
setSplashUrl(splashUrl);
|
||||
}
|
||||
}, [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 {Permissions} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {
|
||||
MEDIA_PROXY_AVATAR_SIZE_PROFILE,
|
||||
MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
|
||||
MEDIA_PROXY_PROFILE_BANNER_SIZE_MODAL,
|
||||
} from '@fluxer/constants/src/MediaProxyAssetSizes';
|
||||
import {PublicUserFlags, RelationshipTypes} from '@fluxer/constants/src/UserConstants';
|
||||
@@ -282,7 +282,7 @@ const UserProfileMobileSheetContent: React.FC<UserProfileMobileSheetContentProps
|
||||
);
|
||||
const shouldAutoplayProfileAnimations = useAutoplayExpandedProfileAnimations();
|
||||
const {avatarUrl, hoverAvatarUrl} = useMemo(
|
||||
() => ProfileDisplayUtils.getProfileAvatarUrls(profileContext, undefined, MEDIA_PROXY_AVATAR_SIZE_PROFILE),
|
||||
() => ProfileDisplayUtils.getProfileAvatarUrls(profileContext, undefined, MEDIA_PROXY_AVATAR_SIZE_DEFAULT),
|
||||
[profileContext],
|
||||
);
|
||||
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 channelItemSurfaceStyles from '@app/features/app/components/layout/ChannelItemSurface.module.css';
|
||||
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 {MockAvatar} from '@app/features/ui/components/MockAvatar';
|
||||
import type {RadioOption} 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 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 {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {clsx} from 'clsx';
|
||||
@@ -92,7 +92,7 @@ const ChannelListPreview = observer(({mode}: {mode: ChannelTypingIndicatorMode})
|
||||
key={index}
|
||||
size={12}
|
||||
userTag={`User ${index}`}
|
||||
avatarUrl={cdnUrl(`avatars/${index}.png`)}
|
||||
avatarUrl={getDefaultAvatarURLForIndex(index)}
|
||||
data-flx="user.appearance-tab.interface-tab.channel-list-preview.mock-avatar"
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,482 +1,3 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
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 {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);
|
||||
});
|
||||
}
|
||||
export * from '@app/features/user/utils/AvatarUtils';
|
||||
@@ -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