diff --git a/app/javascript/flavours/glitch/actions/lists.js b/app/javascript/flavours/glitch/actions/lists.js
index 3c3af5fee..3021fa5b5 100644
--- a/app/javascript/flavours/glitch/actions/lists.js
+++ b/app/javascript/flavours/glitch/actions/lists.js
@@ -40,6 +40,13 @@ export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST';
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;
@@ -311,3 +318,50 @@ export const removeFromListFail = (listId, accountId, error) => ({
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'])));
+};
+
diff --git a/app/javascript/flavours/glitch/features/account/components/action_bar.js b/app/javascript/flavours/glitch/features/account/components/action_bar.js
index 3d6eeb06a..acbe46f8a 100644
--- a/app/javascript/flavours/glitch/features/account/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js
@@ -25,6 +25,7 @@ const messages = defineMessages({
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}' },
});
@@ -43,6 +44,7 @@ export default class ActionBar extends React.PureComponent {
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
+ onAddToList: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
@@ -85,6 +87,7 @@ export default class ActionBar extends React.PureComponent {
}
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);
}
diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
index 89b9be92b..8dc0be93e 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
@@ -23,6 +23,7 @@ export default class Header extends ImmutablePureComponent {
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
+ onAddToList: PropTypes.func.isRequired,
hideTabs: PropTypes.bool,
};
@@ -78,6 +79,10 @@ export default class Header extends ImmutablePureComponent {
this.props.onEndorseToggle(this.props.account);
}
+ handleAddToList = () => {
+ this.props.onAddToList(this.props.account);
+ }
+
render () {
const { account, hideTabs } = this.props;
@@ -106,6 +111,7 @@ export default class Header extends ImmutablePureComponent {
onBlockDomain={this.handleBlockDomain}
onUnblockDomain={this.handleUnblockDomain}
onEndorseToggle={this.handleEndorseToggle}
+ onAddToList={this.handleAddToList}
/>
{!hideTabs && (
diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
index f5f56d85c..e333c31a1 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
@@ -120,6 +120,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(unblockDomain(domain));
},
+ onAddToList(account){
+ dispatch(openModal('LIST_ADDER', {
+ accountId: account.get('id'),
+ }));
+ },
+
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
diff --git a/app/javascript/flavours/glitch/features/list_adder/components/account.js b/app/javascript/flavours/glitch/features/list_adder/components/account.js
new file mode 100644
index 000000000..1369aac07
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/list_adder/components/account.js
@@ -0,0 +1,43 @@
+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 (
+
+ );
+ }
+
+}
diff --git a/app/javascript/flavours/glitch/features/list_adder/components/list.js b/app/javascript/flavours/glitch/features/list_adder/components/list.js
new file mode 100644
index 000000000..cb8eb7d7a
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/list_adder/components/list.js
@@ -0,0 +1,68 @@
+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 = ;
+ } else {
+ button = ;
+ }
+
+ return (
+
+
+
+
+ {list.get('title')}
+
+
+
+ {button}
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/flavours/glitch/features/list_adder/index.js b/app/javascript/flavours/glitch/features/list_adder/index.js
new file mode 100644
index 000000000..cb8a15e8c
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/list_adder/index.js
@@ -0,0 +1,73 @@
+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 (
+
+
+
+
+
+
+
+ {listIds.map(ListId =>
)}
+
+
+ );
+ }
+
+}
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 c9f54804a..303e05db6 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,
+ ListAdder,
PinnedAccountsEditor,
} from 'flavours/glitch/util/async-components';
@@ -36,6 +37,7 @@ const MODAL_COMPONENTS = {
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal,
'LIST_EDITOR': ListEditor,
+ 'LIST_ADDER':ListAdder,
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
'PINNED_ACCOUNTS_EDITOR': PinnedAccountsEditor,
};
diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js
index ae9e2b639..5b1ec4abc 100644
--- a/app/javascript/flavours/glitch/reducers/index.js
+++ b/app/javascript/flavours/glitch/reducers/index.js
@@ -26,6 +26,7 @@ import height_cache from './height_cache';
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';
@@ -57,6 +58,7 @@ const reducers = {
custom_emojis,
lists,
listEditor,
+ listAdder,
filters,
pinnedAccountsEditor,
};
diff --git a/app/javascript/flavours/glitch/reducers/list_adder.js b/app/javascript/flavours/glitch/reducers/list_adder.js
new file mode 100644
index 000000000..b8c1b0e26
--- /dev/null
+++ b/app/javascript/flavours/glitch/reducers/list_adder.js
@@ -0,0 +1,47 @@
+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;
+ }
+};
diff --git a/app/javascript/flavours/glitch/styles/components/lists.scss b/app/javascript/flavours/glitch/styles/components/lists.scss
index f5837c6c4..d00a1941b 100644
--- a/app/javascript/flavours/glitch/styles/components/lists.scss
+++ b/app/javascript/flavours/glitch/styles/components/lists.scss
@@ -51,3 +51,44 @@
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;
+ }
+}
diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js
index 557ce317e..e96af845f 100644
--- a/app/javascript/flavours/glitch/util/async-components.js
+++ b/app/javascript/flavours/glitch/util/async-components.js
@@ -145,3 +145,7 @@ export function EmbedModal () {
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');
+}