Merge branch 'main' into glitch-soc/merge-upstream

Conflicts:
- `config/i18n-tasks.yml`:
  Upstream added new ignored strings, glitch-soc has extra ignored strings
  because of the theming system.
  Added upstream's changes.
This commit is contained in:
Claire
2023-01-18 17:38:11 +01:00
55 changed files with 875 additions and 136 deletions

View File

@@ -160,6 +160,18 @@ export function submitCompose(routerHistory) {
dispatch(submitComposeRequest());
// If we're editing a post with media attachments, those have not
// necessarily been changed on the server. Do it now in the same
// API call.
let media_attributes;
if (statusId !== null) {
media_attributes = media.map(item => ({
id: item.get('id'),
description: item.get('description'),
focus: item.get('focus'),
}));
}
api(getState).request({
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
method: statusId === null ? 'post' : 'put',
@@ -167,6 +179,7 @@ export function submitCompose(routerHistory) {
status,
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
media_ids: media.map(item => item.get('id')),
media_attributes,
sensitive: getState().getIn(['compose', 'sensitive']),
spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
visibility: getState().getIn(['compose', 'privacy']),
@@ -377,11 +390,31 @@ export function changeUploadCompose(id, params) {
return (dispatch, getState) => {
dispatch(changeUploadComposeRequest());
api(getState).put(`/api/v1/media/${id}`, params).then(response => {
dispatch(changeUploadComposeSuccess(response.data));
}).catch(error => {
dispatch(changeUploadComposeFail(id, error));
});
let media = getState().getIn(['compose', 'media_attachments']).find((item) => item.get('id') === id);
// Editing already-attached media is deferred to editing the post itself.
// For simplicity's sake, fake an API reply.
if (media && !media.get('unattached')) {
let { description, focus } = params;
const data = media.toJS();
if (description) {
data.description = description;
}
if (focus) {
focus = focus.split(',');
data.meta = { focus: { x: parseFloat(focus[0]), y: parseFloat(focus[1]) } };
}
dispatch(changeUploadComposeSuccess(data, true));
} else {
api(getState).put(`/api/v1/media/${id}`, params).then(response => {
dispatch(changeUploadComposeSuccess(response.data, false));
}).catch(error => {
dispatch(changeUploadComposeFail(id, error));
});
}
};
}
@@ -392,10 +425,11 @@ export function changeUploadComposeRequest() {
};
}
export function changeUploadComposeSuccess(media) {
export function changeUploadComposeSuccess(media, attached) {
return {
type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
media: media,
attached: attached,
skipLoading: true,
};
}

View File

@@ -1,9 +1,17 @@
import api from '../api';
import api, { getLinks } from '../api';
export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST';
export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS';
export const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL';
export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST';
export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS';
export const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL';
export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST';
export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS';
export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL';
export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST';
export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS';
export const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL';
@@ -37,6 +45,78 @@ export const fetchHashtagFail = error => ({
error,
});
export const fetchFollowedHashtags = () => (dispatch, getState) => {
dispatch(fetchFollowedHashtagsRequest());
api(getState).get('/api/v1/followed_tags').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null));
}).catch(err => {
dispatch(fetchFollowedHashtagsFail(err));
});
};
export function fetchFollowedHashtagsRequest() {
return {
type: FOLLOWED_HASHTAGS_FETCH_REQUEST,
};
};
export function fetchFollowedHashtagsSuccess(followed_tags, next) {
return {
type: FOLLOWED_HASHTAGS_FETCH_SUCCESS,
followed_tags,
next,
};
};
export function fetchFollowedHashtagsFail(error) {
return {
type: FOLLOWED_HASHTAGS_FETCH_FAIL,
error,
};
};
export function expandFollowedHashtags() {
return (dispatch, getState) => {
const url = getState().getIn(['followed_tags', 'next']);
if (url === null) {
return;
}
dispatch(expandFollowedHashtagsRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandFollowedHashtagsFail(error));
});
};
};
export function expandFollowedHashtagsRequest() {
return {
type: FOLLOWED_HASHTAGS_EXPAND_REQUEST,
};
};
export function expandFollowedHashtagsSuccess(followed_tags, next) {
return {
type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
followed_tags,
next,
};
};
export function expandFollowedHashtagsFail(error) {
return {
type: FOLLOWED_HASHTAGS_EXPAND_FAIL,
error,
};
};
export const followHashtag = name => (dispatch, getState) => {
dispatch(followHashtagRequest(name));

View File

@@ -50,7 +50,7 @@ export default class Trends extends React.PureComponent {
<Hashtag
key={hashtag.name}
name={hashtag.name}
to={`/admin/tags/${hashtag.id}`}
to={hashtag.id === undefined ? undefined : `/admin/tags/${hashtag.id}`}
people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
history={hashtag.history.reverse().map(day => day.uses)}

View File

@@ -46,6 +46,7 @@ const messages = defineMessages({
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
@@ -193,7 +194,7 @@ class Header extends ImmutablePureComponent {
}
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
}
if (me !== account.get('id')) {
@@ -242,6 +243,7 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });

View File

@@ -11,6 +11,7 @@ const messages = defineMessages({
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
@@ -45,6 +46,7 @@ class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });

View File

@@ -43,10 +43,10 @@ export default class Upload extends ImmutablePureComponent {
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
<div className='compose-form__upload__actions'>
<button type='button' className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
{!!media.get('unattached') && (<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>)}
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
</div>
{(media.get('description') || '').length === 0 && !!media.get('unattached') && (
{(media.get('description') || '').length === 0 && (
<div className='compose-form__upload__warning'>
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button>
</div>

View File

@@ -0,0 +1,89 @@
import { debounce } from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import ColumnHeader from 'mastodon/components/column_header';
import ScrollableList from 'mastodon/components/scrollable_list';
import Column from 'mastodon/features/ui/components/column';
import { Helmet } from 'react-helmet';
import Hashtag from 'mastodon/components/hashtag';
import { expandFollowedHashtags, fetchFollowedHashtags } from 'mastodon/actions/tags';
const messages = defineMessages({
heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' },
});
const mapStateToProps = state => ({
hashtags: state.getIn(['followed_tags', 'items']),
isLoading: state.getIn(['followed_tags', 'isLoading'], true),
hasMore: !!state.getIn(['followed_tags', 'next']),
});
export default @connect(mapStateToProps)
@injectIntl
class FollowedTags extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
hashtags: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
multiColumn: PropTypes.bool,
};
componentDidMount() {
this.props.dispatch(fetchFollowedHashtags());
};
handleLoadMore = debounce(() => {
this.props.dispatch(expandFollowedHashtags());
}, 300, { leading: true });
render () {
const { intl, hashtags, isLoading, hasMore, multiColumn } = this.props;
const emptyMessage = <FormattedMessage id='empty_column.followed_tags' defaultMessage='You have not followed any hashtags yet. When you do, they will show up here.' />;
return (
<Column bindToDocument={!multiColumn}>
<ColumnHeader
icon='hashtag'
title={intl.formatMessage(messages.heading)}
showBackButton
multiColumn={multiColumn}
/>
<ScrollableList
scrollKey='followed_tags'
emptyMessage={emptyMessage}
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
bindToDocument={!multiColumn}
>
{hashtags.map((hashtag) => (
<Hashtag
key={hashtag.get('name')}
name={hashtag.get('name')}
to={`/tags/${hashtag.get('name')}`}
withGraph={false}
// Taken from ImmutableHashtag. Should maybe refactor ImmutableHashtag to accept more options?
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
/>
))}
</ScrollableList>
<Helmet>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
}

View File

@@ -320,7 +320,7 @@ class FocalPointModal extends ImmutablePureComponent {
<React.Fragment>
<label className='setting-text-label' htmlFor='upload-modal__thumbnail'><FormattedMessage id='upload_form.thumbnail' defaultMessage='Change thumbnail' /></label>
<Button disabled={isUploadingThumbnail} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
<Button disabled={isUploadingThumbnail || !media.get('unattached')} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
<label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.chooseImage)}</span>

View File

@@ -30,7 +30,7 @@ const SignInBanner = () => {
return (
<div className='sign-in-banner'>
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.' /></p>
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.' /></p>
<a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
{signupButton}
</div>

View File

@@ -42,6 +42,7 @@ import {
FollowRequests,
FavouritedStatuses,
BookmarkedStatuses,
FollowedTags,
ListTimeline,
Blocks,
DomainBlocks,
@@ -54,7 +55,7 @@ import {
About,
PrivacyPolicy,
} from './util/async-components';
import initialState, { me, owner, singleUserMode, showTrends } from '../../initial_state';
import initialState, { me, owner, singleUserMode, showTrends, trendsAsLanding } from '../../initial_state';
import { closeOnboarding, INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
import Header from './components/header';
@@ -163,7 +164,7 @@ class SwitchingColumnsArea extends React.PureComponent {
}
} else if (singleUserMode && owner && initialState?.accounts[owner]) {
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
} else if (showTrends) {
} else if (showTrends && trendsAsLanding) {
redirect = <Redirect from='/' to='/explore' exact />;
} else {
redirect = <Redirect from='/' to='/about' exact />;
@@ -216,6 +217,7 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
<WrappedRoute path='/blocks' component={Blocks} content={children} />
<WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} />
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
<WrappedRoute path='/mutes' component={Mutes} content={children} />
<WrappedRoute path='/lists' component={Lists} content={children} />

View File

@@ -90,6 +90,10 @@ export function FavouritedStatuses () {
return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses');
}
export function FollowedTags () {
return import(/* webpackChunkName: "features/followed_tags" */'../../followed_tags');
}
export function BookmarkedStatuses () {
return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses');
}

View File

@@ -75,6 +75,7 @@
* @property {boolean} timeline_preview
* @property {string} title
* @property {boolean} trends
* @property {boolean} trends_as_landing_page
* @property {boolean} unfollow_modal
* @property {boolean} use_blurhash
* @property {boolean=} use_pending_items
@@ -126,6 +127,7 @@ export const singleUserMode = getMeta('single_user_mode');
export const source_url = getMeta('source_url');
export const timelinePreview = getMeta('timeline_preview');
export const title = getMeta('title');
export const trendsAsLanding = getMeta('trends_as_landing_page');
export const unfollowModal = getMeta('unfollow_modal');
export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items');

View File

@@ -1391,6 +1391,10 @@
"defaultMessage": "Lists",
"id": "navigation_bar.lists"
},
{
"defaultMessage": "Followed hashtags",
"id": "navigation_bar.followed_tags"
},
{
"defaultMessage": "Blocked users",
"id": "navigation_bar.blocks"
@@ -4220,7 +4224,7 @@
"id": "sign_in_banner.create_account"
},
{
"defaultMessage": "Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.",
"defaultMessage": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
"id": "sign_in_banner.text"
},
{
@@ -4310,4 +4314,4 @@
],
"path": "app/javascript/mastodon/features/video/index.json"
}
]
]

View File

@@ -383,6 +383,7 @@
"navigation_bar.favourites": "Favourites",
"navigation_bar.filters": "Muted words",
"navigation_bar.follow_requests": "Follow requests",
"navigation_bar.followed_tags": "Followed hashtags",
"navigation_bar.follows_and_followers": "Follows and followers",
"navigation_bar.lists": "Lists",
"navigation_bar.misc": "Misc",
@@ -545,7 +546,7 @@
"server_banner.server_stats": "Server stats:",
"sign_in_banner.create_account": "Create account",
"sign_in_banner.sign_in": "Sign in",
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.",
"sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_domain": "Open moderation interface for {domain}",
"status.admin_status": "Open this post in the moderation interface",

View File

@@ -444,7 +444,7 @@ export default function compose(state = initialState, action) {
.setIn(['media_modal', 'dirty'], false)
.update('media_attachments', list => list.map(item => {
if (item.get('id') === action.media.id) {
return fromJS(action.media).set('unattached', true);
return fromJS(action.media).set('unattached', !action.attached);
}
return item;

View File

@@ -0,0 +1,42 @@
import {
FOLLOWED_HASHTAGS_FETCH_REQUEST,
FOLLOWED_HASHTAGS_FETCH_SUCCESS,
FOLLOWED_HASHTAGS_FETCH_FAIL,
FOLLOWED_HASHTAGS_EXPAND_REQUEST,
FOLLOWED_HASHTAGS_EXPAND_SUCCESS,
FOLLOWED_HASHTAGS_EXPAND_FAIL,
} from 'mastodon/actions/tags';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
const initialState = ImmutableMap({
items: ImmutableList(),
isLoading: false,
next: null,
});
export default function followed_tags(state = initialState, action) {
switch(action.type) {
case FOLLOWED_HASHTAGS_FETCH_REQUEST:
return state.set('isLoading', true);
case FOLLOWED_HASHTAGS_FETCH_SUCCESS:
return state.withMutations(map => {
map.set('items', fromJS(action.followed_tags));
map.set('isLoading', false);
map.set('next', action.next);
});
case FOLLOWED_HASHTAGS_FETCH_FAIL:
return state.set('isLoading', false);
case FOLLOWED_HASHTAGS_EXPAND_REQUEST:
return state.set('isLoading', true);
case FOLLOWED_HASHTAGS_EXPAND_SUCCESS:
return state.withMutations(map => {
map.update('items', set => set.concat(fromJS(action.followed_tags)));
map.set('isLoading', false);
map.set('next', action.next);
});
case FOLLOWED_HASHTAGS_EXPAND_FAIL:
return state.set('isLoading', false);
default:
return state;
}
};

View File

@@ -40,6 +40,7 @@ import picture_in_picture from './picture_in_picture';
import accounts_map from './accounts_map';
import history from './history';
import tags from './tags';
import followed_tags from './followed_tags';
const reducers = {
announcements,
@@ -83,6 +84,7 @@ const reducers = {
picture_in_picture,
history,
tags,
followed_tags,
};
export default combineReducers(reducers);