Merge commit '715cbee93d419e26cf952bf7fc70f9c1cc217d8b' into glitch-soc/merge-upstream
This commit is contained in:
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) {
|
||||
|
||||
+3
-3
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user