Add normalizers, fix tests

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
environments/review-develop-3zknud/deployments/243^2
marcin mikołajczak 2022-06-08 23:43:27 +02:00
rodzic 6c45dcb109
commit a66c174c2d
11 zmienionych plików z 174 dodań i 92 usunięć

Wyświetl plik

@ -10,16 +10,14 @@ import { shortNumberFormat } from '../utils/numbers';
import Permalink from './permalink';
import { HStack, Stack, Text } from './ui';
import type { Hashtag as HashtagEntity } from 'soapbox/reducers/search';
import type { TrendingHashtag } from 'soapbox/reducers/trends';
import type { Tag } from 'soapbox/types/entities';
interface IHashtag {
hashtag: HashtagEntity | TrendingHashtag,
hashtag: Tag,
}
const Hashtag: React.FC<IHashtag> = ({ hashtag }) => {
const history = (hashtag as TrendingHashtag).history;
const count = Number(history?.get(0)?.accounts);
const count = Number(hashtag.history?.get(0)?.accounts);
const brandColor = useSelector((state) => getSoapboxConfig(state).brandColor);
return (
@ -29,7 +27,7 @@ const Hashtag: React.FC<IHashtag> = ({ hashtag }) => {
<Text tag='span' size='sm' weight='semibold'>#{hashtag.name}</Text>
</Permalink>
{history && (
{hashtag.history && (
<Text theme='muted' size='sm'>
<FormattedMessage
id='trends.count_by_accounts'
@ -43,12 +41,12 @@ const Hashtag: React.FC<IHashtag> = ({ hashtag }) => {
)}
</Stack>
{history && (
{hashtag.history && (
<div className='w-[40px]' data-testid='sparklines'>
<Sparklines
width={40}
height={28}
data={history.reverse().map((day) => +day.uses).toArray()}
data={hashtag.history.reverse().map((day) => +day.uses).toArray()}
>
<SparklinesCurve style={{ fill: 'none' }} color={brandColor} />
</Sparklines>

Wyświetl plik

@ -1,22 +1,26 @@
import { List as ImmutableList } from 'immutable';
import { List as ImmutableList, Record as ImmutableRecord } from 'immutable';
import React from 'react';
import { render, screen } from '../../../../jest/test-helpers';
import { normalizeTag } from '../../../../normalizers';
import TrendsPanel from '../trends-panel';
describe('<TrendsPanel />', () => {
it('renders trending hashtags', () => {
const store = {
trends: {
items: ImmutableList([{
name: 'hashtag 1',
history: ImmutableList([{
day: '1652745600',
uses: '294',
accounts: '180',
}]),
}]),
},
trends: ImmutableRecord({
items: ImmutableList([
normalizeTag({
name: 'hashtag 1',
history: [{
day: '1652745600',
uses: '294',
accounts: '180',
}],
}),
]),
isLoading: false,
})(),
};
render(<TrendsPanel limit={1} />, null, store);
@ -27,18 +31,19 @@ describe('<TrendsPanel />', () => {
it('renders multiple trends', () => {
const store = {
trends: {
trends: ImmutableRecord({
items: ImmutableList([
{
normalizeTag({
name: 'hashtag 1',
history: ImmutableList([{ accounts: [] }]),
},
{
}),
normalizeTag({
name: 'hashtag 2',
history: ImmutableList([{ accounts: [] }]),
},
}),
]),
},
isLoading: false,
})(),
};
render(<TrendsPanel limit={3} />, null, store);
@ -47,18 +52,19 @@ describe('<TrendsPanel />', () => {
it('respects the limit prop', () => {
const store = {
trends: {
trends: ImmutableRecord({
items: ImmutableList([
{
normalizeTag({
name: 'hashtag 1',
history: ImmutableList([{ accounts: [] }]),
},
{
history: [{ accounts: [] }],
}),
normalizeTag({
name: 'hashtag 2',
history: ImmutableList([{ accounts: [] }]),
},
history: [{ accounts: [] }],
}),
]),
},
isLoading: false,
})(),
};
render(<TrendsPanel limit={1} />, null, store);
@ -67,9 +73,10 @@ describe('<TrendsPanel />', () => {
it('renders empty', () => {
const store = {
trends: {
trends: ImmutableRecord({
items: ImmutableList([]),
},
isLoading: false,
})(),
};
render(<TrendsPanel limit={1} />, null, store);

Wyświetl plik

@ -1,5 +1,5 @@
import userEvent from '@testing-library/user-event';
import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
import { Map as ImmutableMap, Record as ImmutableRecord, Set as ImmutableSet } from 'immutable';
import React from 'react';
import { __stub } from 'soapbox/api';
@ -24,13 +24,13 @@ describe('<ReportModal />', () => {
avatar: 'test.jpg',
}),
}),
reports: ImmutableMap({
new: {
reports: ImmutableRecord({
new: ImmutableRecord({
account_id: '1',
status_ids: ImmutableSet(['1']),
rule_ids: ImmutableSet(),
},
}),
})(),
})(),
statuses: ImmutableMap({
'1': normalizeStatus(status),
}),

Wyświetl plik

@ -14,7 +14,7 @@ interface ITrendsPanel {
const TrendsPanel = ({ limit }: ITrendsPanel) => {
const dispatch = useDispatch();
const trends = useAppSelector((state) => state.trends.get('items'));
const trends = useAppSelector((state) => state.trends.items);
const sortedTrends = React.useMemo(() => {
return trends.sort((a, b) => {

Wyświetl plik

@ -0,0 +1,22 @@
/**
* History normalizer:
* Converts API daily usage history of a hashtag into our internal format.
* @see {@link https://docs.joinmastodon.org/entities/history/}
*/
import {
Map as ImmutableMap,
Record as ImmutableRecord,
fromJS,
} from 'immutable';
// https://docs.joinmastodon.org/entities/history/
export const HistoryRecord = ImmutableRecord({
accounts: '',
day: '',
uses: '',
});
export const normalizeHistory = (history: Record<string, any>) => {
return HistoryRecord(
ImmutableMap(fromJS(history)),
);
};

Wyświetl plik

@ -6,6 +6,7 @@ export { CardRecord, normalizeCard } from './card';
export { ChatRecord, normalizeChat } from './chat';
export { ChatMessageRecord, normalizeChatMessage } from './chat_message';
export { EmojiRecord, normalizeEmoji } from './emoji';
export { HistoryRecord, normalizeHistory } from './history';
export { InstanceRecord, normalizeInstance } from './instance';
export { ListRecord, normalizeList } from './list';
export { MentionRecord, normalizeMention } from './mention';
@ -14,5 +15,6 @@ export { PollRecord, PollOptionRecord, normalizePoll } from './poll';
export { RelationshipRecord, normalizeRelationship } from './relationship';
export { StatusRecord, normalizeStatus } from './status';
export { StatusEditRecord, normalizeStatusEdit } from './status_edit';
export { TagRecord, normalizeTag } from './tag';
export { SoapboxConfigRecord, normalizeSoapboxConfig } from './soapbox/soapbox_config';

Wyświetl plik

@ -0,0 +1,40 @@
/**
* Tag normalizer:
* Converts API tags into our internal format.
* @see {@link https://docs.joinmastodon.org/entities/tag/}
*/
import {
List as ImmutableList,
Map as ImmutableMap,
Record as ImmutableRecord,
fromJS,
} from 'immutable';
import { normalizeHistory } from './history';
import type { History } from 'soapbox/types/entities';
// https://docs.joinmastodon.org/entities/tag/
export const TagRecord = ImmutableRecord({
name: '',
url: '',
history: null as ImmutableList<History> | null,
});
const normalizeHistoryList = (tag: ImmutableMap<string, any>) => {
if (tag.get('history')){
return tag.update('history', ImmutableList(), attachments => {
return attachments.map(normalizeHistory);
});
} else {
return tag.set('history', null);
}
};
export const normalizeTag = (tag: Record<string, any>) => {
return TagRecord(
ImmutableMap(fromJS(tag)).withMutations(tag => {
normalizeHistoryList(tag);
}),
);
};

Wyświetl plik

@ -1,4 +1,4 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { Map as ImmutableMap, List as ImmutableList, Record as ImmutableRecord } from 'immutable';
import {
SEARCH_CHANGE,
@ -10,14 +10,24 @@ import reducer from '../search';
describe('search reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap({
expect(reducer(undefined, {}).toJS()).toEqual({
value: '',
submitted: false,
submittedValue: '',
hidden: false,
results: ImmutableMap(),
results: {
accounts: [],
statuses: [],
hashtags: [],
accountsHasMore: false,
statusesHasMore: false,
hashtagsHasMore: false,
accountsLoaded: false,
statusesLoaded: false,
hashtagsLoaded: false,
},
filter: 'accounts',
}));
});
});
describe('SEARCH_CHANGE', () => {
@ -30,42 +40,54 @@ describe('search reducer', () => {
describe('SEARCH_CLEAR', () => {
it('resets the state', () => {
const state = ImmutableMap({
const state = ImmutableRecord({
value: 'hello world',
submitted: true,
submittedValue: 'hello world',
hidden: false,
results: ImmutableMap(),
results: ImmutableRecord({})(),
filter: 'statuses',
});
})();
const action = { type: SEARCH_CLEAR };
const expected = ImmutableMap({
const expected = {
value: '',
submitted: false,
submittedValue: '',
hidden: false,
results: ImmutableMap(),
results: {
accounts: [],
statuses: [],
hashtags: [],
accountsHasMore: false,
statusesHasMore: false,
hashtagsHasMore: false,
accountsLoaded: false,
statusesLoaded: false,
hashtagsLoaded: false,
},
filter: 'accounts',
});
};
expect(reducer(state, action)).toEqual(expected);
expect(reducer(state, action).toJS()).toEqual(expected);
});
});
describe(SEARCH_EXPAND_SUCCESS, () => {
it('imports hashtags as maps', () => {
const state = ImmutableMap({
const state = ImmutableRecord({
value: 'artist',
submitted: true,
submittedValue: 'artist',
hidden: false,
results: ImmutableMap({
results: ImmutableRecord({
hashtags: ImmutableList(),
}),
hashtagsHasMore: false,
hashtagsLoaded: true,
})(),
filter: 'hashtags',
});
})();
const action = {
type: SEARCH_EXPAND_SUCCESS,
@ -82,24 +104,26 @@ describe('search reducer', () => {
searchType: 'hashtags',
};
const expected = ImmutableMap({
const expected = {
value: 'artist',
submitted: true,
submittedValue: 'artist',
hidden: false,
results: ImmutableMap({
hashtags: fromJS([{
name: 'artist',
url: 'https://gleasonator.com/tags/artist',
history: [],
}]),
results: {
hashtags: [
{
name: 'artist',
url: 'https://gleasonator.com/tags/artist',
history: [],
},
],
hashtagsHasMore: false,
hashtagsLoaded: true,
}),
},
filter: 'hashtags',
});
};
expect(reducer(state, action)).toEqual(expected);
expect(reducer(state, action).toJS()).toEqual(expected);
});
});
});

Wyświetl plik

@ -1,6 +1,6 @@
import { OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord, fromJS } from 'immutable';
import { APIEntity } from 'soapbox/types/entities';
import { normalizeTag } from 'soapbox/normalizers';
import {
COMPOSE_MENTION,
@ -20,16 +20,12 @@ import {
} from '../actions/search';
import type { AnyAction } from 'redux';
const HashtagRecord = ImmutableRecord({
name: '',
url: '',
});
import type { APIEntity, Tag } from 'soapbox/types/entities';
const ResultsRecord = ImmutableRecord({
accounts: ImmutableOrderedSet<string>(),
statuses: ImmutableOrderedSet<string>(),
hashtags: ImmutableOrderedSet<Hashtag>(), // it's a list of maps
hashtags: ImmutableOrderedSet<Tag>(), // it's a list of maps
accountsHasMore: false,
statusesHasMore: false,
hashtagsHasMore: false,
@ -49,7 +45,6 @@ const ReducerRecord = ImmutableRecord({
type State = ReturnType<typeof ReducerRecord>;
type APIEntities = Array<APIEntity>;
export type Hashtag = ReturnType<typeof HashtagRecord>;
export type SearchFilter = 'accounts' | 'statuses' | 'hashtags';
const toIds = (items: APIEntities) => {
@ -62,7 +57,7 @@ const importResults = (state: State, results: APIEntity, searchTerm: string, sea
state.set('results', ResultsRecord({
accounts: toIds(results.accounts),
statuses: toIds(results.statuses),
hashtags: ImmutableOrderedSet(results.hashtags.map(HashtagRecord)), // it's a list of maps
hashtags: ImmutableOrderedSet(results.hashtags.map(normalizeTag)), // it's a list of records
accountsHasMore: results.accounts.length >= 20,
statusesHasMore: results.statuses.length >= 20,
hashtagsHasMore: results.hashtags.length >= 20,

Wyświetl plik

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

Wyświetl plik

@ -8,6 +8,7 @@ import {
ChatMessageRecord,
EmojiRecord,
FieldRecord,
HistoryRecord,
InstanceRecord,
ListRecord,
MentionRecord,
@ -17,6 +18,7 @@ import {
RelationshipRecord,
StatusEditRecord,
StatusRecord,
TagRecord,
} from 'soapbox/normalizers';
import type { Record as ImmutableRecord } from 'immutable';
@ -29,6 +31,7 @@ type Chat = ReturnType<typeof ChatRecord>;
type ChatMessage = ReturnType<typeof ChatMessageRecord>;
type Emoji = ReturnType<typeof EmojiRecord>;
type Field = ReturnType<typeof FieldRecord>;
type History = ReturnType<typeof HistoryRecord>;
type Instance = ReturnType<typeof InstanceRecord>;
type List = ReturnType<typeof ListRecord>;
type Mention = ReturnType<typeof MentionRecord>;
@ -37,6 +40,7 @@ type Poll = ReturnType<typeof PollRecord>;
type PollOption = ReturnType<typeof PollOptionRecord>;
type Relationship = ReturnType<typeof RelationshipRecord>;
type StatusEdit = ReturnType<typeof StatusEditRecord>;
type Tag = ReturnType<typeof TagRecord>;
interface Account extends ReturnType<typeof AccountRecord> {
// HACK: we can't do a circular reference in the Record definition itself,
@ -64,6 +68,7 @@ export {
ChatMessage,
Emoji,
Field,
History,
Instance,
List,
Mention,
@ -73,6 +78,7 @@ export {
Relationship,
Status,
StatusEdit,
Tag,
// Utility types
APIEntity,