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 PublicIcon from '@/material-icons/400-24px/public.svg?react'; 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 className='column-settings'> <section> <div className='column-settings__row'> <SettingToggle settings={settings} settingPath={['onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} /> </div> </section> </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({ maxId, 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 the social web that people on {domain} follow.' values={{ domain }} /> </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' iconComponent={PublicIcon} active={hasUnread} title={intl.formatMessage(messages.title)} onPin={handlePin} onClick={handleHeaderClick} multiColumn={multiColumn} > <ColumnSettings /> </ColumnHeader> <div className='account__section-headline'> <NavLink exact to='/public/local'> <FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' /> </NavLink> <NavLink exact to='/public/remote'> <FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' /> </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} /> <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;