nicolium: Allow displaying avatars next to mentions

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-03-17 11:44:23 +01:00
parent 70db9d5798
commit fe59a5b34f
11 changed files with 135 additions and 12 deletions

View File

@ -0,0 +1,25 @@
import React from 'react';
import { useAccount } from '@/queries/accounts/use-account';
import Avatar from '../ui/avatar';
import HoverAccountWrapper from './hover-account-wrapper';
interface IMentionWithAvatar {
id: string;
username: string;
}
const MentionWithAvatar: React.FC<IMentionWithAvatar> = ({ id, username }) => {
const { data: account } = useAccount(id);
return (
<HoverAccountWrapper accountId={id} element='span' className='⁂-mention-with-avatar'>
<Avatar size={16} src={account?.avatar || ''} alt={account?.avatar_description} />
<span>@{username}</span>
</HoverAccountWrapper>
);
};
export { MentionWithAvatar };

View File

@ -73,7 +73,7 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo
} else if (typeof item.action === 'function') {
event.preventDefault();
item.action(event);
} else if (typeof item.onChange == 'function') {
} else if (typeof item.onChange === 'function') {
event.preventDefault();
item.onChange(!item.checked);
}

View File

@ -18,6 +18,7 @@ import { makeEmojiMap } from '@/utils/normalizers';
import Purify from '@/utils/url-purify';
import HoverAccountWrapper from '../accounts/hover-account-wrapper';
import { MentionWithAvatar } from '../accounts/mention-with-avatar';
import HashtagLink from '../hashtag-link';
import StatusMention from './status-mention';
@ -131,6 +132,7 @@ interface IParsedContent {
displayTargetHost?: boolean;
greentext?: boolean;
speakAsCat?: boolean;
displayMentionAvatars?: boolean;
}
// Adapted from Mastodon https://github.com/mastodon/mastodon/blob/main/app/javascript/mastodon/components/hashtag_bar.tsx
@ -175,7 +177,15 @@ function parseContent(
};
function parseContent(
{ html, mentions, hasQuote, emojis, greentext = false, speakAsCat = false }: IParsedContent,
{
html,
mentions,
hasQuote,
emojis,
greentext = false,
speakAsCat = false,
displayMentionAvatars = false,
}: IParsedContent,
extractHashtags = false,
) {
if (html.length === 0) {
@ -272,9 +282,13 @@ function parseContent(
e.stopPropagation();
}}
>
<HoverAccountWrapper accountId={mention.id} element='span'>
@{mention.username}
</HoverAccountWrapper>
{displayMentionAvatars ? (
<MentionWithAvatar id={mention.id} username={mention.username} />
) : (
<HoverAccountWrapper accountId={mention.id} element='span'>
@{mention.username}
</HoverAccountWrapper>
)}
</Link>
);
}
@ -371,13 +385,14 @@ function parseContent(
const ParsedContent: React.FC<IParsedContent> = React.memo(
(props) => {
const { urlPrivacy } = useSettings();
const { urlPrivacy, displayMentionAvatars } = useSettings();
props = { ...props };
props.cleanUrls ??= urlPrivacy.clearLinksInContent;
props.redirectUrls ??= urlPrivacy.redirectLinksMode !== 'off';
props.displayTargetHost ??= urlPrivacy.displayTargetHost;
props.displayMentionAvatars ??= displayMentionAvatars;
return parseContent(props, false);
},

View File

@ -77,7 +77,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(
withMedia,
compose = false,
}) => {
const { urlPrivacy, displaySpoilers, renderMfm } = useSettings();
const { urlPrivacy, displaySpoilers, renderMfm, displayMentionAvatars } = useSettings();
const { greentext } = useFrontendConfig();
const { data: account } = useAccount(status.account_id);
@ -174,10 +174,11 @@ const StatusContent: React.FC<IStatusContent> = React.memo(
displayTargetHost: urlPrivacy.displayTargetHost,
greentext,
speakAsCat: account?.speak_as_cat,
displayMentionAvatars,
},
true,
);
}, [content, renderMfm, account?.speak_as_cat]);
}, [content, renderMfm, account?.speak_as_cat, displayMentionAvatars]);
const spoilerText =
status.spoiler_text_map && statusMeta.currentLanguage

View File

@ -2,8 +2,10 @@ import React from 'react';
import { Link } from '@/components/link';
import { useAccount } from '@/queries/accounts/use-account';
import { useSettings } from '@/stores/settings';
import HoverAccountWrapper from '../accounts/hover-account-wrapper';
import { MentionWithAvatar } from '../accounts/mention-with-avatar';
interface IStatusMention {
accountId: string;
@ -13,6 +15,8 @@ interface IStatusMention {
const StatusMention: React.FC<IStatusMention> = ({ accountId, fallback }) => {
const { data: account } = useAccount(accountId);
const { displayMentionAvatars } = useSettings();
if (!account)
return (
<HoverAccountWrapper accountId={accountId} element='span'>
@ -29,9 +33,13 @@ const StatusMention: React.FC<IStatusMention> = ({ accountId, fallback }) => {
e.stopPropagation();
}}
>
<HoverAccountWrapper accountId={accountId} element='span'>
@{account.acct}
</HoverAccountWrapper>
{displayMentionAvatars ? (
<MentionWithAvatar id={accountId} username={account.acct} />
) : (
<HoverAccountWrapper accountId={accountId} element='span'>
@{account.acct}
</HoverAccountWrapper>
)}
</Link>
);
};

View File

@ -724,6 +724,21 @@ const Preferences = () => {
/>
</ListItem>
<ListItem
label={
<FormattedMessage
id='preferences.fields.display_mention_avatars'
defaultMessage='Show avatars next to mentions'
/>
}
>
<SettingToggle
settings={settings}
settingPath={['displayMentionAvatars']}
onChange={onToggleChange}
/>
</ListItem>
<ListItem
label={
<FormattedMessage

View File

@ -12,6 +12,7 @@ import { useCurrentAccount } from '@/contexts/current-account-context';
import Emojify from '@/features/emoji/emojify';
import { useAcct } from '@/hooks/use-acct';
import { useAccountScrobbleQuery } from '@/queries/accounts/account-scrobble';
import { useSettings } from '@/stores/settings';
import { capitalize } from '@/utils/strings';
import { ProfileField } from '../../util/async-components';
@ -51,6 +52,7 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
const acct = useAcct(account);
const me = useCurrentAccount();
const ownAccount = account?.id === me;
const { displayMentionAvatars } = useSettings();
const { data: scrobble } = useAccountScrobbleQuery(account?.id);
@ -222,6 +224,7 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
html={account.note}
emojis={account.emojis}
speakAsCat={account.speak_as_cat}
displayMentionAvatars={displayMentionAvatars}
/>
</Markup>
)}

View File

@ -1586,6 +1586,7 @@
"preferences.fields.display_media.default": "Hide posts marked as sensitive",
"preferences.fields.display_media.hide_all": "Always hide media posts",
"preferences.fields.display_media.show_all": "Always show posts",
"preferences.fields.display_mention_avatars": "Show avatars next to mentions",
"preferences.fields.implicit_addressing_label": "Include mentions in post content when replying",
"preferences.fields.interface_size": "Interface size",
"preferences.fields.known_languages_label": "Languages you know",

View File

@ -42,6 +42,10 @@ const messages = defineMessages({
treeIndentView: { id: 'status.thread.tree_indent_view', defaultMessage: 'Tree (indented)' },
linearView: { id: 'status.thread.linear_view', defaultMessage: 'Linear view' },
expandAll: { id: 'status.thread.expand_all', defaultMessage: 'Expand all posts' },
showAvatars: {
id: 'preferences.fields.display_mention_avatars',
defaultMessage: 'Show avatars next to mentions',
},
});
const StatusPage: React.FC = () => {
@ -60,6 +64,7 @@ const StatusPage: React.FC = () => {
const {
displaySpoilers,
threads: { displayMode },
displayMentionAvatars,
} = useSettings();
const handleRefresh = () => {
@ -98,6 +103,15 @@ const StatusPage: React.FC = () => {
},
];
menu.push(null, {
text: intl.formatMessage(messages.showAvatars),
onChange: (checked) => {
changeSetting(['displayMentionAvatars'], checked);
},
type: 'toggle',
checked: displayMentionAvatars,
});
if (!displaySpoilers && expandAllStatuses) {
menu.push(null, {
text: intl.formatMessage(messages.expandAll),
@ -106,7 +120,7 @@ const StatusPage: React.FC = () => {
});
}
return menu;
}, [displayMode, expandAllStatuses]);
}, [displayMode, expandAllStatuses, displayMentionAvatars]);
if (status?.event) {
return (

View File

@ -57,6 +57,7 @@ const settingsSchema = v.object({
rememberTimelinePosition: v.fallback(v.boolean(), true),
accountNicknames: v.fallback(v.record(v.string(), v.string()), {}),
useSystemMediaControls: v.fallback(v.boolean(), false),
displayMentionAvatars: v.fallback(v.boolean(), false),
theme: v.optional(
coerceObject({

View File

@ -245,3 +245,43 @@
padding: 1rem;
}
}
.-mention-with-avatar {
display: inline-flex;
gap: 0.25rem;
align-items: center;
padding: 0.125rem 0.5rem 0.125rem 0.25rem;
border-radius: 9999px;
vertical-align: middle;
background: rgb(var(--color-gray-200));
.dark & {
background: rgb(var(--color-primary-800));
}
.black & {
background: rgb(var(--color-gray-800));
}
&:hover {
text-decoration: underline;
}
.-avatar {
overflow: hidden;
border-radius: 9999px;
.text-lg & {
width: 1.5rem !important;
height: 1.5rem !important;
}
&--placeholder {
width: 1rem;
height: 1rem;
}
}
}