Add first-time user education hint about quote removal on Quote notifications (#35986)
Some checks failed
Check i18n / check-i18n (push) Waiting to run
Chromatic / Run Chromatic (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Crowdin / Upload translations / upload-translations (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.2) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
CSS Linting / lint (push) Has been cancelled

This commit is contained in:
diondiondion 2025-09-04 15:01:12 +02:00 committed by GitHub
parent 42be0ca0eb
commit e7c30cd072
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 179 additions and 45 deletions

View File

@ -13,9 +13,9 @@ import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
const offset = [0, 4] as OffsetValue;
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
export const AltTextBadge: React.FC<{
description: string;
}> = ({ description }) => {
export const AltTextBadge: React.FC<{ description: string }> = ({
description,
}) => {
const accessibilityId = useId();
const anchorRef = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
@ -56,7 +56,7 @@ export const AltTextBadge: React.FC<{
{({ props }) => (
<div {...props} className='hover-card-controller'>
<div // eslint-disable-line jsx-a11y/no-noninteractive-element-interactions
className='media-gallery__alt__popover dropdown-animation'
className='info-tooltip dropdown-animation'
role='region'
id={accessibilityId}
onMouseDown={handleMouseDown}

View File

@ -1,8 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-call,
@typescript-eslint/no-unsafe-return,
@typescript-eslint/no-unsafe-assignment,
@typescript-eslint/no-unsafe-member-access
-- the settings store is not yet typed */
import type { PropsWithChildren } from 'react';
import { useCallback, useState, useEffect } from 'react';
@ -23,31 +18,48 @@ interface Props {
id: string;
}
export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
id,
children,
}) => {
const dismissed = useAppSelector((state) =>
export function useDismissableBannerState({ id }: Props) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const dismissed: boolean = useAppSelector((state) =>
/* eslint-disable-next-line */
state.settings.getIn(['dismissed_banners', id], false),
);
const [isVisible, setIsVisible] = useState(
!bannerSettings.get(id) && !dismissed,
);
const dispatch = useAppDispatch();
const [visible, setVisible] = useState(!bannerSettings.get(id) && !dismissed);
const intl = useIntl();
const handleDismiss = useCallback(() => {
setVisible(false);
const dismiss = useCallback(() => {
setIsVisible(false);
bannerSettings.set(id, true);
dispatch(changeSetting(['dismissed_banners', id], true));
}, [id, dispatch]);
useEffect(() => {
if (!visible && !dismissed) {
// Store legacy localStorage setting on server
if (!isVisible && !dismissed) {
dispatch(changeSetting(['dismissed_banners', id], true));
}
}, [id, dispatch, visible, dismissed]);
}, [id, dispatch, isVisible, dismissed]);
if (!visible) {
return {
isVisible,
dismiss,
};
}
export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
id,
children,
}) => {
const intl = useIntl();
const { isVisible, dismiss } = useDismissableBannerState({
id,
});
if (!isVisible) {
return null;
}
@ -58,7 +70,7 @@ export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
icon='times'
iconComponent={CloseIcon}
title={intl.formatMessage(messages.dismiss)}
onClick={handleDismiss}
onClick={dismiss}
/>
</div>

View File

@ -20,11 +20,12 @@ import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { me } from '../initial_state';
import { me } from '../../initial_state';
import { IconButton } from './icon_button';
import { isFeatureEnabled } from '../utils/environment';
import { ReblogButton } from './status/reblog_button';
import { IconButton } from '../icon_button';
import { isFeatureEnabled } from '../../utils/environment';
import { ReblogButton } from '../status/reblog_button';
import { RemoveQuoteHint } from './remove_quote_hint';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@ -77,6 +78,7 @@ class StatusActionBar extends ImmutablePureComponent {
status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.record,
quotedAccountId: PropTypes.string,
contextType: PropTypes.string,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onDelete: PropTypes.func,
@ -240,7 +242,7 @@ class StatusActionBar extends ImmutablePureComponent {
};
render () {
const { status, relationship, quotedAccountId, intl, withDismiss, withCounters, scrollKey } = this.props;
const { status, relationship, quotedAccountId, contextType, intl, withDismiss, withCounters, scrollKey } = this.props;
const { signedIn, permissions } = this.props.identity;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
@ -249,6 +251,7 @@ class StatusActionBar extends ImmutablePureComponent {
const account = status.get('account');
const writtenByMe = status.getIn(['account', 'id']) === me;
const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']);
const isQuotingMe = quotedAccountId === me;
let menu = [];
@ -293,7 +296,7 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
menu.push(null);
if (quotedAccountId === me) {
if (isQuotingMe) {
menu.push({ text: intl.formatMessage(messages.revokeQuote, { name: account.get('username') }), action: this.handleRevokeQuoteClick, dangerous: true });
}
@ -360,6 +363,8 @@ class StatusActionBar extends ImmutablePureComponent {
const bookmarkTitle = intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark);
const favouriteTitle = intl.formatMessage(status.get('favourited') ? messages.removeFavourite : messages.favourite);
const isReply = status.get('in_reply_to_account_id') === status.getIn(['account', 'id']);
const shouldShowQuoteRemovalHint = isQuotingMe && contextType === 'notifications';
return (
<div className='status__action-bar'>
@ -375,17 +380,23 @@ class StatusActionBar extends ImmutablePureComponent {
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={bookmarkTitle} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
</div>
<div className='status__action-bar__button-wrapper'>
<Dropdown
scrollKey={scrollKey}
status={status}
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
/>
</div>
<RemoveQuoteHint className='status__action-bar__button-wrapper' canShowHint={shouldShowQuoteRemovalHint}>
{(dismissQuoteHint) => (
<Dropdown
scrollKey={scrollKey}
status={status}
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
onOpen={() => {
dismissQuoteHint();
return true;
}}
/>
)}
</RemoveQuoteHint>
</div>
);
}

View File

@ -0,0 +1,90 @@
import { useRef } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import Overlay from 'react-overlays/Overlay';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { Button } from '../button';
import { useDismissableBannerState } from '../dismissable_banner';
import { Icon } from '../icon';
const DISMISSABLE_BANNER_ID = 'notifications/remove_quote_hint';
export const RemoveQuoteHint: React.FC<{
canShowHint: boolean;
className?: string;
children: (dismiss: () => void) => React.ReactNode;
}> = ({ canShowHint, className, children }) => {
const anchorRef = useRef<HTMLDivElement>(null);
const intl = useIntl();
const { isVisible, dismiss } = useDismissableBannerState({
id: DISMISSABLE_BANNER_ID,
});
return (
<div className={className} ref={anchorRef}>
{children(dismiss)}
{isVisible && canShowHint && (
<Overlay
show
flip
offset={[12, 10]}
placement='bottom-end'
target={anchorRef.current}
container={anchorRef.current}
>
{({ props, placement }) => (
<div
{...props}
className={classNames(
'info-tooltip info-tooltip--solid dropdown-animation',
placement,
)}
>
<h4>
<FormattedMessage
id='remove_quote_hint.title'
defaultMessage='Want to remove your quoted post?'
/>
</h4>
<FormattedMessage
id='remove_quote_hint.message'
defaultMessage='You can do so from the {icon} options menu.'
values={{
icon: (
<Icon
id='ellipsis-h'
icon={MoreHorizIcon}
aria-label={intl.formatMessage({
id: 'status.more',
defaultMessage: 'More',
})}
style={{ verticalAlign: 'middle' }}
/>
),
}}
>
{(text) => <p>{text}</p>}
</FormattedMessage>
<FormattedMessage
id='remove_quote_hint.button_label'
defaultMessage='Got it'
>
{(text) => (
<Button plain compact onClick={dismiss}>
{text}
</Button>
)}
</FormattedMessage>
</div>
)}
</Overlay>
)}
</div>
);
};

View File

@ -768,6 +768,9 @@
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.today": "today",
"remove_quote_hint.button_label": "Got it",
"remove_quote_hint.message": "You can do so from the {icon} options menu.",
"remove_quote_hint.title": "Want to remove your quoted post?",
"reply_indicator.attachments": "{count, plural, one {# attachment} other {# attachments}}",
"reply_indicator.cancel": "Cancel",
"reply_indicator.poll": "Poll",

View File

@ -117,6 +117,7 @@ const initialState = ImmutableMap({
'explore/links': false,
'explore/statuses': false,
'explore/tags': false,
'notifications/remove_quote_hint': false,
}),
});

View File

@ -471,8 +471,8 @@
}
}
body > [data-popper-placement] {
z-index: 3;
[data-popper-placement] {
z-index: 9999;
}
.invisible {
@ -7127,7 +7127,8 @@ a.status-card {
cursor: default;
}
.media-gallery__alt__popover {
.info-tooltip {
color: $white;
background: color.change($black, $alpha: 0.65);
backdrop-filter: $backdrop-blur-filter;
border-radius: 4px;
@ -7139,20 +7140,36 @@ a.status-card {
max-height: 30em;
overflow-y: auto;
&--solid {
color: var(--nested-card-text);
background:
/* This is a bit of a silly hack for layering two background colours
* since --nested-card-background is too transparent for a tooltip */
linear-gradient(
var(--nested-card-background),
var(--nested-card-background)
),
linear-gradient(var(--background-color), var(--background-color));
border: var(--nested-card-border);
}
h4 {
font-size: 15px;
line-height: 20px;
font-weight: 500;
color: $white;
margin-bottom: 8px;
}
p {
font-size: 15px;
line-height: 20px;
color: color.change($white, $alpha: 0.85);
opacity: 0.85;
white-space: pre-line;
}
.button {
margin-block-start: 8px;
}
}
.attachment-list {