Display list column (#5750)
This commit is contained in:
		
							parent
							
								
									269a445c0b
								
							
						
					
					
						commit
						31ac5f0e00
					
				
					 10 changed files with 166 additions and 1 deletions
				
			
		
							
								
								
									
										28
									
								
								app/javascript/mastodon/actions/lists.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/javascript/mastodon/actions/lists.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | |||
| import api from '../api'; | ||||
| 
 | ||||
| export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; | ||||
| export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; | ||||
| export const LIST_FETCH_FAIL    = 'LIST_FETCH_FAIL'; | ||||
| 
 | ||||
| export const fetchList = id => (dispatch, getState) => { | ||||
|   dispatch(fetchListRequest(id)); | ||||
| 
 | ||||
|   api(getState).get(`/api/v1/lists/${id}`) | ||||
|     .then(({ data }) => dispatch(fetchListSuccess(data))) | ||||
|     .catch(err => dispatch(fetchListFail(err))); | ||||
| }; | ||||
| 
 | ||||
| export const fetchListRequest = id => ({ | ||||
|   type: LIST_FETCH_REQUEST, | ||||
|   id, | ||||
| }); | ||||
| 
 | ||||
| export const fetchListSuccess = list => ({ | ||||
|   type: LIST_FETCH_SUCCESS, | ||||
|   list, | ||||
| }); | ||||
| 
 | ||||
| export const fetchListFail = error => ({ | ||||
|   type: LIST_FETCH_FAIL, | ||||
|   error, | ||||
| }); | ||||
|  | @ -51,3 +51,4 @@ export const connectCommunityStream = () => connectTimelineStream('community', ' | |||
| export const connectMediaStream = () => connectTimelineStream('community', 'public:local'); | ||||
| export const connectPublicStream = () => connectTimelineStream('public', 'public'); | ||||
| export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`); | ||||
| export const connectListStream = (id) => connectTimelineStream(`list:${id}`, `list&list=${id}`); | ||||
|  |  | |||
|  | @ -118,6 +118,7 @@ export const refreshCommunityTimeline    = () => refreshTimeline('community', '/ | |||
| export const refreshAccountTimeline      = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); | ||||
| export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true }); | ||||
| export const refreshHashtagTimeline      = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); | ||||
| export const refreshListTimeline         = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`); | ||||
| 
 | ||||
| export function refreshTimelineFail(timeline, error, skipLoading) { | ||||
|   return { | ||||
|  | @ -158,6 +159,7 @@ export const expandCommunityTimeline    = () => expandTimeline('community', '/ap | |||
| export const expandAccountTimeline      = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); | ||||
| export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true }); | ||||
| export const expandHashtagTimeline      = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); | ||||
| export const expandListTimeline         = id => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`); | ||||
| 
 | ||||
| export function expandTimelineRequest(timeline) { | ||||
|   return { | ||||
|  |  | |||
							
								
								
									
										106
									
								
								app/javascript/mastodon/features/list_timeline/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								app/javascript/mastodon/features/list_timeline/index.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,106 @@ | |||
| import React from 'react'; | ||||
| import { connect } from 'react-redux'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import StatusListContainer from '../ui/containers/status_list_container'; | ||||
| import Column from '../../components/column'; | ||||
| import ColumnHeader from '../../components/column_header'; | ||||
| import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import { connectListStream } from '../../actions/streaming'; | ||||
| import { refreshListTimeline, expandListTimeline } from '../../actions/timelines'; | ||||
| import { fetchList } from '../../actions/lists'; | ||||
| 
 | ||||
| const mapStateToProps = (state, props) => ({ | ||||
|   list: state.getIn(['lists', props.params.id]), | ||||
|   hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0, | ||||
| }); | ||||
| 
 | ||||
| @connect(mapStateToProps) | ||||
| export default class ListTimeline extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     params: PropTypes.object.isRequired, | ||||
|     dispatch: PropTypes.func.isRequired, | ||||
|     columnId: PropTypes.string, | ||||
|     hasUnread: PropTypes.bool, | ||||
|     multiColumn: PropTypes.bool, | ||||
|     list: ImmutablePropTypes.map, | ||||
|   }; | ||||
| 
 | ||||
|   handlePin = () => { | ||||
|     const { columnId, dispatch } = this.props; | ||||
| 
 | ||||
|     if (columnId) { | ||||
|       dispatch(removeColumn(columnId)); | ||||
|     } else { | ||||
|       dispatch(addColumn('LIST', { id: this.props.params.id })); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleMove = (dir) => { | ||||
|     const { columnId, dispatch } = this.props; | ||||
|     dispatch(moveColumn(columnId, dir)); | ||||
|   } | ||||
| 
 | ||||
|   handleHeaderClick = () => { | ||||
|     this.column.scrollTop(); | ||||
|   } | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|     const { dispatch } = this.props; | ||||
|     const { id } = this.props.params; | ||||
| 
 | ||||
|     dispatch(fetchList(id)); | ||||
|     dispatch(refreshListTimeline(id)); | ||||
| 
 | ||||
|     this.disconnect = dispatch(connectListStream(id)); | ||||
|   } | ||||
| 
 | ||||
|   componentWillUnmount () { | ||||
|     if (this.disconnect) { | ||||
|       this.disconnect(); | ||||
|       this.disconnect = null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setRef = c => { | ||||
|     this.column = c; | ||||
|   } | ||||
| 
 | ||||
|   handleLoadMore = () => { | ||||
|     const { id } = this.props.params; | ||||
|     this.props.dispatch(expandListTimeline(id)); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { hasUnread, columnId, multiColumn, list } = this.props; | ||||
|     const { id } = this.props.params; | ||||
|     const pinned = !!columnId; | ||||
|     const title  = list ? list.get('title') : id; | ||||
| 
 | ||||
|     return ( | ||||
|       <Column ref={this.setRef}> | ||||
|         <ColumnHeader | ||||
|           icon='bars' | ||||
|           active={hasUnread} | ||||
|           title={title} | ||||
|           onPin={this.handlePin} | ||||
|           onMove={this.handleMove} | ||||
|           onClick={this.handleHeaderClick} | ||||
|           pinned={pinned} | ||||
|           multiColumn={multiColumn} | ||||
|         /> | ||||
| 
 | ||||
|         <StatusListContainer | ||||
|           trackScroll={!pinned} | ||||
|           scrollKey={`list_timeline-${columnId}`} | ||||
|           timelineId={`list:${id}`} | ||||
|           loadMore={this.handleLoadMore} | ||||
|           emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet.' />} | ||||
|         /> | ||||
|       </Column> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -11,7 +11,7 @@ import BundleContainer from '../containers/bundle_container'; | |||
| import ColumnLoading from './column_loading'; | ||||
| import DrawerLoading from './drawer_loading'; | ||||
| import BundleColumnError from './bundle_column_error'; | ||||
| import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components'; | ||||
| import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components'; | ||||
| 
 | ||||
| import detectPassiveEvents from 'detect-passive-events'; | ||||
| import { scrollRight } from '../../../scroll'; | ||||
|  | @ -24,6 +24,7 @@ const componentMap = { | |||
|   'COMMUNITY': CommunityTimeline, | ||||
|   'HASHTAG': HashtagTimeline, | ||||
|   'FAVOURITES': FavouritedStatuses, | ||||
|   'LIST': ListTimeline, | ||||
| }; | ||||
| 
 | ||||
| @component => injectIntl(component, { withRef: true }) | ||||
|  |  | |||
|  | @ -33,6 +33,7 @@ import { | |||
|   FollowRequests, | ||||
|   GenericNotFound, | ||||
|   FavouritedStatuses, | ||||
|   ListTimeline, | ||||
|   Blocks, | ||||
|   Mutes, | ||||
|   PinnedStatuses, | ||||
|  | @ -372,6 +373,7 @@ export default class UI extends React.Component { | |||
|               <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} /> | ||||
|               <WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} /> | ||||
|               <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} /> | ||||
|               <WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} /> | ||||
| 
 | ||||
|               <WrappedRoute path='/notifications' component={Notifications} content={children} /> | ||||
|               <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} /> | ||||
|  |  | |||
|  | @ -26,6 +26,10 @@ export function HashtagTimeline () { | |||
|   return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline'); | ||||
| } | ||||
| 
 | ||||
| export function ListTimeline () { | ||||
|   return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline'); | ||||
| } | ||||
| 
 | ||||
| export function Status () { | ||||
|   return import(/* webpackChunkName: "features/status" */'../../status'); | ||||
| } | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ import media_attachments from './media_attachments'; | |||
| import notifications from './notifications'; | ||||
| import height_cache from './height_cache'; | ||||
| import custom_emojis from './custom_emojis'; | ||||
| import lists from './lists'; | ||||
| 
 | ||||
| const reducers = { | ||||
|   timelines, | ||||
|  | @ -47,6 +48,7 @@ const reducers = { | |||
|   notifications, | ||||
|   height_cache, | ||||
|   custom_emojis, | ||||
|   lists, | ||||
| }; | ||||
| 
 | ||||
| export default combineReducers(reducers); | ||||
|  |  | |||
							
								
								
									
										15
									
								
								app/javascript/mastodon/reducers/lists.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/javascript/mastodon/reducers/lists.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| import { LIST_FETCH_SUCCESS } from '../actions/lists'; | ||||
| import { Map as ImmutableMap, fromJS } from 'immutable'; | ||||
| 
 | ||||
| const initialState = ImmutableMap(); | ||||
| 
 | ||||
| const normalizeList = (state, list) => state.set(list.id, fromJS(list)); | ||||
| 
 | ||||
| export default function lists(state = initialState, action) { | ||||
|   switch(action.type) { | ||||
|   case LIST_FETCH_SUCCESS: | ||||
|     return normalizeList(state, action.list); | ||||
|   default: | ||||
|     return state; | ||||
|   } | ||||
| }; | ||||
|  | @ -2,4 +2,8 @@ | |||
| 
 | ||||
| class REST::ListSerializer < ActiveModel::Serializer | ||||
|   attributes :id, :title | ||||
| 
 | ||||
|   def id | ||||
|     object.id.to_s | ||||
|   end | ||||
| end | ||||
|  |  | |||
		Reference in a new issue