Merge commit '715cbee93d419e26cf952bf7fc70f9c1cc217d8b' into glitch-soc/merge-upstream

This commit is contained in:
Claire committed 2025-05-03 17:37:26 +02:00
commit 3ec3814f8e
198 files changed
+1539 -1058

No files matched your search

@@ -30,9 +30,6 @@ const messages = defineMessages({
class PrivacyDropdown extends PureComponent {
static propTypes = {
isUserTouching: PropTypes.func,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
noDirect: PropTypes.bool,
@@ -15,16 +15,6 @@ const mapDispatchToProps = dispatch => ({
dispatch(changeComposeVisibility(value));
},
isUserTouching,
onModalOpen: props => dispatch(openModal({
modalType: 'ACTIONS',
modalProps: props,
})),
onModalClose: () => dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
})),
});
export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown);
@@ -9,6 +9,8 @@ import {
fetchHashtag,
followHashtag,
unfollowHashtag,
featureHashtag,
unfeatureHashtag,
} from 'mastodon/actions/tags_typed';
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
import { Button } from 'mastodon/components/button';
@@ -28,6 +30,11 @@ const messages = defineMessages({
id: 'hashtag.admin_moderation',
defaultMessage: 'Open moderation interface for #{name}',
},
feature: { id: 'hashtag.feature', defaultMessage: 'Feature on profile' },
unfeature: {
id: 'hashtag.unfeature',
defaultMessage: "Don't feature on profile",
},
});
const usesRenderer = (displayNumber: React.ReactNode, pluralReady: number) => (
@@ -88,22 +95,51 @@ export const HashtagHeader: React.FC<{
}, [dispatch, tagId, setTag]);
const menu = useMemo(() => {
const tmp = [];
const arr = [];
if (
tag &&
signedIn &&
(permissions & PERMISSION_MANAGE_TAXONOMIES) ===
PERMISSION_MANAGE_TAXONOMIES
) {
tmp.push({
text: intl.formatMessage(messages.adminModeration, { name: tag.id }),
href: `/admin/tags/${tag.id}`,
if (tag && signedIn) {
const handleFeature = () => {
if (tag.featuring) {
void dispatch(unfeatureHashtag({ tagId })).then((result) => {
if (isFulfilled(result)) {
setTag(result.payload);
}
return '';
});
} else {
void dispatch(featureHashtag({ tagId })).then((result) => {
if (isFulfilled(result)) {
setTag(result.payload);
}
return '';
});
}
};
arr.push({
text: intl.formatMessage(
tag.featuring ? messages.unfeature : messages.feature,
),
action: handleFeature,
});
arr.push(null);
if (
(permissions & PERMISSION_MANAGE_TAXONOMIES) ===
PERMISSION_MANAGE_TAXONOMIES
) {
arr.push({
text: intl.formatMessage(messages.adminModeration, { name: tagId }),
href: `/admin/tags/${tag.id}`,
});
}
}
return tmp;
}, [signedIn, permissions, intl, tag]);
return arr;
}, [setTag, dispatch, tagId, signedIn, permissions, intl, tag]);
const handleFollow = useCallback(() => {
if (!signedIn || !tag) {
@@ -39,9 +39,9 @@ export const NotificationMention: React.FC<{
unread: boolean;
}> = ({ notification, unread }) => {
const [isDirect, isReply] = useAppSelector((state) => {
const status = state.statuses.get(notification.statusId) as
| Status
| undefined;
const status = notification.statusId
? (state.statuses.get(notification.statusId) as Status | undefined)
: undefined;
if (!status) return [false, false] as const;
@@ -65,6 +65,7 @@ import { textForScreenReader, defaultMediaVisibility } from '../../components/st
import StatusContainer from '../../containers/status_container';
import { deleteModal } from '../../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
import { getAncestorsIds, getDescendantsIds } from 'mastodon/selectors/contexts';
import Column from '../ui/components/column';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
@@ -83,69 +84,15 @@ const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
const getAncestorsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'inReplyTos']),
], (statusId, inReplyTos) => {
let ancestorsIds = ImmutableList();
ancestorsIds = ancestorsIds.withMutations(mutable => {
let id = statusId;
while (id && !mutable.includes(id)) {
mutable.unshift(id);
id = inReplyTos.get(id);
}
});
return ancestorsIds;
});
const getDescendantsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'replies']),
state => state.get('statuses'),
], (statusId, contextReplies, statuses) => {
let descendantsIds = [];
const ids = [statusId];
while (ids.length > 0) {
let id = ids.pop();
const replies = contextReplies.get(id);
if (statusId !== id) {
descendantsIds.push(id);
}
if (replies) {
replies.reverse().forEach(reply => {
if (!ids.includes(reply) && !descendantsIds.includes(reply) && statusId !== reply) ids.push(reply);
});
}
}
let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account'));
if (insertAt !== -1) {
descendantsIds.forEach((id, idx) => {
if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === statuses.get(id).get('account')) {
descendantsIds.splice(idx, 1);
descendantsIds.splice(insertAt, 0, id);
insertAt += 1;
}
});
}
return ImmutableList(descendantsIds);
});
const mapStateToProps = (state, props) => {
const status = getStatus(state, { id: props.params.statusId, contextType: 'detailed' });
let ancestorsIds = ImmutableList();
let descendantsIds = ImmutableList();
let ancestorsIds = [];
let descendantsIds = [];
if (status) {
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
descendantsIds = getDescendantsIds(state, { id: status.get('id') });
ancestorsIds = getAncestorsIds(state, status.get('in_reply_to_id'));
descendantsIds = getDescendantsIds(state, status.get('id'));
}
return {
@@ -188,8 +135,8 @@ class Status extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired,
status: ImmutablePropTypes.map,
isLoading: PropTypes.bool,
ancestorsIds: ImmutablePropTypes.list.isRequired,
descendantsIds: ImmutablePropTypes.list.isRequired,
ancestorsIds: PropTypes.arrayOf(PropTypes.string).isRequired,
descendantsIds: PropTypes.arrayOf(PropTypes.string).isRequired,
intl: PropTypes.object.isRequired,
askReplyConfirmation: PropTypes.bool,
multiColumn: PropTypes.bool,
@@ -383,7 +330,7 @@ class Status extends ImmutablePureComponent {
handleToggleAll = () => {
const { status, ancestorsIds, descendantsIds } = this.props;
const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS());
const statusIds = [status.get('id')].concat(ancestorsIds, descendantsIds);
if (status.get('hidden')) {
this.props.dispatch(revealStatus(statusIds));
@@ -482,13 +429,13 @@ class Status extends ImmutablePureComponent {
const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) {
this._selectChild(ancestorsIds.size - 1, true);
this._selectChild(ancestorsIds.length - 1, true);
} else {
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index, true);
this._selectChild(ancestorsIds.length + index, true);
} else {
this._selectChild(index - 1, true);
}
@@ -499,13 +446,13 @@ class Status extends ImmutablePureComponent {
const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) {
this._selectChild(ancestorsIds.size + 1, false);
this._selectChild(ancestorsIds.length + 1, false);
} else {
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index + 2, false);
this._selectChild(ancestorsIds.length + index + 2, false);
} else {
this._selectChild(index + 1, false);
}
@@ -536,8 +483,8 @@ class Status extends ImmutablePureComponent {
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType='thread'
previousId={i > 0 ? list.get(i - 1) : undefined}
nextId={list.get(i + 1) || (ancestors && statusId)}
previousId={i > 0 ? list[i - 1] : undefined}
nextId={list[i + 1] || (ancestors && statusId)}
rootId={statusId}
/>
));
@@ -574,7 +521,7 @@ class Status extends ImmutablePureComponent {
componentDidUpdate (prevProps) {
const { status, ancestorsIds } = this.props;
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) {
if (status && (ancestorsIds.length > prevProps.ancestorsIds.length || prevProps.status?.get('id') !== status.get('id'))) {
this._scrollStatusIntoView();
}
}
@@ -621,11 +568,11 @@ class Status extends ImmutablePureComponent {
);
}
if (ancestorsIds && ancestorsIds.size > 0) {
if (ancestorsIds && ancestorsIds.length > 0) {
ancestors = <>{this.renderChildren(ancestorsIds, true)}</>;
}
if (descendantsIds && descendantsIds.size > 0) {
if (descendantsIds && descendantsIds.length > 0) {
descendants = <>{this.renderChildren(descendantsIds)}</>;
}
@@ -1,48 +0,0 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { IconButton } from '../../../components/icon_button';
export default class ActionsModal extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
actions: PropTypes.array,
onClick: PropTypes.func,
};
renderAction = (action, i) => {
if (action === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { icon = null, iconComponent = null, text, meta = null, active = false, href = '#' } = action;
return (
<li key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
{icon && <IconButton title={text} icon={icon} iconComponent={iconComponent} role='presentation' tabIndex={-1} inverted />}
<div>
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
<div>{meta}</div>
</div>
</a>
</li>
);
};
render () {
return (
<div className='modal-root__modal actions-modal'>
<ul className={classNames({ 'with-status': !!status })}>
{this.props.actions.map(this.renderAction)}
</ul>
</div>
);
}
}
@@ -0,0 +1,65 @@
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import type { MenuItem } from 'mastodon/models/dropdown_menu';
import {
isActionItem,
isExternalLinkItem,
} from 'mastodon/models/dropdown_menu';
export const ActionsModal: React.FC<{
actions: MenuItem[];
onClick: React.MouseEventHandler;
}> = ({ actions, onClick }) => (
<div className='modal-root__modal actions-modal'>
<ul>
{actions.map((option, i: number) => {
if (option === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { text, dangerous } = option;
let element: React.ReactElement;
if (isActionItem(option)) {
element = (
<button onClick={onClick} data-index={i}>
{text}
</button>
);
} else if (isExternalLinkItem(option)) {
element = (
<a
href={option.href}
target={option.target ?? '_target'}
data-method={option.method}
rel='noopener'
onClick={onClick}
data-index={i}
>
{text}
</a>
);
} else {
element = (
<Link to={option.to} onClick={onClick} data-index={i}>
{text}
</Link>
);
}
return (
<li
className={classNames({
'dropdown-menu__item--dangerous': dangerous,
})}
key={`${text}-${i}`}
>
{element}
</li>
);
})}
</ul>
</div>
);
@@ -24,7 +24,7 @@ import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
import BundleContainer from '../containers/bundle_container';
import ActionsModal from './actions_modal';
import { ActionsModal } from './actions_modal';
import AudioModal from './audio_modal';
import { BoostModal } from './boost_modal';
import {