Reducers: TypeScript, fixes

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
dnd
marcin mikołajczak 2022-06-05 16:17:26 +02:00
rodzic 5bb26c9b47
commit 41a2b1f08f
15 zmienionych plików z 142 dodań i 119 usunięć

Wyświetl plik

@ -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<React.MouseEvent | React.KeyboardEvent>,

Wyświetl plik

@ -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<string, any>;
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) => (
<div className='logentry' key={i}>
<div className='logentry__message'>{item.get('message')}</div>
{items.map((item) => item && (
<div className='logentry' key={item.id}>
<div className='logentry__message'>{item.message}</div>
<div className='logentry__timestamp'>
<FormattedDate
value={new Date(item.get('time') * 1000)}
value={new Date(item.time * 1000)}
hour12={false}
year='numeric'
month='short'

Wyświetl plik

@ -20,9 +20,9 @@ const Bookmarks: React.FC = () => {
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}

Wyświetl plik

@ -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}
/>

Wyświetl plik

@ -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 (
<div className='column-list'>
{suggestions.size > 0 ? suggestions.map((suggestion: { account: string }, idx: number) => (
<Account key={idx} id={suggestion.account} />
{suggestions.size > 0 ? suggestions.map((suggestion) => (
<Account key={suggestion.account} id={suggestion.account} />
)) : (
<div className='column-list__empty-message'>
<FormattedMessage id='empty_column.follow_recommendations' defaultMessage='Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.' />

Wyświetl plik

@ -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<string, any>) => (
<div key={suggestion.get('account')} className='py-2'>
{suggestions.map((suggestion) => (
<div key={suggestion.account} className='py-2'>
<AccountContainer
// @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't
id={suggestion.get('account')}
id={suggestion.account}
showProfileHoverCard={false}
/>
</div>

Wyświetl plik

@ -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,
};
};

Wyświetl plik

@ -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 = () => {
<ScrollableList
scrollKey='scheduled_statuses'
hasMore={hasMore}
isLoading={isLoading}
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
onLoadMore={() => handleLoadMore(dispatch)}
emptyMessage={emptyMessage}
>

Wyświetl plik

@ -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('<WhoToFollow />', () => {
avatar: 'test.jpg',
}),
}),
suggestions: ImmutableMap({
items: fromJS([{
suggestions: {
items: ImmutableOrderedSet([{
source: 'staff',
account: '1',
}]),
}),
},
};
render(<WhoToFollowPanel limit={1} />, null, store);
@ -44,8 +44,8 @@ describe('<WhoToFollow />', () => {
avatar: 'test.jpg',
}),
}),
suggestions: ImmutableMap({
items: fromJS([
suggestions: {
items: ImmutableOrderedSet([
{
source: 'staff',
account: '1',
@ -55,7 +55,7 @@ describe('<WhoToFollow />', () => {
account: '2',
},
]),
}),
},
};
render(<WhoToFollowPanel limit={3} />, null, store);
@ -78,8 +78,8 @@ describe('<WhoToFollow />', () => {
avatar: 'test.jpg',
}),
}),
suggestions: ImmutableMap({
items: fromJS([
suggestions: {
items: ImmutableOrderedSet([
{
source: 'staff',
account: '1',
@ -89,7 +89,7 @@ describe('<WhoToFollow />', () => {
account: '2',
},
]),
}),
},
};
render(<WhoToFollowPanel limit={1} />, null, store);
@ -112,9 +112,9 @@ describe('<WhoToFollow />', () => {
avatar: 'test.jpg',
}),
}),
suggestions: ImmutableMap({
items: fromJS([]),
}),
suggestions: {
items: ImmutableOrderedSet([]),
},
};
render(<WhoToFollowPanel limit={1} />, null, store);

Wyświetl plik

@ -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<string, any>) => {
dispatch(dismissSuggestion(account.get('id')));
const handleDismiss = (account: AccountEntity) => {
dispatch(dismissSuggestion(account.id));
};
React.useEffect(() => {
@ -45,11 +46,11 @@ const WhoToFollowPanel = ({ limit }: IWhoToFollowPanel) => {
title={<FormattedMessage id='who_to_follow.title' defaultMessage='People To Follow' />}
// onAction={handleAction}
>
{suggestionsToRender.map((suggestion: ImmutableMap<string, any>) => (
{suggestionsToRender.map((suggestion) => (
<AccountContainer
key={suggestion.get('account')}
key={suggestion.account}
// @ts-ignore: TS thinks `id` is passed to <Account>, 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}

Wyświetl plik

@ -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(),
}),
}));
});
});

Wyświetl plik

@ -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: [],
},
});
});
});

Wyświetl plik

@ -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<string, any>(),
id: 0,
message: '',
time: 0,
});
const ReducerRecord = ImmutableRecord({
items: ImmutableMap(),
index: ImmutableOrderedSet(),
items: ImmutableMap<string, LogEntry>(),
index: ImmutableOrderedSet<number>(),
total: 0,
});
type LogEntry = ReturnType<typeof LogEntryRecord>;
type State = ReturnType<typeof ReducerRecord>;
type APIEntity = Record<string, any>;
type APIEntities = Array<APIEntity>;
const parseItems = (items: APIEntities) => {
const ids: Array<number> = [];
const map: Record<number, any> = {};
const map: Record<string, LogEntry> = {};
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);
});
};

Wyświetl plik

@ -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,
});

Wyświetl plik

@ -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<string>(),
});
const initialState = ImmutableMap({
favourites: initialMap,
bookmarks: initialMap,
pins: initialMap,
scheduled_statuses: initialMap,
type State = ImmutableMap<string, StatusList>;
type StatusList = ReturnType<typeof StatusListRecord>;
type Status = string | StatusEntity;
type Statuses = Array<string | StatusEntity>;
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<string>);
});
};
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<string>).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;
}