mirror of
https://github.com/mastodon/mastodon.git
synced 2025-09-06 18:01:05 +00:00
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
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:
parent
42be0ca0eb
commit
e7c30cd072
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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",
|
||||
|
|
|
@ -117,6 +117,7 @@ const initialState = ImmutableMap({
|
|||
'explore/links': false,
|
||||
'explore/statuses': false,
|
||||
'explore/tags': false,
|
||||
'notifications/remove_quote_hint': false,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user