Merge branch 'reducer-records' into 'develop'

Refactor some reducers with Immutable.Record

See merge request soapbox-pub/soapbox-fe!1093
next-old
Alex Gleason 2022-03-11 21:01:15 +00:00
commit 6be0b61569
14 zmienionych plików z 600 dodań i 724 usunięć

Wyświetl plik

@ -0,0 +1,17 @@
import { Record as ImmutableRecord, fromJS } from 'immutable';
import { normalizeNotification } from '../notification';
describe('normalizeNotification()', () => {
it('normalizes an empty map', () => {
const notification = fromJS({});
const result = normalizeNotification(notification);
expect(ImmutableRecord.isRecord(result)).toBe(true);
expect(result.type).toEqual('');
expect(result.account).toBe(null);
expect(result.target).toBe(null);
expect(result.status).toBe(null);
expect(result.id).toEqual('');
});
});

Wyświetl plik

@ -60,7 +60,7 @@ const InstanceRecord = ImmutableRecord({
});
// Build Mastodon configuration from Pleroma instance
const pleromaToMastodonConfig = instance => {
const pleromaToMastodonConfig = (instance: ImmutableMap<string, any>) => {
return ImmutableMap({
statuses: ImmutableMap({
max_characters: instance.get('max_toot_chars'),
@ -75,10 +75,10 @@ const pleromaToMastodonConfig = instance => {
};
// Get the software's default attachment limit
const getAttachmentLimit = software => software === PLEROMA ? Infinity : 4;
const getAttachmentLimit = (software: string) => software === PLEROMA ? Infinity : 4;
// Normalize instance (Pleroma, Mastodon, etc.) to Mastodon's format
export const normalizeInstance = instance => {
export const normalizeInstance = (instance: ImmutableMap<string, any>) => {
const { software } = parseVersion(instance.get('version'));
const mastodonConfig = pleromaToMastodonConfig(instance);

Wyświetl plik

@ -0,0 +1,20 @@
import {
Map as ImmutableMap,
Record as ImmutableRecord,
} from 'immutable';
// https://docs.joinmastodon.org/entities/notification/
const NotificationRecord = ImmutableRecord({
account: null,
chat_message: null, // pleroma:chat_mention
created_at: new Date(),
emoji: null, // pleroma:emoji_reaction
id: '',
status: null,
target: null, // move
type: '',
});
export const normalizeNotification = (notification: ImmutableMap<string, any>) => {
return NotificationRecord(notification);
};

Wyświetl plik

@ -11,7 +11,7 @@ import { IStatus } from 'soapbox/types';
import { mergeDefined, makeEmojiMap } from 'soapbox/utils/normalizers';
const StatusRecord = ImmutableRecord({
account: ImmutableMap(),
account: null,
application: null,
bookmarked: false,
card: null,

Wyświetl plik

@ -1,21 +1,11 @@
import {
Map as ImmutableMap,
List as ImmutableList,
OrderedSet as ImmutableOrderedSet,
} from 'immutable';
import { Record as ImmutableRecord } from 'immutable';
import reducer from '../admin';
describe('admin reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap({
reports: ImmutableMap(),
openReports: ImmutableOrderedSet(),
users: ImmutableMap(),
latestUsers: ImmutableOrderedSet(),
awaitingApproval: ImmutableOrderedSet(),
configs: ImmutableList(),
needsReboot: false,
}));
const result = reducer(undefined, {});
expect(ImmutableRecord.isRecord(result)).toBe(true);
expect(result.needsReboot).toBe(false);
});
});

Wyświetl plik

@ -1,6 +1,11 @@
import { List as ImmutableList } from 'immutable';
import { Record as ImmutableRecord, List as ImmutableList } from 'immutable';
import * as actions from 'soapbox/actions/alerts';
import {
ALERT_SHOW,
ALERT_DISMISS,
ALERT_CLEAR,
} from 'soapbox/actions/alerts';
import { applyActions } from 'soapbox/test_helpers';
import reducer from '../alerts';
@ -9,66 +14,65 @@ describe('alerts reducer', () => {
expect(reducer(undefined, {})).toEqual(ImmutableList());
});
it('should handle ALERT_SHOW', () => {
const state = ImmutableList([]);
const action = {
type: actions.ALERT_SHOW,
title: 'alert_title',
message: 'this is an alert message',
};
expect(reducer(state, action).toJS()).toMatchObject([
{
describe('ALERT_SHOW', () => {
it('imports the alert', () => {
const action = {
type: ALERT_SHOW,
title: 'alert_title',
message: 'this is an alert message',
};
const expected = [{
key: 0,
message: 'this is an alert message',
title: 'alert_title',
},
]);
});
}];
// it('should handle ALERT_DISMISS', () => {
// const state = ImmutableList([
// {
// key: 0,
// message: 'message_1',
// title: 'title_1',
// },
// {
// key: 1,
// message: 'message_2',
// title: 'title_2',
// },
// ]);
// const action = {
// type: actions.ALERT_DISMISS,
// alert: { key: 0 },
// };
// expect(reducer(state, action).toJS()).toMatchObject([
// {
// key: 1,
// message: 'message_2',
// title: 'title_2',
// }
// ]);
// });
it('should handle ALERT_CLEAR', () => {
const state = ImmutableList([
{
key: 0,
message: 'message_1',
title: 'title_1',
},
{
key: 1,
message: 'message_2',
title: 'title_2',
},
]);
const action = {
type: actions.ALERT_CLEAR,
};
expect(reducer(state, action).toJS()).toMatchObject({
const result = reducer(undefined, action);
expect(ImmutableRecord.isRecord(result.get(0))).toBe(true);
expect(result.toJS()).toMatchObject(expected);
});
});
describe('ALERT_CLEAR', () => {
it('deletes the alerts', () => {
const actions = [{
type: ALERT_SHOW,
title: 'Oops!',
message: 'Server is down',
}, {
type: ALERT_SHOW,
title: 'Uh-oh!',
message: 'Shit done fucked up',
}, {
type: ALERT_CLEAR,
}];
const result = applyActions(undefined, actions, reducer);
expect(result.isEmpty()).toBe(true);
});
});
describe('ALERT_DISMISS', () => {
it('deletes an individual alert', () => {
const actions = [{
type: ALERT_SHOW,
title: 'Oops!',
message: 'Server is down',
}, {
type: ALERT_SHOW,
title: 'Uh-oh!',
message: 'Shit done fucked up',
}, {
type: ALERT_DISMISS,
alert: {
key: 0,
},
}];
const result = applyActions(undefined, actions, reducer);
expect(result.size).toEqual(1);
expect(result.get(0).key).toEqual(1);
});
});
});

Wyświetl plik

@ -1,9 +1,11 @@
import { Map as ImmutableMap } from 'immutable';
import { Record as ImmutableRecord } from 'immutable';
import reducer from '../meta';
describe('meta reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap());
const result = reducer(undefined, {});
expect(ImmutableRecord.isRecord(result)).toBe(true);
expect(result.instance_fetch_failed).toBe(false);
});
});

Wyświetl plik

@ -2,6 +2,7 @@ import {
Map as ImmutableMap,
List as ImmutableList,
Set as ImmutableSet,
Record as ImmutableRecord,
OrderedSet as ImmutableOrderedSet,
fromJS,
is,
@ -20,7 +21,7 @@ import {
ADMIN_USERS_APPROVE_SUCCESS,
} from '../actions/admin';
const initialState = ImmutableMap({
const ReducerRecord = ImmutableRecord({
reports: ImmutableMap(),
openReports: ImmutableOrderedSet(),
users: ImmutableMap(),
@ -126,7 +127,7 @@ function handleReportDiffs(state, reports) {
});
}
export default function admin(state = initialState, action) {
export default function admin(state = ReducerRecord(), action) {
switch(action.type) {
case ADMIN_CONFIG_FETCH_SUCCESS:
case ADMIN_CONFIG_UPDATE_SUCCESS:

Wyświetl plik

@ -1,12 +1,13 @@
import {
Map as ImmutableMap,
Record as ImmutableRecord,
OrderedSet as ImmutableOrderedSet,
fromJS,
} from 'immutable';
import { ADMIN_LOG_FETCH_SUCCESS } from 'soapbox/actions/admin';
const initialState = ImmutableMap({
const ReducerRecord = ImmutableRecord({
items: ImmutableMap(),
index: ImmutableOrderedSet(),
total: 0,
@ -34,7 +35,7 @@ const importItems = (state, items, total) => {
});
};
export default function admin_log(state = initialState, action) {
export default function admin_log(state = ReducerRecord(), action) {
switch(action.type) {
case ADMIN_LOG_FETCH_SUCCESS:
return importItems(state, action.items, action.total);

Wyświetl plik

@ -1,4 +1,4 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { Record as ImmutableRecord, List as ImmutableList } from 'immutable';
import {
ALERT_SHOW,
@ -6,21 +6,38 @@ import {
ALERT_CLEAR,
} from '../actions/alerts';
const initialState = ImmutableList([]);
const AlertRecord = ImmutableRecord({
key: 0,
title: '',
message: '',
severity: 'info',
actionLabel: '',
actionLink: '',
});
const initialState = ImmutableList();
// Get next key based on last alert
const getNextKey = state => state.size > 0 ? state.last().get('key') + 1 : 0;
// Import the alert
const importAlert = (state, alert) => {
const key = getNextKey(state);
const record = AlertRecord({ ...alert, key });
return state.push(record);
};
// Delete an alert by its key
const deleteAlert = (state, alert) => {
return state.filterNot(item => item.key === alert.key);
};
export default function alerts(state = initialState, action) {
switch(action.type) {
case ALERT_SHOW:
return state.push(ImmutableMap({
key: state.size > 0 ? state.last().get('key') + 1 : 0,
title: action.title,
message: action.message,
severity: action.severity || 'info',
actionLabel: action.actionLabel,
actionLink: action.actionLink,
}));
return importAlert(state, action);
case ALERT_DISMISS:
return state.filterNot(item => item.get('key') === action.alert.key);
return deleteAlert(state, action.alert);
case ALERT_CLEAR:
return state.clear();
default:

Wyświetl plik

@ -1,12 +1,14 @@
'use strict';
import { Map as ImmutableMap } from 'immutable';
import { Record as ImmutableRecord } from 'immutable';
import { INSTANCE_FETCH_FAIL } from 'soapbox/actions/instance';
const initialState = ImmutableMap();
const ReducerRecord = ImmutableRecord({
instance_fetch_failed: false,
});
export default function meta(state = initialState, action) {
export default function meta(state = ReducerRecord(), action) {
switch(action.type) {
case INSTANCE_FETCH_FAIL:
return state.set('instance_fetch_failed', true);

Wyświetl plik

@ -1,10 +1,10 @@
import { Map as ImmutableMap, OrderedMap as ImmutableOrderedMap, fromJS } from 'immutable';
import {
MARKER_FETCH_SUCCESS,
MARKER_SAVE_REQUEST,
MARKER_SAVE_SUCCESS,
} from 'soapbox/actions/markers';
Record as ImmutableRecord,
OrderedMap as ImmutableOrderedMap,
fromJS,
} from 'immutable';
import { normalizeNotification } from 'soapbox/normalizers/notification';
import {
ACCOUNT_BLOCK_SUCCESS,
@ -12,6 +12,11 @@ import {
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
FOLLOW_REQUEST_REJECT_SUCCESS,
} from '../actions/accounts';
import {
MARKER_FETCH_SUCCESS,
MARKER_SAVE_REQUEST,
MARKER_SAVE_SUCCESS,
} from '../actions/markers';
import {
NOTIFICATIONS_UPDATE,
NOTIFICATIONS_EXPAND_SUCCESS,
@ -27,7 +32,7 @@ import {
} from '../actions/notifications';
import { TIMELINE_DELETE } from '../actions/timelines';
const initialState = ImmutableMap({
const ReducerRecord = ImmutableRecord({
items: ImmutableOrderedMap(),
hasMore: true,
top: false,
@ -48,16 +53,16 @@ const comparator = (a, b) => {
return 0;
};
const notificationToMap = notification => ImmutableMap({
id: notification.id,
type: notification.type,
account: notification.account.id,
target: notification.target ? notification.target.id : null,
created_at: notification.created_at,
status: notification.status ? notification.status.id : null,
emoji: notification.emoji,
chat_message: notification.chat_message,
});
const minifyNotification = notification => {
return notification.mergeWith((o, n) => n || o, {
account: notification.getIn(['account', 'id']),
status: notification.getIn(['status', 'id']),
});
};
const fixNotification = notification => {
return minifyNotification(normalizeNotification(notification));
};
const isValid = notification => {
try {
@ -88,7 +93,7 @@ const countFuture = (notifications, lastId) => {
}, 0);
};
const normalizeNotification = (state, notification) => {
const importNotification = (state, notification) => {
const top = state.get('top');
if (!top) state = state.update('unread', unread => unread + 1);
@ -98,7 +103,7 @@ const normalizeNotification = (state, notification) => {
map = map.take(20);
}
return map.set(notification.id, notificationToMap(notification)).sort(comparator);
return map.set(notification.id, fixNotification(notification)).sort(comparator);
});
};
@ -106,7 +111,7 @@ const processRawNotifications = notifications => (
ImmutableOrderedMap(
notifications
.filter(isValid)
.map(n => [n.id, notificationToMap(n)]),
.map(n => [n.id, fixNotification(n)]),
));
const expandNormalizedNotifications = (state, notifications, next) => {
@ -176,23 +181,23 @@ const importMarker = (state, marker) => {
});
};
export default function notifications(state = initialState, action) {
export default function notifications(state = ReducerRecord(), action) {
switch(action.type) {
case NOTIFICATIONS_EXPAND_REQUEST:
return state.set('isLoading', true);
case NOTIFICATIONS_EXPAND_FAIL:
return state.set('isLoading', false);
case NOTIFICATIONS_FILTER_SET:
return state.set('items', ImmutableOrderedMap()).set('hasMore', true);
return state.delete('items').set('hasMore', true);
case NOTIFICATIONS_SCROLL_TOP:
return updateTop(state, action.top);
case NOTIFICATIONS_UPDATE:
return normalizeNotification(state, action.notification);
return importNotification(state, action.notification);
case NOTIFICATIONS_UPDATE_QUEUE:
return updateNotificationsQueue(state, action.notification, action.intlMessages, action.intlLocale);
case NOTIFICATIONS_DEQUEUE:
return state.withMutations(mutable => {
mutable.set('queuedNotifications', ImmutableOrderedMap());
mutable.delete('queuedNotifications');
mutable.set('totalQueuedNotificationsCount', 0);
});
case NOTIFICATIONS_EXPAND_SUCCESS:
@ -205,7 +210,7 @@ export default function notifications(state = initialState, action) {
case FOLLOW_REQUEST_REJECT_SUCCESS:
return filterNotificationIds(state, [action.id], 'follow_request');
case NOTIFICATIONS_CLEAR:
return state.set('items', ImmutableOrderedMap()).set('hasMore', false);
return state.delete('items').set('hasMore', false);
case NOTIFICATIONS_MARK_READ_REQUEST:
return state.set('lastRead', action.lastRead);
case MARKER_FETCH_SUCCESS:
@ -214,16 +219,6 @@ export default function notifications(state = initialState, action) {
return importMarker(state, fromJS(action.marker));
case TIMELINE_DELETE:
return deleteByStatus(state, action.id);
// Disable for now
// https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/432
//
// case TIMELINE_DISCONNECT:
// // This is kind of a hack - `null` renders a LoadGap in the component
// // https://github.com/tootsuite/mastodon/pull/6886
// return action.timeline === 'home' ?
// state.update('items', items => items.first() ? ImmutableOrderedSet([null]).union(items) : items) :
// state;
default:
return state;
}

Wyświetl plik

@ -33,3 +33,8 @@ export const createComponent = (children, props = {}) => {
</Provider>,
);
};
// Apply actions to the state, one at a time
export const applyActions = (state, actions, reducer) => {
return actions.reduce((state, action) => reducer(state, action), state);
};