Merge branch 'ts' into 'develop'

Reducers: TypeScript

See merge request soapbox-pub/soapbox-fe!1498
This commit is contained in:
marcin mikołajczak 2022-06-06 19:03:34 +00:00
commit bdb958a613
62 changed files with 610 additions and 484 deletions

View File

@ -142,7 +142,7 @@ export function expandDomainBlocks() {
return (dispatch, getState) => { return (dispatch, getState) => {
if (!isLoggedIn(getState)) return; if (!isLoggedIn(getState)) return;
const url = getState().getIn(['domain_lists', 'blocks', 'next']); const url = getState().domain_lists.blocks.next;
if (!url) { if (!url) {
return; return;

View File

@ -249,7 +249,7 @@ export interface IDropdown extends RouteComponentProps {
) => void, ) => void,
onClose?: (id: number) => void, onClose?: (id: number) => void,
dropdownPlacement?: string, dropdownPlacement?: string,
openDropdownId?: number, openDropdownId?: number | null,
openedViaKeyboard?: boolean, openedViaKeyboard?: boolean,
text?: string, text?: string,
onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>, onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>,

View File

@ -92,7 +92,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
if (!account) return null; if (!account) return null;
const accountBio = { __html: account.note_emojified }; const accountBio = { __html: account.note_emojified };
const followedBy = me !== account.id && account.relationship.get('followed_by') === true; const followedBy = me !== account.id && account.relationship?.followed_by === true;
return ( return (
<div <div

View File

@ -581,7 +581,7 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
const favouriteCount = status.favourites_count; const favouriteCount = status.favourites_count;
const emojiReactCount = reduceEmoji( const emojiReactCount = reduceEmoji(
(status.getIn(['pleroma', 'emoji_reactions']) || ImmutableList()) as ImmutableList<any>, (status.pleroma.get('emoji_reactions') || ImmutableList()) as ImmutableList<any>,
favouriteCount, favouriteCount,
status.favourited, status.favourited,
allowedEmoji, allowedEmoji,

View File

@ -35,7 +35,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow(account) { onFollow(account) {
dispatch((_, getState) => { dispatch((_, getState) => {
const unfollowModal = getSettings(getState()).get('unfollowModal'); const unfollowModal = getSettings(getState()).get('unfollowModal');
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { if (account.relationship?.following || account.relationship?.requested) {
if (unfollowModal) { if (unfollowModal) {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/minus.svg'), icon: require('@tabler/icons/icons/minus.svg'),
@ -54,7 +54,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onBlock(account) { onBlock(account) {
if (account.getIn(['relationship', 'blocking'])) { if (account.relationship?.blocking) {
dispatch(unblockAccount(account.get('id'))); dispatch(unblockAccount(account.get('id')));
} else { } else {
dispatch(blockAccount(account.get('id'))); dispatch(blockAccount(account.get('id')));
@ -62,7 +62,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onMute(account) { onMute(account) {
if (account.getIn(['relationship', 'muting'])) { if (account.relationship?.muting) {
dispatch(unmuteAccount(account.get('id'))); dispatch(unmuteAccount(account.get('id')));
} else { } else {
dispatch(initMuteModal(account)); dispatch(initMuteModal(account));

View File

@ -11,9 +11,9 @@ import type { RootState } from 'soapbox/store';
const mapStateToProps = (state: RootState) => ({ 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.get('placement'), dropdownPlacement: state.dropdown_menu.placement,
openDropdownId: state.dropdown_menu.get('openId'), openDropdownId: state.dropdown_menu.openId,
openedViaKeyboard: state.dropdown_menu.get('keyboard'), openedViaKeyboard: state.dropdown_menu.keyboard,
}); });
const mapDispatchToProps = (dispatch: Dispatch, { status, items }: Partial<IDropdown>) => ({ const mapDispatchToProps = (dispatch: Dispatch, { status, items }: Partial<IDropdown>) => ({

View File

@ -235,8 +235,8 @@ class Header extends ImmutablePureComponent {
// }); // });
// } // }
if (account.getIn(['relationship', 'following'])) { if (account.relationship?.following) {
if (account.getIn(['relationship', 'showing_reblogs'])) { if (account.relationship?.showing_reblogs) {
menu.push({ menu.push({
text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }),
action: this.props.onReblogToggle, action: this.props.onReblogToggle,
@ -251,7 +251,7 @@ class Header extends ImmutablePureComponent {
} }
if (features.accountSubscriptions) { if (features.accountSubscriptions) {
if (account.getIn(['relationship', 'subscribing'])) { if (account.relationship?.subscribing) {
menu.push({ menu.push({
text: intl.formatMessage(messages.unsubscribe, { name: account.get('username') }), text: intl.formatMessage(messages.unsubscribe, { name: account.get('username') }),
action: this.props.onSubscriptionToggle, action: this.props.onSubscriptionToggle,
@ -274,7 +274,7 @@ class Header extends ImmutablePureComponent {
}); });
} }
// menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle }); // menu.push({ text: intl.formatMessage(account.relationship?.endorsed ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
menu.push(null); menu.push(null);
} else if (features.lists && features.unrestrictedLists) { } else if (features.lists && features.unrestrictedLists) {
menu.push({ menu.push({
@ -284,7 +284,7 @@ class Header extends ImmutablePureComponent {
}); });
} }
if (features.removeFromFollowers && account.getIn(['relationship', 'followed_by'])) { if (features.removeFromFollowers && account.relationship?.followed_by) {
menu.push({ menu.push({
text: intl.formatMessage(messages.removeFromFollowers), text: intl.formatMessage(messages.removeFromFollowers),
action: this.props.onRemoveFromFollowers, action: this.props.onRemoveFromFollowers,
@ -292,7 +292,7 @@ class Header extends ImmutablePureComponent {
}); });
} }
if (account.getIn(['relationship', 'muting'])) { if (account.relationship?.muting) {
menu.push({ menu.push({
text: intl.formatMessage(messages.unmute, { name: account.get('username') }), text: intl.formatMessage(messages.unmute, { name: account.get('username') }),
action: this.props.onMute, action: this.props.onMute,
@ -306,7 +306,7 @@ class Header extends ImmutablePureComponent {
}); });
} }
if (account.getIn(['relationship', 'blocking'])) { if (account.relationship?.blocking) {
menu.push({ menu.push({
text: intl.formatMessage(messages.unblock, { name: account.get('username') }), text: intl.formatMessage(messages.unblock, { name: account.get('username') }),
action: this.props.onBlock, action: this.props.onBlock,
@ -332,7 +332,7 @@ class Header extends ImmutablePureComponent {
menu.push(null); menu.push(null);
if (account.getIn(['relationship', 'domain_blocking'])) { if (account.relationship?.domain_blocking) {
menu.push({ menu.push({
text: intl.formatMessage(messages.unblockDomain, { domain }), text: intl.formatMessage(messages.unblockDomain, { domain }),
action: this.props.onUnblockDomain, action: this.props.onUnblockDomain,
@ -463,7 +463,7 @@ class Header extends ImmutablePureComponent {
if (!account || !me) return info; if (!account || !me) return info;
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { if (me !== account.get('id') && account.relationship?.followed_by) {
info.push( info.push(
<Badge <Badge
key='followed_by' key='followed_by'
@ -471,7 +471,7 @@ class Header extends ImmutablePureComponent {
title={<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />} title={<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />}
/>, />,
); );
} else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) { } else if (me !== account.get('id') && account.relationship?.blocking) {
info.push( info.push(
<Badge <Badge
key='blocked' key='blocked'
@ -481,7 +481,7 @@ class Header extends ImmutablePureComponent {
); );
} }
if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) { if (me !== account.get('id') && account.relationship?.muting) {
info.push( info.push(
<Badge <Badge
key='muted' key='muted'
@ -489,7 +489,7 @@ class Header extends ImmutablePureComponent {
title={<FormattedMessage id='account.muted' defaultMessage='Muted' />} title={<FormattedMessage id='account.muted' defaultMessage='Muted' />}
/>, />,
); );
} else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) { } else if (me !== account.get('id') && account.relationship?.domain_blocking) {
info.push( info.push(
<Badge <Badge
key='domain_blocked' key='domain_blocked'

View File

@ -75,7 +75,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow(account) { onFollow(account) {
dispatch((_, getState) => { dispatch((_, getState) => {
const unfollowModal = getSettings(getState()).get('unfollowModal'); const unfollowModal = getSettings(getState()).get('unfollowModal');
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { if (account.relationship?.following || account.relationship?.requested) {
if (unfollowModal) { if (unfollowModal) {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
@ -92,7 +92,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onBlock(account) { onBlock(account) {
if (account.getIn(['relationship', 'blocking'])) { if (account.relationship?.blocking) {
dispatch(unblockAccount(account.get('id'))); dispatch(unblockAccount(account.get('id')));
} else { } else {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
@ -119,7 +119,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onReblogToggle(account) { onReblogToggle(account) {
if (account.getIn(['relationship', 'showing_reblogs'])) { if (account.relationship?.showing_reblogs) {
dispatch(followAccount(account.get('id'), { reblogs: false })); dispatch(followAccount(account.get('id'), { reblogs: false }));
} else { } else {
dispatch(followAccount(account.get('id'), { reblogs: true })); dispatch(followAccount(account.get('id'), { reblogs: true }));
@ -127,7 +127,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onSubscriptionToggle(account) { onSubscriptionToggle(account) {
if (account.getIn(['relationship', 'subscribing'])) { if (account.relationship?.subscribing) {
dispatch(unsubscribeAccount(account.get('id'))); dispatch(unsubscribeAccount(account.get('id')));
} else { } else {
dispatch(subscribeAccount(account.get('id'))); dispatch(subscribeAccount(account.get('id')));
@ -135,7 +135,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onNotifyToggle(account) { onNotifyToggle(account) {
if (account.getIn(['relationship', 'notifying'])) { if (account.relationship?.notifying) {
dispatch(followAccount(account.get('id'), { notify: false })); dispatch(followAccount(account.get('id'), { notify: false }));
} else { } else {
dispatch(followAccount(account.get('id'), { notify: true })); dispatch(followAccount(account.get('id'), { notify: true }));
@ -143,7 +143,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onEndorseToggle(account) { onEndorseToggle(account) {
if (account.getIn(['relationship', 'endorsed'])) { if (account.relationship?.endorsed) {
dispatch(unpinAccount(account.get('id'))); dispatch(unpinAccount(account.get('id')));
} else { } else {
dispatch(pinAccount(account.get('id'))); dispatch(pinAccount(account.get('id')));
@ -155,7 +155,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onMute(account) { onMute(account) {
if (account.getIn(['relationship', 'muting'])) { if (account.relationship?.muting) {
dispatch(unmuteAccount(account.get('id'))); dispatch(unmuteAccount(account.get('id')));
} else { } else {
dispatch(initMuteModal(account)); dispatch(initMuteModal(account));

View File

@ -39,7 +39,7 @@ const ReportStatus: React.FC<IReportStatus> = ({ status }) => {
return [{ return [{
text: intl.formatMessage(messages.viewStatus, { acct: `@${acct}` }), text: intl.formatMessage(messages.viewStatus, { acct: `@${acct}` }),
to: `/@${acct}/posts/${status.get('id')}`, to: `/@${acct}/posts/${status.id}`,
icon: require('@tabler/icons/icons/pencil.svg'), icon: require('@tabler/icons/icons/pencil.svg'),
}, { }, {
text: intl.formatMessage(messages.deleteStatus, { acct: `@${acct}` }), text: intl.formatMessage(messages.deleteStatus, { acct: `@${acct}` }),

View File

@ -7,8 +7,6 @@ import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import type { Map as ImmutableMap } from 'immutable';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'column.admin.moderation_log', defaultMessage: 'Moderation Log' }, 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.' }, 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 intl = useIntl();
const dispatch = useAppDispatch(); 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 items = useAppSelector((state) => {
const hasMore = useAppSelector((state) => state.admin_log.get('total', 0) - state.admin_log.get('index').count() > 0); 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 [isLoading, setIsLoading] = useState(true);
const [lastPage, setLastPage] = useState(0); const [lastPage, setLastPage] = useState(0);
@ -56,12 +56,12 @@ const ModerationLog = () => {
hasMore={hasMore} hasMore={hasMore}
onLoadMore={handleLoadMore} onLoadMore={handleLoadMore}
> >
{items.map((item, i) => ( {items.map((item) => item && (
<div className='logentry' key={i}> <div className='logentry' key={item.id}>
<div className='logentry__message'>{item.get('message')}</div> <div className='logentry__message'>{item.message}</div>
<div className='logentry__timestamp'> <div className='logentry__timestamp'>
<FormattedDate <FormattedDate
value={new Date(item.get('time') * 1000)} value={new Date(item.time * 1000)}
hour12={false} hour12={false}
year='numeric' year='numeric'
month='short' month='short'

View File

@ -17,7 +17,7 @@ const Search: React.FC = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const intl = useIntl(); const intl = useIntl();
const value = useAppSelector(state => state.aliases.getIn(['suggestions', 'value'])) as string; const value = useAppSelector(state => state.aliases.suggestions.value);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(changeAliasesSuggestions(e.target.value)); dispatch(changeAliasesSuggestions(e.target.value));

View File

@ -35,11 +35,11 @@ const Aliases = () => {
const instance = state.instance; const instance = state.instance;
const features = getFeatures(instance); const features = getFeatures(instance);
if (features.accountMoving) return state.aliases.getIn(['aliases', 'items'], ImmutableList()); if (features.accountMoving) return state.aliases.aliases.items;
return account!.pleroma.get('also_known_as'); return account!.pleroma.get('also_known_as');
}) as ImmutableList<string>; }) as ImmutableList<string>;
const searchAccountIds = useAppSelector((state) => state.aliases.getIn(['suggestions', 'items'])) as ImmutableList<string>; const searchAccountIds = useAppSelector((state) => state.aliases.suggestions.items);
const loaded = useAppSelector((state) => state.aliases.getIn(['suggestions', 'loaded'])); const loaded = useAppSelector((state) => state.aliases.suggestions.loaded);
useEffect(() => { useEffect(() => {
dispatch(fetchAliases); dispatch(fetchAliases);

View File

@ -20,9 +20,9 @@ const Bookmarks: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const intl = useIntl(); const intl = useIntl();
const statusIds = useAppSelector((state) => state.status_lists.getIn(['bookmarks', 'items'])); const statusIds = useAppSelector((state) => state.status_lists.get('bookmarks')!.items);
const isLoading = useAppSelector((state) => state.status_lists.getIn(['bookmarks', 'isLoading'], true)); const isLoading = useAppSelector((state) => state.status_lists.get('bookmarks')!.isLoading);
const hasMore = useAppSelector((state) => !!state.status_lists.getIn(['bookmarks', 'next'])); const hasMore = useAppSelector((state) => !!state.status_lists.get('bookmarks')!.next);
React.useEffect(() => { React.useEffect(() => {
dispatch(fetchBookmarkedStatuses()); dispatch(fetchBookmarkedStatuses());
@ -43,7 +43,7 @@ const Bookmarks: React.FC = () => {
statusIds={statusIds} statusIds={statusIds}
scrollKey='bookmarked_statuses' scrollKey='bookmarked_statuses'
hasMore={hasMore} hasMore={hasMore}
isLoading={isLoading} isLoading={typeof isLoading === 'boolean' ? isLoading : true}
onLoadMore={() => handleLoadMore(dispatch)} onLoadMore={() => handleLoadMore(dispatch)}
onRefresh={handleRefresh} onRefresh={handleRefresh}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}

View File

@ -26,7 +26,7 @@ const AccountCard: React.FC<IAccountCard> = ({ id }) => {
if (!account) return null; if (!account) return null;
const followedBy = me !== account.id && account.relationship.get('followed_by'); const followedBy = me !== account.id && account.relationship?.followed_by;
return ( return (
<div className='directory__card'> <div className='directory__card'>

View File

@ -24,8 +24,8 @@ const DomainBlocks: React.FC = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const intl = useIntl(); const intl = useIntl();
const domains = useAppSelector((state) => state.domain_lists.getIn(['blocks', 'items'])) as string[]; const domains = useAppSelector((state) => state.domain_lists.blocks.items);
const hasMore = useAppSelector((state) => !!state.domain_lists.getIn(['blocks', 'next'])); const hasMore = useAppSelector((state) => !!state.domain_lists.blocks.next);
React.useEffect(() => { React.useEffect(() => {
dispatch(fetchDomainBlocks()); dispatch(fetchDomainBlocks());

View File

@ -32,9 +32,9 @@ const mapStateToProps = (state, { params }) => {
if (isMyAccount) { if (isMyAccount) {
return { return {
isMyAccount, isMyAccount,
statusIds: state.getIn(['status_lists', 'favourites', 'items']), statusIds: state.status_lists.get('favourites').items,
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true), isLoading: state.status_lists.get('favourites').isLoading,
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']), hasMore: !!state.status_lists.get('favourites').next,
}; };
} }
@ -57,9 +57,9 @@ const mapStateToProps = (state, { params }) => {
unavailable, unavailable,
username, username,
isAccount: !!state.getIn(['accounts', accountId]), isAccount: !!state.getIn(['accounts', accountId]),
statusIds: state.getIn(['status_lists', `favourites:${accountId}`, 'items'], []), statusIds: state.status_lists.get(`favourites:${accountId}`)?.items || [],
isLoading: state.getIn(['status_lists', `favourites:${accountId}`, 'isLoading'], true), isLoading: state.status_lists.get(`favourites:${accountId}`)?.isLoading,
hasMore: !!state.getIn(['status_lists', `favourites:${accountId}`, 'next']), hasMore: !!state.status_lists.get(`favourites:${accountId}`)?.next,
}; };
}; };
@ -147,7 +147,7 @@ class Favourites extends ImmutablePureComponent {
statusIds={statusIds} statusIds={statusIds}
scrollKey='favourited_statuses' scrollKey='favourited_statuses'
hasMore={hasMore} hasMore={hasMore}
isLoading={isLoading} isLoading={typeof isLoading === 'boolean' ? isLoading : true}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
/> />

View File

@ -11,8 +11,8 @@ import Account from './account';
const FollowRecommendationsList: React.FC = () => { const FollowRecommendationsList: React.FC = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const suggestions = useAppSelector((state) => state.suggestions.get('items')); const suggestions = useAppSelector((state) => state.suggestions.items);
const isLoading = useAppSelector((state) => state.suggestions.get('isLoading')); const isLoading = useAppSelector((state) => state.suggestions.isLoading);
useEffect(() => { useEffect(() => {
if (suggestions.size === 0) { if (suggestions.size === 0) {
@ -30,8 +30,8 @@ const FollowRecommendationsList: React.FC = () => {
return ( return (
<div className='column-list'> <div className='column-list'>
{suggestions.size > 0 ? suggestions.map((suggestion: { account: string }, idx: number) => ( {suggestions.size > 0 ? suggestions.map((suggestion) => (
<Account key={idx} id={suggestion.account} /> <Account key={suggestion.account} id={suggestion.account} />
)) : ( )) : (
<div className='column-list__empty-message'> <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.' /> <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.' />

View File

@ -1,4 +1,3 @@
import { Map as ImmutableMap } from 'immutable';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -13,9 +12,9 @@ import { useAppSelector } from 'soapbox/hooks';
const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => { const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const suggestions = useAppSelector((state) => state.suggestions.get('items')); const suggestions = useAppSelector((state) => state.suggestions.items);
const hasMore = useAppSelector((state) => !!state.suggestions.get('next')); const hasMore = useAppSelector((state) => !!state.suggestions.next);
const isLoading = useAppSelector((state) => state.suggestions.get('isLoading')); const isLoading = useAppSelector((state) => state.suggestions.isLoading);
const handleLoadMore = debounce(() => { const handleLoadMore = debounce(() => {
if (isLoading) { if (isLoading) {
@ -40,11 +39,11 @@ const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
useWindowScroll={false} useWindowScroll={false}
style={{ height: 320 }} style={{ height: 320 }}
> >
{suggestions.map((suggestion: ImmutableMap<string, any>) => ( {suggestions.map((suggestion) => (
<div key={suggestion.get('account')} className='py-2'> <div key={suggestion.account} className='py-2'>
<AccountContainer <AccountContainer
// @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't // @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't
id={suggestion.get('account')} id={suggestion.account}
showProfileHoverCard={false} showProfileHoverCard={false}
/> />
</div> </div>

View File

@ -21,8 +21,8 @@ const mapStateToProps = (state, { params }) => {
const meUsername = state.getIn(['accounts', me, 'username'], ''); const meUsername = state.getIn(['accounts', me, 'username'], '');
return { return {
isMyAccount: (username.toLowerCase() === meUsername.toLowerCase()), isMyAccount: (username.toLowerCase() === meUsername.toLowerCase()),
statusIds: state.getIn(['status_lists', 'pins', 'items']), statusIds: state.status_lists.get('pins').items,
hasMore: !!state.getIn(['status_lists', 'pins', 'next']), hasMore: !!state.status_lists.get('pins').next,
}; };
}; };

View File

@ -22,9 +22,9 @@ const ScheduledStatuses = () => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const statusIds = useAppSelector((state) => state.status_lists.getIn(['scheduled_statuses', 'items'])); const statusIds = useAppSelector((state) => state.status_lists.get('scheduled_statuses')!.items);
const isLoading = useAppSelector((state) => state.status_lists.getIn(['scheduled_statuses', 'isLoading'])); const isLoading = useAppSelector((state) => state.status_lists.get('scheduled_statuses')!.isLoading);
const hasMore = useAppSelector((state) => !!state.status_lists.getIn(['scheduled_statuses', 'next'])); const hasMore = useAppSelector((state) => !!state.status_lists.get('scheduled_statuses')!.next);
useEffect(() => { useEffect(() => {
dispatch(fetchScheduledStatuses()); dispatch(fetchScheduledStatuses());
@ -37,7 +37,7 @@ const ScheduledStatuses = () => {
<ScrollableList <ScrollableList
scrollKey='scheduled_statuses' scrollKey='scheduled_statuses'
hasMore={hasMore} hasMore={hasMore}
isLoading={isLoading} isLoading={typeof isLoading === 'boolean' ? isLoading : true}
onLoadMore={() => handleLoadMore(dispatch)} onLoadMore={() => handleLoadMore(dispatch)}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
> >

View File

@ -387,9 +387,9 @@ class ActionBar extends React.PureComponent<IActionBar, IActionBarState> {
if (me) { if (me) {
if (features.bookmarks) { if (features.bookmarks) {
menu.push({ menu.push({
text: intl.formatMessage(status.get('bookmarked') ? messages.unbookmark : messages.bookmark), text: intl.formatMessage(status.bookmarked ? messages.unbookmark : messages.bookmark),
action: this.handleBookmarkClick, action: this.handleBookmarkClick,
icon: require(status.get('bookmarked') ? '@tabler/icons/icons/bookmark-off.svg' : '@tabler/icons/icons/bookmark.svg'), icon: require(status.bookmarked ? '@tabler/icons/icons/bookmark-off.svg' : '@tabler/icons/icons/bookmark.svg'),
}); });
} }
@ -406,7 +406,7 @@ class ActionBar extends React.PureComponent<IActionBar, IActionBarState> {
menu.push(null); menu.push(null);
} else if (status.visibility === 'private') { } else if (status.visibility === 'private') {
menu.push({ menu.push({
text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog_private),
action: this.handleReblogClick, action: this.handleReblogClick,
icon: require('@tabler/icons/icons/repeat.svg'), icon: require('@tabler/icons/icons/repeat.svg'),
}); });
@ -496,7 +496,7 @@ class ActionBar extends React.PureComponent<IActionBar, IActionBarState> {
} }
menu.push({ menu.push({
text: intl.formatMessage(status.get('sensitive') === false ? messages.markStatusSensitive : messages.markStatusNotSensitive), text: intl.formatMessage(status.sensitive === false ? messages.markStatusSensitive : messages.markStatusNotSensitive),
action: this.handleToggleStatusSensitivity, action: this.handleToggleStatusSensitivity,
icon: require('@tabler/icons/icons/alert-triangle.svg'), icon: require('@tabler/icons/icons/alert-triangle.svg'),
}); });
@ -523,18 +523,18 @@ class ActionBar extends React.PureComponent<IActionBar, IActionBarState> {
} }
} }
const canShare = ('share' in navigator) && status.get('visibility') === 'public'; const canShare = ('share' in navigator) && status.visibility === 'public';
let reblogIcon = require('@tabler/icons/icons/repeat.svg'); let reblogIcon = require('@tabler/icons/icons/repeat.svg');
if (status.get('visibility') === 'direct') { if (status.visibility === 'direct') {
reblogIcon = require('@tabler/icons/icons/mail.svg'); reblogIcon = require('@tabler/icons/icons/mail.svg');
} else if (status.get('visibility') === 'private') { } else if (status.visibility === 'private') {
reblogIcon = require('@tabler/icons/icons/lock.svg'); reblogIcon = require('@tabler/icons/icons/lock.svg');
} }
const reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private'); const reblog_disabled = (status.visibility === 'direct' || status.visibility === 'private');
const reblogMenu: Menu = [{ const reblogMenu: Menu = [{
text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog), text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog),

View File

@ -53,7 +53,7 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
const getNormalizedReacts = () => { const getNormalizedReacts = () => {
return reduceEmoji( return reduceEmoji(
ImmutableList(status.getIn(['pleroma', 'emoji_reactions']) as any), ImmutableList(status.pleroma.get('emoji_reactions') as any),
status.favourites_count, status.favourites_count,
status.favourited, status.favourited,
allowedEmoji, allowedEmoji,

View File

@ -332,7 +332,7 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
handleEditClick = (status: StatusEntity) => { handleEditClick = (status: StatusEntity) => {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch(editStatus(status.get('id'))); dispatch(editStatus(status.id));
} }
handleDirectClick = (account: AccountEntity, router: History) => { handleDirectClick = (account: AccountEntity, router: History) => {

View File

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

View File

@ -52,7 +52,7 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small }) =
const me = useAppSelector((state) => state.me); const me = useAppSelector((state) => state.me);
const handleFollow = () => { const handleFollow = () => {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { if (account.relationship?.following || account.relationship?.requested) {
dispatch(unfollowAccount(account.id)); dispatch(unfollowAccount(account.id));
} else { } else {
dispatch(followAccount(account.id)); dispatch(followAccount(account.id));
@ -60,7 +60,7 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small }) =
}; };
const handleBlock = () => { const handleBlock = () => {
if (account.getIn(['relationship', 'blocking'])) { if (account.relationship?.blocking) {
dispatch(unblockAccount(account.id)); dispatch(unblockAccount(account.id));
} else { } else {
dispatch(blockAccount(account.id)); dispatch(blockAccount(account.id));
@ -68,7 +68,7 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small }) =
}; };
const handleMute = () => { const handleMute = () => {
if (account.getIn(['relationship', 'muting'])) { if (account.relationship?.muting) {
dispatch(unmuteAccount(account.id)); dispatch(unmuteAccount(account.id));
} else { } else {
dispatch(muteAccount(account.id)); dispatch(muteAccount(account.id));
@ -85,7 +85,7 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small }) =
/** Handles actionType='muting' */ /** Handles actionType='muting' */
const mutingAction = () => { const mutingAction = () => {
const isMuted = account.getIn(['relationship', 'muting']); const isMuted = account.relationship?.muting;
const messageKey = isMuted ? messages.unmute : messages.mute; const messageKey = isMuted ? messages.unmute : messages.mute;
const text = intl.formatMessage(messageKey, { name: account.username }); const text = intl.formatMessage(messageKey, { name: account.username });
@ -101,7 +101,7 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small }) =
/** Handles actionType='blocking' */ /** Handles actionType='blocking' */
const blockingAction = () => { const blockingAction = () => {
const isBlocked = account.getIn(['relationship', 'blocking']); const isBlocked = account.relationship?.blocking;
const messageKey = isBlocked ? messages.unblock : messages.block; const messageKey = isBlocked ? messages.unblock : messages.block;
const text = intl.formatMessage(messageKey, { name: account.username }); const text = intl.formatMessage(messageKey, { name: account.username });
@ -154,8 +154,8 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small }) =
} }
if (me !== account.id) { if (me !== account.id) {
const isFollowing = account.getIn(['relationship', 'following']); const isFollowing = account.relationship?.following;
const blockedBy = account.getIn(['relationship', 'blocked_by']) as boolean; const blockedBy = account.relationship?.blocked_by as boolean;
if (actionType) { if (actionType) {
if (actionType === 'muting') { if (actionType === 'muting') {
@ -165,10 +165,10 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small }) =
} }
} }
if (account.relationship.isEmpty()) { if (!account.relationship) {
// Wait until the relationship is loaded // Wait until the relationship is loaded
return null; return null;
} else if (account.getIn(['relationship', 'requested'])) { } else if (account.relationship?.requested) {
// Awaiting acceptance // Awaiting acceptance
return ( return (
<Button <Button
@ -178,7 +178,7 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small }) =
onClick={handleFollow} onClick={handleFollow}
/> />
); );
} else if (!account.getIn(['relationship', 'blocking']) && !account.getIn(['relationship', 'muting'])) { } else if (!account.relationship?.blocking && !account.relationship?.muting) {
// Follow & Unfollow // Follow & Unfollow
return ( return (
<Button <Button
@ -195,7 +195,7 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small }) =
)} )}
</Button> </Button>
); );
} else if (account.getIn(['relationship', 'blocking'])) { } else if (account.relationship?.blocking) {
// Unblock // Unblock
return ( return (
<Button <Button

View File

@ -47,9 +47,9 @@ const SelectedStatus = ({ statusId }: { statusId: string }) => {
return ( return (
<Stack space={2} className='p-4 rounded-lg bg-gray-100 dark:bg-slate-700'> <Stack space={2} className='p-4 rounded-lg bg-gray-100 dark:bg-slate-700'>
<AccountContainer <AccountContainer
id={status.get('account') as any} id={status.account as any}
showProfileHoverCard={false} showProfileHoverCard={false}
timestamp={status.get('created_at')} timestamp={status.created_at}
hideActions hideActions
/> />
@ -59,10 +59,10 @@ const SelectedStatus = ({ statusId }: { statusId: string }) => {
collapsable collapsable
/> />
{status.get('media_attachments').size > 0 && ( {status.media_attachments.size > 0 && (
<AttachmentThumbs <AttachmentThumbs
media={status.get('media_attachments')} media={status.media_attachments}
sensitive={status.get('sensitive')} sensitive={status.sensitive}
/> />
)} )}
</Stack> </Stack>

View File

@ -29,14 +29,14 @@ const mapStateToProps = state => {
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
onSubscriptionToggle(account) { onSubscriptionToggle(account) {
if (account.getIn(['relationship', 'subscribing'])) { if (account.relationship?.subscribing) {
dispatch(unsubscribeAccount(account.get('id'))); dispatch(unsubscribeAccount(account.get('id')));
} else { } else {
dispatch(subscribeAccount(account.get('id'))); dispatch(subscribeAccount(account.get('id')));
} }
}, },
onNotifyToggle(account) { onNotifyToggle(account) {
if (account.getIn(['relationship', 'notifying'])) { if (account.relationship?.notifying) {
dispatch(followAccount(account.get('id'), { notify: false })); dispatch(followAccount(account.get('id'), { notify: false }));
} else { } else {
dispatch(followAccount(account.get('id'), { notify: true })); dispatch(followAccount(account.get('id'), { notify: true }));
@ -60,9 +60,9 @@ class SubscriptionButton extends ImmutablePureComponent {
render() { render() {
const { account, intl, features } = this.props; const { account, intl, features } = this.props;
const subscribing = features.accountNotifies ? account.getIn(['relationship', 'notifying']) : account.getIn(['relationship', 'subscribing']); const subscribing = features.accountNotifies ? account.relationship?.notifying : account.relationship?.subscribing;
const following = account.getIn(['relationship', 'following']); const following = account.relationship?.following;
const requested = account.getIn(['relationship', 'requested']); const requested = account.relationship?.requested;
if (requested || following) { if (requested || following) {
return ( return (

View File

@ -1,6 +1,5 @@
import { Map as ImmutableMap } from 'immutable';
import * as React from 'react'; 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 { useDispatch } from 'react-redux';
import { fetchSuggestions, dismissSuggestion } from 'soapbox/actions/suggestions'; 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 AccountContainer from 'soapbox/containers/account_container';
import { useAppSelector } from 'soapbox/hooks'; import { useAppSelector } from 'soapbox/hooks';
import type { Account as AccountEntity } from 'soapbox/types/entities';
const messages = defineMessages({ const messages = defineMessages({
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
}); });
@ -20,11 +21,11 @@ const WhoToFollowPanel = ({ limit }: IWhoToFollowPanel) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const intl = useIntl(); 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 suggestionsToRender = suggestions.slice(0, limit);
const handleDismiss = (account: ImmutableMap<string, any>) => { const handleDismiss = (account: AccountEntity) => {
dispatch(dismissSuggestion(account.get('id'))); dispatch(dismissSuggestion(account.id));
}; };
React.useEffect(() => { React.useEffect(() => {
@ -45,11 +46,11 @@ const WhoToFollowPanel = ({ limit }: IWhoToFollowPanel) => {
title={<FormattedMessage id='who_to_follow.title' defaultMessage='People To Follow' />} title={<FormattedMessage id='who_to_follow.title' defaultMessage='People To Follow' />}
// onAction={handleAction} // onAction={handleAction}
> >
{suggestionsToRender.map((suggestion: ImmutableMap<string, any>) => ( {suggestionsToRender.map((suggestion) => (
<AccountContainer <AccountContainer
key={suggestion.get('account')} key={suggestion.account}
// @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't // @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')} actionIcon={require('@tabler/icons/icons/x.svg')}
actionTitle={intl.formatMessage(messages.dismissSuggestion)} actionTitle={intl.formatMessage(messages.dismissSuggestion)}
onActionClick={handleDismiss} onActionClick={handleDismiss}

View File

@ -343,7 +343,7 @@ const UI: React.FC = ({ children }) => {
const features = useFeatures(); const features = useFeatures();
const vapidKey = useAppSelector(state => getVapidKey(state)); const vapidKey = useAppSelector(state => getVapidKey(state));
const dropdownMenuIsOpen = useAppSelector(state => state.dropdown_menu.get('openId') !== null); const dropdownMenuIsOpen = useAppSelector(state => state.dropdown_menu.openId !== null);
const accessToken = useAppSelector(state => getAccessToken(state)); const accessToken = useAppSelector(state => getAccessToken(state));
const streamingUrl = useAppSelector(state => state.instance.urls.get('streaming_api')); const streamingUrl = useAppSelector(state => state.instance.urls.get('streaming_api'));
const standalone = useAppSelector(isStandalone); const standalone = useAppSelector(isStandalone);

View File

@ -17,7 +17,7 @@ import { unescapeHTML } from 'soapbox/utils/html';
import { mergeDefined, makeEmojiMap } from 'soapbox/utils/normalizers'; import { mergeDefined, makeEmojiMap } from 'soapbox/utils/normalizers';
import type { PatronAccount } from 'soapbox/reducers/patron'; import type { PatronAccount } from 'soapbox/reducers/patron';
import type { Emoji, Field, EmbeddedEntity } from 'soapbox/types/entities'; import type { Emoji, Field, EmbeddedEntity, Relationship } from 'soapbox/types/entities';
// https://docs.joinmastodon.org/entities/account/ // https://docs.joinmastodon.org/entities/account/
export const AccountRecord = ImmutableRecord({ export const AccountRecord = ImmutableRecord({
@ -61,7 +61,7 @@ export const AccountRecord = ImmutableRecord({
note_emojified: '', note_emojified: '',
note_plain: '', note_plain: '',
patron: null as PatronAccount | null, patron: null as PatronAccount | null,
relationship: ImmutableMap<string, any>(), relationship: null as Relationship | null,
should_refetch: false, should_refetch: false,
staff: false, staff: false,
}); });

View File

@ -11,6 +11,7 @@ export { ListRecord, normalizeList } from './list';
export { MentionRecord, normalizeMention } from './mention'; export { MentionRecord, normalizeMention } from './mention';
export { NotificationRecord, normalizeNotification } from './notification'; export { NotificationRecord, normalizeNotification } from './notification';
export { PollRecord, PollOptionRecord, normalizePoll } from './poll'; export { PollRecord, PollOptionRecord, normalizePoll } from './poll';
export { RelationshipRecord, normalizeRelationship } from './relationship';
export { StatusRecord, normalizeStatus } from './status'; export { StatusRecord, normalizeStatus } from './status';
export { StatusEditRecord, normalizeStatusEdit } from './status_edit'; export { StatusEditRecord, normalizeStatusEdit } from './status_edit';

View File

@ -0,0 +1,35 @@
/**
* Relationship normalizer:
* Converts API relationships into our internal format.
* @see {@link https://docs.joinmastodon.org/entities/relationship/}
*/
import {
Map as ImmutableMap,
Record as ImmutableRecord,
fromJS,
} from 'immutable';
// https://docs.joinmastodon.org/entities/relationship/
// https://api.pleroma.social/#operation/AccountController.relationships
export const RelationshipRecord = ImmutableRecord({
blocked_by: false,
blocking: false,
domain_blocking: false,
endorsed: false,
followed_by: false,
following: false,
id: '',
muting: false,
muting_notifications: false,
note: '',
notifying: false,
requested: false,
showing_reblogs: false,
subscribing: false,
});
export const normalizeRelationship = (relationship: Record<string, any>) => {
return RelationshipRecord(
ImmutableMap(fromJS(relationship)),
);
};

View File

@ -1,13 +0,0 @@
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
import reducer from '../domain_lists';
describe('domain_lists reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap({
blocks: ImmutableMap({
items: ImmutableOrderedSet(),
}),
}));
});
});

View File

@ -0,0 +1,12 @@
import reducer from '../domain_lists';
describe('domain_lists reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {} as any).toJS()).toEqual({
blocks: {
items: [],
next: null,
},
});
});
});

View File

@ -1,13 +1,11 @@
import { Map as ImmutableMap } from 'immutable';
import reducer from '../dropdown_menu'; import reducer from '../dropdown_menu';
describe('dropdown_menu reducer', () => { describe('dropdown_menu reducer', () => {
it('should return the initial state', () => { it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap({ expect(reducer(undefined, {} as any).toJS()).toEqual({
openId: null, openId: null,
placement: null, placement: null,
keyboard: false, keyboard: false,
})); });
}); });
}); });

View File

@ -1,4 +1,4 @@
import { Map as ImmutableMap, fromJS } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import lain from 'soapbox/__fixtures__/lain.json'; import lain from 'soapbox/__fixtures__/lain.json';
@ -9,7 +9,7 @@ import reducer from '../relationships';
describe('relationships reducer', () => { describe('relationships reducer', () => {
it('should return the initial state', () => { it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap()); expect(reducer(undefined, {} as any)).toEqual(ImmutableMap());
}); });
describe('ACCOUNT_IMPORT', () => { describe('ACCOUNT_IMPORT', () => {
@ -18,8 +18,8 @@ describe('relationships reducer', () => {
type: ACCOUNT_IMPORT, type: ACCOUNT_IMPORT,
account: lain, account: lain,
}; };
const state = ImmutableMap(); const state = ImmutableMap<string, any>();
expect(reducer(state, action)).toEqual(fromJS({ expect(reducer(state, action).toJS()).toEqual({
'9v5bqYwY2jfmvPNhTM': { '9v5bqYwY2jfmvPNhTM': {
blocked_by: false, blocked_by: false,
blocking: false, blocking: false,
@ -30,11 +30,13 @@ describe('relationships reducer', () => {
id: '9v5bqYwY2jfmvPNhTM', id: '9v5bqYwY2jfmvPNhTM',
muting: false, muting: false,
muting_notifications: false, muting_notifications: false,
note: '',
notifying: false,
requested: false, requested: false,
showing_reblogs: true, showing_reblogs: true,
subscribing: false, subscribing: false,
}, },
})); });
}); });
}); });
}); });

View File

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

View File

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

View File

@ -1,40 +0,0 @@
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import { SUGGESTIONS_DISMISS } from 'soapbox/actions/suggestions';
import reducer from '../suggestions';
describe('suggestions reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap({
items: ImmutableOrderedSet(),
next: null,
isLoading: false,
}));
});
describe('SUGGESTIONS_DISMISS', () => {
it('should remove the account', () => {
const action = { type: SUGGESTIONS_DISMISS, id: '123' };
const state = fromJS({
items: [
{ account: '123', source: 'past_interactions' },
{ account: '456', source: 'past_interactions' },
{ account: '789', source: 'past_interactions' },
],
isLoading: false,
});
const expected = fromJS({
items: [
{ account: '456', source: 'past_interactions' },
{ account: '789', source: 'past_interactions' },
],
isLoading: false,
});
expect(reducer(state, action)).toEqual(expected);
});
});
});

View File

@ -0,0 +1,41 @@
import { SUGGESTIONS_FETCH_SUCCESS, SUGGESTIONS_DISMISS } from 'soapbox/actions/suggestions';
import reducer from '../suggestions';
describe('suggestions reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {} as any).toJS()).toEqual({
items: [],
next: null,
isLoading: false,
});
});
describe('SUGGESTIONS_DISMISS', () => {
it('should remove the account', () => {
let state = reducer(undefined, {} as any);
state = reducer(state, {
type: SUGGESTIONS_FETCH_SUCCESS,
accounts: [
{ id: '123' },
{ id: '456' },
{ id: '789' },
],
});
const action = { type: SUGGESTIONS_DISMISS, id: '123' };
const expected = {
items: [
{ account: '456', source: 'past_interactions' },
{ account: '789', source: 'past_interactions' },
],
isLoading: false,
next: null,
};
expect(reducer(state, action).toJS()).toEqual(expected);
});
});
});

View File

@ -27,9 +27,9 @@ describe('timelines reducer', () => {
describe('TIMELINE_EXPAND_FAIL', () => { describe('TIMELINE_EXPAND_FAIL', () => {
it('sets loading to false', () => { it('sets loading to false', () => {
const state = fromJS({ const state = ImmutableMap(fromJS({
home: { isLoading: true }, home: { isLoading: true },
}); }));
const action = { const action = {
type: TIMELINE_EXPAND_FAIL, type: TIMELINE_EXPAND_FAIL,
@ -43,9 +43,9 @@ describe('timelines reducer', () => {
describe('TIMELINE_EXPAND_SUCCESS', () => { describe('TIMELINE_EXPAND_SUCCESS', () => {
it('sets loading to false', () => { it('sets loading to false', () => {
const state = fromJS({ const state = ImmutableMap(fromJS({
home: { isLoading: true }, home: { isLoading: true },
}); }));
const action = { const action = {
type: TIMELINE_EXPAND_SUCCESS, type: TIMELINE_EXPAND_SUCCESS,
@ -70,9 +70,9 @@ describe('timelines reducer', () => {
}); });
it('merges new status IDs', () => { it('merges new status IDs', () => {
const state = fromJS({ const state = ImmutableMap(fromJS({
home: { items: ImmutableOrderedSet(['5', '2', '1']) }, home: { items: ImmutableOrderedSet(['5', '2', '1']) },
}); }));
const expected = ImmutableOrderedSet(['6', '5', '4', '2', '1']); const expected = ImmutableOrderedSet(['6', '5', '4', '2', '1']);
@ -87,9 +87,9 @@ describe('timelines reducer', () => {
}); });
it('merges old status IDs', () => { it('merges old status IDs', () => {
const state = fromJS({ const state = ImmutableMap(fromJS({
home: { items: ImmutableOrderedSet(['6', '4', '3']) }, home: { items: ImmutableOrderedSet(['6', '4', '3']) },
}); }));
const expected = ImmutableOrderedSet(['6', '4', '3', '5', '2', '1']); const expected = ImmutableOrderedSet(['6', '4', '3', '5', '2', '1']);
@ -104,9 +104,9 @@ describe('timelines reducer', () => {
}); });
it('overrides pinned post IDs', () => { it('overrides pinned post IDs', () => {
const state = fromJS({ const state = ImmutableMap(fromJS({
'account:1:pinned': { items: ImmutableOrderedSet(['5', '2', '1']) }, 'account:1:pinned': { items: ImmutableOrderedSet(['5', '2', '1']) },
}); }));
const expected = ImmutableOrderedSet(['9', '8', '7']); const expected = ImmutableOrderedSet(['9', '8', '7']);

View File

@ -30,15 +30,14 @@ import {
ADMIN_USERS_UNSUGGEST_FAIL, ADMIN_USERS_UNSUGGEST_FAIL,
} from 'soapbox/actions/admin'; } from 'soapbox/actions/admin';
import { CHATS_FETCH_SUCCESS, CHATS_EXPAND_SUCCESS, CHAT_FETCH_SUCCESS } from 'soapbox/actions/chats'; import { CHATS_FETCH_SUCCESS, CHATS_EXPAND_SUCCESS, CHAT_FETCH_SUCCESS } from 'soapbox/actions/chats';
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
import { normalizeAccount } from 'soapbox/normalizers/account';
import { normalizeId } from 'soapbox/utils/normalizers';
import { import {
ACCOUNT_IMPORT, ACCOUNT_IMPORT,
ACCOUNTS_IMPORT, ACCOUNTS_IMPORT,
ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP,
} from '../actions/importer'; } from 'soapbox/actions/importer';
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
import { normalizeAccount } from 'soapbox/normalizers/account';
import { normalizeId } from 'soapbox/utils/normalizers';
type AccountRecord = ReturnType<typeof normalizeAccount>; type AccountRecord = ReturnType<typeof normalizeAccount>;
type AccountMap = ImmutableMap<string, any>; type AccountMap = ImmutableMap<string, any>;

View File

@ -1,54 +0,0 @@
import { Map as ImmutableMap, fromJS } from 'immutable';
import { STREAMING_FOLLOW_RELATIONSHIPS_UPDATE } from 'soapbox/actions/streaming';
import {
ACCOUNT_FOLLOW_SUCCESS,
ACCOUNT_UNFOLLOW_SUCCESS,
} from '../actions/accounts';
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
const normalizeAccount = (state, account) => state.set(account.id, fromJS({
followers_count: account.followers_count,
following_count: account.following_count,
statuses_count: account.statuses_count,
}));
const normalizeAccounts = (state, accounts) => {
accounts.forEach(account => {
state = normalizeAccount(state, account);
});
return state;
};
const updateFollowCounters = (state, counterUpdates) => {
return state.withMutations(state => {
counterUpdates.forEach(counterUpdate => {
state.update(counterUpdate.id, ImmutableMap(), counters => counters.merge({
followers_count: counterUpdate.follower_count,
following_count: counterUpdate.following_count,
}));
});
});
};
const initialState = ImmutableMap();
export default function accountsCounters(state = initialState, action) {
switch (action.type) {
case ACCOUNT_IMPORT:
return normalizeAccount(state, action.account);
case ACCOUNTS_IMPORT:
return normalizeAccounts(state, action.accounts);
case ACCOUNT_FOLLOW_SUCCESS:
return action.alreadyFollowing ? state :
state.updateIn([action.relationship.id, 'followers_count'], num => num + 1);
case ACCOUNT_UNFOLLOW_SUCCESS:
return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1));
case STREAMING_FOLLOW_RELATIONSHIPS_UPDATE:
return updateFollowCounters(state, [action.follower, action.following]);
default:
return state;
}
}

View File

@ -0,0 +1,64 @@
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord } from 'immutable';
import {
ACCOUNT_FOLLOW_SUCCESS,
ACCOUNT_UNFOLLOW_SUCCESS,
} from 'soapbox/actions/accounts';
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from 'soapbox/actions/importer';
import { STREAMING_FOLLOW_RELATIONSHIPS_UPDATE } from 'soapbox/actions/streaming';
import type { AnyAction } from 'redux';
const CounterRecord = ImmutableRecord({
followers_count: 0,
following_count: 0,
statuses_count: 0,
});
type Counter = ReturnType<typeof CounterRecord>;
type State = ImmutableMap<string, Counter>;
type APIEntity = Record<string, any>;
type APIEntities = Array<APIEntity>;
const normalizeAccount = (state: State, account: APIEntity) => state.set(account.id, CounterRecord({
followers_count: account.followers_count,
following_count: account.following_count,
statuses_count: account.statuses_count,
}));
const normalizeAccounts = (state: State, accounts: ImmutableList<APIEntities>) => {
accounts.forEach(account => {
state = normalizeAccount(state, account);
});
return state;
};
const updateFollowCounters = (state: State, counterUpdates: APIEntities) => {
return state.withMutations(state => {
counterUpdates.forEach((counterUpdate) => {
state.update(counterUpdate.id, CounterRecord(), counters => counters.merge({
followers_count: counterUpdate.follower_count,
following_count: counterUpdate.following_count,
}));
});
});
};
export default function accountsCounters(state: State = ImmutableMap<string, Counter>(), action: AnyAction) {
switch (action.type) {
case ACCOUNT_IMPORT:
return normalizeAccount(state, action.account);
case ACCOUNTS_IMPORT:
return normalizeAccounts(state, action.accounts);
case ACCOUNT_FOLLOW_SUCCESS:
return action.alreadyFollowing ? state :
state.updateIn([action.relationship.id, 'followers_count'], 0, (count) => typeof count === 'number' ? count + 1 : 0);
case ACCOUNT_UNFOLLOW_SUCCESS:
return state.updateIn([action.relationship.id, 'followers_count'], 0, (count) => typeof count === 'number' ? Math.max(0, count - 1) : 0);
case STREAMING_FOLLOW_RELATIONSHIPS_UPDATE:
return updateFollowCounters(state, [action.follower, action.following]);
default:
return state;
}
}

View File

@ -1,33 +0,0 @@
/**
* Accounts Meta: private user data only the owner should see.
* @module soapbox/reducers/accounts_meta
*/
import { Map as ImmutableMap, fromJS } from 'immutable';
import { VERIFY_CREDENTIALS_SUCCESS, AUTH_ACCOUNT_REMEMBER_SUCCESS } from 'soapbox/actions/auth';
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me';
const initialState = ImmutableMap();
const importAccount = (state, account) => {
const accountId = account.get('id');
return state.set(accountId, ImmutableMap({
pleroma: account.get('pleroma', ImmutableMap()).delete('settings_store'),
source: account.get('source', ImmutableMap()),
}));
};
export default function accounts_meta(state = initialState, action) {
switch (action.type) {
case ME_FETCH_SUCCESS:
case ME_PATCH_SUCCESS:
return importAccount(state, fromJS(action.me));
case VERIFY_CREDENTIALS_SUCCESS:
case AUTH_ACCOUNT_REMEMBER_SUCCESS:
return importAccount(state, fromJS(action.account));
default:
return state;
}
}

View File

@ -0,0 +1,41 @@
/**
* Accounts Meta: private user data only the owner should see.
* @module soapbox/reducers/accounts_meta
*/
import { Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
import { VERIFY_CREDENTIALS_SUCCESS, AUTH_ACCOUNT_REMEMBER_SUCCESS } from 'soapbox/actions/auth';
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me';
import type { AnyAction } from 'redux';
const MetaRecord = ImmutableRecord({
pleroma: ImmutableMap<string, any>(),
source: ImmutableMap<string, any>(),
});
type Meta = ReturnType<typeof MetaRecord>;
type State = ImmutableMap<string, Meta>;
const importAccount = (state: State, account: ImmutableMap<string, any>) => {
const accountId = account.get('id');
return state.set(accountId, MetaRecord({
pleroma: account.get('pleroma', ImmutableMap()).delete('settings_store'),
source: account.get('source', ImmutableMap()),
}));
};
export default function accounts_meta(state: State = ImmutableMap<string, Meta>(), action: AnyAction) {
switch (action.type) {
case ME_FETCH_SUCCESS:
case ME_PATCH_SUCCESS:
return importAccount(state, ImmutableMap(fromJS(action.me)));
case VERIFY_CREDENTIALS_SUCCESS:
case AUTH_ACCOUNT_REMEMBER_SUCCESS:
return importAccount(state, ImmutableMap(fromJS(action.account)));
default:
return state;
}
}

View File

@ -2,40 +2,53 @@ import {
Map as ImmutableMap, Map as ImmutableMap,
Record as ImmutableRecord, Record as ImmutableRecord,
OrderedSet as ImmutableOrderedSet, OrderedSet as ImmutableOrderedSet,
fromJS,
} from 'immutable'; } from 'immutable';
import { ADMIN_LOG_FETCH_SUCCESS } from 'soapbox/actions/admin'; 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({ const ReducerRecord = ImmutableRecord({
items: ImmutableMap(), items: ImmutableMap<string, LogEntry>(),
index: ImmutableOrderedSet(), index: ImmutableOrderedSet<number>(),
total: 0, total: 0,
}); });
const parseItems = items => { type LogEntry = ReturnType<typeof LogEntryRecord>;
const ids = []; type State = ReturnType<typeof ReducerRecord>;
const map = {}; type APIEntity = Record<string, any>;
type APIEntities = Array<APIEntity>;
const parseItems = (items: APIEntities) => {
const ids: Array<number> = [];
const map: Record<string, LogEntry> = {};
items.forEach(item => { items.forEach(item => {
ids.push(item.id); ids.push(item.id);
map[item.id] = item; map[item.id] = LogEntryRecord(item);
}); });
return { ids: ids, map: map }; return { ids: ids, map: map };
}; };
const importItems = (state, items, total) => { const importItems = (state: State, items: APIEntities, total: number) => {
const { ids, map } = parseItems(items); const { ids, map } = parseItems(items);
return state.withMutations(state => { return state.withMutations(state => {
state.update('index', v => v.union(ids)); 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); state.set('total', total);
}); });
}; };
export default function admin_log(state = ReducerRecord(), action) { export default function admin_log(state = ReducerRecord(), action: AnyAction) {
switch (action.type) { switch (action.type) {
case ADMIN_LOG_FETCH_SUCCESS: case ADMIN_LOG_FETCH_SUCCESS:
return importItems(state, action.items, action.total); return importItems(state, action.items, action.total);

View File

@ -1,4 +1,4 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { List as ImmutableList, Record as ImmutableRecord } from 'immutable';
import { import {
ALIASES_SUGGESTIONS_READY, ALIASES_SUGGESTIONS_READY,
@ -7,19 +7,23 @@ import {
ALIASES_FETCH_SUCCESS, ALIASES_FETCH_SUCCESS,
} from '../actions/aliases'; } from '../actions/aliases';
const initialState = ImmutableMap({ import type { AnyAction } from 'redux';
aliases: ImmutableMap({ import type { APIEntity } from 'soapbox/types/entities';
const ReducerRecord = ImmutableRecord({
aliases: ImmutableRecord({
items: ImmutableList<string>(),
loaded: false, loaded: false,
items: ImmutableList(), })(),
}), suggestions: ImmutableRecord({
suggestions: ImmutableMap({ items: ImmutableList<string>(),
value: '', value: '',
loaded: false, loaded: false,
items: ImmutableList(), })(),
}),
}); });
export default function aliasesReducer(state = initialState, action) { export default function aliasesReducer(state = ReducerRecord(), action: AnyAction) {
switch (action.type) { switch (action.type) {
case ALIASES_FETCH_SUCCESS: case ALIASES_FETCH_SUCCESS:
return state return state
@ -30,7 +34,7 @@ export default function aliasesReducer(state = initialState, action) {
.setIn(['suggestions', 'loaded'], false); .setIn(['suggestions', 'loaded'], false);
case ALIASES_SUGGESTIONS_READY: case ALIASES_SUGGESTIONS_READY:
return state return state
.setIn(['suggestions', 'items'], ImmutableList(action.accounts.map(item => item.id))) .setIn(['suggestions', 'items'], ImmutableList(action.accounts.map((item: APIEntity) => item.id)))
.setIn(['suggestions', 'loaded'], true); .setIn(['suggestions', 'loaded'], true);
case ALIASES_SUGGESTIONS_CLEAR: case ALIASES_SUGGESTIONS_CLEAR:
return state.update('suggestions', suggestions => suggestions.withMutations(map => { return state.update('suggestions', suggestions => suggestions.withMutations(map => {

View File

@ -1,26 +0,0 @@
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
import {
DOMAIN_BLOCKS_FETCH_SUCCESS,
DOMAIN_BLOCKS_EXPAND_SUCCESS,
DOMAIN_UNBLOCK_SUCCESS,
} from '../actions/domain_blocks';
const initialState = ImmutableMap({
blocks: ImmutableMap({
items: ImmutableOrderedSet(),
}),
});
export default function domainLists(state = initialState, action) {
switch (action.type) {
case DOMAIN_BLOCKS_FETCH_SUCCESS:
return state.setIn(['blocks', 'items'], ImmutableOrderedSet(action.domains)).setIn(['blocks', 'next'], action.next);
case DOMAIN_BLOCKS_EXPAND_SUCCESS:
return state.updateIn(['blocks', 'items'], set => set.union(action.domains)).setIn(['blocks', 'next'], action.next);
case DOMAIN_UNBLOCK_SUCCESS:
return state.updateIn(['blocks', 'items'], set => set.delete(action.domain));
default:
return state;
}
}

View File

@ -0,0 +1,33 @@
import { OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord } from 'immutable';
import {
DOMAIN_BLOCKS_FETCH_SUCCESS,
DOMAIN_BLOCKS_EXPAND_SUCCESS,
DOMAIN_UNBLOCK_SUCCESS,
} from '../actions/domain_blocks';
import type { AnyAction } from 'redux';
const BlocksRecord = ImmutableRecord({
items: ImmutableOrderedSet<string>(),
next: null as string | null,
});
const ReducerRecord = ImmutableRecord({
blocks: BlocksRecord(),
});
type State = ReturnType<typeof ReducerRecord>;
export default function domainLists(state: State = ReducerRecord(), action: AnyAction) {
switch (action.type) {
case DOMAIN_BLOCKS_FETCH_SUCCESS:
return state.setIn(['blocks', 'items'], ImmutableOrderedSet(action.domains)).setIn(['blocks', 'next'], action.next);
case DOMAIN_BLOCKS_EXPAND_SUCCESS:
return state.updateIn(['blocks', 'items'], set => (set as ImmutableOrderedSet<string>).union(action.domains)).setIn(['blocks', 'next'], action.next);
case DOMAIN_UNBLOCK_SUCCESS:
return state.updateIn(['blocks', 'items'], set => (set as ImmutableOrderedSet<string>).delete(action.domain));
default:
return state;
}
}

View File

@ -1,19 +0,0 @@
import { Map as ImmutableMap } from 'immutable';
import {
DROPDOWN_MENU_OPEN,
DROPDOWN_MENU_CLOSE,
} from '../actions/dropdown_menu';
const initialState = ImmutableMap({ openId: null, placement: null, keyboard: false });
export default function dropdownMenu(state = initialState, action) {
switch (action.type) {
case DROPDOWN_MENU_OPEN:
return state.merge({ openId: action.id, placement: action.placement, keyboard: action.keyboard });
case DROPDOWN_MENU_CLOSE:
return state.get('openId') === action.id ? state.set('openId', null) : state;
default:
return state;
}
}

View File

@ -0,0 +1,28 @@
import { Record as ImmutableRecord } from 'immutable';
import {
DROPDOWN_MENU_OPEN,
DROPDOWN_MENU_CLOSE,
} 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 any as DropdownPlacement,
keyboard: false,
});
type State = ReturnType<typeof ReducerRecord>;
export default function dropdownMenu(state: State = ReducerRecord(), action: AnyAction) {
switch (action.type) {
case DROPDOWN_MENU_OPEN:
return state.merge({ openId: action.id, placement: action.placement, keyboard: action.keyboard });
case DROPDOWN_MENU_CLOSE:
return state.openId === action.id ? state.set('openId', null) : state;
default:
return state;
}
}

View File

@ -1,7 +1,8 @@
import { Map as ImmutableMap, fromJS } from 'immutable'; import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { get } from 'lodash'; import { get } from 'lodash';
import { STREAMING_FOLLOW_RELATIONSHIPS_UPDATE } from 'soapbox/actions/streaming'; import { STREAMING_FOLLOW_RELATIONSHIPS_UPDATE } from 'soapbox/actions/streaming';
import { normalizeRelationship } from 'soapbox/normalizers/relationship';
import { ACCOUNT_NOTE_SUBMIT_SUCCESS } from '../actions/account-notes'; import { ACCOUNT_NOTE_SUBMIT_SUCCESS } from '../actions/account-notes';
import { import {
@ -31,17 +32,22 @@ import {
ACCOUNTS_IMPORT, ACCOUNTS_IMPORT,
} from '../actions/importer'; } from '../actions/importer';
const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship)); import type { AnyAction } from 'redux';
const normalizeRelationships = (state, relationships) => { type Relationship = ReturnType<typeof normalizeRelationship>;
type State = ImmutableMap<string, Relationship>;
type APIEntity = Record<string, any>;
type APIEntities = Array<APIEntity>;
const normalizeRelationships = (state: State, relationships: APIEntities) => {
relationships.forEach(relationship => { relationships.forEach(relationship => {
state = normalizeRelationship(state, relationship); state = state.set(relationship.id, normalizeRelationship(relationship));
}); });
return state; return state;
}; };
const setDomainBlocking = (state, accounts, blocking) => { const setDomainBlocking = (state: State, accounts: ImmutableList<string>, blocking: boolean) => {
return state.withMutations(map => { return state.withMutations(map => {
accounts.forEach(id => { accounts.forEach(id => {
map.setIn([id, 'domain_blocking'], blocking); map.setIn([id, 'domain_blocking'], blocking);
@ -49,14 +55,14 @@ const setDomainBlocking = (state, accounts, blocking) => {
}); });
}; };
const importPleromaAccount = (state, account) => { const importPleromaAccount = (state: State, account: APIEntity) => {
const relationship = get(account, ['pleroma', 'relationship'], {}); const relationship = get(account, ['pleroma', 'relationship'], {});
if (relationship.id && relationship !== {}) if (relationship.id && relationship !== {})
return normalizeRelationship(state, relationship); return normalizeRelationships(state, [relationship]);
return state; return state;
}; };
const importPleromaAccounts = (state, accounts) => { const importPleromaAccounts = (state: State, accounts: APIEntities) => {
accounts.forEach(account => { accounts.forEach(account => {
state = importPleromaAccount(state, account); state = importPleromaAccount(state, account);
}); });
@ -64,7 +70,7 @@ const importPleromaAccounts = (state, accounts) => {
return state; return state;
}; };
const followStateToRelationship = followState => { const followStateToRelationship = (followState: string) => {
switch (followState) { switch (followState) {
case 'follow_pending': case 'follow_pending':
return { following: false, requested: true }; return { following: false, requested: true };
@ -77,14 +83,12 @@ const followStateToRelationship = followState => {
} }
}; };
const updateFollowRelationship = (state, id, followState) => { const updateFollowRelationship = (state: State, id: string, followState: string) => {
const map = followStateToRelationship(followState); const map = followStateToRelationship(followState);
return state.update(id, ImmutableMap(), relationship => relationship.merge(map)); return state.update(id, normalizeRelationship({}), relationship => relationship.merge(map));
}; };
const initialState = ImmutableMap(); export default function relationships(state: State = ImmutableMap<string, Relationship>(), action: AnyAction) {
export default function relationships(state = initialState, action) {
switch (action.type) { switch (action.type) {
case ACCOUNT_IMPORT: case ACCOUNT_IMPORT:
return importPleromaAccount(state, action.account); return importPleromaAccount(state, action.account);
@ -110,7 +114,7 @@ export default function relationships(state = initialState, action) {
case ACCOUNT_UNPIN_SUCCESS: case ACCOUNT_UNPIN_SUCCESS:
case ACCOUNT_NOTE_SUBMIT_SUCCESS: case ACCOUNT_NOTE_SUBMIT_SUCCESS:
case ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS: case ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS:
return normalizeRelationship(state, action.relationship); return normalizeRelationships(state, [action.relationship]);
case RELATIONSHIPS_FETCH_SUCCESS: case RELATIONSHIPS_FETCH_SUCCESS:
return normalizeRelationships(state, action.relationships); return normalizeRelationships(state, action.relationships);
case DOMAIN_BLOCK_SUCCESS: case DOMAIN_BLOCK_SUCCESS:

View File

@ -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 { import {
BOOKMARKED_STATUSES_FETCH_REQUEST, BOOKMARKED_STATUSES_FETCH_REQUEST,
@ -44,29 +48,38 @@ import {
SCHEDULED_STATUS_CANCEL_SUCCESS, SCHEDULED_STATUS_CANCEL_SUCCESS,
} from '../actions/scheduled_statuses'; } from '../actions/scheduled_statuses';
const initialMap = ImmutableMap({ import type { AnyAction } from 'redux';
next: null, import type { Status as StatusEntity } from 'soapbox/types/entities';
const StatusListRecord = ImmutableRecord({
next: null as string | null,
loaded: false, loaded: false,
items: ImmutableOrderedSet(), isLoading: null as boolean | null,
items: ImmutableOrderedSet<string>(),
}); });
const initialState = ImmutableMap({ type State = ImmutableMap<string, StatusList>;
favourites: initialMap, type StatusList = ReturnType<typeof StatusListRecord>;
bookmarks: initialMap, type Status = string | StatusEntity;
pins: initialMap, type Statuses = Array<string | StatusEntity>;
scheduled_statuses: initialMap,
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 = []) => ( const getStatusIds = (statuses: Statuses = []) => (
ImmutableOrderedSet(statuses.map(status => status.id)) 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) => { const normalizeList = (state: State, listType: string, statuses: Statuses, next: string | null) => {
return state.update(listType, initialMap, listMap => listMap.withMutations(map => { return state.update(listType, StatusListRecord(), listMap => listMap.withMutations(map => {
map.set('next', next); map.set('next', next);
map.set('loaded', true); map.set('loaded', true);
map.set('isLoading', false); 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); 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('next', next);
map.set('isLoading', false); 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); const statusId = getStatusId(status);
return state.updateIn([listType, 'items'], ImmutableOrderedSet(), items => { 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); 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) { switch (action.type) {
case FAVOURITED_STATUSES_FETCH_REQUEST: case FAVOURITED_STATUSES_FETCH_REQUEST:
case FAVOURITED_STATUSES_EXPAND_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); return appendToList(state, 'scheduled_statuses', action.statuses, action.next);
case SCHEDULED_STATUS_CANCEL_REQUEST: case SCHEDULED_STATUS_CANCEL_REQUEST:
case SCHEDULED_STATUS_CANCEL_SUCCESS: 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: default:
return state; return state;
} }

View File

@ -116,7 +116,7 @@ export const calculateStatus = (
// Check whether a status is a quote by secondary characteristics // Check whether a status is a quote by secondary characteristics
const isQuote = (status: StatusRecord) => { const isQuote = (status: StatusRecord) => {
return Boolean(status.getIn(['pleroma', 'quote_url'])); return Boolean(status.pleroma.get('quote_url'));
}; };
// Preserve quote if an existing status already has it // Preserve quote if an existing status already has it
@ -124,7 +124,7 @@ const fixQuote = (status: StatusRecord, oldStatus?: StatusRecord): StatusRecord
if (oldStatus && !status.quote && isQuote(status)) { if (oldStatus && !status.quote && isQuote(status)) {
return status return status
.set('quote', oldStatus.quote) .set('quote', oldStatus.quote)
.updateIn(['pleroma', 'quote_visible'], visible => visible || oldStatus.getIn(['pleroma', 'quote_visible'])); .updateIn(['pleroma', 'quote_visible'], visible => visible || oldStatus.pleroma.get('quote_visible'));
} else { } else {
return status; return status;
} }

View File

@ -1,8 +1,8 @@
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import { OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord } from 'immutable';
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'soapbox/actions/accounts'; import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'soapbox/actions/accounts';
import { DOMAIN_BLOCK_SUCCESS } from 'soapbox/actions/domain_blocks'; import { DOMAIN_BLOCK_SUCCESS } from 'soapbox/actions/domain_blocks';
import { import {
SUGGESTIONS_FETCH_REQUEST, SUGGESTIONS_FETCH_REQUEST,
SUGGESTIONS_FETCH_SUCCESS, SUGGESTIONS_FETCH_SUCCESS,
@ -11,46 +11,58 @@ import {
SUGGESTIONS_V2_FETCH_REQUEST, SUGGESTIONS_V2_FETCH_REQUEST,
SUGGESTIONS_V2_FETCH_SUCCESS, SUGGESTIONS_V2_FETCH_SUCCESS,
SUGGESTIONS_V2_FETCH_FAIL, SUGGESTIONS_V2_FETCH_FAIL,
} from '../actions/suggestions'; } from 'soapbox/actions/suggestions';
const initialState = ImmutableMap({ import type { AnyAction } from 'redux';
items: ImmutableOrderedSet(), import type { APIEntity } from 'soapbox/types/entities';
next: null,
const SuggestionRecord = ImmutableRecord({
source: '',
account: '',
});
const ReducerRecord = ImmutableRecord({
items: ImmutableOrderedSet<Suggestion>(),
next: null as string | null,
isLoading: false, isLoading: false,
}); });
type State = ReturnType<typeof ReducerRecord>;
type Suggestion = ReturnType<typeof SuggestionRecord>;
type APIEntities = Array<APIEntity>;
// Convert a v1 account into a v2 suggestion // Convert a v1 account into a v2 suggestion
const accountToSuggestion = account => { const accountToSuggestion = (account: APIEntity) => {
return { return {
source: 'past_interactions', source: 'past_interactions',
account: account.id, account: account.id,
}; };
}; };
const importAccounts = (state, accounts) => { const importAccounts = (state: State, accounts: APIEntities) => {
return state.withMutations(state => { return state.withMutations(state => {
state.set('items', fromJS(accounts.map(accountToSuggestion))); state.set('items', ImmutableOrderedSet(accounts.map(accountToSuggestion).map(suggestion => SuggestionRecord(suggestion))));
state.set('isLoading', false); state.set('isLoading', false);
}); });
}; };
const importSuggestions = (state, suggestions, next) => { const importSuggestions = (state: State, suggestions: APIEntities, next: string | null) => {
return state.withMutations(state => { return state.withMutations(state => {
state.update('items', items => items.concat(fromJS(suggestions.map(x => ({ ...x, account: x.account.id }))))); state.update('items', items => items.concat(suggestions.map(x => ({ ...x, account: x.account.id })).map(suggestion => SuggestionRecord(suggestion))));
state.set('isLoading', false); state.set('isLoading', false);
state.set('next', next); state.set('next', next);
}); });
}; };
const dismissAccount = (state, accountId) => { const dismissAccount = (state: State, accountId: string) => {
return state.update('items', items => items.filterNot(item => item.get('account') === accountId)); return state.update('items', items => items.filterNot(item => item.account === accountId));
}; };
const dismissAccounts = (state, accountIds) => { const dismissAccounts = (state: State, accountIds: Array<string>) => {
return state.update('items', items => items.filterNot(item => accountIds.includes(item.get('account')))); return state.update('items', items => items.filterNot(item => accountIds.includes(item.account)));
}; };
export default function suggestionsReducer(state = initialState, action) { export default function suggestionsReducer(state: State = ReducerRecord(), action: AnyAction) {
switch (action.type) { switch (action.type) {
case SUGGESTIONS_FETCH_REQUEST: case SUGGESTIONS_FETCH_REQUEST:
case SUGGESTIONS_V2_FETCH_REQUEST: case SUGGESTIONS_V2_FETCH_REQUEST:

View File

@ -22,7 +22,7 @@ const getAccountBase = (state: RootState, id: string) => state.accounts.
const getAccountCounters = (state: RootState, id: string) => state.accounts_counters.get(id); const getAccountCounters = (state: RootState, id: string) => state.accounts_counters.get(id);
const getAccountRelationship = (state: RootState, id: string) => state.relationships.get(id); const getAccountRelationship = (state: RootState, id: string) => state.relationships.get(id);
const getAccountMoved = (state: RootState, id: string) => state.accounts.get(state.accounts.get(id)?.moved || ''); const getAccountMoved = (state: RootState, id: string) => state.accounts.get(state.accounts.get(id)?.moved || '');
const getAccountMeta = (state: RootState, id: string) => state.accounts_meta.get(id, ImmutableMap()); const getAccountMeta = (state: RootState, id: string) => state.accounts_meta.get(id);
const getAccountAdminData = (state: RootState, id: string) => state.admin.users.get(id); const getAccountAdminData = (state: RootState, id: string) => state.admin.users.get(id);
const getAccountPatron = (state: RootState, id: string) => { const getAccountPatron = (state: RootState, id: string) => {
const url = state.accounts.get(id)?.url; const url = state.accounts.get(id)?.url;
@ -42,10 +42,12 @@ export const makeGetAccount = () => {
if (!base) return null; if (!base) return null;
return base.withMutations(map => { return base.withMutations(map => {
map.merge(counters); if (counters) map.merge(counters);
map.merge(meta); if (meta) {
map.set('pleroma', meta.get('pleroma', ImmutableMap()).merge(base.get('pleroma', ImmutableMap()))); // Lol, thanks Pleroma map.merge(meta);
map.set('relationship', relationship); map.set('pleroma', meta.pleroma.merge(base.get('pleroma', ImmutableMap()))); // Lol, thanks Pleroma
}
if (relationship) map.set('relationship', relationship);
map.set('moved', moved || null); map.set('moved', moved || null);
map.set('patron', patron || null); map.set('patron', patron || null);
map.setIn(['pleroma', 'admin'], admin); map.setIn(['pleroma', 'admin'], admin);

View File

@ -14,6 +14,7 @@ import {
NotificationRecord, NotificationRecord,
PollRecord, PollRecord,
PollOptionRecord, PollOptionRecord,
RelationshipRecord,
StatusEditRecord, StatusEditRecord,
StatusRecord, StatusRecord,
} from 'soapbox/normalizers'; } from 'soapbox/normalizers';
@ -34,6 +35,7 @@ type Mention = ReturnType<typeof MentionRecord>;
type Notification = ReturnType<typeof NotificationRecord>; type Notification = ReturnType<typeof NotificationRecord>;
type Poll = ReturnType<typeof PollRecord>; type Poll = ReturnType<typeof PollRecord>;
type PollOption = ReturnType<typeof PollOptionRecord>; type PollOption = ReturnType<typeof PollOptionRecord>;
type Relationship = ReturnType<typeof RelationshipRecord>;
type StatusEdit = ReturnType<typeof StatusEditRecord>; type StatusEdit = ReturnType<typeof StatusEditRecord>;
interface Account extends ReturnType<typeof AccountRecord> { interface Account extends ReturnType<typeof AccountRecord> {
@ -68,6 +70,7 @@ export {
Notification, Notification,
Poll, Poll,
PollOption, PollOption,
Relationship,
Status, Status,
StatusEdit, StatusEdit,

View File

@ -1,4 +1,6 @@
import { fromJS } from 'immutable'; import { List as ImmutableList, Map as ImmutableMap, fromJS } from 'immutable';
import { normalizeStatus } from 'soapbox/normalizers';
import { import {
sortEmoji, sortEmoji,
@ -11,7 +13,7 @@ import {
simulateUnEmojiReact, simulateUnEmojiReact,
} from '../emoji_reacts'; } from '../emoji_reacts';
const ALLOWED_EMOJI = fromJS([ const ALLOWED_EMOJI = ImmutableList([
'👍', '👍',
'❤', '❤',
'😂', '😂',
@ -30,7 +32,7 @@ describe('filterEmoji', () => {
{ 'count': 1, 'me': true, 'name': '😡' }, { 'count': 1, 'me': true, 'name': '😡' },
{ 'count': 1, 'me': true, 'name': '🔪' }, { 'count': 1, 'me': true, 'name': '🔪' },
{ 'count': 1, 'me': true, 'name': '😠' }, { 'count': 1, 'me': true, 'name': '😠' },
]); ]) as ImmutableList<ImmutableMap<string, any>>;
it('filters only allowed emoji', () => { it('filters only allowed emoji', () => {
expect(filterEmoji(emojiReacts, ALLOWED_EMOJI)).toEqual(fromJS([ expect(filterEmoji(emojiReacts, ALLOWED_EMOJI)).toEqual(fromJS([
{ 'count': 1, 'me': true, 'name': '😂' }, { 'count': 1, 'me': true, 'name': '😂' },
@ -49,7 +51,7 @@ describe('sortEmoji', () => {
{ 'count': 20, 'me': true, 'name': '👍' }, { 'count': 20, 'me': true, 'name': '👍' },
{ 'count': 7, 'me': true, 'name': '😂' }, { 'count': 7, 'me': true, 'name': '😂' },
{ 'count': 15, 'me': true, 'name': '❤' }, { 'count': 15, 'me': true, 'name': '❤' },
]); ]) as ImmutableList<ImmutableMap<string, any>>;
it('sorts the emoji by count', () => { it('sorts the emoji by count', () => {
expect(sortEmoji(emojiReacts)).toEqual(fromJS([ expect(sortEmoji(emojiReacts)).toEqual(fromJS([
{ 'count': 20, 'me': true, 'name': '👍' }, { 'count': 20, 'me': true, 'name': '👍' },
@ -72,7 +74,7 @@ describe('mergeEmojiFavourites', () => {
{ 'count': 20, 'me': false, 'name': '👍' }, { 'count': 20, 'me': false, 'name': '👍' },
{ 'count': 15, 'me': false, 'name': '❤' }, { 'count': 15, 'me': false, 'name': '❤' },
{ 'count': 7, 'me': false, 'name': '😯' }, { 'count': 7, 'me': false, 'name': '😯' },
]); ]) as ImmutableList<ImmutableMap<string, any>>;
it('combines 👍 reacts with favourites', () => { it('combines 👍 reacts with favourites', () => {
expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([ expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([
{ 'count': 32, 'me': true, 'name': '👍' }, { 'count': 32, 'me': true, 'name': '👍' },
@ -86,7 +88,7 @@ describe('mergeEmojiFavourites', () => {
const emojiReacts = fromJS([ const emojiReacts = fromJS([
{ 'count': 15, 'me': false, 'name': '❤' }, { 'count': 15, 'me': false, 'name': '❤' },
{ 'count': 7, 'me': false, 'name': '😯' }, { 'count': 7, 'me': false, 'name': '😯' },
]); ]) as ImmutableList<ImmutableMap<string, any>>;
it('adds 👍 reacts to the map equaling favourite count', () => { it('adds 👍 reacts to the map equaling favourite count', () => {
expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([ expect(mergeEmojiFavourites(emojiReacts, favouritesCount, favourited)).toEqual(fromJS([
{ 'count': 15, 'me': false, 'name': '❤' }, { 'count': 15, 'me': false, 'name': '❤' },
@ -116,7 +118,7 @@ describe('reduceEmoji', () => {
{ 'count': 15, 'me': true, 'name': '❤' }, { 'count': 15, 'me': true, 'name': '❤' },
{ 'count': 1, 'me': false, 'name': '👀' }, { 'count': 1, 'me': false, 'name': '👀' },
{ 'count': 1, 'me': false, 'name': '🍩' }, { 'count': 1, 'me': false, 'name': '🍩' },
]); ]) as ImmutableList<ImmutableMap<string, any>>;
it('sorts, filters, and combines emoji and favourites', () => { it('sorts, filters, and combines emoji and favourites', () => {
expect(reduceEmoji(emojiReacts, 7, true, ALLOWED_EMOJI)).toEqual(fromJS([ expect(reduceEmoji(emojiReacts, 7, true, ALLOWED_EMOJI)).toEqual(fromJS([
{ 'count': 27, 'me': true, 'name': '👍' }, { 'count': 27, 'me': true, 'name': '👍' },
@ -138,7 +140,7 @@ describe('oneEmojiPerAccount', () => {
{ 'count': 2, 'me': true, 'name': '❤', accounts: [{ id: '1' }, { id: '2' }] }, { 'count': 2, 'me': true, 'name': '❤', accounts: [{ id: '1' }, { id: '2' }] },
{ 'count': 1, 'me': true, 'name': '😯', accounts: [{ id: '1' }] }, { 'count': 1, 'me': true, 'name': '😯', accounts: [{ id: '1' }] },
{ 'count': 1, 'me': false, 'name': '😂', accounts: [{ id: '3' }] }, { 'count': 1, 'me': false, 'name': '😂', accounts: [{ id: '3' }] },
]); ]) as ImmutableList<ImmutableMap<string, any>>;
expect(oneEmojiPerAccount(emojiReacts, '1')).toEqual(fromJS([ expect(oneEmojiPerAccount(emojiReacts, '1')).toEqual(fromJS([
{ 'count': 2, 'me': true, 'name': '👍', accounts: [{ id: '1' }, { id: '2' }] }, { 'count': 2, 'me': true, 'name': '👍', accounts: [{ id: '1' }, { id: '2' }] },
{ 'count': 1, 'me': false, 'name': '😂', accounts: [{ id: '3' }] }, { 'count': 1, 'me': false, 'name': '😂', accounts: [{ id: '3' }] },
@ -148,7 +150,7 @@ describe('oneEmojiPerAccount', () => {
describe('getReactForStatus', () => { describe('getReactForStatus', () => {
it('returns a single owned react (including favourite) for the status', () => { it('returns a single owned react (including favourite) for the status', () => {
const status = fromJS({ const status = normalizeStatus(fromJS({
favourited: false, favourited: false,
pleroma: { pleroma: {
emoji_reactions: [ emoji_reactions: [
@ -158,27 +160,27 @@ describe('getReactForStatus', () => {
{ 'count': 7, 'me': false, 'name': '😂' }, { 'count': 7, 'me': false, 'name': '😂' },
], ],
}, },
}); }));
expect(getReactForStatus(status, ALLOWED_EMOJI)).toEqual('❤'); expect(getReactForStatus(status, ALLOWED_EMOJI)).toEqual('❤');
}); });
it('returns a thumbs-up for a favourite', () => { it('returns a thumbs-up for a favourite', () => {
const status = fromJS({ favourites_count: 1, favourited: true }); const status = normalizeStatus(fromJS({ favourites_count: 1, favourited: true }));
expect(getReactForStatus(status)).toEqual('👍'); expect(getReactForStatus(status)).toEqual('👍');
}); });
it('returns undefined when a status has no reacts (or favourites)', () => { it('returns undefined when a status has no reacts (or favourites)', () => {
const status = fromJS({}); const status = normalizeStatus(fromJS({}));
expect(getReactForStatus(status)).toEqual(undefined); expect(getReactForStatus(status)).toEqual(undefined);
}); });
it('returns undefined when a status has no valid reacts (or favourites)', () => { it('returns undefined when a status has no valid reacts (or favourites)', () => {
const status = fromJS([ const status = normalizeStatus(fromJS([
{ 'count': 1, 'me': true, 'name': '🔪' }, { 'count': 1, 'me': true, 'name': '🔪' },
{ 'count': 1, 'me': true, 'name': '🌵' }, { 'count': 1, 'me': true, 'name': '🌵' },
{ 'count': 1, 'me': false, 'name': '👀' }, { 'count': 1, 'me': false, 'name': '👀' },
{ 'count': 1, 'me': false, 'name': '🍩' }, { 'count': 1, 'me': false, 'name': '🍩' },
]); ]));
expect(getReactForStatus(status)).toEqual(undefined); expect(getReactForStatus(status)).toEqual(undefined);
}); });
}); });
@ -188,7 +190,7 @@ describe('simulateEmojiReact', () => {
const emojiReacts = fromJS([ const emojiReacts = fromJS([
{ 'count': 2, 'me': false, 'name': '👍' }, { 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' }, { 'count': 2, 'me': false, 'name': '❤' },
]); ]) as ImmutableList<ImmutableMap<string, any>>;
expect(simulateEmojiReact(emojiReacts, '❤')).toEqual(fromJS([ expect(simulateEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
{ 'count': 2, 'me': false, 'name': '👍' }, { 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 3, 'me': true, 'name': '❤' }, { 'count': 3, 'me': true, 'name': '❤' },
@ -199,7 +201,7 @@ describe('simulateEmojiReact', () => {
const emojiReacts = fromJS([ const emojiReacts = fromJS([
{ 'count': 2, 'me': false, 'name': '👍' }, { 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' }, { 'count': 2, 'me': false, 'name': '❤' },
]); ]) as ImmutableList<ImmutableMap<string, any>>;
expect(simulateEmojiReact(emojiReacts, '😯')).toEqual(fromJS([ expect(simulateEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
{ 'count': 2, 'me': false, 'name': '👍' }, { 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' }, { 'count': 2, 'me': false, 'name': '❤' },
@ -213,7 +215,7 @@ describe('simulateUnEmojiReact', () => {
const emojiReacts = fromJS([ const emojiReacts = fromJS([
{ 'count': 2, 'me': false, 'name': '👍' }, { 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 3, 'me': true, 'name': '❤' }, { 'count': 3, 'me': true, 'name': '❤' },
]); ]) as ImmutableList<ImmutableMap<string, any>>;
expect(simulateUnEmojiReact(emojiReacts, '❤')).toEqual(fromJS([ expect(simulateUnEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
{ 'count': 2, 'me': false, 'name': '👍' }, { 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' }, { 'count': 2, 'me': false, 'name': '❤' },
@ -225,7 +227,7 @@ describe('simulateUnEmojiReact', () => {
{ 'count': 2, 'me': false, 'name': '👍' }, { 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' }, { 'count': 2, 'me': false, 'name': '❤' },
{ 'count': 1, 'me': true, 'name': '😯' }, { 'count': 1, 'me': true, 'name': '😯' },
]); ]) as ImmutableList<ImmutableMap<string, any>>;
expect(simulateUnEmojiReact(emojiReacts, '😯')).toEqual(fromJS([ expect(simulateUnEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
{ 'count': 2, 'me': false, 'name': '👍' }, { 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' }, { 'count': 2, 'me': false, 'name': '❤' },

View File

@ -1,71 +1,73 @@
import { fromJS } from 'immutable'; import { fromJS } from 'immutable';
import { normalizeStatus } from 'soapbox/normalizers/status';
import { shouldFilter } from '../timelines'; import { shouldFilter } from '../timelines';
describe('shouldFilter', () => { describe('shouldFilter', () => {
it('returns false under normal circumstances', () => { it('returns false under normal circumstances', () => {
const columnSettings = fromJS({}); const columnSettings = fromJS({});
const status = fromJS({}); const status = normalizeStatus(fromJS({}));
expect(shouldFilter(status, columnSettings)).toBe(false); expect(shouldFilter(status, columnSettings)).toBe(false);
}); });
it('reblog: returns true when `shows.reblog == false`', () => { it('reblog: returns true when `shows.reblog == false`', () => {
const columnSettings = fromJS({ shows: { reblog: false } }); const columnSettings = fromJS({ shows: { reblog: false } });
const status = fromJS({ reblog: {} }); const status = normalizeStatus(fromJS({ reblog: {} }));
expect(shouldFilter(status, columnSettings)).toBe(true); expect(shouldFilter(status, columnSettings)).toBe(true);
}); });
it('reblog: returns false when `shows.reblog == true`', () => { it('reblog: returns false when `shows.reblog == true`', () => {
const columnSettings = fromJS({ shows: { reblog: true } }); const columnSettings = fromJS({ shows: { reblog: true } });
const status = fromJS({ reblog: {} }); const status = normalizeStatus(fromJS({ reblog: {} }));
expect(shouldFilter(status, columnSettings)).toBe(false); expect(shouldFilter(status, columnSettings)).toBe(false);
}); });
it('reply: returns true when `shows.reply == false`', () => { it('reply: returns true when `shows.reply == false`', () => {
const columnSettings = fromJS({ shows: { reply: false } }); const columnSettings = fromJS({ shows: { reply: false } });
const status = fromJS({ in_reply_to_id: '1234' }); const status = normalizeStatus(fromJS({ in_reply_to_id: '1234' }));
expect(shouldFilter(status, columnSettings)).toBe(true); expect(shouldFilter(status, columnSettings)).toBe(true);
}); });
it('reply: returns false when `shows.reply == true`', () => { it('reply: returns false when `shows.reply == true`', () => {
const columnSettings = fromJS({ shows: { reply: true } }); const columnSettings = fromJS({ shows: { reply: true } });
const status = fromJS({ in_reply_to_id: '1234' }); const status = normalizeStatus(fromJS({ in_reply_to_id: '1234' }));
expect(shouldFilter(status, columnSettings)).toBe(false); expect(shouldFilter(status, columnSettings)).toBe(false);
}); });
it('direct: returns true when `shows.direct == false`', () => { it('direct: returns true when `shows.direct == false`', () => {
const columnSettings = fromJS({ shows: { direct: false } }); const columnSettings = fromJS({ shows: { direct: false } });
const status = fromJS({ visibility: 'direct' }); const status = normalizeStatus(fromJS({ visibility: 'direct' }));
expect(shouldFilter(status, columnSettings)).toBe(true); expect(shouldFilter(status, columnSettings)).toBe(true);
}); });
it('direct: returns false when `shows.direct == true`', () => { it('direct: returns false when `shows.direct == true`', () => {
const columnSettings = fromJS({ shows: { direct: true } }); const columnSettings = fromJS({ shows: { direct: true } });
const status = fromJS({ visibility: 'direct' }); const status = normalizeStatus(fromJS({ visibility: 'direct' }));
expect(shouldFilter(status, columnSettings)).toBe(false); expect(shouldFilter(status, columnSettings)).toBe(false);
}); });
it('direct: returns false for a public post when `shows.direct == false`', () => { it('direct: returns false for a public post when `shows.direct == false`', () => {
const columnSettings = fromJS({ shows: { direct: false } }); const columnSettings = fromJS({ shows: { direct: false } });
const status = fromJS({ visibility: 'public' }); const status = normalizeStatus(fromJS({ visibility: 'public' }));
expect(shouldFilter(status, columnSettings)).toBe(false); expect(shouldFilter(status, columnSettings)).toBe(false);
}); });
it('multiple settings', () => { it('multiple settings', () => {
const columnSettings = fromJS({ shows: { reblog: false, reply: false, direct: false } }); const columnSettings = fromJS({ shows: { reblog: false, reply: false, direct: false } });
const status = fromJS({ reblog: null, in_reply_to_id: null, visibility: 'direct' }); const status = normalizeStatus(fromJS({ reblog: null, in_reply_to_id: null, visibility: 'direct' }));
expect(shouldFilter(status, columnSettings)).toBe(true); expect(shouldFilter(status, columnSettings)).toBe(true);
}); });
it('multiple settings', () => { it('multiple settings', () => {
const columnSettings = fromJS({ shows: { reblog: false, reply: true, direct: false } }); const columnSettings = fromJS({ shows: { reblog: false, reply: true, direct: false } });
const status = fromJS({ reblog: null, in_reply_to_id: '1234', visibility: 'public' }); const status = normalizeStatus(fromJS({ reblog: null, in_reply_to_id: '1234', visibility: 'public' }));
expect(shouldFilter(status, columnSettings)).toBe(false); expect(shouldFilter(status, columnSettings)).toBe(false);
}); });
it('multiple settings', () => { it('multiple settings', () => {
const columnSettings = fromJS({ shows: { reblog: true, reply: false, direct: true } }); const columnSettings = fromJS({ shows: { reblog: true, reply: false, direct: true } });
const status = fromJS({ reblog: {}, in_reply_to_id: '1234', visibility: 'direct' }); const status = normalizeStatus(fromJS({ reblog: {}, in_reply_to_id: '1234', visibility: 'direct' }));
expect(shouldFilter(status, columnSettings)).toBe(true); expect(shouldFilter(status, columnSettings)).toBe(true);
}); });
}); });

View File

@ -82,9 +82,9 @@ export const reduceEmoji = (emojiReacts: ImmutableList<EmojiReact>, favouritesCo
export const getReactForStatus = (status: any, allowedEmoji = ALLOWED_EMOJI): string | undefined => { export const getReactForStatus = (status: any, allowedEmoji = ALLOWED_EMOJI): string | undefined => {
const result = reduceEmoji( const result = reduceEmoji(
status.getIn(['pleroma', 'emoji_reactions'], ImmutableList()), status.pleroma.get('emoji_reactions', ImmutableList()),
status.get('favourites_count', 0), status.favourites_count || 0,
status.get('favourited'), status.favourited,
allowedEmoji, allowedEmoji,
).filter(e => e.get('me') === true) ).filter(e => e.get('me') === true)
.getIn([0, 'name']); .getIn([0, 'name']);

View File

@ -4,9 +4,9 @@ import type { Status as StatusEntity } from 'soapbox/types/entities';
export const shouldFilter = (status: StatusEntity, columnSettings: any) => { export const shouldFilter = (status: StatusEntity, columnSettings: any) => {
const shows = ImmutableMap({ const shows = ImmutableMap({
reblog: status.get('reblog') !== null, reblog: status.reblog !== null,
reply: status.get('in_reply_to_id') !== null, reply: status.in_reply_to_id !== null,
direct: status.get('visibility') === 'direct', direct: status.visibility === 'direct',
}); });
return shows.some((value, key) => { return shows.some((value, key) => {