Merge commit 'eb118d85238da8d0583868abab2756bb329742ba' into glitch-soc/merge-upstream

Conflicts:
- `yarn.lock`:
  Upstream updated dependencies textually adjacent to glitch-soc-only ones.
  Updated them as upstream did.
This commit is contained in:
Claire committed 2025-09-10 18:11:12 +02:00
commit 06f45a2046
219 files changed
+2373 -1267

No files matched your search

@@ -18,7 +18,7 @@ export const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' },
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Fewer algorithmic fanfares' },
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Hidden from Mastodon search results, trending, and public timelines' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers' },
private_long: { id: 'privacy.private.long', defaultMessage: 'Only your followers' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Specific people' },
@@ -79,10 +79,12 @@ const visibilityOptions = {
const PrivacyModalButton: FC<PrivacyDropdownProps> = ({ disabled = false }) => {
const intl = useIntl();
const { visibility, quotePolicy } = useAppSelector((state) => ({
visibility: state.compose.get('privacy') as StatusVisibility,
quotePolicy: state.compose.get('quote_policy') as ApiQuotePolicy,
}));
const quotePolicy = useAppSelector(
(state) => state.compose.get('quote_policy') as ApiQuotePolicy,
);
const visibility = useAppSelector(
(state) => state.compose.get('privacy') as StatusVisibility,
);
const { icon, iconComponent } = useMemo(() => {
const option = visibilityOptions[visibility];
@@ -1,5 +1,7 @@
import type { ComponentPropsWithoutRef, ElementType } from 'react';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import { useEmojify } from './hooks';
import type { CustomEmojiMapArg } from './types';
@@ -13,7 +15,7 @@ type EmojiHTMLProps<Element extends ElementType = 'div'> = Omit<
shallow?: boolean;
};
export const EmojiHTML = ({
export const ModernEmojiHTML = ({
extraEmojis,
htmlString,
as: Wrapper = 'div', // Rename for syntax highlighting
@@ -34,3 +36,14 @@ export const EmojiHTML = ({
<Wrapper {...props} dangerouslySetInnerHTML={{ __html: emojifiedHtml }} />
);
};
export const EmojiHTML = <Element extends ElementType>(
props: EmojiHTMLProps<Element>,
) => {
if (isModernEmojiEnabled()) {
return <ModernEmojiHTML {...props} />;
}
const { as: asElement, htmlString, extraEmojis, ...rest } = props;
const Wrapper = asElement ?? 'div';
return <Wrapper {...rest} dangerouslySetInnerHTML={{ __html: htmlString }} />;
};
@@ -143,16 +143,14 @@ export const SelectWithLabel: React.FC<PropsWithChildren<Props>> = ({
</div>
<div className='app-form__toggle__toggle'>
<div>
<Dropdown
value={value}
onChange={onChange}
disabled={disabled}
aria-labelledby={labelId}
aria-describedby={descId}
options={options}
/>
</div>
<Dropdown
value={value}
onChange={onChange}
disabled={disabled}
aria-labelledby={labelId}
aria-describedby={descId}
options={options}
/>
</div>
</label>
);
@@ -20,7 +20,7 @@ import { IconButton } from '../../../components/icon_button';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { me } from '../../../initial_state';
import { isFeatureEnabled } from '@/mastodon/utils/environment';
import { ReblogButton } from '@/mastodon/components/status/reblog_button';
import { BoostButton } from '@/mastodon/components/status/boost_button';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -310,7 +310,7 @@ class ActionBar extends PureComponent {
<div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} iconComponent={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'>
<ReblogButton status={status} />
<BoostButton status={status} />
</div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={favouriteTitle} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} /></div>
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={bookmarkTitle} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div>
@@ -1,6 +1,7 @@
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { DropdownMenuItemContent } from 'mastodon/components/dropdown_menu';
import type { MenuItem } from 'mastodon/models/dropdown_menu';
import {
isActionItem,
@@ -18,14 +19,14 @@ export const ActionsModal: React.FC<{
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { text, dangerous } = option;
const { text, highlighted, disabled, dangerous } = option;
let element: React.ReactElement;
if (isActionItem(option)) {
element = (
<button onClick={onClick} data-index={i}>
{text}
<button onClick={onClick} data-index={i} disabled={disabled}>
<DropdownMenuItemContent item={option} />
</button>
);
} else if (isExternalLinkItem(option)) {
@@ -38,21 +39,22 @@ export const ActionsModal: React.FC<{
onClick={onClick}
data-index={i}
>
{text}
<DropdownMenuItemContent item={option} />
</a>
);
} else {
element = (
<Link to={option.to} onClick={onClick} data-index={i}>
{text}
<DropdownMenuItemContent item={option} />
</Link>
);
}
return (
<li
className={classNames({
className={classNames('dropdown-menu__item', {
'dropdown-menu__item--dangerous': dangerous,
'dropdown-menu__item--highlighted': highlighted,
})}
key={`${text}-${i}`}
>
@@ -43,10 +43,6 @@ export const ConfirmationModal: React.FC<
onSecondary?.();
}, [onClose, onSecondary]);
const handleCancel = useCallback(() => {
onClose();
}, [onClose]);
return (
<div className='modal-root__modal safety-action-modal'>
<div className='safety-action-modal__top'>
@@ -58,7 +54,7 @@ export const ConfirmationModal: React.FC<
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
<button onClick={handleCancel} className='link-button'>
<button onClick={onClose} className='link-button'>
{cancel ?? (
<FormattedMessage
id='confirmation_modal.cancel'
@@ -11,3 +11,4 @@ export { ConfirmLogOutModal } from './log_out';
export { ConfirmFollowToListModal } from './follow_to_list';
export { ConfirmMissingAltTextModal } from './missing_alt_text';
export { ConfirmRevokeQuoteModal } from './revoke_quote';
export { QuietPostQuoteInfoModal } from './quiet_post_quote_info';
@@ -0,0 +1,77 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { quoteCompose } from '@/mastodon/actions/compose_typed';
import { closeModal } from '@/mastodon/actions/modal';
import { changeSetting } from '@/mastodon/actions/settings';
import type { Status } from '@/mastodon/models/status';
import { useAppDispatch } from '@/mastodon/store';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
title: {
id: 'confirmations.quiet_post_quote_info.title',
defaultMessage: 'Quoting quiet public posts',
},
message: {
id: 'confirmations.quiet_post_quote_info.message',
defaultMessage:
'When quoting a quiet public post, your post will be hidden from trending timelines.',
},
got_it: {
id: 'confirmations.quiet_post_quote_info.got_it',
defaultMessage: 'Got it',
},
dismiss: {
id: 'confirmations.quiet_post_quote_info.dismiss',
defaultMessage: "Don't remind me again",
},
});
/**
* [1] Since we only want this modal to have two buttons – "Don't ask again" and
* "Got it" – , we have to use the `onClose` handler to handle the "Don't ask again"
* functionality. Because of this, we need to set `closeWhenConfirm` to false and
* close the modal manually.
* This prevents the modal from being dismissed permanently when just confirming.
*/
export const QuietPostQuoteInfoModal: React.FC<{ status: Status }> = ({
status,
}) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const confirm = useCallback(() => {
dispatch(quoteCompose(status));
// [1]
dispatch(
closeModal({ modalType: 'CONFIRM_QUIET_QUOTE', ignoreFocus: true }),
);
}, [dispatch, status]);
const dismiss = useCallback(() => {
dispatch(quoteCompose(status));
dispatch(
changeSetting(['dismissed_banners', 'quote/quiet_post_hint'], true),
);
// [1]
dispatch(
closeModal({ modalType: 'CONFIRM_QUIET_QUOTE', ignoreFocus: true }),
);
}, [dispatch, status]);
return (
<ConfirmationModal
closeWhenConfirm={false} // [1]
title={intl.formatMessage(messages.title)}
message={intl.formatMessage(messages.message)}
confirm={intl.formatMessage(messages.got_it)}
cancel={intl.formatMessage(messages.dismiss)}
onConfirm={confirm}
onClose={dismiss}
/>
);
};
@@ -201,8 +201,6 @@ class MediaModal extends ImmutablePureComponent {
preview={image.get('preview_url')}
blurhash={image.get('blurhash')}
src={image.get('url')}
width={image.get('width')}
height={image.get('height')}
frameRate={image.getIn(['meta', 'original', 'frame_rate'])}
aspectRatio={`${image.getIn(['meta', 'original', 'width'])} / ${image.getIn(['meta', 'original', 'height'])}`}
startTime={currentTime || 0}
@@ -219,8 +217,6 @@ class MediaModal extends ImmutablePureComponent {
return (
<GIFV
src={image.get('url')}
width={width}
height={height}
key={image.get('url')}
alt={description}
lang={lang}
@@ -38,6 +38,7 @@ import {
ConfirmFollowToListModal,
ConfirmMissingAltTextModal,
ConfirmRevokeQuoteModal,
QuietPostQuoteInfoModal,
} from './confirmation_modals';
import { ImageModal } from './image_modal';
import MediaModal from './media_modal';
@@ -62,6 +63,7 @@ export const MODAL_COMPONENTS = {
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }),
'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }),
'CONFIRM_QUIET_QUOTE': () => Promise.resolve({ default: QuietPostQuoteInfoModal }),
'MUTE': MuteModal,
'BLOCK': BlockModal,
'DOMAIN_BLOCK': DomainBlockModal,
@@ -163,7 +163,6 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
value: 'unlisted',
text: intl.formatMessage(privacyMessages.unlisted_short),
meta: intl.formatMessage(privacyMessages.unlisted_long),
extra: intl.formatMessage(privacyMessages.unlisted_extra),
icon: 'unlock',
iconComponent: QuietTimeIcon,
},
@@ -199,8 +198,11 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
onClose();
}, [onChange, onClose, visibility, quotePolicy]);
const privacyDropdownId = useId();
const quoteDropdownId = useId();
const uniqueId = useId();
const visibilityLabelId = `${uniqueId}-visibility-label`;
const visibilityDescriptionId = `${uniqueId}-visibility-desc`;
const quoteLabelId = `${uniqueId}-quote-label`;
const quoteDescriptionId = `${uniqueId}-quote-desc`;
return (
<div className='modal-root__modal dialog-modal visibility-modal'>
@@ -235,28 +237,36 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
/>
</div>
<div className='dialog-modal__content__form'>
<label
htmlFor={privacyDropdownId}
className={classNames('visibility-dropdown__label', {
<div
className={classNames('visibility-dropdown', {
disabled: disableVisibility,
})}
>
<FormattedMessage
id='visibility_modal.privacy_label'
defaultMessage='Visibility'
/>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label
className='visibility-dropdown__label'
id={visibilityLabelId}
>
<FormattedMessage
id='visibility_modal.privacy_label'
defaultMessage='Visibility'
/>
</label>
<Dropdown
items={visibilityItems}
classPrefix='visibility-dropdown'
current={visibility}
onChange={handleVisibilityChange}
title={intl.formatMessage(privacyMessages.change_privacy)}
labelId={visibilityLabelId}
descriptionId={visibilityDescriptionId}
classPrefix='visibility-dropdown'
disabled={disableVisibility}
id={privacyDropdownId}
/>
{!!statusId && (
<p className='visibility-dropdown__helper'>
<p
className='visibility-dropdown__helper'
id='visibilityDescriptionId'
>
<FormattedMessage
id='visibility_modal.helper.privacy_editing'
defaultMessage="Visibility can't be changed after a post is published."
@@ -264,37 +274,47 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
</p>
)}
{!statusId && disablePublicVisibilities && (
<p className='visibility-dropdown__helper'>
<p
className='visibility-dropdown__helper'
id='visibilityDescriptionId'
>
<FormattedMessage
id='visibility_modal.helper.privacy_private_self_quote'
defaultMessage='Self-quotes of private posts cannot be made public.'
/>
</p>
)}
</label>
</div>
<label
htmlFor={quoteDropdownId}
className={classNames('visibility-dropdown__label', {
<div
className={classNames('visibility-dropdown', {
disabled: disableQuotePolicy,
})}
>
<FormattedMessage
id='visibility_modal.quote_label'
defaultMessage='Who can quote'
/>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label className='visibility-dropdown__label' id={quoteLabelId}>
<FormattedMessage
id='visibility_modal.quote_label'
defaultMessage='Who can quote'
/>
</label>
<Dropdown
items={quoteItems}
onChange={handleQuotePolicyChange}
classPrefix='visibility-dropdown'
current={disableQuotePolicy ? 'nobody' : quotePolicy}
title={intl.formatMessage(messages.buttonTitle)}
onChange={handleQuotePolicyChange}
labelId={quoteLabelId}
descriptionId={quoteDescriptionId}
classPrefix='visibility-dropdown'
disabled={disableQuotePolicy}
id={quoteDropdownId}
/>
<QuotePolicyHelper policy={quotePolicy} visibility={visibility} />
</label>
<QuotePolicyHelper
policy={quotePolicy}
visibility={visibility}
className='visibility-dropdown__helper'
id={quoteDescriptionId}
/>
</div>
</div>
<div className='dialog-modal__content__actions'>
<Button onClick={onClose} secondary>
@@ -317,42 +337,44 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
);
VisibilityModal.displayName = 'VisibilityModal';
const QuotePolicyHelper: FC<{
policy: ApiQuotePolicy;
visibility: StatusVisibility;
}> = ({ policy, visibility }) => {
const QuotePolicyHelper: FC<
{
policy: ApiQuotePolicy;
visibility: StatusVisibility;
} & React.ComponentPropsWithoutRef<'p'>
> = ({ policy, visibility, ...otherProps }) => {
let hintText: React.ReactElement | undefined;
if (visibility === 'unlisted' && policy !== 'nobody') {
return (
<p className='visibility-dropdown__helper'>
<FormattedMessage
id='visibility_modal.helper.unlisted_quoting'
defaultMessage='When people quote you, their post will also be hidden from trending timelines.'
/>
</p>
hintText = (
<FormattedMessage
id='visibility_modal.helper.unlisted_quoting'
defaultMessage='When people quote you, their post will also be hidden from trending timelines.'
/>
);
}
if (visibility === 'private') {
return (
<p className='visibility-dropdown__helper'>
<FormattedMessage
id='visibility_modal.helper.private_quoting'
defaultMessage="Follower-only posts authored on Mastodon can't be quoted by others."
/>
</p>
hintText = (
<FormattedMessage
id='visibility_modal.helper.private_quoting'
defaultMessage="Follower-only posts authored on Mastodon can't be quoted by others."
/>
);
}
if (visibility === 'direct') {
return (
<p className='visibility-dropdown__helper'>
<FormattedMessage
id='visibility_modal.helper.direct_quoting'
defaultMessage="Private mentions authored on Mastodon can't be quoted by others."
/>
</p>
hintText = (
<FormattedMessage
id='visibility_modal.helper.direct_quoting'
defaultMessage="Private mentions authored on Mastodon can't be quoted by others."
/>
);
}
return null;
if (!hintText) {
return null;
}
return <p {...otherProps}>{hintText}</p>;
};