Add media timeline (#6631)
This commit is contained in:
		
							parent
							
								
									05f8c375a2
								
							
						
					
					
						commit
						7403e5d306
					
				
					 53 changed files with 205 additions and 35 deletions
				
			
		|  | @ -40,10 +40,9 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => { | |||
|   dispatch(expandHomeTimeline({}, () => dispatch(expandNotifications({}, done)))); | ||||
| }; | ||||
| 
 | ||||
| export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); | ||||
| export const connectCommunityStream = () => connectTimelineStream('community', 'public:local'); | ||||
| 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 connectDirectStream = () => connectTimelineStream('direct', 'direct'); | ||||
| export const connectListStream = (id) => connectTimelineStream(`list:${id}`, `list&list=${id}`); | ||||
| export const connectUserStream      = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); | ||||
| export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); | ||||
| export const connectPublicStream    = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`); | ||||
| export const connectHashtagStream   = tag => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`); | ||||
| export const connectDirectStream    = () => connectTimelineStream('direct', 'direct'); | ||||
| export const connectListStream      = id => connectTimelineStream(`list:${id}`, `list&list=${id}`); | ||||
|  |  | |||
|  | @ -93,15 +93,15 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { | |||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export const expandHomeTimeline         = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); | ||||
| export const expandPublicTimeline       = ({ maxId } = {}, done = noOp) => expandTimeline('public', '/api/v1/timelines/public', { max_id: maxId }, done); | ||||
| export const expandCommunityTimeline    = ({ maxId } = {}, done = noOp) => expandTimeline('community', '/api/v1/timelines/public', { local: true, max_id: maxId }, done); | ||||
| export const expandDirectTimeline       = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); | ||||
| export const expandAccountTimeline      = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); | ||||
| export const expandHomeTimeline            = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); | ||||
| export const expandPublicTimeline          = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done); | ||||
| export const expandCommunityTimeline       = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); | ||||
| export const expandDirectTimeline          = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); | ||||
| export const expandAccountTimeline         = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); | ||||
| export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); | ||||
| export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true }); | ||||
| export const expandHashtagTimeline      = (hashtag, { maxId } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }, done); | ||||
| export const expandListTimeline         = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); | ||||
| export const expandAccountMediaTimeline    = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true }); | ||||
| export const expandHashtagTimeline         = (hashtag, { maxId } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }, done); | ||||
| export const expandListTimeline            = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); | ||||
| 
 | ||||
| export function expandTimelineRequest(timeline) { | ||||
|   return { | ||||
|  |  | |||
|  | @ -1,12 +1,13 @@ | |||
| import React from 'react'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||
| import { NavLink } from 'react-router-dom'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import StatusListContainer from '../ui/containers/status_list_container'; | ||||
| import Column from '../../components/column'; | ||||
| import ColumnHeader from '../../components/column_header'; | ||||
| import { expandCommunityTimeline } from '../../actions/timelines'; | ||||
| import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; | ||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||
| import ColumnSettingsContainer from './containers/column_settings_container'; | ||||
| import { connectCommunityStream } from '../../actions/streaming'; | ||||
| 
 | ||||
|  | @ -14,20 +15,25 @@ const messages = defineMessages({ | |||
|   title: { id: 'column.community', defaultMessage: 'Local timeline' }, | ||||
| }); | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
|   hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0, | ||||
| const mapStateToProps = (state, { onlyMedia }) => ({ | ||||
|   hasUnread: state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`, 'unread']) > 0, | ||||
| }); | ||||
| 
 | ||||
| @connect(mapStateToProps) | ||||
| @injectIntl | ||||
| export default class CommunityTimeline extends React.PureComponent { | ||||
| 
 | ||||
|   static defaultProps = { | ||||
|     onlyMedia: false, | ||||
|   }; | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     dispatch: PropTypes.func.isRequired, | ||||
|     columnId: PropTypes.string, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|     hasUnread: PropTypes.bool, | ||||
|     multiColumn: PropTypes.bool, | ||||
|     onlyMedia: PropTypes.bool, | ||||
|   }; | ||||
| 
 | ||||
|   handlePin = () => { | ||||
|  | @ -50,10 +56,10 @@ export default class CommunityTimeline extends React.PureComponent { | |||
|   } | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|     const { dispatch } = this.props; | ||||
|     const { dispatch, onlyMedia } = this.props; | ||||
| 
 | ||||
|     dispatch(expandCommunityTimeline()); | ||||
|     this.disconnect = dispatch(connectCommunityStream()); | ||||
|     dispatch(expandCommunityTimeline({ onlyMedia })); | ||||
|     this.disconnect = dispatch(connectCommunityStream({ onlyMedia })); | ||||
|   } | ||||
| 
 | ||||
|   componentWillUnmount () { | ||||
|  | @ -68,13 +74,22 @@ export default class CommunityTimeline extends React.PureComponent { | |||
|   } | ||||
| 
 | ||||
|   handleLoadMore = maxId => { | ||||
|     this.props.dispatch(expandCommunityTimeline({ maxId })); | ||||
|     const { dispatch, onlyMedia } = this.props; | ||||
| 
 | ||||
|     dispatch(expandCommunityTimeline({ maxId, onlyMedia })); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { intl, hasUnread, columnId, multiColumn } = this.props; | ||||
|     const { intl, hasUnread, columnId, multiColumn, onlyMedia } = this.props; | ||||
|     const pinned = !!columnId; | ||||
| 
 | ||||
|     const headline = ( | ||||
|       <div className='community-timeline__section-headline'> | ||||
|         <NavLink exact to='/timelines/public/local' replace><FormattedMessage id='timeline.posts' defaultMessage='Toots' /></NavLink> | ||||
|         <NavLink exact to='/timelines/public/local/media' replace><FormattedMessage id='timeline.media' defaultMessage='Media' /></NavLink> | ||||
|       </div> | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|       <Column ref={this.setRef}> | ||||
|         <ColumnHeader | ||||
|  | @ -91,9 +106,10 @@ export default class CommunityTimeline extends React.PureComponent { | |||
|         </ColumnHeader> | ||||
| 
 | ||||
|         <StatusListContainer | ||||
|           prepend={headline} | ||||
|           trackScroll={!pinned} | ||||
|           scrollKey={`community_timeline-${columnId}`} | ||||
|           timelineId='community' | ||||
|           timelineId={`community${onlyMedia ? ':media' : ''}`} | ||||
|           onLoadMore={this.handleLoadMore} | ||||
|           emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} | ||||
|         /> | ||||
|  |  | |||
|  | @ -1,12 +1,13 @@ | |||
| import React from 'react'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||
| import { NavLink } from 'react-router-dom'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import StatusListContainer from '../ui/containers/status_list_container'; | ||||
| import Column from '../../components/column'; | ||||
| import ColumnHeader from '../../components/column_header'; | ||||
| import { expandPublicTimeline } from '../../actions/timelines'; | ||||
| import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; | ||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||
| import ColumnSettingsContainer from './containers/column_settings_container'; | ||||
| import { connectPublicStream } from '../../actions/streaming'; | ||||
| 
 | ||||
|  | @ -14,20 +15,25 @@ const messages = defineMessages({ | |||
|   title: { id: 'column.public', defaultMessage: 'Federated timeline' }, | ||||
| }); | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
|   hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0, | ||||
| const mapStateToProps = (state, { onlyMedia }) => ({ | ||||
|   hasUnread: state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`, 'unread']) > 0, | ||||
| }); | ||||
| 
 | ||||
| @connect(mapStateToProps) | ||||
| @injectIntl | ||||
| export default class PublicTimeline extends React.PureComponent { | ||||
| 
 | ||||
|   static defaultProps = { | ||||
|     onlyMedia: false, | ||||
|   }; | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     dispatch: PropTypes.func.isRequired, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|     columnId: PropTypes.string, | ||||
|     multiColumn: PropTypes.bool, | ||||
|     hasUnread: PropTypes.bool, | ||||
|     onlyMedia: PropTypes.bool, | ||||
|   }; | ||||
| 
 | ||||
|   handlePin = () => { | ||||
|  | @ -50,10 +56,10 @@ export default class PublicTimeline extends React.PureComponent { | |||
|   } | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|     const { dispatch } = this.props; | ||||
|     const { dispatch, onlyMedia } = this.props; | ||||
| 
 | ||||
|     dispatch(expandPublicTimeline()); | ||||
|     this.disconnect = dispatch(connectPublicStream()); | ||||
|     dispatch(expandPublicTimeline({ onlyMedia })); | ||||
|     this.disconnect = dispatch(connectPublicStream({ onlyMedia })); | ||||
|   } | ||||
| 
 | ||||
|   componentWillUnmount () { | ||||
|  | @ -68,13 +74,22 @@ export default class PublicTimeline extends React.PureComponent { | |||
|   } | ||||
| 
 | ||||
|   handleLoadMore = maxId => { | ||||
|     this.props.dispatch(expandPublicTimeline({ maxId })); | ||||
|     const { dispatch, onlyMedia } = this.props; | ||||
| 
 | ||||
|     dispatch(expandPublicTimeline({ maxId, onlyMedia })); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { intl, columnId, hasUnread, multiColumn } = this.props; | ||||
|     const { intl, columnId, hasUnread, multiColumn, onlyMedia } = this.props; | ||||
|     const pinned = !!columnId; | ||||
| 
 | ||||
|     const headline = ( | ||||
|       <div className='public-timeline__section-headline'> | ||||
|         <NavLink exact to='/timelines/public' replace><FormattedMessage id='timeline.posts' defaultMessage='Toots' /></NavLink> | ||||
|         <NavLink exact to='/timelines/public/media' replace><FormattedMessage id='timeline.media' defaultMessage='Media' /></NavLink> | ||||
|       </div> | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|       <Column ref={this.setRef}> | ||||
|         <ColumnHeader | ||||
|  | @ -91,7 +106,8 @@ export default class PublicTimeline extends React.PureComponent { | |||
|         </ColumnHeader> | ||||
| 
 | ||||
|         <StatusListContainer | ||||
|           timelineId='public' | ||||
|           prepend={headline} | ||||
|           timelineId={`public${onlyMedia ? ':media' : ''}`} | ||||
|           onLoadMore={this.handleLoadMore} | ||||
|           trackScroll={!pinned} | ||||
|           scrollKey={`public_timeline-${columnId}`} | ||||
|  |  | |||
|  | @ -141,7 +141,9 @@ class SwitchingColumnsArea extends React.PureComponent { | |||
|           <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> | ||||
|           <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} /> | ||||
|           <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} /> | ||||
|           <WrappedRoute path='/timelines/public/media' extract component={PublicTimeline} content={children} componentParams={{ onlyMedia: true }} /> | ||||
|           <WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} /> | ||||
|           <WrappedRoute path='/timelines/public/local/media' component={CommunityTimeline} content={children} componentParams={{ onlyMedia: true }} /> | ||||
|           <WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} /> | ||||
|           <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} /> | ||||
|           <WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} /> | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "المحلي", | ||||
|   "tabs_bar.notifications": "الإخطارات", | ||||
|   "tabs_bar.search": "البحث", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.", | ||||
|   "upload_area.title": "إسحب ثم أفلت للرفع", | ||||
|   "upload_button.label": "إضافة وسائط", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Local", | ||||
|   "tabs_bar.notifications": "Известия", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", | ||||
|   "upload_area.title": "Drag & drop to upload", | ||||
|   "upload_button.label": "Добави медия", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Local", | ||||
|   "tabs_bar.notifications": "Notificacions", | ||||
|   "tabs_bar.search": "Cerca", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "El vostre esborrany es perdrà si sortiu de Mastodon.", | ||||
|   "upload_area.title": "Arrossega i deixa anar per carregar", | ||||
|   "upload_button.label": "Afegir multimèdia", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Lucale", | ||||
|   "tabs_bar.notifications": "Nutificazione", | ||||
|   "tabs_bar.search": "Cercà", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "A bruttacopia sarà persa s'ellu hè chjosu Mastodon.", | ||||
|   "upload_area.title": "Drag & drop per caricà un fugliale", | ||||
|   "upload_button.label": "Aghjunghje un media", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Lokal", | ||||
|   "tabs_bar.notifications": "Mitteilungen", | ||||
|   "tabs_bar.search": "Suchen", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.", | ||||
|   "upload_area.title": "Zum Hochladen hereinziehen", | ||||
|   "upload_button.label": "Mediendatei hinzufügen", | ||||
|  |  | |||
|  | @ -596,6 +596,14 @@ | |||
|         "defaultMessage": "Local timeline", | ||||
|         "id": "column.community" | ||||
|       }, | ||||
|       { | ||||
|         "defaultMessage": "Toots", | ||||
|         "id": "timeline.posts" | ||||
|       }, | ||||
|       { | ||||
|         "defaultMessage": "Media", | ||||
|         "id": "timeline.media" | ||||
|       }, | ||||
|       { | ||||
|         "defaultMessage": "The local timeline is empty. Write something publicly to get the ball rolling!", | ||||
|         "id": "empty_column.community" | ||||
|  | @ -1393,6 +1401,14 @@ | |||
|         "defaultMessage": "Federated timeline", | ||||
|         "id": "column.public" | ||||
|       }, | ||||
|       { | ||||
|         "defaultMessage": "Toots", | ||||
|         "id": "timeline.posts" | ||||
|       }, | ||||
|       { | ||||
|         "defaultMessage": "Media", | ||||
|         "id": "timeline.media" | ||||
|       }, | ||||
|       { | ||||
|         "defaultMessage": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", | ||||
|         "id": "empty_column.public" | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Local", | ||||
|   "tabs_bar.notifications": "Notifications", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", | ||||
|   "upload_area.title": "Drag & drop to upload", | ||||
|   "upload_button.label": "Add media", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Local", | ||||
|   "tabs_bar.notifications": "Notifications", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", | ||||
|   "upload_area.title": "Drag & drop to upload", | ||||
|   "upload_button.label": "Add media", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Loka tempolinio", | ||||
|   "tabs_bar.notifications": "Sciigoj", | ||||
|   "tabs_bar.search": "Serĉi", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Via malneto perdiĝos se vi eliras de Mastodon.", | ||||
|   "upload_area.title": "Altreni kaj lasi por alŝuti", | ||||
|   "upload_button.label": "Aldoni aŭdovidaĵon", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Local", | ||||
|   "tabs_bar.notifications": "Notificaciones", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.", | ||||
|   "upload_area.title": "Arrastra y suelta para subir", | ||||
|   "upload_button.label": "Subir multimedia", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Local", | ||||
|   "tabs_bar.notifications": "Notifications", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", | ||||
|   "upload_area.title": "Drag & drop to upload", | ||||
|   "upload_button.label": "Add media", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "محلی", | ||||
|   "tabs_bar.notifications": "اعلانها", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "اگر از ماستدون خارج شوید پیشنویس شما پاک خواهد شد.", | ||||
|   "upload_area.title": "برای بارگذاری به اینجا بکشید", | ||||
|   "upload_button.label": "افزودن تصویر", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Paikallinen", | ||||
|   "tabs_bar.notifications": "Ilmoitukset", | ||||
|   "tabs_bar.search": "Hae", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Luonnos häviää, jos poistut Mastodonista.", | ||||
|   "upload_area.title": "Lataa raahaamalla ja pudottamalla tähän", | ||||
|   "upload_button.label": "Lisää mediaa", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Fil public local", | ||||
|   "tabs_bar.notifications": "Notifications", | ||||
|   "tabs_bar.search": "Chercher", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.", | ||||
|   "upload_area.title": "Glissez et déposez pour envoyer", | ||||
|   "upload_button.label": "Joindre un média", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Local", | ||||
|   "tabs_bar.notifications": "Notificacións", | ||||
|   "tabs_bar.search": "Buscar", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "O borrador perderase se sae de Mastodon.", | ||||
|   "upload_area.title": "Arrastre e solte para subir", | ||||
|   "upload_button.label": "Engadir medios", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "ציר זמן מקומי", | ||||
|   "tabs_bar.notifications": "התראות", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.", | ||||
|   "upload_area.title": "ניתן להעלות על ידי Drag & drop", | ||||
|   "upload_button.label": "הוספת מדיה", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Lokalno", | ||||
|   "tabs_bar.notifications": "Notifikacije", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", | ||||
|   "upload_area.title": "Povuci i spusti kako bi uploadao", | ||||
|   "upload_button.label": "Dodaj media", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Local", | ||||
|   "tabs_bar.notifications": "Értesítések", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "A piszkozata el fog vesztődni ha elhagyja Mastodon-t.", | ||||
|   "upload_area.title": "Húzza ide a feltöltéshez", | ||||
|   "upload_button.label": "Média hozzáadása", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Տեղական", | ||||
|   "tabs_bar.notifications": "Ծանուցումներ", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Քո սեւագիրը կկորի, եթե լքես Մաստոդոնը։", | ||||
|   "upload_area.title": "Քաշիր ու նետիր՝ վերբեռնելու համար", | ||||
|   "upload_button.label": "Ավելացնել մեդիա", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Lokal", | ||||
|   "tabs_bar.notifications": "Notifikasi", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Naskah anda akan hilang jika anda keluar dari Mastodon.", | ||||
|   "upload_area.title": "Seret & lepaskan untuk mengunggah", | ||||
|   "upload_button.label": "Tambahkan media", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Lokala", | ||||
|   "tabs_bar.notifications": "Savigi", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", | ||||
|   "upload_area.title": "Tranar faligar por kargar", | ||||
|   "upload_button.label": "Adjuntar kontenajo", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Locale", | ||||
|   "tabs_bar.notifications": "Notifiche", | ||||
|   "tabs_bar.search": "Cerca", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "La bozza andrà persa se esci da Mastodon.", | ||||
|   "upload_area.title": "Trascina per caricare", | ||||
|   "upload_button.label": "Aggiungi file multimediale", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "ローカル", | ||||
|   "tabs_bar.notifications": "通知", | ||||
|   "tabs_bar.search": "検索", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。", | ||||
|   "upload_area.title": "ドラッグ&ドロップでアップロード", | ||||
|   "upload_button.label": "メディアを追加", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "로컬", | ||||
|   "tabs_bar.notifications": "알림", | ||||
|   "tabs_bar.search": "검색", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "지금 나가면 저장되지 않은 항목을 잃게 됩니다.", | ||||
|   "upload_area.title": "드래그 & 드롭으로 업로드", | ||||
|   "upload_button.label": "미디어 추가", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Lokaal", | ||||
|   "tabs_bar.notifications": "Meldingen", | ||||
|   "tabs_bar.search": "Zoeken", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Je concept zal verloren gaan als je Mastodon verlaat.", | ||||
|   "upload_area.title": "Hierin slepen om te uploaden", | ||||
|   "upload_button.label": "Media toevoegen", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Lokal", | ||||
|   "tabs_bar.notifications": "Varslinger", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Din kladd vil bli forkastet om du forlater Mastodon.", | ||||
|   "upload_area.title": "Dra og slipp for å laste opp", | ||||
|   "upload_button.label": "Legg til media", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Flux public local", | ||||
|   "tabs_bar.notifications": "Notificacions", | ||||
|   "tabs_bar.search": "Recèrcas", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Vòstre brolhon serà perdut se quitatz Mastodon.", | ||||
|   "upload_area.title": "Lisatz e depausatz per mandar", | ||||
|   "upload_button.label": "Ajustar un mèdia", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Lokalne", | ||||
|   "tabs_bar.notifications": "Powiadomienia", | ||||
|   "tabs_bar.search": "Szukaj", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.", | ||||
|   "upload_area.title": "Przeciągnij i upuść aby wysłać", | ||||
|   "upload_button.label": "Dodaj zawartość multimedialną", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Local", | ||||
|   "tabs_bar.notifications": "Notificações", | ||||
|   "tabs_bar.search": "Buscar", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Seu rascunho será perdido se você sair do Mastodon.", | ||||
|   "upload_area.title": "Arraste e solte para enviar", | ||||
|   "upload_button.label": "Adicionar mídia", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Local", | ||||
|   "tabs_bar.notifications": "Notificações", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "O teu rascunho vai ser perdido se abandonares o Mastodon.", | ||||
|   "upload_area.title": "Arraste e solte para enviar", | ||||
|   "upload_button.label": "Adicionar media", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Локальная", | ||||
|   "tabs_bar.notifications": "Уведомления", | ||||
|   "tabs_bar.search": "Поиск", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Ваш черновик будет утерян, если вы покинете Mastodon.", | ||||
|   "upload_area.title": "Перетащите сюда, чтобы загрузить", | ||||
|   "upload_button.label": "Добавить медиаконтент", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Lokálna", | ||||
|   "tabs_bar.notifications": "Notifikácie", | ||||
|   "tabs_bar.search": "Hľadaj", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Čo máte rozpísané sa stratí, ak opustíte Mastodon.", | ||||
|   "upload_area.title": "Ťahaj a pusti pre nahratie", | ||||
|   "upload_button.label": "Pridať médiá", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Lokalno", | ||||
|   "tabs_bar.notifications": "Obvestila", | ||||
|   "tabs_bar.search": "Poišči", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Vaš osnutek bo izgubljen, če zapustite Mastodona.", | ||||
|   "upload_area.title": "Povlecite in spustite za pošiljanje", | ||||
|   "upload_button.label": "Dodaj medij", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Lokalno", | ||||
|   "tabs_bar.notifications": "Obaveštenja", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Ako napustite Mastodont, izgubićete napisani nacrt.", | ||||
|   "upload_area.title": "Prevucite ovde da otpremite", | ||||
|   "upload_button.label": "Dodaj multimediju", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Локално", | ||||
|   "tabs_bar.notifications": "Обавештења", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Ако напустите Мастодонт, изгубићете написани нацрт.", | ||||
|   "upload_area.title": "Превуците овде да отпремите", | ||||
|   "upload_button.label": "Додај мултимедију", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Lokal", | ||||
|   "tabs_bar.notifications": "Meddelanden", | ||||
|   "tabs_bar.search": "Sök", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Ditt utkast kommer att förloras om du lämnar Mastodon.", | ||||
|   "upload_area.title": "Dra & släpp för att ladda upp", | ||||
|   "upload_button.label": "Lägg till media", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Local", | ||||
|   "tabs_bar.notifications": "Notifications", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", | ||||
|   "upload_area.title": "Drag & drop to upload", | ||||
|   "upload_button.label": "Add media", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Local", | ||||
|   "tabs_bar.notifications": "Notifications", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", | ||||
|   "upload_area.title": "Drag & drop to upload", | ||||
|   "upload_button.label": "Add media", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Yerel", | ||||
|   "tabs_bar.notifications": "Bildirimler", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", | ||||
|   "upload_area.title": "Upload için sürükle bırak yapınız", | ||||
|   "upload_button.label": "Görsel ekle", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "Локальна", | ||||
|   "tabs_bar.notifications": "Сповіщення", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", | ||||
|   "upload_area.title": "Перетягніть сюди, щоб завантажити", | ||||
|   "upload_button.label": "Додати медіаконтент", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "本站", | ||||
|   "tabs_bar.notifications": "通知", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会被丢弃。", | ||||
|   "upload_area.title": "将文件拖放到此处开始上传", | ||||
|   "upload_button.label": "上传媒体文件", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "本站", | ||||
|   "tabs_bar.notifications": "通知", | ||||
|   "tabs_bar.search": "搜尋", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "如果你現在離開 Mastodon,你的草稿內容將會被丟棄。", | ||||
|   "upload_area.title": "將檔案拖放至此上載", | ||||
|   "upload_button.label": "上載媒體檔案", | ||||
|  |  | |||
|  | @ -280,6 +280,8 @@ | |||
|   "tabs_bar.local_timeline": "本地", | ||||
|   "tabs_bar.notifications": "通知", | ||||
|   "tabs_bar.search": "Search", | ||||
|   "timeline.media": "Media", | ||||
|   "timeline.posts": "Toots", | ||||
|   "ui.beforeunload": "如果離開 Mastodon,你的草稿將會不見。", | ||||
|   "upload_area.title": "拖放來上傳", | ||||
|   "upload_button.label": "增加媒體", | ||||
|  |  | |||
|  | @ -4737,6 +4737,8 @@ a.status-card { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .community-timeline__section-headline, | ||||
| .public-timeline__section-headline, | ||||
| .account__section-headline { | ||||
|   background: darken($ui-base-color, 4%); | ||||
|   border-bottom: 1px solid lighten($ui-base-color, 8%); | ||||
|  |  | |||
|  | @ -81,6 +81,11 @@ class BatchedRemoveStatusService < BaseService | |||
|       redis.publish('timeline:public', payload) | ||||
|       redis.publish('timeline:public:local', payload) if status.local? | ||||
| 
 | ||||
|       if status.media_attachments.exists? | ||||
|         redis.publish('timeline:public:media', payload) | ||||
|         redis.publish('timeline:public:local:media', payload) if status.local? | ||||
|       end | ||||
| 
 | ||||
|       @tags[status.id].each do |hashtag| | ||||
|         redis.publish("timeline:hashtag:#{hashtag}", payload) | ||||
|         redis.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local? | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ class FanOutOnWriteService < BaseService | |||
|     return if status.reply? && status.in_reply_to_account_id != status.account_id | ||||
| 
 | ||||
|     deliver_to_public(status) | ||||
|     deliver_to_media(status) if status.media_attachments.exists? | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
|  | @ -85,6 +86,13 @@ class FanOutOnWriteService < BaseService | |||
|     Redis.current.publish('timeline:public:local', @payload) if status.local? | ||||
|   end | ||||
| 
 | ||||
|   def deliver_to_media(status) | ||||
|     Rails.logger.debug "Delivering status #{status.id} to media timeline" | ||||
| 
 | ||||
|     Redis.current.publish('timeline:public:media', @payload) | ||||
|     Redis.current.publish('timeline:public:local:media', @payload) if status.local? | ||||
|   end | ||||
| 
 | ||||
|   def deliver_to_direct_timelines(status) | ||||
|     Rails.logger.debug "Delivering status #{status.id} to direct timelines" | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ class RemoveStatusService < BaseService | |||
|     remove_reblogs | ||||
|     remove_from_hashtags | ||||
|     remove_from_public | ||||
|     remove_from_media if status.media_attachments.exists? | ||||
|     remove_from_direct if status.direct_visibility? | ||||
| 
 | ||||
|     @status.destroy! | ||||
|  | @ -131,6 +132,13 @@ class RemoveStatusService < BaseService | |||
|     Redis.current.publish('timeline:public:local', @payload) if @status.local? | ||||
|   end | ||||
| 
 | ||||
|   def remove_from_media | ||||
|     return unless @status.public_visibility? | ||||
| 
 | ||||
|     Redis.current.publish('timeline:public:media', @payload) | ||||
|     Redis.current.publish('timeline:public:local:media', @payload) if @status.local? | ||||
|   end | ||||
| 
 | ||||
|   def remove_from_direct | ||||
|     @mentions.each do |mention| | ||||
|       Redis.current.publish("timeline:direct:#{mention.account.id}", @payload) if mention.account.local? | ||||
|  |  | |||
|  | @ -241,7 +241,9 @@ const startWorker = (workerId) => { | |||
| 
 | ||||
|   const PUBLIC_STREAMS = [ | ||||
|     'public', | ||||
|     'public:media', | ||||
|     'public:local', | ||||
|     'public:local:media', | ||||
|     'hashtag', | ||||
|     'hashtag:local', | ||||
|   ]; | ||||
|  | @ -459,11 +461,17 @@ const startWorker = (workerId) => { | |||
|   }); | ||||
| 
 | ||||
|   app.get('/api/v1/streaming/public', (req, res) => { | ||||
|     streamFrom('timeline:public', req, streamToHttp(req, res), streamHttpEnd(req), true); | ||||
|     const onlyMedia = req.query.only_media === '1' || req.query.only_media === 'true'; | ||||
|     const channel   = onlyMedia ? 'timeline:public:media' : 'timeline:public'; | ||||
| 
 | ||||
|     streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true); | ||||
|   }); | ||||
| 
 | ||||
|   app.get('/api/v1/streaming/public/local', (req, res) => { | ||||
|     streamFrom('timeline:public:local', req, streamToHttp(req, res), streamHttpEnd(req), true); | ||||
|     const onlyMedia = req.query.only_media === '1' || req.query.only_media === 'true'; | ||||
|     const channel   = onlyMedia ? 'timeline:public:local:media' : 'timeline:public:local'; | ||||
| 
 | ||||
|     streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true); | ||||
|   }); | ||||
| 
 | ||||
|   app.get('/api/v1/streaming/direct', (req, res) => { | ||||
|  | @ -521,6 +529,12 @@ const startWorker = (workerId) => { | |||
|     case 'public:local': | ||||
|       streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true); | ||||
|       break; | ||||
|     case 'public:media': | ||||
|       streamFrom('timeline:public:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); | ||||
|       break; | ||||
|     case 'public:local:media': | ||||
|       streamFrom('timeline:public:local:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); | ||||
|       break; | ||||
|     case 'direct': | ||||
|       streamFrom(`timeline:direct:${req.accountId}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true); | ||||
|       break; | ||||
|  |  | |||
		Reference in a new issue