diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 30e643363a..8365961b6c 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -195,3 +195,10 @@ export const connectDirectStream = () => */ export const connectListStream = listId => connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) }); + +/** + * @param {string} accountId + * @returns {function(): void} + */ +export const connectProfileStream = accountId => + connectTimelineStream(`account:${accountId}`, 'profile', { account_id: accountId }); diff --git a/app/javascript/mastodon/features/account_timeline/index.jsx b/app/javascript/mastodon/features/account_timeline/index.jsx index 105c2e4e50..8cdddb9a27 100644 --- a/app/javascript/mastodon/features/account_timeline/index.jsx +++ b/app/javascript/mastodon/features/account_timeline/index.jsx @@ -7,21 +7,20 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; +import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts'; +import { fetchFeaturedTags } from 'mastodon/actions/featured_tags'; +import { connectProfileStream } from 'mastodon/actions/streaming'; +import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'mastodon/actions/timelines'; +import { ColumnBackButton } from 'mastodon/components/column_back_button'; +import { LoadingIndicator } from 'mastodon/components/loading_indicator'; +import StatusList from 'mastodon/components/status_list'; import { TimelineHint } from 'mastodon/components/timeline_hint'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; -import { me } from 'mastodon/initial_state'; +import Column from 'mastodon/features/ui/components/column'; import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; import { getAccountHidden } from 'mastodon/selectors'; import { useAppSelector } from 'mastodon/store'; -import { lookupAccount, fetchAccount } from '../../actions/accounts'; -import { fetchFeaturedTags } from '../../actions/featured_tags'; -import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines'; -import { ColumnBackButton } from '../../components/column_back_button'; -import { LoadingIndicator } from '../../components/loading_indicator'; -import StatusList from '../../components/status_list'; -import Column from '../ui/components/column'; - import { LimitedAccountHint } from './components/limited_account_hint'; import HeaderContainer from './containers/header_container'; @@ -114,9 +113,12 @@ class AccountTimeline extends ImmutablePureComponent { dispatch(fetchFeaturedTags(accountId)); dispatch(expandAccountTimeline(accountId, { withReplies, tagged })); - if (accountId === me) { - dispatch(connectTimeline(`account:${me}`)); + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; } + + this.disconnect = dispatch(connectProfileStream(accountId)); } componentDidMount () { @@ -142,17 +144,12 @@ class AccountTimeline extends ImmutablePureComponent { } dispatch(expandAccountTimeline(accountId, { withReplies, tagged })); } - - if (prevProps.accountId === me && accountId !== me) { - dispatch(disconnectTimeline({ timeline: `account:${me}` })); - } } componentWillUnmount () { - const { dispatch, accountId } = this.props; - - if (accountId === me) { - dispatch(disconnectTimeline({ timeline: `account:${me}` })); + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; } } diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 3c084bc857..aebb810b21 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -63,6 +63,7 @@ class FanOutOnWriteService < BaseService def fan_out_to_public_streams! broadcast_to_hashtag_streams! broadcast_to_public_streams! + broadcast_to_profile_streams! end def deliver_to_self! @@ -145,6 +146,10 @@ class FanOutOnWriteService < BaseService end end + def broadcast_to_profile_streams! + redis.publish("timeline:profile:#{@status.account_id}:public", anonymous_payload) + end + def deliver_to_conversation! AccountConversation.add_status(@account, @status) unless update? end diff --git a/streaming/index.js b/streaming/index.js index e00da1bb83..6af06c56a9 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -420,6 +420,8 @@ const startServer = async () => { return 'direct'; case '/api/v1/streaming/list': return 'list'; + case '/api/v1/streaming/profile': + return 'profile'; default: return undefined; } @@ -972,6 +974,7 @@ const startServer = async () => { * @property {string} [tag] * @property {string} [list] * @property {string} [only_media] + * @property {string} [account_id] */ /** @@ -1096,6 +1099,17 @@ const startServer = async () => { reject(new AuthenticationError('Not authorized to stream this list')); }); + break; + case 'profile': + if (!params.account_id) { + reject(new RequestError('Missing account id parameter')); + return; + } + + resolve({ + channelIds: [`timeline:profile:${params.account_id}:public`], + options: { needsFiltering: true }, + }); break; default: reject(new RequestError('Unknown stream type'));