diff --git a/packages/nicolium/src/features/account/components/account-menu.tsx b/packages/nicolium/src/features/account/components/account-menu.tsx index e54b8341b..19de6fc03 100644 --- a/packages/nicolium/src/features/account/components/account-menu.tsx +++ b/packages/nicolium/src/features/account/components/account-menu.tsx @@ -2,6 +2,7 @@ import { GOTOSOCIAL, MASTODON } from 'pl-api'; import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { changeSetting } from '@/actions/settings'; import DropdownMenu, { type Menu } from '@/components/dropdown-menu'; import IconButton from '@/components/ui/icon-button'; import { useClient } from '@/hooks/use-client'; @@ -86,6 +87,10 @@ const messages = defineMessages({ id: 'account.load_activities.fail', defaultMessage: 'Failed to fetch latest posts', }, + nickname: { id: 'account.nickname.modal_header', defaultMessage: 'Set nickname for @{name}' }, + nicknamePlaceholder: { id: 'account.nickname.placeholder', defaultMessage: 'Enter a nickname' }, + nicknameSave: { id: 'account.nickname.save', defaultMessage: 'Save nickname' }, + nicknameSaved: { id: 'account.nickname.success', defaultMessage: 'Nickname saved' }, note: { id: 'account_note.modal_header', defaultMessage: 'Edit note for @{name}' }, notePlaceholder: { id: 'account_note.placeholder', defaultMessage: 'Add a note' }, noteSaved: { id: 'account_note.success', defaultMessage: 'Note saved' }, @@ -206,6 +211,28 @@ const AccountMenu: React.FC = ({ account }) => { }); }; + const onEditNickname = () => { + const currentNickname = settings.accountNicknames[account.id] ?? ''; + openModal('TEXT_FIELD', { + heading: ( + + ), + placeholder: intl.formatMessage(messages.nicknamePlaceholder), + confirm: , + onConfirm: (value) => { + const trimmed = value.trim(); + changeSetting(['accountNicknames', account.id], trimmed || undefined); + toast.success(messages.nicknameSaved); + }, + text: currentNickname, + singleLine: true, + }); + }; + const onReport = () => { openModal('REPORT', { accountId: account.id }); }; @@ -455,6 +482,12 @@ const AccountMenu: React.FC = ({ account }) => { }); } + menu.push({ + text: intl.formatMessage(messages.nickname, { name: account.acct }), + action: onEditNickname, + icon: require('@phosphor-icons/core/regular/tag.svg'), + }); + menu.push(null); if (features.removeFromFollowers && account.relationship?.followed_by) { diff --git a/packages/nicolium/src/features/ui/components/panels/profile-info-panel.tsx b/packages/nicolium/src/features/ui/components/panels/profile-info-panel.tsx index 27f88f9c8..a24f18af8 100644 --- a/packages/nicolium/src/features/ui/components/panels/profile-info-panel.tsx +++ b/packages/nicolium/src/features/ui/components/panels/profile-info-panel.tsx @@ -33,10 +33,14 @@ const messages = defineMessages({ deactivated: { id: 'account.deactivated', defaultMessage: 'Deactivated' }, bot: { id: 'account.badges.bot', defaultMessage: 'Bot' }, pronouns: { id: 'account.pronouns.with_label', defaultMessage: 'Pronouns: {pronouns}' }, + originalDisplayName: { + id: 'account.original_display_name', + defaultMessage: 'You have assigned a nickname to this user.', + }, }); interface IProfileInfoPanel { - account?: Account; + account?: Account & { original_display_name?: string }; /** Username from URL params, in case the account isn't found. */ username: string; } @@ -177,6 +181,19 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => )} + {account.original_display_name && + account.original_display_name !== account.display_name && ( + + {'('} + + {')'} + + )} + {account.bot && } {badges.length > 0 &&
{badges}
} diff --git a/packages/nicolium/src/features/ui/components/panels/user-panel.tsx b/packages/nicolium/src/features/ui/components/panels/user-panel.tsx index 866491346..72a71924f 100644 --- a/packages/nicolium/src/features/ui/components/panels/user-panel.tsx +++ b/packages/nicolium/src/features/ui/components/panels/user-panel.tsx @@ -20,6 +20,10 @@ const messages = defineMessages({ defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.', }, + originalDisplayName: { + id: 'account.original_display_name', + defaultMessage: 'You have assigned a nickname to this user.', + }, }); interface IUserPanel { @@ -83,6 +87,19 @@ const UserPanel: React.FC = ({ accountId, action, badges, domain }) + {account.original_display_name && + account.original_display_name !== account.display_name && ( + + {'('} + + {')'} + + )} + {verified && } {badges && badges.length > 0 && ( diff --git a/packages/nicolium/src/features/ui/router/index.tsx b/packages/nicolium/src/features/ui/router/index.tsx index 0e6a4e110..488779b04 100644 --- a/packages/nicolium/src/features/ui/router/index.tsx +++ b/packages/nicolium/src/features/ui/router/index.tsx @@ -146,6 +146,9 @@ const layouts = { getParentRoute: () => rootRoute, path: '/@{$username}', component: ProfileLayout, + validateSearch: v.object({ + with_replies: v.optional(v.boolean()), + }), }), remoteInstance: createRoute({ getParentRoute: () => rootRoute, diff --git a/packages/nicolium/src/layouts/profile-layout.tsx b/packages/nicolium/src/layouts/profile-layout.tsx index 84c451aed..414911146 100644 --- a/packages/nicolium/src/layouts/profile-layout.tsx +++ b/packages/nicolium/src/layouts/profile-layout.tsx @@ -26,6 +26,7 @@ import { LOCAL_STORAGE_REDIRECT_KEY } from '@/utils/redirect'; /** Layout to display a user's profile. */ const ProfileLayout: React.FC = () => { const { username } = layouts.profile.useParams(); + const { with_replies: withReplies } = layouts.profile.useSearch(); const location = useLocation(); const { data: account, isUnauthorized } = useAccountLookup(username, true); @@ -80,7 +81,7 @@ const ProfileLayout: React.FC = () => { let activeItem; const pathname = location.pathname.replace(`@${username}/`, ''); - if (pathname.endsWith('/with_replies')) { + if (withReplies) { activeItem = 'replies'; } else if (pathname.endsWith('/media')) { activeItem = 'media'; diff --git a/packages/nicolium/src/locales/en.json b/packages/nicolium/src/locales/en.json index b7119434b..d068e0815 100644 --- a/packages/nicolium/src/locales/en.json +++ b/packages/nicolium/src/locales/en.json @@ -53,6 +53,11 @@ "account.mute": "Mute @{name}", "account.muted": "Muted", "account.never_active": "Never", + "account.nickname.modal_header": "Set nickname for @{name}", + "account.nickname.placeholder": "Enter a nickname", + "account.nickname.save": "Save nickname", + "account.nickname.success": "Nickname saved", + "account.original_display_name": "You have assigned a nickname to this user.", "account.posts": "Posts", "account.posts_with_replies": "Posts & replies", "account.profile": "Profile", diff --git a/packages/nicolium/src/queries/accounts/use-account.ts b/packages/nicolium/src/queries/accounts/use-account.ts index 89b19f09a..0a3ad1f2d 100644 --- a/packages/nicolium/src/queries/accounts/use-account.ts +++ b/packages/nicolium/src/queries/accounts/use-account.ts @@ -7,6 +7,7 @@ import { useLoggedIn } from '@/hooks/use-logged-in'; import { useCredentialAccount } from '@/queries/accounts/use-account-credentials'; import { useRelationshipQuery } from '@/queries/accounts/use-relationship'; import { queryKeys } from '@/queries/keys'; +import { useSettings } from '@/stores/settings'; import type { NicoliumResponse } from '@/api'; @@ -30,6 +31,9 @@ const useAccount = (accountId?: string, withRelationship = false) => { const features = useFeatures(); const { me } = useLoggedIn(); const queryClient = useQueryClient(); + const { accountNicknames } = useSettings(); + + const nickname = accountNicknames[accountId ?? '']; const accountQuery = useQuery({ queryKey: queryKeys.accounts.show(accountId!), @@ -63,19 +67,14 @@ const useAccount = (accountId?: string, withRelationship = false) => { const mergedRelationship = relationship ?? accountQuery.data.relationship; const mergedIsAdmin = credentialIsAdmin ?? accountQuery.data.is_admin; - if ( - mergedRelationship === accountQuery.data.relationship && - mergedIsAdmin === accountQuery.data.is_admin - ) { - return accountQuery.data; - } - return { ...accountQuery.data, + display_name: nickname ?? accountQuery.data.display_name, + original_display_name: accountQuery.data.display_name, relationship: mergedRelationship, is_admin: mergedIsAdmin, }; - }, [accountQuery.data, relationship, credentialIsAdmin]); + }, [accountQuery.data, relationship, credentialIsAdmin, nickname]); return { ...accountQuery, diff --git a/packages/nicolium/src/schemas/frontend-settings.ts b/packages/nicolium/src/schemas/frontend-settings.ts index 6e52a4cf6..143cf6a86 100644 --- a/packages/nicolium/src/schemas/frontend-settings.ts +++ b/packages/nicolium/src/schemas/frontend-settings.ts @@ -55,6 +55,7 @@ const settingsSchema = v.object({ storeSettingsInNotes: v.fallback(v.boolean(), false), composeInTimelines: v.fallback(v.boolean(), true), rememberTimelinePosition: v.fallback(v.boolean(), true), + accountNicknames: v.fallback(v.record(v.string(), v.string()), {}), theme: v.optional( coerceObject({