pl-fe: default avatar/header detection cleanup

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-10-27 11:43:19 +01:00
parent ef1aba32af
commit f50ac8d73e
9 changed files with 46 additions and 45 deletions

View File

@ -1,6 +1,7 @@
import pick from 'lodash.pick';
import * as v from 'valibot';
import { isDefaultAvatar, isDefaultHeader } from '../utils/accounts';
import { guessFqn } from '../utils/domain';
import { customEmojiSchema } from './custom-emoji';
@ -30,6 +31,8 @@ const preprocessAccount = v.transform((account: any) => {
domain,
avatar: account.avatar || account.avatar_static,
header: account.header || account.header_static,
avatar_default: isDefaultAvatar(account.avatar || account.avatar_static),
header_default: isDefaultHeader(account.header || account.header_static),
local: typeof account.pleroma?.is_local === 'boolean' ? account.pleroma.is_local : account.acct.split('@')[1] === undefined,
discoverable: account.discoverable || account.pleroma?.source?.discoverable,
verified: account.verified || account.pleroma?.tags?.includes('verified'),
@ -187,6 +190,9 @@ const baseAccountSchema = v.object({
pleroma: v.optional(v.any(), undefined),
source: v.optional(v.any(), undefined),
}),
avatar_default: v.fallback(v.boolean(), false),
header_default: v.fallback(v.boolean(), false),
});
const accountWithMovedAccountSchema = v.object({

View File

@ -1,5 +1,6 @@
import * as v from 'valibot';
import { isDefaultAvatar, isDefaultHeader } from '../utils/accounts';
import { getDomainFromURL } from '../utils/domain';
import { customEmojiSchema } from './custom-emoji';
@ -35,6 +36,8 @@ const groupSchema = v.pipe(v.any(), v.transform((group: any) => {
...group,
avatar: group.avatar || group.avatar_static,
header: group.header || group.header_static,
avatar_default: isDefaultAvatar(group.avatar || group.avatar_static),
header_default: isDefaultHeader(group.header || group.header_static),
};
}), v.object({
avatar: v.fallback(v.string(), ''),
@ -58,6 +61,9 @@ const groupSchema = v.pipe(v.any(), v.transform((group: any) => {
avatar_description: v.fallback(v.string(), ''),
header_description: v.fallback(v.string(), ''),
avatar_default: v.fallback(v.boolean(), false),
header_default: v.fallback(v.boolean(), false),
}));
/**

View File

@ -0,0 +1,27 @@
/** Default header filenames from various backends */
const DEFAULT_HEADERS: string[] = [
'/assets/default_header.webp', // GoToSocial
'/headers/original/missing.png', // Mastodon
'/api/v1/accounts/identicon', // Mitra
'/images/banner.png', // Pleroma
'/assets/transparent.png', // Iceshrimp.net
];
/** Check if the avatar is a default avatar */
const isDefaultHeader = (url: string = '') => url === '' || DEFAULT_HEADERS.some(header => url.endsWith(header));
/** Default avatar filenames from various backends */
const DEFAULT_AVATARS = [
...([1, 2, 3, 4, 5, 6].map(i => `/assets/default_avatars/GoToSocial_icon${i}.webp`)), // GoToSocial
'/avatars/original/missing.png', // Mastodon
'/api/v1/accounts/identicon', // Mitra
'/images/avi.png', // Pleroma
];
/** Check if the avatar is a default avatar */
const isDefaultAvatar = (url: string = '') => url === '' || DEFAULT_AVATARS.some(avatar => url.endsWith(avatar));
export {
isDefaultHeader,
isDefaultAvatar,
};

View File

@ -5,7 +5,6 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import StillImage, { IStillImage } from 'pl-fe/components/still-image';
import { useSettings } from 'pl-fe/stores/settings';
import { isDefaultAvatar } from 'pl-fe/utils/accounts';
import AltIndicator from '../alt-indicator';

View File

@ -43,7 +43,6 @@ import { blockDomainMutationOptions, unblockDomainMutationOptions } from 'pl-fe/
import { useModalsActions } from 'pl-fe/stores/modals';
import { useSettings } from 'pl-fe/stores/settings';
import toast from 'pl-fe/toast';
import { isDefaultHeader } from 'pl-fe/utils/accounts';
import copy from 'pl-fe/utils/copy';
import type { PlfeResponse } from 'pl-fe/api';
@ -614,7 +613,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
let header: React.ReactNode;
if (settings.disableUserProvidedMedia) {
if (!account.header_description || isDefaultHeader(account.header)) return null;
if (!account.header_description || account.header_default) return null;
else return (
<Popover
interaction='hover'
@ -644,7 +643,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
/>
);
if (!isDefaultHeader(account.header)) {
if (!account.header_default) {
header = (
<a href={account.header} onClick={handleHeaderClick} target='_blank'>
{header}

View File

@ -12,7 +12,6 @@ import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import Emojify from 'pl-fe/features/emoji/emojify';
import { useModalsActions } from 'pl-fe/stores/modals';
import { isDefaultHeader } from 'pl-fe/utils/accounts';
import GroupActionButton from './group-action-button';
import GroupMemberCount from './group-member-count';
@ -101,7 +100,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
/>
);
if (!isDefaultHeader(group.header)) {
if (!group.header_default) {
header = (
<a href={group.header} onClick={handleHeaderClick} target='_blank' className='relative w-full'>
{header}

View File

@ -19,12 +19,8 @@ import { useTextField } from 'pl-fe/hooks/forms/use-text-field';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { useInstance } from 'pl-fe/hooks/use-instance';
import toast from 'pl-fe/toast';
import { isDefaultAvatar, isDefaultHeader } from 'pl-fe/utils/accounts';
import { unescapeHTML } from 'pl-fe/utils/html';
const nonDefaultAvatar = (url: string | undefined) => url && isDefaultAvatar(url) ? undefined : url;
const nonDefaultHeader = (url: string | undefined) => url && isDefaultHeader(url) ? undefined : url;
const messages = defineMessages({
heading: { id: 'navigation_bar.edit_group', defaultMessage: 'Edit Group' },
groupNamePlaceholder: { id: 'manage_group.fields.name_placeholder', defaultMessage: 'Group Name' },
@ -47,8 +43,8 @@ const EditGroup: React.FC<IEditGroup> = ({ params: { groupId } }) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const avatar = useImageField({ maxPixels: 400 * 400, preview: nonDefaultAvatar(group?.avatar) });
const header = useImageField({ maxPixels: 1920 * 1080, preview: nonDefaultHeader(group?.header) });
const avatar = useImageField({ maxPixels: 400 * 400, preview: group?.avatar_default === false ? group.avatar : undefined });
const header = useImageField({ maxPixels: 1920 * 1080, preview: group?.header_default === false ? group.header : undefined });
const displayName = useTextField(group?.display_name);
const note = useTextField(unescapeHTML(group?.note));

View File

@ -28,13 +28,9 @@ import { useFeatures } from 'pl-fe/hooks/use-features';
import { useInstance } from 'pl-fe/hooks/use-instance';
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
import toast from 'pl-fe/toast';
import { isDefaultAvatar, isDefaultHeader } from 'pl-fe/utils/accounts';
import type { StreamfieldComponent } from 'pl-fe/components/ui/streamfield';
const nonDefaultAvatar = (url: string | undefined) => url && isDefaultAvatar(url) ? undefined : url;
const nonDefaultHeader = (url: string | undefined) => url && isDefaultHeader(url) ? undefined : url;
/**
* Whether the user is hiding their follows and/or followers.
* Pleroma's config is granular, but we simplify it into one setting.
@ -209,8 +205,8 @@ const EditProfilePage: React.FC = () => {
const [muteStrangers, setMuteStrangers] = useState(false);
const [customCSSEditorExpanded, setCustomCSSEditorExpanded] = useState(false);
const avatar = useImageField({ maxPixels: 400 * 400, preview: nonDefaultAvatar(account?.avatar) });
const header = useImageField({ maxPixels: 1920 * 1080, preview: nonDefaultHeader(account?.header) });
const avatar = useImageField({ maxPixels: 400 * 400, preview: account?.avatar_default === false ? account.avatar : undefined });
const header = useImageField({ maxPixels: 1920 * 1080, preview: account?.header_default === false ? account.header : undefined });
useEffect(() => {
client.settings.verifyCredentials().then((credentialAccount) => {

View File

@ -28,35 +28,8 @@ const getAcct = (account: Pick<Account, 'fqn' | 'acct'>, displayFqn: boolean): s
displayFqn === true ? account.fqn : account.acct
);
/** Default header filenames from various backends */
const DEFAULT_HEADERS: string[] = [
'/assets/default_header.webp', // GoToSocial
'/headers/original/missing.png', // Mastodon
'/api/v1/accounts/identicon', // Mitra
'/images/banner.png', // Pleroma
'/assets/transparent.png', // Iceshrimp.net
require('pl-fe/assets/images/header-missing.png'), // header not provided by backend
];
/** Check if the avatar is a default avatar */
const isDefaultHeader = (url: string) => url === '' || DEFAULT_HEADERS.some(header => url.endsWith(header));
/** Default avatar filenames from various backends */
const DEFAULT_AVATARS = [
...(range(1, 6).map(i => `/assets/default_avatars/GoToSocial_icon${i}.webp`)), // GoToSocial
'/avatars/original/missing.png', // Mastodon
'/api/v1/accounts/identicon', // Mitra
'/images/avi.png', // Pleroma
require('pl-fe/assets/images/avatar-missing.png'), // avatar not provided by backend
];
/** Check if the avatar is a default avatar */
const isDefaultAvatar = (url: string) => url === '' || DEFAULT_AVATARS.some(avatar => url.endsWith(avatar));
export {
getDomain,
getBaseURL,
getAcct,
isDefaultHeader,
isDefaultAvatar,
};