Show mute end date in badge (#37747)

This commit is contained in:
Echo 2026-02-05 21:01:28 +01:00 committed by GitHub
parent acdd0b33a3
commit 7a4945c0d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 84 additions and 24 deletions

View File

@ -8,8 +8,9 @@ export interface ApiRelationshipJSON {
following: boolean; following: boolean;
id: string; id: string;
languages: string[] | null; languages: string[] | null;
muting_notifications: boolean;
muting: boolean; muting: boolean;
muting_notifications: boolean;
muting_expires_at: string | null;
note: string; note: string;
notifying: boolean; notifying: boolean;
requested_by: boolean; requested_by: boolean;

View File

@ -8,7 +8,7 @@ const meta = {
component: badges.Badge, component: badges.Badge,
title: 'Components/Badge', title: 'Components/Badge',
args: { args: {
label: 'Example', label: undefined,
}, },
} satisfies Meta<typeof badges.Badge>; } satisfies Meta<typeof badges.Badge>;
@ -16,16 +16,22 @@ export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof meta>;
export const Default: Story = {}; export const Default: Story = {
args: {
label: 'Example',
},
};
export const Domain: Story = { export const Domain: Story = {
args: { args: {
...Default.args,
domain: 'example.com', domain: 'example.com',
}, },
}; };
export const CustomIcon: Story = { export const CustomIcon: Story = {
args: { args: {
...Default.args,
icon: <CelebrationIcon />, icon: <CelebrationIcon />,
}, },
}; };
@ -57,6 +63,13 @@ export const Muted: Story = {
}, },
}; };
export const MutedWithDate: Story = {
render(args) {
const futureDate = new Date(new Date().getFullYear(), 11, 31).toISOString();
return <badges.MutedBadge {...args} expiresAt={futureDate} />;
},
};
export const Blocked: Story = { export const Blocked: Story = {
render(args) { render(args) {
return <badges.BlockedBadge {...args} />; return <badges.BlockedBadge {...args} />;

View File

@ -1,6 +1,6 @@
import type { FC, ReactNode } from 'react'; import type { FC, ReactNode } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
@ -36,21 +36,25 @@ export const Badge: FC<BadgeProps> = ({
</div> </div>
); );
export const AdminBadge: FC<Partial<BadgeProps>> = (props) => ( export const AdminBadge: FC<Partial<BadgeProps>> = ({ label, ...props }) => (
<Badge <Badge
icon={<AdminIcon />} icon={<AdminIcon />}
label={ label={
<FormattedMessage id='account.badges.admin' defaultMessage='Admin' /> label ?? (
<FormattedMessage id='account.badges.admin' defaultMessage='Admin' />
)
} }
{...props} {...props}
/> />
); );
export const GroupBadge: FC<Partial<BadgeProps>> = (props) => ( export const GroupBadge: FC<Partial<BadgeProps>> = ({ label, ...props }) => (
<Badge <Badge
icon={<GroupsIcon />} icon={<GroupsIcon />}
label={ label={
<FormattedMessage id='account.badges.group' defaultMessage='Group' /> label ?? (
<FormattedMessage id='account.badges.group' defaultMessage='Group' />
)
} }
{...props} {...props}
/> />
@ -66,21 +70,54 @@ export const AutomatedBadge: FC<{ className?: string }> = ({ className }) => (
/> />
); );
export const MutedBadge: FC<Partial<BadgeProps>> = (props) => ( export const MutedBadge: FC<
<Badge Partial<BadgeProps> & { expiresAt?: string | null }
icon={<VolumeOffIcon />} > = ({ expiresAt, label, ...props }) => {
label={ // Format the date, only showing the year if it's different from the current year.
<FormattedMessage id='account.badges.muted' defaultMessage='Muted' /> const intl = useIntl();
} let formattedDate: string | null = null;
{...props} if (expiresAt) {
/> const expiresDate = new Date(expiresAt);
); const isCurrentYear =
expiresDate.getFullYear() === new Date().getFullYear();
formattedDate = intl.formatDate(expiresDate, {
month: 'short',
day: 'numeric',
...(isCurrentYear ? {} : { year: 'numeric' }),
});
}
return (
<Badge
icon={<VolumeOffIcon />}
label={
label ??
(formattedDate ? (
<FormattedMessage
id='account.badges.muted_until'
defaultMessage='Muted until {until}'
values={{
until: formattedDate,
}}
/>
) : (
<FormattedMessage id='account.badges.muted' defaultMessage='Muted' />
))
}
{...props}
/>
);
};
export const BlockedBadge: FC<Partial<BadgeProps>> = (props) => ( export const BlockedBadge: FC<Partial<BadgeProps>> = ({ label, ...props }) => (
<Badge <Badge
icon={<BlockIcon />} icon={<BlockIcon />}
label={ label={
<FormattedMessage id='account.badges.blocked' defaultMessage='Blocked' /> label ?? (
<FormattedMessage
id='account.badges.blocked'
defaultMessage='Blocked'
/>
)
} }
{...props} {...props}
/> />

View File

@ -106,6 +106,7 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
<MutedBadge <MutedBadge
key='muted-badge' key='muted-badge'
className={classNames(className, classes.badgeMuted)} className={classNames(className, classes.badgeMuted)}
expiresAt={relationship.muting_expires_at}
/>, />,
); );
} }

View File

@ -23,6 +23,7 @@
"account.badges.domain_blocked": "Blocked domain", "account.badges.domain_blocked": "Blocked domain",
"account.badges.group": "Group", "account.badges.group": "Group",
"account.badges.muted": "Muted", "account.badges.muted": "Muted",
"account.badges.muted_until": "Muted until {until}",
"account.block": "Block @{name}", "account.block": "Block @{name}",
"account.block_domain": "Block domain {domain}", "account.block_domain": "Block domain {domain}",
"account.block_short": "Block", "account.block_short": "Block",

View File

@ -15,8 +15,9 @@ const RelationshipFactory = Record<RelationshipShape>({
following: false, following: false,
id: '', id: '',
languages: null, languages: null,
muting_notifications: false,
muting: false, muting: false,
muting_notifications: false,
muting_expires_at: null,
note: '', note: '',
notifying: false, notifying: false,
requested_by: false, requested_by: false,

View File

@ -99,10 +99,11 @@ export const relationshipsFactory: FactoryFunction<ApiRelationshipJSON> = ({
blocking: false, blocking: false,
blocked_by: false, blocked_by: false,
languages: null, languages: null,
muting: false,
muting_notifications: false, muting_notifications: false,
muting_expires_at: null,
note: '', note: '',
requested_by: false, requested_by: false,
muting: false,
requested: false, requested: false,
domain_blocking: false, domain_blocking: false,
endorsed: false, endorsed: false,

View File

@ -39,6 +39,7 @@ module Account::Mappings
Mute.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |mute, mapping| Mute.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |mute, mapping|
mapping[mute.target_account_id] = { mapping[mute.target_account_id] = {
notifications: mute.hide_notifications?, notifications: mute.hide_notifications?,
expires_at: mute.expires_at,
} }
end end
end end

View File

@ -4,7 +4,7 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
# Please update `app/javascript/mastodon/api_types/relationships.ts` when making changes to the attributes # Please update `app/javascript/mastodon/api_types/relationships.ts` when making changes to the attributes
attributes :id, :following, :showing_reblogs, :notifying, :languages, :followed_by, attributes :id, :following, :showing_reblogs, :notifying, :languages, :followed_by,
:blocking, :blocked_by, :muting, :muting_notifications, :blocking, :blocked_by, :muting, :muting_notifications, :muting_expires_at,
:requested, :requested_by, :domain_blocking, :endorsed, :note :requested, :requested_by, :domain_blocking, :endorsed, :note
def id def id
@ -52,6 +52,10 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
(instance_options[:relationships].muting[object.id] || {})[:notifications] || false (instance_options[:relationships].muting[object.id] || {})[:notifications] || false
end end
def muting_expires_at
(instance_options[:relationships].muting[object.id] || {})[:expires_at]&.iso8601
end
def requested def requested
instance_options[:relationships].requested[object.id] ? true : false instance_options[:relationships].requested[object.id] ? true : false
end end

View File

@ -93,13 +93,13 @@ RSpec.describe Account::Mappings do
context 'when Mute#hide_notifications?' do context 'when Mute#hide_notifications?' do
let(:hide) { true } let(:hide) { true }
it { is_expected.to eq(target_account_id => { notifications: true }) } it { is_expected.to eq(target_account_id => { expires_at: nil, notifications: true }) }
end end
context 'when not Mute#hide_notifications?' do context 'when not Mute#hide_notifications?' do
let(:hide) { false } let(:hide) { false }
it { is_expected.to eq(target_account_id => { notifications: false }) } it { is_expected.to eq(target_account_id => { expires_at: nil, notifications: false }) }
end end
end end