Change local and federated timelines to be in a single firehose column (#25641)
This commit is contained in:
		
					parent
					
						
							
								0139b1c8e1
							
						
					
				
			
			
				commit
				
					
						cea9db5a0b
					
				
			
		
					 6 changed files with 234 additions and 12 deletions
				
			
		
							
								
								
									
										210
									
								
								app/javascript/mastodon/features/firehose/index.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								app/javascript/mastodon/features/firehose/index.jsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,210 @@ | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  | import { useRef, useCallback, useEffect } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; | ||||||
|  | 
 | ||||||
|  | import { Helmet } from 'react-helmet'; | ||||||
|  | import { NavLink } from 'react-router-dom'; | ||||||
|  | 
 | ||||||
|  | import { addColumn } from 'mastodon/actions/columns'; | ||||||
|  | import { changeSetting } from 'mastodon/actions/settings'; | ||||||
|  | import { connectPublicStream, connectCommunityStream } from 'mastodon/actions/streaming'; | ||||||
|  | import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines'; | ||||||
|  | import DismissableBanner from 'mastodon/components/dismissable_banner'; | ||||||
|  | import initialState, { domain } from 'mastodon/initial_state'; | ||||||
|  | import { useAppDispatch, useAppSelector } from 'mastodon/store'; | ||||||
|  | 
 | ||||||
|  | import Column from '../../components/column'; | ||||||
|  | import ColumnHeader from '../../components/column_header'; | ||||||
|  | import SettingToggle from '../notifications/components/setting_toggle'; | ||||||
|  | import StatusListContainer from '../ui/containers/status_list_container'; | ||||||
|  | 
 | ||||||
|  | const messages = defineMessages({ | ||||||
|  |   title: { id: 'column.firehose', defaultMessage: 'Live feeds' }, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | // TODO: use a proper React context later on | ||||||
|  | const useIdentity = () => ({ | ||||||
|  |   signedIn: !!initialState.meta.me, | ||||||
|  |   accountId: initialState.meta.me, | ||||||
|  |   disabledAccountId: initialState.meta.disabled_account_id, | ||||||
|  |   accessToken: initialState.meta.access_token, | ||||||
|  |   permissions: initialState.role ? initialState.role.permissions : 0, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const ColumnSettings = () => { | ||||||
|  |   const dispatch = useAppDispatch(); | ||||||
|  |   const settings = useAppSelector((state) => state.getIn(['settings', 'firehose'])); | ||||||
|  |   const onChange = useCallback( | ||||||
|  |     (key, checked) => dispatch(changeSetting(['firehose', ...key], checked)), | ||||||
|  |     [dispatch], | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div> | ||||||
|  |       <div className='column-settings__row'> | ||||||
|  |         <SettingToggle | ||||||
|  |           settings={settings} | ||||||
|  |           settingPath={['onlyMedia']} | ||||||
|  |           onChange={onChange} | ||||||
|  |           label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const Firehose = ({ feedType, multiColumn }) => { | ||||||
|  |   const dispatch = useAppDispatch(); | ||||||
|  |   const intl = useIntl(); | ||||||
|  |   const { signedIn } = useIdentity(); | ||||||
|  |   const columnRef = useRef(null); | ||||||
|  | 
 | ||||||
|  |   const onlyMedia = useAppSelector((state) => state.getIn(['settings', 'firehose', 'onlyMedia'], false)); | ||||||
|  |   const hasUnread = useAppSelector((state) => state.getIn(['timelines', `${feedType}${onlyMedia ? ':media' : ''}`, 'unread'], 0) > 0); | ||||||
|  | 
 | ||||||
|  |   const handlePin = useCallback( | ||||||
|  |     () => { | ||||||
|  |       switch(feedType) { | ||||||
|  |       case 'community': | ||||||
|  |         dispatch(addColumn('COMMUNITY', { other: { onlyMedia } })); | ||||||
|  |         break; | ||||||
|  |       case 'public': | ||||||
|  |         dispatch(addColumn('PUBLIC', { other: { onlyMedia } })); | ||||||
|  |         break; | ||||||
|  |       case 'public:remote': | ||||||
|  |         dispatch(addColumn('REMOTE', { other: { onlyMedia, onlyRemote: true } })); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     [dispatch, onlyMedia, feedType], | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const handleLoadMore = useCallback( | ||||||
|  |     (maxId) => { | ||||||
|  |       switch(feedType) { | ||||||
|  |       case 'community': | ||||||
|  |         dispatch(expandCommunityTimeline({ onlyMedia })); | ||||||
|  |         break; | ||||||
|  |       case 'public': | ||||||
|  |         dispatch(expandPublicTimeline({ maxId, onlyMedia })); | ||||||
|  |         break; | ||||||
|  |       case 'public:remote': | ||||||
|  |         dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote: true })); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     [dispatch, onlyMedia, feedType], | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const handleHeaderClick = useCallback(() => columnRef.current?.scrollTop(), []); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     let disconnect; | ||||||
|  | 
 | ||||||
|  |     switch(feedType) { | ||||||
|  |     case 'community': | ||||||
|  |       dispatch(expandCommunityTimeline({ onlyMedia })); | ||||||
|  |       if (signedIn) { | ||||||
|  |         disconnect = dispatch(connectCommunityStream({ onlyMedia })); | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     case 'public': | ||||||
|  |       dispatch(expandPublicTimeline({ onlyMedia })); | ||||||
|  |       if (signedIn) { | ||||||
|  |         disconnect = dispatch(connectPublicStream({ onlyMedia })); | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     case 'public:remote': | ||||||
|  |       dispatch(expandPublicTimeline({ onlyMedia, onlyRemote: true })); | ||||||
|  |       if (signedIn) { | ||||||
|  |         disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote: true })); | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return () => disconnect?.(); | ||||||
|  |   }, [dispatch, signedIn, feedType, onlyMedia]); | ||||||
|  | 
 | ||||||
|  |   const prependBanner = feedType === 'community' ? ( | ||||||
|  |     <DismissableBanner id='community_timeline'> | ||||||
|  |       <FormattedMessage | ||||||
|  |         id='dismissable_banner.community_timeline' | ||||||
|  |         defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' | ||||||
|  |         values={{ domain }} | ||||||
|  |       /> | ||||||
|  |     </DismissableBanner> | ||||||
|  |   ) : ( | ||||||
|  |    <DismissableBanner id='public_timeline'> | ||||||
|  |      <FormattedMessage | ||||||
|  |        id='dismissable_banner.public_timeline' | ||||||
|  |        defaultMessage='These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.' | ||||||
|  |      /> | ||||||
|  |    </DismissableBanner> | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const emptyMessage = feedType === 'community' ? ( | ||||||
|  |     <FormattedMessage | ||||||
|  |       id='empty_column.community' | ||||||
|  |       defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' | ||||||
|  |     /> | ||||||
|  |   ) : ( | ||||||
|  |    <FormattedMessage | ||||||
|  |      id='empty_column.public' | ||||||
|  |      defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' | ||||||
|  |    /> | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}> | ||||||
|  |       <ColumnHeader | ||||||
|  |         icon='globe' | ||||||
|  |         active={hasUnread} | ||||||
|  |         title={intl.formatMessage(messages.title)} | ||||||
|  |         onPin={handlePin} | ||||||
|  |         onClick={handleHeaderClick} | ||||||
|  |         multiColumn={multiColumn} | ||||||
|  |       > | ||||||
|  |         <ColumnSettings /> | ||||||
|  |       </ColumnHeader> | ||||||
|  | 
 | ||||||
|  |       <div className='scrollable scrollable--flex'> | ||||||
|  |         <div className='account__section-headline'> | ||||||
|  |           <NavLink exact to='/public/local'> | ||||||
|  |             <FormattedMessage tagName='div' id='firehose.local' defaultMessage='Local' /> | ||||||
|  |           </NavLink> | ||||||
|  | 
 | ||||||
|  |           <NavLink exact to='/public/remote'> | ||||||
|  |             <FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Remote' /> | ||||||
|  |           </NavLink> | ||||||
|  | 
 | ||||||
|  |           <NavLink exact to='/public'> | ||||||
|  |             <FormattedMessage tagName='div' id='firehose.all' defaultMessage='All' /> | ||||||
|  |           </NavLink> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <StatusListContainer | ||||||
|  |           prepend={prependBanner} | ||||||
|  |           timelineId={`${feedType}${onlyMedia ? ':media' : ''}`} | ||||||
|  |           onLoadMore={handleLoadMore} | ||||||
|  |           trackScroll | ||||||
|  |           scrollKey='firehose' | ||||||
|  |           emptyMessage={emptyMessage} | ||||||
|  |           bindToDocument={!multiColumn} | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <Helmet> | ||||||
|  |         <title>{intl.formatMessage(messages.title)}</title> | ||||||
|  |         <meta name='robots' content='noindex' /> | ||||||
|  |       </Helmet> | ||||||
|  |     </Column> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | Firehose.propTypes = { | ||||||
|  |   multiColumn: PropTypes.bool, | ||||||
|  |   feedType: PropTypes.string, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default Firehose; | ||||||
|  | @ -20,8 +20,7 @@ const messages = defineMessages({ | ||||||
|   home: { id: 'tabs_bar.home', defaultMessage: 'Home' }, |   home: { id: 'tabs_bar.home', defaultMessage: 'Home' }, | ||||||
|   notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, |   notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, | ||||||
|   explore: { id: 'explore.title', defaultMessage: 'Explore' }, |   explore: { id: 'explore.title', defaultMessage: 'Explore' }, | ||||||
|   local: { id: 'tabs_bar.local_timeline', defaultMessage: 'Local' }, |   firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' }, | ||||||
|   federated: { id: 'tabs_bar.federated_timeline', defaultMessage: 'Federated' }, |  | ||||||
|   direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' }, |   direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' }, | ||||||
|   favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, |   favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, | ||||||
|   bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, |   bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, | ||||||
|  | @ -43,6 +42,10 @@ class NavigationPanel extends Component { | ||||||
|     intl: PropTypes.object.isRequired, |     intl: PropTypes.object.isRequired, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   isFirehoseActive = (match, location) => { | ||||||
|  |     return match || location.pathname.startsWith('/public'); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { intl } = this.props; |     const { intl } = this.props; | ||||||
|     const { signedIn, disabledAccountId } = this.context.identity; |     const { signedIn, disabledAccountId } = this.context.identity; | ||||||
|  | @ -69,10 +72,7 @@ class NavigationPanel extends Component { | ||||||
|         )} |         )} | ||||||
| 
 | 
 | ||||||
|         {(signedIn || timelinePreview) && ( |         {(signedIn || timelinePreview) && ( | ||||||
|           <> |           <ColumnLink transparent to='/public/local' isActive={this.isFirehoseActive} icon='globe' text={intl.formatMessage(messages.firehose)} /> | ||||||
|             <ColumnLink transparent to='/public/local' icon='users' text={intl.formatMessage(messages.local)} /> |  | ||||||
|             <ColumnLink transparent exact to='/public' icon='globe' text={intl.formatMessage(messages.federated)} /> |  | ||||||
|           </> |  | ||||||
|         )} |         )} | ||||||
| 
 | 
 | ||||||
|         {!signedIn && ( |         {!signedIn && ( | ||||||
|  |  | ||||||
|  | @ -36,8 +36,7 @@ import { | ||||||
|   Status, |   Status, | ||||||
|   GettingStarted, |   GettingStarted, | ||||||
|   KeyboardShortcuts, |   KeyboardShortcuts, | ||||||
|   PublicTimeline, |   Firehose, | ||||||
|   CommunityTimeline, |  | ||||||
|   AccountTimeline, |   AccountTimeline, | ||||||
|   AccountGallery, |   AccountGallery, | ||||||
|   HomeTimeline, |   HomeTimeline, | ||||||
|  | @ -188,8 +187,11 @@ class SwitchingColumnsArea extends PureComponent { | ||||||
|           <WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} /> |           <WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} /> | ||||||
| 
 | 
 | ||||||
|           <WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} /> |           <WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} /> | ||||||
|           <WrappedRoute path={['/public', '/timelines/public']} exact component={PublicTimeline} content={children} /> |           <Redirect from='/timelines/public' to='/public' exact /> | ||||||
|           <WrappedRoute path={['/public/local', '/timelines/public/local']} exact component={CommunityTimeline} content={children} /> |           <Redirect from='/timelines/public/local' to='/public/local' exact /> | ||||||
|  |           <WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} /> | ||||||
|  |           <WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} content={children} /> | ||||||
|  |           <WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} /> | ||||||
|           <WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} /> |           <WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} /> | ||||||
|           <WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} /> |           <WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} /> | ||||||
|           <WrappedRoute path='/lists/:id' component={ListTimeline} content={children} /> |           <WrappedRoute path='/lists/:id' component={ListTimeline} content={children} /> | ||||||
|  |  | ||||||
|  | @ -22,6 +22,10 @@ export function CommunityTimeline () { | ||||||
|   return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline'); |   return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function Firehose () { | ||||||
|  |   return import(/* webpackChunkName: "features/firehose" */'../../firehose'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export function HashtagTimeline () { | export function HashtagTimeline () { | ||||||
|   return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline'); |   return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline'); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -114,6 +114,7 @@ | ||||||
|   "column.directory": "Browse profiles", |   "column.directory": "Browse profiles", | ||||||
|   "column.domain_blocks": "Blocked domains", |   "column.domain_blocks": "Blocked domains", | ||||||
|   "column.favourites": "Favourites", |   "column.favourites": "Favourites", | ||||||
|  |   "column.firehose": "Live feeds", | ||||||
|   "column.follow_requests": "Follow requests", |   "column.follow_requests": "Follow requests", | ||||||
|   "column.home": "Home", |   "column.home": "Home", | ||||||
|   "column.lists": "Lists", |   "column.lists": "Lists", | ||||||
|  | @ -267,6 +268,9 @@ | ||||||
|   "filter_modal.select_filter.subtitle": "Use an existing category or create a new one", |   "filter_modal.select_filter.subtitle": "Use an existing category or create a new one", | ||||||
|   "filter_modal.select_filter.title": "Filter this post", |   "filter_modal.select_filter.title": "Filter this post", | ||||||
|   "filter_modal.title.status": "Filter a post", |   "filter_modal.title.status": "Filter a post", | ||||||
|  |   "firehose.all": "All", | ||||||
|  |   "firehose.local": "Local", | ||||||
|  |   "firehose.remote": "Remote", | ||||||
|   "follow_request.authorize": "Authorize", |   "follow_request.authorize": "Authorize", | ||||||
|   "follow_request.reject": "Reject", |   "follow_request.reject": "Reject", | ||||||
|   "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", |   "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", | ||||||
|  | @ -649,9 +653,7 @@ | ||||||
|   "subscribed_languages.target": "Change subscribed languages for {target}", |   "subscribed_languages.target": "Change subscribed languages for {target}", | ||||||
|   "suggestions.dismiss": "Dismiss suggestion", |   "suggestions.dismiss": "Dismiss suggestion", | ||||||
|   "suggestions.header": "You might be interested in…", |   "suggestions.header": "You might be interested in…", | ||||||
|   "tabs_bar.federated_timeline": "Federated", |  | ||||||
|   "tabs_bar.home": "Home", |   "tabs_bar.home": "Home", | ||||||
|   "tabs_bar.local_timeline": "Local", |  | ||||||
|   "tabs_bar.notifications": "Notifications", |   "tabs_bar.notifications": "Notifications", | ||||||
|   "time_remaining.days": "{number, plural, one {# day} other {# days}} left", |   "time_remaining.days": "{number, plural, one {# day} other {# days}} left", | ||||||
|   "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", |   "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", | ||||||
|  |  | ||||||
|  | @ -79,6 +79,10 @@ const initialState = ImmutableMap({ | ||||||
|     }), |     }), | ||||||
|   }), |   }), | ||||||
| 
 | 
 | ||||||
|  |   firehose: ImmutableMap({ | ||||||
|  |     onlyMedia: false, | ||||||
|  |   }), | ||||||
|  | 
 | ||||||
|   community: ImmutableMap({ |   community: ImmutableMap({ | ||||||
|     regex: ImmutableMap({ |     regex: ImmutableMap({ | ||||||
|       body: '', |       body: '', | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue