export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS';
export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL';
+export const LIST_ADDER_RESET = 'LIST_ADDER_RESET';
+export const LIST_ADDER_SETUP = 'LIST_ADDER_SETUP';
+
+export const LIST_ADDER_LISTS_FETCH_REQUEST = 'LIST_ADDER_LISTS_FETCH_REQUEST';
+export const LIST_ADDER_LISTS_FETCH_SUCCESS = 'LIST_ADDER_LISTS_FETCH_SUCCESS';
+export const LIST_ADDER_LISTS_FETCH_FAIL = 'LIST_ADDER_LISTS_FETCH_FAIL';
+
export const fetchList = id => (dispatch, getState) => {
if (getState().getIn(['lists', id])) {
return;
accountId,
error,
});
+
+export const resetListAdder = () => ({
+ type: LIST_ADDER_RESET,
+});
+
+export const setupListAdder = accountId => (dispatch, getState) => {
+ dispatch({
+ type: LIST_ADDER_SETUP,
+ account: getState().getIn(['accounts', accountId]),
+ });
+ dispatch(fetchLists());
+ dispatch(fetchAccountLists(accountId));
+};
+
+export const fetchAccountLists = accountId => (dispatch, getState) => {
+ dispatch(fetchAccountListsRequest(accountId));
+
+ api(getState).get(`/api/v1/accounts/${accountId}/lists`)
+ .then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data)))
+ .catch(err => dispatch(fetchAccountListsFail(accountId, err)));
+};
+
+export const fetchAccountListsRequest = id => ({
+ type:LIST_ADDER_LISTS_FETCH_REQUEST,
+ id,
+});
+
+export const fetchAccountListsSuccess = (id, lists) => ({
+ type: LIST_ADDER_LISTS_FETCH_SUCCESS,
+ id,
+ lists,
+});
+
+export const fetchAccountListsFail = (id, err) => ({
+ type: LIST_ADDER_LISTS_FETCH_FAIL,
+ id,
+ err,
+});
+
+export const addToListAdder = listId => (dispatch, getState) => {
+ dispatch(addToList(listId, getState().getIn(['listAdder', 'accountId'])));
+};
+
+export const removeFromListAdder = listId => (dispatch, getState) => {
+ dispatch(removeFromList(listId, getState().getIn(['listAdder', 'accountId'])));
+};
+
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
+ add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
});
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
+ onAddToList: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
}
menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
+ menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
menu.push(null);
}
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
+ onAddToList: PropTypes.func.isRequired,
hideTabs: PropTypes.bool,
};
this.props.onEndorseToggle(this.props.account);
}
+ handleAddToList = () => {
+ this.props.onAddToList(this.props.account);
+ }
+
render () {
const { account, hideTabs } = this.props;
onBlockDomain={this.handleBlockDomain}
onUnblockDomain={this.handleUnblockDomain}
onEndorseToggle={this.handleEndorseToggle}
+ onAddToList={this.handleAddToList}
/>
{!hideTabs && (
dispatch(unblockDomain(domain));
},
+ onAddToList(account){
+ dispatch(openModal('LIST_ADDER', {
+ accountId: account.get('id'),
+ }));
+ },
+
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
--- /dev/null
+import React from 'react';
+import { connect } from 'react-redux';
+import { makeGetAccount } from '../../../selectors';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Avatar from '../../../components/avatar';
+import DisplayName from '../../../components/display_name';
+import { injectIntl } from 'react-intl';
+
+const makeMapStateToProps = () => {
+ const getAccount = makeGetAccount();
+
+ const mapStateToProps = (state, { accountId }) => ({
+ account: getAccount(state, accountId),
+ });
+
+ return mapStateToProps;
+};
+
+
+export default @connect(makeMapStateToProps)
+@injectIntl
+class Account extends ImmutablePureComponent {
+
+ static propTypes = {
+ account: ImmutablePropTypes.map.isRequired,
+ };
+
+ render () {
+ const { account } = this.props;
+ return (
+ <div className='account'>
+ <div className='account__wrapper'>
+ <div className='account__display-name'>
+ <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
+ <DisplayName account={account} />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+}
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import IconButton from '../../../components/icon_button';
+import { defineMessages, injectIntl } from 'react-intl';
+import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
+
+const messages = defineMessages({
+ remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
+ add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
+});
+
+const MapStateToProps = (state, { listId, added }) => ({
+ list: state.get('lists').get(listId),
+ added: typeof added === 'undefined' ? state.getIn(['listAdder', 'lists', 'items']).includes(listId) : added,
+});
+
+const mapDispatchToProps = (dispatch, { listId }) => ({
+ onRemove: () => dispatch(removeFromListAdder(listId)),
+ onAdd: () => dispatch(addToListAdder(listId)),
+});
+
+export default @connect(MapStateToProps, mapDispatchToProps)
+@injectIntl
+class List extends ImmutablePureComponent {
+
+ static propTypes = {
+ list: ImmutablePropTypes.map.isRequired,
+ intl: PropTypes.object.isRequired,
+ onRemove: PropTypes.func.isRequired,
+ onAdd: PropTypes.func.isRequired,
+ added: PropTypes.bool,
+ };
+
+ static defaultProps = {
+ added: false,
+ };
+
+ render () {
+ const { list, intl, onRemove, onAdd, added } = this.props;
+
+ let button;
+
+ if (added) {
+ button = <IconButton icon='times' title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
+ } else {
+ button = <IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={onAdd} />;
+ }
+
+ return (
+ <div className='list'>
+ <div className='list__wrapper'>
+ <div className='list__display-name'>
+ <i className='fa fa-fw fa-list-ul column-link__icon' />
+ {list.get('title')}
+ </div>
+
+ <div className='account__relationship'>
+ {button}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+}
--- /dev/null
+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 } from 'react-intl';
+import { setupListAdder, resetListAdder } from '../../actions/lists';
+import { createSelector } from 'reselect';
+import List from './components/list';
+import Account from './components/account';
+import NewListForm from '../lists/components/new_list_form';
+// hack
+
+const getOrderedLists = createSelector([state => state.get('lists')], lists => {
+ if (!lists) {
+ return lists;
+ }
+
+ return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
+});
+
+const mapStateToProps = state => ({
+ listIds: getOrderedLists(state).map(list=>list.get('id')),
+});
+
+const mapDispatchToProps = dispatch => ({
+ onInitialize: accountId => dispatch(setupListAdder(accountId)),
+ onReset: () => dispatch(resetListAdder()),
+});
+
+export default @connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+class ListAdder extends ImmutablePureComponent {
+
+ static propTypes = {
+ accountId: PropTypes.string.isRequired,
+ onClose: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ onInitialize: PropTypes.func.isRequired,
+ onReset: PropTypes.func.isRequired,
+ listIds: ImmutablePropTypes.list.isRequired,
+ };
+
+ componentDidMount () {
+ const { onInitialize, accountId } = this.props;
+ onInitialize(accountId);
+ }
+
+ componentWillUnmount () {
+ const { onReset } = this.props;
+ onReset();
+ }
+
+ render () {
+ const { accountId, listIds } = this.props;
+
+ return (
+ <div className='modal-root__modal list-adder'>
+ <div className='list-adder__account'>
+ <Account accountId={accountId} />
+ </div>
+
+ <NewListForm />
+
+
+ <div className='list-adder__lists'>
+ {listIds.map(ListId => <List key={ListId} listId={ListId} />)}
+ </div>
+ </div>
+ );
+ }
+
+}
SettingsModal,
EmbedModal,
ListEditor,
+ ListAdder,
PinnedAccountsEditor,
} from 'flavours/glitch/util/async-components';
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal,
'LIST_EDITOR': ListEditor,
+ 'LIST_ADDER':ListAdder,
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
'PINNED_ACCOUNTS_EDITOR': PinnedAccountsEditor,
};
import custom_emojis from './custom_emojis';
import lists from './lists';
import listEditor from './list_editor';
+import listAdder from './list_adder';
import filters from './filters';
import pinnedAccountsEditor from './pinned_accounts_editor';
custom_emojis,
lists,
listEditor,
+ listAdder,
filters,
pinnedAccountsEditor,
};
--- /dev/null
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+import {
+ LIST_ADDER_RESET,
+ LIST_ADDER_SETUP,
+ LIST_ADDER_LISTS_FETCH_REQUEST,
+ LIST_ADDER_LISTS_FETCH_SUCCESS,
+ LIST_ADDER_LISTS_FETCH_FAIL,
+ LIST_EDITOR_ADD_SUCCESS,
+ LIST_EDITOR_REMOVE_SUCCESS,
+} from '../actions/lists';
+
+const initialState = ImmutableMap({
+ accountId: null,
+
+ lists: ImmutableMap({
+ items: ImmutableList(),
+ loaded: false,
+ isLoading: false,
+ }),
+});
+
+export default function listAdderReducer(state = initialState, action) {
+ switch(action.type) {
+ case LIST_ADDER_RESET:
+ return initialState;
+ case LIST_ADDER_SETUP:
+ return state.withMutations(map => {
+ map.set('accountId', action.account.get('id'));
+ });
+ case LIST_ADDER_LISTS_FETCH_REQUEST:
+ return state.setIn(['lists', 'isLoading'], true);
+ case LIST_ADDER_LISTS_FETCH_FAIL:
+ return state.setIn(['lists', 'isLoading'], false);
+ case LIST_ADDER_LISTS_FETCH_SUCCESS:
+ return state.update('lists', lists => lists.withMutations(map => {
+ map.set('isLoading', false);
+ map.set('loaded', true);
+ map.set('items', ImmutableList(action.lists.map(item => item.id)));
+ }));
+ case LIST_EDITOR_ADD_SUCCESS:
+ return state.updateIn(['lists', 'items'], list => list.unshift(action.listId));
+ case LIST_EDITOR_REMOVE_SUCCESS:
+ return state.updateIn(['lists', 'items'], list => list.filterNot(item => item === action.listId));
+ default:
+ return state;
+ }
+};
margin-bottom: 0;
}
}
+
+.list-adder {
+ background: $ui-base-color;
+ flex-direction: column;
+ border-radius: 8px;
+ box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
+ width: 380px;
+ overflow: hidden;
+
+ @media screen and (max-width: 420px) {
+ width: 90%;
+ }
+
+ &__account {
+ background: lighten($ui-base-color, 13%);
+ }
+
+ &__lists {
+ background: lighten($ui-base-color, 13%);
+ height: 50vh;
+ border-radius: 0 0 8px 8px;
+ overflow-y: auto;
+ }
+
+ .list {
+ padding: 10px;
+ border-bottom: 1px solid lighten($ui-base-color, 8%);
+ }
+
+ .list__wrapper {
+ display: flex;
+ }
+
+ .list__display-name {
+ flex: 1 1 auto;
+ overflow: hidden;
+ text-decoration: none;
+ font-size: 16px;
+ padding: 10px;
+ }
+}
export function GettingStartedMisc () {
return import(/* webpackChunkName: "flavours/glitch/async/getting_started_misc" */'flavours/glitch/features/getting_started_misc');
}
+
+export function ListAdder () {
+ return import(/* webpackChunkName: "features/glitch/async/list_adder" */'flavours/glitch/features/list_adder');
+}