pl-fe: Avoid immutable

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-11-05 21:05:32 +01:00
parent cc858d4f0f
commit 7e5a63c664
12 changed files with 103 additions and 76 deletions

View file

@ -1,4 +1,3 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import throttle from 'lodash/throttle';
import React, { useState, useRef, useCallback, useEffect } from 'react';
@ -30,7 +29,7 @@ const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({
...rest
}) => {
const dispatch = useAppDispatch();
const [accountIds, setAccountIds] = useState(ImmutableOrderedSet<string>());
const [accountIds, setAccountIds] = useState<Array<string>>([]);
const controller = useRef(new AbortController());
const refreshCancelToken = () => {
@ -39,14 +38,14 @@ const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({
};
const clearResults = () => {
setAccountIds(ImmutableOrderedSet());
setAccountIds([]);
};
const handleAccountSearch = useCallback(throttle((q) => {
dispatch(accountSearch(q, controller.current.signal))
.then((accounts: { id: string }[]) => {
const accountIds = accounts.map(account => account.id);
setAccountIds(ImmutableOrderedSet(accountIds));
setAccountIds(accountIds);
})
.catch(noOp);
}, 900, { leading: true, trailing: true }), []);
@ -79,7 +78,7 @@ const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({
<AutosuggestInput
value={value}
onChange={handleChange}
suggestions={accountIds.toList()}
suggestions={accountIds}
onSuggestionsFetchRequested={noOp}
onSuggestionsClearRequested={noOp}
onSuggestionSelected={handleSelected}

View file

@ -1,5 +1,4 @@
import clsx from 'clsx';
import { List as ImmutableList } from 'immutable';
import React, { PureComponent } from 'react';
import AutosuggestEmoji from 'pl-fe/components/autosuggest-emoji';
@ -17,7 +16,7 @@ type AutoSuggestion = string | Emoji;
interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputElement>, 'onChange' | 'onKeyUp' | 'onKeyDown'> {
value: string;
suggestions: ImmutableList<any>;
suggestions: Array<any>;
disabled?: boolean;
placeholder?: string;
onSuggestionSelected: (tokenStart: number, lastToken: string | null, suggestion: AutoSuggestion) => void;
@ -40,7 +39,7 @@ class AutosuggestInput extends PureComponent<IAutosuggestInput> {
static defaultProps = {
autoFocus: false,
autoSelect: true,
searchTokens: ImmutableList(['@', ':', '#']),
searchTokens: ['@', ':', '#'],
};
getFirstIndex = () => this.props.autoSelect ? 0 : -1;
@ -79,7 +78,7 @@ class AutosuggestInput extends PureComponent<IAutosuggestInput> {
const { suggestions, menu, disabled } = this.props;
const { selectedSuggestion, suggestionsHidden } = this.state;
const firstIndex = this.getFirstIndex();
const lastIndex = suggestions.size + (menu || []).length - 1;
const lastIndex = suggestions.length + (menu || []).length - 1;
if (disabled) {
e.preventDefault();
@ -94,7 +93,7 @@ class AutosuggestInput extends PureComponent<IAutosuggestInput> {
switch (e.key) {
case 'Escape':
if (suggestions.size === 0 || suggestionsHidden) {
if (suggestions.length === 0 || suggestionsHidden) {
document.querySelector('.ui')?.parentElement?.focus();
} else {
e.preventDefault();
@ -103,14 +102,14 @@ class AutosuggestInput extends PureComponent<IAutosuggestInput> {
break;
case 'ArrowDown':
if (!suggestionsHidden && (suggestions.size > 0 || menu)) {
if (!suggestionsHidden && (suggestions.length > 0 || menu)) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, lastIndex) });
}
break;
case 'ArrowUp':
if (!suggestionsHidden && (suggestions.size > 0 || menu)) {
if (!suggestionsHidden && (suggestions.length > 0 || menu)) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, firstIndex) });
}
@ -119,15 +118,15 @@ class AutosuggestInput extends PureComponent<IAutosuggestInput> {
case 'Enter':
case 'Tab':
// Select suggestion
if (!suggestionsHidden && selectedSuggestion > -1 && (suggestions.size > 0 || menu)) {
if (!suggestionsHidden && selectedSuggestion > -1 && (suggestions.length > 0 || menu)) {
e.preventDefault();
e.stopPropagation();
this.setState({ selectedSuggestion: firstIndex });
if (selectedSuggestion < suggestions.size) {
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
if (selectedSuggestion < suggestions.length) {
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions[selectedSuggestion]);
} else if (menu) {
const item = menu[selectedSuggestion - suggestions.size];
const item = menu[selectedSuggestion - suggestions.length];
this.handleMenuItemAction(item, e);
}
}
@ -154,7 +153,7 @@ class AutosuggestInput extends PureComponent<IAutosuggestInput> {
onSuggestionClick: React.EventHandler<React.MouseEvent | React.TouchEvent> = (e) => {
const index = Number(e.currentTarget?.getAttribute('data-index'));
const suggestion = this.props.suggestions.get(index);
const suggestion = this.props.suggestions[index];
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.input?.focus();
e.preventDefault();
@ -162,7 +161,7 @@ class AutosuggestInput extends PureComponent<IAutosuggestInput> {
componentDidUpdate(prevProps: IAutosuggestInput, prevState: any) {
const { suggestions } = this.props;
if (suggestions !== prevProps.suggestions && suggestions.size > 0 && prevState.suggestionsHidden && prevState.focused) {
if (suggestions !== prevProps.suggestions && suggestions.length > 0 && prevState.suggestionsHidden && prevState.focused) {
this.setState({ suggestionsHidden: false });
}
}
@ -231,7 +230,9 @@ class AutosuggestInput extends PureComponent<IAutosuggestInput> {
return menu.map((item, i) => (
<a
className={clsx('flex cursor-pointer items-center space-x-2 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-100 focus:bg-gray-100 dark:text-gray-500 dark:hover:bg-gray-800 dark:focus:bg-primary-800', { selected: suggestions.size - selectedSuggestion === i })}
className={clsx('flex cursor-pointer items-center space-x-2 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-100 focus:bg-gray-100 dark:text-gray-500 dark:hover:bg-gray-800 dark:focus:bg-primary-800', {
selected: suggestions.length - selectedSuggestion === i,
})}
href='#'
role='button'
tabIndex={0}
@ -261,7 +262,7 @@ class AutosuggestInput extends PureComponent<IAutosuggestInput> {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, menu, theme } = this.props;
const { suggestionsHidden } = this.state;
const visible = !suggestionsHidden && (!suggestions.isEmpty() || (menu && value));
const visible = !suggestionsHidden && (suggestions.length || (menu && value));
return [
<div key='input' className='relative w-full'>

View file

@ -1,5 +1,4 @@
import clsx from 'clsx';
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import throttle from 'lodash/throttle';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -24,7 +23,7 @@ interface ILocationSearch {
const LocationSearch: React.FC<ILocationSearch> = ({ onSelected }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const [locationIds, setLocationIds] = useState(ImmutableOrderedSet<string>());
const [locationIds, setLocationIds] = useState<Array<string>>([]);
const controller = useRef(new AbortController());
const [value, setValue] = useState('');
@ -63,14 +62,14 @@ const LocationSearch: React.FC<ILocationSearch> = ({ onSelected }) => {
};
const clearResults = () => {
setLocationIds(ImmutableOrderedSet());
setLocationIds([]);
};
const handleLocationSearch = useCallback(throttle(q => {
dispatch(locationSearch(q, controller.current.signal))
.then((locations: { origin_id: string }[]) => {
const locationIds = locations.map(location => location.origin_id);
setLocationIds(ImmutableOrderedSet(locationIds));
setLocationIds(locationIds);
})
.catch(noOp);
}, 900, { leading: true, trailing: true }), []);
@ -88,7 +87,7 @@ const LocationSearch: React.FC<ILocationSearch> = ({ onSelected }) => {
placeholder={intl.formatMessage(messages.placeholder)}
value={value}
onChange={handleChange}
suggestions={locationIds.toList()}
suggestions={locationIds}
onSuggestionsFetchRequested={noOp}
onSuggestionsClearRequested={noOp}
onSuggestionSelected={handleSelected}

View file

@ -81,7 +81,7 @@ const AuthToken: React.FC<IAuthToken> = ({ token, isCurrent }) => {
const AuthTokenList: React.FC = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const tokens = useAppSelector(state => state.security.get('tokens').toReversed());
const tokens = useAppSelector(state => state.security.tokens.toReversed());
const currentTokenId = useAppSelector(state => {
const currentToken = state.auth.tokens.valueSeq().find((token) => token.me === state.auth.me);

View file

@ -90,7 +90,7 @@ const Option: React.FC<IOption> = ({
maxLength={maxChars}
value={title}
onChange={handleOptionTitleChange}
suggestions={suggestions}
suggestions={suggestions.toArray()}
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
onSuggestionsClearRequested={onSuggestionsClearRequested}
onSuggestionSelected={onSuggestionSelected}

View file

@ -37,7 +37,7 @@ const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
placeholder={intl.formatMessage(messages.placeholder)}
value={value}
onChange={handleChangeSpoilerText}
suggestions={suggestions}
suggestions={suggestions.toArray()}
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
onSuggestionsClearRequested={onSuggestionsClearRequested}
onSuggestionSelected={onSuggestionSelected}

View file

@ -26,7 +26,7 @@ const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
const features = useFeatures();
const dispatch = useAppDispatch();
const tag = useAppSelector((state) => state.tags.get(tagId));
const tag = useAppSelector((state) => state.tags[tagId]);
const { isLoggedIn } = useLoggedIn();
const theme = useTheme();
const isMobile = useIsMobile();

View file

@ -49,7 +49,7 @@ const Settings = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const mfa = useAppSelector((state) => state.security.get('mfa'));
const mfa = useAppSelector((state) => state.security.mfa);
const features = useFeatures();
const { account } = useOwnAccount();

View file

@ -1,4 +1,4 @@
import { Record as ImmutableRecord } from 'immutable';
import { create } from 'mutative';
import {
MFA_FETCH_SUCCESS,
@ -10,37 +10,52 @@ import { FETCH_TOKENS_SUCCESS, REVOKE_TOKEN_SUCCESS } from '../actions/security'
import type { OauthToken } from 'pl-api';
import type { AnyAction } from 'redux';
const ReducerRecord = ImmutableRecord({
tokens: [] as Array<OauthToken>,
interface State {
tokens: Array<OauthToken>;
mfa: {
settings: Record<string, boolean>;
};
}
const initialState: State = {
tokens: [],
mfa: {
settings: {
totp: false,
},
},
});
};
type State = ReturnType<typeof ReducerRecord>;
const deleteToken = (state: State, tokenId: number) => state.tokens = state.tokens.filter(token => token.id !== tokenId);
const deleteToken = (state: State, tokenId: number) => state.update('tokens', tokens => tokens.filter(token => token.id !== tokenId));
const importMfa = (state: State, data: any) => state.mfa = data;
const importMfa = (state: State, data: any) => state.set('mfa', data);
const enableMfa = (state: State, method: string) => state.mfa.settings = { ...state.mfa.settings, [method]: true };
const enableMfa = (state: State, method: string) => state.update('mfa', mfa => ({ settings: { ...mfa.settings, [method]: true } }));
const disableMfa = (state: State, method: string) => state.mfa.settings = { ...state.mfa.settings, [method]: false };
const disableMfa = (state: State, method: string) => state.update('mfa', mfa => ({ settings: { ...mfa.settings, [method]: false } }));
const security = (state = ReducerRecord(), action: AnyAction) => {
const security = (state = initialState, action: AnyAction) => {
switch (action.type) {
case FETCH_TOKENS_SUCCESS:
return state.set('tokens', action.tokens);
return create(state, (draft) => {
draft.tokens = action.tokens;
});
case REVOKE_TOKEN_SUCCESS:
return deleteToken(state, action.tokenId);
return create(state, (draft) => {
deleteToken(draft, action.tokenId);
});
case MFA_FETCH_SUCCESS:
return importMfa(state, action.data);
return create(state, (draft) => {
importMfa(draft, action.data);
});
case MFA_CONFIRM_SUCCESS:
return enableMfa(state, action.method);
return create(state, (draft) => {
enableMfa(draft, action.method);
});
case MFA_DISABLE_SUCCESS:
return disableMfa(state, action.method);
return create(state, (draft) => {
disableMfa(draft, action.method);
});
default:
return state;
}

View file

@ -1,4 +1,4 @@
import { Map as ImmutableMap } from 'immutable';
import { create } from 'mutative';
import {
HASHTAG_FETCH_SUCCESS,
@ -11,18 +11,26 @@ import {
import type { Tag } from 'pl-api';
const initialState = ImmutableMap<string, Tag>();
type State = Record<string, Tag>;
const initialState: State = {};
const tags = (state = initialState, action: TagsAction) => {
switch (action.type) {
case HASHTAG_FETCH_SUCCESS:
return state.set(action.name, action.tag);
return create(state, (draft) => {
draft[action.name] = action.tag;
});
case HASHTAG_FOLLOW_REQUEST:
case HASHTAG_UNFOLLOW_FAIL:
return state.setIn([action.name, 'following'], true);
return create(state, (draft) => {
if (draft[action.name]) draft[action.name].following = true;
});
case HASHTAG_FOLLOW_FAIL:
case HASHTAG_UNFOLLOW_REQUEST:
return state.setIn([action.name, 'following'], false);
return create(state, (draft) => {
if (draft[action.name]) draft[action.name].following = false;
});
default:
return state;
}

View file

@ -1,30 +1,32 @@
import { Record as ImmutableRecord } from 'immutable';
import { create } from 'mutative';
import { TRENDING_STATUSES_FETCH_REQUEST, TRENDING_STATUSES_FETCH_SUCCESS, type TrendingStatusesAction } from 'pl-fe/actions/trending-statuses';
import type { Status } from 'pl-api';
const ReducerRecord = ImmutableRecord({
items: Array<string>(),
isLoading: false,
});
interface State {
items: Array<string>;
isLoading: boolean;
}
type State = ReturnType<typeof ReducerRecord>;
const initialState: State = {
items: [],
isLoading: false,
};
const toIds = (items: Array<Status>) => items.map(item => item.id);
const importStatuses = (state: State, statuses: Array<Status>) =>
state.withMutations(state => {
state.set('items', toIds(statuses));
state.set('isLoading', false);
});
const trending_statuses = (state: State = ReducerRecord(), action: TrendingStatusesAction) => {
const trending_statuses = (state = initialState, action: TrendingStatusesAction) => {
switch (action.type) {
case TRENDING_STATUSES_FETCH_REQUEST:
return state.set('isLoading', true);
return create(state, (draft) => {
draft.isLoading = true;
});
case TRENDING_STATUSES_FETCH_SUCCESS:
return importStatuses(state, action.statuses);
return create(state, (draft) => {
draft.items = toIds(action.statuses);
draft.isLoading = false;
});
default:
return state;
}

View file

@ -1,22 +1,25 @@
import { Record as ImmutableRecord } from 'immutable';
import { create } from 'mutative';
import { TRENDS_FETCH_SUCCESS, type TrendsAction } from '../actions/trends';
import type { Tag } from 'pl-api';
const ReducerRecord = ImmutableRecord({
items: Array<Tag>(),
interface State {
items: Array<Tag>;
isLoading: boolean;
}
const initialState: State = {
items: [],
isLoading: false,
});
};
type State = ReturnType<typeof ReducerRecord>;
const trendsReducer = (state: State = ReducerRecord(), action: TrendsAction) => {
const trendsReducer = (state = initialState, action: TrendsAction) => {
switch (action.type) {
case TRENDS_FETCH_SUCCESS:
return state.withMutations(map => {
map.set('items', action.tags);
map.set('isLoading', false);
return create(state, (draft) => {
draft.items = action.tags;
draft.isLoading = false;
});
default:
return state;