[Glitch] Implement adding a user to a list from their profile
Port bb5558de62 to glitch-soc
			
			
This commit is contained in:
		| @@ -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']))); | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|       } | ||||
|  | ||||
|   | ||||
| @@ -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 && ( | ||||
|   | ||||
| @@ -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)); | ||||
|   | ||||
| @@ -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 ( | ||||
|       <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> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -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 = <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> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										73
									
								
								app/javascript/flavours/glitch/features/list_adder/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								app/javascript/flavours/glitch/features/list_adder/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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 ( | ||||
|       <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> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -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, | ||||
| }; | ||||
|   | ||||
| @@ -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, | ||||
| }; | ||||
|   | ||||
							
								
								
									
										47
									
								
								app/javascript/flavours/glitch/reducers/list_adder.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								app/javascript/flavours/glitch/reducers/list_adder.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
|   } | ||||
| }; | ||||
| @@ -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; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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'); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user