Fix logged-out quote menu UX, simplify Interaction dialog copy (#36124)

This commit is contained in:
diondiondion 2025-09-15 17:38:11 +02:00 committed by GitHub
parent 943cdc5b21
commit 38fa0102c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 51 additions and 142 deletions

View File

@ -46,7 +46,6 @@ export const FollowButton: React.FC<{
openModal({ openModal({
modalType: 'INTERACTION', modalType: 'INTERACTION',
modalProps: { modalProps: {
type: 'follow',
accountId: accountId, accountId: accountId,
url: account?.url, url: account?.url,
}, },

View File

@ -109,7 +109,6 @@ export const Poll: React.FC<PollProps> = ({ pollId, disabled, status }) => {
openModal({ openModal({
modalType: 'INTERACTION', modalType: 'INTERACTION',
modalProps: { modalProps: {
type: 'vote',
accountId: status.getIn(['account', 'id']), accountId: status.getIn(['account', 'id']),
url: status.get('uri'), url: status.get('uri'),
}, },

View File

@ -72,6 +72,18 @@ export const StatusBoostButton: FC<ReblogButtonProps> = ({
const statusId = status.get('id') as string; const statusId = status.get('id') as string;
const wasBoosted = !!status.get('reblogged'); const wasBoosted = !!status.get('reblogged');
const showLoginPrompt = useCallback(() => {
dispatch(
openModal({
modalType: 'INTERACTION',
modalProps: {
accountId: status.getIn(['account', 'id']),
url: status.get('uri'),
},
}),
);
}, [dispatch, status]);
const items = useMemo(() => { const items = useMemo(() => {
const boostItem = boostItemState(statusState); const boostItem = boostItemState(statusState);
const quoteItem = quoteItemState(statusState); const quoteItem = quoteItemState(statusState);
@ -87,6 +99,8 @@ export const StatusBoostButton: FC<ReblogButtonProps> = ({
action: (event) => { action: (event) => {
if (isLoggedIn) { if (isLoggedIn) {
dispatch(toggleReblog(statusId, event.shiftKey)); dispatch(toggleReblog(statusId, event.shiftKey));
} else {
showLoginPrompt();
} }
}, },
}, },
@ -100,34 +114,37 @@ export const StatusBoostButton: FC<ReblogButtonProps> = ({
action: () => { action: () => {
if (isLoggedIn) { if (isLoggedIn) {
dispatch(quoteComposeById(statusId)); dispatch(quoteComposeById(statusId));
} else {
showLoginPrompt();
} }
}, },
}, },
] satisfies [ActionMenuItemWithIcon, ActionMenuItemWithIcon]; ] satisfies [ActionMenuItemWithIcon, ActionMenuItemWithIcon];
}, [dispatch, intl, isLoggedIn, statusId, statusState, wasBoosted]); }, [
dispatch,
intl,
isLoggedIn,
showLoginPrompt,
statusId,
statusState,
wasBoosted,
]);
const boostIcon = items[0].icon; const boostIcon = items[0].icon;
const handleDropdownOpen = useCallback( const handleDropdownOpen = useCallback(
(event: MouseEvent | KeyboardEvent) => { (event: MouseEvent | KeyboardEvent) => {
if (event.shiftKey) {
if (!isLoggedIn) { if (!isLoggedIn) {
dispatch( showLoginPrompt();
openModal({ return false;
modalType: 'INTERACTION', }
modalProps: {
type: 'reblog',
accountId: status.getIn(['account', 'id']),
url: status.get('uri'),
},
}),
);
} else if (event.shiftKey) {
dispatch(toggleReblog(status.get('id'), true)); dispatch(toggleReblog(status.get('id'), true));
return false; return false;
} }
return true; return true;
}, },
[dispatch, isLoggedIn, status], [dispatch, isLoggedIn, showLoginPrompt, status],
); );
return ( return (
@ -223,7 +240,6 @@ export const LegacyReblogButton: FC<ReblogButtonProps> = ({
openModal({ openModal({
modalType: 'INTERACTION', modalType: 'INTERACTION',
modalProps: { modalProps: {
type: 'reblog',
accountId: status.getIn(['account', 'id']), accountId: status.getIn(['account', 'id']),
url: status.get('uri'), url: status.get('uri'),
}, },

View File

@ -129,6 +129,7 @@ export function boostItemState({
} }
export function quoteItemState({ export function quoteItemState({
isLoggedIn,
isMine, isMine,
isQuoteAutomaticallyAccepted, isQuoteAutomaticallyAccepted,
isQuoteManuallyAccepted, isQuoteManuallyAccepted,
@ -149,7 +150,8 @@ export function quoteItemState({
} else if (isQuoteManuallyAccepted) { } else if (isQuoteManuallyAccepted) {
iconText.title = messages.request_quote; iconText.title = messages.request_quote;
iconText.meta = messages.quote_manual_review; iconText.meta = messages.quote_manual_review;
} else { // We don't show the disabled state when logged out
} else if (isLoggedIn) {
iconText.disabled = true; iconText.disabled = true;
iconText.iconComponent = FormatQuoteOff; iconText.iconComponent = FormatQuoteOff;
iconText.meta = isQuoteFollowersOnly iconText.meta = isQuoteFollowersOnly

View File

@ -122,7 +122,7 @@ class StatusActionBar extends ImmutablePureComponent {
if (signedIn) { if (signedIn) {
this.props.onReply(this.props.status); this.props.onReply(this.props.status);
} else { } else {
this.props.onInteractionModal('reply', this.props.status); this.props.onInteractionModal(this.props.status);
} }
}; };
@ -140,7 +140,7 @@ class StatusActionBar extends ImmutablePureComponent {
if (signedIn) { if (signedIn) {
this.props.onFavourite(this.props.status); this.props.onFavourite(this.props.status);
} else { } else {
this.props.onInteractionModal('favourite', this.props.status); this.props.onInteractionModal(this.props.status);
} }
}; };

View File

@ -235,11 +235,10 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
dispatch(deployPictureInPicture({statusId: status.get('id'), accountId: status.getIn(['account', 'id']), playerType: type, props: mediaProps})); dispatch(deployPictureInPicture({statusId: status.get('id'), accountId: status.getIn(['account', 'id']), playerType: type, props: mediaProps}));
}, },
onInteractionModal (type, status) { onInteractionModal (status) {
dispatch(openModal({ dispatch(openModal({
modalType: 'INTERACTION', modalType: 'INTERACTION',
modalProps: { modalProps: {
type,
accountId: status.getIn(['account', 'id']), accountId: status.getIn(['account', 'id']),
url: status.get('uri'), url: status.get('uri'),
}, },

View File

@ -7,15 +7,9 @@ import classNames from 'classnames';
import { escapeRegExp } from 'lodash'; import { escapeRegExp } from 'lodash';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react';
import { openModal, closeModal } from 'mastodon/actions/modal'; import { openModal, closeModal } from 'mastodon/actions/modal';
import { apiRequest } from 'mastodon/api'; import { apiRequest } from 'mastodon/api';
import { Button } from 'mastodon/components/button'; import { Button } from 'mastodon/components/button';
import { Icon } from 'mastodon/components/icon';
import { import {
domain as localDomain, domain as localDomain,
registrationsOpen, registrationsOpen,
@ -408,8 +402,7 @@ const LoginForm: React.FC<{
const InteractionModal: React.FC<{ const InteractionModal: React.FC<{
accountId: string; accountId: string;
url: string; url: string;
type: 'reply' | 'reblog' | 'favourite' | 'follow' | 'vote'; }> = ({ accountId, url }) => {
}> = ({ accountId, url, type }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const displayNameHtml = useAppSelector( const displayNameHtml = useAppSelector(
(state) => state.accounts.get(accountId)?.display_name_html ?? '', (state) => state.accounts.get(accountId)?.display_name_html ?? '',
@ -437,93 +430,6 @@ const InteractionModal: React.FC<{
); );
}, [dispatch]); }, [dispatch]);
let title: React.ReactNode,
icon: React.ReactNode,
actionPrompt: React.ReactNode;
switch (type) {
case 'reply':
icon = <Icon id='reply' icon={ReplyIcon} />;
title = (
<FormattedMessage
id='interaction_modal.title.reply'
defaultMessage="Reply to {name}'s post"
values={{ name }}
/>
);
actionPrompt = (
<FormattedMessage
id='interaction_modal.action.reply'
defaultMessage='To continue, you need to reply from your account.'
/>
);
break;
case 'reblog':
icon = <Icon id='retweet' icon={RepeatIcon} />;
title = (
<FormattedMessage
id='interaction_modal.title.reblog'
defaultMessage="Boost {name}'s post"
values={{ name }}
/>
);
actionPrompt = (
<FormattedMessage
id='interaction_modal.action.reblog'
defaultMessage='To continue, you need to reblog from your account.'
/>
);
break;
case 'favourite':
icon = <Icon id='star' icon={StarIcon} />;
title = (
<FormattedMessage
id='interaction_modal.title.favourite'
defaultMessage="Favorite {name}'s post"
values={{ name }}
/>
);
actionPrompt = (
<FormattedMessage
id='interaction_modal.action.favourite'
defaultMessage='To continue, you need to favorite from your account.'
/>
);
break;
case 'follow':
icon = <Icon id='user-plus' icon={PersonAddIcon} />;
title = (
<FormattedMessage
id='interaction_modal.title.follow'
defaultMessage='Follow {name}'
values={{ name }}
/>
);
actionPrompt = (
<FormattedMessage
id='interaction_modal.action.follow'
defaultMessage='To continue, you need to follow from your account.'
/>
);
break;
case 'vote':
icon = <Icon id='tasks' icon={InsertChartIcon} />;
title = (
<FormattedMessage
id='interaction_modal.title.vote'
defaultMessage="Vote in {name}'s poll"
values={{ name }}
/>
);
actionPrompt = (
<FormattedMessage
id='interaction_modal.action.vote'
defaultMessage='To continue, you need to vote from your account.'
/>
);
break;
}
let signupButton; let signupButton;
if (sso_redirect) { if (sso_redirect) {
@ -559,9 +465,18 @@ const InteractionModal: React.FC<{
<div className='modal-root__modal interaction-modal'> <div className='modal-root__modal interaction-modal'>
<div className='interaction-modal__lead'> <div className='interaction-modal__lead'>
<h3> <h3>
<span className='interaction-modal__icon'>{icon}</span> {title} <FormattedMessage
id='interaction_modal.title'
defaultMessage='Sign in to continue'
/>
</h3> </h3>
<p>{actionPrompt}</p> <p>
<FormattedMessage
id='interaction_modal.action'
defaultMessage="To interact with {name}'s post, you need to sign into your account on whatever Mastodon server you use."
values={{ name }}
/>
</p>
</div> </div>
<LoginForm resourceUrl={url} /> <LoginForm resourceUrl={url} />

View File

@ -92,7 +92,6 @@ export const Footer: React.FC<{
openModal({ openModal({
modalType: 'INTERACTION', modalType: 'INTERACTION',
modalProps: { modalProps: {
type: 'reply',
accountId: status.getIn(['account', 'id']), accountId: status.getIn(['account', 'id']),
url: status.get('uri'), url: status.get('uri'),
}, },
@ -113,7 +112,6 @@ export const Footer: React.FC<{
openModal({ openModal({
modalType: 'INTERACTION', modalType: 'INTERACTION',
modalProps: { modalProps: {
type: 'favourite',
accountId: status.getIn(['account', 'id']), accountId: status.getIn(['account', 'id']),
url: status.get('uri'), url: status.get('uri'),
}, },
@ -135,7 +133,6 @@ export const Footer: React.FC<{
openModal({ openModal({
modalType: 'INTERACTION', modalType: 'INTERACTION',
modalProps: { modalProps: {
type: 'reblog',
accountId: status.getIn(['account', 'id']), accountId: status.getIn(['account', 'id']),
url: status.get('uri'), url: status.get('uri'),
}, },

View File

@ -186,7 +186,6 @@ class Status extends ImmutablePureComponent {
dispatch(openModal({ dispatch(openModal({
modalType: 'INTERACTION', modalType: 'INTERACTION',
modalProps: { modalProps: {
type: 'favourite',
accountId: status.getIn(['account', 'id']), accountId: status.getIn(['account', 'id']),
url: status.get('uri'), url: status.get('uri'),
}, },
@ -216,7 +215,6 @@ class Status extends ImmutablePureComponent {
dispatch(openModal({ dispatch(openModal({
modalType: 'INTERACTION', modalType: 'INTERACTION',
modalProps: { modalProps: {
type: 'reply',
accountId: status.getIn(['account', 'id']), accountId: status.getIn(['account', 'id']),
url: status.get('uri'), url: status.get('uri'),
}, },
@ -234,7 +232,6 @@ class Status extends ImmutablePureComponent {
dispatch(openModal({ dispatch(openModal({
modalType: 'INTERACTION', modalType: 'INTERACTION',
modalProps: { modalProps: {
type: 'reblog',
accountId: status.getIn(['account', 'id']), accountId: status.getIn(['account', 'id']),
url: status.get('uri'), url: status.get('uri'),
}, },

View File

@ -453,20 +453,12 @@
"ignore_notifications_modal.private_mentions_title": "Ignore notifications from unsolicited Private Mentions?", "ignore_notifications_modal.private_mentions_title": "Ignore notifications from unsolicited Private Mentions?",
"info_button.label": "Help", "info_button.label": "Help",
"info_button.what_is_alt_text": "<h1>What is alt text?</h1> <p>Alt text provides image descriptions for people with vision impairments, low-bandwidth connections, or those seeking extra context.</p> <p>You can improve accessibility and understanding for everyone by writing clear, concise, and objective alt text.</p> <ul> <li>Capture important elements</li> <li>Summarize text in images</li> <li>Use regular sentence structure</li> <li>Avoid redundant information</li> <li>Focus on trends and key findings in complex visuals (like diagrams or maps)</li> </ul>", "info_button.what_is_alt_text": "<h1>What is alt text?</h1> <p>Alt text provides image descriptions for people with vision impairments, low-bandwidth connections, or those seeking extra context.</p> <p>You can improve accessibility and understanding for everyone by writing clear, concise, and objective alt text.</p> <ul> <li>Capture important elements</li> <li>Summarize text in images</li> <li>Use regular sentence structure</li> <li>Avoid redundant information</li> <li>Focus on trends and key findings in complex visuals (like diagrams or maps)</li> </ul>",
"interaction_modal.action.favourite": "To continue, you need to favorite from your account.", "interaction_modal.action": "To interact with {name}'s post, you need to sign into your account on whatever Mastodon server you use.",
"interaction_modal.action.follow": "To continue, you need to follow from your account.",
"interaction_modal.action.reblog": "To continue, you need to reblog from your account.",
"interaction_modal.action.reply": "To continue, you need to reply from your account.",
"interaction_modal.action.vote": "To continue, you need to vote from your account.",
"interaction_modal.go": "Go", "interaction_modal.go": "Go",
"interaction_modal.no_account_yet": "Don't have an account yet?", "interaction_modal.no_account_yet": "Don't have an account yet?",
"interaction_modal.on_another_server": "On a different server", "interaction_modal.on_another_server": "On a different server",
"interaction_modal.on_this_server": "On this server", "interaction_modal.on_this_server": "On this server",
"interaction_modal.title.favourite": "Favorite {name}'s post", "interaction_modal.title": "Sign in to continue",
"interaction_modal.title.follow": "Follow {name}",
"interaction_modal.title.reblog": "Boost {name}'s post",
"interaction_modal.title.reply": "Reply to {name}'s post",
"interaction_modal.title.vote": "Vote in {name}'s poll",
"interaction_modal.username_prompt": "E.g. {example}", "interaction_modal.username_prompt": "E.g. {example}",
"intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",

View File

@ -9533,13 +9533,6 @@ noscript {
font-size: 14px; font-size: 14px;
} }
&__icon {
color: $highlight-text-color;
display: flex;
align-items: center;
justify-content: center;
}
&__lead { &__lead {
margin-bottom: 20px; margin-bottom: 20px;