Merge branch 'main' into captcha-modal

This commit is contained in:
danidfra 2024-10-09 20:09:19 -03:00
commit 2aa21874f7
34 changed files with 260 additions and 254 deletions

View File

@ -132,7 +132,6 @@
"react-error-boundary": "^4.0.11",
"react-helmet": "^6.1.0",
"react-hot-toast": "^2.4.0",
"react-immutable-pure-component": "^2.2.2",
"react-inlinesvg": "^4.0.0",
"react-intl": "^6.0.0",
"react-motion": "^0.5.2",
@ -147,7 +146,6 @@
"react-swipeable-views": "^0.14.0",
"react-virtuoso": "^4.10.4",
"redux": "^5.0.0",
"redux-immutable": "^4.0.0",
"redux-thunk": "^3.1.0",
"reselect": "^5.0.0",
"sass": "^1.69.5",

View File

@ -73,15 +73,17 @@ describe('fetchAccount()', () => {
avatar: 'test.jpg',
});
const state = rootState
.set('entities', {
const state = {
...rootState,
entities: {
'ACCOUNTS': {
store: {
[id]: account,
},
lists: {},
},
});
},
};
store = mockStore(state);
@ -170,15 +172,17 @@ describe('fetchAccountByUsername()', () => {
birthday: undefined,
});
state = rootState
.set('entities', {
state = {
...rootState,
entities: {
'ACCOUNTS': {
store: {
[id]: account,
},
lists: {},
},
});
},
};
store = mockStore(state);
@ -189,16 +193,19 @@ describe('fetchAccountByUsername()', () => {
describe('when "accountByUsername" feature is enabled', () => {
beforeEach(() => {
const state = rootState
.set('instance', buildInstance({
const state = {
...rootState,
me: '123',
instance: buildInstance({
version: '2.7.2 (compatible; Pleroma 2.4.52-1337-g4779199e.gleasonator+soapbox)',
pleroma: {
metadata: {
features: [],
},
},
}))
.set('me', '123');
}),
};
store = mockStore(state);
});
@ -252,16 +259,19 @@ describe('fetchAccountByUsername()', () => {
describe('when "accountLookup" feature is enabled', () => {
beforeEach(() => {
const state = rootState
.set('instance', buildInstance({
const state = {
...rootState,
me: '123',
instance: buildInstance({
version: '3.4.1 (compatible; TruthSocial 1.0.0)',
pleroma: {
metadata: {
features: [],
},
},
}))
.set('me', '123');
}),
};
store = mockStore(state);
});
@ -317,7 +327,7 @@ describe('fetchAccountByUsername()', () => {
describe('when using the accountSearch function', () => {
beforeEach(() => {
const state = rootState.set('me', '123');
const state = { ...rootState, me: '123' };
store = mockStore(state);
});
@ -384,7 +394,7 @@ describe('blockAccount()', () => {
describe('when logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -398,7 +408,7 @@ describe('blockAccount()', () => {
describe('when logged in', () => {
beforeEach(() => {
const state = rootState.set('me', '123');
const state = { ...rootState, me: '123' };
store = mockStore(state);
});
@ -451,7 +461,7 @@ describe('unblockAccount()', () => {
describe('when logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -465,7 +475,7 @@ describe('unblockAccount()', () => {
describe('when logged in', () => {
beforeEach(() => {
const state = rootState.set('me', '123');
const state = { ...rootState, me: '123' };
store = mockStore(state);
});
@ -517,7 +527,7 @@ describe('muteAccount()', () => {
describe('when logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -531,7 +541,7 @@ describe('muteAccount()', () => {
describe('when logged in', () => {
beforeEach(() => {
const state = rootState.set('me', '123');
const state = { ...rootState, me: '123' };
store = mockStore(state);
});
@ -584,7 +594,7 @@ describe('unmuteAccount()', () => {
describe('when logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -598,7 +608,7 @@ describe('unmuteAccount()', () => {
describe('when logged in', () => {
beforeEach(() => {
const state = rootState.set('me', '123');
const state = { ...rootState, me: '123' };
store = mockStore(state);
});
@ -650,7 +660,7 @@ describe('subscribeAccount()', () => {
describe('when logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -664,7 +674,7 @@ describe('subscribeAccount()', () => {
describe('when logged in', () => {
beforeEach(() => {
const state = rootState.set('me', '123');
const state = { ...rootState, me: '123' };
store = mockStore(state);
});
@ -716,7 +726,7 @@ describe('unsubscribeAccount()', () => {
describe('when logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -730,7 +740,7 @@ describe('unsubscribeAccount()', () => {
describe('when logged in', () => {
beforeEach(() => {
const state = rootState.set('me', '123');
const state = { ...rootState, me: '123' };
store = mockStore(state);
});
@ -782,7 +792,7 @@ describe('removeFromFollowers()', () => {
describe('when logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -796,7 +806,7 @@ describe('removeFromFollowers()', () => {
describe('when logged in', () => {
beforeEach(() => {
const state = rootState.set('me', '123');
const state = { ...rootState, me: '123' };
store = mockStore(state);
});
@ -848,7 +858,7 @@ describe('fetchFollowers()', () => {
describe('when logged in', () => {
beforeEach(() => {
const state = rootState.set('me', '123');
const state = { ...rootState, me: '123' };
store = mockStore(state);
});
@ -905,7 +915,7 @@ describe('expandFollowers()', () => {
describe('when logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -919,29 +929,35 @@ describe('expandFollowers()', () => {
describe('when logged in', () => {
beforeEach(() => {
const state = rootState
.set('user_lists', ReducerRecord({
const state = {
...rootState,
me: '123',
user_lists: ReducerRecord({
followers: ImmutableMap({
[id]: ListRecord({
next: 'next_url',
}),
}),
}))
.set('me', '123');
}),
};
store = mockStore(state);
});
describe('when the url is null', () => {
beforeEach(() => {
const state = rootState
.set('user_lists', ReducerRecord({
const state = {
...rootState,
me: '123',
user_lists: ReducerRecord({
followers: ImmutableMap({
[id]: ListRecord({
next: null,
}),
}),
}))
.set('me', '123');
}),
};
store = mockStore(state);
});
@ -1006,7 +1022,7 @@ describe('fetchFollowing()', () => {
describe('when logged in', () => {
beforeEach(() => {
const state = rootState.set('me', '123');
const state = { ...rootState, me: '123' };
store = mockStore(state);
});
@ -1063,7 +1079,7 @@ describe('expandFollowing()', () => {
describe('when logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -1077,29 +1093,35 @@ describe('expandFollowing()', () => {
describe('when logged in', () => {
beforeEach(() => {
const state = rootState
.set('user_lists', ReducerRecord({
const state = {
...rootState,
me: '123',
user_lists: ReducerRecord({
following: ImmutableMap({
[id]: ListRecord({
next: 'next_url',
}),
}),
}))
.set('me', '123');
}),
};
store = mockStore(state);
});
describe('when the url is null', () => {
beforeEach(() => {
const state = rootState
.set('user_lists', ReducerRecord({
const state = {
...rootState,
me: '123',
user_lists: ReducerRecord({
following: ImmutableMap({
[id]: ListRecord({
next: null,
}),
}),
}))
.set('me', '123');
}),
};
store = mockStore(state);
});
@ -1164,7 +1186,7 @@ describe('fetchRelationships()', () => {
describe('when logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -1178,16 +1200,18 @@ describe('fetchRelationships()', () => {
describe('when logged in', () => {
beforeEach(() => {
const state = rootState
.set('me', '123');
const state = { ...rootState, me: '123' };
store = mockStore(state);
});
describe('without newAccountIds', () => {
beforeEach(() => {
const state = rootState
.set('relationships', ImmutableMap({ [id]: buildRelationship() }))
.set('me', '123');
const state = {
...rootState,
me: '123',
relationships: ImmutableMap({ [id]: buildRelationship() }),
};
store = mockStore(state);
});
@ -1201,9 +1225,12 @@ describe('fetchRelationships()', () => {
describe('with a successful API request', () => {
beforeEach(() => {
const state = rootState
.set('relationships', ImmutableMap({}))
.set('me', '123');
const state = {
...rootState,
me: '123',
relationships: ImmutableMap(),
};
store = mockStore(state);
__stub((mock) => {
@ -1255,7 +1282,7 @@ describe('fetchRelationships()', () => {
describe('fetchFollowRequests()', () => {
describe('when logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -1269,16 +1296,18 @@ describe('fetchFollowRequests()', () => {
describe('when logged in', () => {
beforeEach(() => {
const state = rootState
.set('me', '123');
const state = { ...rootState, me: '123' };
store = mockStore(state);
});
describe('with a successful API request', () => {
beforeEach(() => {
const state = rootState
.set('relationships', ImmutableMap({}))
.set('me', '123');
const state = {
...rootState,
me: '123',
relationships: ImmutableMap(),
};
store = mockStore(state);
__stub((mock) => {
@ -1329,7 +1358,7 @@ describe('fetchFollowRequests()', () => {
describe('expandFollowRequests()', () => {
describe('when logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -1343,25 +1372,29 @@ describe('expandFollowRequests()', () => {
describe('when logged in', () => {
beforeEach(() => {
const state = rootState
.set('user_lists', ReducerRecord({
const state = {
...rootState,
me: '123',
user_lists: ReducerRecord({
follow_requests: ListRecord({
next: 'next_url',
}),
}))
.set('me', '123');
}),
};
store = mockStore(state);
});
describe('when the url is null', () => {
beforeEach(() => {
const state = rootState
.set('user_lists', ReducerRecord({
const state = {
...rootState,
me: '123',
user_lists: ReducerRecord({
follow_requests: ListRecord({
next: null,
}),
}))
.set('me', '123');
}),
};
store = mockStore(state);
});
@ -1425,7 +1458,7 @@ describe('authorizeFollowRequest()', () => {
describe('when logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -1439,7 +1472,7 @@ describe('authorizeFollowRequest()', () => {
describe('when logged in', () => {
beforeEach(() => {
const state = rootState.set('me', '123');
const state = { ...rootState, me: '123' };
store = mockStore(state);
});

View File

@ -16,7 +16,7 @@ describe('fetchBlocks()', () => {
describe('if logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -30,7 +30,7 @@ describe('fetchBlocks()', () => {
describe('if logged in', () => {
beforeEach(() => {
const state = rootState.set('me', '1234');
const state = { ...rootState, me: '1234' };
store = mockStore(state);
});
@ -89,7 +89,7 @@ describe('expandBlocks()', () => {
describe('if logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -103,15 +103,18 @@ describe('expandBlocks()', () => {
describe('if logged in', () => {
beforeEach(() => {
const state = rootState.set('me', '1234');
const state = { ...rootState, me: '1234' };
store = mockStore(state);
});
describe('without a url', () => {
beforeEach(() => {
const state = rootState
.set('me', '1234')
.set('user_lists', UserListsRecord({ blocks: ListRecord({ next: null }) }));
const state = {
...rootState,
me: '1234',
user_lists: UserListsRecord({ blocks: ListRecord({ next: null }) }),
};
store = mockStore(state);
});
@ -125,9 +128,11 @@ describe('expandBlocks()', () => {
describe('with a url', () => {
beforeEach(() => {
const state = rootState
.set('me', '1234')
.set('user_lists', UserListsRecord({ blocks: ListRecord({ next: 'example' }) }));
const state = {
...rootState,
me: '1234',
user_lists: UserListsRecord({ blocks: ListRecord({ next: 'example' }) }),
};
store = mockStore(state);
});

View File

@ -25,10 +25,12 @@ describe('uploadCompose()', () => {
},
});
const state = rootState
.set('me', '1234')
.set('instance', instance)
.setIn(['compose', 'home'], ReducerCompose());
const state = {
...rootState,
me: '1234',
instance,
compose: rootState.compose.set('home', ReducerCompose()),
};
store = mockStore(state);
files = [{
@ -71,10 +73,12 @@ describe('uploadCompose()', () => {
},
});
const state = rootState
.set('me', '1234')
.set('instance', instance)
.setIn(['compose', 'home'], ReducerCompose());
const state = {
...rootState,
me: '1234',
instance,
compose: rootState.compose.set('home', ReducerCompose()),
};
store = mockStore(state);
files = [{
@ -105,9 +109,11 @@ describe('uploadCompose()', () => {
describe('submitCompose()', () => {
it('inserts mentions from text', async() => {
const state = rootState
.set('me', '123')
.setIn(['compose', 'home'], ReducerCompose({ text: '@alex hello @mkljczk@pl.fediverse.pl @gg@汉语/漢語.com alex@alexgleason.me' }));
const state = {
...rootState,
me: '1234',
compose: rootState.compose.set('home', ReducerCompose({ text: '@alex hello @mkljczk@pl.fediverse.pl @gg@汉语/漢語.com alex@alexgleason.me' })),
};
const store = mockStore(state);
await store.dispatch(submitCompose('home'));

View File

@ -1,10 +1,9 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import get from 'lodash/get';
import { gte } from 'semver';
import { instanceSchema } from 'soapbox/schemas';
import { RootState } from 'soapbox/store';
import { getAuthUserUrl, getMeUrl } from 'soapbox/utils/auth';
import { MASTODON, parseVersion, PLEROMA, REBASED } from 'soapbox/utils/features';
import { getFeatures } from 'soapbox/utils/features';
import api from '../api';
@ -19,18 +18,6 @@ export const getHost = (state: RootState) => {
}
};
const supportsInstanceV2 = (instance: Record<string, any>): boolean => {
const v = parseVersion(get(instance, 'version'));
return (v.software === MASTODON && gte(v.compatVersion, '4.0.0')) ||
(v.software === PLEROMA && v.build === REBASED && gte(v.version, '2.5.54'));
};
/** We may need to fetch nodeinfo on Pleroma < 2.1 */
const needsNodeinfo = (instance: Record<string, any>): boolean => {
const v = parseVersion(get(instance, 'version'));
return v.software === PLEROMA && !get(instance, ['pleroma', 'metadata']);
};
interface InstanceData {
instance: Record<string, any>;
host: string | null | undefined;
@ -40,15 +27,14 @@ export const fetchInstance = createAsyncThunk<InstanceData, InstanceData['host']
'instance/fetch',
async(host, { dispatch, getState, rejectWithValue }) => {
try {
const { data: instance } = await api(getState).get('/api/v1/instance');
const { data } = await api(getState).get('/api/v1/instance');
const instance = instanceSchema.parse(data);
const features = getFeatures(instance);
if (supportsInstanceV2(instance)) {
if (features.instanceV2) {
dispatch(fetchInstanceV2(host));
}
if (needsNodeinfo(instance)) {
dispatch(fetchNodeinfo());
}
return { instance, host };
} catch (e) {
return rejectWithValue(e);
@ -67,8 +53,3 @@ export const fetchInstanceV2 = createAsyncThunk<InstanceData, InstanceData['host
}
},
);
export const fetchNodeinfo = createAsyncThunk<void, void, { state: RootState }>(
'nodeinfo/fetch',
async(_arg, { getState }) => await api(getState).get('/nodeinfo/2.1.json'),
);

View File

@ -37,23 +37,25 @@ describe('fetchMe()', () => {
const token = '123';
beforeEach(() => {
const state = rootState
.set('auth', ReducerRecord({
const state = {
...rootState,
auth: ReducerRecord({
me: accountUrl,
users: ImmutableMap({
[accountUrl]: AuthUserRecord({
'access_token': token,
}),
}),
}))
.set('entities', {
}),
entities: {
'ACCOUNTS': {
store: {
[accountUrl]: buildAccount({ url: accountUrl }),
},
lists: {},
},
});
},
};
store = mockStore(state);
});

View File

@ -14,10 +14,14 @@ describe('markReadNotifications()', () => {
'10': normalizeNotification({ id: '10' }),
});
const state = rootState
.set('me', '123')
.setIn(['notifications', 'lastRead'], '9')
.setIn(['notifications', 'items'], items);
const state = {
...rootState,
me: '123',
notifications: rootState.notifications.merge({
lastRead: '9',
items,
}),
};
const store = mockStore(state);

View File

@ -16,7 +16,8 @@ describe('checkOnboarding()', () => {
it('does nothing if localStorage item is not set', async() => {
mockGetItem = vi.fn().mockReturnValue(null);
const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
const state = { ...rootState };
state.onboarding.needsOnboarding = false;
const store = mockStore(state);
await store.dispatch(checkOnboardingStatus());
@ -29,7 +30,8 @@ describe('checkOnboarding()', () => {
it('does nothing if localStorage item is invalid', async() => {
mockGetItem = vi.fn().mockReturnValue('invalid');
const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
const state = { ...rootState };
state.onboarding.needsOnboarding = false;
const store = mockStore(state);
await store.dispatch(checkOnboardingStatus());
@ -42,7 +44,8 @@ describe('checkOnboarding()', () => {
it('dispatches the correct action', async() => {
mockGetItem = vi.fn().mockReturnValue('1');
const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
const state = { ...rootState };
state.onboarding.needsOnboarding = false;
const store = mockStore(state);
await store.dispatch(checkOnboardingStatus());
@ -65,7 +68,8 @@ describe('startOnboarding()', () => {
});
it('dispatches the correct action', async() => {
const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
const state = { ...rootState };
state.onboarding.needsOnboarding = false;
const store = mockStore(state);
await store.dispatch(startOnboarding());
@ -88,7 +92,8 @@ describe('endOnboarding()', () => {
});
it('dispatches the correct action', async() => {
const state = rootState.setIn(['onboarding', 'needsOnboarding'], false);
const state = { ...rootState };
state.onboarding.needsOnboarding = false;
const store = mockStore(state);
await store.dispatch(endOnboarding());

View File

@ -45,7 +45,7 @@ const unsubscribe = ({ registration, subscription }: {
const sendSubscriptionToBackend = (subscription: PushSubscription, me: Me) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const alerts = getState().push_notifications.alerts.toJS();
const params = { subscription, data: { alerts } };
const params = { subscription: subscription.toJSON(), data: { alerts } };
if (me) {
const data = pushNotificationsSetting.get(me);
@ -54,7 +54,7 @@ const sendSubscriptionToBackend = (subscription: PushSubscription, me: Me) =>
}
}
return dispatch(createPushSubscription(params) as any);
return dispatch(createPushSubscription(params));
};
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload

View File

@ -18,7 +18,15 @@ const PUSH_SUBSCRIPTION_DELETE_FAIL = 'PUSH_SUBSCRIPTION_DELETE_FAIL';
import type { AppDispatch, RootState } from 'soapbox/store';
const createPushSubscription = (params: Record<string, any>) =>
interface CreatePushSubscriptionParams {
subscription: PushSubscriptionJSON;
data?: {
alerts?: Record<string, boolean>;
policy?: 'all' | 'followed' | 'follower' | 'none';
};
}
const createPushSubscription = (params: CreatePushSubscriptionParams) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: PUSH_SUBSCRIPTION_CREATE_REQUEST, params });
return api(getState).post('/api/v1/push/subscription', params).then(({ data: subscription }) =>

View File

@ -1,21 +0,0 @@
import { rootState } from 'soapbox/jest/test-helpers';
import { RootState } from 'soapbox/store';
import { getSoapboxConfig } from './soapbox';
const ASCII_HEART = '❤'; // '\u2764\uFE0F'
const RED_HEART_RGI = '❤️'; // '\u2764'
describe('getSoapboxConfig()', () => {
it('returns RGI heart on Pleroma > 2.3', () => {
const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.3.0)') as RootState;
expect(getSoapboxConfig(state).allowedEmoji.includes(RED_HEART_RGI)).toBe(true);
expect(getSoapboxConfig(state).allowedEmoji.includes(ASCII_HEART)).toBe(false);
});
it('returns an ASCII heart on Pleroma < 2.3', () => {
const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.0.0)') as RootState;
expect(getSoapboxConfig(state).allowedEmoji.includes(ASCII_HEART)).toBe(true);
expect(getSoapboxConfig(state).allowedEmoji.includes(RED_HEART_RGI)).toBe(false);
});
});

View File

@ -26,7 +26,7 @@ describe('fetchStatusQuotes()', () => {
let store: ReturnType<typeof mockStore>;
beforeEach(() => {
const state = rootState.set('me', '1234');
const state = { ...rootState, me: '1234' };
store = mockStore(state);
});
@ -81,9 +81,12 @@ describe('expandStatusQuotes()', () => {
describe('without a url', () => {
beforeEach(() => {
const state = rootState
.set('me', '1234')
.set('status_lists', ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: null }) }));
const state = {
...rootState,
me: '1234',
status_lists: ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: null }) }),
};
store = mockStore(state);
});
@ -97,8 +100,12 @@ describe('expandStatusQuotes()', () => {
describe('with a url', () => {
beforeEach(() => {
const state = rootState.set('me', '1234')
.set('status_lists', ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: 'example' }) }));
const state = {
...rootState,
status_lists: ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: 'example' }) }),
me: '1234',
};
store = mockStore(state);
});

View File

@ -30,7 +30,7 @@ describe('deleteStatus()', () => {
describe('if logged out', () => {
beforeEach(() => {
const state = rootState.set('me', null);
const state = { ...rootState, me: null };
store = mockStore(state);
});
@ -49,11 +49,14 @@ describe('deleteStatus()', () => {
});
beforeEach(() => {
const state = rootState
.set('me', '1234')
.set('statuses', fromJS({
const state = {
...rootState,
me: '1234',
statuses: fromJS({
[statusId]: cachedStatus,
}) as any);
}) as any,
};
store = mockStore(state);
});

View File

@ -316,11 +316,12 @@ const toggleStatusHidden = (status: Status) => {
}
};
const translateStatus = (id: string, targetLanguage?: string) => (dispatch: AppDispatch, getState: () => RootState) => {
const translateStatus = (id: string, lang?: string) => (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: STATUS_TRANSLATE_REQUEST, id });
api(getState).post(`/api/v1/statuses/${id}/translate`, {
target_language: targetLanguage,
lang, // Mastodon API
target_language: lang, // HACK: Rebased and Pleroma compatibility
}).then(response => {
dispatch({
type: STATUS_TRANSLATE_SUCCESS,

View File

@ -5,7 +5,13 @@ import { renderHook, rootState, waitFor } from 'soapbox/jest/test-helpers';
import { useGroupLookup } from './useGroupLookup';
const group = buildGroup({ id: '1', slug: 'soapbox' });
const state = rootState.setIn(['instance', 'version'], '3.4.1 (compatible; TruthSocial 1.0.0)');
const state = {
...rootState,
instance: {
...rootState.instance,
version: '3.4.1 (compatible; TruthSocial 1.0.0)',
},
};
describe('useGroupLookup hook', () => {
describe('with a successful request', () => {

View File

@ -105,8 +105,8 @@ export default (getState: () => RootState, authType: string = 'user'): AxiosInst
const me = state.me;
const baseURL = me ? getAuthBaseURL(state, me) : '';
const relayUrl = state.getIn(['instance', 'nostr', 'relay']) as string | undefined;
const pubkey = state.getIn(['instance', 'nostr', 'pubkey']) as string | undefined;
const relayUrl = state.instance?.nostr?.relay;
const pubkey = state.instance?.nostr?.pubkey;
const nostrSign = Boolean(relayUrl && pubkey);
return baseClient(accessToken, baseURL, nostrSign);

View File

@ -1,7 +1,6 @@
import clsx from 'clsx';
import { List as ImmutableList } from 'immutable';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import React, { PureComponent } from 'react';
import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji';
import Icon from 'soapbox/components/icon';
@ -35,7 +34,7 @@ export interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputEl
theme?: InputThemes;
}
export default class AutosuggestInput extends ImmutablePureComponent<IAutosuggestInput> {
export default class AutosuggestInput extends PureComponent<IAutosuggestInput> {
static defaultProps = {
autoFocus: false,

View File

@ -22,7 +22,7 @@ describe('<QuotedStatus />', () => {
contentHtml: 'hello world',
}) as ReducerStatus;
const state = rootState.setIn(['accounts', '1'], account);
const state = rootState/*.accounts.set('1', account)*/;
render(<QuotedStatus status={status} />, undefined, state);
screen.getByText(/hello world/i);

View File

@ -21,7 +21,7 @@ const status = normalizeStatus({
}) as ReducerStatus;
describe('<Status />', () => {
const state = rootState.setIn(['accounts', '1'], account);
const state = rootState/*.accounts.set('1', account)*/;
it('renders content', () => {
render(<Status status={status} />, undefined, state);

View File

@ -94,10 +94,12 @@ describe('<SensitiveContentOverlay />', () => {
beforeEach(() => {
status = normalizeStatus({ sensitive: true }) as ReducerStatus;
store = rootState
.set('settings', ImmutableMap({
store = {
...rootState,
settings: ImmutableMap({
displayMedia: 'show_all',
}));
}),
};
});
it('displays the "Under review" warning', () => {

View File

@ -39,8 +39,8 @@ const AccountTimeline: React.FC<IAccountTimeline> = ({ params, withReplies = fal
const isBlocked = useAppSelector(state => state.relationships.getIn([account?.id, 'blocked_by']) === true);
const unavailable = isBlocked && !features.blockersVisible;
const patronEnabled = soapboxConfig.getIn(['extensions', 'patron', 'enabled']) === true;
const isLoading = useAppSelector(state => state.getIn(['timelines', `account:${path}`, 'isLoading']) === true);
const hasMore = useAppSelector(state => state.getIn(['timelines', `account:${path}`, 'hasMore']) === true);
const isLoading = useAppSelector(state => state.timelines.getIn([`account:${path}`, 'isLoading']) === true);
const hasMore = useAppSelector(state => state.timelines.getIn([`account:${path}`, 'hasMore']) === true);
const next = useAppSelector(state => state.timelines.get(`account:${path}`)?.next);
const accountUsername = account?.username || params.username;

View File

@ -74,8 +74,8 @@ const ChatComposer = React.forwardRef<HTMLTextAreaElement | null, IChatComposer>
const { chat } = useChatContext();
const isBlocked = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocked_by']));
const isBlocking = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocking']));
const isBlocked = useAppSelector((state) => state.relationships.getIn([chat?.account?.id, 'blocked_by']));
const isBlocking = useAppSelector((state) => state.relationships.getIn([chat?.account?.id, 'blocking']));
const maxCharacterCount = useAppSelector((state) => state.instance.configuration.chats.max_characters);
const attachmentLimit = useAppSelector(state => state.instance.configuration.chats.max_media_attachments);

View File

@ -35,8 +35,8 @@ const ChatListItem: React.FC<IChatListItemInterface> = ({ chat, onClick }) => {
const { isUsingMainChatPage } = useChatContext();
const { deleteChat } = useChatActions(chat?.id as string);
const isBlocked = useAppSelector((state) => state.getIn(['relationships', chat.account.id, 'blocked_by']));
const isBlocking = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocking']));
const isBlocked = useAppSelector((state) => state.relationships.getIn([chat.account.id, 'blocked_by']));
const isBlocking = useAppSelector((state) => state.relationships.getIn([chat?.account?.id, 'blocking']));
const menu = useMemo((): Menu => [{
text: intl.formatMessage(messages.leaveChat),

View File

@ -68,9 +68,11 @@ Object.assign(navigator, {
},
});
const store = rootState
.set('me', '1')
.set('instance', buildInstance({ version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)' }));
const store = {
...rootState,
me: '1',
instance: buildInstance({ version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)' }),
};
const renderComponentWithChatContext = () => render(
<VirtuosoMockContext.Provider value={{ viewportHeight: 300, itemHeight: 100 }}>

View File

@ -91,7 +91,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat }) => {
const formattedChatMessages = chatMessages || [];
const isBlocked = useAppSelector((state) => state.getIn(['relationships', chat.account.id, 'blocked_by']));
const isBlocked = useAppSelector((state) => state.relationships.getIn([chat.account.id, 'blocked_by']));
const lastChatMessage = chatMessages ? chatMessages[chatMessages.length - 1] : null;

View File

@ -60,7 +60,7 @@ const ChatPageMain = () => {
const handleUpdateChat = (value: MessageExpirationValues) => updateChat.mutate({ message_expiration: value });
const isBlocking = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocking']));
const isBlocking = useAppSelector((state) => state.relationships.getIn([chat?.account?.id, 'blocking']));
const handleBlockUser = () => {
dispatch(openModal('CONFIRM', {

View File

@ -17,16 +17,18 @@ const account = buildAccount({
},
});
const store = rootState
.set('me', id)
.set('entities', {
const store = {
...rootState,
me: id,
entities: {
'ACCOUNTS': {
store: {
[id]: account,
},
lists: {},
},
});
},
};
describe('<ChatWidget />', () => {
describe('when on the /chats endpoint', () => {

View File

@ -40,7 +40,7 @@ const ChatSettings = () => {
const handleUpdateChat = (value: MessageExpirationValues) => updateChat.mutate({ message_expiration: value });
const isBlocking = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocking']));
const isBlocking = useAppSelector((state) => state.relationships.getIn([chat?.account?.id, 'blocking']));
const closeSettings = () => {
changeScreen(ChatWidgetScreens.CHAT, chat?.id);

View File

@ -116,11 +116,11 @@ describe('useChatMessages', () => {
describe('when the user is blocked', () => {
beforeEach(() => {
const state = rootState
.set(
'relationships',
ImmutableMap({ '1': buildRelationship({ blocked_by: true }) }),
);
const state = {
...rootState,
relationships: ImmutableMap({ '1': buildRelationship({ blocked_by: true }) }),
};
store = mockStore(state);
});

View File

@ -79,7 +79,7 @@ const isLastMessage = (chatMessageId: string): boolean => {
const useChatMessages = (chat: IChat) => {
const api = useApi();
const isBlocked = useAppSelector((state) => state.getIn(['relationships', chat.account.id, 'blocked_by']));
const isBlocked = useAppSelector((state) => state.relationships.getIn([chat.account.id, 'blocked_by']));
const getChatMessages = async (chatId: string, pageParam?: any): Promise<PaginatedResult<ChatMessage>> => {
const nextPageLink = pageParam?.link;

View File

@ -1,5 +1,4 @@
import { Record as ImmutableRecord } from 'immutable';
import { combineReducers } from 'redux-immutable';
import { combineReducers } from '@reduxjs/toolkit';
import { AUTH_LOGGED_OUT } from 'soapbox/actions/auth';
import * as BuildConfig from 'soapbox/build-config';
@ -120,39 +119,29 @@ const reducers = {
user_lists,
};
// Build a default state from all reducers: it has the key and `undefined`
export const StateRecord = ImmutableRecord(
Object.keys(reducers).reduce((params: Record<string, any>, reducer) => {
params[reducer] = undefined;
return params;
}, {}),
);
const appReducer = combineReducers(reducers);
const appReducer = combineReducers(reducers, StateRecord);
type AppState = ReturnType<typeof appReducer>;
// Clear the state (mostly) when the user logs out
const logOut = (state: any = StateRecord()): ReturnType<typeof appReducer> => {
const logOut = (state: AppState): ReturnType<typeof appReducer> => {
if (BuildConfig.NODE_ENV === 'production') {
location.href = '/login';
}
const whitelist: string[] = ['instance', 'soapbox', 'custom_emojis', 'auth'];
const newState = rootReducer(undefined, { type: '' });
return StateRecord(
whitelist.reduce((acc: Record<string, any>, curr) => {
acc[curr] = state.get(curr);
return acc;
}, {}),
) as unknown as ReturnType<typeof appReducer>;
const { instance, soapbox, custom_emojis, auth } = state;
return { ...newState, instance, soapbox, custom_emojis, auth };
};
const rootReducer: typeof appReducer = (state, action) => {
switch (action.type) {
case AUTH_LOGGED_OUT:
return appReducer(logOut(state), action);
return appReducer(logOut(state as AppState), action);
default:
return appReducer(state, action);
}
};
export default rootReducer;
export default appReducer;

View File

@ -711,6 +711,7 @@ const getInstanceFeatures = (instance: Instance) => {
instanceV2: any([
v.software === MASTODON && gte(v.compatVersion, '4.0.0'),
v.software === PLEROMA && v.build === REBASED && gte(v.version, '2.5.54'),
v.software === DITTO,
]),
/**

View File

@ -1,17 +0,0 @@
// Type definitions for redux-immutable v4.0.0
// Project: https://github.com/gajus/redux-immutable
// Definitions by: Sebastian Sebald <https://github.com/sebald>
// Gavin Gregory <https://github.com/gavingregory>
// Kanitkorn Sujautra <https://github.com/lukyth>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
declare module 'redux-immutable' {
import { Collection, Record } from 'immutable';
import { ReducersMapObject, Reducer, Action } from 'redux';
export function combineReducers<S, A extends Action, T>(reducers: ReducersMapObject<S, A>, getDefaultState?: () => Collection.Keyed<T, S>): Reducer<S, A>;
export function combineReducers<S, A extends Action>(reducers: ReducersMapObject<S, A>, getDefaultState?: () => Collection.Indexed<S>): Reducer<S, A>;
export function combineReducers<S>(reducers: ReducersMapObject<S, any>, getDefaultState?: () => Collection.Indexed<S>): Reducer<S>;
export function combineReducers<S extends object, T extends object>(reducers: ReducersMapObject<S, any>, getDefaultState?: Record.Factory<T>): Reducer<ReturnType<Record.Factory<S>>>;
}

View File

@ -7192,11 +7192,6 @@ react-hot-toast@^2.4.0:
dependencies:
goober "^2.1.10"
react-immutable-pure-component@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/react-immutable-pure-component/-/react-immutable-pure-component-2.2.2.tgz#3014d3e20cd5a7a4db73b81f1f1464f4d351684b"
integrity sha512-vkgoMJUDqHZfXXnjVlG3keCxSO/U6WeDQ5/Sl0GK2cH8TOxEzQ5jXqDXHEL/jqk6fsNxV05oH5kD7VNMUE2k+A==
react-inlinesvg@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/react-inlinesvg/-/react-inlinesvg-4.0.3.tgz#69aa4d9c01b037abb800bfa103cb5591c6f3fe76"
@ -7443,11 +7438,6 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
redux-immutable@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/redux-immutable/-/redux-immutable-4.0.0.tgz#3a1a32df66366462b63691f0e1dc35e472bbc9f3"
integrity sha1-Ohoy32Y2ZGK2NpHw4dw15HK7yfM=
redux-thunk@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"