kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Add normalizers, fix tests
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>environments/review-develop-3zknud/deployments/243^2
rodzic
6c45dcb109
commit
a66c174c2d
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
}),
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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)),
|
||||
);
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}),
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
Ładowanie…
Reference in New Issue