nicolium: Allow assigning nicknames to users

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-03-14 11:55:47 +01:00
parent 5921a773c9
commit 5415270232
10 changed files with 99 additions and 45 deletions

View File

@ -4,7 +4,7 @@ import { defineMessages, FormattedDate, useIntl } from 'react-intl';
import IconButton from '@/components/ui/icon-button';
import { DatePicker } from '@/features/ui/util/async-components';
import { useFeatures } from '@/hooks/use-features';
import { useInstance } from '@/stores/instance';
import { useInstance } from '@/hooks/use-instance';
const messages = defineMessages({
birthdayPlaceholder: {

View File

@ -415,39 +415,21 @@ const Status: React.FC<IStatus> = React.memo((props) => {
/>
);
} else if (isReblog) {
const accounts = status.accounts ?? [status.account];
const renderedAccounts = accounts.slice(0, 2).map(
(account) =>
!!account && (
<Link
key={account.acct}
to='/@{$username}'
params={{ username: account.acct }}
className='hover:underline'
>
<bdi className='truncate'>
<strong className='text-gray-800 dark:text-gray-200'>
<Emojify text={account.display_name} emojis={account.emojis} />
</strong>
</bdi>
</Link>
),
);
if (accounts.length > 2) {
renderedAccounts.push(
<FormattedMessage
id='notification.more'
defaultMessage='{count, plural, one {# other} other {# others}}'
values={{ count: accounts.length - renderedAccounts.length }}
/>,
);
}
const values = {
name: <FormattedList type='conjunction' value={renderedAccounts} />,
count: accounts.length,
name: (
<Link
key={status.account.acct}
to='/@{$username}'
params={{ username: status.account.acct }}
className='hover:underline'
>
<bdi className='truncate'>
<strong className='text-gray-800 dark:text-gray-200'>
<Emojify text={status.account.display_name} emojis={status.account.emojis} />
</strong>
</bdi>
</Link>
),
};
return (
@ -535,7 +517,7 @@ const Status: React.FC<IStatus> = React.memo((props) => {
)
);
}
}, [status.accounts, group?.id]);
}, [status.account, group?.id]);
if (!status) return null;

View File

@ -4,8 +4,10 @@ import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { initReport, ReportableEntities } from '@/actions/reports';
import { changeSetting } from '@/actions/settings';
import DropdownMenu, { type Menu } from '@/components/dropdown-menu';
import IconButton from '@/components/ui/icon-button';
import { useAppDispatch } from '@/hooks/use-app-dispatch';
import { useClient } from '@/hooks/use-client';
import { useFeatures } from '@/hooks/use-features';
import { useOwnAccount } from '@/hooks/use-own-account';
@ -91,6 +93,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' },
@ -105,6 +111,7 @@ interface IAccountMenu {
const AccountMenu: React.FC<IAccountMenu> = ({ account }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { mentionCompose, directCompose } = useComposeActions();
const client = useClient();
@ -212,6 +219,28 @@ const AccountMenu: React.FC<IAccountMenu> = ({ account }) => {
});
};
const onEditNickname = () => {
const currentNickname = settings.accountNicknames[account.id] ?? '';
openModal('TEXT_FIELD', {
heading: (
<FormattedMessage
id='account.nickname.modal_header'
defaultMessage='Set nickname for @{name}'
values={{ name: account.acct }}
/>
),
placeholder: intl.formatMessage(messages.nicknamePlaceholder),
confirm: <FormattedMessage id='account.nickname.save' defaultMessage='Save nickname' />,
onConfirm: (value) => {
const trimmed = value.trim();
dispatch(changeSetting(['accountNicknames', account.id], trimmed || undefined));
toast.success(messages.nicknameSaved);
},
text: currentNickname,
singleLine: true,
});
};
const onReport = () => {
initReport(ReportableEntities.ACCOUNT, account);
};
@ -461,6 +490,12 @@ const AccountMenu: React.FC<IAccountMenu> = ({ 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) {

View File

@ -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<IProfileInfoPanel> = ({ account, username }) =>
)}
</Text>
{account.original_display_name &&
account.original_display_name !== account.display_name && (
<Text
theme='muted'
truncate
title={intl.formatMessage(messages.originalDisplayName)}
>
{'('}
<Emojify text={account.original_display_name} emojis={account.emojis} />
{')'}
</Text>
)}
{account.bot && <Badge slug='bot' title={intl.formatMessage(messages.bot)} />}
{badges.length > 0 && <div className='flex items-center gap-1'>{badges}</div>}

View File

@ -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<IUserPanel> = ({ accountId, action, badges, domain })
<Emojify text={account.display_name} emojis={account.emojis} />
</Text>
{account.original_display_name &&
account.original_display_name !== account.display_name && (
<Text
theme='muted'
truncate
title={intl.formatMessage(messages.originalDisplayName)}
>
{'('}
<Emojify text={account.original_display_name} emojis={account.emojis} />
{')'}
</Text>
)}
{verified && <VerificationBadge />}
{badges && badges.length > 0 && (

View File

@ -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",

View File

@ -69,7 +69,6 @@ const FrontendConfigEditor: React.FC = () => {
const features = useFeatures();
const initialData = useAppSelector((state) => state.frontendConfig);
console.log(initialData);
const { mutate: updateConfig, isPending } = useUpdateAdminConfig();
const [data, setData] = useState(v.parse(frontendConfigSchema, initialData));

View File

@ -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,

View File

@ -66,7 +66,6 @@ const useStatusQuery = (statusId?: string) => {
data: {
...statusQuery.data,
account: account.data!,
accounts: [account.data!],
},
};
}, [statusQuery.data, account.data]) as unknown as UseQueryResult<NormalizedStatus>;

View File

@ -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({