Merge branch 'suggestions-v2' into 'develop'

SuggestionsV2: pull in Mastodon's upstream code

See merge request soapbox-pub/soapbox-fe!765
v1.x.x
Alex Gleason 2021-09-17 00:00:26 +00:00
commit 34dce143e5
11 zmienionych plików z 278 dodań i 0 usunięć

Wyświetl plik

@ -17,6 +17,7 @@ export function normalizeAccount(account) {
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
account.note_emojified = emojify(account.note, emojiMap);
account.note_plain = unescapeHTML(account.note);
if (account.fields) {
account.fields = account.fields.map(pair => ({

Wyświetl plik

@ -0,0 +1,18 @@
import api from '../api';
import { importFetchedAccount } from './importer';
export const SUGGESTIONS_V2_FETCH_REQUEST = 'SUGGESTIONS_V2_FETCH_REQUEST';
export const SUGGESTIONS_V2_FETCH_SUCCESS = 'SUGGESTIONS_V2_FETCH_SUCCESS';
export const SUGGESTIONS_V2_FETCH_FAIL = 'SUGGESTIONS_V2_FETCH_FAIL';
export function fetchSuggestions() {
return (dispatch, getState) => {
dispatch({ type: SUGGESTIONS_V2_FETCH_REQUEST, skipLoading: true });
api(getState).get('/api/v2/suggestions').then(({ data: suggestions }) => {
suggestions.forEach(({ account }) => dispatch(importFetchedAccount(account)));
dispatch({ type: SUGGESTIONS_V2_FETCH_SUCCESS, suggestions, skipLoading: true });
}).catch(error => {
dispatch({ type: SUGGESTIONS_V2_FETCH_FAIL, error, skipLoading: true, skipAlert: true });
});
};
}

Wyświetl plik

@ -0,0 +1,85 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { makeGetAccount } from 'soapbox/selectors';
import Avatar from 'soapbox/components/avatar';
import DisplayName from 'soapbox/components/display_name';
import Permalink from 'soapbox/components/permalink';
import IconButton from 'soapbox/components/icon_button';
import { injectIntl, defineMessages } from 'react-intl';
import { followAccount, unfollowAccount } from 'soapbox/actions/accounts';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
});
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, props) => ({
account: getAccount(state, props.id),
});
return mapStateToProps;
};
const getFirstSentence = str => {
const arr = str.split(/(([.?!]+\s)|[.。?!\n•])/);
return arr[0];
};
export default @connect(makeMapStateToProps)
@injectIntl
class Account extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
intl: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
};
handleFollow = () => {
const { account, dispatch } = this.props;
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
dispatch(unfollowAccount(account.get('id')));
} else {
dispatch(followAccount(account.get('id')));
}
}
render() {
const { account, intl } = this.props;
let button;
if (account.getIn(['relationship', 'following'])) {
button = <IconButton icon='check' title={intl.formatMessage(messages.unfollow)} active onClick={this.handleFollow} />;
} else {
button = <IconButton icon='plus' title={intl.formatMessage(messages.follow)} onClick={this.handleFollow} />;
}
return (
<div className='account follow-recommendations-account'>
<div className='account__wrapper'>
<Permalink className='account__display-name account__display-name--with-note' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} />
<div className='account__note'>{getFirstSentence(account.get('note_plain'))}</div>
</Permalink>
<div className='account__relationship'>
{button}
</div>
</div>
</div>
);
}
}

Wyświetl plik

@ -0,0 +1,79 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { fetchSuggestions } from 'soapbox/actions/suggestions_v2';
import Column from 'soapbox/features/ui/components/column';
import Account from './components/account';
import Button from 'soapbox/components/button';
const mapStateToProps = state => ({
suggestions: state.getIn(['suggestions_v2', 'items']),
isLoading: state.getIn(['suggestions_v2', 'isLoading']),
});
export default @connect(mapStateToProps)
class FollowRecommendations extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
};
static propTypes = {
dispatch: PropTypes.func.isRequired,
suggestions: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
};
componentDidMount() {
const { dispatch, suggestions } = this.props;
// Don't re-fetch if we're e.g. navigating backwards to this page,
// since we don't want followed accounts to disappear from the list
if (suggestions.size === 0) {
dispatch(fetchSuggestions(true));
}
}
handleDone = () => {
const { router } = this.context;
router.history.push('/');
}
render() {
const { suggestions, isLoading } = this.props;
return (
<Column>
<div className='scrollable follow-recommendations-container'>
<div className='column-title'>
<h3><FormattedMessage id='follow_recommendations.heading' defaultMessage="Follow people you'd like to see posts from! Here are some suggestions." /></h3>
<p><FormattedMessage id='follow_recommendations.lead' defaultMessage="Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!" /></p>
</div>
{!isLoading && (
<>
<div className='column-list'>
{suggestions.size > 0 ? suggestions.map(suggestion => (
<Account key={suggestion.get('account')} id={suggestion.get('account')} />
)) : (
<div className='column-list__empty-message'>
<FormattedMessage id='empty_column.follow_recommendations' defaultMessage='Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.' />
</div>
)}
</div>
<div className='column-actions'>
<Button onClick={this.handleDone}><FormattedMessage id='follow_recommendations.done' defaultMessage='Done' /></Button>
</div>
</>
)}
</div>
</Column>
);
}
}

Wyświetl plik

@ -106,6 +106,7 @@ import {
UserIndex,
FederationRestrictions,
Aliases,
FollowRecommendations,
} from './util/async-components';
// Dummy import, to make sure that <Status /> ends up in the application bundle.
@ -250,6 +251,7 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/notifications' page={DefaultPage} component={Notifications} content={children} />
<WrappedRoute path='/search' publicRoute page={DefaultPage} component={Search} content={children} />
<WrappedRoute path='/suggestions' publicRoute page={DefaultPage} component={FollowRecommendations} content={children} />
<WrappedRoute path='/chats' exact page={DefaultPage} component={ChatIndex} content={children} />
<WrappedRoute path='/chats/:chatId' page={DefaultPage} component={ChatRoom} content={children} />

Wyświetl plik

@ -281,3 +281,7 @@ export function Aliases() {
export function ScheduleForm() {
return import(/* webpackChunkName: "features/compose" */'../../compose/components/schedule_form');
}
export function FollowRecommendations() {
return import(/* webpackChunkName: "features/follow_recommendations" */'../../follow_recommendations');
}

Wyświetl plik

@ -31,6 +31,7 @@ import listAdder from './list_adder';
import filters from './filters';
import conversations from './conversations';
import suggestions from './suggestions';
import suggestions_v2 from './suggestions_v2';
import polls from './polls';
import identity_proofs from './identity_proofs';
import trends from './trends';
@ -88,6 +89,7 @@ const appReducer = combineReducers({
filters,
conversations,
suggestions,
suggestions_v2,
polls,
trends,
groups,

Wyświetl plik

@ -0,0 +1,37 @@
import {
SUGGESTIONS_V2_FETCH_REQUEST,
SUGGESTIONS_V2_FETCH_SUCCESS,
SUGGESTIONS_V2_FETCH_FAIL,
} from '../actions/suggestions_v2';
import { SUGGESTIONS_DISMISS } from '../actions/suggestions';
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'soapbox/actions/accounts';
import { DOMAIN_BLOCK_SUCCESS } from 'soapbox/actions/domain_blocks';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
const initialState = ImmutableMap({
items: ImmutableList(),
isLoading: false,
});
export default function suggestionsReducer(state = initialState, action) {
switch(action.type) {
case SUGGESTIONS_V2_FETCH_REQUEST:
return state.set('isLoading', true);
case SUGGESTIONS_V2_FETCH_SUCCESS:
return state.withMutations(map => {
map.set('items', fromJS(action.suggestions.map(x => ({ ...x, account: x.account.id }))));
map.set('isLoading', false);
});
case SUGGESTIONS_V2_FETCH_FAIL:
return state.set('isLoading', false);
case SUGGESTIONS_DISMISS:
return state.update('items', list => list.filterNot(x => x.account === action.id));
case ACCOUNT_BLOCK_SUCCESS:
case ACCOUNT_MUTE_SUCCESS:
return state.update('items', list => list.filterNot(x => x.account === action.relationship.id));
case DOMAIN_BLOCK_SUCCESS:
return state.update('items', list => list.filterNot(x => action.accounts.includes(x.account)));
default:
return state;
}
}

Wyświetl plik

@ -10,6 +10,7 @@ export const getFeatures = createSelector([
], (v, features, federation) => {
return {
suggestions: v.software === 'Mastodon' && gte(v.compatVersion, '2.4.3'),
suggestionsV2: v.software === 'Mastodon' && gte(v.compatVersion, '3.4.0'),
trends: v.software === 'Mastodon' && gte(v.compatVersion, '3.0.0'),
emojiReacts: v.software === 'Pleroma' && gte(v.version, '2.0.0'),
emojiReactsRGI: v.software === 'Pleroma' && gte(v.version, '2.2.49'),

Wyświetl plik

@ -206,6 +206,13 @@
display: flex;
}
}
&__note {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--primary-text-color--faint);
}
}
.account__wrapper {

Wyświetl plik

@ -768,3 +768,45 @@
}
}
}
.column-title {
text-align: center;
padding: 40px;
.logo {
fill: var(--primary-text-color);
width: 50px;
margin: 0 auto;
margin-bottom: 40px;
}
h3 {
font-size: 24px;
line-height: 1.5;
font-weight: 700;
margin-bottom: 10px;
}
p {
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: var(--primary-text-color--faint);
}
}
.column-actions {
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
padding-top: 40px;
&__background {
position: absolute;
left: 0;
bottom: 0;
height: 220px;
width: auto;
}
}