From 2bd468358b067247716c786f6c30f8bb91264de4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 12 May 2022 11:38:01 -0500 Subject: [PATCH 01/71] Rearrange SidebarNavigation --- app/soapbox/components/sidebar-navigation.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/soapbox/components/sidebar-navigation.tsx b/app/soapbox/components/sidebar-navigation.tsx index a6f2fef6b..d436cb4cb 100644 --- a/app/soapbox/components/sidebar-navigation.tsx +++ b/app/soapbox/components/sidebar-navigation.tsx @@ -149,12 +149,6 @@ const SidebarNavigation = () => { {account && ( <> - } - /> - { text={} /> + {renderMessagesLink()} + + } + /> + { )} - {account && renderMessagesLink()} - {menu.length > 0 && ( Date: Thu, 12 May 2022 11:55:13 -0500 Subject: [PATCH 02/71] SidebarNavigationLink: add jsdoc comments --- app/soapbox/components/sidebar-navigation-link.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/soapbox/components/sidebar-navigation-link.tsx b/app/soapbox/components/sidebar-navigation-link.tsx index 9dbedd46a..9442a4cc4 100644 --- a/app/soapbox/components/sidebar-navigation-link.tsx +++ b/app/soapbox/components/sidebar-navigation-link.tsx @@ -5,13 +5,19 @@ import { NavLink } from 'react-router-dom'; import { Icon, Text, Counter } from './ui'; interface ISidebarNavigationLink { + /** Notification count, if any. */ count?: number, + /** URL to an SVG icon. */ icon: string, - text: string | React.ReactElement, + /** Link label. */ + text: React.ReactElement, + /** Route to an internal page. */ to?: string, + /** Callback when the link is clicked. */ onClick?: React.EventHandler, } +/** Desktop sidebar navigation link. */ const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, ref: React.ForwardedRef): JSX.Element => { const { icon, text, to = '', count, onClick } = props; const isActive = location.pathname === to; From 02726cfcc36e47d002225c2e59e2874c8fc0903a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 13 May 2022 13:07:11 -0500 Subject: [PATCH 03/71] Fix paginated comments, improve Mastodon tombstones --- app/soapbox/actions/statuses.js | 18 +++++++++++++- app/soapbox/features/status/index.tsx | 3 ++- app/soapbox/reducers/contexts.js | 34 +++++++++++++++++++++++---- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/app/soapbox/actions/statuses.js b/app/soapbox/actions/statuses.js index 176f7325f..ba1c3e74d 100644 --- a/app/soapbox/actions/statuses.js +++ b/app/soapbox/actions/statuses.js @@ -179,10 +179,18 @@ export function fetchContext(id) { }; } -export function fetchNext(next) { +export function fetchNext(statusId, next) { return async(dispatch, getState) => { const response = await api(getState).get(next); dispatch(importFetchedStatuses(response.data)); + + dispatch({ + type: CONTEXT_FETCH_SUCCESS, + id: statusId, + ancestors: [], + descendants: response.data, + }); + return { next: getNextLink(response) }; }; } @@ -213,6 +221,14 @@ export function fetchStatusWithContext(id) { dispatch(fetchDescendants(id)), dispatch(fetchStatus(id)), ]); + + dispatch({ + type: CONTEXT_FETCH_SUCCESS, + id, + ancestors: responses[0].data, + descendants: responses[1].data, + }); + const next = getNextLink(responses[1]); return { next }; } else { diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index 25247ac6f..49df34423 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -654,10 +654,11 @@ class Status extends ImmutablePureComponent { } handleLoadMore = () => { + const { status } = this.props; const { next } = this.state; if (next) { - this.props.dispatch(fetchNext(next)).then(({ next }) => { + this.props.dispatch(fetchNext(status.id, next)).then(({ next }) => { this.setState({ next }); }).catch(() => {}); } diff --git a/app/soapbox/reducers/contexts.js b/app/soapbox/reducers/contexts.js index c92ae503d..18ee80caf 100644 --- a/app/soapbox/reducers/contexts.js +++ b/app/soapbox/reducers/contexts.js @@ -67,15 +67,41 @@ const insertTombstone = (state, ancestorId, descendantId) => { }); }; -const importBranch = (state, statuses, rootId) => { +/** Find the highest level status from this statusId. */ +const getRootNode = (state, statusId, initialId = statusId) => { + const parent = state.getIn(['inReplyTos', statusId]); + + if (!parent) { + return statusId; + } else if (parent === initialId) { + // Prevent cycles + return parent; + } else { + return getRootNode(state, parent, initialId); + } +}; + +/** Route fromId to toId by inserting tombstones. */ +const connectNodes = (state, fromId, toId) => { + const root = getRootNode(state, fromId); + + if (root !== toId) { + return insertTombstone(state, toId, fromId); + } else { + return state; + } +}; + +const importBranch = (state, statuses, statusId) => { return state.withMutations(state => { statuses.forEach((status, i) => { - const lastId = rootId && i === 0 ? rootId : (statuses[i - 1] || {}).id; + const prevId = statusId && i === 0 ? statusId : (statuses[i - 1] || {}).id; if (status.in_reply_to_id) { importStatus(state, status); - } else if (lastId) { - insertTombstone(state, lastId, status.id); + connectNodes(state, status.id, statusId); + } else if (prevId) { + insertTombstone(state, prevId, status.id); } }); }); From 2290bfb334d5fb4ca0b858057e523ed426454c3b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 13 May 2022 14:30:11 -0500 Subject: [PATCH 04/71] Contexts reducer: convert to TypeScript --- app/soapbox/reducers/contexts.js | 189 ------------------------ app/soapbox/reducers/contexts.ts | 243 +++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+), 189 deletions(-) delete mode 100644 app/soapbox/reducers/contexts.js create mode 100644 app/soapbox/reducers/contexts.ts diff --git a/app/soapbox/reducers/contexts.js b/app/soapbox/reducers/contexts.js deleted file mode 100644 index 18ee80caf..000000000 --- a/app/soapbox/reducers/contexts.js +++ /dev/null @@ -1,189 +0,0 @@ -import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; - -import { STATUS_IMPORT, STATUSES_IMPORT } from 'soapbox/actions/importer'; - -import { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, -} from '../actions/accounts'; -import { - STATUS_CREATE_REQUEST, - STATUS_CREATE_SUCCESS, -} from '../actions/statuses'; -import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; -import { TIMELINE_DELETE } from '../actions/timelines'; - -const initialState = ImmutableMap({ - inReplyTos: ImmutableMap(), - replies: ImmutableMap(), -}); - -const importStatus = (state, status, idempotencyKey) => { - const { id, in_reply_to_id } = status; - if (!in_reply_to_id) return state; - - return state.withMutations(state => { - state.setIn(['inReplyTos', id], in_reply_to_id); - - state.updateIn(['replies', in_reply_to_id], ImmutableOrderedSet(), ids => { - return ids.add(id).sort(); - }); - - if (idempotencyKey) { - deletePendingStatus(state, status, idempotencyKey); - } - }); -}; - -const importStatuses = (state, statuses) => { - return state.withMutations(state => { - statuses.forEach(status => importStatus(state, status)); - }); -}; - -const isReplyTo = (state, childId, parentId, initialId = null) => { - if (!childId) return false; - - // Prevent cycles - if (childId === initialId) return false; - initialId = initialId || childId; - - if (childId === parentId) { - return true; - } else { - const nextId = state.getIn(['inReplyTos', childId]); - return isReplyTo(state, nextId, parentId, initialId); - } -}; - -const insertTombstone = (state, ancestorId, descendantId) => { - // Prevent infinite loop if the API returns a bogus response - if (isReplyTo(state, ancestorId, descendantId)) return state; - - const tombstoneId = `${descendantId}-tombstone`; - return state.withMutations(state => { - importStatus(state, { id: tombstoneId, in_reply_to_id: ancestorId }); - importStatus(state, { id: descendantId, in_reply_to_id: tombstoneId }); - }); -}; - -/** Find the highest level status from this statusId. */ -const getRootNode = (state, statusId, initialId = statusId) => { - const parent = state.getIn(['inReplyTos', statusId]); - - if (!parent) { - return statusId; - } else if (parent === initialId) { - // Prevent cycles - return parent; - } else { - return getRootNode(state, parent, initialId); - } -}; - -/** Route fromId to toId by inserting tombstones. */ -const connectNodes = (state, fromId, toId) => { - const root = getRootNode(state, fromId); - - if (root !== toId) { - return insertTombstone(state, toId, fromId); - } else { - return state; - } -}; - -const importBranch = (state, statuses, statusId) => { - return state.withMutations(state => { - statuses.forEach((status, i) => { - const prevId = statusId && i === 0 ? statusId : (statuses[i - 1] || {}).id; - - if (status.in_reply_to_id) { - importStatus(state, status); - connectNodes(state, status.id, statusId); - } else if (prevId) { - insertTombstone(state, prevId, status.id); - } - }); - }); -}; - -const normalizeContext = (state, id, ancestors, descendants) => state.withMutations(state => { - importBranch(state, ancestors); - importBranch(state, descendants, id); - - if (ancestors.length > 0 && !state.getIn(['inReplyTos', id])) { - insertTombstone(state, ancestors[ancestors.length - 1].id, id); - } -}); - -const deleteStatus = (state, id) => { - return state.withMutations(state => { - const parentId = state.getIn(['inReplyTos', id]); - const replies = state.getIn(['replies', id], ImmutableOrderedSet()); - - // Delete from its parent's tree - state.updateIn(['replies', parentId], ImmutableOrderedSet(), ids => ids.delete(id)); - - // Dereference children - replies.forEach(reply => state.deleteIn(['inReplyTos', reply])); - - state.deleteIn(['inReplyTos', id]); - state.deleteIn(['replies', id]); - }); -}; - -const deleteStatuses = (state, ids) => { - return state.withMutations(state => { - ids.forEach(id => deleteStatus(state, id)); - }); -}; - -const filterContexts = (state, relationship, statuses) => { - const ownedStatusIds = statuses - .filter(status => status.get('account') === relationship.id) - .map(status => status.get('id')); - - return deleteStatuses(state, ownedStatusIds); -}; - -const importPendingStatus = (state, params, idempotencyKey) => { - const id = `末pending-${idempotencyKey}`; - const { in_reply_to_id } = params; - return importStatus(state, { id, in_reply_to_id }); -}; - -const deletePendingStatus = (state, { in_reply_to_id }, idempotencyKey) => { - const id = `末pending-${idempotencyKey}`; - - return state.withMutations(state => { - state.deleteIn(['inReplyTos', id]); - - if (in_reply_to_id) { - state.updateIn(['replies', in_reply_to_id], ImmutableOrderedSet(), ids => { - return ids.delete(id).sort(); - }); - } - }); -}; - -export default function replies(state = initialState, action) { - switch (action.type) { - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - return filterContexts(state, action.relationship, action.statuses); - case CONTEXT_FETCH_SUCCESS: - return normalizeContext(state, action.id, action.ancestors, action.descendants); - case TIMELINE_DELETE: - return deleteStatuses(state, [action.id]); - case STATUS_CREATE_REQUEST: - return importPendingStatus(state, action.params, action.idempotencyKey); - case STATUS_CREATE_SUCCESS: - return deletePendingStatus(state, action.status, action.idempotencyKey); - case STATUS_IMPORT: - return importStatus(state, action.status, action.idempotencyKey); - case STATUSES_IMPORT: - return importStatuses(state, action.statuses); - default: - return state; - } -} diff --git a/app/soapbox/reducers/contexts.ts b/app/soapbox/reducers/contexts.ts new file mode 100644 index 000000000..b8ddb5ea3 --- /dev/null +++ b/app/soapbox/reducers/contexts.ts @@ -0,0 +1,243 @@ +import { + Map as ImmutableMap, + Record as ImmutableRecord, + OrderedSet as ImmutableOrderedSet, +} from 'immutable'; + +import { STATUS_IMPORT, STATUSES_IMPORT } from 'soapbox/actions/importer'; + +import { + ACCOUNT_BLOCK_SUCCESS, + ACCOUNT_MUTE_SUCCESS, +} from '../actions/accounts'; +import { + STATUS_CREATE_REQUEST, + STATUS_CREATE_SUCCESS, +} from '../actions/statuses'; +import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; +import { TIMELINE_DELETE } from '../actions/timelines'; + +import type { ReducerStatus } from './statuses'; +import type { AnyAction } from 'redux'; + +const ReducerRecord = ImmutableRecord({ + inReplyTos: ImmutableMap(), + replies: ImmutableMap>(), +}); + +type State = ReturnType; + +/** Minimal status fields needed to process context. */ +type ContextStatus = { + id: string, + in_reply_to_id: string | null, +} + +/** Import a single status into the reducer, setting replies and replyTos. */ +const importStatus = (state: State, status: ContextStatus, idempotencyKey?: string): State => { + const { id, in_reply_to_id: inReplyToId } = status; + if (!inReplyToId) return state; + + return state.withMutations(state => { + const replies = state.replies.get(inReplyToId) || ImmutableOrderedSet(); + const newReplies = replies.add(id).sort(); + + state.setIn(['replies', inReplyToId], newReplies); + state.setIn(['inReplyTos', id], inReplyToId); + + if (idempotencyKey) { + deletePendingStatus(state, status, idempotencyKey); + } + }); +}; + +/** Import multiple statuses into the state. */ +const importStatuses = (state: State, statuses: ContextStatus[]): State => { + return state.withMutations(state => { + statuses.forEach(status => importStatus(state, status)); + }); +}; + +const isReplyTo = ( + state: State, + childId: string | undefined, + parentId: string, + initialId: string | null = null, +): boolean => { + if (!childId) return false; + + // Prevent cycles + if (childId === initialId) return false; + initialId = initialId || childId; + + if (childId === parentId) { + return true; + } else { + const nextId = state.inReplyTos.get(childId); + return isReplyTo(state, nextId, parentId, initialId); + } +}; + +/** Insert a fake status ID connecting descendant to ancestor. */ +const insertTombstone = (state: State, ancestorId: string, descendantId: string): State => { + // Prevent infinite loop if the API returns a bogus response + if (isReplyTo(state, ancestorId, descendantId)) return state; + + const tombstoneId = `${descendantId}-tombstone`; + return state.withMutations(state => { + importStatus(state, { id: tombstoneId, in_reply_to_id: ancestorId }); + importStatus(state, { id: descendantId, in_reply_to_id: tombstoneId }); + }); +}; + +/** Find the highest level status from this statusId. */ +const getRootNode = (state: State, statusId: string, initialId = statusId): string => { + const parent = state.inReplyTos.get(statusId); + + if (!parent) { + return statusId; + } else if (parent === initialId) { + // Prevent cycles + return parent; + } else { + return getRootNode(state, parent, initialId); + } +}; + +/** Route fromId to toId by inserting tombstones. */ +const connectNodes = (state: State, fromId: string, toId: string): State => { + const root = getRootNode(state, fromId); + + if (root !== toId) { + return insertTombstone(state, toId, fromId); + } else { + return state; + } +}; + +/** Import a branch of ancestors or descendants, in relation to statusId. */ +const importBranch = (state: State, statuses: ContextStatus[], statusId?: string): State => { + return state.withMutations(state => { + statuses.forEach((status, i) => { + const prevId = statusId && i === 0 ? statusId : (statuses[i - 1] || {}).id; + + if (status.in_reply_to_id) { + importStatus(state, status); + + // On Mastodon, in_reply_to_id can refer to an unavailable status, + // so traverse the tree up and insert a connecting tombstone if needed. + if (statusId) { + connectNodes(state, status.id, statusId); + } + } else if (prevId) { + // On Pleroma, in_reply_to_id will be null if the parent is unavailable, + // so insert the tombstone now. + insertTombstone(state, prevId, status.id); + } + }); + }); +}; + +/** Import a status's ancestors and descendants. */ +const normalizeContext = ( + state: State, + id: string, + ancestors: ContextStatus[], + descendants: ContextStatus[], +) => state.withMutations(state => { + importBranch(state, ancestors); + importBranch(state, descendants, id); + + if (ancestors.length > 0 && !state.getIn(['inReplyTos', id])) { + insertTombstone(state, ancestors[ancestors.length - 1].id, id); + } +}); + +/** Remove a status from the reducer. */ +const deleteStatus = (state: State, id: string): State => { + return state.withMutations(state => { + // Delete from its parent's tree + const parentId = state.inReplyTos.get(id); + if (parentId) { + const parentReplies = state.replies.get(parentId) || ImmutableOrderedSet(); + const newParentReplies = parentReplies.delete(id); + state.setIn(['replies', parentId], newParentReplies); + } + + // Dereference children + const replies = state.replies.get(id) || ImmutableOrderedSet(); + replies.forEach(reply => state.deleteIn(['inReplyTos', reply])); + + state.deleteIn(['inReplyTos', id]); + state.deleteIn(['replies', id]); + }); +}; + +/** Delete multiple statuses from the reducer. */ +const deleteStatuses = (state: State, ids: string[]): State => { + return state.withMutations(state => { + ids.forEach(id => deleteStatus(state, id)); + }); +}; + +/** Delete statuses upon blocking or muting a user. */ +const filterContexts = ( + state: State, + relationship: { id: string }, + /** The entire statuses map from the store. */ + statuses: ImmutableMap, +): State => { + const ownedStatusIds = statuses + .filter(status => status.account === relationship.id) + .map(status => status.id) + .toList() + .toArray(); + + return deleteStatuses(state, ownedStatusIds); +}; + +/** Add a fake status ID for a pending status. */ +const importPendingStatus = (state: State, params: ContextStatus, idempotencyKey: string): State => { + const id = `末pending-${idempotencyKey}`; + const { in_reply_to_id } = params; + return importStatus(state, { id, in_reply_to_id }); +}; + +/** Delete a pending status from the reducer. */ +const deletePendingStatus = (state: State, params: ContextStatus, idempotencyKey: string): State => { + const id = `末pending-${idempotencyKey}`; + const { in_reply_to_id: inReplyToId } = params; + + return state.withMutations(state => { + state.deleteIn(['inReplyTos', id]); + + if (inReplyToId) { + const replies = state.replies.get(inReplyToId) || ImmutableOrderedSet(); + const newReplies = replies.delete(id).sort(); + state.setIn(['replies', inReplyToId], newReplies); + } + }); +}; + +/** Contexts reducer. Used for building a nested tree structure for threads. */ +export default function replies(state = ReducerRecord(), action: AnyAction) { + switch (action.type) { + case ACCOUNT_BLOCK_SUCCESS: + case ACCOUNT_MUTE_SUCCESS: + return filterContexts(state, action.relationship, action.statuses); + case CONTEXT_FETCH_SUCCESS: + return normalizeContext(state, action.id, action.ancestors, action.descendants); + case TIMELINE_DELETE: + return deleteStatuses(state, [action.id]); + case STATUS_CREATE_REQUEST: + return importPendingStatus(state, action.params, action.idempotencyKey); + case STATUS_CREATE_SUCCESS: + return deletePendingStatus(state, action.status, action.idempotencyKey); + case STATUS_IMPORT: + return importStatus(state, action.status, action.idempotencyKey); + case STATUSES_IMPORT: + return importStatuses(state, action.statuses); + default: + return state; + } +} From 0329cc4d040cab683ebc525a7dda0d29bd276d08 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 13 May 2022 14:42:15 -0500 Subject: [PATCH 05/71] Clean up contexts test --- .../reducers/__tests__/contexts-test.js | 24 +++++++++---------- app/soapbox/reducers/contexts.ts | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/soapbox/reducers/__tests__/contexts-test.js b/app/soapbox/reducers/__tests__/contexts-test.js index 26d6ecb39..30de0a49c 100644 --- a/app/soapbox/reducers/__tests__/contexts-test.js +++ b/app/soapbox/reducers/__tests__/contexts-test.js @@ -9,11 +9,11 @@ import context2 from 'soapbox/__fixtures__/context_2.json'; import { CONTEXT_FETCH_SUCCESS } from 'soapbox/actions/statuses'; import { TIMELINE_DELETE } from 'soapbox/actions/timelines'; -import reducer from '../contexts'; +import reducer, { ReducerRecord } from '../contexts'; describe('contexts reducer', () => { it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ImmutableMap({ + expect(reducer(undefined, {})).toEqual(ReducerRecord({ inReplyTos: ImmutableMap(), replies: ImmutableMap(), })); @@ -25,7 +25,7 @@ describe('contexts reducer', () => { result = reducer(result, { type: CONTEXT_FETCH_SUCCESS, id: '9zIH8WYwtnUx4yDzUm', ancestors: context1.ancestors, descendants: context1.descendants }); result = reducer(result, { type: CONTEXT_FETCH_SUCCESS, id: '9zIH7PUdhK3Ircg4hM', ancestors: context2.ancestors, descendants: context2.descendants }); - expect(result).toEqual(ImmutableMap({ + expect(result).toEqual(ReducerRecord({ inReplyTos: ImmutableMap({ '9zIH7PUdhK3Ircg4hM': '9zIH6kDXA10YqhMKqO', '9zIH7mMGgc1RmJwDLM': '9zIH6kDXA10YqhMKqO', @@ -60,22 +60,22 @@ describe('contexts reducer', () => { it('deletes the status', () => { const action = { type: TIMELINE_DELETE, id: 'B' }; - const state = fromJS({ - inReplyTos: { + const state = ReducerRecord({ + inReplyTos: fromJS({ B: 'A', C: 'B', - }, - replies: { + }), + replies: fromJS({ A: ImmutableOrderedSet(['B']), B: ImmutableOrderedSet(['C']), - }, + }), }); - const expected = fromJS({ - inReplyTos: {}, - replies: { + const expected = ReducerRecord({ + inReplyTos: fromJS({}), + replies: fromJS({ A: ImmutableOrderedSet(), - }, + }), }); expect(reducer(state, action)).toEqual(expected); diff --git a/app/soapbox/reducers/contexts.ts b/app/soapbox/reducers/contexts.ts index b8ddb5ea3..5cdb8de0b 100644 --- a/app/soapbox/reducers/contexts.ts +++ b/app/soapbox/reducers/contexts.ts @@ -20,7 +20,7 @@ import { TIMELINE_DELETE } from '../actions/timelines'; import type { ReducerStatus } from './statuses'; import type { AnyAction } from 'redux'; -const ReducerRecord = ImmutableRecord({ +export const ReducerRecord = ImmutableRecord({ inReplyTos: ImmutableMap(), replies: ImmutableMap>(), }); From b80571437a236bda42d00fc3ac8b5b838c1e075d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 13 May 2022 15:02:20 -0500 Subject: [PATCH 06/71] Contexts: component TypeScript fixes --- .../features/status/components/thread-status.tsx | 5 +++-- app/soapbox/features/status/index.tsx | 16 +++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/soapbox/features/status/components/thread-status.tsx b/app/soapbox/features/status/components/thread-status.tsx index 13a486756..6919e5e8d 100644 --- a/app/soapbox/features/status/components/thread-status.tsx +++ b/app/soapbox/features/status/components/thread-status.tsx @@ -11,11 +11,12 @@ interface IThreadStatus { focusedStatusId: string, } +/** Status with reply-connector in threads. */ const ThreadStatus: React.FC = (props): JSX.Element => { const { id, focusedStatusId } = props; - const replyToId = useAppSelector(state => state.contexts.getIn(['inReplyTos', id])); - const replyCount = useAppSelector(state => state.contexts.getIn(['replies', id], ImmutableOrderedSet()).size); + const replyToId = useAppSelector(state => state.contexts.inReplyTos.get(id)); + const replyCount = useAppSelector(state => state.contexts.replies.get(id, ImmutableOrderedSet()).size); const isLoaded = useAppSelector(state => Boolean(state.statuses.get(id))); const renderConnector = (): JSX.Element | null => { diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index 49df34423..55a2db40e 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -97,11 +97,11 @@ const makeMapStateToProps = () => { const getStatus = makeGetStatus(); const getAncestorsIds = createSelector([ - (_: RootState, statusId: string) => statusId, - (state: RootState) => state.contexts.get('inReplyTos'), + (_: RootState, statusId: string | undefined) => statusId, + (state: RootState) => state.contexts.inReplyTos, ], (statusId, inReplyTos) => { - let ancestorsIds = ImmutableOrderedSet(); - let id = statusId; + let ancestorsIds = ImmutableOrderedSet(); + let id: string | undefined = statusId; while (id && !ancestorsIds.includes(id)) { ancestorsIds = ImmutableOrderedSet([id]).union(ancestorsIds); @@ -113,13 +113,15 @@ const makeMapStateToProps = () => { const getDescendantsIds = createSelector([ (_: RootState, statusId: string) => statusId, - (state: RootState) => state.contexts.get('replies'), + (state: RootState) => state.contexts.replies, ], (statusId, contextReplies) => { let descendantsIds = ImmutableOrderedSet(); const ids = [statusId]; while (ids.length > 0) { - const id = ids.shift(); + const id = ids.shift(); + if (!id) break; + const replies = contextReplies.get(id); if (descendantsIds.includes(id)) { @@ -147,7 +149,7 @@ const makeMapStateToProps = () => { if (status) { const statusId = status.id; - ancestorsIds = getAncestorsIds(state, state.contexts.getIn(['inReplyTos', statusId])); + ancestorsIds = getAncestorsIds(state, state.contexts.inReplyTos.get(statusId)); descendantsIds = getDescendantsIds(state, statusId); ancestorsIds = ancestorsIds.delete(statusId).subtract(descendantsIds); descendantsIds = descendantsIds.delete(statusId).subtract(ancestorsIds); From 75745d2c46fed14aca2521daee777988374779c5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 13 May 2022 15:08:38 -0500 Subject: [PATCH 07/71] Clean up contexts test --- app/soapbox/jest/test-helpers.tsx | 2 +- .../reducers/__tests__/contexts-test.js | 26 ++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app/soapbox/jest/test-helpers.tsx b/app/soapbox/jest/test-helpers.tsx index 49bf08bf2..459049b52 100644 --- a/app/soapbox/jest/test-helpers.tsx +++ b/app/soapbox/jest/test-helpers.tsx @@ -18,7 +18,7 @@ const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); let rootState = rootReducer(undefined, {} as Action); -// Apply actions to the state, one at a time +/** Apply actions to the state, one at a time. */ const applyActions = (state: any, actions: any, reducer: any) => { return actions.reduce((state: any, action: any) => reducer(state, action), state); }; diff --git a/app/soapbox/reducers/__tests__/contexts-test.js b/app/soapbox/reducers/__tests__/contexts-test.js index 30de0a49c..9708120bb 100644 --- a/app/soapbox/reducers/__tests__/contexts-test.js +++ b/app/soapbox/reducers/__tests__/contexts-test.js @@ -6,8 +6,10 @@ import { import context1 from 'soapbox/__fixtures__/context_1.json'; import context2 from 'soapbox/__fixtures__/context_2.json'; +import { STATUSES_IMPORT } from 'soapbox/actions/importer'; import { CONTEXT_FETCH_SUCCESS } from 'soapbox/actions/statuses'; import { TIMELINE_DELETE } from 'soapbox/actions/timelines'; +import { applyActions } from 'soapbox/jest/test-helpers'; import reducer, { ReducerRecord } from '../contexts'; @@ -21,9 +23,27 @@ describe('contexts reducer', () => { it('should support rendering a complete tree', () => { // https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/422 - let result; - result = reducer(result, { type: CONTEXT_FETCH_SUCCESS, id: '9zIH8WYwtnUx4yDzUm', ancestors: context1.ancestors, descendants: context1.descendants }); - result = reducer(result, { type: CONTEXT_FETCH_SUCCESS, id: '9zIH7PUdhK3Ircg4hM', ancestors: context2.ancestors, descendants: context2.descendants }); + const actions = [{ + type: STATUSES_IMPORT, + statuses: [ + ...context1.ancestors, + ...context1.descendants, + ...context2.ancestors, + ...context2.descendants, + ], + }, { + type: CONTEXT_FETCH_SUCCESS, + id: '9zIH8WYwtnUx4yDzUm', + ancestors: context1.ancestors, + descendants: context1.descendants, + }, { + type: CONTEXT_FETCH_SUCCESS, + id: '9zIH7PUdhK3Ircg4hM', + ancestors: context2.ancestors, + descendants: context2.descendants, + }]; + + const result = applyActions(undefined, actions, reducer); expect(result).toEqual(ReducerRecord({ inReplyTos: ImmutableMap({ From b4e27e5dcdf1408a2828836a696fe0ceb7270b03 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 13 May 2022 16:34:45 -0500 Subject: [PATCH 08/71] Contexts: stop spewing tombstones everywhere --- app/soapbox/reducers/contexts.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/soapbox/reducers/contexts.ts b/app/soapbox/reducers/contexts.ts index 5cdb8de0b..219914da8 100644 --- a/app/soapbox/reducers/contexts.ts +++ b/app/soapbox/reducers/contexts.ts @@ -106,9 +106,10 @@ const getRootNode = (state: State, statusId: string, initialId = statusId): stri /** Route fromId to toId by inserting tombstones. */ const connectNodes = (state: State, fromId: string, toId: string): State => { - const root = getRootNode(state, fromId); + const fromRoot = getRootNode(state, fromId); + const toRoot = getRootNode(state, toId); - if (root !== toId) { + if (fromRoot !== toRoot) { return insertTombstone(state, toId, fromId); } else { return state; From e6617af0f9d16d3df87f9482e0d32adec091e52a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 13 May 2022 16:58:38 -0500 Subject: [PATCH 09/71] Refactor context test, remove dumb fixtures --- app/soapbox/__fixtures__/context_1.json | 739 ------------------ app/soapbox/__fixtures__/context_2.json | 739 ------------------ .../reducers/__tests__/contexts-test.js | 97 +-- 3 files changed, 43 insertions(+), 1532 deletions(-) delete mode 100644 app/soapbox/__fixtures__/context_1.json delete mode 100644 app/soapbox/__fixtures__/context_2.json diff --git a/app/soapbox/__fixtures__/context_1.json b/app/soapbox/__fixtures__/context_1.json deleted file mode 100644 index 2e37a5502..000000000 --- a/app/soapbox/__fixtures__/context_1.json +++ /dev/null @@ -1,739 +0,0 @@ -{ - "ancestors": [ - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "id": "9v5bmRalQvjOy0ECcC", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "favicon": "https://gleasonator.com/favicon.png", - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": true, - "is_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

A

", - "created_at": "2020-09-18T20:07:10.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH6kDXA10YqhMKqO", - "in_reply_to_account_id": null, - "in_reply_to_id": null, - "language": null, - "media_attachments": [], - "mentions": [], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "A" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": null, - "local": true, - "parent_visible": false, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/9995c074-2ff6-4a01-b596-7ef6971ed5d2", - "url": "https://gleasonator.com/notice/9zIH6kDXA10YqhMKqO", - "visibility": "direct" - }, - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "id": "9v5bmRalQvjOy0ECcC", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "favicon": "https://gleasonator.com/favicon.png", - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": true, - "is_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

B

", - "created_at": "2020-09-18T20:07:18.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH7PUdhK3Ircg4hM", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH6kDXA10YqhMKqO", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "B" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/992ca99a-425d-46eb-b094-60412e9fb141", - "url": "https://gleasonator.com/notice/9zIH7PUdhK3Ircg4hM", - "visibility": "direct" - }, - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "id": "9v5bmRalQvjOy0ECcC", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "favicon": "https://gleasonator.com/favicon.png", - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": true, - "is_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

C

", - "created_at": "2020-09-18T20:07:22.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH7mMGgc1RmJwDLM", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH6kDXA10YqhMKqO", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "C" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/a2c25ef5-a40e-4098-b07e-b468989ef749", - "url": "https://gleasonator.com/notice/9zIH7mMGgc1RmJwDLM", - "visibility": "direct" - } - ], - "descendants": [ - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "id": "9v5bmRalQvjOy0ECcC", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "favicon": "https://gleasonator.com/favicon.png", - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": true, - "is_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

E

", - "created_at": "2020-09-18T20:07:38.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH9GTCDWEFSRt2um", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH7PUdhK3Ircg4hM", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "E" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/a1e45493-2158-4f11-88ca-ba621429dbe5", - "url": "https://gleasonator.com/notice/9zIH9GTCDWEFSRt2um", - "visibility": "direct" - }, - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "id": "9v5bmRalQvjOy0ECcC", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "favicon": "https://gleasonator.com/favicon.png", - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": true, - "is_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

F

", - "created_at": "2020-09-18T20:07:42.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH9fhaP9atiJoOJc", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH8WYwtnUx4yDzUm", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "F" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/ee661cf9-35d4-4e84-88ff-13b5950f7556", - "url": "https://gleasonator.com/notice/9zIH9fhaP9atiJoOJc", - "visibility": "direct" - } - ] -} diff --git a/app/soapbox/__fixtures__/context_2.json b/app/soapbox/__fixtures__/context_2.json deleted file mode 100644 index c5cf2a813..000000000 --- a/app/soapbox/__fixtures__/context_2.json +++ /dev/null @@ -1,739 +0,0 @@ -{ - "ancestors": [ - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "id": "9v5bmRalQvjOy0ECcC", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "favicon": "https://gleasonator.com/favicon.png", - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": true, - "is_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

A

", - "created_at": "2020-09-18T20:07:10.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH6kDXA10YqhMKqO", - "in_reply_to_account_id": null, - "in_reply_to_id": null, - "language": null, - "media_attachments": [], - "mentions": [], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "A" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": null, - "local": true, - "parent_visible": false, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/9995c074-2ff6-4a01-b596-7ef6971ed5d2", - "url": "https://gleasonator.com/notice/9zIH6kDXA10YqhMKqO", - "visibility": "direct" - } - ], - "descendants": [ - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "id": "9v5bmRalQvjOy0ECcC", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "favicon": "https://gleasonator.com/favicon.png", - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": true, - "is_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

C

", - "created_at": "2020-09-18T20:07:22.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH7mMGgc1RmJwDLM", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH6kDXA10YqhMKqO", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "C" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/a2c25ef5-a40e-4098-b07e-b468989ef749", - "url": "https://gleasonator.com/notice/9zIH7mMGgc1RmJwDLM", - "visibility": "direct" - }, - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "id": "9v5bmRalQvjOy0ECcC", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "favicon": "https://gleasonator.com/favicon.png", - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": true, - "is_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

D

", - "created_at": "2020-09-18T20:07:30.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH8WYwtnUx4yDzUm", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH7PUdhK3Ircg4hM", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "D" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/bb423adc-ed86-42d8-942e-84efbe7b1acf", - "url": "https://gleasonator.com/notice/9zIH8WYwtnUx4yDzUm", - "visibility": "direct" - }, - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "id": "9v5bmRalQvjOy0ECcC", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "favicon": "https://gleasonator.com/favicon.png", - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": true, - "is_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

E

", - "created_at": "2020-09-18T20:07:38.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH9GTCDWEFSRt2um", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH7PUdhK3Ircg4hM", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "E" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/a1e45493-2158-4f11-88ca-ba621429dbe5", - "url": "https://gleasonator.com/notice/9zIH9GTCDWEFSRt2um", - "visibility": "direct" - }, - { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "avatar_static": "https://media.gleasonator.com/26f0ca4ef51f7047829fdb65a43cb7d0304413ce0a5d00dd1638458994608718.jpg", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "follow_requests_count": 0, - "followers_count": 725, - "following_count": 1211, - "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "id": "9v5bmRalQvjOy0ECcC", - "locked": false, - "note": "Fediverse developer. I come in peace.

#vegan #freeculture #atheist #antiporn #gendercritical.

Boosts ≠ endorsements.", - "pleroma": { - "accepts_chat_messages": true, - "allow_following_move": true, - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "deactivated": false, - "favicon": "https://gleasonator.com/favicon.png", - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": true, - "is_moderator": false, - "notification_settings": { - "block_from_strangers": false, - "hide_notification_contents": false - }, - "relationship": {}, - "skip_thread_containment": false, - "tags": [], - "unread_conversation_count": 95, - "unread_notifications_count": 0 - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - } - ], - "note": "Fediverse developer. I come in peace.\r\n\r\n#vegan #freeculture #atheist #antiporn #gendercritical.\r\n\r\nBoosts ≠ endorsements.", - "pleroma": { - "actor_type": "Person", - "discoverable": false, - "no_rich_text": false, - "show_role": true - }, - "privacy": "public", - "sensitive": false - }, - "statuses_count": 9157, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "

F

", - "created_at": "2020-09-18T20:07:42.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9zIH9fhaP9atiJoOJc", - "in_reply_to_account_id": "9v5bmRalQvjOy0ECcC", - "in_reply_to_id": "9zIH8WYwtnUx4yDzUm", - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "F" - }, - "conversation_id": 5089485, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": "alex", - "local": true, - "parent_visible": true, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/ee661cf9-35d4-4e84-88ff-13b5950f7556", - "url": "https://gleasonator.com/notice/9zIH9fhaP9atiJoOJc", - "visibility": "direct" - } - ] -} diff --git a/app/soapbox/reducers/__tests__/contexts-test.js b/app/soapbox/reducers/__tests__/contexts-test.js index 9708120bb..47d421f3b 100644 --- a/app/soapbox/reducers/__tests__/contexts-test.js +++ b/app/soapbox/reducers/__tests__/contexts-test.js @@ -4,9 +4,7 @@ import { fromJS, } from 'immutable'; -import context1 from 'soapbox/__fixtures__/context_1.json'; -import context2 from 'soapbox/__fixtures__/context_2.json'; -import { STATUSES_IMPORT } from 'soapbox/actions/importer'; +import { STATUS_IMPORT } from 'soapbox/actions/importer'; import { CONTEXT_FETCH_SUCCESS } from 'soapbox/actions/statuses'; import { TIMELINE_DELETE } from 'soapbox/actions/timelines'; import { applyActions } from 'soapbox/jest/test-helpers'; @@ -21,59 +19,50 @@ describe('contexts reducer', () => { })); }); - it('should support rendering a complete tree', () => { - // https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/422 - const actions = [{ - type: STATUSES_IMPORT, - statuses: [ - ...context1.ancestors, - ...context1.descendants, - ...context2.ancestors, - ...context2.descendants, - ], - }, { - type: CONTEXT_FETCH_SUCCESS, - id: '9zIH8WYwtnUx4yDzUm', - ancestors: context1.ancestors, - descendants: context1.descendants, - }, { - type: CONTEXT_FETCH_SUCCESS, - id: '9zIH7PUdhK3Ircg4hM', - ancestors: context2.ancestors, - descendants: context2.descendants, - }]; + describe(CONTEXT_FETCH_SUCCESS, () => { + it('inserts a tombstone connecting an orphaned descendant', () => { + const status = { id: 'A', in_reply_to_id: null }; - const result = applyActions(undefined, actions, reducer); + const context = { + id: 'A', + ancestors: [], + descendants: [ + { id: 'C', in_reply_to_id: 'B' }, + ], + }; - expect(result).toEqual(ReducerRecord({ - inReplyTos: ImmutableMap({ - '9zIH7PUdhK3Ircg4hM': '9zIH6kDXA10YqhMKqO', - '9zIH7mMGgc1RmJwDLM': '9zIH6kDXA10YqhMKqO', - '9zIH9GTCDWEFSRt2um': '9zIH7PUdhK3Ircg4hM', - '9zIH9fhaP9atiJoOJc': '9zIH8WYwtnUx4yDzUm', - '9zIH8WYwtnUx4yDzUm': '9zIH7PUdhK3Ircg4hM', - '9zIH8WYwtnUx4yDzUm-tombstone': '9zIH7mMGgc1RmJwDLM', - }), - replies: ImmutableMap({ - '9zIH6kDXA10YqhMKqO': ImmutableOrderedSet([ - '9zIH7PUdhK3Ircg4hM', - '9zIH7mMGgc1RmJwDLM', - ]), - '9zIH7PUdhK3Ircg4hM': ImmutableOrderedSet([ - '9zIH8WYwtnUx4yDzUm', - '9zIH9GTCDWEFSRt2um', - ]), - '9zIH8WYwtnUx4yDzUm': ImmutableOrderedSet([ - '9zIH9fhaP9atiJoOJc', - ]), - '9zIH8WYwtnUx4yDzUm-tombstone': ImmutableOrderedSet([ - '9zIH8WYwtnUx4yDzUm', - ]), - '9zIH7mMGgc1RmJwDLM': ImmutableOrderedSet([ - '9zIH8WYwtnUx4yDzUm-tombstone', - ]), - }), - })); + const actions = [ + { type: STATUS_IMPORT, status }, + { type: CONTEXT_FETCH_SUCCESS, ...context }, + ]; + + const result = applyActions(undefined, actions, reducer); + expect(result.inReplyTos.get('C')).toBe('C-tombstone'); + expect(result.replies.get('A').toArray()).toEqual(['C-tombstone']); + }); + }); + + describe(CONTEXT_FETCH_SUCCESS, () => { + it('inserts a tombstone connecting an orphaned descendant (with null in_reply_to_id)', () => { + const status = { id: 'A', in_reply_to_id: null }; + + const context = { + id: 'A', + ancestors: [], + descendants: [ + { id: 'C', in_reply_to_id: null }, + ], + }; + + const actions = [ + { type: STATUS_IMPORT, status }, + { type: CONTEXT_FETCH_SUCCESS, ...context }, + ]; + + const result = applyActions(undefined, actions, reducer); + expect(result.inReplyTos.get('C')).toBe('C-tombstone'); + expect(result.replies.get('A').toArray()).toEqual(['C-tombstone']); + }); }); describe(TIMELINE_DELETE, () => { From 8ed6d9fc6b68263353df65d71124e185d610657b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 13 May 2022 17:11:31 -0500 Subject: [PATCH 10/71] Test context loops --- .../reducers/__tests__/contexts-test.js | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/app/soapbox/reducers/__tests__/contexts-test.js b/app/soapbox/reducers/__tests__/contexts-test.js index 47d421f3b..d5270f0d8 100644 --- a/app/soapbox/reducers/__tests__/contexts-test.js +++ b/app/soapbox/reducers/__tests__/contexts-test.js @@ -40,9 +40,7 @@ describe('contexts reducer', () => { expect(result.inReplyTos.get('C')).toBe('C-tombstone'); expect(result.replies.get('A').toArray()).toEqual(['C-tombstone']); }); - }); - describe(CONTEXT_FETCH_SUCCESS, () => { it('inserts a tombstone connecting an orphaned descendant (with null in_reply_to_id)', () => { const status = { id: 'A', in_reply_to_id: null }; @@ -63,6 +61,32 @@ describe('contexts reducer', () => { expect(result.inReplyTos.get('C')).toBe('C-tombstone'); expect(result.replies.get('A').toArray()).toEqual(['C-tombstone']); }); + + it('doesn\'t explode when it encounters a loop', () => { + const status = { id: 'A', in_reply_to_id: null }; + + const context = { + id: 'A', + ancestors: [], + descendants: [ + { id: 'C', in_reply_to_id: 'E' }, + { id: 'D', in_reply_to_id: 'C' }, + { id: 'E', in_reply_to_id: 'D' }, + { id: 'F', in_reply_to_id: 'F' }, + ], + }; + + const actions = [ + { type: STATUS_IMPORT, status }, + { type: CONTEXT_FETCH_SUCCESS, ...context }, + ]; + + const result = applyActions(undefined, actions, reducer); + + // These checks are superficial. We just don't want a stack overflow! + expect(result.inReplyTos.get('C')).toBe('C-tombstone'); + expect(result.replies.get('A').toArray()).toEqual(['C-tombstone', 'F-tombstone']); + }); }); describe(TIMELINE_DELETE, () => { From 0b8fbdfbb9cbff476f3184c429993fe4840e6219 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 13 May 2022 17:17:06 -0500 Subject: [PATCH 11/71] Contexts: remove unnecessary isReplyTo function (now handled by connectNodes) --- app/soapbox/reducers/contexts.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/app/soapbox/reducers/contexts.ts b/app/soapbox/reducers/contexts.ts index 219914da8..bfc830c93 100644 --- a/app/soapbox/reducers/contexts.ts +++ b/app/soapbox/reducers/contexts.ts @@ -58,31 +58,8 @@ const importStatuses = (state: State, statuses: ContextStatus[]): State => { }); }; -const isReplyTo = ( - state: State, - childId: string | undefined, - parentId: string, - initialId: string | null = null, -): boolean => { - if (!childId) return false; - - // Prevent cycles - if (childId === initialId) return false; - initialId = initialId || childId; - - if (childId === parentId) { - return true; - } else { - const nextId = state.inReplyTos.get(childId); - return isReplyTo(state, nextId, parentId, initialId); - } -}; - /** Insert a fake status ID connecting descendant to ancestor. */ const insertTombstone = (state: State, ancestorId: string, descendantId: string): State => { - // Prevent infinite loop if the API returns a bogus response - if (isReplyTo(state, ancestorId, descendantId)) return state; - const tombstoneId = `${descendantId}-tombstone`; return state.withMutations(state => { importStatus(state, { id: tombstoneId, in_reply_to_id: ancestorId }); From 9f8a74b376bbe7ffce833a5641dd4dd03181ade0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 13 May 2022 17:25:58 -0500 Subject: [PATCH 12/71] Paginated context: fetch the status before ancestors/descendants --- app/soapbox/actions/statuses.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/actions/statuses.js b/app/soapbox/actions/statuses.js index ba1c3e74d..a274f181d 100644 --- a/app/soapbox/actions/statuses.js +++ b/app/soapbox/actions/statuses.js @@ -216,10 +216,10 @@ export function fetchStatusWithContext(id) { const features = getFeatures(getState().instance); if (features.paginatedContext) { + await dispatch(fetchStatus(id)); const responses = await Promise.all([ dispatch(fetchAncestors(id)), dispatch(fetchDescendants(id)), - dispatch(fetchStatus(id)), ]); dispatch({ From 1307b436b1bda304be42b6a600334b1cfea3801a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 13 May 2022 20:23:03 -0500 Subject: [PATCH 13/71] Focus the active item in the thread --- app/soapbox/components/scrollable_list.tsx | 15 ++++++++---- app/soapbox/features/status/index.tsx | 27 +++++++++++----------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/app/soapbox/components/scrollable_list.tsx b/app/soapbox/components/scrollable_list.tsx index 672520fa5..cc4b55867 100644 --- a/app/soapbox/components/scrollable_list.tsx +++ b/app/soapbox/components/scrollable_list.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Virtuoso, Components } from 'react-virtuoso'; +import { Virtuoso, Components, VirtuosoProps, VirtuosoHandle } from 'react-virtuoso'; import PullToRefresh from 'soapbox/components/pull-to-refresh'; import { useSettings } from 'soapbox/hooks'; @@ -25,7 +25,7 @@ const List: Components['List'] = React.forwardRef((props, ref) => { return
; }); -interface IScrollableList { +interface IScrollableList extends VirtuosoProps { scrollKey?: string, onLoadMore?: () => void, isLoading?: boolean, @@ -45,7 +45,7 @@ interface IScrollableList { } /** Legacy ScrollableList with Virtuoso for backwards-compatibility */ -const ScrollableList: React.FC = ({ +const ScrollableList = React.forwardRef(({ prepend = null, alwaysPrepend, children, @@ -61,7 +61,9 @@ const ScrollableList: React.FC = ({ hasMore, placeholderComponent: Placeholder, placeholderCount = 0, -}) => { + initialTopMostItemIndex = 0, + scrollerRef, +}, ref) => { const settings = useSettings(); const autoloadMore = settings.get('autoloadMore'); @@ -126,6 +128,7 @@ const ScrollableList: React.FC = ({ /** Render the actual Virtuoso list */ const renderFeed = (): JSX.Element => ( = ({ endReached={handleEndReached} isScrolling={isScrolling => isScrolling && onScroll && onScroll()} itemContent={renderItem} + initialTopMostItemIndex={showLoading ? 0 : initialTopMostItemIndex} context={{ listClassName: className, itemClassName, @@ -145,6 +149,7 @@ const ScrollableList: React.FC = ({ Item, Footer: loadMore, }} + scrollerRef={scrollerRef} /> ); @@ -162,6 +167,6 @@ const ScrollableList: React.FC = ({ {renderBody()} ); -}; +}); export default ScrollableList; diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index 25247ac6f..16c398df7 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -65,6 +65,7 @@ import ThreadStatus from './components/thread-status'; import type { AxiosError } from 'axios'; import type { History } from 'history'; +import type { VirtuosoHandle } from 'react-virtuoso'; import type { AnyAction } from 'redux'; import type { ThunkDispatch } from 'redux-thunk'; import type { RootState } from 'soapbox/store'; @@ -210,6 +211,7 @@ class Status extends ImmutablePureComponent { node: HTMLDivElement | null = null; status: HTMLDivElement | null = null; + scroller: VirtuosoHandle | null = null; _scrolledIntoView: boolean = false; fetchData = async() => { @@ -615,11 +617,10 @@ class Status extends ImmutablePureComponent { } componentDidUpdate(prevProps: IStatus, prevState: IStatusState) { - const { params, status, displayMedia } = this.props; - const { ancestorsIds } = prevProps; + const { params, status, displayMedia, ancestorsIds } = this.props; + const { isLoaded } = this.state; if (params.statusId !== prevProps.params.statusId) { - this._scrolledIntoView = false; this.fetchData(); } @@ -627,17 +628,11 @@ class Status extends ImmutablePureComponent { this.setState({ showMedia: defaultMediaVisibility(status, displayMedia), loadedStatusId: status.id }); } - if (this._scrolledIntoView) { - return; - } - - if (prevProps.status && ancestorsIds && ancestorsIds.size > 0 && this.node) { - const element = this.node.querySelector('.detailed-status'); - - window.requestAnimationFrame(() => { - element?.scrollIntoView(true); + if (params.statusId !== prevProps.params.statusId || status?.id !== prevProps.status?.id || ancestorsIds.size > prevProps.ancestorsIds.size || isLoaded !== prevState.isLoaded) { + this.scroller?.scrollToIndex({ + index: this.props.ancestorsIds.size, + offset: -80, }); - this._scrolledIntoView = true; } } @@ -671,6 +666,10 @@ class Status extends ImmutablePureComponent { })); } + setScrollerRef = (c: VirtuosoHandle) => { + this.scroller = c; + } + render() { const { me, status, ancestorsIds, descendantsIds, intl } = this.props; @@ -788,10 +787,12 @@ class Status extends ImmutablePureComponent {
} + initialTopMostItemIndex={ancestorsIds.size} > {children} From ac610f98d06809b3a5987ec1296d2090de73015a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 14 May 2022 10:37:06 +0200 Subject: [PATCH 14/71] Reverse auth sessions list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/features/auth_token_list/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/auth_token_list/index.tsx b/app/soapbox/features/auth_token_list/index.tsx index 8014def9f..4563e4b34 100644 --- a/app/soapbox/features/auth_token_list/index.tsx +++ b/app/soapbox/features/auth_token_list/index.tsx @@ -55,7 +55,7 @@ const AuthToken: React.FC = ({ token }) => { const AuthTokenList: React.FC = () =>{ const dispatch = useAppDispatch(); const intl = useIntl(); - const tokens = useAppSelector(state => state.security.get('tokens')); + const tokens = useAppSelector(state => state.security.get('tokens').reverse()); useEffect(() => { dispatch(fetchOAuthTokens()); From 3d898957e3dfa55981e2186d0daf7b6059cc868b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 14 May 2022 11:21:12 -0500 Subject: [PATCH 15/71] ActionButton: put remote follow behind feature detection --- .../features/ui/components/action-button.tsx | 41 +++++++++++++------ app/soapbox/utils/features.ts | 6 +++ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/app/soapbox/features/ui/components/action-button.tsx b/app/soapbox/features/ui/components/action-button.tsx index e79c22567..c710f43d9 100644 --- a/app/soapbox/features/ui/components/action-button.tsx +++ b/app/soapbox/features/ui/components/action-button.tsx @@ -105,10 +105,9 @@ const ActionButton = ({ account, actionType, small }: iActionButton) => { ); }; - const empty = <>; - - if (!me) { - // Remote follow + /** Render a remote follow button, depending on features. */ + const renderRemoteFollow = (): JSX.Element | null => { + // Remote follow through the API. if (features.remoteInteractionsAPI) { return (