From 51ddab84cc69c378b9bb10f692e0c50176b0e54a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 10 Dec 2024 11:49:15 +0100 Subject: [PATCH] Add streaming updates for profiles in web UI --- app/javascript/mastodon/actions/streaming.js | 29 +++++++++++++++ .../features/account_gallery/index.jsx | 16 +++++++++ .../features/account_timeline/index.jsx | 35 +++++++++---------- app/javascript/mastodon/selectors/index.js | 12 ++++--- app/services/fan_out_on_write_service.rb | 9 +++++ streaming/index.js | 15 ++++++++ 6 files changed, 92 insertions(+), 24 deletions(-) diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 478e0cae45..4bdd3ecfc2 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -192,3 +192,32 @@ export const connectDirectStream = () => */ export const connectListStream = listId => connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) }); + +/** + * @param {string} accountId + * @param {Object} options + * @param {boolean} options.withReplies + * @param {string} options.tagged + * @param {boolean} options.onlyMedia + * @returns {function(): void} + */ +export const connectProfileStream = (accountId, { withReplies, tagged, onlyMedia }) => + connectTimelineStream(`account:${accountId}${onlyMedia ? ':media' : ''}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, 'profile', { account_id: accountId }, { + accept (status) { + let passThrough = true; + + if (!withReplies) { + passThrough = passThrough && (status.in_reply_to_id === null || status.in_reply_to_account_id === status.account.id); + } + + if (tagged) { + passThrough = passThrough && status.tags.some(tag => tag.name === tagged); + } + + if (onlyMedia) { + passThrough = passThrough && status.media_attachments.length > 0; + } + + return passThrough; + }, + }); diff --git a/app/javascript/mastodon/features/account_gallery/index.jsx b/app/javascript/mastodon/features/account_gallery/index.jsx index 35a0fbd2c6..cb917163b1 100644 --- a/app/javascript/mastodon/features/account_gallery/index.jsx +++ b/app/javascript/mastodon/features/account_gallery/index.jsx @@ -8,6 +8,7 @@ import { connect } from 'react-redux'; import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts'; import { openModal } from 'mastodon/actions/modal'; +import { connectProfileStream } from 'mastodon/actions/streaming'; import { ColumnBackButton } from 'mastodon/components/column_back_button'; import { LoadMore } from 'mastodon/components/load_more'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; @@ -90,7 +91,15 @@ class AccountGallery extends ImmutablePureComponent { const { accountId, isAccount, dispatch } = this.props; if (!isAccount) dispatch(fetchAccount(accountId)); + dispatch(expandAccountMediaTimeline(accountId)); + + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; + } + + this.disconnect = dispatch(connectProfileStream(accountId, { onlyMedia: true })); } componentDidMount () { @@ -103,6 +112,13 @@ class AccountGallery extends ImmutablePureComponent { } } + componentWillUnmount () { + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; + } + } + componentDidUpdate (prevProps) { const { params: { acct }, accountId, dispatch } = this.props; diff --git a/app/javascript/mastodon/features/account_timeline/index.jsx b/app/javascript/mastodon/features/account_timeline/index.jsx index 105c2e4e50..18b9adec91 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, { withReplies, tagged })); } 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/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index 345ceac49a..d5498cc41d 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -93,14 +93,16 @@ export const makeGetReport = () => createSelector([ export const getAccountGallery = createSelector([ (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()), - state => state.get('statuses'), - (state, id) => state.getIn(['accounts', id]), -], (statusIds, statuses, account) => { + state => state.get('statuses'), +], (statusIds, statuses) => { let medias = ImmutableList(); statusIds.forEach(statusId => { - const status = statuses.get(statusId).set('account', account); - medias = medias.concat(status.get('media_attachments').map(media => media.set('status', status))); + const status = statuses.get(statusId); + + if (status) { + medias = medias.concat(status.get('media_attachments').map(media => media.set('status', status))); + } }); return medias; diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 3c084bc857..7bdfe08033 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -18,6 +18,7 @@ class FanOutOnWriteService < BaseService warm_payload_cache! fan_out_to_local_recipients! + fan_out_to_profile_streams! if distributable? fan_out_to_public_recipients! if broadcastable? fan_out_to_public_streams! if broadcastable? end @@ -145,6 +146,10 @@ class FanOutOnWriteService < BaseService end end + def fan_out_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 @@ -168,6 +173,10 @@ class FanOutOnWriteService < BaseService @options[:update] end + def distributable? + @status.distributable? + end + def broadcastable? @status.public_visibility? && !@status.reblog? && !@account.silenced? end diff --git a/streaming/index.js b/streaming/index.js index e00da1bb83..4d52d4a11d 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -87,6 +87,7 @@ const PUBLIC_CHANNELS = [ 'public:remote:media', 'hashtag', 'hashtag:local', + 'profile', ]; // Used for priming the counters/gauges for the various metrics that are @@ -420,6 +421,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 +975,7 @@ const startServer = async () => { * @property {string} [tag] * @property {string} [list] * @property {string} [only_media] + * @property {string} [account_id] */ /** @@ -1096,6 +1100,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'));