Redesign public hashtag page to use a masonry layout (#9822)
This commit is contained in:
		
							parent
							
								
									4ab42287c0
								
							
						
					
					
						commit
						bc642ac24b
					
				
					 11 changed files with 392 additions and 77 deletions
				
			
		|  | @ -3,6 +3,8 @@ | |||
| class TagsController < ApplicationController | ||||
|   PAGE_SIZE = 20 | ||||
| 
 | ||||
|   layout 'public' | ||||
| 
 | ||||
|   before_action :set_body_classes | ||||
|   before_action :set_instance_presenter | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,15 +1,17 @@ | |||
| import React from 'react'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import PropTypes from 'prop-types'; | ||||
| 
 | ||||
| export default class DisplayName extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     account: ImmutablePropTypes.map.isRequired, | ||||
|     others: ImmutablePropTypes.list, | ||||
|     localDomain: PropTypes.string, | ||||
|   }; | ||||
| 
 | ||||
|   render () { | ||||
|     const { account, others } = this.props; | ||||
|     const { account, others, localDomain } = this.props; | ||||
|     const displayNameHtml = { __html: account.get('display_name_html') }; | ||||
| 
 | ||||
|     let suffix; | ||||
|  | @ -17,7 +19,13 @@ export default class DisplayName extends React.PureComponent { | |||
|     if (others && others.size > 1) { | ||||
|       suffix = `+${others.size}`; | ||||
|     } else { | ||||
|       suffix = <span className='display-name__account'>@{account.get('acct')}</span>; | ||||
|       let acct = account.get('acct'); | ||||
| 
 | ||||
|       if (acct.indexOf('@') === -1 && localDomain) { | ||||
|         acct = `${acct}@${localDomain}`; | ||||
|       } | ||||
| 
 | ||||
|       suffix = <span className='display-name__account'>@{acct}</span>; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|  |  | |||
|  | @ -77,7 +77,7 @@ class Status extends ImmutablePureComponent { | |||
|     'account', | ||||
|     'muted', | ||||
|     'hidden', | ||||
|   ] | ||||
|   ]; | ||||
| 
 | ||||
|   handleClick = () => { | ||||
|     if (this.props.onClick) { | ||||
|  |  | |||
|  | @ -1,28 +1,32 @@ | |||
| import React from 'react'; | ||||
| import { connect } from 'react-redux'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import StatusListContainer from '../../ui/containers/status_list_container'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import { expandHashtagTimeline } from '../../../actions/timelines'; | ||||
| import Column from '../../../components/column'; | ||||
| import ColumnHeader from '../../../components/column_header'; | ||||
| import { connectHashtagStream } from '../../../actions/streaming'; | ||||
| import Masonry from 'react-masonry-infinite'; | ||||
| import { List as ImmutableList } from 'immutable'; | ||||
| import DetailedStatusContainer from '../../status/containers/detailed_status_container'; | ||||
| import { debounce } from 'lodash'; | ||||
| import LoadingIndicator from '../../../components/loading_indicator'; | ||||
| 
 | ||||
| export default @connect() | ||||
| const mapStateToProps = (state, { hashtag }) => ({ | ||||
|   statusIds: state.getIn(['timelines', `hashtag:${hashtag}`, 'items'], ImmutableList()), | ||||
|   isLoading: state.getIn(['timelines', `hashtag:${hashtag}`, 'isLoading'], false), | ||||
|   hasMore: state.getIn(['timelines', `hashtag:${hashtag}`, 'hasMore'], false), | ||||
| }); | ||||
| 
 | ||||
| export default @connect(mapStateToProps) | ||||
| class HashtagTimeline extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     dispatch: PropTypes.func.isRequired, | ||||
|     statusIds: ImmutablePropTypes.list.isRequired, | ||||
|     isLoading: PropTypes.bool.isRequired, | ||||
|     hasMore: PropTypes.bool.isRequired, | ||||
|     hashtag: PropTypes.string.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   handleHeaderClick = () => { | ||||
|     this.column.scrollTop(); | ||||
|   } | ||||
| 
 | ||||
|   setRef = c => { | ||||
|     this.column = c; | ||||
|   } | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|     const { dispatch, hashtag } = this.props; | ||||
| 
 | ||||
|  | @ -37,28 +41,52 @@ class HashtagTimeline extends React.PureComponent { | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleLoadMore = maxId => { | ||||
|     this.props.dispatch(expandHashtagTimeline(this.props.hashtag, { maxId })); | ||||
|   handleLoadMore = () => { | ||||
|     const maxId = this.props.statusIds.last(); | ||||
| 
 | ||||
|     if (maxId) { | ||||
|       this.props.dispatch(expandHashtagTimeline(this.props.hashtag, { maxId })); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setRef = c => { | ||||
|     this.masonry = c; | ||||
|   } | ||||
| 
 | ||||
|   handleHeightChange = debounce(() => { | ||||
|     if (!this.masonry) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this.masonry.forcePack(); | ||||
|   }, 50) | ||||
| 
 | ||||
|   render () { | ||||
|     const { hashtag } = this.props; | ||||
|     const { statusIds, hasMore, isLoading } = this.props; | ||||
| 
 | ||||
|     const sizes = [ | ||||
|       { columns: 1, gutter: 0 }, | ||||
|       { mq: '415px', columns: 1, gutter: 10 }, | ||||
|       { mq: '640px', columns: 2, gutter: 10 }, | ||||
|       { mq: '960px', columns: 3, gutter: 10 }, | ||||
|       { mq: '1255px', columns: 3, gutter: 10 }, | ||||
|     ]; | ||||
| 
 | ||||
|     const loader = (isLoading && statusIds.isEmpty()) ? <LoadingIndicator key={0} /> : undefined; | ||||
| 
 | ||||
|     return ( | ||||
|       <Column ref={this.setRef}> | ||||
|         <ColumnHeader | ||||
|           icon='hashtag' | ||||
|           title={hashtag} | ||||
|           onClick={this.handleHeaderClick} | ||||
|         /> | ||||
| 
 | ||||
|         <StatusListContainer | ||||
|           trackScroll={false} | ||||
|           scrollKey='standalone_hashtag_timeline' | ||||
|           timelineId={`hashtag:${hashtag}`} | ||||
|           onLoadMore={this.handleLoadMore} | ||||
|         /> | ||||
|       </Column> | ||||
|       <Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}> | ||||
|         {statusIds.map(statusId => ( | ||||
|           <div className='statuses-grid__item' key={statusId}> | ||||
|             <DetailedStatusContainer | ||||
|               id={statusId} | ||||
|               showThread | ||||
|               measureHeight | ||||
|               onHeightChange={this.handleHeightChange} | ||||
|             /> | ||||
|           </div> | ||||
|         )).toArray()} | ||||
|       </Masonry> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ import { FormattedDate, FormattedNumber } from 'react-intl'; | |||
| import Card from './card'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| import Video from '../../video'; | ||||
| import scheduleIdleTask from '../../ui/util/schedule_idle_task'; | ||||
| 
 | ||||
| export default class DetailedStatus extends ImmutablePureComponent { | ||||
| 
 | ||||
|  | @ -23,10 +24,17 @@ export default class DetailedStatus extends ImmutablePureComponent { | |||
|     onOpenMedia: PropTypes.func.isRequired, | ||||
|     onOpenVideo: PropTypes.func.isRequired, | ||||
|     onToggleHidden: PropTypes.func.isRequired, | ||||
|     measureHeight: PropTypes.bool, | ||||
|     onHeightChange: PropTypes.func, | ||||
|     domain: PropTypes.string.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|     height: null, | ||||
|   }; | ||||
| 
 | ||||
|   handleAccountClick = (e) => { | ||||
|     if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { | ||||
|     if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) { | ||||
|       e.preventDefault(); | ||||
|       this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); | ||||
|     } | ||||
|  | @ -42,13 +50,56 @@ export default class DetailedStatus extends ImmutablePureComponent { | |||
|     this.props.onToggleHidden(this.props.status); | ||||
|   } | ||||
| 
 | ||||
|   _measureHeight (heightJustChanged) { | ||||
|     if (this.props.measureHeight && this.node) { | ||||
|       scheduleIdleTask(() => this.node && this.setState({ height: this.node.offsetHeight })); | ||||
| 
 | ||||
|       if (this.props.onHeightChange && heightJustChanged) { | ||||
|         this.props.onHeightChange(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setRef = c => { | ||||
|     this.node = c; | ||||
|     this._measureHeight(); | ||||
|   } | ||||
| 
 | ||||
|   componentDidUpdate (prevProps, prevState) { | ||||
|     this._measureHeight(prevState.height !== this.state.height); | ||||
|   } | ||||
| 
 | ||||
|   handleModalLink = e => { | ||||
|     e.preventDefault(); | ||||
| 
 | ||||
|     let href; | ||||
| 
 | ||||
|     if (e.target.nodeName !== 'A') { | ||||
|       href = e.target.parentNode.href; | ||||
|     } else { | ||||
|       href = e.target.href; | ||||
|     } | ||||
| 
 | ||||
|     window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; | ||||
|     const outerStyle = { boxSizing: 'border-box' }; | ||||
| 
 | ||||
|     if (!status) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     let media           = ''; | ||||
|     let applicationLink = ''; | ||||
|     let reblogLink = ''; | ||||
|     let reblogIcon = 'retweet'; | ||||
|     let favouriteLink = ''; | ||||
| 
 | ||||
|     if (this.props.measureHeight) { | ||||
|       outerStyle.height = `${this.state.height}px`; | ||||
|     } | ||||
| 
 | ||||
|     if (status.get('media_attachments').size > 0) { | ||||
|       if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { | ||||
|  | @ -95,20 +146,51 @@ export default class DetailedStatus extends ImmutablePureComponent { | |||
| 
 | ||||
|     if (status.get('visibility') === 'private') { | ||||
|       reblogLink = <i className={`fa fa-${reblogIcon}`} />; | ||||
|     } else if (this.context.router) { | ||||
|       reblogLink = ( | ||||
|         <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'> | ||||
|           <i className={`fa fa-${reblogIcon}`} /> | ||||
|           <span className='detailed-status__reblogs'> | ||||
|             <FormattedNumber value={status.get('reblogs_count')} /> | ||||
|           </span> | ||||
|         </Link> | ||||
|       ); | ||||
|     } else { | ||||
|       reblogLink = (<Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'> | ||||
|         <i className={`fa fa-${reblogIcon}`} /> | ||||
|         <span className='detailed-status__reblogs'> | ||||
|           <FormattedNumber value={status.get('reblogs_count')} /> | ||||
|         </span> | ||||
|       </Link>); | ||||
|       reblogLink = ( | ||||
|         <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}> | ||||
|           <i className={`fa fa-${reblogIcon}`} /> | ||||
|           <span className='detailed-status__reblogs'> | ||||
|             <FormattedNumber value={status.get('reblogs_count')} /> | ||||
|           </span> | ||||
|         </a> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (this.context.router) { | ||||
|       favouriteLink = ( | ||||
|         <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'> | ||||
|           <i className='fa fa-star' /> | ||||
|           <span className='detailed-status__favorites'> | ||||
|             <FormattedNumber value={status.get('favourites_count')} /> | ||||
|           </span> | ||||
|         </Link> | ||||
|       ); | ||||
|     } else { | ||||
|       favouriteLink = ( | ||||
|         <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}> | ||||
|           <i className='fa fa-star' /> | ||||
|           <span className='detailed-status__favorites'> | ||||
|             <FormattedNumber value={status.get('favourites_count')} /> | ||||
|           </span> | ||||
|         </a> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className='detailed-status'> | ||||
|       <div ref={this.setRef} className='detailed-status' style={outerStyle}> | ||||
|         <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'> | ||||
|           <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div> | ||||
|           <DisplayName account={status.get('account')} /> | ||||
|           <DisplayName account={status.get('account')} localDomain={this.props.domain} /> | ||||
|         </a> | ||||
| 
 | ||||
|         <StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} /> | ||||
|  | @ -118,12 +200,7 @@ export default class DetailedStatus extends ImmutablePureComponent { | |||
|         <div className='detailed-status__meta'> | ||||
|           <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'> | ||||
|             <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /> | ||||
|           </a>{applicationLink} · {reblogLink} · <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'> | ||||
|             <i className='fa fa-star' /> | ||||
|             <span className='detailed-status__favorites'> | ||||
|               <FormattedNumber value={status.get('favourites_count')} /> | ||||
|             </span> | ||||
|           </Link> | ||||
|           </a>{applicationLink} · {reblogLink} · {favouriteLink} | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|  |  | |||
|  | @ -0,0 +1,172 @@ | |||
| import React from 'react'; | ||||
| import { connect } from 'react-redux'; | ||||
| import DetailedStatus from '../components/detailed_status'; | ||||
| import { makeGetStatus } from '../../../selectors'; | ||||
| import { | ||||
|   replyCompose, | ||||
|   mentionCompose, | ||||
|   directCompose, | ||||
| } from '../../../actions/compose'; | ||||
| import { | ||||
|   reblog, | ||||
|   favourite, | ||||
|   unreblog, | ||||
|   unfavourite, | ||||
|   pin, | ||||
|   unpin, | ||||
| } from '../../../actions/interactions'; | ||||
| import { blockAccount } from '../../../actions/accounts'; | ||||
| import { | ||||
|   muteStatus, | ||||
|   unmuteStatus, | ||||
|   deleteStatus, | ||||
|   hideStatus, | ||||
|   revealStatus, | ||||
| } from '../../../actions/statuses'; | ||||
| import { initMuteModal } from '../../../actions/mutes'; | ||||
| import { initReport } from '../../../actions/reports'; | ||||
| import { openModal } from '../../../actions/modal'; | ||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||
| import { boostModal, deleteModal } from '../../../initial_state'; | ||||
| import { showAlertForError } from '../../../actions/alerts'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, | ||||
|   deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, | ||||
|   redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, | ||||
|   redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, | ||||
|   blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, | ||||
|   replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, | ||||
|   replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, | ||||
| }); | ||||
| 
 | ||||
| const makeMapStateToProps = () => { | ||||
|   const getStatus = makeGetStatus(); | ||||
| 
 | ||||
|   const mapStateToProps = (state, props) => ({ | ||||
|     status: getStatus(state, props), | ||||
|     domain: state.getIn(['meta', 'domain']), | ||||
|   }); | ||||
| 
 | ||||
|   return mapStateToProps; | ||||
| }; | ||||
| 
 | ||||
| const mapDispatchToProps = (dispatch, { intl }) => ({ | ||||
| 
 | ||||
|   onReply (status, router) { | ||||
|     dispatch((_, getState) => { | ||||
|       let state = getState(); | ||||
|       if (state.getIn(['compose', 'text']).trim().length !== 0) { | ||||
|         dispatch(openModal('CONFIRM', { | ||||
|           message: intl.formatMessage(messages.replyMessage), | ||||
|           confirm: intl.formatMessage(messages.replyConfirm), | ||||
|           onConfirm: () => dispatch(replyCompose(status, router)), | ||||
|         })); | ||||
|       } else { | ||||
|         dispatch(replyCompose(status, router)); | ||||
|       } | ||||
|     }); | ||||
|   }, | ||||
| 
 | ||||
|   onModalReblog (status) { | ||||
|     dispatch(reblog(status)); | ||||
|   }, | ||||
| 
 | ||||
|   onReblog (status, e) { | ||||
|     if (status.get('reblogged')) { | ||||
|       dispatch(unreblog(status)); | ||||
|     } else { | ||||
|       if (e.shiftKey || !boostModal) { | ||||
|         this.onModalReblog(status); | ||||
|       } else { | ||||
|         dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   onFavourite (status) { | ||||
|     if (status.get('favourited')) { | ||||
|       dispatch(unfavourite(status)); | ||||
|     } else { | ||||
|       dispatch(favourite(status)); | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   onPin (status) { | ||||
|     if (status.get('pinned')) { | ||||
|       dispatch(unpin(status)); | ||||
|     } else { | ||||
|       dispatch(pin(status)); | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   onEmbed (status) { | ||||
|     dispatch(openModal('EMBED', { | ||||
|       url: status.get('url'), | ||||
|       onError: error => dispatch(showAlertForError(error)), | ||||
|     })); | ||||
|   }, | ||||
| 
 | ||||
|   onDelete (status, history, withRedraft = false) { | ||||
|     if (!deleteModal) { | ||||
|       dispatch(deleteStatus(status.get('id'), history, withRedraft)); | ||||
|     } else { | ||||
|       dispatch(openModal('CONFIRM', { | ||||
|         message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), | ||||
|         confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), | ||||
|         onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), | ||||
|       })); | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   onDirect (account, router) { | ||||
|     dispatch(directCompose(account, router)); | ||||
|   }, | ||||
| 
 | ||||
|   onMention (account, router) { | ||||
|     dispatch(mentionCompose(account, router)); | ||||
|   }, | ||||
| 
 | ||||
|   onOpenMedia (media, index) { | ||||
|     dispatch(openModal('MEDIA', { media, index })); | ||||
|   }, | ||||
| 
 | ||||
|   onOpenVideo (media, time) { | ||||
|     dispatch(openModal('VIDEO', { media, time })); | ||||
|   }, | ||||
| 
 | ||||
|   onBlock (account) { | ||||
|     dispatch(openModal('CONFIRM', { | ||||
|       message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, | ||||
|       confirm: intl.formatMessage(messages.blockConfirm), | ||||
|       onConfirm: () => dispatch(blockAccount(account.get('id'))), | ||||
|     })); | ||||
|   }, | ||||
| 
 | ||||
|   onReport (status) { | ||||
|     dispatch(initReport(status.get('account'), status)); | ||||
|   }, | ||||
| 
 | ||||
|   onMute (account) { | ||||
|     dispatch(initMuteModal(account)); | ||||
|   }, | ||||
| 
 | ||||
|   onMuteConversation (status) { | ||||
|     if (status.get('muted')) { | ||||
|       dispatch(unmuteStatus(status.get('id'))); | ||||
|     } else { | ||||
|       dispatch(muteStatus(status.get('id'))); | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   onToggleHidden (status) { | ||||
|     if (status.get('hidden')) { | ||||
|       dispatch(revealStatus(status.get('id'))); | ||||
|     } else { | ||||
|       dispatch(hideStatus(status.get('id'))); | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(DetailedStatus)); | ||||
|  | @ -425,3 +425,30 @@ | |||
|     border-radius: 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| $maximum-width: 1235px; | ||||
| $fluid-breakpoint: $maximum-width + 20px; | ||||
| 
 | ||||
| .statuses-grid { | ||||
|   min-height: 600px; | ||||
| 
 | ||||
|   &__item { | ||||
|     width: (960px - 20px) / 3; | ||||
| 
 | ||||
|     @media screen and (max-width: $fluid-breakpoint) { | ||||
|       width: (940px - 20px) / 3; | ||||
|     } | ||||
| 
 | ||||
|     @media screen and (max-width: $no-gap-breakpoint) { | ||||
|       width: 100vw; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .detailed-status { | ||||
|     border-radius: 4px; | ||||
| 
 | ||||
|     @media screen and (max-width: $no-gap-breakpoint) { | ||||
|       border-bottom: 1px solid lighten($ui-base-color, 12%); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -8,33 +8,5 @@ | |||
|   = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous' | ||||
|   = render 'og' | ||||
| 
 | ||||
| .landing-page.tag-page.alternative | ||||
|   .features | ||||
|     .container | ||||
|       .grid | ||||
|         .column-1 | ||||
|           #mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name)) } } | ||||
| 
 | ||||
|         .column-2 | ||||
|           .about-mastodon | ||||
|             .about-hashtag.landing-page__information | ||||
|               .brand | ||||
|                 = link_to root_url do | ||||
|                   = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' | ||||
| 
 | ||||
|               %p= t 'about.about_hashtag_html', hashtag: @tag.name | ||||
| 
 | ||||
|               .cta | ||||
|                 - if user_signed_in? | ||||
|                   = link_to t('settings.back'), root_path, class: 'button button-secondary' | ||||
|                 - else | ||||
|                   = link_to t('auth.login'), new_user_session_path, class: 'button button-secondary' | ||||
|                 = link_to t('about.learn_more'), about_path, class: 'button button-alternative' | ||||
| 
 | ||||
|             .landing-page__features.landing-page__information | ||||
|               %h3= t 'about.what_is_mastodon' | ||||
|               %p= t 'about.about_mastodon_html' | ||||
| 
 | ||||
|               = render 'features' | ||||
| 
 | ||||
| #mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name)) } } | ||||
| #modal-container | ||||
|  |  | |||
|  | @ -98,6 +98,7 @@ | |||
|     "react-immutable-proptypes": "^2.1.0", | ||||
|     "react-immutable-pure-component": "^1.1.1", | ||||
|     "react-intl": "^2.7.2", | ||||
|     "react-masonry-infinite": "^1.2.2", | ||||
|     "react-motion": "^0.5.2", | ||||
|     "react-notification": "^6.8.4", | ||||
|     "react-overlays": "^0.8.3", | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ RSpec.describe TagsController, type: :controller do | |||
| 
 | ||||
|       it 'renders application layout' do | ||||
|         get :show, params: { id: 'test', max_id: late.id } | ||||
|         expect(response).to render_template layout: 'application' | ||||
|         expect(response).to render_template layout: 'public' | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										28
									
								
								yarn.lock
									
										
									
									
									
								
							
							
						
						
									
										28
									
								
								yarn.lock
									
										
									
									
									
								
							|  | @ -1681,6 +1681,13 @@ braces@^2.3.0, braces@^2.3.1: | |||
|     split-string "^3.0.2" | ||||
|     to-regex "^3.0.1" | ||||
| 
 | ||||
| bricks.js@^1.7.0: | ||||
|   version "1.8.0" | ||||
|   resolved "https://registry.yarnpkg.com/bricks.js/-/bricks.js-1.8.0.tgz#8fdeb3c0226af251f4d5727a7df7f9ac0092b4b2" | ||||
|   integrity sha1-j96zwCJq8lH01XJ6fff5rACStLI= | ||||
|   dependencies: | ||||
|     knot.js "^1.1.5" | ||||
| 
 | ||||
| brorand@^1.0.1: | ||||
|   version "1.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" | ||||
|  | @ -5528,6 +5535,11 @@ kleur@^2.0.1: | |||
|   resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300" | ||||
|   integrity sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ== | ||||
| 
 | ||||
| knot.js@^1.1.5: | ||||
|   version "1.1.5" | ||||
|   resolved "https://registry.yarnpkg.com/knot.js/-/knot.js-1.1.5.tgz#28e72522f703f50fe98812fde224dd72728fef5d" | ||||
|   integrity sha1-KOclIvcD9Q/piBL94iTdcnKP710= | ||||
| 
 | ||||
| lcid@^1.0.0: | ||||
|   version "1.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" | ||||
|  | @ -7558,6 +7570,13 @@ react-immutable-pure-component@^1.1.1: | |||
|   optionalDependencies: | ||||
|     "@types/react" "16.4.6" | ||||
| 
 | ||||
| react-infinite-scroller@^1.0.12: | ||||
|   version "1.2.4" | ||||
|   resolved "https://registry.yarnpkg.com/react-infinite-scroller/-/react-infinite-scroller-1.2.4.tgz#f67eaec4940a4ce6417bebdd6e3433bfc38826e9" | ||||
|   integrity sha512-/oOa0QhZjXPqaD6sictN2edFMsd3kkMiE19Vcz5JDgHpzEJVqYcmq+V3mkwO88087kvKGe1URNksHEOt839Ubw== | ||||
|   dependencies: | ||||
|     prop-types "^15.5.8" | ||||
| 
 | ||||
| react-input-autosize@^2.2.1: | ||||
|   version "2.2.1" | ||||
|   resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8" | ||||
|  | @ -7596,6 +7615,15 @@ react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4: | |||
|   resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" | ||||
|   integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== | ||||
| 
 | ||||
| react-masonry-infinite@^1.2.2: | ||||
|   version "1.2.2" | ||||
|   resolved "https://registry.yarnpkg.com/react-masonry-infinite/-/react-masonry-infinite-1.2.2.tgz#20c1386f9ccdda9747527c8f42bc2c02dd2e7951" | ||||
|   integrity sha1-IME4b5zN2pdHUnyPQrwsAt0ueVE= | ||||
|   dependencies: | ||||
|     bricks.js "^1.7.0" | ||||
|     prop-types "^15.5.10" | ||||
|     react-infinite-scroller "^1.0.12" | ||||
| 
 | ||||
| react-motion@^0.5.2: | ||||
|   version "0.5.2" | ||||
|   resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" | ||||
|  |  | |||
		Reference in a new issue