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' }, | ||||
|   notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, | ||||
|   explore: { id: 'explore.title', defaultMessage: 'Explore' }, | ||||
|   local: { id: 'tabs_bar.local_timeline', defaultMessage: 'Local' }, | ||||
|   federated: { id: 'tabs_bar.federated_timeline', defaultMessage: 'Federated' }, | ||||
|   firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' }, | ||||
|   direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' }, | ||||
|   favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, | ||||
|   bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, | ||||
|  | @ -43,6 +42,10 @@ class NavigationPanel extends Component { | |||
|     intl: PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   isFirehoseActive = (match, location) => { | ||||
|     return match || location.pathname.startsWith('/public'); | ||||
|   }; | ||||
| 
 | ||||
|   render () { | ||||
|     const { intl } = this.props; | ||||
|     const { signedIn, disabledAccountId } = this.context.identity; | ||||
|  | @ -69,10 +72,7 @@ class NavigationPanel extends Component { | |||
|         )} | ||||
| 
 | ||||
|         {(signedIn || timelinePreview) && ( | ||||
|           <> | ||||
|             <ColumnLink transparent to='/public/local' icon='users' text={intl.formatMessage(messages.local)} /> | ||||
|             <ColumnLink transparent exact to='/public' icon='globe' text={intl.formatMessage(messages.federated)} /> | ||||
|           </> | ||||
|           <ColumnLink transparent to='/public/local' isActive={this.isFirehoseActive} icon='globe' text={intl.formatMessage(messages.firehose)} /> | ||||
|         )} | ||||
| 
 | ||||
|         {!signedIn && ( | ||||
|  |  | |||
|  | @ -36,8 +36,7 @@ import { | |||
|   Status, | ||||
|   GettingStarted, | ||||
|   KeyboardShortcuts, | ||||
|   PublicTimeline, | ||||
|   CommunityTimeline, | ||||
|   Firehose, | ||||
|   AccountTimeline, | ||||
|   AccountGallery, | ||||
|   HomeTimeline, | ||||
|  | @ -188,8 +187,11 @@ class SwitchingColumnsArea extends PureComponent { | |||
|           <WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} /> | ||||
| 
 | ||||
|           <WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} /> | ||||
|           <WrappedRoute path={['/public', '/timelines/public']} exact component={PublicTimeline} content={children} /> | ||||
|           <WrappedRoute path={['/public/local', '/timelines/public/local']} exact component={CommunityTimeline} content={children} /> | ||||
|           <Redirect from='/timelines/public' to='/public' exact /> | ||||
|           <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='/tags/:id' component={HashtagTimeline} 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'); | ||||
| } | ||||
| 
 | ||||
| export function Firehose () { | ||||
|   return import(/* webpackChunkName: "features/firehose" */'../../firehose'); | ||||
| } | ||||
| 
 | ||||
| export function HashtagTimeline () { | ||||
|   return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline'); | ||||
| } | ||||
|  |  | |||
|  | @ -114,6 +114,7 @@ | |||
|   "column.directory": "Browse profiles", | ||||
|   "column.domain_blocks": "Blocked domains", | ||||
|   "column.favourites": "Favourites", | ||||
|   "column.firehose": "Live feeds", | ||||
|   "column.follow_requests": "Follow requests", | ||||
|   "column.home": "Home", | ||||
|   "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.title": "Filter this post", | ||||
|   "filter_modal.title.status": "Filter a post", | ||||
|   "firehose.all": "All", | ||||
|   "firehose.local": "Local", | ||||
|   "firehose.remote": "Remote", | ||||
|   "follow_request.authorize": "Authorize", | ||||
|   "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.", | ||||
|  | @ -649,9 +653,7 @@ | |||
|   "subscribed_languages.target": "Change subscribed languages for {target}", | ||||
|   "suggestions.dismiss": "Dismiss suggestion", | ||||
|   "suggestions.header": "You might be interested in…", | ||||
|   "tabs_bar.federated_timeline": "Federated", | ||||
|   "tabs_bar.home": "Home", | ||||
|   "tabs_bar.local_timeline": "Local", | ||||
|   "tabs_bar.notifications": "Notifications", | ||||
|   "time_remaining.days": "{number, plural, one {# day} other {# days}} left", | ||||
|   "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", | ||||
|  |  | |||
|  | @ -79,6 +79,10 @@ const initialState = ImmutableMap({ | |||
|     }), | ||||
|   }), | ||||
| 
 | ||||
|   firehose: ImmutableMap({ | ||||
|     onlyMedia: false, | ||||
|   }), | ||||
| 
 | ||||
|   community: ImmutableMap({ | ||||
|     regex: ImmutableMap({ | ||||
|       body: '', | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue