TypeScript, FC (reducers, search)

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
chats-fixes
marcin mikołajczak 2022-06-07 18:25:53 +02:00
rodzic bdb958a613
commit 6c45dcb109
40 zmienionych plików z 609 dodań i 672 usunięć

Wyświetl plik

@ -26,6 +26,8 @@ export interface MenuItem {
icon: string,
count?: number,
destructive?: boolean,
meta?: string,
active?: boolean,
}
export type Menu = Array<MenuItem | null>;

Wyświetl plik

@ -10,24 +10,26 @@ import { shortNumberFormat } from '../utils/numbers';
import Permalink from './permalink';
import { HStack, Stack, Text } from './ui';
import type { Map as ImmutableMap } from 'immutable';
import type { Hashtag as HashtagEntity } from 'soapbox/reducers/search';
import type { TrendingHashtag } from 'soapbox/reducers/trends';
interface IHashtag {
hashtag: ImmutableMap<string, any>,
hashtag: HashtagEntity | TrendingHashtag,
}
const Hashtag: React.FC<IHashtag> = ({ hashtag }) => {
const count = Number(hashtag.getIn(['history', 0, 'accounts']));
const brandColor = useSelector((state) => getSoapboxConfig(state).get('brandColor'));
const history = (hashtag as TrendingHashtag).history;
const count = Number(history?.get(0)?.accounts);
const brandColor = useSelector((state) => getSoapboxConfig(state).brandColor);
return (
<HStack alignItems='center' justifyContent='between' data-testid='hashtag'>
<Stack>
<Permalink href={hashtag.get('url')} to={`/tags/${hashtag.get('name')}`} className='hover:underline'>
<Text tag='span' size='sm' weight='semibold'>#{hashtag.get('name')}</Text>
<Permalink href={hashtag.url} to={`/tags/${hashtag.name}`} className='hover:underline'>
<Text tag='span' size='sm' weight='semibold'>#{hashtag.name}</Text>
</Permalink>
{hashtag.get('history') && (
{history && (
<Text theme='muted' size='sm'>
<FormattedMessage
id='trends.count_by_accounts'
@ -41,12 +43,12 @@ const Hashtag: React.FC<IHashtag> = ({ hashtag }) => {
)}
</Stack>
{hashtag.get('history') && (
{history && (
<div className='w-[40px]' data-testid='sparklines'>
<Sparklines
width={40}
height={28}
data={hashtag.get('history').reverse().map((day: ImmutableMap<string, any>) => day.get('uses')).toArray()}
data={history.reverse().map((day) => +day.uses).toArray()}
>
<SparklinesCurve style={{ fill: 'none' }} color={brandColor} />
</Sparklines>

Wyświetl plik

@ -3,7 +3,7 @@ import React from 'react';
import PullToRefresh from './pull-to-refresh';
interface IPullable {
children: JSX.Element,
children: React.ReactNode,
}
/**

Wyświetl plik

@ -7,7 +7,7 @@ import { useSoapboxConfig } from 'soapbox/hooks';
import { Card, CardBody, CardHeader, CardTitle } from '../card/card';
interface IColumn {
export interface IColumn {
/** Route the back button goes to. */
backHref?: string,
/** Column title text. */

Wyświetl plik

@ -10,7 +10,7 @@ import type { DropdownPlacement, IDropdown } from 'soapbox/components/dropdown_m
import type { RootState } from 'soapbox/store';
const mapStateToProps = (state: RootState) => ({
isModalOpen: Boolean(state.modals.size && state.modals.last().modalType === 'ACTIONS'),
isModalOpen: Boolean(state.modals.size && state.modals.last()!.modalType === 'ACTIONS'),
dropdownPlacement: state.dropdown_menu.placement,
openDropdownId: state.dropdown_menu.openId,
openedViaKeyboard: state.dropdown_menu.keyboard,

Wyświetl plik

@ -50,8 +50,8 @@ const Search = (props: ISearch) => {
const history = useHistory();
const intl = useIntl();
const value = useAppSelector((state) => state.search.get('value'));
const submitted = useAppSelector((state) => state.search.get('submitted'));
const value = useAppSelector((state) => state.search.value);
const submitted = useAppSelector((state) => state.search.submitted);
const debouncedSubmit = debounce(() => {
dispatch(submitSearch());

Wyświetl plik

@ -1,173 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import Hashtag from 'soapbox/components/hashtag';
import ScrollableList from 'soapbox/components/scrollable_list';
import { Tabs } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import StatusContainer from 'soapbox/containers/status_container';
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account';
import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder_hashtag';
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
const messages = defineMessages({
accounts: { id: 'search_results.accounts', defaultMessage: 'People' },
statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' },
hashtags: { id: 'search_results.hashtags', defaultMessage: 'Hashtags' },
});
export default @injectIntl
class SearchResults extends ImmutablePureComponent {
static propTypes = {
value: PropTypes.string,
results: ImmutablePropTypes.map.isRequired,
submitted: PropTypes.bool,
expandSearch: PropTypes.func.isRequired,
selectedFilter: PropTypes.string.isRequired,
selectFilter: PropTypes.func.isRequired,
features: PropTypes.object.isRequired,
suggestions: ImmutablePropTypes.list,
trendingStatuses: ImmutablePropTypes.list,
trends: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
};
handleLoadMore = () => this.props.expandSearch(this.props.selectedFilter);
handleSelectFilter = newActiveFilter => this.props.selectFilter(newActiveFilter);
componentDidMount() {
this.props.fetchTrendingStatuses();
}
renderFilterBar() {
const { intl, selectedFilter } = this.props;
const items = [
{
text: intl.formatMessage(messages.accounts),
action: () => this.handleSelectFilter('accounts'),
name: 'accounts',
},
{
text: intl.formatMessage(messages.statuses),
action: () => this.handleSelectFilter('statuses'),
name: 'statuses',
},
{
text: intl.formatMessage(messages.hashtags),
action: () => this.handleSelectFilter('hashtags'),
name: 'hashtags',
},
];
return <Tabs items={items} activeItem={selectedFilter} />;
}
render() {
const { value, results, submitted, selectedFilter, suggestions, trendingStatuses, trends } = this.props;
let searchResults;
let hasMore = false;
let loaded;
let noResultsMessage;
let placeholderComponent = PlaceholderStatus;
if (selectedFilter === 'accounts') {
hasMore = results.get('accountsHasMore');
loaded = results.get('accountsLoaded');
placeholderComponent = PlaceholderAccount;
if (results.get('accounts') && results.get('accounts').size > 0) {
searchResults = results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />);
} else if (!submitted && suggestions && !suggestions.isEmpty()) {
searchResults = suggestions.map(suggestion => <AccountContainer key={suggestion.get('account')} id={suggestion.get('account')} />);
} else if (loaded) {
noResultsMessage = (
<div className='empty-column-indicator'>
<FormattedMessage
id='empty_column.search.accounts'
defaultMessage='There are no people results for "{term}"'
values={{ term: value }}
/>
</div>
);
}
}
if (selectedFilter === 'statuses') {
hasMore = results.get('statusesHasMore');
loaded = results.get('statusesLoaded');
if (results.get('statuses') && results.get('statuses').size > 0) {
searchResults = results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />);
} else if (!submitted && trendingStatuses && !trendingStatuses.isEmpty()) {
searchResults = trendingStatuses.map(statusId => <StatusContainer key={statusId} id={statusId} />);
} else if (loaded) {
noResultsMessage = (
<div className='empty-column-indicator'>
<FormattedMessage
id='empty_column.search.statuses'
defaultMessage='There are no posts results for "{term}"'
values={{ term: value }}
/>
</div>
);
}
}
if (selectedFilter === 'hashtags') {
hasMore = results.get('hashtagsHasMore');
loaded = results.get('hashtagsLoaded');
placeholderComponent = PlaceholderHashtag;
if (results.get('hashtags') && results.get('hashtags').size > 0) {
searchResults = results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />);
} else if (!submitted && suggestions && !suggestions.isEmpty()) {
searchResults = trends.map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />);
} else if (loaded) {
noResultsMessage = (
<div className='empty-column-indicator'>
<FormattedMessage
id='empty_column.search.hashtags'
defaultMessage='There are no hashtags results for "{term}"'
values={{ term: value }}
/>
</div>
);
}
}
return (
<>
{this.renderFilterBar()}
{noResultsMessage || (
<ScrollableList
key={selectedFilter}
scrollKey={`${selectedFilter}:${value}`}
isLoading={submitted && !loaded}
showLoading={submitted && !loaded && results.isEmpty()}
hasMore={hasMore}
onLoadMore={this.handleLoadMore}
placeholderComponent={placeholderComponent}
placeholderCount={20}
className={classNames({
'divide-gray-200 dark:divide-slate-700 divide-solid divide-y': selectedFilter === 'statuses',
})}
itemClassName={classNames({ 'pb-4': selectedFilter === 'accounts' })}
>
{searchResults}
</ScrollableList>
)}
</>
);
}
}

Wyświetl plik

@ -0,0 +1,172 @@
import classNames from 'classnames';
import React from 'react';
import { useEffect } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { defineMessages } from 'react-intl';
import { expandSearch, setFilter } from 'soapbox/actions/search';
import { fetchTrendingStatuses } from 'soapbox/actions/trending_statuses';
import Hashtag from 'soapbox/components/hashtag';
import ScrollableList from 'soapbox/components/scrollable_list';
import { Tabs } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import StatusContainer from 'soapbox/containers/status_container';
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account';
import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder_hashtag';
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import type { SearchFilter } from 'soapbox/reducers/search';
const messages = defineMessages({
accounts: { id: 'search_results.accounts', defaultMessage: 'People' },
statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' },
hashtags: { id: 'search_results.hashtags', defaultMessage: 'Hashtags' },
});
const SearchResults = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const value = useAppSelector((state) => state.search.submittedValue);
const results = useAppSelector((state) => state.search.results);
const suggestions = useAppSelector((state) => state.suggestions.items);
const trendingStatuses = useAppSelector((state) => state.trending_statuses.items);
const trends = useAppSelector((state) => state.trends.items);
const submitted = useAppSelector((state) => state.search.submitted);
const selectedFilter = useAppSelector((state) => state.search.filter);
const handleLoadMore = () => dispatch(expandSearch(selectedFilter));
const selectFilter = (newActiveFilter: SearchFilter) => dispatch(setFilter(newActiveFilter));
const renderFilterBar = () => {
const items = [
{
text: intl.formatMessage(messages.accounts),
action: () => selectFilter('accounts'),
name: 'accounts',
},
{
text: intl.formatMessage(messages.statuses),
action: () => selectFilter('statuses'),
name: 'statuses',
},
{
text: intl.formatMessage(messages.hashtags),
action: () => selectFilter('hashtags'),
name: 'hashtags',
},
];
return <Tabs items={items} activeItem={selectedFilter} />;
};
useEffect(() => {
dispatch(fetchTrendingStatuses());
}, []);
let searchResults;
let hasMore = false;
let loaded;
let noResultsMessage;
let placeholderComponent = PlaceholderStatus as React.ComponentType;
if (selectedFilter === 'accounts') {
hasMore = results.accountsHasMore;
loaded = results.accountsLoaded;
placeholderComponent = PlaceholderAccount;
if (results.accounts && results.accounts.size > 0) {
searchResults = results.accounts.map(accountId => <AccountContainer key={accountId} id={accountId} />);
} else if (!submitted && suggestions && !suggestions.isEmpty()) {
searchResults = suggestions.map(suggestion => <AccountContainer key={suggestion.account} id={suggestion.account} />);
} else if (loaded) {
noResultsMessage = (
<div className='empty-column-indicator'>
<FormattedMessage
id='empty_column.search.accounts'
defaultMessage='There are no people results for "{term}"'
values={{ term: value }}
/>
</div>
);
}
}
if (selectedFilter === 'statuses') {
hasMore = results.statusesHasMore;
loaded = results.statusesLoaded;
if (results.statuses && results.statuses.size > 0) {
searchResults = results.statuses.map((statusId: string) => (
// @ts-ignore
<StatusContainer key={statusId} id={statusId} />
));
} else if (!submitted && trendingStatuses && !trendingStatuses.isEmpty()) {
searchResults = trendingStatuses.map((statusId: string) => (
// @ts-ignore
<StatusContainer key={statusId} id={statusId} />
));
} else if (loaded) {
noResultsMessage = (
<div className='empty-column-indicator'>
<FormattedMessage
id='empty_column.search.statuses'
defaultMessage='There are no posts results for "{term}"'
values={{ term: value }}
/>
</div>
);
}
}
if (selectedFilter === 'hashtags') {
hasMore = results.hashtagsHasMore;
loaded = results.hashtagsLoaded;
placeholderComponent = PlaceholderHashtag;
if (results.hashtags && results.hashtags.size > 0) {
searchResults = results.hashtags.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
} else if (!submitted && suggestions && !suggestions.isEmpty()) {
searchResults = trends.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
} else if (loaded) {
noResultsMessage = (
<div className='empty-column-indicator'>
<FormattedMessage
id='empty_column.search.hashtags'
defaultMessage='There are no hashtags results for "{term}"'
values={{ term: value }}
/>
</div>
);
}
}
return (
<>
{renderFilterBar()}
{noResultsMessage || (
<ScrollableList
key={selectedFilter}
scrollKey={`${selectedFilter}:${value}`}
isLoading={submitted && !loaded}
showLoading={submitted && !loaded && searchResults?.isEmpty()}
hasMore={hasMore}
onLoadMore={handleLoadMore}
placeholderComponent={placeholderComponent}
placeholderCount={20}
className={classNames({
'divide-gray-200 dark:divide-slate-700 divide-solid divide-y': selectedFilter === 'statuses',
})}
itemClassName={classNames({ 'pb-4': selectedFilter === 'accounts' })}
>
{searchResults || []}
</ScrollableList>
)}
</>
);
};
export default SearchResults;

Wyświetl plik

@ -1,33 +0,0 @@
import { connect } from 'react-redux';
import { expandSearch, setFilter } from 'soapbox/actions/search';
import { fetchSuggestions, dismissSuggestion } from 'soapbox/actions/suggestions';
import { fetchTrendingStatuses } from 'soapbox/actions/trending_statuses';
import { getFeatures } from 'soapbox/utils/features';
import SearchResults from '../components/search_results';
const mapStateToProps = state => {
const instance = state.get('instance');
return {
value: state.getIn(['search', 'submittedValue']),
results: state.getIn(['search', 'results']),
suggestions: state.getIn(['suggestions', 'items']),
trendingStatuses: state.getIn(['trending_statuses', 'items']),
trends: state.getIn(['trends', 'items']),
submitted: state.getIn(['search', 'submitted']),
selectedFilter: state.getIn(['search', 'filter']),
features: getFeatures(instance),
};
};
const mapDispatchToProps = dispatch => ({
fetchSuggestions: () => dispatch(fetchSuggestions()),
fetchTrendingStatuses: () => dispatch(fetchTrendingStatuses()),
expandSearch: type => dispatch(expandSearch(type)),
dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))),
selectFilter: newActiveFilter => dispatch(setFilter(newActiveFilter)),
});
export default connect(mapStateToProps, mapDispatchToProps)(SearchResults);

Wyświetl plik

@ -1,4 +1,3 @@
import { Set as ImmutableSet } from 'immutable';
import { connect } from 'react-redux';
import { toggleStatusReport } from '../../../actions/reports';
@ -6,7 +5,7 @@ import StatusCheckBox from '../components/status_check_box';
const mapStateToProps = (state, { id }) => ({
status: state.getIn(['statuses', id]),
checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id),
checked: state.reports.new.status_ids.includes(id),
});
const mapDispatchToProps = (dispatch, { id }) => ({

Wyświetl plik

@ -1,60 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { NavLink } from 'react-router-dom';
const mapStateToProps = state => ({
value: state.getIn(['search', 'value']),
submitted: state.getIn(['search', 'submitted']),
});
class Header extends ImmutablePureComponent {
static propTypes = {
value: PropTypes.string,
submitted: PropTypes.bool,
};
state = {
submittedValue: '',
};
componentDidUpdate(prevProps) {
if (this.props.submitted) {
const submittedValue = this.props.value;
this.setState({ submittedValue });
}
}
render() {
const { submittedValue } = this.state;
if (!submittedValue) {
return null;
}
return (
<div className='search-header'>
<div className='search-header__text-container'>
<h1 className='search-header__title-text'>
{submittedValue}
</h1>
</div>
<div className='search-header__type-filters'>
<div className='account__section-headline'>
<div className='search-header__type-filters-tabs'>
<NavLink to='/search' activeClassName='active'>
<FormattedMessage id='search_results.top' defaultMessage='Top' />
</NavLink>
</div>
</div>
</div>
</div>
);
}
}
export default connect(mapStateToProps)(Header);

Wyświetl plik

@ -3,7 +3,7 @@ import { defineMessages, useIntl } from 'react-intl';
import { Column } from 'soapbox/components/ui';
import Search from 'soapbox/features/compose/components/search';
import SearchResultsContainer from 'soapbox/features/compose/containers/search_results_container';
import SearchResults from 'soapbox/features/compose/components/search_results';
const messages = defineMessages({
heading: { id: 'column.search', defaultMessage: 'Search' },
@ -16,7 +16,7 @@ const SearchPage = () => {
<Column label={intl.formatMessage(messages.heading)}>
<div className='space-y-4'>
<Search autoFocus autoSubmit />
<SearchResultsContainer />
<SearchResults />
</div>
</Column>
);

Wyświetl plik

@ -1,4 +1,4 @@
import { Map as ImmutableMap, fromJS } from 'immutable';
import { List as ImmutableList } from 'immutable';
import React from 'react';
import { render, screen } from '../../../../jest/test-helpers';
@ -7,16 +7,16 @@ import TrendsPanel from '../trends-panel';
describe('<TrendsPanel />', () => {
it('renders trending hashtags', () => {
const store = {
trends: ImmutableMap({
items: fromJS([{
trends: {
items: ImmutableList([{
name: 'hashtag 1',
history: [{
history: ImmutableList([{
day: '1652745600',
uses: '294',
accounts: '180',
}],
}]),
}]),
}),
},
};
render(<TrendsPanel limit={1} />, null, store);
@ -27,18 +27,18 @@ describe('<TrendsPanel />', () => {
it('renders multiple trends', () => {
const store = {
trends: ImmutableMap({
items: fromJS([
trends: {
items: ImmutableList([
{
name: 'hashtag 1',
history: [{ accounts: [] }],
history: ImmutableList([{ accounts: [] }]),
},
{
name: 'hashtag 2',
history: [{ accounts: [] }],
history: ImmutableList([{ accounts: [] }]),
},
]),
}),
},
};
render(<TrendsPanel limit={3} />, null, store);
@ -47,18 +47,18 @@ describe('<TrendsPanel />', () => {
it('respects the limit prop', () => {
const store = {
trends: ImmutableMap({
items: fromJS([
trends: {
items: ImmutableList([
{
name: 'hashtag 1',
history: [{ accounts: [] }],
history: ImmutableList([{ accounts: [] }]),
},
{
name: 'hashtag 2',
history: [{ accounts: [] }],
history: ImmutableList([{ accounts: [] }]),
},
]),
}),
},
};
render(<TrendsPanel limit={1} />, null, store);
@ -67,9 +67,9 @@ describe('<TrendsPanel />', () => {
it('renders empty', () => {
const store = {
trends: ImmutableMap({
items: fromJS([]),
}),
trends: {
items: ImmutableList([]),
},
};
render(<TrendsPanel limit={1} />, null, store);

Wyświetl plik

@ -1,101 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { changeAccountNoteComment, submitAccountNote } from 'soapbox/actions/account-notes';
import { closeModal } from 'soapbox/actions/modals';
import { Modal, Text } from 'soapbox/components/ui';
import { makeGetAccount } from 'soapbox/selectors';
const messages = defineMessages({
placeholder: { id: 'account_note.placeholder', defaultMessage: 'No comment provided' },
save: { id: 'account_note.save', defaultMessage: 'Save' },
});
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = state => ({
isSubmitting: state.getIn(['account_notes', 'edit', 'isSubmitting']),
account: getAccount(state, state.getIn(['account_notes', 'edit', 'account_id'])),
comment: state.getIn(['account_notes', 'edit', 'comment']),
});
return mapStateToProps;
};
const mapDispatchToProps = dispatch => {
return {
onConfirm() {
dispatch(submitAccountNote());
},
onClose() {
dispatch(closeModal());
},
onCommentChange(comment) {
dispatch(changeAccountNoteComment(comment));
},
};
};
export default @connect(makeMapStateToProps, mapDispatchToProps)
@injectIntl
class AccountNoteModal extends React.PureComponent {
static propTypes = {
isSubmitting: PropTypes.bool,
account: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
onCommentChange: PropTypes.func.isRequired,
comment: PropTypes.string,
intl: PropTypes.object.isRequired,
};
handleCommentChange = e => {
this.props.onCommentChange(e.target.value);
}
handleSubmit = () => {
this.props.onConfirm();
}
handleKeyDown = e => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
this.handleSubmit();
}
}
render() {
const { account, isSubmitting, comment, onClose, intl } = this.props;
return (
<Modal
title={<FormattedMessage id='account_note.target' defaultMessage='Note for @{target}' values={{ target: account.get('acct') }} />}
onClose={onClose}
confirmationAction={this.handleSubmit}
confirmationText={intl.formatMessage(messages.save)}
confirmationDisabled={isSubmitting}
>
<Text theme='muted'>
<FormattedMessage id='account_note.hint' defaultMessage='You can keep notes about this user for yourself (this will not be shared with them):' />
</Text>
<textarea
className='setting-text light'
placeholder={intl.formatMessage(messages.placeholder)}
value={comment}
onChange={this.handleCommentChange}
onKeyDown={this.handleKeyDown}
disabled={isSubmitting}
autoFocus
/>
</Modal>
);
}
}

Wyświetl plik

@ -0,0 +1,68 @@
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { changeAccountNoteComment, submitAccountNote } from 'soapbox/actions/account-notes';
import { closeModal } from 'soapbox/actions/modals';
import { Modal, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
const messages = defineMessages({
placeholder: { id: 'account_note.placeholder', defaultMessage: 'No comment provided' },
save: { id: 'account_note.save', defaultMessage: 'Save' },
});
const getAccount = makeGetAccount();
const AccountNoteModal = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const isSubmitting = useAppSelector((state) => state.account_notes.edit.isSubmitting);
const account = useAppSelector((state) => getAccount(state, state.account_notes.edit.account!));
const comment = useAppSelector((state) => state.account_notes.edit.comment);
const onClose = () => {
dispatch(closeModal('ACCOUNT_NOTE'));
};
const handleCommentChange: React.ChangeEventHandler<HTMLTextAreaElement> = e => {
dispatch(changeAccountNoteComment(e.target.value));
};
const handleSubmit = () => {
dispatch(submitAccountNote());
};
const handleKeyDown: React.KeyboardEventHandler<HTMLTextAreaElement> = e => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
handleSubmit();
}
};
return (
<Modal
title={<FormattedMessage id='account_note.target' defaultMessage='Note for @{target}' values={{ target: account!.acct }} />}
onClose={onClose}
confirmationAction={handleSubmit}
confirmationText={intl.formatMessage(messages.save)}
confirmationDisabled={isSubmitting}
>
<Text theme='muted'>
<FormattedMessage id='account_note.hint' defaultMessage='You can keep notes about this user for yourself (this will not be shared with them):' />
</Text>
<textarea
className='setting-text light'
placeholder={intl.formatMessage(messages.placeholder)}
value={comment}
onChange={handleCommentChange}
onKeyDown={handleKeyDown}
disabled={isSubmitting}
autoFocus
/>
</Modal>
);
};
export default AccountNoteModal;

Wyświetl plik

@ -1,18 +1,27 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import spring from 'react-motion/lib/spring';
import { spring } from 'react-motion';
import Icon from 'soapbox/components/icon';
import StatusContent from 'soapbox/components/status_content';
import { Stack } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import Icon from '../../../components/icon';
import StatusContent from '../../../components/status_content';
import { Stack } from '../../../components/ui';
import AccountContainer from '../../../containers/account_container';
import Motion from '../util/optional_motion';
const ActionsModal = ({ status, actions, onClick, onClose }) => {
const renderAction = (action, i) => {
import type { Menu, MenuItem } from 'soapbox/components/dropdown_menu';
import type { Status as StatusEntity } from 'soapbox/types/entities';
interface IActionsModal {
status: StatusEntity,
actions: Menu,
onClick: () => void,
onClose: () => void,
}
const ActionsModal: React.FC<IActionsModal> = ({ status, actions, onClick, onClose }) => {
const renderAction = (action: MenuItem | null, i: number) => {
if (action === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
@ -48,9 +57,10 @@ const ActionsModal = ({ status, actions, onClick, onClose }) => {
{status && (
<Stack space={2} className='p-4 bg-gray-50 dark:bg-slate-800 border-b border-solid border-gray-200 dark:border-gray-700'>
<AccountContainer
account={status.get('account')}
key={status.account as string}
id={status.account as string}
showProfileHoverCard={false}
timestamp={status.get('created_at')}
timestamp={status.created_at}
/>
<StatusContent status={status} />
</Stack>
@ -73,11 +83,4 @@ const ActionsModal = ({ status, actions, onClick, onClose }) => {
);
};
ActionsModal.propTypes = {
status: ImmutablePropTypes.record,
actions: PropTypes.array,
onClick: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
export default ActionsModal;

Wyświetl plik

@ -1,37 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Pullable from 'soapbox/components/pullable';
import { Column } from 'soapbox/components/ui';
import ColumnHeader from './column_header';
export default class UIColumn extends React.PureComponent {
static propTypes = {
heading: PropTypes.string,
icon: PropTypes.string,
children: PropTypes.node,
active: PropTypes.bool,
showBackBtn: PropTypes.bool,
};
static defaultProps = {
showBackBtn: true,
}
render() {
const { heading, icon, children, active, showBackBtn, ...rest } = this.props;
const columnHeaderId = heading && heading.replace(/ /g, '-');
return (
<Column aria-labelledby={columnHeaderId} {...rest}>
{heading && <ColumnHeader icon={icon} active={active} type={heading} columnHeaderId={columnHeaderId} showBackBtn={showBackBtn} />}
<Pullable>
{children}
</Pullable>
</Column>
);
}
}

Wyświetl plik

@ -0,0 +1,36 @@
import React from 'react';
import Pullable from 'soapbox/components/pullable';
import { Column } from 'soapbox/components/ui';
import ColumnHeader from './column_header';
import type { IColumn } from 'soapbox/components/ui/column/column';
interface IUIColumn extends IColumn {
heading?: string,
icon?: string,
active?: boolean,
}
const UIColumn: React.FC<IUIColumn> = ({
heading,
icon,
children,
active,
...rest
}) => {
const columnHeaderId = heading && heading.replace(/ /g, '-');
return (
<Column aria-labelledby={columnHeaderId} {...rest}>
{heading && <ColumnHeader icon={icon} active={active} type={heading} columnHeaderId={columnHeaderId} />}
<Pullable>
{children}
</Pullable>
</Column>
);
};
export default UIColumn;

Wyświetl plik

@ -1,32 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import Column from './column';
const messages = defineMessages({
title: { id: 'column_forbidden.title', defaultMessage: 'Forbidden' },
body: { id: 'column_forbidden.body', defaultMessage: 'You do not have permission to access this page.' },
});
class ColumnForbidden extends React.PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
}
render() {
const { intl: { formatMessage } } = this.props;
return (
<Column label={formatMessage(messages.title)}>
<div className='error-column'>
{formatMessage(messages.body)}
</div>
</Column>
);
}
}
export default injectIntl(ColumnForbidden);

Wyświetl plik

@ -0,0 +1,23 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import Column from './column';
const messages = defineMessages({
title: { id: 'column_forbidden.title', defaultMessage: 'Forbidden' },
body: { id: 'column_forbidden.body', defaultMessage: 'You do not have permission to access this page.' },
});
const ColumnForbidden = () => {
const intl = useIntl();
return (
<Column label={intl.formatMessage(messages.title)}>
<div className='error-column'>
{intl.formatMessage(messages.body)}
</div>
</Column>
);
};
export default ColumnForbidden;

Wyświetl plik

@ -1,40 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
// import classNames from 'classnames';
// import Icon from 'soapbox/components/icon';
import SubNavigation from 'soapbox/components/sub_navigation';
export default class ColumnHeader extends React.PureComponent {
static propTypes = {
icon: PropTypes.string,
type: PropTypes.string,
active: PropTypes.bool,
onClick: PropTypes.func,
columnHeaderId: PropTypes.string,
};
handleClick = () => {
this.props.onClick();
}
render() {
const { type } = this.props;
return <SubNavigation message={type} />;
}
// render() {
// const { icon, type, active, columnHeaderId } = this.props;
//
// return (
// <h1 className={classNames('column-header', { active })} id={columnHeaderId || null}>
// <button onClick={this.handleClick}>
// {icon && <Icon id={icon} fixedWidth className='column-header__icon' />}
// {type}
// </button>
// </h1>
// );
// }
}

Wyświetl plik

@ -0,0 +1,47 @@
import React from 'react';
// import classNames from 'classnames';
// import Icon from 'soapbox/components/icon';
import SubNavigation from 'soapbox/components/sub_navigation';
interface IColumnHeader {
icon?: string,
type: string
active?: boolean,
columnHeaderId?: string,
}
const ColumnHeader: React.FC<IColumnHeader> = ({ type }) => {
return <SubNavigation message={type} />;
};
export default ColumnHeader;
// export default class ColumnHeader extends React.PureComponent {
// static propTypes = {
// icon: PropTypes.string,
// type: PropTypes.string,
// active: PropTypes.bool,
// onClick: PropTypes.func,
// columnHeaderId: PropTypes.string,
// };
// handleClick = () => {
// this.props.onClick();
// }
// render() {
// const { icon, type, active, columnHeaderId } = this.props;
// return (
// <h1 className={classNames('column-header', { active })} id={columnHeaderId || null}>
// <button onClick={this.handleClick}>
// {icon && <Icon id={icon} fixedWidth className='column-header__icon' />}
// {type}
// </button>
// </h1>
// );
// }
// }

Wyświetl plik

@ -77,14 +77,14 @@ const ReportModal = ({ onClose }: IReportModal) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const accountId = useAppSelector((state) => state.reports.getIn(['new', 'account_id']) as string);
const account = useAccount(accountId);
const accountId = useAppSelector((state) => state.reports.new.account_id);
const account = useAccount(accountId as string);
const isBlocked = useAppSelector((state) => state.reports.getIn(['new', 'block']) as boolean);
const isSubmitting = useAppSelector((state) => state.reports.getIn(['new', 'isSubmitting']) as boolean);
const isBlocked = useAppSelector((state) => state.reports.new.block);
const isSubmitting = useAppSelector((state) => state.reports.new.isSubmitting);
const rules = useAppSelector((state) => state.rules.items);
const ruleIds = useAppSelector((state) => state.reports.getIn(['new', 'rule_ids']) as ImmutableSet<string>);
const selectedStatusIds = useAppSelector((state) => state.reports.getIn(['new', 'status_ids']) as ImmutableSet<string>);
const ruleIds = useAppSelector((state) => state.reports.new.rule_ids);
const selectedStatusIds = useAppSelector((state) => state.reports.new.status_ids);
const isReportingAccount = useMemo(() => selectedStatusIds.size === 0, []);
const shouldRequireRule = rules.length > 0;

Wyświetl plik

@ -30,11 +30,11 @@ const OtherActionsStep = ({ account }: IOtherActionsStep) => {
const features = useFeatures();
const intl = useIntl();
const statusIds = useAppSelector((state) => OrderedSet(state.timelines.getIn([`account:${account.id}:with_replies`, 'items'])).union(state.reports.getIn(['new', 'status_ids']) as Iterable<unknown>) as OrderedSet<string>);
const isBlocked = useAppSelector((state) => state.reports.getIn(['new', 'block']) as boolean);
const isForward = useAppSelector((state) => state.reports.getIn(['new', 'forward']) as boolean);
const statusIds = useAppSelector((state) => OrderedSet(state.timelines.getIn([`account:${account.id}:with_replies`, 'items'])).union(state.reports.new.status_ids) as OrderedSet<string>);
const isBlocked = useAppSelector((state) => state.reports.new.block);
const isForward = useAppSelector((state) => state.reports.new.forward);
const canForward = isRemote(account as any) && features.federating;
const isSubmitting = useAppSelector((state) => state.reports.getIn(['new', 'isSubmitting']) as boolean);
const isSubmitting = useAppSelector((state) => state.reports.new.isSubmitting);
const [showAdditionalStatuses, setShowAdditionalStatuses] = useState<boolean>(false);

Wyświetl plik

@ -8,7 +8,6 @@ import { fetchRules } from 'soapbox/actions/rules';
import { FormGroup, Stack, Text, Textarea } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import type { Set as ImmutableSet } from 'immutable';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
const messages = defineMessages({
@ -31,12 +30,12 @@ const ReasonStep = (_props: IReasonStep) => {
const [isNearBottom, setNearBottom] = useState<boolean>(false);
const [isNearTop, setNearTop] = useState<boolean>(true);
const comment = useAppSelector((state) => state.reports.getIn(['new', 'comment']) as string);
const comment = useAppSelector((state) => state.reports.new.comment);
const rules = useAppSelector((state) => state.rules.items);
const ruleIds = useAppSelector((state) => state.reports.getIn(['new', 'rule_ids']) as ImmutableSet<string>);
const ruleIds = useAppSelector((state) => state.reports.new.rule_ids);
const shouldRequireRule = rules.length > 0;
const selectedStatusIds = useAppSelector((state) => state.reports.getIn(['new', 'status_ids']) as ImmutableSet<string>);
const selectedStatusIds = useAppSelector((state) => state.reports.new.status_ids);
const isReportingAccount = useMemo(() => selectedStatusIds.size === 0, []);
const handleCommentChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {

Wyświetl plik

@ -11,7 +11,7 @@ import { Modal, HStack, Stack, Text } from 'soapbox/components/ui';
const mapStateToProps = state => {
return {
isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
isSubmitting: state.reports.new.isSubmitting,
account: state.getIn(['mutes', 'new', 'account']),
notifications: state.getIn(['mutes', 'new', 'notifications']),
};

Wyświetl plik

@ -1,4 +1,3 @@
import { Map as ImmutableMap } from 'immutable';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
@ -15,10 +14,10 @@ interface ITrendsPanel {
const TrendsPanel = ({ limit }: ITrendsPanel) => {
const dispatch = useDispatch();
const trends: any = useAppSelector((state) => state.trends.get('items'));
const trends = useAppSelector((state) => state.trends.get('items'));
const sortedTrends = React.useMemo(() => {
return trends.sort((a: ImmutableMap<string, any>, b: ImmutableMap<string, any>) => {
return trends.sort((a, b) => {
const num_a = Number(a.getIn(['history', 0, 'accounts']));
const num_b = Number(b.getIn(['history', 0, 'accounts']));
return num_b - num_a;
@ -35,8 +34,8 @@ const TrendsPanel = ({ limit }: ITrendsPanel) => {
return (
<Widget title={<FormattedMessage id='trends.title' defaultMessage='Trends' />}>
{sortedTrends.map((hashtag: ImmutableMap<string, any>) => (
<Hashtag key={hashtag.get('name')} hashtag={hashtag} />
{sortedTrends.map((hashtag) => (
<Hashtag key={hashtag.name} hashtag={hashtag} />
))}
</Widget>
);

Wyświetl plik

@ -16,10 +16,10 @@ describe('modal reducer', () => {
modalType: 'type1',
modalProps: { props1: '1' },
};
expect(reducer(state, action)).toMatchObject(ImmutableList([{
expect(reducer(state, action).toJS()).toMatchObject([{
modalType: 'type1',
modalProps: { props1: '1' },
}]));
}]);
});
it('should handle MODAL_CLOSE', () => {

Wyświetl plik

@ -1,19 +1,17 @@
import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
import reducer from '../reports';
describe('reports reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap({
new: ImmutableMap({
expect(reducer(undefined, {}).toJS()).toEqual({
new: {
isSubmitting: false,
account_id: null,
status_ids: ImmutableSet(),
status_ids: [],
comment: '',
forward: false,
block: false,
rule_ids: ImmutableSet(),
}),
}));
rule_ids: [],
},
});
});
});

Wyświetl plik

@ -1,12 +1,10 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import reducer from '../trends';
describe('trends reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap({
items: ImmutableList(),
expect(reducer(undefined, {}).toJS()).toEqual({
items: [],
isLoading: false,
}));
});
});
});

Wyświetl plik

@ -12,7 +12,7 @@ import {
const EditRecord = ImmutableRecord({
isSubmitting: false,
account: null,
comment: null,
comment: '',
});
const ReducerRecord = ImmutableRecord({
@ -26,7 +26,7 @@ export default function account_notes(state: State = ReducerRecord(), action: An
case ACCOUNT_NOTE_INIT_MODAL:
return state.withMutations((state) => {
state.setIn(['edit', 'isSubmitting'], false);
state.setIn(['edit', 'account_id'], action.account.get('id'));
state.setIn(['edit', 'account'], action.account.get('id'));
state.setIn(['edit', 'comment'], action.comment);
});
case ACCOUNT_NOTE_CHANGE_COMMENT:

Wyświetl plik

@ -1,13 +1,22 @@
import { List as ImmutableList } from 'immutable';
import { List as ImmutableList, Record as ImmutableRecord } from 'immutable';
import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modals';
const initialState = ImmutableList();
import type { AnyAction } from 'redux';
export default function modal(state = initialState, action) {
const ModalRecord = ImmutableRecord({
modalType: '',
modalProps: null as Record<string, any> | null,
});
type Modal = ReturnType<typeof ModalRecord>;
type State = ImmutableList<Modal>;
export default function modal(state: State = ImmutableList<Modal>(), action: AnyAction) {
switch (action.type) {
case MODAL_OPEN:
return state.push({ modalType: action.modalType, modalProps: action.modalProps });
return state.push(ModalRecord({ modalType: action.modalType, modalProps: action.modalProps }));
case MODAL_CLOSE:
if (state.size === 0) {
return state;

Wyświetl plik

@ -1,4 +1,4 @@
import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
import { Record as ImmutableRecord, Set as ImmutableSet } from 'immutable';
import {
REPORT_INIT,
@ -13,39 +13,45 @@ import {
REPORT_RULE_CHANGE,
} from '../actions/reports';
const initialState = ImmutableMap({
new: ImmutableMap({
isSubmitting: false,
account_id: null,
status_ids: ImmutableSet(),
comment: '',
forward: false,
block: false,
rule_ids: ImmutableSet(),
}),
import type { AnyAction } from 'redux';
const NewReportRecord = ImmutableRecord({
isSubmitting: false,
account_id: null as string | null,
status_ids: ImmutableSet<string>(),
comment: '',
forward: false,
block: false,
rule_ids: ImmutableSet<string>(),
});
export default function reports(state = initialState, action) {
const ReducerRecord = ImmutableRecord({
new: NewReportRecord(),
});
type State = ReturnType<typeof ReducerRecord>;
export default function reports(state: State = ReducerRecord(), action: AnyAction) {
switch (action.type) {
case REPORT_INIT:
return state.withMutations(map => {
map.setIn(['new', 'isSubmitting'], false);
map.setIn(['new', 'account_id'], action.account.get('id'));
map.setIn(['new', 'account_id'], action.account.id);
if (state.getIn(['new', 'account_id']) !== action.account.get('id')) {
map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : ImmutableSet());
if (state.new.account_id !== action.account.id) {
map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.reblog?.id || action.status.id]) : ImmutableSet());
map.setIn(['new', 'comment'], '');
} else if (action.status) {
map.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id'))));
map.updateIn(['new', 'status_ids'], set => (set as ImmutableSet<string>).add(action.status.reblog?.id || action.status.id));
}
});
case REPORT_STATUS_TOGGLE:
return state.updateIn(['new', 'status_ids'], ImmutableSet(), set => {
return state.updateIn(['new', 'status_ids'], set => {
if (action.checked) {
return set.add(action.statusId);
return (set as ImmutableSet<string>).add(action.statusId);
}
return set.remove(action.statusId);
return (set as ImmutableSet<string>).remove(action.statusId);
});
case REPORT_COMMENT_CHANGE:
return state.setIn(['new', 'comment'], action.comment);
@ -54,12 +60,12 @@ export default function reports(state = initialState, action) {
case REPORT_BLOCK_CHANGE:
return state.setIn(['new', 'block'], action.block);
case REPORT_RULE_CHANGE:
return state.updateIn(['new', 'rule_ids'], ImmutableSet(), (set) => {
if (set.includes(action.rule_id)) {
return set.remove(action.rule_id);
return state.updateIn(['new', 'rule_ids'], (set) => {
if ((set as ImmutableSet<string>).includes(action.rule_id)) {
return (set as ImmutableSet<string>).remove(action.rule_id);
}
return set.add(action.rule_id);
return (set as ImmutableSet<string>).add(action.rule_id);
});
case REPORT_SUBMIT_REQUEST:
return state.setIn(['new', 'isSubmitting'], true);

Wyświetl plik

@ -1,4 +1,6 @@
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import { OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord, fromJS } from 'immutable';
import { APIEntity } from 'soapbox/types/entities';
import {
COMPOSE_MENTION,
@ -17,26 +19,50 @@ import {
SEARCH_EXPAND_SUCCESS,
} from '../actions/search';
const initialState = ImmutableMap({
import type { AnyAction } from 'redux';
const HashtagRecord = ImmutableRecord({
name: '',
url: '',
});
const ResultsRecord = ImmutableRecord({
accounts: ImmutableOrderedSet<string>(),
statuses: ImmutableOrderedSet<string>(),
hashtags: ImmutableOrderedSet<Hashtag>(), // it's a list of maps
accountsHasMore: false,
statusesHasMore: false,
hashtagsHasMore: false,
accountsLoaded: false,
statusesLoaded: false,
hashtagsLoaded: false,
});
const ReducerRecord = ImmutableRecord({
value: '',
submitted: false,
submittedValue: '',
hidden: false,
results: ImmutableMap(),
filter: 'accounts',
results: ResultsRecord(),
filter: 'accounts' as SearchFilter,
});
const toIds = items => {
type State = ReturnType<typeof ReducerRecord>;
type APIEntities = Array<APIEntity>;
export type Hashtag = ReturnType<typeof HashtagRecord>;
export type SearchFilter = 'accounts' | 'statuses' | 'hashtags';
const toIds = (items: APIEntities) => {
return ImmutableOrderedSet(items.map(item => item.id));
};
const importResults = (state, results, searchTerm, searchType) => {
const importResults = (state: State, results: APIEntity, searchTerm: string, searchType: SearchFilter) => {
return state.withMutations(state => {
if (state.get('value') === searchTerm && state.get('filter') === searchType) {
state.set('results', ImmutableMap({
if (state.value === searchTerm && state.filter === searchType) {
state.set('results', ResultsRecord({
accounts: toIds(results.accounts),
statuses: toIds(results.statuses),
hashtags: fromJS(results.hashtags), // it's a list of maps
hashtags: ImmutableOrderedSet(results.hashtags.map(HashtagRecord)), // it's a list of maps
accountsHasMore: results.accounts.length >= 20,
statusesHasMore: results.statuses.length >= 20,
hashtagsHasMore: results.hashtags.length >= 20,
@ -50,38 +76,38 @@ const importResults = (state, results, searchTerm, searchType) => {
});
};
const paginateResults = (state, searchType, results, searchTerm) => {
const paginateResults = (state: State, searchType: SearchFilter, results: APIEntity, searchTerm: string) => {
return state.withMutations(state => {
if (state.get('value') === searchTerm) {
if (state.value === searchTerm) {
state.setIn(['results', `${searchType}HasMore`], results[searchType].length >= 20);
state.setIn(['results', `${searchType}Loaded`], true);
state.updateIn(['results', searchType], items => {
const data = results[searchType];
// Hashtags are a list of maps. Others are IDs.
if (searchType === 'hashtags') {
return items.concat(fromJS(data));
return (items as ImmutableOrderedSet<string>).concat(fromJS(data));
} else {
return items.concat(toIds(data));
return (items as ImmutableOrderedSet<string>).concat(toIds(data));
}
});
}
});
};
const handleSubmitted = (state, value) => {
const handleSubmitted = (state: State, value: string) => {
return state.withMutations(state => {
state.set('results', ImmutableMap());
state.set('results', ResultsRecord());
state.set('submitted', true);
state.set('submittedValue', value);
});
};
export default function search(state = initialState, action) {
export default function search(state = ReducerRecord(), action: AnyAction) {
switch (action.type) {
case SEARCH_CHANGE:
return state.set('value', action.value);
case SEARCH_CLEAR:
return initialState;
return ReducerRecord();
case SEARCH_SHOW:
return state.set('hidden', false);
case COMPOSE_REPLY:

Wyświetl plik

@ -1,5 +1,4 @@
import { Map as ImmutableMap, List as ImmutableList, Record as ImmutableRecord, fromJS } from 'immutable';
import { AnyAction } from 'redux';
import {
MFA_FETCH_SUCCESS,
@ -11,6 +10,8 @@ import {
REVOKE_TOKEN_SUCCESS,
} from '../actions/security';
import type { AnyAction } from 'redux';
const TokenRecord = ImmutableRecord({
id: 0,
app_name: '',

Wyświetl plik

@ -1,31 +0,0 @@
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
import {
TRENDING_STATUSES_FETCH_REQUEST,
TRENDING_STATUSES_FETCH_SUCCESS,
} from 'soapbox/actions/trending_statuses';
const initialState = ImmutableMap({
items: ImmutableOrderedSet(),
isLoading: false,
});
const toIds = items => ImmutableOrderedSet(items.map(item => item.id));
const importStatuses = (state, statuses) => {
return state.withMutations(state => {
state.set('items', toIds(statuses));
state.set('isLoading', false);
});
};
export default function trending_statuses(state = initialState, action) {
switch (action.type) {
case TRENDING_STATUSES_FETCH_REQUEST:
return state.set('isLoading', true);
case TRENDING_STATUSES_FETCH_SUCCESS:
return importStatuses(state, action.statuses);
default:
return state;
}
}

Wyświetl plik

@ -0,0 +1,37 @@
import { OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord } from 'immutable';
import {
TRENDING_STATUSES_FETCH_REQUEST,
TRENDING_STATUSES_FETCH_SUCCESS,
} from 'soapbox/actions/trending_statuses';
import { APIEntity } from 'soapbox/types/entities';
import type { AnyAction } from 'redux';
const ReducerRecord = ImmutableRecord({
items: ImmutableOrderedSet<string>(),
isLoading: false,
});
type State = ReturnType<typeof ReducerRecord>;
type APIEntities = Array<APIEntity>;
const toIds = (items: APIEntities) => ImmutableOrderedSet(items.map(item => item.id));
const importStatuses = (state: State, statuses: APIEntities) => {
return state.withMutations(state => {
state.set('items', toIds(statuses));
state.set('isLoading', false);
});
};
export default function trending_statuses(state: State = ReducerRecord(), action: AnyAction) {
switch (action.type) {
case TRENDING_STATUSES_FETCH_REQUEST:
return state.set('isLoading', true);
case TRENDING_STATUSES_FETCH_SUCCESS:
return importStatuses(state, action.statuses);
default:
return state;
}
}

Wyświetl plik

@ -1,28 +0,0 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import {
TRENDS_FETCH_REQUEST,
TRENDS_FETCH_SUCCESS,
TRENDS_FETCH_FAIL,
} from '../actions/trends';
const initialState = ImmutableMap({
items: ImmutableList(),
isLoading: false,
});
export default function trendsReducer(state = initialState, action) {
switch (action.type) {
case TRENDS_FETCH_REQUEST:
return state.set('isLoading', true);
case TRENDS_FETCH_SUCCESS:
return state.withMutations(map => {
map.set('items', fromJS(action.tags.map((x => x))));
map.set('isLoading', false);
});
case TRENDS_FETCH_FAIL:
return state.set('isLoading', false);
default:
return state;
}
}

Wyświetl plik

@ -0,0 +1,47 @@
import { List as ImmutableList, Record as ImmutableRecord } from 'immutable';
import {
TRENDS_FETCH_REQUEST,
TRENDS_FETCH_SUCCESS,
TRENDS_FETCH_FAIL,
} from '../actions/trends';
import type { AnyAction } from 'redux';
import type { APIEntity } from 'soapbox/types/entities';
const HistoryRecord = ImmutableRecord({
accounts: '',
day: '',
uses: '',
});
const TrendingHashtagRecord = ImmutableRecord({
name: '',
url: '',
history: ImmutableList<History>(),
});
const ReducerRecord = ImmutableRecord({
items: ImmutableList<TrendingHashtag>(),
isLoading: false,
});
type State = ReturnType<typeof ReducerRecord>;
type History = ReturnType<typeof HistoryRecord>;
export type TrendingHashtag = ReturnType<typeof TrendingHashtagRecord>;
export default function trendsReducer(state: State = ReducerRecord(), action: AnyAction) {
switch (action.type) {
case TRENDS_FETCH_REQUEST:
return state.set('isLoading', true);
case TRENDS_FETCH_SUCCESS:
return state.withMutations(map => {
map.set('items', ImmutableList(action.tags.map((item: APIEntity) => TrendingHashtagRecord({ ...item, history: ImmutableList(item.history.map(HistoryRecord)) }))));
map.set('isLoading', false);
});
case TRENDS_FETCH_FAIL:
return state.set('isLoading', false);
default:
return state;
}
}