Add account media gallery view to web UI (#3120)
* Add account media gallery view to web UI * Link media view from account dropdown * Adjust link
This commit is contained in:
		
					parent
					
						
							
								b369fc2de4
							
						
					
				
			
			
				commit
				
					
						de475cf8d3
					
				
			
		
					 14 changed files with 381 additions and 20 deletions
				
			
		|  | @ -37,6 +37,14 @@ export const ACCOUNT_TIMELINE_EXPAND_REQUEST = 'ACCOUNT_TIMELINE_EXPAND_REQUEST' | |||
| export const ACCOUNT_TIMELINE_EXPAND_SUCCESS = 'ACCOUNT_TIMELINE_EXPAND_SUCCESS'; | ||||
| export const ACCOUNT_TIMELINE_EXPAND_FAIL    = 'ACCOUNT_TIMELINE_EXPAND_FAIL'; | ||||
| 
 | ||||
| export const ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST = 'ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST'; | ||||
| export const ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS = 'ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS'; | ||||
| export const ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL    = 'ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL'; | ||||
| 
 | ||||
| export const ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST = 'ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST'; | ||||
| export const ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS = 'ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS'; | ||||
| export const ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL    = 'ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL'; | ||||
| 
 | ||||
| export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST'; | ||||
| export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS'; | ||||
| export const FOLLOWERS_FETCH_FAIL    = 'FOLLOWERS_FETCH_FAIL'; | ||||
|  | @ -96,17 +104,17 @@ export function fetchAccountTimeline(id, replace = false) { | |||
|     const ids      = getState().getIn(['timelines', 'accounts_timelines', id, 'items'], Immutable.List()); | ||||
|     const newestId = ids.size > 0 ? ids.first() : null; | ||||
| 
 | ||||
|     let params = ''; | ||||
|     let params = {}; | ||||
|     let skipLoading = false; | ||||
| 
 | ||||
|     if (newestId !== null && !replace) { | ||||
|       params      = `?since_id=${newestId}`; | ||||
|       params.since_id = newestId; | ||||
|       skipLoading = true; | ||||
|     } | ||||
| 
 | ||||
|     dispatch(fetchAccountTimelineRequest(id, skipLoading)); | ||||
| 
 | ||||
|     api(getState).get(`/api/v1/accounts/${id}/statuses${params}`).then(response => { | ||||
|     api(getState).get(`/api/v1/accounts/${id}/statuses`, { params }).then(response => { | ||||
|       dispatch(fetchAccountTimelineSuccess(id, response.data, replace, skipLoading)); | ||||
|     }).catch(error => { | ||||
|       dispatch(fetchAccountTimelineFail(id, error, skipLoading)); | ||||
|  | @ -114,6 +122,29 @@ export function fetchAccountTimeline(id, replace = false) { | |||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function fetchAccountMediaTimeline(id, replace = false) { | ||||
|   return (dispatch, getState) => { | ||||
|     const ids      = getState().getIn(['timelines', 'accounts_media_timelines', id, 'items'], Immutable.List()); | ||||
|     const newestId = ids.size > 0 ? ids.first() : null; | ||||
| 
 | ||||
|     let params = { only_media: 'true', limit: 12 }; | ||||
|     let skipLoading = false; | ||||
| 
 | ||||
|     if (newestId !== null && !replace) { | ||||
|       params.since_id = newestId; | ||||
|       skipLoading = true; | ||||
|     } | ||||
| 
 | ||||
|     dispatch(fetchAccountMediaTimelineRequest(id, skipLoading)); | ||||
| 
 | ||||
|     api(getState).get(`/api/v1/accounts/${id}/statuses`, { params }).then(response => { | ||||
|       dispatch(fetchAccountMediaTimelineSuccess(id, response.data, replace, skipLoading)); | ||||
|     }).catch(error => { | ||||
|       dispatch(fetchAccountMediaTimelineFail(id, error, skipLoading)); | ||||
|     }); | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function expandAccountTimeline(id) { | ||||
|   return (dispatch, getState) => { | ||||
|     const lastId = getState().getIn(['timelines', 'accounts_timelines', id, 'items'], Immutable.List()).last(); | ||||
|  | @ -134,6 +165,27 @@ export function expandAccountTimeline(id) { | |||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function expandAccountMediaTimeline(id) { | ||||
|   return (dispatch, getState) => { | ||||
|     const lastId = getState().getIn(['timelines', 'accounts_media_timelines', id, 'items'], Immutable.List()).last(); | ||||
| 
 | ||||
|     dispatch(expandAccountMediaTimelineRequest(id)); | ||||
| 
 | ||||
|     api(getState).get(`/api/v1/accounts/${id}/statuses`, { | ||||
|       params: { | ||||
|         limit: 12, | ||||
|         only_media: 'true', | ||||
|         max_id: lastId | ||||
|       } | ||||
|     }).then(response => { | ||||
|       const next = getLinks(response).refs.find(link => link.rel === 'next'); | ||||
|       dispatch(expandAccountMediaTimelineSuccess(id, response.data, next)); | ||||
|     }).catch(error => { | ||||
|       dispatch(expandAccountMediaTimelineFail(id, error)); | ||||
|     }); | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function fetchAccountRequest(id) { | ||||
|   return { | ||||
|     type: ACCOUNT_FETCH_REQUEST, | ||||
|  | @ -251,6 +303,34 @@ export function fetchAccountTimelineFail(id, error, skipLoading) { | |||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function fetchAccountMediaTimelineRequest(id, skipLoading) { | ||||
|   return { | ||||
|     type: ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST, | ||||
|     id, | ||||
|     skipLoading | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function fetchAccountMediaTimelineSuccess(id, statuses, replace, skipLoading) { | ||||
|   return { | ||||
|     type: ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS, | ||||
|     id, | ||||
|     statuses, | ||||
|     replace, | ||||
|     skipLoading | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function fetchAccountMediaTimelineFail(id, error, skipLoading) { | ||||
|   return { | ||||
|     type: ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL, | ||||
|     id, | ||||
|     error, | ||||
|     skipLoading, | ||||
|     skipAlert: error.response.status === 404 | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function expandAccountTimelineRequest(id) { | ||||
|   return { | ||||
|     type: ACCOUNT_TIMELINE_EXPAND_REQUEST, | ||||
|  | @ -275,6 +355,30 @@ export function expandAccountTimelineFail(id, error) { | |||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function expandAccountMediaTimelineRequest(id) { | ||||
|   return { | ||||
|     type: ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST, | ||||
|     id | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function expandAccountMediaTimelineSuccess(id, statuses, next) { | ||||
|   return { | ||||
|     type: ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS, | ||||
|     id, | ||||
|     statuses, | ||||
|     next | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function expandAccountMediaTimelineFail(id, error) { | ||||
|   return { | ||||
|     type: ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL, | ||||
|     id, | ||||
|     error | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function blockAccount(id) { | ||||
|   return (dispatch, getState) => { | ||||
|     dispatch(blockAccountRequest(id)); | ||||
|  |  | |||
|  | @ -4,6 +4,10 @@ import PropTypes from 'prop-types'; | |||
| 
 | ||||
| class DropdownMenu extends React.PureComponent { | ||||
| 
 | ||||
|   static contextTypes = { | ||||
|     router: PropTypes.object | ||||
|   }; | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     icon: PropTypes.string.isRequired, | ||||
|     items: PropTypes.array.isRequired, | ||||
|  | @ -26,13 +30,17 @@ class DropdownMenu extends React.PureComponent { | |||
| 
 | ||||
|   handleClick = (e) => { | ||||
|     const i = Number(e.currentTarget.getAttribute('data-index')); | ||||
|     const { action } = this.props.items[i]; | ||||
|     const { action, to } = this.props.items[i]; | ||||
| 
 | ||||
|     e.preventDefault(); | ||||
| 
 | ||||
|     if (typeof action === 'function') { | ||||
|       e.preventDefault(); | ||||
|       action(); | ||||
|       this.dropdown.hide(); | ||||
|     } else if (to) { | ||||
|       this.context.router.push(to); | ||||
|     } | ||||
| 
 | ||||
|     this.dropdown.hide(); | ||||
|   } | ||||
| 
 | ||||
|   renderItem = (item, i) => { | ||||
|  |  | |||
|  | @ -26,6 +26,7 @@ import GettingStarted from '../features/getting_started'; | |||
| import PublicTimeline from '../features/public_timeline'; | ||||
| import CommunityTimeline from '../features/community_timeline'; | ||||
| import AccountTimeline from '../features/account_timeline'; | ||||
| import AccountGallery from '../features/account_gallery'; | ||||
| import HomeTimeline from '../features/home_timeline'; | ||||
| import Compose from '../features/compose'; | ||||
| import Followers from '../features/followers'; | ||||
|  | @ -204,6 +205,7 @@ class Mastodon extends React.PureComponent { | |||
|               <Route path='accounts/:accountId' component={AccountTimeline} /> | ||||
|               <Route path='accounts/:accountId/followers' component={Followers} /> | ||||
|               <Route path='accounts/:accountId/following' component={Following} /> | ||||
|               <Route path='accounts/:accountId/media' component={AccountGallery} /> | ||||
| 
 | ||||
|               <Route path='follow_requests' component={FollowRequests} /> | ||||
|               <Route path='blocks' component={Blocks} /> | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ const messages = defineMessages({ | |||
|   mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, | ||||
|   follow: { id: 'account.follow', defaultMessage: 'Follow' }, | ||||
|   report: { id: 'account.report', defaultMessage: 'Report @{name}' }, | ||||
|   media: { id: 'account.media', defaultMessage: 'Media' }, | ||||
|   disclaimer: { id: 'account.disclaimer', defaultMessage: 'This user is from another instance. This number may be larger.' }, | ||||
|   blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, | ||||
|   unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, | ||||
|  | @ -43,6 +44,8 @@ class ActionBar extends React.PureComponent { | |||
| 
 | ||||
|     menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); | ||||
|     menu.push(null); | ||||
|     menu.push({ text: intl.formatMessage(messages.media), to: `/accounts/${account.get('id')}/media` }); | ||||
|     menu.push(null); | ||||
| 
 | ||||
|     if (account.get('id') === me) { | ||||
|       menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); | ||||
|  |  | |||
|  | @ -0,0 +1,39 @@ | |||
| import React from 'react'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| import Permalink from '../../../components/permalink'; | ||||
| 
 | ||||
| class MediaItem extends ImmutablePureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     media: ImmutablePropTypes.map.isRequired | ||||
|   }; | ||||
| 
 | ||||
|   render () { | ||||
|     const { media } = this.props; | ||||
|     const status = media.get('status'); | ||||
| 
 | ||||
|     let content, style; | ||||
| 
 | ||||
|     if (media.get('type') === 'gifv') { | ||||
|       content = <span className='media-gallery__gifv__label'>GIF</span>; | ||||
|     } | ||||
| 
 | ||||
|     if (!status.get('sensitive')) { | ||||
|       style = { backgroundImage: `url(${media.get('preview_url')})` }; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className='account-gallery__item'> | ||||
|         <Permalink | ||||
|           to={`/statuses/${status.get('id')}`} | ||||
|           href={status.get('url')} | ||||
|           style={style}> | ||||
|           {content} | ||||
|         </Permalink> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default MediaItem; | ||||
							
								
								
									
										99
									
								
								app/javascript/mastodon/features/account_gallery/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								app/javascript/mastodon/features/account_gallery/index.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,99 @@ | |||
| import React from 'react'; | ||||
| import { connect } from 'react-redux'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { | ||||
|   fetchAccount, | ||||
|   fetchAccountMediaTimeline, | ||||
|   expandAccountMediaTimeline | ||||
| } from '../../actions/accounts'; | ||||
| import LoadingIndicator from '../../components/loading_indicator'; | ||||
| import Column from '../ui/components/column'; | ||||
| import ColumnBackButton from '../../components/column_back_button'; | ||||
| import Immutable from 'immutable'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| import { getAccountGallery } from '../../selectors'; | ||||
| import MediaItem from './components/media_item'; | ||||
| import HeaderContainer from '../account_timeline/containers/header_container'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import { ScrollContainer } from 'react-router-scroll'; | ||||
| 
 | ||||
| const mapStateToProps = (state, props) => ({ | ||||
|   medias: getAccountGallery(state, Number(props.params.accountId)), | ||||
|   isLoading: state.getIn(['timelines', 'accounts_media_timelines', Number(props.params.accountId), 'isLoading']), | ||||
|   hasMore: !!state.getIn(['timelines', 'accounts_media_timelines', Number(props.params.accountId), 'next']), | ||||
|   autoPlayGif: state.getIn(['meta', 'auto_play_gif']), | ||||
| }); | ||||
| 
 | ||||
| class AccountGallery extends ImmutablePureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     params: PropTypes.object.isRequired, | ||||
|     dispatch: PropTypes.func.isRequired, | ||||
|     medias: ImmutablePropTypes.list.isRequired, | ||||
|     isLoading: PropTypes.bool, | ||||
|     hasMore: PropTypes.bool, | ||||
|     autoPlayGif: PropTypes.bool, | ||||
|   }; | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|     this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); | ||||
|     this.props.dispatch(fetchAccountMediaTimeline(Number(this.props.params.accountId))); | ||||
|   } | ||||
| 
 | ||||
|   componentWillReceiveProps (nextProps) { | ||||
|     if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { | ||||
|       this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); | ||||
|       this.props.dispatch(fetchAccountMediaTimeline(Number(this.props.params.accountId))); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleScroll = (e) => { | ||||
|     const { scrollTop, scrollHeight, clientHeight } = e.target; | ||||
| 
 | ||||
|     if (scrollTop === scrollHeight - clientHeight) { | ||||
|       this.props.dispatch(expandAccountMediaTimeline(Number(this.props.params.accountId))); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { medias, autoPlayGif, isLoading } = this.props; | ||||
| 
 | ||||
|     if (!medias && isLoading) { | ||||
|       return ( | ||||
|         <Column> | ||||
|           <LoadingIndicator /> | ||||
|         </Column> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <Column> | ||||
|         <ColumnBackButton /> | ||||
| 
 | ||||
|         <ScrollContainer scrollKey='account_gallery'> | ||||
|           <div className='scrollable' onScroll={this.handleScroll}> | ||||
|             <HeaderContainer accountId={this.props.params.accountId} /> | ||||
| 
 | ||||
|             <div className='account-section-headline'> | ||||
|               <FormattedMessage id='account.media' defaultMessage='Media' /> | ||||
|             </div> | ||||
| 
 | ||||
|             <div className='account-gallery__container'> | ||||
|               {medias.map(media => | ||||
|                 <MediaItem | ||||
|                   key={media.get('id')} | ||||
|                   media={media} | ||||
|                   autoPlayGif={autoPlayGif} | ||||
|                 /> | ||||
|               )} | ||||
|             </div> | ||||
|           </div> | ||||
|         </ScrollContainer> | ||||
|       </Column> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export default connect(mapStateToProps)(AccountGallery); | ||||
|  | @ -28,16 +28,11 @@ class Blocks extends ImmutablePureComponent { | |||
|     intl: PropTypes.object.isRequired | ||||
|   }; | ||||
| 
 | ||||
|   constructor (props, context) { | ||||
|     super(props, context); | ||||
|     this.handleScroll = this.handleScroll.bind(this); | ||||
|   } | ||||
| 
 | ||||
|   componentWillMount () { | ||||
|     this.props.dispatch(fetchBlocks()); | ||||
|   } | ||||
| 
 | ||||
|   handleScroll (e) { | ||||
|   handleScroll = (e) => { | ||||
|     const { scrollTop, scrollHeight, clientHeight } = e.target; | ||||
| 
 | ||||
|     if (scrollTop === scrollHeight - clientHeight) { | ||||
|  |  | |||
|  | @ -21,16 +21,11 @@ const mapStateToProps = state => ({ | |||
| 
 | ||||
| class Mutes extends ImmutablePureComponent { | ||||
| 
 | ||||
|   constructor (props, context) { | ||||
|     super(props, context); | ||||
|     this.handleScroll = this.handleScroll.bind(this); | ||||
|   } | ||||
| 
 | ||||
|   componentWillMount () { | ||||
|     this.props.dispatch(fetchMutes()); | ||||
|   } | ||||
| 
 | ||||
|   handleScroll (e) { | ||||
|   handleScroll = (e) => { | ||||
|     const { scrollTop, scrollHeight, clientHeight } = e.target; | ||||
| 
 | ||||
|     if (scrollTop === scrollHeight - clientHeight) { | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ | |||
|   "account.followers": "フォロワー", | ||||
|   "account.follows": "フォロー", | ||||
|   "account.follows_you": "フォローされています", | ||||
|   "account.media": "Media", | ||||
|   "account.media": "メディア", | ||||
|   "account.mention": "返信", | ||||
|   "account.mute": "ミュート", | ||||
|   "account.posts": "投稿", | ||||
|  |  | |||
|  | @ -23,6 +23,8 @@ import { | |||
| import { | ||||
|   ACCOUNT_TIMELINE_FETCH_SUCCESS, | ||||
|   ACCOUNT_TIMELINE_EXPAND_SUCCESS, | ||||
|   ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS, | ||||
|   ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS, | ||||
|   ACCOUNT_BLOCK_SUCCESS | ||||
| } from '../actions/accounts'; | ||||
| import { | ||||
|  | @ -113,6 +115,8 @@ export default function statuses(state = initialState, action) { | |||
|   case TIMELINE_EXPAND_SUCCESS: | ||||
|   case ACCOUNT_TIMELINE_FETCH_SUCCESS: | ||||
|   case ACCOUNT_TIMELINE_EXPAND_SUCCESS: | ||||
|   case ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS: | ||||
|   case ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS: | ||||
|   case CONTEXT_FETCH_SUCCESS: | ||||
|   case NOTIFICATIONS_REFRESH_SUCCESS: | ||||
|   case NOTIFICATIONS_EXPAND_SUCCESS: | ||||
|  |  | |||
|  | @ -24,6 +24,12 @@ import { | |||
|   ACCOUNT_TIMELINE_EXPAND_REQUEST, | ||||
|   ACCOUNT_TIMELINE_EXPAND_SUCCESS, | ||||
|   ACCOUNT_TIMELINE_EXPAND_FAIL, | ||||
|   ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST, | ||||
|   ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS, | ||||
|   ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL, | ||||
|   ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST, | ||||
|   ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS, | ||||
|   ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL, | ||||
|   ACCOUNT_BLOCK_SUCCESS, | ||||
|   ACCOUNT_MUTE_SUCCESS | ||||
| } from '../actions/accounts'; | ||||
|  | @ -79,6 +85,7 @@ const initialState = Immutable.Map({ | |||
|   }), | ||||
| 
 | ||||
|   accounts_timelines: Immutable.Map(), | ||||
|   accounts_media_timelines: Immutable.Map(), | ||||
|   ancestors: Immutable.Map(), | ||||
|   descendants: Immutable.Map() | ||||
| }); | ||||
|  | @ -148,6 +155,20 @@ const normalizeAccountTimeline = (state, accountId, statuses, replace = false) = | |||
|     .update('items', Immutable.List(), list => (replace ? ids : ids.concat(list)))); | ||||
| }; | ||||
| 
 | ||||
| const normalizeAccountMediaTimeline = (state, accountId, statuses, next) => { | ||||
|   let ids = Immutable.List(); | ||||
| 
 | ||||
|   statuses.forEach((status, i) => { | ||||
|     state = normalizeStatus(state, status); | ||||
|     ids   = ids.set(i, status.get('id')); | ||||
|   }); | ||||
| 
 | ||||
|   return state.updateIn(['accounts_media_timelines', accountId], Immutable.Map(), map => map | ||||
|     .set('isLoading', false) | ||||
|     .set('next', next) | ||||
|     .update('items', Immutable.List(), list => ids.concat(list))); | ||||
| }; | ||||
| 
 | ||||
| const appendNormalizedAccountTimeline = (state, accountId, statuses, next) => { | ||||
|   let moreIds = Immutable.List([]); | ||||
| 
 | ||||
|  | @ -162,6 +183,20 @@ const appendNormalizedAccountTimeline = (state, accountId, statuses, next) => { | |||
|     .update('items', list => list.concat(moreIds))); | ||||
| }; | ||||
| 
 | ||||
| const appendNormalizedAccountMediaTimeline = (state, accountId, statuses, next) => { | ||||
|   let moreIds = Immutable.List([]); | ||||
| 
 | ||||
|   statuses.forEach((status, i) => { | ||||
|     state   = normalizeStatus(state, status); | ||||
|     moreIds = moreIds.set(i, status.get('id')); | ||||
|   }); | ||||
| 
 | ||||
|   return state.updateIn(['accounts_media_timelines', accountId], Immutable.Map(), map => map | ||||
|     .set('isLoading', false) | ||||
|     .set('next', next) | ||||
|     .update('items', list => list.concat(moreIds))); | ||||
| }; | ||||
| 
 | ||||
| const updateTimeline = (state, timeline, status, references) => { | ||||
|   const top = state.getIn([timeline, 'top']); | ||||
| 
 | ||||
|  | @ -205,6 +240,7 @@ const deleteStatus = (state, id, accountId, references, reblogOf) => { | |||
| 
 | ||||
|   // Remove references from account timelines
 | ||||
|   state = state.updateIn(['accounts_timelines', accountId, 'items'], Immutable.List([]), list => list.filterNot(item => item === id)); | ||||
|   state = state.updateIn(['accounts_media_timelines', accountId, 'items'], Immutable.List([]), list => list.filterNot(item => item === id)); | ||||
| 
 | ||||
|   // Remove references from context
 | ||||
|   state.getIn(['descendants', id], Immutable.List()).forEach(descendantId => { | ||||
|  | @ -302,6 +338,16 @@ export default function timelines(state = initialState, action) { | |||
|     return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace); | ||||
|   case ACCOUNT_TIMELINE_EXPAND_SUCCESS: | ||||
|     return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.next); | ||||
|   case ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST: | ||||
|   case ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST: | ||||
|     return state.updateIn(['accounts_media_timelines', action.id], Immutable.Map(), map => map.set('isLoading', true)); | ||||
|   case ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL: | ||||
|   case ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL: | ||||
|     return state.updateIn(['accounts_media_timelines', action.id], Immutable.Map(), map => map.set('isLoading', false)); | ||||
|   case ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS: | ||||
|     return normalizeAccountMediaTimeline(state, action.id, Immutable.fromJS(action.statuses), action.next); | ||||
|   case ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS: | ||||
|     return appendNormalizedAccountMediaTimeline(state, action.id, Immutable.fromJS(action.statuses), action.next); | ||||
|   case ACCOUNT_BLOCK_SUCCESS: | ||||
|   case ACCOUNT_MUTE_SUCCESS: | ||||
|     return filterTimelines(state, action.relationship, action.statuses); | ||||
|  |  | |||
|  | @ -74,3 +74,17 @@ export const makeGetNotification = () => { | |||
|     return base.set('account', account); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| export const getAccountGallery = createSelector([ | ||||
|   (state, id) => state.getIn(['timelines', 'accounts_media_timelines', id, 'items'], Immutable.List()), | ||||
|   state       => state.get('statuses'), | ||||
| ], (statusIds, statuses) => { | ||||
|   let medias = Immutable.List(); | ||||
| 
 | ||||
|   statusIds.forEach(statusId => { | ||||
|     const status = statuses.get(statusId); | ||||
|     medias = medias.concat(status.get('media_attachments').map(media => media.set('status', status))); | ||||
|   }); | ||||
| 
 | ||||
|   return medias; | ||||
| }); | ||||
|  |  | |||
|  | @ -3427,3 +3427,55 @@ button.icon-button.active i.fa-retweet { | |||
|   transform: translate(-50%, -50%); | ||||
| } | ||||
| /* End Video Player */ | ||||
| 
 | ||||
| .account-gallery__container { | ||||
|   margin: -2px; | ||||
|   padding: 4px; | ||||
| } | ||||
| 
 | ||||
| .account-gallery__item { | ||||
|   float: left; | ||||
|   width: 96px; | ||||
|   height: 96px; | ||||
|   margin: 2px; | ||||
| 
 | ||||
|   a { | ||||
|     display: block; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     background-color: $base-overlay-background; | ||||
|     background-size: cover; | ||||
|     background-position: center; | ||||
|     position: relative; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .account-section-headline { | ||||
|   color: lighten($ui-base-color, 26%); | ||||
|   background: lighten($ui-base-color, 2%); | ||||
|   border-bottom: 1px solid lighten($ui-base-color, 4%); | ||||
|   padding: 15px 10px; | ||||
|   font-size: 14px; | ||||
|   font-weight: 500; | ||||
|   position: relative; | ||||
|   cursor: default; | ||||
| 
 | ||||
|   &::before, | ||||
|   &::after { | ||||
|     display: block; | ||||
|     content: ""; | ||||
|     position: absolute; | ||||
|     bottom: 0; | ||||
|     left: 18px; | ||||
|     width: 0; | ||||
|     height: 0; | ||||
|     border-style: solid; | ||||
|     border-width: 0 10px 10px; | ||||
|     border-color: transparent transparent lighten($ui-base-color, 4%); | ||||
|   } | ||||
| 
 | ||||
|   &::after { | ||||
|     bottom: -1px; | ||||
|     border-color: transparent transparent $ui-base-color; | ||||
|   } | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue