diff --git a/app/soapbox/components/dropdown_menu.tsx b/app/soapbox/components/dropdown_menu.tsx index cf4b09b7c..d56a72a01 100644 --- a/app/soapbox/components/dropdown_menu.tsx +++ b/app/soapbox/components/dropdown_menu.tsx @@ -249,7 +249,7 @@ export interface IDropdown extends RouteComponentProps { ) => void, onClose?: (id: number) => void, dropdownPlacement?: string, - openDropdownId?: number, + openDropdownId?: number | null, openedViaKeyboard?: boolean, text?: string, onShiftClick?: React.EventHandler, diff --git a/app/soapbox/features/admin/moderation_log.tsx b/app/soapbox/features/admin/moderation_log.tsx index 83163a382..b7a9bce2e 100644 --- a/app/soapbox/features/admin/moderation_log.tsx +++ b/app/soapbox/features/admin/moderation_log.tsx @@ -7,8 +7,6 @@ import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import Column from '../ui/components/column'; -import type { Map as ImmutableMap } from 'immutable'; - const messages = defineMessages({ heading: { id: 'column.admin.moderation_log', defaultMessage: 'Moderation Log' }, emptyMessage: { id: 'admin.moderation_log.empty_message', defaultMessage: 'You have not performed any moderation actions yet. When you do, a history will be shown here.' }, @@ -18,8 +16,10 @@ const ModerationLog = () => { const intl = useIntl(); const dispatch = useAppDispatch(); - const items = useAppSelector((state) => state.admin_log.get('index').map((i: number) => state.admin_log.getIn(['items', String(i)]))) as ImmutableMap; - const hasMore = useAppSelector((state) => state.admin_log.get('total', 0) - state.admin_log.get('index').count() > 0); + const items = useAppSelector((state) => { + return state.admin_log.index.map((i) => state.admin_log.items.get(String(i))); + }); + const hasMore = useAppSelector((state) => state.admin_log.total - state.admin_log.index.count() > 0); const [isLoading, setIsLoading] = useState(true); const [lastPage, setLastPage] = useState(0); @@ -56,12 +56,12 @@ const ModerationLog = () => { hasMore={hasMore} onLoadMore={handleLoadMore} > - {items.map((item, i) => ( -
-
{item.get('message')}
+ {items.map((item) => item && ( +
+
{item.message}
{ const dispatch = useAppDispatch(); const intl = useIntl(); - const statusIds = useAppSelector((state) => state.status_lists.getIn(['bookmarks', 'items'])); - const isLoading = useAppSelector((state) => state.status_lists.getIn(['bookmarks', 'isLoading'], true)); - const hasMore = useAppSelector((state) => !!state.status_lists.getIn(['bookmarks', 'next'])); + const statusIds = useAppSelector((state) => state.status_lists.get('bookmarks')!.items); + const isLoading = useAppSelector((state) => state.status_lists.get('bookmarks')!.isLoading); + const hasMore = useAppSelector((state) => !!state.status_lists.get('bookmarks')!.next); React.useEffect(() => { dispatch(fetchBookmarkedStatuses()); @@ -43,7 +43,7 @@ const Bookmarks: React.FC = () => { statusIds={statusIds} scrollKey='bookmarked_statuses' hasMore={hasMore} - isLoading={isLoading} + isLoading={typeof isLoading === 'boolean' ? isLoading : true} onLoadMore={() => handleLoadMore(dispatch)} onRefresh={handleRefresh} emptyMessage={emptyMessage} diff --git a/app/soapbox/features/favourited_statuses/index.js b/app/soapbox/features/favourited_statuses/index.js index 73ad5a255..0de3d520d 100644 --- a/app/soapbox/features/favourited_statuses/index.js +++ b/app/soapbox/features/favourited_statuses/index.js @@ -32,9 +32,9 @@ const mapStateToProps = (state, { params }) => { if (isMyAccount) { return { isMyAccount, - statusIds: state.getIn(['status_lists', 'favourites', 'items']), - isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true), - hasMore: !!state.getIn(['status_lists', 'favourites', 'next']), + statusIds: state.status_lists.get('favourites').items, + isLoading: state.status_lists.get('favourites').isLoading, + hasMore: !!state.status_lists.get('favourites').next, }; } @@ -57,9 +57,9 @@ const mapStateToProps = (state, { params }) => { unavailable, username, isAccount: !!state.getIn(['accounts', accountId]), - statusIds: state.getIn(['status_lists', `favourites:${accountId}`, 'items'], []), - isLoading: state.getIn(['status_lists', `favourites:${accountId}`, 'isLoading'], true), - hasMore: !!state.getIn(['status_lists', `favourites:${accountId}`, 'next']), + statusIds: state.status_lists.get(`favourites:${accountId}`)?.items || [], + isLoading: state.status_lists.get(`favourites:${accountId}`)?.isLoading, + hasMore: !!state.status_lists.get(`favourites:${accountId}`)?.next, }; }; @@ -147,7 +147,7 @@ class Favourites extends ImmutablePureComponent { statusIds={statusIds} scrollKey='favourited_statuses' hasMore={hasMore} - isLoading={isLoading} + isLoading={typeof isLoading === 'boolean' ? isLoading : true} onLoadMore={this.handleLoadMore} emptyMessage={emptyMessage} /> diff --git a/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx b/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx index 841f5e7d0..e9e295d58 100644 --- a/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx +++ b/app/soapbox/features/follow_recommendations/components/follow_recommendations_list.tsx @@ -11,8 +11,8 @@ import Account from './account'; const FollowRecommendationsList: React.FC = () => { const dispatch = useDispatch(); - const suggestions = useAppSelector((state) => state.suggestions.get('items')); - const isLoading = useAppSelector((state) => state.suggestions.get('isLoading')); + const suggestions = useAppSelector((state) => state.suggestions.items); + const isLoading = useAppSelector((state) => state.suggestions.isLoading); useEffect(() => { if (suggestions.size === 0) { @@ -30,8 +30,8 @@ const FollowRecommendationsList: React.FC = () => { return (
- {suggestions.size > 0 ? suggestions.map((suggestion: { account: string }, idx: number) => ( - + {suggestions.size > 0 ? suggestions.map((suggestion) => ( + )) : (
diff --git a/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx b/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx index 2ef5ab828..0b09ca4c1 100644 --- a/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx +++ b/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx @@ -1,4 +1,3 @@ -import { Map as ImmutableMap } from 'immutable'; import debounce from 'lodash/debounce'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; @@ -13,9 +12,9 @@ import { useAppSelector } from 'soapbox/hooks'; const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => { const dispatch = useDispatch(); - const suggestions = useAppSelector((state) => state.suggestions.get('items')); - const hasMore = useAppSelector((state) => !!state.suggestions.get('next')); - const isLoading = useAppSelector((state) => state.suggestions.get('isLoading')); + const suggestions = useAppSelector((state) => state.suggestions.items); + const hasMore = useAppSelector((state) => !!state.suggestions.next); + const isLoading = useAppSelector((state) => state.suggestions.isLoading); const handleLoadMore = debounce(() => { if (isLoading) { @@ -40,11 +39,11 @@ const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => { useWindowScroll={false} style={{ height: 320 }} > - {suggestions.map((suggestion: ImmutableMap) => ( -
+ {suggestions.map((suggestion) => ( +
, but it isn't - id={suggestion.get('account')} + id={suggestion.account} showProfileHoverCard={false} />
diff --git a/app/soapbox/features/pinned_statuses/index.js b/app/soapbox/features/pinned_statuses/index.js index b6649a01b..9901131ba 100644 --- a/app/soapbox/features/pinned_statuses/index.js +++ b/app/soapbox/features/pinned_statuses/index.js @@ -21,8 +21,8 @@ const mapStateToProps = (state, { params }) => { const meUsername = state.getIn(['accounts', me, 'username'], ''); return { isMyAccount: (username.toLowerCase() === meUsername.toLowerCase()), - statusIds: state.getIn(['status_lists', 'pins', 'items']), - hasMore: !!state.getIn(['status_lists', 'pins', 'next']), + statusIds: state.status_lists.get('pins').items, + hasMore: !!state.status_lists.get('pins').next, }; }; diff --git a/app/soapbox/features/scheduled_statuses/index.tsx b/app/soapbox/features/scheduled_statuses/index.tsx index 006e8894c..148c9c0a4 100644 --- a/app/soapbox/features/scheduled_statuses/index.tsx +++ b/app/soapbox/features/scheduled_statuses/index.tsx @@ -22,9 +22,9 @@ const ScheduledStatuses = () => { const intl = useIntl(); const dispatch = useAppDispatch(); - const statusIds = useAppSelector((state) => state.status_lists.getIn(['scheduled_statuses', 'items'])); - const isLoading = useAppSelector((state) => state.status_lists.getIn(['scheduled_statuses', 'isLoading'])); - const hasMore = useAppSelector((state) => !!state.status_lists.getIn(['scheduled_statuses', 'next'])); + const statusIds = useAppSelector((state) => state.status_lists.get('scheduled_statuses')!.items); + const isLoading = useAppSelector((state) => state.status_lists.get('scheduled_statuses')!.isLoading); + const hasMore = useAppSelector((state) => !!state.status_lists.get('scheduled_statuses')!.next); useEffect(() => { dispatch(fetchScheduledStatuses()); @@ -37,7 +37,7 @@ const ScheduledStatuses = () => { handleLoadMore(dispatch)} emptyMessage={emptyMessage} > diff --git a/app/soapbox/features/ui/components/__tests__/who-to-follow-panel.test.tsx b/app/soapbox/features/ui/components/__tests__/who-to-follow-panel.test.tsx index 92123eb24..56e4f8422 100644 --- a/app/soapbox/features/ui/components/__tests__/who-to-follow-panel.test.tsx +++ b/app/soapbox/features/ui/components/__tests__/who-to-follow-panel.test.tsx @@ -1,4 +1,4 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; +import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; import React from 'react'; import { render, screen } from '../../../../jest/test-helpers'; @@ -16,12 +16,12 @@ describe('', () => { avatar: 'test.jpg', }), }), - suggestions: ImmutableMap({ - items: fromJS([{ + suggestions: { + items: ImmutableOrderedSet([{ source: 'staff', account: '1', }]), - }), + }, }; render(, null, store); @@ -44,8 +44,8 @@ describe('', () => { avatar: 'test.jpg', }), }), - suggestions: ImmutableMap({ - items: fromJS([ + suggestions: { + items: ImmutableOrderedSet([ { source: 'staff', account: '1', @@ -55,7 +55,7 @@ describe('', () => { account: '2', }, ]), - }), + }, }; render(, null, store); @@ -78,8 +78,8 @@ describe('', () => { avatar: 'test.jpg', }), }), - suggestions: ImmutableMap({ - items: fromJS([ + suggestions: { + items: ImmutableOrderedSet([ { source: 'staff', account: '1', @@ -89,7 +89,7 @@ describe('', () => { account: '2', }, ]), - }), + }, }; render(, null, store); @@ -112,9 +112,9 @@ describe('', () => { avatar: 'test.jpg', }), }), - suggestions: ImmutableMap({ - items: fromJS([]), - }), + suggestions: { + items: ImmutableOrderedSet([]), + }, }; render(, null, store); diff --git a/app/soapbox/features/ui/components/who-to-follow-panel.tsx b/app/soapbox/features/ui/components/who-to-follow-panel.tsx index 8decff7db..fd6089730 100644 --- a/app/soapbox/features/ui/components/who-to-follow-panel.tsx +++ b/app/soapbox/features/ui/components/who-to-follow-panel.tsx @@ -1,6 +1,5 @@ -import { Map as ImmutableMap } from 'immutable'; import * as React from 'react'; -import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; import { fetchSuggestions, dismissSuggestion } from 'soapbox/actions/suggestions'; @@ -8,6 +7,8 @@ import { Widget } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; import { useAppSelector } from 'soapbox/hooks'; +import type { Account as AccountEntity } from 'soapbox/types/entities'; + const messages = defineMessages({ dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, }); @@ -20,11 +21,11 @@ const WhoToFollowPanel = ({ limit }: IWhoToFollowPanel) => { const dispatch = useDispatch(); const intl = useIntl(); - const suggestions = useAppSelector((state) => state.suggestions.get('items')); + const suggestions = useAppSelector((state) => state.suggestions.items); const suggestionsToRender = suggestions.slice(0, limit); - const handleDismiss = (account: ImmutableMap) => { - dispatch(dismissSuggestion(account.get('id'))); + const handleDismiss = (account: AccountEntity) => { + dispatch(dismissSuggestion(account.id)); }; React.useEffect(() => { @@ -45,11 +46,11 @@ const WhoToFollowPanel = ({ limit }: IWhoToFollowPanel) => { title={} // onAction={handleAction} > - {suggestionsToRender.map((suggestion: ImmutableMap) => ( + {suggestionsToRender.map((suggestion) => ( , but it isn't - id={suggestion.get('account')} + id={suggestion.account} actionIcon={require('@tabler/icons/icons/x.svg')} actionTitle={intl.formatMessage(messages.dismissSuggestion)} onActionClick={handleDismiss} diff --git a/app/soapbox/reducers/__tests__/status_lists-test.js b/app/soapbox/reducers/__tests__/status_lists-test.js deleted file mode 100644 index e04a24377..000000000 --- a/app/soapbox/reducers/__tests__/status_lists-test.js +++ /dev/null @@ -1,30 +0,0 @@ -import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; - -import reducer from '../status_lists'; - -describe('status_lists reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {})).toEqual(ImmutableMap({ - favourites: ImmutableMap({ - next: null, - loaded: false, - items: ImmutableOrderedSet(), - }), - bookmarks: ImmutableMap({ - next: null, - loaded: false, - items: ImmutableOrderedSet(), - }), - pins: ImmutableMap({ - next: null, - loaded: false, - items: ImmutableOrderedSet(), - }), - scheduled_statuses: ImmutableMap({ - next: null, - loaded: false, - items: ImmutableOrderedSet(), - }), - })); - }); -}); diff --git a/app/soapbox/reducers/__tests__/status_lists.test.ts b/app/soapbox/reducers/__tests__/status_lists.test.ts new file mode 100644 index 000000000..2333df89a --- /dev/null +++ b/app/soapbox/reducers/__tests__/status_lists.test.ts @@ -0,0 +1,32 @@ +import reducer from '../status_lists'; + +describe('status_lists reducer', () => { + it('should return the initial state', () => { + expect(reducer(undefined, {} as any).toJS()).toEqual({ + favourites: { + next: null, + loaded: false, + isLoading: null, + items: [], + }, + bookmarks: { + next: null, + loaded: false, + isLoading: null, + items: [], + }, + pins: { + next: null, + loaded: false, + isLoading: null, + items: [], + }, + scheduled_statuses: { + next: null, + loaded: false, + isLoading: null, + items: [], + }, + }); + }); +}); diff --git a/app/soapbox/reducers/admin_log.ts b/app/soapbox/reducers/admin_log.ts index 494592492..fc02a1c5c 100644 --- a/app/soapbox/reducers/admin_log.ts +++ b/app/soapbox/reducers/admin_log.ts @@ -2,30 +2,37 @@ import { Map as ImmutableMap, Record as ImmutableRecord, OrderedSet as ImmutableOrderedSet, - fromJS, } from 'immutable'; import { ADMIN_LOG_FETCH_SUCCESS } from 'soapbox/actions/admin'; import type { AnyAction } from 'redux'; +const LogEntryRecord = ImmutableRecord({ + data: ImmutableMap(), + id: 0, + message: '', + time: 0, +}); + const ReducerRecord = ImmutableRecord({ - items: ImmutableMap(), - index: ImmutableOrderedSet(), + items: ImmutableMap(), + index: ImmutableOrderedSet(), total: 0, }); +type LogEntry = ReturnType; type State = ReturnType; type APIEntity = Record; type APIEntities = Array; const parseItems = (items: APIEntities) => { const ids: Array = []; - const map: Record = {}; + const map: Record = {}; items.forEach(item => { ids.push(item.id); - map[item.id] = item; + map[item.id] = LogEntryRecord(item); }); return { ids: ids, map: map }; @@ -36,7 +43,7 @@ const importItems = (state: State, items: APIEntities, total: number) => { return state.withMutations(state => { state.update('index', v => v.union(ids)); - state.update('items', v => v.merge(fromJS(map))); + state.update('items', v => v.merge(map)); state.set('total', total); }); }; diff --git a/app/soapbox/reducers/dropdown_menu.ts b/app/soapbox/reducers/dropdown_menu.ts index 6c36ab8b2..2b5c50252 100644 --- a/app/soapbox/reducers/dropdown_menu.ts +++ b/app/soapbox/reducers/dropdown_menu.ts @@ -6,10 +6,11 @@ import { } from '../actions/dropdown_menu'; import type { AnyAction } from 'redux'; +import type { DropdownPlacement } from 'soapbox/components/dropdown_menu'; const ReducerRecord = ImmutableRecord({ openId: null as number | null, - placement: null as 'top' | 'bottom' | null, + placement: null as any as DropdownPlacement, keyboard: false, }); diff --git a/app/soapbox/reducers/status_lists.js b/app/soapbox/reducers/status_lists.ts similarity index 73% rename from app/soapbox/reducers/status_lists.js rename to app/soapbox/reducers/status_lists.ts index e07b7078c..be87e2910 100644 --- a/app/soapbox/reducers/status_lists.js +++ b/app/soapbox/reducers/status_lists.ts @@ -1,4 +1,8 @@ -import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; +import { + Map as ImmutableMap, + OrderedSet as ImmutableOrderedSet, + Record as ImmutableRecord, +} from 'immutable'; import { BOOKMARKED_STATUSES_FETCH_REQUEST, @@ -44,29 +48,38 @@ import { SCHEDULED_STATUS_CANCEL_SUCCESS, } from '../actions/scheduled_statuses'; -const initialMap = ImmutableMap({ - next: null, +import type { AnyAction } from 'redux'; +import type { Status as StatusEntity } from 'soapbox/types/entities'; + +const StatusListRecord = ImmutableRecord({ + next: null as string | null, loaded: false, - items: ImmutableOrderedSet(), + isLoading: null as boolean | null, + items: ImmutableOrderedSet(), }); -const initialState = ImmutableMap({ - favourites: initialMap, - bookmarks: initialMap, - pins: initialMap, - scheduled_statuses: initialMap, +type State = ImmutableMap; +type StatusList = ReturnType; +type Status = string | StatusEntity; +type Statuses = Array; + +const initialState: State = ImmutableMap({ + favourites: StatusListRecord(), + bookmarks: StatusListRecord(), + pins: StatusListRecord(), + scheduled_statuses: StatusListRecord(), }); -const getStatusId = status => typeof status === 'string' ? status : status.get('id'); +const getStatusId = (status: string | StatusEntity) => typeof status === 'string' ? status : status.id; -const getStatusIds = (statuses = []) => ( - ImmutableOrderedSet(statuses.map(status => status.id)) +const getStatusIds = (statuses: Statuses = []) => ( + ImmutableOrderedSet(statuses.map(getStatusId)) ); -const setLoading = (state, listType, loading) => state.setIn([listType, 'isLoading'], loading); +const setLoading = (state: State, listType: string, loading: boolean) => state.setIn([listType, 'isLoading'], loading); -const normalizeList = (state, listType, statuses, next) => { - return state.update(listType, initialMap, listMap => listMap.withMutations(map => { +const normalizeList = (state: State, listType: string, statuses: Statuses, next: string | null) => { + return state.update(listType, StatusListRecord(), listMap => listMap.withMutations(map => { map.set('next', next); map.set('loaded', true); map.set('isLoading', false); @@ -74,29 +87,29 @@ const normalizeList = (state, listType, statuses, next) => { })); }; -const appendToList = (state, listType, statuses, next) => { +const appendToList = (state: State, listType: string, statuses: Statuses, next: string | null) => { const newIds = getStatusIds(statuses); - return state.update(listType, initialMap, listMap => listMap.withMutations(map => { + return state.update(listType, StatusListRecord(), listMap => listMap.withMutations(map => { map.set('next', next); map.set('isLoading', false); - map.update('items', ImmutableOrderedSet(), items => items.union(newIds)); + map.update('items', items => items.union(newIds)); })); }; -const prependOneToList = (state, listType, status) => { +const prependOneToList = (state: State, listType: string, status: Status) => { const statusId = getStatusId(status); return state.updateIn([listType, 'items'], ImmutableOrderedSet(), items => { - return ImmutableOrderedSet([statusId]).union(items); + return ImmutableOrderedSet([statusId]).union(items as ImmutableOrderedSet); }); }; -const removeOneFromList = (state, listType, status) => { +const removeOneFromList = (state: State, listType: string, status: Status) => { const statusId = getStatusId(status); - return state.updateIn([listType, 'items'], ImmutableOrderedSet(), items => items.delete(statusId)); + return state.updateIn([listType, 'items'], ImmutableOrderedSet(), items => (items as ImmutableOrderedSet).delete(statusId)); }; -export default function statusLists(state = initialState, action) { +export default function statusLists(state = initialState, action: AnyAction) { switch (action.type) { case FAVOURITED_STATUSES_FETCH_REQUEST: case FAVOURITED_STATUSES_EXPAND_REQUEST: @@ -154,7 +167,7 @@ export default function statusLists(state = initialState, action) { return appendToList(state, 'scheduled_statuses', action.statuses, action.next); case SCHEDULED_STATUS_CANCEL_REQUEST: case SCHEDULED_STATUS_CANCEL_SUCCESS: - return removeOneFromList(state, 'scheduled_statuses', action.id || action.status.get('id')); + return removeOneFromList(state, 'scheduled_statuses', action.id || action.status.id); default: return state; }