diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js
index d5c4a02f9..d67ab112e 100644
--- a/app/javascript/flavours/glitch/actions/accounts.js
+++ b/app/javascript/flavours/glitch/actions/accounts.js
@@ -72,6 +72,17 @@ export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
+export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST';
+export const PINNED_ACCOUNTS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_FETCH_SUCCESS';
+export const PINNED_ACCOUNTS_FETCH_FAIL = 'PINNED_ACCOUNTS_FETCH_FAIL';
+
+export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY';
+export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR';
+export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE';
+
+export const PINNED_ACCOUNTS_EDITOR_RESET = 'PINNED_ACCOUNTS_EDITOR_RESET';
+
+
export function fetchAccount(id) {
return (dispatch, getState) => {
dispatch(fetchRelationships([id]));
@@ -733,3 +744,76 @@ export function unpinAccountFail(error) {
error,
};
};
+
+export function fetchPinnedAccounts() {
+ return (dispatch, getState) => {
+ dispatch(fetchPinnedAccountsRequest());
+
+ api(getState).get(`/api/v1/endorsements`, { params: { limit: 0 } })
+ .then(({ data }) => dispatch(fetchPinnedAccountsSuccess(data)))
+ .catch(err => dispatch(fetchPinnedAccountsFail(err)));
+ };
+};
+
+export function fetchPinnedAccountsRequest() {
+ return {
+ type: PINNED_ACCOUNTS_FETCH_REQUEST,
+ };
+};
+
+export function fetchPinnedAccountsSuccess(accounts, next) {
+ return {
+ type: PINNED_ACCOUNTS_FETCH_SUCCESS,
+ accounts,
+ next,
+ };
+};
+
+export function fetchPinnedAccountsFail(error) {
+ return {
+ type: PINNED_ACCOUNTS_FETCH_FAIL,
+ error,
+ };
+};
+
+export function fetchPinnedAccountsSuggestions(q) {
+ return (dispatch, getState) => {
+ const params = {
+ q,
+ resolve: false,
+ limit: 4,
+ following: true,
+ };
+
+ api(getState).get('/api/v1/accounts/search', { params })
+ .then(({ data }) => dispatch(fetchPinnedAccountsSuggestionsReady(q, data)));
+ };
+};
+
+export function fetchPinnedAccountsSuggestionsReady(query, accounts) {
+ return {
+ type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY,
+ query,
+ accounts,
+ };
+};
+
+export function clearPinnedAccountsSuggestions() {
+ return {
+ type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR,
+ };
+};
+
+export function changePinnedAccountsSuggestions(value) {
+ return {
+ type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE,
+ value,
+ }
+};
+
+export function resetPinnedAccountsEditor() {
+ return {
+ type: PINNED_ACCOUNTS_EDITOR_RESET,
+ };
+};
+
diff --git a/app/javascript/flavours/glitch/features/getting_started_misc/index.js b/app/javascript/flavours/glitch/features/getting_started_misc/index.js
index cbd4b8fe2..ee4452472 100644
--- a/app/javascript/flavours/glitch/features/getting_started_misc/index.js
+++ b/app/javascript/flavours/glitch/features/getting_started_misc/index.js
@@ -21,6 +21,7 @@ const messages = defineMessages({
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
+ featured_users: { id: 'navigation_bar.featured_users', defaultMessage: 'Featured users' },
});
@connect()
@@ -33,10 +34,13 @@ export default class gettingStartedMisc extends ImmutablePureComponent {
};
openOnboardingModal = (e) => {
- e.preventDefault();
this.props.dispatch(openModal('ONBOARDING'));
}
+ openFeaturedAccountsModal = (e) => {
+ this.props.dispatch(openModal('PINNED_ACCOUNTS_EDITOR'));
+ }
+
render () {
const { intl } = this.props;
@@ -50,6 +54,7 @@ export default class gettingStartedMisc extends ImmutablePureComponent {
+
diff --git a/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/account_container.js b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/account_container.js
new file mode 100644
index 000000000..149d05c32
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/account_container.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { makeGetAccount } from 'flavours/glitch/selectors';
+import { injectIntl } from 'react-intl';
+import { pinAccount, unpinAccount } from 'flavours/glitch/actions/accounts';
+import Account from 'flavours/glitch/features/list_editor/components/account';
+
+const makeMapStateToProps = () => {
+ const getAccount = makeGetAccount();
+
+ const mapStateToProps = (state, { accountId, added }) => ({
+ account: getAccount(state, accountId),
+ added: typeof added === 'undefined' ? state.getIn(['pinnedAccountsEditor', 'accounts', 'items']).includes(accountId) : added,
+ });
+
+ return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { accountId }) => ({
+ onRemove: () => dispatch(unpinAccount(accountId)),
+ onAdd: () => dispatch(pinAccount(accountId)),
+});
+
+export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));
diff --git a/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/search_container.js b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/search_container.js
new file mode 100644
index 000000000..5a1efce0a
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/search_container.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { injectIntl } from 'react-intl';
+import {
+ fetchPinnedAccountsSuggestions,
+ clearPinnedAccountsSuggestions,
+ changePinnedAccountsSuggestions
+} from '../../../actions/accounts';
+import Search from 'flavours/glitch/features/list_editor/components/search';
+
+const mapStateToProps = state => ({
+ value: state.getIn(['pinnedAccountsEditor', 'suggestions', 'value']),
+});
+
+const mapDispatchToProps = dispatch => ({
+ onSubmit: value => dispatch(fetchPinnedAccountsSuggestions(value)),
+ onClear: () => dispatch(clearPinnedAccountsSuggestions()),
+ onChange: value => dispatch(changePinnedAccountsSuggestions(value)),
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Search));
diff --git a/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js b/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js
new file mode 100644
index 000000000..7484e458e
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js
@@ -0,0 +1,78 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import { fetchPinnedAccounts, clearPinnedAccountsSuggestions, resetPinnedAccountsEditor } from 'flavours/glitch/actions/accounts';
+import AccountContainer from './containers/account_container';
+import SearchContainer from './containers/search_container';
+import Motion from 'flavours/glitch/util/optional_motion';
+import spring from 'react-motion/lib/spring';
+
+const mapStateToProps = state => ({
+ accountIds: state.getIn(['pinnedAccountsEditor', 'accounts', 'items']),
+ searchAccountIds: state.getIn(['pinnedAccountsEditor', 'suggestions', 'items']),
+});
+
+const mapDispatchToProps = dispatch => ({
+ onInitialize: () => dispatch(fetchPinnedAccounts()),
+ onClear: () => dispatch(clearPinnedAccountsSuggestions()),
+ onReset: () => dispatch(resetPinnedAccountsEditor()),
+});
+
+@connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+export default class PinnedAccountsEditor extends ImmutablePureComponent {
+
+ static propTypes = {
+ onClose: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ onInitialize: PropTypes.func.isRequired,
+ onClear: PropTypes.func.isRequired,
+ onReset: PropTypes.func.isRequired,
+ title: PropTypes.string.isRequired,
+ accountIds: ImmutablePropTypes.list.isRequired,
+ searchAccountIds: ImmutablePropTypes.list.isRequired,
+ };
+
+ componentDidMount () {
+ const { onInitialize } = this.props;
+ onInitialize();
+ }
+
+ componentWillUnmount () {
+ const { onReset } = this.props;
+ onReset();
+ }
+
+ render () {
+ const { accountIds, searchAccountIds, onClear } = this.props;
+ const showSearch = searchAccountIds.size > 0;
+
+ return (
+
+
+
+
+
+
+
+ {accountIds.map(accountId =>
)}
+
+
+ {showSearch &&
}
+
+
+ {({ x }) =>
+ (
+ {searchAccountIds.map(accountId =>
)}
+
)
+ }
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
index 23a7603d8..c9f54804a 100644
--- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js
+++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
@@ -19,6 +19,7 @@ import {
SettingsModal,
EmbedModal,
ListEditor,
+ PinnedAccountsEditor,
} from 'flavours/glitch/util/async-components';
const MODAL_COMPONENTS = {
@@ -36,6 +37,7 @@ const MODAL_COMPONENTS = {
'EMBED': EmbedModal,
'LIST_EDITOR': ListEditor,
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
+ 'PINNED_ACCOUNTS_EDITOR': PinnedAccountsEditor,
};
export default class ModalRoot extends React.PureComponent {
diff --git a/app/javascript/flavours/glitch/reducers/accounts.js b/app/javascript/flavours/glitch/reducers/accounts.js
index c38b3cc95..c2f016a87 100644
--- a/app/javascript/flavours/glitch/reducers/accounts.js
+++ b/app/javascript/flavours/glitch/reducers/accounts.js
@@ -6,6 +6,8 @@ import {
FOLLOWING_EXPAND_SUCCESS,
FOLLOW_REQUESTS_FETCH_SUCCESS,
FOLLOW_REQUESTS_EXPAND_SUCCESS,
+ PINNED_ACCOUNTS_FETCH_SUCCESS,
+ PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY,
} from 'flavours/glitch/actions/accounts';
import {
BLOCKS_FETCH_SUCCESS,
@@ -141,6 +143,8 @@ export default function accounts(state = initialState, action) {
case MUTES_EXPAND_SUCCESS:
case LIST_ACCOUNTS_FETCH_SUCCESS:
case LIST_EDITOR_SUGGESTIONS_READY:
+ case PINNED_ACCOUNTS_FETCH_SUCCESS:
+ case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY:
return action.accounts ? normalizeAccounts(state, action.accounts) : state;
case NOTIFICATIONS_EXPAND_SUCCESS:
case SEARCH_FETCH_SUCCESS:
diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js
index 7b7bc2ca2..218a5ac8f 100644
--- a/app/javascript/flavours/glitch/reducers/index.js
+++ b/app/javascript/flavours/glitch/reducers/index.js
@@ -28,6 +28,7 @@ import custom_emojis from './custom_emojis';
import lists from './lists';
import listEditor from './list_editor';
import filters from './filters';
+import pinnedAccountsEditor from './pinned_accounts_editor';
const reducers = {
dropdown_menu,
@@ -59,6 +60,7 @@ const reducers = {
lists,
listEditor,
filters,
+ pinnedAccountsEditor,
};
export default combineReducers(reducers);
diff --git a/app/javascript/flavours/glitch/reducers/pinned_accounts_editor.js b/app/javascript/flavours/glitch/reducers/pinned_accounts_editor.js
new file mode 100644
index 000000000..267521bb8
--- /dev/null
+++ b/app/javascript/flavours/glitch/reducers/pinned_accounts_editor.js
@@ -0,0 +1,57 @@
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+import {
+ PINNED_ACCOUNTS_EDITOR_RESET,
+ PINNED_ACCOUNTS_FETCH_REQUEST,
+ PINNED_ACCOUNTS_FETCH_SUCCESS,
+ PINNED_ACCOUNTS_FETCH_FAIL,
+ PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY,
+ PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR,
+ PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE,
+ ACCOUNT_PIN_SUCCESS,
+ ACCOUNT_UNPIN_SUCCESS,
+} from '../actions/accounts';
+
+const initialState = ImmutableMap({
+ accounts: ImmutableMap({
+ items: ImmutableList(),
+ loaded: false,
+ isLoading: false,
+ }),
+
+ suggestions: ImmutableMap({
+ value: '',
+ items: ImmutableList(),
+ }),
+});
+
+export default function listEditorReducer(state = initialState, action) {
+ switch(action.type) {
+ case PINNED_ACCOUNTS_EDITOR_RESET:
+ return initialState;
+ case PINNED_ACCOUNTS_FETCH_REQUEST:
+ return state.setIn(['accounts', 'isLoading'], true);
+ case PINNED_ACCOUNTS_FETCH_FAIL:
+ return state.setIn(['accounts', 'isLoading'], false);
+ case PINNED_ACCOUNTS_FETCH_SUCCESS:
+ return state.update('accounts', accounts => accounts.withMutations(map => {
+ map.set('isLoading', false);
+ map.set('loaded', true);
+ map.set('items', ImmutableList(action.accounts.map(item => item.id)));
+ }));
+ case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE:
+ return state.setIn(['suggestions', 'value'], action.value);
+ case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY:
+ return state.setIn(['suggestions', 'items'], ImmutableList(action.accounts.map(item => item.id)));
+ case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR:
+ return state.update('suggestions', suggestions => suggestions.withMutations(map => {
+ map.set('items', ImmutableList());
+ map.set('value', '');
+ }));
+ case ACCOUNT_PIN_SUCCESS:
+ return state.updateIn(['accounts', 'items'], list => list.unshift(action.relationship.id));
+ case ACCOUNT_UNPIN_SUCCESS:
+ return state.updateIn(['accounts', 'items'], list => list.filterNot(item => item === action.relationship.id));
+ default:
+ return state;
+ }
+};
diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js
index 3d6d3d1f4..557ce317e 100644
--- a/app/javascript/flavours/glitch/util/async-components.js
+++ b/app/javascript/flavours/glitch/util/async-components.js
@@ -38,6 +38,10 @@ export function ListEditor () {
return import(/* webpackChunkName: "flavours/glitch/async/list_editor" */'flavours/glitch/features/list_editor');
}
+export function PinnedAccountsEditor () {
+ return import(/* webpackChunkName: "flavours/glitch/async/pinned_accounts_editor" */'flavours/glitch/features/pinned_accounts_editor');
+}
+
export function DirectTimeline() {
return import(/* webpackChunkName: "flavours/glitch/async/direct_timeline" */'flavours/glitch/features/direct_timeline');
}