mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(app): stop own profile preview trapping mobile web (#3463)
This commit is contained in:
1 parent
d7bbe933fa
commit
381699623e
4 files changed
+215
-187
No files matched your search
@@ -766,6 +766,7 @@ export const UserProfileModal: UserProfileModalComponent = observer(
|
|||||||
return (
|
return (
|
||||||
<Modal.Root
|
<Modal.Root
|
||||||
size="medium"
|
size="medium"
|
||||||
|
centered
|
||||||
initialFocusRef={autoFocusNote ? noteRef : undefined}
|
initialFocusRef={autoFocusNote ? noteRef : undefined}
|
||||||
className={userProfileModalStyles.modalRoot}
|
className={userProfileModalStyles.modalRoot}
|
||||||
data-flx="user.user-profile-modal.modal-root"
|
data-flx="user.user-profile-modal.modal-root"
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ import * as UserCommands from '@app/features/user/commands/UserCommands';
|
|||||||
import * as UserProfileCommands from '@app/features/user/commands/UserProfileCommands';
|
import * as UserProfileCommands from '@app/features/user/commands/UserProfileCommands';
|
||||||
import styles from '@app/features/user/components/modals/tabs/MyProfileTab.module.css';
|
import styles from '@app/features/user/components/modals/tabs/MyProfileTab.module.css';
|
||||||
import {AccentColorPicker} from '@app/features/user/components/modals/tabs/my_profile_tab/AccentColorPicker';
|
import {AccentColorPicker} from '@app/features/user/components/modals/tabs/my_profile_tab/AccentColorPicker';
|
||||||
import {AvatarUploader} from '@app/features/user/components/modals/tabs/my_profile_tab/AvatarUploader';
|
import {
|
||||||
|
AvatarUploader,
|
||||||
|
useChangeAvatarAction,
|
||||||
|
} from '@app/features/user/components/modals/tabs/my_profile_tab/AvatarUploader';
|
||||||
import {BannerUploader} from '@app/features/user/components/modals/tabs/my_profile_tab/BannerUploader';
|
import {BannerUploader} from '@app/features/user/components/modals/tabs/my_profile_tab/BannerUploader';
|
||||||
import {BIO_MARKDOWN_PARSER_FLAGS, BioEditor} from '@app/features/user/components/modals/tabs/my_profile_tab/BioEditor';
|
import {BIO_MARKDOWN_PARSER_FLAGS, BioEditor} from '@app/features/user/components/modals/tabs/my_profile_tab/BioEditor';
|
||||||
import {UsernameSection} from '@app/features/user/components/modals/tabs/my_profile_tab/MyProfileTabUsernameSection';
|
import {UsernameSection} from '@app/features/user/components/modals/tabs/my_profile_tab/MyProfileTabUsernameSection';
|
||||||
@@ -615,6 +618,7 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
|
|||||||
form.setValue('avatar', null);
|
form.setValue('avatar', null);
|
||||||
transitionAvatarAsset({type: 'asset.cleared'});
|
transitionAvatarAsset({type: 'asset.cleared'});
|
||||||
}, [form, transitionAvatarAsset]);
|
}, [form, transitionAvatarAsset]);
|
||||||
|
const {openChangeAvatar} = useChangeAvatarAction({onAvatarChange: handleAvatarChange});
|
||||||
const handleBannerChange = useCallback(
|
const handleBannerChange = useCallback(
|
||||||
(base64: string) => {
|
(base64: string) => {
|
||||||
form.setValue('banner', base64);
|
form.setValue('banner', base64);
|
||||||
@@ -674,6 +678,7 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
|
|||||||
const guilds = Guilds.getGuilds();
|
const guilds = Guilds.getGuilds();
|
||||||
const selectedGuild = selectedGuildId ? guilds.find((g) => g.id === selectedGuildId) : null;
|
const selectedGuild = selectedGuildId ? guilds.find((g) => g.id === selectedGuildId) : null;
|
||||||
const isPerGuildProfileCustomizationDisabled = isPerGuildProfile && !hasPerGuildProfiles;
|
const isPerGuildProfileCustomizationDisabled = isPerGuildProfile && !hasPerGuildProfiles;
|
||||||
|
const isAvatarCustomizationDisabled = isProfileCustomizationLocked || isPerGuildProfileCustomizationDisabled;
|
||||||
const isPronounsDisabled = isProfileCustomizationLocked;
|
const isPronounsDisabled = isProfileCustomizationLocked;
|
||||||
const profileCustomizationDescription = isProfileEmailLocked
|
const profileCustomizationDescription = isProfileEmailLocked
|
||||||
? isPerGuildProfile
|
? isPerGuildProfile
|
||||||
@@ -840,7 +845,7 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
|
|||||||
hasAvatar={hasAvatar}
|
hasAvatar={hasAvatar}
|
||||||
onAvatarChange={handleAvatarChange}
|
onAvatarChange={handleAvatarChange}
|
||||||
onAvatarClear={handleAvatarClear}
|
onAvatarClear={handleAvatarClear}
|
||||||
disabled={isProfileCustomizationLocked || isPerGuildProfileCustomizationDisabled}
|
disabled={isAvatarCustomizationDisabled}
|
||||||
disableModeSelection={isProfileCustomizationLocked}
|
disableModeSelection={isProfileCustomizationLocked}
|
||||||
isPerGuildProfile={isPerGuildProfile}
|
isPerGuildProfile={isPerGuildProfile}
|
||||||
errorMessage={form.formState.errors.avatar?.message}
|
errorMessage={form.formState.errors.avatar?.message}
|
||||||
@@ -934,6 +939,9 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
|
|||||||
}}
|
}}
|
||||||
ignoreGuildAvatarInPreview={isPerGuildProfile && avatarAsset.mode === 'inherit'}
|
ignoreGuildAvatarInPreview={isPerGuildProfile && avatarAsset.mode === 'inherit'}
|
||||||
ignoreGuildBannerInPreview={isPerGuildProfile && bannerAsset.mode === 'inherit'}
|
ignoreGuildBannerInPreview={isPerGuildProfile && bannerAsset.mode === 'inherit'}
|
||||||
|
onAvatarClick={
|
||||||
|
mobileLayout.enabled && !isAvatarCustomizationDisabled ? openChangeAvatar : undefined
|
||||||
|
}
|
||||||
data-flx="user.my-profile-tab.my-profile-tab-component.profile-preview"
|
data-flx="user.my-profile-tab.my-profile-tab-component.profile-preview"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+201
-184
@@ -116,6 +116,203 @@ interface AvatarUploaderProps {
|
|||||||
onAvatarModeChange?: (mode: AvatarMode) => void;
|
onAvatarModeChange?: (mode: AvatarMode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ChangeAvatarActionOptions {
|
||||||
|
onAvatarChange: (base64: string) => void;
|
||||||
|
requireAnimatedAvatarEntitlement?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChangeAvatarAction({
|
||||||
|
onAvatarChange,
|
||||||
|
requireAnimatedAvatarEntitlement = true,
|
||||||
|
}: ChangeAvatarActionOptions) {
|
||||||
|
const {i18n} = useLingui();
|
||||||
|
const PREMIUM_PRODUCT_NAME = getPremiumProductName();
|
||||||
|
const hasAnimatedAvatarEntitlement = isLimitToggleEnabled(
|
||||||
|
{feature_animated_avatar: LimitResolver.resolve({key: 'feature_animated_avatar', fallback: 0})},
|
||||||
|
'feature_animated_avatar',
|
||||||
|
);
|
||||||
|
const canUploadAnimatedAvatar = !requireAnimatedAvatarEntitlement || hasAnimatedAvatarEntitlement;
|
||||||
|
const processAvatarFile = useCallback(
|
||||||
|
async (file: File) => {
|
||||||
|
if (file.size > GlobalLimits.getAvatarMaxSize()) {
|
||||||
|
showGenericErrorModal({
|
||||||
|
title: i18n._(COULDN_T_UPLOAD_AVATAR_DESCRIPTOR),
|
||||||
|
message: i18n._(AVATAR_FILE_IS_TOO_LARGE_PLEASE_CHOOSE_A_DESCRIPTOR, {
|
||||||
|
imageMaxSizeLabel: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
|
||||||
|
}),
|
||||||
|
dataFlx: 'user.user-error-modal-utils.show-user-error-modal.generic-error-modal',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const svg = isSvgFile(file);
|
||||||
|
if (!svg && !(await canCropFile(file))) {
|
||||||
|
showGenericErrorModal({
|
||||||
|
title: i18n._(COULDN_T_UPLOAD_AVATAR_DESCRIPTOR),
|
||||||
|
message: getAssetFormatErrorMessage(i18n, 'avatar', 'unsupported_mime'),
|
||||||
|
dataFlx: 'user.user-error-modal-utils.show-user-error-modal.generic-error-modal',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {format, animated} = svg ? {format: 'unknown', animated: false} : await inspectImageFile(file);
|
||||||
|
if (animated && !canUploadAnimatedAvatar) {
|
||||||
|
if (shouldShowPremiumFeatures()) {
|
||||||
|
ModalCommands.push(
|
||||||
|
modal(() => (
|
||||||
|
<ConfirmModal
|
||||||
|
title={i18n._(ANIMATED_AVATARS_REQUIRE_DESCRIPTOR, {premiumProductName: getPremiumProductName()})}
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
<p data-flx="user.my-profile-tab.avatar-uploader.handle-avatar-upload.p">
|
||||||
|
<Trans>
|
||||||
|
Animated avatars ({ANIMATED_AVATAR_FORMATS}) are exclusively available to {PREMIUM_PRODUCT_NAME}{' '}
|
||||||
|
subscribers.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className={styles.spacedParagraph}
|
||||||
|
data-flx="user.my-profile-tab.avatar-uploader.handle-avatar-upload.spaced-paragraph"
|
||||||
|
>
|
||||||
|
{RuntimeConfig.usesUniqueUsernames ? (
|
||||||
|
<Trans>
|
||||||
|
With {PREMIUM_PRODUCT_NAME}, you can use animated avatars ({ANIMATED_AVATAR_FORMATS}) and
|
||||||
|
profile banners, and unlock many other {PREMIUM_PRODUCT_NAME} perks.
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>
|
||||||
|
With {PREMIUM_PRODUCT_NAME}, you can use animated avatars ({ANIMATED_AVATAR_FORMATS}) and
|
||||||
|
profile banners, customize your tag, and unlock many other {PREMIUM_PRODUCT_NAME} perks.
|
||||||
|
</Trans>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
primaryText={i18n._(GET_PREMIUM_DESCRIPTOR, {premiumProductName: getPremiumProductName()})}
|
||||||
|
primaryVariant="primary"
|
||||||
|
secondaryText={i18n._(CANCEL_DESCRIPTOR)}
|
||||||
|
onPrimary={() => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
PremiumModalCommands.open();
|
||||||
|
}, 0);
|
||||||
|
}}
|
||||||
|
data-flx="user.my-profile-tab.avatar-uploader.handle-avatar-upload.confirm-modal"
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ModalCommands.push(
|
||||||
|
modal(() => (
|
||||||
|
<GenericErrorModal
|
||||||
|
title={i18n._(ANIMATED_AVATARS_NOT_AVAILABLE_DESCRIPTOR)}
|
||||||
|
message={
|
||||||
|
<Trans>
|
||||||
|
Animated avatars ({ANIMATED_AVATAR_FORMATS}) are not available on this instance. Upload a static
|
||||||
|
image instead.
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
data-flx="user.my-profile-tab.avatar-uploader.handle-avatar-upload.confirm-modal--2"
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isAnimatedAvif = animated && format === 'avif';
|
||||||
|
if (isAnimatedAvif && !(await canDecodeAnimatedAvif())) {
|
||||||
|
showAnimatedAvifUnsupportedModal({i18n});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const base64 = svg ? await readImageFileAsUploadDataUrl(file) : await readBlobAsDataUrl(file);
|
||||||
|
ModalCommands.push(
|
||||||
|
modal(() => (
|
||||||
|
<AssetCropModal
|
||||||
|
assetType={AssetType.AVATAR}
|
||||||
|
imageUrl={base64}
|
||||||
|
onCropComplete={(croppedBlob) => {
|
||||||
|
void readBlobAsDataUrl(croppedBlob).then(
|
||||||
|
(croppedBase64) => {
|
||||||
|
onAvatarChange(croppedBase64);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
showGenericErrorModal({
|
||||||
|
title: i18n._(COULDN_T_UPLOAD_AVATAR_DESCRIPTOR),
|
||||||
|
message: i18n._(FAILED_TO_PROCESS_CROPPED_IMAGE_DESCRIPTOR),
|
||||||
|
dataFlx: 'user.user-error-modal-utils.show-user-error-modal.generic-error-modal',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onSkip={
|
||||||
|
isAnimatedAvif
|
||||||
|
? undefined
|
||||||
|
: () => {
|
||||||
|
onAvatarChange(base64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data-flx="user.my-profile-tab.avatar-uploader.handle-avatar-upload.asset-crop-modal"
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[canUploadAnimatedAvatar, onAvatarChange, i18n],
|
||||||
|
);
|
||||||
|
const showAvatarUploadError = useCallback(() => {
|
||||||
|
showGenericErrorModal({
|
||||||
|
title: i18n._(COULDN_T_UPLOAD_AVATAR_DESCRIPTOR),
|
||||||
|
message: i18n._(INVALID_IMAGE_TRY_ANOTHER_DESCRIPTOR),
|
||||||
|
dataFlx: 'user.user-error-modal-utils.show-user-error-modal.generic-error-modal',
|
||||||
|
});
|
||||||
|
}, [i18n]);
|
||||||
|
const handlePickAvatarFile = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const [file] = await openFilePicker({accept: getAcceptString('avatar')});
|
||||||
|
if (!file) return;
|
||||||
|
await processAvatarFile(file);
|
||||||
|
} catch {
|
||||||
|
showAvatarUploadError();
|
||||||
|
}
|
||||||
|
}, [processAvatarFile, showAvatarUploadError]);
|
||||||
|
const handleSelectAvatarGif = useCallback(
|
||||||
|
async (gif: Gif) => {
|
||||||
|
try {
|
||||||
|
await processAvatarFile(await downloadGifAsImageFile(gif));
|
||||||
|
} catch {
|
||||||
|
showAvatarUploadError();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[processAvatarFile, showAvatarUploadError],
|
||||||
|
);
|
||||||
|
const handleAvatarUpload = useCallback(() => {
|
||||||
|
const uploadHint = canUploadAnimatedAvatar
|
||||||
|
? formatImageUploadRecommendedHint(i18n, {
|
||||||
|
formats: ANIMATED_IMAGE_FORMATS,
|
||||||
|
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
|
||||||
|
recommendedSize: AVATAR_RECOMMENDED_SIZE_LABEL,
|
||||||
|
})
|
||||||
|
: shouldShowPremiumFeatures()
|
||||||
|
? formatImageUploadRecommendedHintWithNote(i18n, {
|
||||||
|
formats: STATIC_IMAGE_FORMATS,
|
||||||
|
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
|
||||||
|
recommendedSize: AVATAR_RECOMMENDED_SIZE_LABEL,
|
||||||
|
note: i18n._(ANIMATED_AVATARS_REQUIRE_PREMIUM_NOTE_DESCRIPTOR, {
|
||||||
|
animatedAvatarFormats: ANIMATED_AVATAR_FORMATS,
|
||||||
|
premiumProductName: getPremiumProductName(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
: formatImageUploadRecommendedHint(i18n, {
|
||||||
|
formats: STATIC_IMAGE_FORMATS,
|
||||||
|
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
|
||||||
|
recommendedSize: AVATAR_RECOMMENDED_SIZE_LABEL,
|
||||||
|
});
|
||||||
|
openAssetSourceModal({
|
||||||
|
title: i18n._(CHANGE_AVATAR_DESCRIPTOR),
|
||||||
|
uploadHint,
|
||||||
|
onPickUpload: handlePickAvatarFile,
|
||||||
|
onSelectGif: (gif) => void handleSelectAvatarGif(gif),
|
||||||
|
});
|
||||||
|
}, [canUploadAnimatedAvatar, handlePickAvatarFile, handleSelectAvatarGif, i18n]);
|
||||||
|
return {canUploadAnimatedAvatar, openChangeAvatar: handleAvatarUpload};
|
||||||
|
}
|
||||||
|
|
||||||
export const AvatarUploader = observer(
|
export const AvatarUploader = observer(
|
||||||
({
|
({
|
||||||
hasAvatar,
|
hasAvatar,
|
||||||
@@ -129,13 +326,11 @@ export const AvatarUploader = observer(
|
|||||||
avatarMode = 'inherit',
|
avatarMode = 'inherit',
|
||||||
onAvatarModeChange,
|
onAvatarModeChange,
|
||||||
}: AvatarUploaderProps) => {
|
}: AvatarUploaderProps) => {
|
||||||
const PREMIUM_PRODUCT_NAME = getPremiumProductName();
|
|
||||||
const {i18n} = useLingui();
|
const {i18n} = useLingui();
|
||||||
const hasAnimatedAvatarEntitlement = isLimitToggleEnabled(
|
const {canUploadAnimatedAvatar, openChangeAvatar: handleAvatarUpload} = useChangeAvatarAction({
|
||||||
{feature_animated_avatar: LimitResolver.resolve({key: 'feature_animated_avatar', fallback: 0})},
|
onAvatarChange,
|
||||||
'feature_animated_avatar',
|
requireAnimatedAvatarEntitlement,
|
||||||
);
|
});
|
||||||
const canUploadAnimatedAvatar = !requireAnimatedAvatarEntitlement || hasAnimatedAvatarEntitlement;
|
|
||||||
const getAvatarModeOptions = useCallback(
|
const getAvatarModeOptions = useCallback(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@@ -157,184 +352,6 @@ export const AvatarUploader = observer(
|
|||||||
],
|
],
|
||||||
[canUploadAnimatedAvatar, i18n],
|
[canUploadAnimatedAvatar, i18n],
|
||||||
);
|
);
|
||||||
const processAvatarFile = useCallback(
|
|
||||||
async (file: File) => {
|
|
||||||
if (file.size > GlobalLimits.getAvatarMaxSize()) {
|
|
||||||
showGenericErrorModal({
|
|
||||||
title: i18n._(COULDN_T_UPLOAD_AVATAR_DESCRIPTOR),
|
|
||||||
message: i18n._(AVATAR_FILE_IS_TOO_LARGE_PLEASE_CHOOSE_A_DESCRIPTOR, {
|
|
||||||
imageMaxSizeLabel: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
|
|
||||||
}),
|
|
||||||
dataFlx: 'user.user-error-modal-utils.show-user-error-modal.generic-error-modal',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const svg = isSvgFile(file);
|
|
||||||
if (!svg && !(await canCropFile(file))) {
|
|
||||||
showGenericErrorModal({
|
|
||||||
title: i18n._(COULDN_T_UPLOAD_AVATAR_DESCRIPTOR),
|
|
||||||
message: getAssetFormatErrorMessage(i18n, 'avatar', 'unsupported_mime'),
|
|
||||||
dataFlx: 'user.user-error-modal-utils.show-user-error-modal.generic-error-modal',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const {format, animated} = svg ? {format: 'unknown', animated: false} : await inspectImageFile(file);
|
|
||||||
if (animated && !canUploadAnimatedAvatar) {
|
|
||||||
if (shouldShowPremiumFeatures()) {
|
|
||||||
ModalCommands.push(
|
|
||||||
modal(() => (
|
|
||||||
<ConfirmModal
|
|
||||||
title={i18n._(ANIMATED_AVATARS_REQUIRE_DESCRIPTOR, {premiumProductName: getPremiumProductName()})}
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
<p data-flx="user.my-profile-tab.avatar-uploader.handle-avatar-upload.p">
|
|
||||||
<Trans>
|
|
||||||
Animated avatars ({ANIMATED_AVATAR_FORMATS}) are exclusively available to{' '}
|
|
||||||
{PREMIUM_PRODUCT_NAME} subscribers.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className={styles.spacedParagraph}
|
|
||||||
data-flx="user.my-profile-tab.avatar-uploader.handle-avatar-upload.spaced-paragraph"
|
|
||||||
>
|
|
||||||
{RuntimeConfig.usesUniqueUsernames ? (
|
|
||||||
<Trans>
|
|
||||||
With {PREMIUM_PRODUCT_NAME}, you can use animated avatars ({ANIMATED_AVATAR_FORMATS}) and
|
|
||||||
profile banners, and unlock many other {PREMIUM_PRODUCT_NAME} perks.
|
|
||||||
</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>
|
|
||||||
With {PREMIUM_PRODUCT_NAME}, you can use animated avatars ({ANIMATED_AVATAR_FORMATS}) and
|
|
||||||
profile banners, customize your tag, and unlock many other {PREMIUM_PRODUCT_NAME} perks.
|
|
||||||
</Trans>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
primaryText={i18n._(GET_PREMIUM_DESCRIPTOR, {premiumProductName: getPremiumProductName()})}
|
|
||||||
primaryVariant="primary"
|
|
||||||
secondaryText={i18n._(CANCEL_DESCRIPTOR)}
|
|
||||||
onPrimary={() => {
|
|
||||||
window.setTimeout(() => {
|
|
||||||
PremiumModalCommands.open();
|
|
||||||
}, 0);
|
|
||||||
}}
|
|
||||||
data-flx="user.my-profile-tab.avatar-uploader.handle-avatar-upload.confirm-modal"
|
|
||||||
/>
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ModalCommands.push(
|
|
||||||
modal(() => (
|
|
||||||
<GenericErrorModal
|
|
||||||
title={i18n._(ANIMATED_AVATARS_NOT_AVAILABLE_DESCRIPTOR)}
|
|
||||||
message={
|
|
||||||
<Trans>
|
|
||||||
Animated avatars ({ANIMATED_AVATAR_FORMATS}) are not available on this instance. Upload a static
|
|
||||||
image instead.
|
|
||||||
</Trans>
|
|
||||||
}
|
|
||||||
data-flx="user.my-profile-tab.avatar-uploader.handle-avatar-upload.confirm-modal--2"
|
|
||||||
/>
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const isAnimatedAvif = animated && format === 'avif';
|
|
||||||
if (isAnimatedAvif && !(await canDecodeAnimatedAvif())) {
|
|
||||||
showAnimatedAvifUnsupportedModal({i18n});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const base64 = svg ? await readImageFileAsUploadDataUrl(file) : await readBlobAsDataUrl(file);
|
|
||||||
ModalCommands.push(
|
|
||||||
modal(() => (
|
|
||||||
<AssetCropModal
|
|
||||||
assetType={AssetType.AVATAR}
|
|
||||||
imageUrl={base64}
|
|
||||||
onCropComplete={(croppedBlob) => {
|
|
||||||
void readBlobAsDataUrl(croppedBlob).then(
|
|
||||||
(croppedBase64) => {
|
|
||||||
onAvatarChange(croppedBase64);
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
showGenericErrorModal({
|
|
||||||
title: i18n._(COULDN_T_UPLOAD_AVATAR_DESCRIPTOR),
|
|
||||||
message: i18n._(FAILED_TO_PROCESS_CROPPED_IMAGE_DESCRIPTOR),
|
|
||||||
dataFlx: 'user.user-error-modal-utils.show-user-error-modal.generic-error-modal',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
onSkip={
|
|
||||||
isAnimatedAvif
|
|
||||||
? undefined
|
|
||||||
: () => {
|
|
||||||
onAvatarChange(base64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data-flx="user.my-profile-tab.avatar-uploader.handle-avatar-upload.asset-crop-modal"
|
|
||||||
/>
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[canUploadAnimatedAvatar, onAvatarChange, i18n],
|
|
||||||
);
|
|
||||||
const showAvatarUploadError = useCallback(() => {
|
|
||||||
showGenericErrorModal({
|
|
||||||
title: i18n._(COULDN_T_UPLOAD_AVATAR_DESCRIPTOR),
|
|
||||||
message: i18n._(INVALID_IMAGE_TRY_ANOTHER_DESCRIPTOR),
|
|
||||||
dataFlx: 'user.user-error-modal-utils.show-user-error-modal.generic-error-modal',
|
|
||||||
});
|
|
||||||
}, [i18n]);
|
|
||||||
const handlePickAvatarFile = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const [file] = await openFilePicker({accept: getAcceptString('avatar')});
|
|
||||||
if (!file) return;
|
|
||||||
await processAvatarFile(file);
|
|
||||||
} catch {
|
|
||||||
showAvatarUploadError();
|
|
||||||
}
|
|
||||||
}, [processAvatarFile, showAvatarUploadError]);
|
|
||||||
const handleSelectAvatarGif = useCallback(
|
|
||||||
async (gif: Gif) => {
|
|
||||||
try {
|
|
||||||
await processAvatarFile(await downloadGifAsImageFile(gif));
|
|
||||||
} catch {
|
|
||||||
showAvatarUploadError();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[processAvatarFile, showAvatarUploadError],
|
|
||||||
);
|
|
||||||
const handleAvatarUpload = useCallback(() => {
|
|
||||||
const uploadHint = canUploadAnimatedAvatar
|
|
||||||
? formatImageUploadRecommendedHint(i18n, {
|
|
||||||
formats: ANIMATED_IMAGE_FORMATS,
|
|
||||||
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
|
|
||||||
recommendedSize: AVATAR_RECOMMENDED_SIZE_LABEL,
|
|
||||||
})
|
|
||||||
: shouldShowPremiumFeatures()
|
|
||||||
? formatImageUploadRecommendedHintWithNote(i18n, {
|
|
||||||
formats: STATIC_IMAGE_FORMATS,
|
|
||||||
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
|
|
||||||
recommendedSize: AVATAR_RECOMMENDED_SIZE_LABEL,
|
|
||||||
note: i18n._(ANIMATED_AVATARS_REQUIRE_PREMIUM_NOTE_DESCRIPTOR, {
|
|
||||||
animatedAvatarFormats: ANIMATED_AVATAR_FORMATS,
|
|
||||||
premiumProductName: getPremiumProductName(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
: formatImageUploadRecommendedHint(i18n, {
|
|
||||||
formats: STATIC_IMAGE_FORMATS,
|
|
||||||
maxSize: formatFileSize(i18n.locale, GlobalLimits.getAvatarMaxSize()),
|
|
||||||
recommendedSize: AVATAR_RECOMMENDED_SIZE_LABEL,
|
|
||||||
});
|
|
||||||
openAssetSourceModal({
|
|
||||||
title: i18n._(CHANGE_AVATAR_DESCRIPTOR),
|
|
||||||
uploadHint,
|
|
||||||
onPickUpload: handlePickAvatarFile,
|
|
||||||
onSelectGif: (gif) => void handleSelectAvatarGif(gif),
|
|
||||||
});
|
|
||||||
}, [canUploadAnimatedAvatar, handlePickAvatarFile, handleSelectAvatarGif, i18n]);
|
|
||||||
const handleModeChange = useCallback(
|
const handleModeChange = useCallback(
|
||||||
(mode: AvatarMode) => {
|
(mode: AvatarMode) => {
|
||||||
if (mode === 'custom') {
|
if (mode === 'custom') {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ interface ProfilePreviewProps {
|
|||||||
showMessageButton?: boolean;
|
showMessageButton?: boolean;
|
||||||
showPreviewLabel?: boolean;
|
showPreviewLabel?: boolean;
|
||||||
previewCustomStatus?: CustomStatus | null;
|
previewCustomStatus?: CustomStatus | null;
|
||||||
|
onAvatarClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProfilePreview: React.FC<ProfilePreviewProps> = observer(
|
export const ProfilePreview: React.FC<ProfilePreviewProps> = observer(
|
||||||
@@ -90,6 +91,7 @@ export const ProfilePreview: React.FC<ProfilePreviewProps> = observer(
|
|||||||
showMessageButton = true,
|
showMessageButton = true,
|
||||||
showPreviewLabel = true,
|
showPreviewLabel = true,
|
||||||
previewCustomStatus,
|
previewCustomStatus,
|
||||||
|
onAvatarClick,
|
||||||
}) => {
|
}) => {
|
||||||
const {i18n} = useLingui();
|
const {i18n} = useLingui();
|
||||||
const previewOverrides = useMemo<ProfilePreviewOverrides>(
|
const previewOverrides = useMemo<ProfilePreviewOverrides>(
|
||||||
@@ -245,7 +247,7 @@ export const ProfilePreview: React.FC<ProfilePreviewProps> = observer(
|
|||||||
avatarUrl={finalAvatarUrl}
|
avatarUrl={finalAvatarUrl}
|
||||||
hoverAvatarUrl={finalHoverAvatarUrl}
|
hoverAvatarUrl={finalHoverAvatarUrl}
|
||||||
isClickable={true}
|
isClickable={true}
|
||||||
onAvatarClick={openMockProfile}
|
onAvatarClick={onAvatarClick ?? openMockProfile}
|
||||||
data-flx="user.profile.profile-preview.profile-card-banner"
|
data-flx="user.profile.profile-preview.profile-card-banner"
|
||||||
/>
|
/>
|
||||||
<UserProfileBadges
|
<UserProfileBadges
|
||||||
|
|||||||
Reference in new issue
Block a user