From 1addfb96a9a0519cc9a2fa05822efdbd3a51945f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jul 2023 16:38:21 -0500 Subject: [PATCH] Fix streaming follow update Fixes https://gitlab.com/soapbox-pub/soapbox/-/issues/1469 --- app/soapbox/actions/streaming.ts | 60 ++++++++++++++++----- app/soapbox/entity-store/hooks/useEntity.ts | 3 +- app/soapbox/entity-store/selectors.ts | 9 ++++ app/soapbox/reducers/relationships.ts | 39 -------------- 4 files changed, 59 insertions(+), 52 deletions(-) diff --git a/app/soapbox/actions/streaming.ts b/app/soapbox/actions/streaming.ts index f050c7b15..6a4219af6 100644 --- a/app/soapbox/actions/streaming.ts +++ b/app/soapbox/actions/streaming.ts @@ -1,4 +1,7 @@ import { getLocale, getSettings } from 'soapbox/actions/settings'; +import { importEntities } from 'soapbox/entity-store/actions'; +import { Entities } from 'soapbox/entity-store/entities'; +import { selectEntity } from 'soapbox/entity-store/selectors'; import messages from 'soapbox/locales/messages'; import { ChatKeys, IChat, isLastMessage } from 'soapbox/queries/chats'; import { queryClient } from 'soapbox/queries/client'; @@ -26,21 +29,11 @@ import { } from './timelines'; import type { IStatContext } from 'soapbox/contexts/stat-context'; +import type { Relationship } from 'soapbox/schemas'; import type { AppDispatch, RootState } from 'soapbox/store'; import type { APIEntity, Chat } from 'soapbox/types/entities'; const STREAMING_CHAT_UPDATE = 'STREAMING_CHAT_UPDATE'; -const STREAMING_FOLLOW_RELATIONSHIPS_UPDATE = 'STREAMING_FOLLOW_RELATIONSHIPS_UPDATE'; - -const updateFollowRelationships = (relationships: APIEntity) => - (dispatch: AppDispatch, getState: () => RootState) => { - const me = getState().me; - return dispatch({ - type: STREAMING_FOLLOW_RELATIONSHIPS_UPDATE, - me, - ...relationships, - }); - }; const removeChatMessage = (payload: string) => { const data = JSON.parse(payload); @@ -190,9 +183,52 @@ const connectTimelineStream = ( }; }); +function followStateToRelationship(followState: string) { + switch (followState) { + case 'follow_pending': + return { following: false, requested: true }; + case 'follow_accept': + return { following: true, requested: false }; + case 'follow_reject': + return { following: false, requested: false }; + default: + return {}; + } +} + +interface FollowUpdate { + state: 'follow_pending' | 'follow_accept' | 'follow_reject' + follower: { + id: string + follower_count: number + following_count: number + } + following: { + id: string + follower_count: number + following_count: number + } +} + +function updateFollowRelationships(update: FollowUpdate) { + return (dispatch: AppDispatch, getState: () => RootState) => { + const me = getState().me; + const relationship = selectEntity(getState(), Entities.RELATIONSHIPS, update.following.id); + + if (update.follower.id === me && relationship) { + const updated = { + ...relationship, + ...followStateToRelationship(update.state), + }; + + // Add a small delay to deal with API race conditions. + setTimeout(() => dispatch(importEntities([updated], Entities.RELATIONSHIPS)), 300); + } + }; +} + export { STREAMING_CHAT_UPDATE, - STREAMING_FOLLOW_RELATIONSHIPS_UPDATE, connectTimelineStream, type TimelineStreamOpts, }; diff --git a/app/soapbox/entity-store/hooks/useEntity.ts b/app/soapbox/entity-store/hooks/useEntity.ts index 14c84382c..af4aa06bc 100644 --- a/app/soapbox/entity-store/hooks/useEntity.ts +++ b/app/soapbox/entity-store/hooks/useEntity.ts @@ -5,6 +5,7 @@ import z from 'zod'; import { useAppDispatch, useAppSelector, useLoading } from 'soapbox/hooks'; import { importEntities } from '../actions'; +import { selectEntity } from '../selectors'; import type { Entity } from '../types'; import type { EntitySchema, EntityPath, EntityFn } from './types'; @@ -34,7 +35,7 @@ function useEntity( const defaultSchema = z.custom(); const schema = opts.schema || defaultSchema; - const entity = useAppSelector(state => state.entities[entityType]?.store[entityId] as TEntity | undefined); + const entity = useAppSelector(state => selectEntity(state, entityType, entityId)); const isEnabled = opts.enabled ?? true; const isLoading = isFetching && !entity; diff --git a/app/soapbox/entity-store/selectors.ts b/app/soapbox/entity-store/selectors.ts index d1017c5b6..3e827b360 100644 --- a/app/soapbox/entity-store/selectors.ts +++ b/app/soapbox/entity-store/selectors.ts @@ -26,6 +26,14 @@ function useListState(path: EntitiesPath, key: return useAppSelector(state => selectListState(state, path, key)); } +/** Get a single entity by its ID from the store. */ +function selectEntity( + state: RootState, + entityType: string, id: string, +): TEntity | undefined { + return state.entities[entityType]?.store[id] as TEntity | undefined; +} + /** Get list of entities from Redux. */ function selectEntities(state: RootState, path: EntitiesPath): readonly TEntity[] { const cache = selectCache(state, path); @@ -63,5 +71,6 @@ export { selectListState, useListState, selectEntities, + selectEntity, findEntity, }; \ No newline at end of file diff --git a/app/soapbox/reducers/relationships.ts b/app/soapbox/reducers/relationships.ts index 28a30e148..259885426 100644 --- a/app/soapbox/reducers/relationships.ts +++ b/app/soapbox/reducers/relationships.ts @@ -1,7 +1,6 @@ import { Map as ImmutableMap } from 'immutable'; import get from 'lodash/get'; -import { STREAMING_FOLLOW_RELATIONSHIPS_UPDATE } from 'soapbox/actions/streaming'; import { type Relationship, relationshipSchema } from 'soapbox/schemas'; import { ACCOUNT_NOTE_SUBMIT_SUCCESS } from '../actions/account-notes'; @@ -67,44 +66,12 @@ const importPleromaAccounts = (state: State, accounts: APIEntities) => { return state; }; -const followStateToRelationship = (followState: string) => { - switch (followState) { - case 'follow_pending': - return { following: false, requested: true }; - case 'follow_accept': - return { following: true, requested: false }; - case 'follow_reject': - return { following: false, requested: false }; - default: - return {}; - } -}; - -const updateFollowRelationship = (state: State, id: string, followState: string) => { - const relationship = state.get(id) || relationshipSchema.parse({ id }); - - return state.set(id, { - ...relationship, - ...followStateToRelationship(followState), - }); -}; - export default function relationships(state: State = ImmutableMap(), action: AnyAction) { switch (action.type) { case ACCOUNT_IMPORT: return importPleromaAccount(state, action.account); case ACCOUNTS_IMPORT: return importPleromaAccounts(state, action.accounts); - // case ACCOUNT_FOLLOW_REQUEST: - // return state.setIn([action.id, 'following'], true); - // case ACCOUNT_FOLLOW_FAIL: - // return state.setIn([action.id, 'following'], false); - // case ACCOUNT_UNFOLLOW_REQUEST: - // return state.setIn([action.id, 'following'], false); - // case ACCOUNT_UNFOLLOW_FAIL: - // return state.setIn([action.id, 'following'], true); - // case ACCOUNT_FOLLOW_SUCCESS: - // case ACCOUNT_UNFOLLOW_SUCCESS: case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_UNBLOCK_SUCCESS: case ACCOUNT_MUTE_SUCCESS: @@ -122,12 +89,6 @@ export default function relationships(state: State = ImmutableMap