index.jsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import PropTypes from 'prop-types';
  2. import { useRef, useCallback, useEffect } from 'react';
  3. import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
  4. import { Helmet } from 'react-helmet';
  5. import { NavLink } from 'react-router-dom';
  6. import PublicIcon from '@/material-icons/400-24px/public.svg?react';
  7. import { addColumn } from 'mastodon/actions/columns';
  8. import { changeSetting } from 'mastodon/actions/settings';
  9. import { connectPublicStream, connectCommunityStream } from 'mastodon/actions/streaming';
  10. import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines';
  11. import { DismissableBanner } from 'mastodon/components/dismissable_banner';
  12. import initialState, { domain } from 'mastodon/initial_state';
  13. import { useAppDispatch, useAppSelector } from 'mastodon/store';
  14. import Column from '../../components/column';
  15. import ColumnHeader from '../../components/column_header';
  16. import SettingToggle from '../notifications/components/setting_toggle';
  17. import StatusListContainer from '../ui/containers/status_list_container';
  18. const messages = defineMessages({
  19. title: { id: 'column.firehose', defaultMessage: 'Live feeds' },
  20. });
  21. // TODO: use a proper React context later on
  22. const useIdentity = () => ({
  23. signedIn: !!initialState.meta.me,
  24. accountId: initialState.meta.me,
  25. disabledAccountId: initialState.meta.disabled_account_id,
  26. accessToken: initialState.meta.access_token,
  27. permissions: initialState.role ? initialState.role.permissions : 0,
  28. });
  29. const ColumnSettings = () => {
  30. const dispatch = useAppDispatch();
  31. const settings = useAppSelector((state) => state.getIn(['settings', 'firehose']));
  32. const onChange = useCallback(
  33. (key, checked) => dispatch(changeSetting(['firehose', ...key], checked)),
  34. [dispatch],
  35. );
  36. return (
  37. <div>
  38. <div className='column-settings__row'>
  39. <SettingToggle
  40. settings={settings}
  41. settingPath={['onlyMedia']}
  42. onChange={onChange}
  43. label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
  44. />
  45. </div>
  46. </div>
  47. );
  48. };
  49. const Firehose = ({ feedType, multiColumn }) => {
  50. const dispatch = useAppDispatch();
  51. const intl = useIntl();
  52. const { signedIn } = useIdentity();
  53. const columnRef = useRef(null);
  54. const onlyMedia = useAppSelector((state) => state.getIn(['settings', 'firehose', 'onlyMedia'], false));
  55. const hasUnread = useAppSelector((state) => state.getIn(['timelines', `${feedType}${onlyMedia ? ':media' : ''}`, 'unread'], 0) > 0);
  56. const handlePin = useCallback(
  57. () => {
  58. switch(feedType) {
  59. case 'community':
  60. dispatch(addColumn('COMMUNITY', { other: { onlyMedia } }));
  61. break;
  62. case 'public':
  63. dispatch(addColumn('PUBLIC', { other: { onlyMedia } }));
  64. break;
  65. case 'public:remote':
  66. dispatch(addColumn('REMOTE', { other: { onlyMedia, onlyRemote: true } }));
  67. break;
  68. }
  69. },
  70. [dispatch, onlyMedia, feedType],
  71. );
  72. const handleLoadMore = useCallback(
  73. (maxId) => {
  74. switch(feedType) {
  75. case 'community':
  76. dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
  77. break;
  78. case 'public':
  79. dispatch(expandPublicTimeline({ maxId, onlyMedia }));
  80. break;
  81. case 'public:remote':
  82. dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote: true }));
  83. break;
  84. }
  85. },
  86. [dispatch, onlyMedia, feedType],
  87. );
  88. const handleHeaderClick = useCallback(() => columnRef.current?.scrollTop(), []);
  89. useEffect(() => {
  90. let disconnect;
  91. switch(feedType) {
  92. case 'community':
  93. dispatch(expandCommunityTimeline({ onlyMedia }));
  94. if (signedIn) {
  95. disconnect = dispatch(connectCommunityStream({ onlyMedia }));
  96. }
  97. break;
  98. case 'public':
  99. dispatch(expandPublicTimeline({ onlyMedia }));
  100. if (signedIn) {
  101. disconnect = dispatch(connectPublicStream({ onlyMedia }));
  102. }
  103. break;
  104. case 'public:remote':
  105. dispatch(expandPublicTimeline({ onlyMedia, onlyRemote: true }));
  106. if (signedIn) {
  107. disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote: true }));
  108. }
  109. break;
  110. }
  111. return () => disconnect?.();
  112. }, [dispatch, signedIn, feedType, onlyMedia]);
  113. const prependBanner = feedType === 'community' ? (
  114. <DismissableBanner id='community_timeline'>
  115. <FormattedMessage
  116. id='dismissable_banner.community_timeline'
  117. defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.'
  118. values={{ domain }}
  119. />
  120. </DismissableBanner>
  121. ) : (
  122. <DismissableBanner id='public_timeline'>
  123. <FormattedMessage
  124. id='dismissable_banner.public_timeline'
  125. defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.'
  126. values={{ domain }}
  127. />
  128. </DismissableBanner>
  129. );
  130. const emptyMessage = feedType === 'community' ? (
  131. <FormattedMessage
  132. id='empty_column.community'
  133. defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!'
  134. />
  135. ) : (
  136. <FormattedMessage
  137. id='empty_column.public'
  138. defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up'
  139. />
  140. );
  141. return (
  142. <Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
  143. <ColumnHeader
  144. icon='globe'
  145. iconComponent={PublicIcon}
  146. active={hasUnread}
  147. title={intl.formatMessage(messages.title)}
  148. onPin={handlePin}
  149. onClick={handleHeaderClick}
  150. multiColumn={multiColumn}
  151. >
  152. <ColumnSettings />
  153. </ColumnHeader>
  154. <div className='account__section-headline'>
  155. <NavLink exact to='/public/local'>
  156. <FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' />
  157. </NavLink>
  158. <NavLink exact to='/public/remote'>
  159. <FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' />
  160. </NavLink>
  161. <NavLink exact to='/public'>
  162. <FormattedMessage tagName='div' id='firehose.all' defaultMessage='All' />
  163. </NavLink>
  164. </div>
  165. <StatusListContainer
  166. prepend={prependBanner}
  167. timelineId={`${feedType}${onlyMedia ? ':media' : ''}`}
  168. onLoadMore={handleLoadMore}
  169. trackScroll
  170. scrollKey='firehose'
  171. emptyMessage={emptyMessage}
  172. bindToDocument={!multiColumn}
  173. />
  174. <Helmet>
  175. <title>{intl.formatMessage(messages.title)}</title>
  176. <meta name='robots' content='noindex' />
  177. </Helmet>
  178. </Column>
  179. );
  180. };
  181. Firehose.propTypes = {
  182. multiColumn: PropTypes.bool,
  183. feedType: PropTypes.string,
  184. };
  185. export default Firehose;