diff --git a/packages/nicolium/src/components/accounts/mention-with-avatar.tsx b/packages/nicolium/src/components/accounts/mention-with-avatar.tsx new file mode 100644 index 000000000..73d3f734e --- /dev/null +++ b/packages/nicolium/src/components/accounts/mention-with-avatar.tsx @@ -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 = ({ id, username }) => { + const { data: account } = useAccount(id); + + return ( + + + @{username} + + ); +}; + +export { MentionWithAvatar }; diff --git a/packages/nicolium/src/components/dropdown-menu/dropdown-menu-item.tsx b/packages/nicolium/src/components/dropdown-menu/dropdown-menu-item.tsx index 6f0a326b0..55d4fb4be 100644 --- a/packages/nicolium/src/components/dropdown-menu/dropdown-menu-item.tsx +++ b/packages/nicolium/src/components/dropdown-menu/dropdown-menu-item.tsx @@ -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); } diff --git a/packages/nicolium/src/components/statuses/parsed-content.tsx b/packages/nicolium/src/components/statuses/parsed-content.tsx index 07443fc73..e55cf0b77 100644 --- a/packages/nicolium/src/components/statuses/parsed-content.tsx +++ b/packages/nicolium/src/components/statuses/parsed-content.tsx @@ -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(); }} > - - @{mention.username} - + {displayMentionAvatars ? ( + + ) : ( + + @{mention.username} + + )} ); } @@ -371,13 +385,14 @@ function parseContent( const ParsedContent: React.FC = 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); }, diff --git a/packages/nicolium/src/components/statuses/status-content.tsx b/packages/nicolium/src/components/statuses/status-content.tsx index 68053b334..5f86c6ca7 100644 --- a/packages/nicolium/src/components/statuses/status-content.tsx +++ b/packages/nicolium/src/components/statuses/status-content.tsx @@ -77,7 +77,7 @@ const StatusContent: React.FC = 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 = 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 diff --git a/packages/nicolium/src/components/statuses/status-mention.tsx b/packages/nicolium/src/components/statuses/status-mention.tsx index d0a2b597d..85c6b2cbf 100644 --- a/packages/nicolium/src/components/statuses/status-mention.tsx +++ b/packages/nicolium/src/components/statuses/status-mention.tsx @@ -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 = ({ accountId, fallback }) => { const { data: account } = useAccount(accountId); + const { displayMentionAvatars } = useSettings(); + if (!account) return ( @@ -29,9 +33,13 @@ const StatusMention: React.FC = ({ accountId, fallback }) => { e.stopPropagation(); }} > - - @{account.acct} - + {displayMentionAvatars ? ( + + ) : ( + + @{account.acct} + + )} ); }; diff --git a/packages/nicolium/src/features/preferences/index.tsx b/packages/nicolium/src/features/preferences/index.tsx index 820b22850..5648987b3 100644 --- a/packages/nicolium/src/features/preferences/index.tsx +++ b/packages/nicolium/src/features/preferences/index.tsx @@ -724,6 +724,21 @@ const Preferences = () => { /> + + } + > + + + = ({ 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 = ({ account, username }) => html={account.note} emojis={account.emojis} speakAsCat={account.speak_as_cat} + displayMentionAvatars={displayMentionAvatars} /> )} diff --git a/packages/nicolium/src/locales/en.json b/packages/nicolium/src/locales/en.json index 0f67c3a61..d2564230b 100644 --- a/packages/nicolium/src/locales/en.json +++ b/packages/nicolium/src/locales/en.json @@ -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", diff --git a/packages/nicolium/src/pages/statuses/status.tsx b/packages/nicolium/src/pages/statuses/status.tsx index a24925b39..c81de9928 100644 --- a/packages/nicolium/src/pages/statuses/status.tsx +++ b/packages/nicolium/src/pages/statuses/status.tsx @@ -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 ( diff --git a/packages/nicolium/src/schemas/frontend-settings.ts b/packages/nicolium/src/schemas/frontend-settings.ts index b657ea490..d610b36b6 100644 --- a/packages/nicolium/src/schemas/frontend-settings.ts +++ b/packages/nicolium/src/schemas/frontend-settings.ts @@ -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({ diff --git a/packages/nicolium/src/styles/new/accounts.scss b/packages/nicolium/src/styles/new/accounts.scss index 47c82aedd..c365b1018 100644 --- a/packages/nicolium/src/styles/new/accounts.scss +++ b/packages/nicolium/src/styles/new/accounts.scss @@ -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; + } + } +}