Merge branch 'main' into finish-pure-status-component
This commit is contained in:
commit
7e6418fc9b
|
@ -90,8 +90,6 @@
|
|||
"@types/semver": "^7.3.9",
|
||||
"@webbtc/webln-types": "^3.0.0",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"axios": "^1.2.2",
|
||||
"axios-mock-adapter": "^1.22.0",
|
||||
"blurhash": "^2.0.0",
|
||||
"bowser": "^2.11.0",
|
||||
"browserslist": "^4.16.6",
|
||||
|
|
|
@ -12,7 +12,8 @@ const fetchAboutPage = (slug = 'index', locale?: string) => (dispatch: React.Dis
|
|||
|
||||
const filename = `${slug}${locale ? `.${locale}` : ''}.html`;
|
||||
return api(getState).get(`/instance/about/${filename}`)
|
||||
.then(({ data: html }) => {
|
||||
.then((response) => response.text())
|
||||
.then((html) => {
|
||||
dispatch({ type: FETCH_ABOUT_PAGE_SUCCESS, slug, locale, html });
|
||||
return html;
|
||||
})
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers.tsx';
|
||||
|
||||
import { submitAccountNote } from './account-notes.ts';
|
||||
|
||||
describe('submitAccountNote()', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(rootState);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onPost('/api/v1/accounts/1/note').reply(200, {});
|
||||
});
|
||||
});
|
||||
|
||||
it('post the note to the API', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'ACCOUNT_NOTE_SUBMIT_REQUEST' },
|
||||
{ type: 'ACCOUNT_NOTE_SUBMIT_SUCCESS', relationship: {} },
|
||||
];
|
||||
await store.dispatch(submitAccountNote('1', 'hello'));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onPost('/api/v1/accounts/1/note').networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch failed action', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'ACCOUNT_NOTE_SUBMIT_REQUEST' },
|
||||
{
|
||||
type: 'ACCOUNT_NOTE_SUBMIT_FAIL',
|
||||
error: new Error('Network Error'),
|
||||
},
|
||||
];
|
||||
await store.dispatch(submitAccountNote('1', 'hello'));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -15,8 +15,9 @@ const submitAccountNote = (id: string, value: string) =>
|
|||
.post(`/api/v1/accounts/${id}/note`, {
|
||||
comment: value,
|
||||
})
|
||||
.then(response => {
|
||||
dispatch(submitAccountNoteSuccess(response.data));
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
dispatch(submitAccountNoteSuccess(data));
|
||||
})
|
||||
.catch(error => dispatch(submitAccountNoteFail(error)));
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,10 +1,11 @@
|
|||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||
import { importEntities } from 'soapbox/entity-store/actions.ts';
|
||||
import { Entities } from 'soapbox/entity-store/entities.ts';
|
||||
import { selectAccount } from 'soapbox/selectors/index.ts';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth.ts';
|
||||
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features.ts';
|
||||
|
||||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import {
|
||||
importFetchedAccount,
|
||||
|
@ -12,7 +13,6 @@ import {
|
|||
importErrorWhileFetchingAccountByUsername,
|
||||
} from './importer/index.ts';
|
||||
|
||||
import type { AxiosError, CancelToken } from 'axios';
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store.ts';
|
||||
import type { APIEntity, Status } from 'soapbox/types/entities.ts';
|
||||
|
@ -118,7 +118,7 @@ const BIRTHDAY_REMINDERS_FETCH_REQUEST = 'BIRTHDAY_REMINDERS_FETCH_REQUEST';
|
|||
const BIRTHDAY_REMINDERS_FETCH_SUCCESS = 'BIRTHDAY_REMINDERS_FETCH_SUCCESS';
|
||||
const BIRTHDAY_REMINDERS_FETCH_FAIL = 'BIRTHDAY_REMINDERS_FETCH_FAIL';
|
||||
|
||||
const maybeRedirectLogin = (error: AxiosError, history?: History) => {
|
||||
const maybeRedirectLogin = (error: HTTPError, history?: History) => {
|
||||
// The client is unauthorized - redirect to login.
|
||||
if (history && error?.response?.status === 401) {
|
||||
history.push('/login');
|
||||
|
@ -130,7 +130,7 @@ const noOp = () => new Promise(f => f(undefined));
|
|||
const createAccount = (params: Record<string, any>) =>
|
||||
async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ACCOUNT_CREATE_REQUEST, params });
|
||||
return api(getState, 'app').post('/api/v1/accounts', params).then(({ data: token }) => {
|
||||
return api(getState, 'app').post('/api/v1/accounts', params).then((response) => response.json()).then((token) => {
|
||||
return dispatch({ type: ACCOUNT_CREATE_SUCCESS, params, token });
|
||||
}).catch(error => {
|
||||
dispatch({ type: ACCOUNT_CREATE_FAIL, error, params });
|
||||
|
@ -152,9 +152,10 @@ const fetchAccount = (id: string) =>
|
|||
|
||||
return api(getState)
|
||||
.get(`/api/v1/accounts/${id}`)
|
||||
.then(response => {
|
||||
dispatch(importFetchedAccount(response.data));
|
||||
dispatch(fetchAccountSuccess(response.data));
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
dispatch(importFetchedAccount(data));
|
||||
dispatch(fetchAccountSuccess(data));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(fetchAccountFail(id, error));
|
||||
|
@ -167,10 +168,10 @@ const fetchAccountByUsername = (username: string, history?: History) =>
|
|||
const features = getFeatures(instance);
|
||||
|
||||
if (features.accountByUsername && (me || !features.accountLookup)) {
|
||||
return api(getState).get(`/api/v1/accounts/${username}`).then(response => {
|
||||
dispatch(fetchRelationships([response.data.id]));
|
||||
dispatch(importFetchedAccount(response.data));
|
||||
dispatch(fetchAccountSuccess(response.data));
|
||||
return api(getState).get(`/api/v1/accounts/${username}`).then((response) => response.json()).then((data) => {
|
||||
dispatch(fetchRelationships([data.id]));
|
||||
dispatch(importFetchedAccount(data));
|
||||
dispatch(fetchAccountSuccess(data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchAccountFail(null, error));
|
||||
dispatch(importErrorWhileFetchingAccountByUsername(username));
|
||||
|
@ -230,10 +231,10 @@ const blockAccount = (id: string) =>
|
|||
|
||||
return api(getState)
|
||||
.post(`/api/v1/accounts/${id}/block`)
|
||||
.then(response => {
|
||||
dispatch(importEntities([response.data], Entities.RELATIONSHIPS));
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch(importEntities([data], Entities.RELATIONSHIPS));
|
||||
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
|
||||
return dispatch(blockAccountSuccess(response.data, getState().statuses));
|
||||
return dispatch(blockAccountSuccess(data, getState().statuses));
|
||||
}).catch(error => dispatch(blockAccountFail(error)));
|
||||
};
|
||||
|
||||
|
@ -245,9 +246,9 @@ const unblockAccount = (id: string) =>
|
|||
|
||||
return api(getState)
|
||||
.post(`/api/v1/accounts/${id}/unblock`)
|
||||
.then(response => {
|
||||
dispatch(importEntities([response.data], Entities.RELATIONSHIPS));
|
||||
return dispatch(unblockAccountSuccess(response.data));
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch(importEntities([data], Entities.RELATIONSHIPS));
|
||||
return dispatch(unblockAccountSuccess(data));
|
||||
})
|
||||
.catch(error => dispatch(unblockAccountFail(error)));
|
||||
};
|
||||
|
@ -307,10 +308,10 @@ const muteAccount = (id: string, notifications?: boolean, duration = 0) =>
|
|||
|
||||
return api(getState)
|
||||
.post(`/api/v1/accounts/${id}/mute`, params)
|
||||
.then(response => {
|
||||
dispatch(importEntities([response.data], Entities.RELATIONSHIPS));
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch(importEntities([data], Entities.RELATIONSHIPS));
|
||||
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
|
||||
return dispatch(muteAccountSuccess(response.data, getState().statuses));
|
||||
return dispatch(muteAccountSuccess(data, getState().statuses));
|
||||
})
|
||||
.catch(error => dispatch(muteAccountFail(error)));
|
||||
};
|
||||
|
@ -323,9 +324,9 @@ const unmuteAccount = (id: string) =>
|
|||
|
||||
return api(getState)
|
||||
.post(`/api/v1/accounts/${id}/unmute`)
|
||||
.then(response => {
|
||||
dispatch(importEntities([response.data], Entities.RELATIONSHIPS));
|
||||
return dispatch(unmuteAccountSuccess(response.data));
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch(importEntities([data], Entities.RELATIONSHIPS));
|
||||
return dispatch(unmuteAccountSuccess(data));
|
||||
})
|
||||
.catch(error => dispatch(unmuteAccountFail(error)));
|
||||
};
|
||||
|
@ -369,7 +370,7 @@ const subscribeAccount = (id: string, notifications?: boolean) =>
|
|||
|
||||
return api(getState)
|
||||
.post(`/api/v1/pleroma/accounts/${id}/subscribe`, { notifications })
|
||||
.then(response => dispatch(subscribeAccountSuccess(response.data)))
|
||||
.then((response) => response.json()).then((data) => dispatch(subscribeAccountSuccess(data)))
|
||||
.catch(error => dispatch(subscribeAccountFail(error)));
|
||||
};
|
||||
|
||||
|
@ -381,7 +382,7 @@ const unsubscribeAccount = (id: string) =>
|
|||
|
||||
return api(getState)
|
||||
.post(`/api/v1/pleroma/accounts/${id}/unsubscribe`)
|
||||
.then(response => dispatch(unsubscribeAccountSuccess(response.data)))
|
||||
.then((response) => response.json()).then((data) => dispatch(unsubscribeAccountSuccess(data)))
|
||||
.catch(error => dispatch(unsubscribeAccountFail(error)));
|
||||
};
|
||||
|
||||
|
@ -423,7 +424,7 @@ const removeFromFollowers = (id: string) =>
|
|||
|
||||
return api(getState)
|
||||
.post(`/api/v1/accounts/${id}/remove_from_followers`)
|
||||
.then(response => dispatch(removeFromFollowersSuccess(response.data)))
|
||||
.then((response) => response.json()).then((data) => dispatch(removeFromFollowersSuccess(data)))
|
||||
.catch(error => dispatch(removeFromFollowersFail(id, error)));
|
||||
};
|
||||
|
||||
|
@ -449,12 +450,13 @@ const fetchFollowers = (id: string) =>
|
|||
|
||||
return api(getState)
|
||||
.get(`/api/v1/accounts/${id}/followers`)
|
||||
.then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
.then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchFollowersSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchFollowersSuccess(id, data, next));
|
||||
dispatch(fetchRelationships(data.map((item: APIEntity) => item.id)));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(fetchFollowersFail(id, error));
|
||||
|
@ -493,12 +495,13 @@ const expandFollowers = (id: string) =>
|
|||
|
||||
return api(getState)
|
||||
.get(url)
|
||||
.then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
.then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandFollowersSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(expandFollowersSuccess(id, data, next));
|
||||
dispatch(fetchRelationships(data.map((item: APIEntity) => item.id)));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(expandFollowersFail(id, error));
|
||||
|
@ -529,12 +532,13 @@ const fetchFollowing = (id: string) =>
|
|||
|
||||
return api(getState)
|
||||
.get(`/api/v1/accounts/${id}/following`)
|
||||
.then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
.then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchFollowingSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchFollowingSuccess(id, data, next));
|
||||
dispatch(fetchRelationships(data.map((item: APIEntity) => item.id)));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(fetchFollowingFail(id, error));
|
||||
|
@ -573,12 +577,13 @@ const expandFollowing = (id: string) =>
|
|||
|
||||
return api(getState)
|
||||
.get(url)
|
||||
.then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
.then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandFollowingSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(expandFollowingSuccess(id, data, next));
|
||||
dispatch(fetchRelationships(data.map((item: APIEntity) => item.id)));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(expandFollowingFail(id, error));
|
||||
|
@ -618,9 +623,9 @@ const fetchRelationships = (accountIds: string[]) =>
|
|||
|
||||
return api(getState)
|
||||
.get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`)
|
||||
.then(response => {
|
||||
dispatch(importEntities(response.data, Entities.RELATIONSHIPS));
|
||||
dispatch(fetchRelationshipsSuccess(response.data));
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch(importEntities(data, Entities.RELATIONSHIPS));
|
||||
dispatch(fetchRelationshipsSuccess(data));
|
||||
})
|
||||
.catch(error => dispatch(fetchRelationshipsFail(error)));
|
||||
};
|
||||
|
@ -651,10 +656,11 @@ const fetchFollowRequests = () =>
|
|||
|
||||
return api(getState)
|
||||
.get('/api/v1/follow_requests')
|
||||
.then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null));
|
||||
.then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchFollowRequestsSuccess(data, next));
|
||||
})
|
||||
.catch(error => dispatch(fetchFollowRequestsFail(error)));
|
||||
};
|
||||
|
@ -688,10 +694,11 @@ const expandFollowRequests = () =>
|
|||
|
||||
return api(getState)
|
||||
.get(url)
|
||||
.then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null));
|
||||
.then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(expandFollowRequestsSuccess(data, next));
|
||||
})
|
||||
.catch(error => dispatch(expandFollowRequestsFail(error)));
|
||||
};
|
||||
|
@ -773,8 +780,8 @@ const pinAccount = (id: string) =>
|
|||
|
||||
dispatch(pinAccountRequest(id));
|
||||
|
||||
return api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => {
|
||||
dispatch(pinAccountSuccess(response.data));
|
||||
return api(getState).post(`/api/v1/accounts/${id}/pin`).then((response) => response.json()).then((data) => {
|
||||
dispatch(pinAccountSuccess(data));
|
||||
}).catch(error => {
|
||||
dispatch(pinAccountFail(error));
|
||||
});
|
||||
|
@ -786,8 +793,8 @@ const unpinAccount = (id: string) =>
|
|||
|
||||
dispatch(unpinAccountRequest(id));
|
||||
|
||||
return api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => {
|
||||
dispatch(unpinAccountSuccess(response.data));
|
||||
return api(getState).post(`/api/v1/accounts/${id}/unpin`).then((response) => response.json()).then((data) => {
|
||||
dispatch(unpinAccountSuccess(data));
|
||||
}).catch(error => {
|
||||
dispatch(unpinAccountFail(error));
|
||||
});
|
||||
|
@ -796,7 +803,7 @@ const unpinAccount = (id: string) =>
|
|||
const updateNotificationSettings = (params: Record<string, any>) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: NOTIFICATION_SETTINGS_REQUEST, params });
|
||||
return api(getState).put('/api/pleroma/notification_settings', params).then(({ data }) => {
|
||||
return api(getState).put('/api/pleroma/notification_settings', params).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: NOTIFICATION_SETTINGS_SUCCESS, params, data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: NOTIFICATION_SETTINGS_FAIL, params, error });
|
||||
|
@ -838,9 +845,9 @@ const fetchPinnedAccounts = (id: string) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchPinnedAccountsRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/pleroma/accounts/${id}/endorsements`).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchPinnedAccountsSuccess(id, response.data, null));
|
||||
api(getState).get(`/api/v1/pleroma/accounts/${id}/endorsements`).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchPinnedAccountsSuccess(id, data, null));
|
||||
}).catch(error => {
|
||||
dispatch(fetchPinnedAccountsFail(id, error));
|
||||
});
|
||||
|
@ -867,7 +874,7 @@ const fetchPinnedAccountsFail = (id: string, error: unknown) => ({
|
|||
const accountSearch = (params: Record<string, any>, signal?: AbortSignal) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ACCOUNT_SEARCH_REQUEST, params });
|
||||
return api(getState).get('/api/v1/accounts/search', { params, signal }).then(({ data: accounts }) => {
|
||||
return api(getState).get('/api/v1/accounts/search', { searchParams: params, signal }).then((response) => response.json()).then((accounts) => {
|
||||
dispatch(importFetchedAccounts(accounts));
|
||||
dispatch({ type: ACCOUNT_SEARCH_SUCCESS, accounts });
|
||||
return accounts;
|
||||
|
@ -877,10 +884,10 @@ const accountSearch = (params: Record<string, any>, signal?: AbortSignal) =>
|
|||
});
|
||||
};
|
||||
|
||||
const accountLookup = (acct: string, cancelToken?: CancelToken) =>
|
||||
const accountLookup = (acct: string, signal?: AbortSignal) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ACCOUNT_LOOKUP_REQUEST, acct });
|
||||
return api(getState).get('/api/v1/accounts/lookup', { params: { acct }, cancelToken }).then(({ data: account }) => {
|
||||
return api(getState).get('/api/v1/accounts/lookup', { searchParams: { acct }, signal }).then((response) => response.json()).then((account) => {
|
||||
if (account && account.id) dispatch(importFetchedAccount(account));
|
||||
dispatch({ type: ACCOUNT_LOOKUP_SUCCESS, account });
|
||||
return account;
|
||||
|
@ -898,11 +905,11 @@ const fetchBirthdayReminders = (month: number, day: number) =>
|
|||
|
||||
dispatch({ type: BIRTHDAY_REMINDERS_FETCH_REQUEST, day, month, id: me });
|
||||
|
||||
return api(getState).get('/api/v1/pleroma/birthdays', { params: { day, month } }).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
return api(getState).get('/api/v1/pleroma/birthdays', { searchParams: { day, month } }).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch({
|
||||
type: BIRTHDAY_REMINDERS_FETCH_SUCCESS,
|
||||
accounts: response.data,
|
||||
accounts: data,
|
||||
day,
|
||||
month,
|
||||
id: me,
|
||||
|
|
|
@ -3,9 +3,8 @@ import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } fr
|
|||
import { accountIdsToAccts } from 'soapbox/selectors/index.ts';
|
||||
import { filterBadges, getTagDiff } from 'soapbox/utils/badges.ts';
|
||||
|
||||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store.ts';
|
||||
import type { APIEntity } from 'soapbox/types/entities.ts';
|
||||
|
||||
|
@ -74,7 +73,7 @@ const fetchConfig = () =>
|
|||
dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST });
|
||||
return api(getState)
|
||||
.get('/api/v1/pleroma/admin/config')
|
||||
.then(({ data }) => {
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: ADMIN_CONFIG_FETCH_SUCCESS, configs: data.configs, needsReboot: data.need_reboot });
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_CONFIG_FETCH_FAIL, error });
|
||||
|
@ -86,7 +85,7 @@ const updateConfig = (configs: Record<string, any>[]) =>
|
|||
dispatch({ type: ADMIN_CONFIG_UPDATE_REQUEST, configs });
|
||||
return api(getState)
|
||||
.post('/api/v1/pleroma/admin/config', { configs })
|
||||
.then(({ data }) => {
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: ADMIN_CONFIG_UPDATE_SUCCESS, configs: data.configs, needsReboot: data.need_reboot });
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_CONFIG_UPDATE_FAIL, error, configs });
|
||||
|
@ -111,7 +110,8 @@ function fetchReports(params: Record<string, any> = {}) {
|
|||
dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params });
|
||||
|
||||
try {
|
||||
const { data: reports } = await api(getState).get('/api/v1/admin/reports', { params });
|
||||
const response = await api(getState).get('/api/v1/admin/reports', { searchParams: params });
|
||||
const reports = await response.json();
|
||||
reports.forEach((report: APIEntity) => {
|
||||
dispatch(importFetchedAccount(report.account?.account));
|
||||
dispatch(importFetchedAccount(report.target_account?.account));
|
||||
|
@ -158,8 +158,9 @@ function fetchUsers(filters: Record<string, boolean>, page = 1, query?: string |
|
|||
};
|
||||
|
||||
try {
|
||||
const { data: accounts, ...response } = await api(getState).get(url || '/api/v1/admin/accounts', { params });
|
||||
const next = getLinks(response as AxiosResponse<any, any>).refs.find(link => link.rel === 'next')?.uri;
|
||||
const response = await api(getState).get(url || '/api/v1/admin/accounts', { searchParams: params });
|
||||
const accounts = await response.json();
|
||||
const next = response.next();
|
||||
|
||||
dispatch(importFetchedAccounts(accounts.map(({ account }: APIEntity) => account)));
|
||||
dispatch(fetchRelationships(accounts.map((account_1: APIEntity) => account_1.id)));
|
||||
|
@ -206,8 +207,9 @@ const deleteUser = (accountId: string) =>
|
|||
const nicknames = accountIdsToAccts(getState(), [accountId]);
|
||||
dispatch({ type: ADMIN_USERS_DELETE_REQUEST, accountId });
|
||||
return api(getState)
|
||||
.delete('/api/v1/pleroma/admin/users', { data: { nicknames } })
|
||||
.then(({ data: nicknames }) => {
|
||||
.request('DELETE', '/api/v1/pleroma/admin/users', { nicknames })
|
||||
.then((response) => response.json())
|
||||
.then(({ nicknames }) => {
|
||||
dispatch({ type: ADMIN_USERS_DELETE_SUCCESS, nicknames, accountId });
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_USERS_DELETE_FAIL, error, accountId });
|
||||
|
@ -218,8 +220,9 @@ function approveUser(accountId: string) {
|
|||
return async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountId });
|
||||
try {
|
||||
const { data: user } = await api(getState)
|
||||
.post(`/api/v1/admin/accounts/${accountId}/approve`);
|
||||
const { user } = await api(getState)
|
||||
.post(`/api/v1/admin/accounts/${accountId}/approve`)
|
||||
.then((response) => response.json());
|
||||
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, user, accountId });
|
||||
} catch (error) {
|
||||
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountId });
|
||||
|
@ -231,8 +234,9 @@ function rejectUser(accountId: string) {
|
|||
return async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ADMIN_USERS_REJECT_REQUEST, accountId });
|
||||
try {
|
||||
const { data: user } = await api(getState)
|
||||
.post(`/api/v1/admin/accounts/${accountId}/reject`);
|
||||
const { user } = await api(getState)
|
||||
.post(`/api/v1/admin/accounts/${accountId}/reject`)
|
||||
.then((response) => response.json());
|
||||
dispatch({ type: ADMIN_USERS_REJECT_SUCCESS, user, accountId });
|
||||
} catch (error) {
|
||||
dispatch({ type: ADMIN_USERS_REJECT_FAIL, error, accountId });
|
||||
|
@ -288,7 +292,7 @@ const untagUsers = (accountIds: string[], tags: string[]) =>
|
|||
|
||||
dispatch({ type: ADMIN_USERS_UNTAG_REQUEST, accountIds, tags });
|
||||
return api(getState)
|
||||
.delete('/api/v1/pleroma/admin/users/tag', { data: { nicknames, tags } })
|
||||
.request('DELETE', '/api/v1/pleroma/admin/users/tag', { nicknames, tags })
|
||||
.then(() => {
|
||||
dispatch({ type: ADMIN_USERS_UNTAG_SUCCESS, accountIds, tags });
|
||||
}).catch(error => {
|
||||
|
@ -320,7 +324,7 @@ const addPermission = (accountIds: string[], permissionGroup: string) =>
|
|||
dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup });
|
||||
return api(getState)
|
||||
.post(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { nicknames })
|
||||
.then(({ data }) => {
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_SUCCESS, accountIds, permissionGroup, data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_FAIL, error, accountIds, permissionGroup });
|
||||
|
@ -332,8 +336,8 @@ const removePermission = (accountIds: string[], permissionGroup: string) =>
|
|||
const nicknames = accountIdsToAccts(getState(), accountIds);
|
||||
dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup });
|
||||
return api(getState)
|
||||
.delete(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { data: { nicknames } })
|
||||
.then(({ data }) => {
|
||||
.request('DELETE', `/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { nicknames })
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS, accountIds, permissionGroup, data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_FAIL, error, accountIds, permissionGroup });
|
||||
|
|
|
@ -45,8 +45,8 @@ const fetchAliases = (dispatch: AppDispatch, getState: () => RootState) => {
|
|||
dispatch(fetchAliasesRequest());
|
||||
|
||||
api(getState).get('/api/pleroma/aliases')
|
||||
.then(response => {
|
||||
dispatch(fetchAliasesSuccess(response.data.aliases));
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch(fetchAliasesSuccess(data.aliases));
|
||||
})
|
||||
.catch(err => dispatch(fetchAliasesFail(err)));
|
||||
};
|
||||
|
@ -75,7 +75,7 @@ const fetchAliasesSuggestions = (q: string) =>
|
|||
limit: 4,
|
||||
};
|
||||
|
||||
api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
|
||||
api(getState).get('/api/v1/accounts/search', { searchParams: params }).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchAliasesSuggestionsReady(q, data));
|
||||
}).catch(error => toast.showAlertForError(error));
|
||||
|
@ -111,11 +111,12 @@ const addToAliases = (account: Account) =>
|
|||
dispatch(addToAliasesRequest());
|
||||
|
||||
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, account.pleroma?.ap_id] })
|
||||
.then((response => {
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
toast.success(messages.createSuccess);
|
||||
dispatch(addToAliasesSuccess);
|
||||
dispatch(patchMeSuccess(response.data));
|
||||
}))
|
||||
dispatch(patchMeSuccess(data));
|
||||
})
|
||||
.catch(err => dispatch(addToAliasesFail(err)));
|
||||
|
||||
return;
|
||||
|
@ -162,10 +163,10 @@ const removeFromAliases = (account: string) =>
|
|||
dispatch(removeFromAliasesRequest());
|
||||
|
||||
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: alsoKnownAs.filter((id: string) => id !== account) })
|
||||
.then(response => {
|
||||
.then((response) => response.json()).then((data) => {
|
||||
toast.success(messages.removeSuccess);
|
||||
dispatch(removeFromAliasesSuccess);
|
||||
dispatch(patchMeSuccess(response.data));
|
||||
dispatch(patchMeSuccess(data));
|
||||
})
|
||||
.catch(err => dispatch(removeFromAliasesFail(err)));
|
||||
|
||||
|
@ -174,12 +175,10 @@ const removeFromAliases = (account: string) =>
|
|||
|
||||
dispatch(addToAliasesRequest());
|
||||
|
||||
api(getState).delete('/api/pleroma/aliases', {
|
||||
data: {
|
||||
alias: account,
|
||||
},
|
||||
api(getState).request('DELETE', '/api/pleroma/aliases', {
|
||||
alias: account,
|
||||
})
|
||||
.then(response => {
|
||||
.then(() => {
|
||||
toast.success(messages.removeSuccess);
|
||||
dispatch(removeFromAliasesSuccess);
|
||||
dispatch(fetchAliases);
|
||||
|
|
|
@ -21,7 +21,7 @@ export const APP_VERIFY_CREDENTIALS_FAIL = 'APP_VERIFY_CREDENTIALS_FAIL';
|
|||
export function createApp(params?: Record<string, string>, baseURL?: string) {
|
||||
return (dispatch: React.Dispatch<AnyAction>) => {
|
||||
dispatch({ type: APP_CREATE_REQUEST, params });
|
||||
return baseClient(null, baseURL).post('/api/v1/apps', params).then(({ data: app }) => {
|
||||
return baseClient(null, baseURL).post('/api/v1/apps', params).then((response) => response.json()).then((app) => {
|
||||
dispatch({ type: APP_CREATE_SUCCESS, params, app });
|
||||
return app as Record<string, string>;
|
||||
}).catch(error => {
|
||||
|
@ -34,7 +34,7 @@ export function createApp(params?: Record<string, string>, baseURL?: string) {
|
|||
export function verifyAppCredentials(token: string) {
|
||||
return (dispatch: React.Dispatch<AnyAction>) => {
|
||||
dispatch({ type: APP_VERIFY_CREDENTIALS_REQUEST, token });
|
||||
return baseClient(token).get('/api/v1/apps/verify_credentials').then(({ data: app }) => {
|
||||
return baseClient(token).get('/api/v1/apps/verify_credentials').then((response) => response.json()).then((app) => {
|
||||
dispatch({ type: APP_VERIFY_CREDENTIALS_SUCCESS, token, app });
|
||||
return app;
|
||||
}).catch(error => {
|
||||
|
|
|
@ -14,6 +14,7 @@ import { createApp } from 'soapbox/actions/apps.ts';
|
|||
import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me.ts';
|
||||
import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth.ts';
|
||||
import { startOnboarding } from 'soapbox/actions/onboarding.ts';
|
||||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||
import { custom } from 'soapbox/custom.ts';
|
||||
import { queryClient } from 'soapbox/queries/client.ts';
|
||||
import { selectAccount } from 'soapbox/selectors/index.ts';
|
||||
|
@ -28,7 +29,6 @@ import api, { baseClient } from '../api/index.ts';
|
|||
|
||||
import { importFetchedAccount } from './importer/index.ts';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store.ts';
|
||||
|
||||
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
|
||||
|
@ -132,7 +132,7 @@ export const otpVerify = (code: string, mfa_token: string) =>
|
|||
challenge_type: 'totp',
|
||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
scope: getScopes(getState()),
|
||||
}).then(({ data: token }) => dispatch(authLoggedIn(token)));
|
||||
}).then((response) => response.json()).then((token) => dispatch(authLoggedIn(token)));
|
||||
};
|
||||
|
||||
export const verifyCredentials = (token: string, accountUrl?: string) => {
|
||||
|
@ -141,7 +141,7 @@ export const verifyCredentials = (token: string, accountUrl?: string) => {
|
|||
return (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: VERIFY_CREDENTIALS_REQUEST, token });
|
||||
|
||||
return baseClient(token, baseURL).get('/api/v1/accounts/verify_credentials').then(({ data: account }) => {
|
||||
return baseClient(token, baseURL).get('/api/v1/accounts/verify_credentials').then((response) => response.json()).then((account) => {
|
||||
dispatch(importFetchedAccount(account));
|
||||
dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account });
|
||||
if (account.id === getState().me) dispatch(fetchMeSuccess(account));
|
||||
|
@ -149,7 +149,7 @@ export const verifyCredentials = (token: string, accountUrl?: string) => {
|
|||
}).catch(error => {
|
||||
if (error?.response?.status === 403 && error?.response?.data?.id) {
|
||||
// The user is waitlisted
|
||||
const account = error.response.data;
|
||||
const account = error.data;
|
||||
dispatch(importFetchedAccount(account));
|
||||
dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account });
|
||||
if (account.id === getState().me) dispatch(fetchMeSuccess(account));
|
||||
|
@ -163,18 +163,31 @@ export const verifyCredentials = (token: string, accountUrl?: string) => {
|
|||
};
|
||||
};
|
||||
|
||||
export class MfaRequiredError extends Error {
|
||||
|
||||
constructor(public token: string) {
|
||||
super('MFA is required');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const logIn = (username: string, password: string) =>
|
||||
(dispatch: AppDispatch) => dispatch(getAuthApp()).then(() => {
|
||||
return dispatch(createUserToken(normalizeUsername(username), password));
|
||||
}).catch((error: AxiosError) => {
|
||||
if ((error.response?.data as any)?.error === 'mfa_required') {
|
||||
// If MFA is required, throw the error and handle it in the component.
|
||||
throw error;
|
||||
} else if ((error.response?.data as any)?.identifier === 'awaiting_approval') {
|
||||
toast.error(messages.awaitingApproval);
|
||||
} else {
|
||||
// Return "wrong password" message.
|
||||
toast.error(messages.invalidCredentials);
|
||||
}).catch(async (error) => {
|
||||
if (error instanceof HTTPError) {
|
||||
const data = await error.response.error();
|
||||
if (data) {
|
||||
if (data.error === 'mfa_required' && 'mfa_token' in data && typeof data.mfa_token === 'string') {
|
||||
// If MFA is required, throw the error and handle it in the component.
|
||||
throw new MfaRequiredError(data.mfa_token);
|
||||
} else if (data.error === 'awaiting_approval') {
|
||||
toast.error(messages.awaitingApproval);
|
||||
} else {
|
||||
// Return "wrong password" message.
|
||||
toast.error(messages.invalidCredentials);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ export const BACKUPS_CREATE_FAIL = 'BACKUPS_CREATE_FAIL';
|
|||
export const fetchBackups = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: BACKUPS_FETCH_REQUEST });
|
||||
return api(getState).get('/api/v1/pleroma/backups').then(({ data: backups }) =>
|
||||
return api(getState).get('/api/v1/pleroma/backups').then((response) => response.json()).then((backups) =>
|
||||
dispatch({ type: BACKUPS_FETCH_SUCCESS, backups }),
|
||||
).catch(error => {
|
||||
dispatch({ type: BACKUPS_FETCH_FAIL, error });
|
||||
|
@ -23,7 +23,7 @@ export const fetchBackups = () =>
|
|||
export const createBackup = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: BACKUPS_CREATE_REQUEST });
|
||||
return api(getState).post('/api/v1/pleroma/backups').then(({ data: backups }) =>
|
||||
return api(getState).post('/api/v1/pleroma/backups').then((response) => response.json()).then((backups) =>
|
||||
dispatch({ type: BACKUPS_CREATE_SUCCESS, backups }),
|
||||
).catch(error => {
|
||||
dispatch({ type: BACKUPS_CREATE_FAIL, error });
|
||||
|
|
|
@ -1,190 +0,0 @@
|
|||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers.tsx';
|
||||
import { ListRecord, ReducerRecord as UserListsRecord } from 'soapbox/reducers/user-lists.ts';
|
||||
|
||||
import { expandBlocks, fetchBlocks } from './blocks.ts';
|
||||
|
||||
const account = {
|
||||
acct: 'twoods',
|
||||
display_name: 'Tiger Woods',
|
||||
id: '22',
|
||||
username: 'twoods',
|
||||
};
|
||||
|
||||
describe('fetchBlocks()', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
describe('if logged out', () => {
|
||||
beforeEach(() => {
|
||||
const state = { ...rootState, me: null };
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
it('should do nothing', async() => {
|
||||
await store.dispatch(fetchBlocks());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if logged in', () => {
|
||||
beforeEach(() => {
|
||||
const state = { ...rootState, me: '1234' };
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(async () => {
|
||||
const blocks = await import('soapbox/__fixtures__/blocks.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/blocks').reply(200, blocks, {
|
||||
link: '<https://example.com/api/v1/blocks?since_id=1>; rel=\'prev\'',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch blocks from the API', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'BLOCKS_FETCH_REQUEST' },
|
||||
{ type: 'ACCOUNTS_IMPORT', accounts: [account] },
|
||||
{ type: 'BLOCKS_FETCH_SUCCESS', accounts: [account], next: null },
|
||||
{
|
||||
type: 'RELATIONSHIPS_FETCH_REQUEST',
|
||||
ids: ['22'],
|
||||
skipLoading: true,
|
||||
},
|
||||
];
|
||||
await store.dispatch(fetchBlocks());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/blocks').networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch failed action', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'BLOCKS_FETCH_REQUEST' },
|
||||
{ type: 'BLOCKS_FETCH_FAIL', error: new Error('Network Error') },
|
||||
];
|
||||
await store.dispatch(fetchBlocks());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('expandBlocks()', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
describe('if logged out', () => {
|
||||
beforeEach(() => {
|
||||
const state = { ...rootState, me: null };
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
it('should do nothing', async() => {
|
||||
await store.dispatch(expandBlocks());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if logged in', () => {
|
||||
beforeEach(() => {
|
||||
const state = { ...rootState, me: '1234' };
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('without a url', () => {
|
||||
beforeEach(() => {
|
||||
const state = {
|
||||
...rootState,
|
||||
me: '1234',
|
||||
user_lists: UserListsRecord({ blocks: ListRecord({ next: null }) }),
|
||||
};
|
||||
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
it('should do nothing', async() => {
|
||||
await store.dispatch(expandBlocks());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a url', () => {
|
||||
beforeEach(() => {
|
||||
const state = {
|
||||
...rootState,
|
||||
me: '1234',
|
||||
user_lists: UserListsRecord({ blocks: ListRecord({ next: 'example' }) }),
|
||||
};
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(async () => {
|
||||
const blocks = await import('soapbox/__fixtures__/blocks.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet('example').reply(200, blocks, {
|
||||
link: '<https://example.com/api/v1/blocks?since_id=1>; rel=\'prev\'',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch blocks from the url', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'BLOCKS_EXPAND_REQUEST' },
|
||||
{ type: 'ACCOUNTS_IMPORT', accounts: [account] },
|
||||
{ type: 'BLOCKS_EXPAND_SUCCESS', accounts: [account], next: null },
|
||||
{
|
||||
type: 'RELATIONSHIPS_FETCH_REQUEST',
|
||||
ids: ['22'],
|
||||
skipLoading: true,
|
||||
},
|
||||
];
|
||||
await store.dispatch(expandBlocks());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('example').networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch failed action', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'BLOCKS_EXPAND_REQUEST' },
|
||||
{ type: 'BLOCKS_EXPAND_FAIL', error: new Error('Network Error') },
|
||||
];
|
||||
await store.dispatch(expandBlocks());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import { isLoggedIn } from 'soapbox/utils/auth.ts';
|
||||
|
||||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import { fetchRelationships } from './accounts.ts';
|
||||
import { importFetchedAccounts } from './importer/index.ts';
|
||||
|
@ -22,11 +22,12 @@ const fetchBlocks = () => (dispatch: AppDispatch, getState: () => RootState) =>
|
|||
|
||||
return api(getState)
|
||||
.get('/api/v1/blocks')
|
||||
.then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map((item: any) => item.id)) as any);
|
||||
.then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchBlocksSuccess(data, next));
|
||||
dispatch(fetchRelationships(data.map((item: any) => item.id)) as any);
|
||||
})
|
||||
.catch(error => dispatch(fetchBlocksFail(error)));
|
||||
};
|
||||
|
@ -63,11 +64,12 @@ const expandBlocks = () => (dispatch: AppDispatch, getState: () => RootState) =>
|
|||
|
||||
return api(getState)
|
||||
.get(url)
|
||||
.then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandBlocksSuccess(response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map((item: any) => item.id)) as any);
|
||||
.then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(expandBlocksSuccess(data, next));
|
||||
dispatch(fetchRelationships(data.map((item: any) => item.id)) as any);
|
||||
})
|
||||
.catch(error => dispatch(expandBlocksFail(error)));
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import { importFetchedStatuses } from './importer/index.ts';
|
||||
|
||||
|
@ -23,10 +23,11 @@ const fetchBookmarkedStatuses = () =>
|
|||
|
||||
dispatch(fetchBookmarkedStatusesRequest());
|
||||
|
||||
return api(getState).get('/api/v1/bookmarks').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
return dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
return api(getState).get('/api/v1/bookmarks').then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedStatuses(data));
|
||||
return dispatch(fetchBookmarkedStatusesSuccess(data, next));
|
||||
}).catch(error => {
|
||||
dispatch(fetchBookmarkedStatusesFail(error));
|
||||
});
|
||||
|
@ -58,10 +59,11 @@ const expandBookmarkedStatuses = () =>
|
|||
|
||||
dispatch(expandBookmarkedStatusesRequest());
|
||||
|
||||
return api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
return dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
return api(getState).get(url).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedStatuses(data));
|
||||
return dispatch(expandBookmarkedStatusesSuccess(data, next));
|
||||
}).catch(error => {
|
||||
dispatch(expandBookmarkedStatusesFail(error));
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
|||
import { getSettings, changeSetting } from 'soapbox/actions/settings.ts';
|
||||
import { getFeatures } from 'soapbox/utils/features.ts';
|
||||
|
||||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store.ts';
|
||||
import type { History } from 'soapbox/types/history.ts';
|
||||
|
@ -38,22 +38,19 @@ const CHAT_MESSAGE_DELETE_FAIL = 'CHAT_MESSAGE_DELETE_FAIL';
|
|||
|
||||
const fetchChatsV1 = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
api(getState).get('/api/v1/pleroma/chats').then((response) => {
|
||||
dispatch({ type: CHATS_FETCH_SUCCESS, chats: response.data });
|
||||
api(getState).get('/api/v1/pleroma/chats').then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: CHATS_FETCH_SUCCESS, chats: data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHATS_FETCH_FAIL, error });
|
||||
});
|
||||
|
||||
const fetchChatsV2 = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
api(getState).get('/api/v2/pleroma/chats').then((response) => {
|
||||
let next: { uri: string } | undefined = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
api(getState).get('/api/v2/pleroma/chats').then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
if (!next && response.data.length) {
|
||||
next = { uri: `/api/v2/pleroma/chats?max_id=${response.data[response.data.length - 1].id}&offset=0` };
|
||||
}
|
||||
|
||||
dispatch({ type: CHATS_FETCH_SUCCESS, chats: response.data, next: next ? next.uri : null });
|
||||
dispatch({ type: CHATS_FETCH_SUCCESS, chats: data, next });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHATS_FETCH_FAIL, error });
|
||||
});
|
||||
|
@ -81,10 +78,11 @@ const expandChats = () =>
|
|||
}
|
||||
|
||||
dispatch({ type: CHATS_EXPAND_REQUEST });
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
api(getState).get(url).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
dispatch({ type: CHATS_EXPAND_SUCCESS, chats: response.data, next: next ? next.uri : null });
|
||||
dispatch({ type: CHATS_EXPAND_SUCCESS, chats: data, next });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHATS_EXPAND_FAIL, error });
|
||||
});
|
||||
|
@ -93,7 +91,8 @@ const expandChats = () =>
|
|||
const fetchChatMessages = (chatId: string, maxId: string | null = null) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId, maxId });
|
||||
return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`, { params: { max_id: maxId } }).then(({ data }) => {
|
||||
const searchParams = maxId ? { max_id: maxId } : undefined;
|
||||
return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`, { searchParams }).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: CHAT_MESSAGES_FETCH_SUCCESS, chatId, maxId, chatMessages: data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, maxId, error });
|
||||
|
@ -105,7 +104,7 @@ const sendChatMessage = (chatId: string, params: Record<string, any>) =>
|
|||
const uuid = `末_${Date.now()}_${crypto.randomUUID()}`;
|
||||
const me = getState().me;
|
||||
dispatch({ type: CHAT_MESSAGE_SEND_REQUEST, chatId, params, uuid, me });
|
||||
return api(getState).post(`/api/v1/pleroma/chats/${chatId}/messages`, params).then(({ data }) => {
|
||||
return api(getState).post(`/api/v1/pleroma/chats/${chatId}/messages`, params).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: CHAT_MESSAGE_SEND_SUCCESS, chatId, chatMessage: data, uuid });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHAT_MESSAGE_SEND_FAIL, chatId, error, uuid });
|
||||
|
@ -164,7 +163,7 @@ const toggleMainWindow = () =>
|
|||
const fetchChat = (chatId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: CHAT_FETCH_REQUEST, chatId });
|
||||
return api(getState).get(`/api/v1/pleroma/chats/${chatId}`).then(({ data }) => {
|
||||
return api(getState).get(`/api/v1/pleroma/chats/${chatId}`).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: CHAT_FETCH_SUCCESS, chat: data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHAT_FETCH_FAIL, chatId, error });
|
||||
|
@ -174,7 +173,7 @@ const fetchChat = (chatId: string) =>
|
|||
const startChat = (accountId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: CHAT_FETCH_REQUEST, accountId });
|
||||
return api(getState).post(`/api/v1/pleroma/chats/by-account-id/${accountId}`).then(({ data }) => {
|
||||
return api(getState).post(`/api/v1/pleroma/chats/by-account-id/${accountId}`).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: CHAT_FETCH_SUCCESS, chat: data });
|
||||
return data;
|
||||
}).catch(error => {
|
||||
|
@ -191,7 +190,7 @@ const markChatRead = (chatId: string, lastReadId?: string | null) =>
|
|||
if (!lastReadId) return;
|
||||
|
||||
dispatch({ type: CHAT_READ_REQUEST, chatId, lastReadId });
|
||||
api(getState).post(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId }).then(({ data }) => {
|
||||
api(getState).post(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId }).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: CHAT_READ_SUCCESS, chat: data, lastReadId });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHAT_READ_FAIL, chatId, error, lastReadId });
|
||||
|
@ -201,7 +200,7 @@ const markChatRead = (chatId: string, lastReadId?: string | null) =>
|
|||
const deleteChatMessage = (chatId: string, messageId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: CHAT_MESSAGE_DELETE_REQUEST, chatId, messageId });
|
||||
api(getState).delete(`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`).then(({ data }) => {
|
||||
api(getState).delete(`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: CHAT_MESSAGE_DELETE_SUCCESS, chatId, messageId, chatMessage: data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHAT_MESSAGE_DELETE_FAIL, chatId, messageId, error });
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import axios, { Canceler } from 'axios';
|
||||
import { throttle } from 'es-toolkit';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { defineMessages, IntlShape } from 'react-intl';
|
||||
|
||||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||
import api from 'soapbox/api/index.ts';
|
||||
import { isNativeEmoji } from 'soapbox/features/emoji/index.ts';
|
||||
import emojiSearch from 'soapbox/features/emoji/search.ts';
|
||||
|
@ -29,9 +29,7 @@ import type { AppDispatch, RootState } from 'soapbox/store.ts';
|
|||
import type { APIEntity, Status, Tag } from 'soapbox/types/entities.ts';
|
||||
import type { History } from 'soapbox/types/history.ts';
|
||||
|
||||
const { CancelToken, isCancel } = axios;
|
||||
|
||||
let cancelFetchComposeSuggestions: Canceler;
|
||||
let cancelFetchComposeSuggestions: AbortController | undefined;
|
||||
|
||||
const COMPOSE_CHANGE = 'COMPOSE_CHANGE' as const;
|
||||
const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST' as const;
|
||||
|
@ -369,9 +367,7 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
|
|||
const attachmentLimit = getState().instance.configuration.statuses.max_media_attachments;
|
||||
|
||||
const media = getState().compose.get(composeId)?.media_attachments;
|
||||
const progress = new Array(files.length).fill(0);
|
||||
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
||||
|
||||
const progress: number[] = new Array(files.length).fill(0);
|
||||
const mediaCount = media ? media.size : 0;
|
||||
|
||||
if (files.length + mediaCount > attachmentLimit) {
|
||||
|
@ -389,11 +385,10 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
|
|||
intl,
|
||||
(data) => dispatch(uploadComposeSuccess(composeId, data, f)),
|
||||
(error) => dispatch(uploadComposeFail(composeId, error)),
|
||||
({ loaded }: any) => {
|
||||
progress[i] = loaded;
|
||||
dispatch(uploadComposeProgress(composeId, progress.reduce((a, v) => a + v, 0), total));
|
||||
(e: ProgressEvent) => {
|
||||
progress[i] = e.loaded;
|
||||
dispatch(uploadComposeProgress(composeId, progress.reduce((a, v) => a + v, 0), e.total));
|
||||
},
|
||||
(value) => total += value,
|
||||
));
|
||||
|
||||
});
|
||||
|
@ -433,8 +428,8 @@ const changeUploadCompose = (composeId: string, id: string, params: Record<strin
|
|||
|
||||
dispatch(changeUploadComposeRequest(composeId));
|
||||
|
||||
dispatch(updateMedia(id, params)).then(response => {
|
||||
dispatch(changeUploadComposeSuccess(composeId, response.data));
|
||||
dispatch(updateMedia(id, params)).then((response) => response.json()).then((data) => {
|
||||
dispatch(changeUploadComposeSuccess(composeId, data));
|
||||
}).catch(error => {
|
||||
dispatch(changeUploadComposeFail(composeId, id, error));
|
||||
});
|
||||
|
@ -480,9 +475,8 @@ const setGroupTimelineVisible = (composeId: string, groupTimelineVisible: boolea
|
|||
});
|
||||
|
||||
const clearComposeSuggestions = (composeId: string) => {
|
||||
if (cancelFetchComposeSuggestions) {
|
||||
cancelFetchComposeSuggestions();
|
||||
}
|
||||
cancelFetchComposeSuggestions?.abort();
|
||||
|
||||
return {
|
||||
type: COMPOSE_SUGGESTIONS_CLEAR,
|
||||
id: composeId,
|
||||
|
@ -490,23 +484,20 @@ const clearComposeSuggestions = (composeId: string) => {
|
|||
};
|
||||
|
||||
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId, token) => {
|
||||
if (cancelFetchComposeSuggestions) {
|
||||
cancelFetchComposeSuggestions(composeId);
|
||||
}
|
||||
cancelFetchComposeSuggestions?.abort();
|
||||
|
||||
api(getState).get('/api/v1/accounts/search', {
|
||||
cancelToken: new CancelToken(cancel => {
|
||||
cancelFetchComposeSuggestions = cancel;
|
||||
}),
|
||||
params: {
|
||||
signal: cancelFetchComposeSuggestions?.signal,
|
||||
searchParams: {
|
||||
q: token.slice(1),
|
||||
resolve: false,
|
||||
limit: 10,
|
||||
},
|
||||
}).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(readyComposeSuggestionsAccounts(composeId, token, response.data));
|
||||
}).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(readyComposeSuggestionsAccounts(composeId, token, data));
|
||||
}).catch(error => {
|
||||
if (!isCancel(error)) {
|
||||
if (error instanceof HTTPError) {
|
||||
toast.showAlertForError(error);
|
||||
}
|
||||
});
|
||||
|
@ -519,9 +510,7 @@ const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, composeId: string,
|
|||
};
|
||||
|
||||
const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => {
|
||||
if (cancelFetchComposeSuggestions) {
|
||||
cancelFetchComposeSuggestions(composeId);
|
||||
}
|
||||
cancelFetchComposeSuggestions?.abort();
|
||||
|
||||
const state = getState();
|
||||
|
||||
|
@ -535,18 +524,16 @@ const fetchComposeSuggestionsTags = (dispatch: AppDispatch, getState: () => Root
|
|||
}
|
||||
|
||||
api(getState).get('/api/v2/search', {
|
||||
cancelToken: new CancelToken(cancel => {
|
||||
cancelFetchComposeSuggestions = cancel;
|
||||
}),
|
||||
params: {
|
||||
signal: cancelFetchComposeSuggestions?.signal,
|
||||
searchParams: {
|
||||
q: token.slice(1),
|
||||
limit: 10,
|
||||
type: 'hashtags',
|
||||
},
|
||||
}).then(response => {
|
||||
dispatch(updateSuggestionTags(composeId, token, response.data?.hashtags.map(normalizeTag)));
|
||||
}).then((response) => response.json()).then((data) => {
|
||||
dispatch(updateSuggestionTags(composeId, token, data?.hashtags.map(normalizeTag)));
|
||||
}).catch(error => {
|
||||
if (!isCancel(error)) {
|
||||
if (error instanceof HTTPError) {
|
||||
toast.showAlertForError(error);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import axios from 'axios';
|
||||
|
||||
import * as BuildConfig from 'soapbox/build-config.ts';
|
||||
import { isURL } from 'soapbox/utils/auth.ts';
|
||||
import sourceCode from 'soapbox/utils/code.ts';
|
||||
|
@ -36,17 +34,12 @@ export const prepareRequest = (provider: string) => {
|
|||
localStorage.setItem('soapbox:external:baseurl', baseURL);
|
||||
localStorage.setItem('soapbox:external:scopes', scopes);
|
||||
|
||||
const params = {
|
||||
provider,
|
||||
authorization: {
|
||||
client_id,
|
||||
redirect_uri,
|
||||
scope: scopes,
|
||||
},
|
||||
};
|
||||
const query = new URLSearchParams({ provider });
|
||||
|
||||
const formdata = axios.toFormData(params);
|
||||
const query = new URLSearchParams(formdata as any);
|
||||
// FIXME: I don't know if this is the correct way to encode the query params.
|
||||
query.append('authorization.client_id', client_id);
|
||||
query.append('authorization.redirect_uri', redirect_uri);
|
||||
query.append('authorization.scope', scopes);
|
||||
|
||||
location.href = `${baseURL}/oauth/prepare_request?${query.toString()}`;
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { isLoggedIn } from 'soapbox/utils/auth.ts';
|
||||
|
||||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import {
|
||||
importFetchedAccounts,
|
||||
|
@ -53,13 +53,14 @@ const expandConversations = ({ maxId }: Record<string, any> = {}) => (dispatch:
|
|||
|
||||
const isLoadingRecent = !!params.since_id;
|
||||
|
||||
api(getState).get('/api/v1/conversations', { params })
|
||||
.then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
api(getState).get('/api/v1/conversations', { searchParams: params })
|
||||
.then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
dispatch(importFetchedAccounts(response.data.reduce((aggr: Array<APIEntity>, item: APIEntity) => aggr.concat(item.accounts), [])));
|
||||
dispatch(importFetchedStatuses(response.data.map((item: Record<string, any>) => item.last_status).filter((x?: APIEntity) => !!x)));
|
||||
dispatch(expandConversationsSuccess(response.data, next ? next.uri : null, isLoadingRecent));
|
||||
dispatch(importFetchedAccounts(data.reduce((aggr: Array<APIEntity>, item: APIEntity) => aggr.concat(item.accounts), [])));
|
||||
dispatch(importFetchedStatuses(data.map((item: Record<string, any>) => item.last_status).filter((x?: APIEntity) => !!x)));
|
||||
dispatch(expandConversationsSuccess(data, next, isLoadingRecent));
|
||||
})
|
||||
.catch(err => dispatch(expandConversationsFail(err)));
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@ const fetchDirectory = (params: Record<string, any>) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchDirectoryRequest());
|
||||
|
||||
api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
|
||||
api(getState).get('/api/v1/directory', { searchParams: { ...params, limit: 20 } }).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchDirectorySuccess(data));
|
||||
dispatch(fetchRelationships(data.map((x: APIEntity) => x.id)));
|
||||
|
@ -45,7 +45,7 @@ const expandDirectory = (params: Record<string, any>) =>
|
|||
|
||||
const loadedItems = getState().user_lists.directory.items.size;
|
||||
|
||||
api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
|
||||
api(getState).get('/api/v1/directory', { searchParams: { ...params, offset: loadedItems, limit: 20 } }).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(expandDirectorySuccess(data));
|
||||
dispatch(fetchRelationships(data.map((x: APIEntity) => x.id)));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities.ts';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth.ts';
|
||||
|
||||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import type { EntityStore } from 'soapbox/entity-store/types.ts';
|
||||
import type { Account } from 'soapbox/schemas/index.ts';
|
||||
|
@ -61,13 +61,10 @@ const unblockDomain = (domain: string) =>
|
|||
|
||||
dispatch(unblockDomainRequest(domain));
|
||||
|
||||
// Do it both ways for maximum compatibility
|
||||
const params = {
|
||||
params: { domain },
|
||||
data: { domain },
|
||||
};
|
||||
const data = new FormData();
|
||||
data.append('domain', domain);
|
||||
|
||||
api(getState).delete('/api/v1/domain_blocks', params).then(() => {
|
||||
api(getState).request('DELETE', '/api/v1/domain_blocks', data).then(() => {
|
||||
const accounts = selectAccountsByDomain(getState(), domain);
|
||||
if (!accounts) return;
|
||||
dispatch(unblockDomainSuccess(domain, accounts));
|
||||
|
@ -99,9 +96,10 @@ const fetchDomainBlocks = () =>
|
|||
|
||||
dispatch(fetchDomainBlocksRequest());
|
||||
|
||||
api(getState).get('/api/v1/domain_blocks').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null));
|
||||
api(getState).get('/api/v1/domain_blocks').then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(fetchDomainBlocksSuccess(data, next));
|
||||
}).catch(err => {
|
||||
dispatch(fetchDomainBlocksFail(err));
|
||||
});
|
||||
|
@ -134,9 +132,10 @@ const expandDomainBlocks = () =>
|
|||
|
||||
dispatch(expandDomainBlocksRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null));
|
||||
api(getState).get(url).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(expandDomainBlocksSuccess(data, next));
|
||||
}).catch(err => {
|
||||
dispatch(expandDomainBlocksFail(err));
|
||||
});
|
||||
|
|
|
@ -59,11 +59,11 @@ const fetchEmojiReacts = (id: string, emoji: string) =>
|
|||
? `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||
: `/api/v1/pleroma/statuses/${id}/reactions`;
|
||||
|
||||
return api(getState).get(url).then(response => {
|
||||
response.data.forEach((emojiReact: APIEntity) => {
|
||||
return api(getState).get(url).then((response) => response.json()).then((data) => {
|
||||
data.forEach((emojiReact: APIEntity) => {
|
||||
dispatch(importFetchedAccounts(emojiReact.accounts));
|
||||
});
|
||||
dispatch(fetchEmojiReactsSuccess(id, response.data));
|
||||
dispatch(fetchEmojiReactsSuccess(id, data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchEmojiReactsFail(id, error));
|
||||
});
|
||||
|
@ -77,10 +77,10 @@ const emojiReact = (status: Status, emoji: string, custom?: string) =>
|
|||
|
||||
return api(getState)
|
||||
.put(`/api/v1/pleroma/statuses/${status.id}/reactions/${emoji}`)
|
||||
.then(function(response) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(emojiReactSuccess(status, emoji));
|
||||
}).catch(function(error) {
|
||||
}).catch((error) => {
|
||||
dispatch(emojiReactFail(status, emoji, error));
|
||||
});
|
||||
};
|
||||
|
@ -93,8 +93,8 @@ const unEmojiReact = (status: Status, emoji: string) =>
|
|||
|
||||
return api(getState)
|
||||
.delete(`/api/v1/pleroma/statuses/${status.id}/reactions/${emoji}`)
|
||||
.then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(unEmojiReactSuccess(status, emoji));
|
||||
}).catch(error => {
|
||||
dispatch(unEmojiReactFail(status, emoji, error));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { defineMessages, IntlShape } from 'react-intl';
|
||||
|
||||
import api, { getLinks } from 'soapbox/api/index.ts';
|
||||
import api from 'soapbox/api/index.ts';
|
||||
import toast from 'soapbox/toast.tsx';
|
||||
|
||||
import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer/index.ts';
|
||||
|
@ -97,7 +97,7 @@ const messages = defineMessages({
|
|||
const locationSearch = (query: string, signal?: AbortSignal) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: LOCATION_SEARCH_REQUEST, query });
|
||||
return api(getState).get('/api/v1/pleroma/search/location', { params: { q: query }, signal }).then(({ data: locations }) => {
|
||||
return api(getState).get('/api/v1/pleroma/search/location', { searchParams: { q: query }, signal }).then((response) => response.json()).then((locations) => {
|
||||
dispatch({ type: LOCATION_SEARCH_SUCCESS, locations });
|
||||
return locations;
|
||||
}).catch(error => {
|
||||
|
@ -161,7 +161,7 @@ const uploadEventBanner = (file: File, intl: IntlShape) =>
|
|||
intl,
|
||||
(data) => dispatch(uploadEventBannerSuccess(data, file)),
|
||||
(error) => dispatch(uploadEventBannerFail(error)),
|
||||
({ loaded }: any) => {
|
||||
({ loaded }: ProgressEvent) => {
|
||||
progress = loaded;
|
||||
dispatch(uploadEventBannerProgress(progress));
|
||||
},
|
||||
|
@ -223,11 +223,10 @@ const submitEvent = () =>
|
|||
if (banner) params.banner_id = banner.id;
|
||||
if (location) params.location_id = location.origin_id;
|
||||
|
||||
return api(getState).request({
|
||||
url: id === null ? '/api/v1/pleroma/events' : `/api/v1/pleroma/events/${id}`,
|
||||
method: id === null ? 'post' : 'put',
|
||||
data: params,
|
||||
}).then(({ data }) => {
|
||||
const method = id === null ? 'POST' : 'PUT';
|
||||
const path = id === null ? '/api/v1/pleroma/events' : `/api/v1/pleroma/events/${id}`;
|
||||
|
||||
return api(getState).request(method, path, params).then((response) => response.json()).then((data) => {
|
||||
dispatch(closeModal('COMPOSE_EVENT'));
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(submitEventSuccess(data));
|
||||
|
@ -269,7 +268,7 @@ const joinEvent = (id: string, participationMessage?: string) =>
|
|||
|
||||
return api(getState).post(`/api/v1/pleroma/events/${id}/join`, {
|
||||
participation_message: participationMessage,
|
||||
}).then(({ data }) => {
|
||||
}).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(joinEventSuccess(data));
|
||||
toast.success(
|
||||
|
@ -311,7 +310,7 @@ const leaveEvent = (id: string) =>
|
|||
|
||||
dispatch(leaveEventRequest(status));
|
||||
|
||||
return api(getState).post(`/api/v1/pleroma/events/${id}/leave`).then(({ data }) => {
|
||||
return api(getState).post(`/api/v1/pleroma/events/${id}/leave`).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(leaveEventSuccess(data));
|
||||
}).catch(function(error) {
|
||||
|
@ -339,10 +338,11 @@ const fetchEventParticipations = (id: string) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchEventParticipationsRequest(id));
|
||||
|
||||
return api(getState).get(`/api/v1/pleroma/events/${id}/participations`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
return dispatch(fetchEventParticipationsSuccess(id, response.data, next ? next.uri : null));
|
||||
return api(getState).get(`/api/v1/pleroma/events/${id}/participations`).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedAccounts(data));
|
||||
return dispatch(fetchEventParticipationsSuccess(id, data, next));
|
||||
}).catch(error => {
|
||||
dispatch(fetchEventParticipationsFail(id, error));
|
||||
});
|
||||
|
@ -376,10 +376,11 @@ const expandEventParticipations = (id: string) =>
|
|||
|
||||
dispatch(expandEventParticipationsRequest(id));
|
||||
|
||||
return api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
return dispatch(expandEventParticipationsSuccess(id, response.data, next ? next.uri : null));
|
||||
return api(getState).get(url).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedAccounts(data));
|
||||
return dispatch(expandEventParticipationsSuccess(id, data, next));
|
||||
}).catch(error => {
|
||||
dispatch(expandEventParticipationsFail(id, error));
|
||||
});
|
||||
|
@ -407,10 +408,11 @@ const fetchEventParticipationRequests = (id: string) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchEventParticipationRequestsRequest(id));
|
||||
|
||||
return api(getState).get(`/api/v1/pleroma/events/${id}/participation_requests`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data.map(({ account }: APIEntity) => account)));
|
||||
return dispatch(fetchEventParticipationRequestsSuccess(id, response.data, next ? next.uri : null));
|
||||
return api(getState).get(`/api/v1/pleroma/events/${id}/participation_requests`).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedAccounts(data.map(({ account }: APIEntity) => account)));
|
||||
return dispatch(fetchEventParticipationRequestsSuccess(id, data, next));
|
||||
}).catch(error => {
|
||||
dispatch(fetchEventParticipationRequestsFail(id, error));
|
||||
});
|
||||
|
@ -444,10 +446,11 @@ const expandEventParticipationRequests = (id: string) =>
|
|||
|
||||
dispatch(expandEventParticipationRequestsRequest(id));
|
||||
|
||||
return api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data.map(({ account }: APIEntity) => account)));
|
||||
return dispatch(expandEventParticipationRequestsSuccess(id, response.data, next ? next.uri : null));
|
||||
return api(getState).get(url).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedAccounts(data.map(({ account }: APIEntity) => account)));
|
||||
return dispatch(expandEventParticipationRequestsSuccess(id, data, next));
|
||||
}).catch(error => {
|
||||
dispatch(expandEventParticipationRequestsFail(id, error));
|
||||
});
|
||||
|
@ -555,13 +558,13 @@ const editEvent = (id: string) => (dispatch: AppDispatch, getState: () => RootSt
|
|||
|
||||
dispatch({ type: STATUS_FETCH_SOURCE_REQUEST });
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
|
||||
api(getState).get(`/api/v1/statuses/${id}/source`).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS });
|
||||
dispatch({
|
||||
type: EVENT_FORM_SET,
|
||||
status,
|
||||
text: response.data.text,
|
||||
location: response.data.location,
|
||||
text: data.text,
|
||||
location: data.location,
|
||||
});
|
||||
dispatch(openModal('COMPOSE_EVENT'));
|
||||
}).catch(error => {
|
||||
|
@ -577,13 +580,15 @@ const fetchRecentEvents = () =>
|
|||
|
||||
dispatch({ type: RECENT_EVENTS_FETCH_REQUEST });
|
||||
|
||||
api(getState).get('/api/v1/timelines/public?only_events=true').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
api(getState).get('/api/v1/timelines/public?only_events=true').then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
dispatch(importFetchedStatuses(data));
|
||||
dispatch({
|
||||
type: RECENT_EVENTS_FETCH_SUCCESS,
|
||||
statuses: response.data,
|
||||
next: next ? next.uri : null,
|
||||
statuses: data,
|
||||
next,
|
||||
});
|
||||
}).catch(error => {
|
||||
dispatch({ type: RECENT_EVENTS_FETCH_FAIL, error });
|
||||
|
@ -598,13 +603,15 @@ const fetchJoinedEvents = () =>
|
|||
|
||||
dispatch({ type: JOINED_EVENTS_FETCH_REQUEST });
|
||||
|
||||
api(getState).get('/api/v1/pleroma/events/joined_events').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
api(getState).get('/api/v1/pleroma/events/joined_events').then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
dispatch(importFetchedStatuses(data));
|
||||
dispatch({
|
||||
type: JOINED_EVENTS_FETCH_SUCCESS,
|
||||
statuses: response.data,
|
||||
next: next ? next.uri : null,
|
||||
statuses: data,
|
||||
next,
|
||||
});
|
||||
}).catch(error => {
|
||||
dispatch({ type: JOINED_EVENTS_FETCH_FAIL, error });
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import api, { getLinks } from 'soapbox/api/index.ts';
|
||||
import { MastodonResponse } from 'soapbox/api/MastodonResponse.ts';
|
||||
import api from 'soapbox/api/index.ts';
|
||||
import { normalizeAccount } from 'soapbox/normalizers/index.ts';
|
||||
import toast from 'soapbox/toast.tsx';
|
||||
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import type { RootState } from 'soapbox/store.ts';
|
||||
|
||||
export const EXPORT_FOLLOWS_REQUEST = 'EXPORT_FOLLOWS_REQUEST';
|
||||
|
@ -49,18 +49,31 @@ function fileExport(content: string, fileName: string) {
|
|||
document.body.removeChild(fileToDownload);
|
||||
}
|
||||
|
||||
const listAccounts = (getState: () => RootState) => async(apiResponse: AxiosResponse<any, any>) => {
|
||||
const followings = apiResponse.data;
|
||||
let accounts = [];
|
||||
let next = getLinks(apiResponse).refs.find(link => link.rel === 'next');
|
||||
while (next) {
|
||||
apiResponse = await api(getState).get(next.uri);
|
||||
next = getLinks(apiResponse).refs.find(link => link.rel === 'next');
|
||||
Array.prototype.push.apply(followings, apiResponse.data);
|
||||
}
|
||||
const listAccounts = (getState: () => RootState) => {
|
||||
return async(response: MastodonResponse) => {
|
||||
let { next } = response.pagination();
|
||||
const data = await response.json();
|
||||
|
||||
accounts = followings.map((account: any) => normalizeAccount(account).fqn);
|
||||
return Array.from(new Set(accounts));
|
||||
const map = new Map<string, Record<string, any>>();
|
||||
|
||||
for (const account of data) {
|
||||
map.set(account.id, account);
|
||||
}
|
||||
|
||||
while (next) {
|
||||
const response = await api(getState).get(next);
|
||||
next = response.pagination().next;
|
||||
const data = await response.json();
|
||||
|
||||
for (const account of data) {
|
||||
map.set(account.id, account);
|
||||
}
|
||||
}
|
||||
|
||||
const accts = [...map.values()].map((account) => normalizeAccount(account).fqn);
|
||||
|
||||
return accts;
|
||||
};
|
||||
};
|
||||
|
||||
export const exportFollows = () => (dispatch: React.Dispatch<ExportDataActions>, getState: () => RootState) => {
|
||||
|
|
|
@ -21,7 +21,7 @@ import type { AppDispatch, RootState } from 'soapbox/store.ts';
|
|||
const fetchExternalInstance = (baseURL?: string) => {
|
||||
return baseClient(null, baseURL)
|
||||
.get('/api/v1/instance')
|
||||
.then(({ data: instance }) => instanceV1Schema.parse(instance))
|
||||
.then((response) => response.json()).then((instance) => instanceV1Schema.parse(instance))
|
||||
.catch(error => {
|
||||
if (error.response?.status === 401) {
|
||||
// Authenticated fetch is enabled.
|
||||
|
|
|
@ -18,7 +18,7 @@ export const fetchAccountFamiliarFollowers = (accountId: string) => (dispatch: A
|
|||
});
|
||||
|
||||
api(getState).get(`/api/v1/accounts/familiar_followers?id[]=${accountId}`)
|
||||
.then(({ data }) => {
|
||||
.then((response) => response.json()).then((data) => {
|
||||
const accounts = data.find(({ id }: { id: string }) => id === accountId).accounts;
|
||||
|
||||
dispatch(importFetchedAccounts(accounts));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { isLoggedIn } from 'soapbox/utils/auth.ts';
|
||||
|
||||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import { importFetchedStatuses } from './importer/index.ts';
|
||||
|
||||
|
@ -33,10 +33,11 @@ const fetchFavouritedStatuses = () =>
|
|||
|
||||
dispatch(fetchFavouritedStatusesRequest());
|
||||
|
||||
api(getState).get('/api/v1/favourites').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
api(getState).get('/api/v1/favourites').then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedStatuses(data));
|
||||
dispatch(fetchFavouritedStatusesSuccess(data, next));
|
||||
}).catch(error => {
|
||||
dispatch(fetchFavouritedStatusesFail(error));
|
||||
});
|
||||
|
@ -72,10 +73,11 @@ const expandFavouritedStatuses = () =>
|
|||
|
||||
dispatch(expandFavouritedStatusesRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
api(getState).get(url).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedStatuses(data));
|
||||
dispatch(expandFavouritedStatusesSuccess(data, next));
|
||||
}).catch(error => {
|
||||
dispatch(expandFavouritedStatusesFail(error));
|
||||
});
|
||||
|
@ -106,10 +108,11 @@ const fetchAccountFavouritedStatuses = (accountId: string) =>
|
|||
|
||||
dispatch(fetchAccountFavouritedStatusesRequest(accountId));
|
||||
|
||||
api(getState).get(`/api/v1/pleroma/accounts/${accountId}/favourites`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchAccountFavouritedStatusesSuccess(accountId, response.data, next ? next.uri : null));
|
||||
api(getState).get(`/api/v1/pleroma/accounts/${accountId}/favourites`).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedStatuses(data));
|
||||
dispatch(fetchAccountFavouritedStatusesSuccess(accountId, data, next));
|
||||
}).catch(error => {
|
||||
dispatch(fetchAccountFavouritedStatusesFail(accountId, error));
|
||||
});
|
||||
|
@ -148,10 +151,11 @@ const expandAccountFavouritedStatuses = (accountId: string) =>
|
|||
|
||||
dispatch(expandAccountFavouritedStatusesRequest(accountId));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(expandAccountFavouritedStatusesSuccess(accountId, response.data, next ? next.uri : null));
|
||||
api(getState).get(url).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedStatuses(data));
|
||||
dispatch(expandAccountFavouritedStatusesSuccess(accountId, data, next));
|
||||
}).catch(error => {
|
||||
dispatch(expandAccountFavouritedStatusesFail(accountId, error));
|
||||
});
|
||||
|
|
|
@ -44,7 +44,7 @@ const fetchFiltersV1 = () =>
|
|||
|
||||
return api(getState)
|
||||
.get('/api/v1/filters')
|
||||
.then(({ data }) => dispatch({
|
||||
.then((response) => response.json()).then((data) => dispatch({
|
||||
type: FILTERS_FETCH_SUCCESS,
|
||||
filters: data,
|
||||
skipLoading: true,
|
||||
|
@ -66,7 +66,7 @@ const fetchFiltersV2 = () =>
|
|||
|
||||
return api(getState)
|
||||
.get('/api/v2/filters')
|
||||
.then(({ data }) => dispatch({
|
||||
.then((response) => response.json()).then((data) => dispatch({
|
||||
type: FILTERS_FETCH_SUCCESS,
|
||||
filters: data,
|
||||
skipLoading: true,
|
||||
|
@ -101,7 +101,7 @@ const fetchFilterV1 = (id: string) =>
|
|||
|
||||
return api(getState)
|
||||
.get(`/api/v1/filters/${id}`)
|
||||
.then(({ data }) => dispatch({
|
||||
.then((response) => response.json()).then((data) => dispatch({
|
||||
type: FILTER_FETCH_SUCCESS,
|
||||
filter: data,
|
||||
skipLoading: true,
|
||||
|
@ -123,7 +123,7 @@ const fetchFilterV2 = (id: string) =>
|
|||
|
||||
return api(getState)
|
||||
.get(`/api/v2/filters/${id}`)
|
||||
.then(({ data }) => dispatch({
|
||||
.then((response) => response.json()).then((data) => dispatch({
|
||||
type: FILTER_FETCH_SUCCESS,
|
||||
filter: data,
|
||||
skipLoading: true,
|
||||
|
@ -156,8 +156,8 @@ const createFilterV1 = (title: string, expires_in: string | null, context: Array
|
|||
irreversible: hide,
|
||||
whole_word: keywords[0].whole_word,
|
||||
expires_in,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
|
||||
}).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: data });
|
||||
toast.success(messages.added);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_CREATE_FAIL, error });
|
||||
|
@ -173,8 +173,8 @@ const createFilterV2 = (title: string, expires_in: string | null, context: Array
|
|||
filter_action: hide ? 'hide' : 'warn',
|
||||
expires_in,
|
||||
keywords_attributes,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
|
||||
}).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: data });
|
||||
toast.success(messages.added);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_CREATE_FAIL, error });
|
||||
|
@ -201,8 +201,8 @@ const updateFilterV1 = (id: string, title: string, expires_in: string | null, co
|
|||
irreversible: hide,
|
||||
whole_word: keywords[0].whole_word,
|
||||
expires_in,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
|
||||
}).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: data });
|
||||
toast.success(messages.added);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_UPDATE_FAIL, error });
|
||||
|
@ -218,8 +218,8 @@ const updateFilterV2 = (id: string, title: string, expires_in: string | null, co
|
|||
filter_action: hide ? 'hide' : 'warn',
|
||||
expires_in,
|
||||
keywords_attributes,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
|
||||
}).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: data });
|
||||
toast.success(messages.added);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_UPDATE_FAIL, error });
|
||||
|
@ -240,8 +240,8 @@ const updateFilter = (id: string, title: string, expires_in: string | null, cont
|
|||
const deleteFilterV1 = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_DELETE_REQUEST });
|
||||
return api(getState).delete(`/api/v1/filters/${id}`).then(response => {
|
||||
dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data });
|
||||
return api(getState).delete(`/api/v1/filters/${id}`).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: FILTERS_DELETE_SUCCESS, filter: data });
|
||||
toast.success(messages.removed);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_DELETE_FAIL, error });
|
||||
|
@ -251,8 +251,8 @@ const deleteFilterV1 = (id: string) =>
|
|||
const deleteFilterV2 = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_DELETE_REQUEST });
|
||||
return api(getState).delete(`/api/v2/filters/${id}`).then(response => {
|
||||
dispatch({ type: FILTERS_DELETE_SUCCESS, filter: response.data });
|
||||
return api(getState).delete(`/api/v2/filters/${id}`).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: FILTERS_DELETE_SUCCESS, filter: data });
|
||||
toast.success(messages.removed);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_DELETE_FAIL, error });
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { deleteEntities } from 'soapbox/entity-store/actions.ts';
|
||||
|
||||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import { fetchRelationships } from './accounts.ts';
|
||||
import { importFetchedGroups, importFetchedAccounts } from './importer/index.ts';
|
||||
|
@ -114,7 +114,7 @@ const fetchGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootS
|
|||
dispatch(fetchGroupRequest(id));
|
||||
|
||||
return api(getState).get(`/api/v1/groups/${id}`)
|
||||
.then(({ data }) => {
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedGroups([data]));
|
||||
dispatch(fetchGroupSuccess(data));
|
||||
})
|
||||
|
@ -141,7 +141,7 @@ const fetchGroups = () => (dispatch: AppDispatch, getState: () => RootState) =>
|
|||
dispatch(fetchGroupsRequest());
|
||||
|
||||
return api(getState).get('/api/v1/groups')
|
||||
.then(({ data }) => {
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedGroups(data));
|
||||
dispatch(fetchGroupsSuccess(data));
|
||||
dispatch(fetchGroupRelationships(data.map((item: APIEntity) => item.id)));
|
||||
|
@ -174,8 +174,8 @@ const fetchGroupRelationships = (groupIds: string[]) =>
|
|||
|
||||
dispatch(fetchGroupRelationshipsRequest(newGroupIds));
|
||||
|
||||
return api(getState).get(`/api/v1/groups/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
|
||||
dispatch(fetchGroupRelationshipsSuccess(response.data));
|
||||
return api(getState).get(`/api/v1/groups/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then((response) => response.json()).then((data) => {
|
||||
dispatch(fetchGroupRelationshipsSuccess(data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchGroupRelationshipsFail(error));
|
||||
});
|
||||
|
@ -232,11 +232,12 @@ const fetchGroupBlocks = (id: string) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchGroupBlocksRequest(id));
|
||||
|
||||
return api(getState).get(`/api/v1/groups/${id}/blocks`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
return api(getState).get(`/api/v1/groups/${id}/blocks`).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchGroupBlocksSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchGroupBlocksSuccess(id, data, next));
|
||||
}).catch(error => {
|
||||
dispatch(fetchGroupBlocksFail(id, error));
|
||||
});
|
||||
|
@ -271,12 +272,13 @@ const expandGroupBlocks = (id: string) =>
|
|||
|
||||
dispatch(expandGroupBlocksRequest(id));
|
||||
|
||||
return api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
return api(getState).get(url).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandGroupBlocksSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(expandGroupBlocksSuccess(id, data, next));
|
||||
dispatch(fetchRelationships(data.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(expandGroupBlocksFail(id, error));
|
||||
});
|
||||
|
@ -361,7 +363,8 @@ const groupPromoteAccount = (groupId: string, accountId: string, role: GroupRole
|
|||
dispatch(groupPromoteAccountRequest(groupId, accountId));
|
||||
|
||||
return api(getState).post(`/api/v1/groups/${groupId}/promote`, { account_ids: [accountId], role: role })
|
||||
.then((response) => dispatch(groupPromoteAccountSuccess(groupId, accountId, response.data)))
|
||||
.then((response) => response.json())
|
||||
.then((data) => dispatch(groupPromoteAccountSuccess(groupId, accountId, data)))
|
||||
.catch(err => dispatch(groupPromoteAccountFail(groupId, accountId, err)));
|
||||
};
|
||||
|
||||
|
@ -390,7 +393,8 @@ const groupDemoteAccount = (groupId: string, accountId: string, role: GroupRole)
|
|||
dispatch(groupDemoteAccountRequest(groupId, accountId));
|
||||
|
||||
return api(getState).post(`/api/v1/groups/${groupId}/demote`, { account_ids: [accountId], role: role })
|
||||
.then((response) => dispatch(groupDemoteAccountSuccess(groupId, accountId, response.data)))
|
||||
.then((response) => response.json())
|
||||
.then((data) => dispatch(groupDemoteAccountSuccess(groupId, accountId, data)))
|
||||
.catch(err => dispatch(groupDemoteAccountFail(groupId, accountId, err)));
|
||||
};
|
||||
|
||||
|
@ -418,11 +422,12 @@ const fetchGroupMemberships = (id: string, role: GroupRole) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchGroupMembershipsRequest(id, role));
|
||||
|
||||
return api(getState).get(`/api/v1/groups/${id}/memberships`, { params: { role } }).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
return api(getState).get(`/api/v1/groups/${id}/memberships`, { searchParams: { role } }).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
dispatch(importFetchedAccounts(response.data.map((membership: APIEntity) => membership.account)));
|
||||
dispatch(fetchGroupMembershipsSuccess(id, role, response.data, next ? next.uri : null));
|
||||
dispatch(importFetchedAccounts(data.map((membership: APIEntity) => membership.account)));
|
||||
dispatch(fetchGroupMembershipsSuccess(id, role, data, next));
|
||||
}).catch(error => {
|
||||
dispatch(fetchGroupMembershipsFail(id, role, error));
|
||||
});
|
||||
|
@ -460,12 +465,13 @@ const expandGroupMemberships = (id: string, role: GroupRole) =>
|
|||
|
||||
dispatch(expandGroupMembershipsRequest(id, role));
|
||||
|
||||
return api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
return api(getState).get(url).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
dispatch(importFetchedAccounts(response.data.map((membership: APIEntity) => membership.account)));
|
||||
dispatch(expandGroupMembershipsSuccess(id, role, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(importFetchedAccounts(data.map((membership: APIEntity) => membership.account)));
|
||||
dispatch(expandGroupMembershipsSuccess(id, role, data, next));
|
||||
dispatch(fetchRelationships(data.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(expandGroupMembershipsFail(id, role, error));
|
||||
});
|
||||
|
@ -496,11 +502,12 @@ const fetchGroupMembershipRequests = (id: string) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchGroupMembershipRequestsRequest(id));
|
||||
|
||||
return api(getState).get(`/api/v1/groups/${id}/membership_requests`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
return api(getState).get(`/api/v1/groups/${id}/membership_requests`).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchGroupMembershipRequestsSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchGroupMembershipRequestsSuccess(id, data, next));
|
||||
}).catch(error => {
|
||||
dispatch(fetchGroupMembershipRequestsFail(id, error));
|
||||
});
|
||||
|
@ -535,12 +542,13 @@ const expandGroupMembershipRequests = (id: string) =>
|
|||
|
||||
dispatch(expandGroupMembershipRequestsRequest(id));
|
||||
|
||||
return api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
return api(getState).get(url).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandGroupMembershipRequestsSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(expandGroupMembershipRequestsSuccess(id, data, next));
|
||||
dispatch(fetchRelationships(data.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(expandGroupMembershipRequestsFail(id, error));
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ const fetchHistory = (statusId: string) =>
|
|||
|
||||
dispatch(fetchHistoryRequest(statusId));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => {
|
||||
api(getState).get(`/api/v1/statuses/${statusId}/history`).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedAccounts(data.map((x: APIEntity) => x.account)));
|
||||
dispatch(fetchHistorySuccess(statusId, data));
|
||||
}).catch(error => dispatch(fetchHistoryFail(error)));
|
||||
|
|
|
@ -43,9 +43,9 @@ export const importFollows = (params: FormData) =>
|
|||
dispatch({ type: IMPORT_FOLLOWS_REQUEST });
|
||||
return api(getState)
|
||||
.post('/api/pleroma/follow_import', params)
|
||||
.then(response => {
|
||||
.then((response) => response.json()).then((data) => {
|
||||
toast.success(messages.followersSuccess);
|
||||
dispatch({ type: IMPORT_FOLLOWS_SUCCESS, config: response.data });
|
||||
dispatch({ type: IMPORT_FOLLOWS_SUCCESS, config: data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: IMPORT_FOLLOWS_FAIL, error });
|
||||
});
|
||||
|
@ -56,9 +56,9 @@ export const importBlocks = (params: FormData) =>
|
|||
dispatch({ type: IMPORT_BLOCKS_REQUEST });
|
||||
return api(getState)
|
||||
.post('/api/pleroma/blocks_import', params)
|
||||
.then(response => {
|
||||
.then((response) => response.json()).then((data) => {
|
||||
toast.success(messages.blocksSuccess);
|
||||
dispatch({ type: IMPORT_BLOCKS_SUCCESS, config: response.data });
|
||||
dispatch({ type: IMPORT_BLOCKS_SUCCESS, config: data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: IMPORT_BLOCKS_FAIL, error });
|
||||
});
|
||||
|
@ -69,9 +69,9 @@ export const importMutes = (params: FormData) =>
|
|||
dispatch({ type: IMPORT_MUTES_REQUEST });
|
||||
return api(getState)
|
||||
.post('/api/pleroma/mutes_import', params)
|
||||
.then(response => {
|
||||
.then((response) => response.json()).then((data) => {
|
||||
toast.success(messages.mutesSuccess);
|
||||
dispatch({ type: IMPORT_MUTES_SUCCESS, config: response.data });
|
||||
dispatch({ type: IMPORT_MUTES_SUCCESS, config: data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: IMPORT_MUTES_FAIL, error });
|
||||
});
|
||||
|
|
|
@ -27,7 +27,8 @@ export const fetchInstance = createAsyncThunk<InstanceData, InstanceData['host']
|
|||
'instance/fetch',
|
||||
async(host, { dispatch, getState, rejectWithValue }) => {
|
||||
try {
|
||||
const { data } = await api(getState).get('/api/v1/instance');
|
||||
const response = await api(getState).get('/api/v1/instance');
|
||||
const data = await response.json();
|
||||
const instance = instanceV1Schema.parse(data);
|
||||
const features = getFeatures(instance);
|
||||
|
||||
|
@ -46,7 +47,8 @@ export const fetchInstanceV2 = createAsyncThunk<InstanceData, InstanceData['host
|
|||
'instanceV2/fetch',
|
||||
async(host, { getState, rejectWithValue }) => {
|
||||
try {
|
||||
const { data } = await api(getState).get('/api/v2/instance');
|
||||
const response = await api(getState).get('/api/v2/instance');
|
||||
const data = await response.json();
|
||||
const instance = instanceV2Schema.parse(data);
|
||||
return { instance, host };
|
||||
} catch (e) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { defineMessages } from 'react-intl';
|
|||
import toast from 'soapbox/toast.tsx';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth.ts';
|
||||
|
||||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import { fetchRelationships } from './accounts.ts';
|
||||
import { importFetchedAccounts, importFetchedStatus } from './importer/index.ts';
|
||||
|
@ -107,10 +107,10 @@ const reblog = (status: StatusEntity, effects?: ReblogEffects) =>
|
|||
dispatch(reblogRequest(status));
|
||||
effects?.reblogEffect(status.id);
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.id}/reblog`).then(function(response) {
|
||||
api(getState).post(`/api/v1/statuses/${status.id}/reblog`).then((response) => response.json()).then((data) => {
|
||||
// The reblog API method returns a new status wrapped around the original. In this case we are only
|
||||
// interested in how the original is modified, hence passing it skipping the wrapper
|
||||
dispatch(importFetchedStatus(response.data.reblog));
|
||||
dispatch(importFetchedStatus(data.reblog));
|
||||
dispatch(reblogSuccess(status));
|
||||
}).catch(error => {
|
||||
dispatch(reblogFail(status, error));
|
||||
|
@ -331,8 +331,8 @@ const zap = (account: AccountEntity, status: StatusEntity | undefined, amount: n
|
|||
|
||||
if (status) dispatch(zapRequest(status));
|
||||
|
||||
return api(getState).post('/api/v1/ditto/zap', { amount, comment, account_id: account.id, status_id: status?.id }).then(async function(response) {
|
||||
const { invoice } = response.data;
|
||||
return api(getState).post('/api/v1/ditto/zap', { amount, comment, account_id: account.id, status_id: status?.id }).then(async (response) => {
|
||||
const { invoice } = await response.json();
|
||||
if (!invoice) throw Error('Could not generate invoice');
|
||||
if (!window.webln) return invoice;
|
||||
|
||||
|
@ -372,9 +372,9 @@ const bookmark = (status: StatusEntity) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(bookmarkRequest(status));
|
||||
|
||||
return api(getState).post(`/api/v1/statuses/${status.id}/bookmark`).then(function(response) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(bookmarkSuccess(status, response.data));
|
||||
return api(getState).post(`/api/v1/statuses/${status.id}/bookmark`).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(bookmarkSuccess(status, data));
|
||||
|
||||
toast.success(messages.bookmarkAdded, {
|
||||
actionLink: '/bookmarks/all', actionLabel: messages.view,
|
||||
|
@ -388,9 +388,9 @@ const unbookmark = (status: StatusEntity) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(unbookmarkRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.id}/unbookmark`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unbookmarkSuccess(status, response.data));
|
||||
api(getState).post(`/api/v1/statuses/${status.id}/unbookmark`).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(unbookmarkSuccess(status, data));
|
||||
toast.success(messages.bookmarkRemoved);
|
||||
}).catch(error => {
|
||||
dispatch(unbookmarkFail(status, error));
|
||||
|
@ -446,11 +446,12 @@ const fetchReblogs = (id: string) =>
|
|||
|
||||
dispatch(fetchReblogsRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null));
|
||||
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchRelationships(data.map((item: APIEntity) => item.id)));
|
||||
dispatch(fetchReblogsSuccess(id, data, next));
|
||||
}).catch(error => {
|
||||
dispatch(fetchReblogsFail(id, error));
|
||||
});
|
||||
|
@ -476,11 +477,12 @@ const fetchReblogsFail = (id: string, error: unknown) => ({
|
|||
|
||||
const expandReblogs = (id: string, path: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
api(getState).get(path).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(expandReblogsSuccess(id, response.data, next ? next.uri : null));
|
||||
api(getState).get(path).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchRelationships(data.map((item: APIEntity) => item.id)));
|
||||
dispatch(expandReblogsSuccess(id, data, next));
|
||||
}).catch(error => {
|
||||
dispatch(expandReblogsFail(id, error));
|
||||
});
|
||||
|
@ -505,11 +507,12 @@ const fetchFavourites = (id: string) =>
|
|||
|
||||
dispatch(fetchFavouritesRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null));
|
||||
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchRelationships(data.map((item: APIEntity) => item.id)));
|
||||
dispatch(fetchFavouritesSuccess(id, data, next));
|
||||
}).catch(error => {
|
||||
dispatch(fetchFavouritesFail(id, error));
|
||||
});
|
||||
|
@ -535,11 +538,12 @@ const fetchFavouritesFail = (id: string, error: unknown) => ({
|
|||
|
||||
const expandFavourites = (id: string, path: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
api(getState).get(path).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(expandFavouritesSuccess(id, response.data, next ? next.uri : null));
|
||||
api(getState).get(path).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchRelationships(data.map((item: APIEntity) => item.id)));
|
||||
dispatch(expandFavouritesSuccess(id, data, next));
|
||||
}).catch(error => {
|
||||
dispatch(expandFavouritesFail(id, error));
|
||||
});
|
||||
|
@ -564,10 +568,10 @@ const fetchDislikes = (id: string) =>
|
|||
|
||||
dispatch(fetchDislikesRequest(id));
|
||||
|
||||
api(getState).get(`/api/friendica/statuses/${id}/disliked_by`).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
|
||||
dispatch(fetchDislikesSuccess(id, response.data));
|
||||
api(getState).get(`/api/friendica/statuses/${id}/disliked_by`).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchRelationships(data.map((item: APIEntity) => item.id)));
|
||||
dispatch(fetchDislikesSuccess(id, data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchDislikesFail(id, error));
|
||||
});
|
||||
|
@ -594,9 +598,9 @@ const fetchReactions = (id: string) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchReactionsRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/pleroma/statuses/${id}/reactions`).then(response => {
|
||||
dispatch(importFetchedAccounts((response.data as APIEntity[]).map(({ accounts }) => accounts).flat()));
|
||||
dispatch(fetchReactionsSuccess(id, response.data));
|
||||
api(getState).get(`/api/v1/pleroma/statuses/${id}/reactions`).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedAccounts((data as APIEntity[]).map(({ accounts }) => accounts).flat()));
|
||||
dispatch(fetchReactionsSuccess(id, data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchReactionsFail(id, error));
|
||||
});
|
||||
|
@ -623,10 +627,11 @@ const fetchZaps = (id: string) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchZapsRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/ditto/statuses/${id}/zapped_by`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts((response.data as APIEntity[]).map(({ account }) => account).flat()));
|
||||
dispatch(fetchZapsSuccess(id, response.data, next ? next.uri : null));
|
||||
api(getState).get(`/api/v1/ditto/statuses/${id}/zapped_by`).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedAccounts((data as APIEntity[]).map(({ account }) => account).flat()));
|
||||
dispatch(fetchZapsSuccess(id, data, next));
|
||||
}).catch(error => {
|
||||
dispatch(fetchZapsFail(id, error));
|
||||
});
|
||||
|
@ -652,11 +657,12 @@ const fetchZapsFail = (id: string, error: unknown) => ({
|
|||
|
||||
const expandZaps = (id: string, path: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
api(getState).get(path).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data.map((item: APIEntity) => item.account)));
|
||||
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.account.id)));
|
||||
dispatch(expandZapsSuccess(id, response.data, next ? next.uri : null));
|
||||
api(getState).get(path).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedAccounts(data.map((item: APIEntity) => item.account)));
|
||||
dispatch(fetchRelationships(data.map((item: APIEntity) => item.account.id)));
|
||||
dispatch(expandZapsSuccess(id, data, next));
|
||||
}).catch(error => {
|
||||
dispatch(expandZapsFail(id, error));
|
||||
});
|
||||
|
@ -681,8 +687,8 @@ const pin = (status: StatusEntity) =>
|
|||
|
||||
dispatch(pinRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.id}/pin`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
api(getState).post(`/api/v1/statuses/${status.id}/pin`).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(pinSuccess(status));
|
||||
}).catch(error => {
|
||||
dispatch(pinFail(status, error));
|
||||
|
@ -728,8 +734,8 @@ const unpin = (status: StatusEntity) =>
|
|||
|
||||
dispatch(unpinRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.id}/unpin`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
api(getState).post(`/api/v1/statuses/${status.id}/unpin`).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(unpinSuccess(status));
|
||||
}).catch(error => {
|
||||
dispatch(unpinFail(status, error));
|
||||
|
@ -768,7 +774,7 @@ const remoteInteraction = (ap_id: string, profile: string) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(remoteInteractionRequest(ap_id, profile));
|
||||
|
||||
return api(getState).post('/api/v1/pleroma/remote_interaction', { ap_id, profile }).then(({ data }) => {
|
||||
return api(getState).post('/api/v1/pleroma/remote_interaction', { ap_id, profile }).then((response) => response.json()).then((data) => {
|
||||
if (data.error) throw new Error(data.error);
|
||||
|
||||
dispatch(remoteInteractionSuccess(ap_id, profile, data.url));
|
||||
|
|
|
@ -66,7 +66,7 @@ const fetchList = (id: string | number) => (dispatch: AppDispatch, getState: ()
|
|||
dispatch(fetchListRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/lists/${id}`)
|
||||
.then(({ data }) => dispatch(fetchListSuccess(data)))
|
||||
.then((response) => response.json()).then((data) => dispatch(fetchListSuccess(data)))
|
||||
.catch(err => dispatch(fetchListFail(id, err)));
|
||||
};
|
||||
|
||||
|
@ -92,7 +92,7 @@ const fetchLists = () => (dispatch: AppDispatch, getState: () => RootState) => {
|
|||
dispatch(fetchListsRequest());
|
||||
|
||||
api(getState).get('/api/v1/lists')
|
||||
.then(({ data }) => dispatch(fetchListsSuccess(data)))
|
||||
.then((response) => response.json()).then((data) => dispatch(fetchListsSuccess(data)))
|
||||
.catch(err => dispatch(fetchListsFail(err)));
|
||||
};
|
||||
|
||||
|
@ -140,7 +140,7 @@ const createList = (title: string, shouldReset?: boolean) => (dispatch: AppDispa
|
|||
|
||||
dispatch(createListRequest());
|
||||
|
||||
api(getState).post('/api/v1/lists', { title }).then(({ data }) => {
|
||||
api(getState).post('/api/v1/lists', { title }).then((response) => response.json()).then((data) => {
|
||||
dispatch(createListSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
|
@ -168,7 +168,7 @@ const updateList = (id: string | number, title: string, shouldReset?: boolean) =
|
|||
|
||||
dispatch(updateListRequest(id));
|
||||
|
||||
api(getState).put(`/api/v1/lists/${id}`, { title }).then(({ data }) => {
|
||||
api(getState).put(`/api/v1/lists/${id}`, { title }).then((response) => response.json()).then((data) => {
|
||||
dispatch(updateListSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
|
@ -228,7 +228,7 @@ const fetchListAccounts = (listId: string | number) => (dispatch: AppDispatch, g
|
|||
|
||||
dispatch(fetchListAccountsRequest(listId));
|
||||
|
||||
api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
|
||||
api(getState).get(`/api/v1/lists/${listId}/accounts`, { searchParams: { limit: 0 } }).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchListAccountsSuccess(listId, data, null));
|
||||
}).catch(err => dispatch(fetchListAccountsFail(listId, err)));
|
||||
|
@ -255,14 +255,14 @@ const fetchListAccountsFail = (id: string | number, error: unknown) => ({
|
|||
const fetchListSuggestions = (q: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
const params = {
|
||||
const searchParams = {
|
||||
q,
|
||||
resolve: false,
|
||||
limit: 4,
|
||||
following: true,
|
||||
};
|
||||
|
||||
api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
|
||||
api(getState).get('/api/v1/accounts/search', { searchParams }).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchListSuggestionsReady(q, data));
|
||||
}).catch(error => toast.showAlertForError(error));
|
||||
|
@ -325,7 +325,10 @@ const removeFromList = (listId: string | number, accountId: string) => (dispatch
|
|||
|
||||
dispatch(removeFromListRequest(listId, accountId));
|
||||
|
||||
api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
|
||||
const data = new FormData();
|
||||
data.append('account_ids[]', accountId);
|
||||
|
||||
api(getState).request('DELETE', `/api/v1/lists/${listId}/accounts`, data)
|
||||
.then(() => dispatch(removeFromListSuccess(listId, accountId)))
|
||||
.catch(err => dispatch(removeFromListFail(listId, accountId, err)));
|
||||
};
|
||||
|
@ -368,7 +371,7 @@ const fetchAccountLists = (accountId: string) => (dispatch: AppDispatch, getStat
|
|||
dispatch(fetchAccountListsRequest(accountId));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${accountId}/lists`)
|
||||
.then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data)))
|
||||
.then((response) => response.json()).then((data) => dispatch(fetchAccountListsSuccess(accountId, data)))
|
||||
.catch(err => dispatch(fetchAccountListsFail(accountId, err)));
|
||||
};
|
||||
|
||||
|
|
|
@ -14,9 +14,7 @@ const MARKER_SAVE_FAIL = 'MARKER_SAVE_FAIL';
|
|||
const fetchMarker = (timeline: Array<string>) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: MARKER_FETCH_REQUEST });
|
||||
return api(getState).get('/api/v1/markers', {
|
||||
params: { timeline },
|
||||
}).then(({ data: marker }) => {
|
||||
return api(getState).get('/api/v1/markers', { searchParams: { timeline } }).then((response) => response.json()).then((marker) => {
|
||||
dispatch({ type: MARKER_FETCH_SUCCESS, marker });
|
||||
}).catch(error => {
|
||||
dispatch({ type: MARKER_FETCH_FAIL, error });
|
||||
|
@ -26,7 +24,7 @@ const fetchMarker = (timeline: Array<string>) =>
|
|||
const saveMarker = (marker: APIEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: MARKER_SAVE_REQUEST, marker });
|
||||
return api(getState).post('/api/v1/markers', marker).then(({ data: marker }) => {
|
||||
return api(getState).post('/api/v1/markers', marker).then((response) => response.json()).then((marker) => {
|
||||
dispatch({ type: MARKER_SAVE_SUCCESS, marker });
|
||||
}).catch(error => {
|
||||
dispatch({ type: MARKER_SAVE_FAIL, error });
|
||||
|
|
|
@ -7,7 +7,6 @@ import api from '../api/index.ts';
|
|||
import { verifyCredentials } from './auth.ts';
|
||||
import { importFetchedAccount } from './importer/index.ts';
|
||||
|
||||
import type { RawAxiosRequestHeaders } from 'axios';
|
||||
import type { Account } from 'soapbox/schemas/index.ts';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store.ts';
|
||||
import type { APIEntity } from 'soapbox/types/entities.ts';
|
||||
|
@ -58,14 +57,14 @@ const patchMe = (params: Record<string, any>, isFormData = false) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(patchMeRequest());
|
||||
|
||||
const headers: RawAxiosRequestHeaders = isFormData ? {
|
||||
const headers = isFormData ? {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
} : {};
|
||||
} : undefined;
|
||||
|
||||
return api(getState)
|
||||
.patch('/api/v1/accounts/update_credentials', params, { headers })
|
||||
.then(response => {
|
||||
dispatch(patchMeSuccess(response.data));
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch(patchMeSuccess(data));
|
||||
}).catch(error => {
|
||||
dispatch(patchMeFail(error));
|
||||
throw error;
|
||||
|
|
|
@ -31,15 +31,11 @@ const updateMedia = (mediaId: string, params: Record<string, any>) =>
|
|||
|
||||
const uploadMediaV1 = (data: FormData, onUploadProgress = noOp) =>
|
||||
(dispatch: any, getState: () => RootState) =>
|
||||
api(getState).post('/api/v1/media', data, {
|
||||
onUploadProgress: onUploadProgress,
|
||||
});
|
||||
api(getState).post('/api/v1/media', data, { onUploadProgress });
|
||||
|
||||
const uploadMediaV2 = (data: FormData, onUploadProgress = noOp) =>
|
||||
(dispatch: any, getState: () => RootState) =>
|
||||
api(getState).post('/api/v2/media', data, {
|
||||
onUploadProgress: onUploadProgress,
|
||||
});
|
||||
api(getState).post('/api/v2/media', data, { onUploadProgress });
|
||||
|
||||
const uploadMedia = (data: FormData, onUploadProgress = noOp) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
|
@ -59,8 +55,7 @@ const uploadFile = (
|
|||
intl: IntlShape,
|
||||
onSuccess: (data: APIEntity) => void = () => {},
|
||||
onFail: (error: unknown) => void = () => {},
|
||||
onProgress: (loaded: number) => void = () => {},
|
||||
changeTotal: (value: number) => void = () => {},
|
||||
onUploadProgress: (e: ProgressEvent) => void = () => {},
|
||||
) =>
|
||||
async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
@ -92,21 +87,23 @@ const uploadFile = (
|
|||
}
|
||||
|
||||
// FIXME: Don't define const in loop
|
||||
resizeImage(file).then(resized => {
|
||||
resizeImage(file).then((resized) => {
|
||||
const data = new FormData();
|
||||
data.append('file', resized);
|
||||
// Account for disparity in size of original image and resized data
|
||||
changeTotal(resized.size - file.size);
|
||||
|
||||
return dispatch(uploadMedia(data, onProgress))
|
||||
.then(({ status, data }) => {
|
||||
return dispatch(uploadMedia(data, onUploadProgress))
|
||||
.then(async (response) => {
|
||||
const { status } = response;
|
||||
const data = await response.json();
|
||||
// If server-side processing of the media attachment has not completed yet,
|
||||
// poll the server until it is, before showing the media attachment as uploaded
|
||||
if (status === 200) {
|
||||
onSuccess(data);
|
||||
} else if (status === 202) {
|
||||
const poll = () => {
|
||||
dispatch(fetchMedia(data.id)).then(({ status, data }) => {
|
||||
dispatch(fetchMedia(data.id)).then(async (response) => {
|
||||
const { status } = response;
|
||||
const data = await response.json();
|
||||
if (status === 200) {
|
||||
onSuccess(data);
|
||||
} else if (status === 206) {
|
||||
|
|
|
@ -25,7 +25,7 @@ const MFA_DISABLE_FAIL = 'MFA_DISABLE_FAIL';
|
|||
const fetchMfa = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: MFA_FETCH_REQUEST });
|
||||
return api(getState).get('/api/pleroma/accounts/mfa').then(({ data }) => {
|
||||
return api(getState).get('/api/pleroma/accounts/mfa').then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: MFA_FETCH_SUCCESS, data });
|
||||
}).catch(() => {
|
||||
dispatch({ type: MFA_FETCH_FAIL });
|
||||
|
@ -35,7 +35,7 @@ const fetchMfa = () =>
|
|||
const fetchBackupCodes = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: MFA_BACKUP_CODES_FETCH_REQUEST });
|
||||
return api(getState).get('/api/pleroma/accounts/mfa/backup_codes').then(({ data }) => {
|
||||
return api(getState).get('/api/pleroma/accounts/mfa/backup_codes').then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: MFA_BACKUP_CODES_FETCH_SUCCESS, data });
|
||||
return data;
|
||||
}).catch(() => {
|
||||
|
@ -46,7 +46,7 @@ const fetchBackupCodes = () =>
|
|||
const setupMfa = (method: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: MFA_SETUP_REQUEST, method });
|
||||
return api(getState).get(`/api/pleroma/accounts/mfa/setup/${method}`).then(({ data }) => {
|
||||
return api(getState).get(`/api/pleroma/accounts/mfa/setup/${method}`).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: MFA_SETUP_SUCCESS, data });
|
||||
return data;
|
||||
}).catch((error: unknown) => {
|
||||
|
@ -59,7 +59,7 @@ const confirmMfa = (method: string, code: string, password: string) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const params = { code, password };
|
||||
dispatch({ type: MFA_CONFIRM_REQUEST, method, code });
|
||||
return api(getState).post(`/api/pleroma/accounts/mfa/confirm/${method}`, params).then(({ data }) => {
|
||||
return api(getState).post(`/api/pleroma/accounts/mfa/confirm/${method}`, params).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: MFA_CONFIRM_SUCCESS, method, code });
|
||||
return data;
|
||||
}).catch((error: unknown) => {
|
||||
|
@ -71,7 +71,7 @@ const confirmMfa = (method: string, code: string, password: string) =>
|
|||
const disableMfa = (method: string, password: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: MFA_DISABLE_REQUEST, method });
|
||||
return api(getState).delete(`/api/pleroma/accounts/mfa/${method}`, { data: { password } }).then(({ data }) => {
|
||||
return api(getState).request('DELETE', `/api/pleroma/accounts/mfa/${method}`, { password }).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: MFA_DISABLE_SUCCESS, method });
|
||||
return data;
|
||||
}).catch((error: unknown) => {
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import { OrderedMap as ImmutableOrderedMap } from 'immutable';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers.tsx';
|
||||
import { normalizeNotification } from 'soapbox/normalizers/index.ts';
|
||||
|
||||
import { markReadNotifications } from './notifications.ts';
|
||||
|
||||
describe('markReadNotifications()', () => {
|
||||
it('fires off marker when top notification is newer than lastRead', async() => {
|
||||
__stub((mock) => mock.onPost('/api/v1/markers').reply(200, {}));
|
||||
|
||||
const items = ImmutableOrderedMap({
|
||||
'10': normalizeNotification({ id: '10' }),
|
||||
});
|
||||
|
||||
const state = {
|
||||
...rootState,
|
||||
me: '123',
|
||||
notifications: rootState.notifications.merge({
|
||||
lastRead: '9',
|
||||
items,
|
||||
}),
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
|
||||
const expectedActions = [{
|
||||
type: 'MARKER_SAVE_REQUEST',
|
||||
marker: {
|
||||
notifications: {
|
||||
last_read_id: '10',
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
store.dispatch(markReadNotifications());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
|
@ -2,7 +2,7 @@ import IntlMessageFormat from 'intl-messageformat';
|
|||
import 'intl-pluralrules';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import api, { getLinks } from 'soapbox/api/index.ts';
|
||||
import api from 'soapbox/api/index.ts';
|
||||
import { getFilters, regexFromFilters } from 'soapbox/selectors/index.ts';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth.ts';
|
||||
import { compareId } from 'soapbox/utils/comparators.ts';
|
||||
|
@ -213,10 +213,11 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
|
|||
|
||||
dispatch(expandNotificationsRequest(isLoadingMore));
|
||||
|
||||
return api(getState).get('/api/v1/notifications', { params }).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
return api(getState).get('/api/v1/notifications', { searchParams: params }).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
const entries = (response.data as APIEntity[]).reduce((acc, item) => {
|
||||
const entries = (data as APIEntity[]).reduce((acc, item) => {
|
||||
if (item.account?.id) {
|
||||
acc.accounts[item.account.id] = item.account;
|
||||
}
|
||||
|
@ -239,8 +240,8 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
|
|||
const statusesFromGroups = (Object.values(entries.statuses) as Status[]).filter((status) => !!status.group);
|
||||
dispatch(fetchGroupRelationships(statusesFromGroups.map((status: any) => status.group?.id)));
|
||||
|
||||
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore));
|
||||
fetchRelatedRelationships(dispatch, response.data);
|
||||
dispatch(expandNotificationsSuccess(data, next, isLoadingMore));
|
||||
fetchRelatedRelationships(dispatch, data);
|
||||
done();
|
||||
}).catch(error => {
|
||||
dispatch(expandNotificationsFail(error, isLoadingMore));
|
||||
|
|
|
@ -23,7 +23,7 @@ export const OAUTH_TOKEN_REVOKE_FAIL = 'OAUTH_TOKEN_REVOKE_FAIL';
|
|||
export const obtainOAuthToken = (params: Record<string, unknown>, baseURL?: string) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch({ type: OAUTH_TOKEN_CREATE_REQUEST, params });
|
||||
return baseClient(null, baseURL).post('/oauth/token', params).then(({ data: token }) => {
|
||||
return baseClient(null, baseURL).post('/oauth/token', params).then((response) => response.json()).then((token) => {
|
||||
dispatch({ type: OAUTH_TOKEN_CREATE_SUCCESS, params, token });
|
||||
return token;
|
||||
}).catch(error => {
|
||||
|
@ -36,7 +36,7 @@ export const revokeOAuthToken = (params: Record<string, string>) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: OAUTH_TOKEN_REVOKE_REQUEST, params });
|
||||
const baseURL = getBaseURL(getState());
|
||||
return baseClient(null, baseURL).post('/oauth/revoke', params).then(({ data }) => {
|
||||
return baseClient(null, baseURL).post('/oauth/revoke', params).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: OAUTH_TOKEN_REVOKE_SUCCESS, params, data });
|
||||
return data;
|
||||
}).catch(error => {
|
||||
|
|
|
@ -14,8 +14,8 @@ const PATRON_ACCOUNT_FETCH_FAIL = 'PATRON_ACCOUNT_FETCH_FAIL';
|
|||
const fetchPatronInstance = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: PATRON_INSTANCE_FETCH_REQUEST });
|
||||
return api(getState).get('/api/patron/v1/instance').then(response => {
|
||||
dispatch(importFetchedInstance(response.data));
|
||||
return api(getState).get('/api/patron/v1/instance').then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedInstance(data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchInstanceFail(error));
|
||||
});
|
||||
|
@ -25,8 +25,8 @@ const fetchPatronAccount = (apId: string) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
apId = encodeURIComponent(apId);
|
||||
dispatch({ type: PATRON_ACCOUNT_FETCH_REQUEST });
|
||||
api(getState).get(`/api/patron/v1/accounts/${apId}`).then(response => {
|
||||
dispatch(importFetchedAccount(response.data));
|
||||
api(getState).get(`/api/patron/v1/accounts/${apId}`).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedAccount(data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchAccountFail(error));
|
||||
});
|
||||
|
|
|
@ -18,9 +18,9 @@ const fetchPinnedStatuses = () =>
|
|||
|
||||
dispatch(fetchPinnedStatusesRequest());
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchPinnedStatusesSuccess(response.data, null));
|
||||
api(getState).get(`/api/v1/accounts/${me}/statuses`, { searchParams: { pinned: true } }).then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedStatuses(data));
|
||||
dispatch(fetchPinnedStatusesSuccess(data, null));
|
||||
}).catch(error => {
|
||||
dispatch(fetchPinnedStatusesFail(error));
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@ const vote = (pollId: string, choices: string[]) =>
|
|||
dispatch(voteRequest());
|
||||
|
||||
api(getState).post(`/api/v1/polls/${pollId}/votes`, { choices })
|
||||
.then(({ data }) => {
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedPoll(data));
|
||||
dispatch(voteSuccess(data));
|
||||
})
|
||||
|
@ -30,7 +30,7 @@ const fetchPoll = (pollId: string) =>
|
|||
dispatch(fetchPollRequest());
|
||||
|
||||
api(getState).get(`/api/v1/polls/${pollId}`)
|
||||
.then(({ data }) => {
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch(importFetchedPoll(data));
|
||||
dispatch(fetchPollSuccess(data));
|
||||
})
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { mockStore } from 'soapbox/jest/test-helpers.tsx';
|
||||
|
||||
import { VERIFY_CREDENTIALS_REQUEST } from './auth.ts';
|
||||
import { ACCOUNTS_IMPORT } from './importer/index.ts';
|
||||
import {
|
||||
MASTODON_PRELOAD_IMPORT,
|
||||
preloadMastodon,
|
||||
} from './preload.ts';
|
||||
|
||||
describe('preloadMastodon()', () => {
|
||||
it('creates the expected actions', async () => {
|
||||
const data = await import('soapbox/__fixtures__/mastodon_initial_state.json');
|
||||
|
||||
__stub(mock => {
|
||||
mock.onGet('/api/v1/accounts/verify_credentials')
|
||||
.reply(200, {});
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
store.dispatch(preloadMastodon(data));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions[0].type).toEqual(ACCOUNTS_IMPORT);
|
||||
expect(actions[0].accounts[0].username).toEqual('Gargron');
|
||||
expect(actions[0].accounts[1].username).toEqual('benis911');
|
||||
|
||||
expect(actions[1]).toEqual({
|
||||
type: VERIFY_CREDENTIALS_REQUEST,
|
||||
token: 'Nh15V9JWyY5Fshf2OJ_feNvOIkTV7YGVfEJFr0Y0D6Q',
|
||||
});
|
||||
|
||||
expect(actions[2]).toEqual({ type: MASTODON_PRELOAD_IMPORT, data });
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import { getFeatures } from 'soapbox/utils/features.ts';
|
||||
|
||||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store.ts';
|
||||
import type { APIEntity } from 'soapbox/types/entities.ts';
|
||||
|
@ -32,9 +32,10 @@ const fetchScheduledStatuses = () =>
|
|||
|
||||
dispatch(fetchScheduledStatusesRequest());
|
||||
|
||||
api(getState).get('/api/v1/scheduled_statuses').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(fetchScheduledStatusesSuccess(response.data, next ? next.uri : null));
|
||||
api(getState).get('/api/v1/scheduled_statuses').then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(fetchScheduledStatusesSuccess(data, next));
|
||||
}).catch(error => {
|
||||
dispatch(fetchScheduledStatusesFail(error));
|
||||
});
|
||||
|
@ -43,7 +44,7 @@ const fetchScheduledStatuses = () =>
|
|||
const cancelScheduledStatus = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: SCHEDULED_STATUS_CANCEL_REQUEST, id });
|
||||
api(getState).delete(`/api/v1/scheduled_statuses/${id}`).then(({ data }) => {
|
||||
api(getState).delete(`/api/v1/scheduled_statuses/${id}`).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: SCHEDULED_STATUS_CANCEL_SUCCESS, id, data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: SCHEDULED_STATUS_CANCEL_FAIL, id, error });
|
||||
|
@ -75,9 +76,10 @@ const expandScheduledStatuses = () =>
|
|||
|
||||
dispatch(expandScheduledStatusesRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(expandScheduledStatusesSuccess(response.data, next ? next.uri : null));
|
||||
api(getState).get(url).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(expandScheduledStatusesSuccess(data, next));
|
||||
}).catch(error => {
|
||||
dispatch(expandScheduledStatusesFail(error));
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import { fetchRelationships } from './accounts.ts';
|
||||
import { importFetchedAccounts, importFetchedStatuses } from './importer/index.ts';
|
||||
|
@ -72,20 +72,21 @@ const submitSearch = (filter?: SearchFilter) =>
|
|||
if (accountId) params.account_id = accountId;
|
||||
|
||||
api(getState).get('/api/v2/search', {
|
||||
params,
|
||||
}).then(response => {
|
||||
if (response.data.accounts) {
|
||||
dispatch(importFetchedAccounts(response.data.accounts));
|
||||
searchParams: params,
|
||||
}).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
if (data.accounts) {
|
||||
dispatch(importFetchedAccounts(data.accounts));
|
||||
}
|
||||
|
||||
if (response.data.statuses) {
|
||||
dispatch(importFetchedStatuses(response.data.statuses));
|
||||
if (data.statuses) {
|
||||
dispatch(importFetchedStatuses(data.statuses));
|
||||
}
|
||||
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(fetchSearchSuccess(response.data, value, type, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.accounts.map((item: APIEntity) => item.id)));
|
||||
dispatch(fetchSearchSuccess(data, value, type, next));
|
||||
dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(fetchSearchFail(error));
|
||||
});
|
||||
|
@ -143,9 +144,10 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: (
|
|||
}
|
||||
|
||||
api(getState).get(url, {
|
||||
params,
|
||||
}).then(response => {
|
||||
const data = response.data;
|
||||
searchParams: params,
|
||||
}).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
if (data.accounts) {
|
||||
dispatch(importFetchedAccounts(data.accounts));
|
||||
|
@ -155,9 +157,7 @@ const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: (
|
|||
dispatch(importFetchedStatuses(data.statuses));
|
||||
}
|
||||
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(expandSearchSuccess(data, value, type, next ? next.uri : null));
|
||||
dispatch(expandSearchSuccess(data, value, type, next));
|
||||
dispatch(fetchRelationships(data.accounts.map((item: APIEntity) => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(expandSearchFail(error));
|
||||
|
|
|
@ -50,7 +50,7 @@ const MOVE_ACCOUNT_FAIL = 'MOVE_ACCOUNT_FAIL';
|
|||
const fetchOAuthTokens = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FETCH_TOKENS_REQUEST });
|
||||
return api(getState).get('/api/oauth_tokens').then(({ data: tokens }) => {
|
||||
return api(getState).get('/api/oauth_tokens').then((response) => response.json()).then((tokens) => {
|
||||
dispatch({ type: FETCH_TOKENS_SUCCESS, tokens });
|
||||
}).catch(() => {
|
||||
dispatch({ type: FETCH_TOKENS_FAIL });
|
||||
|
@ -74,9 +74,9 @@ const changePassword = (oldPassword: string, newPassword: string, confirmation:
|
|||
password: oldPassword,
|
||||
new_password: newPassword,
|
||||
new_password_confirmation: confirmation,
|
||||
}).then(response => {
|
||||
if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure
|
||||
dispatch({ type: CHANGE_PASSWORD_SUCCESS, response });
|
||||
}).then((response) => response.json()).then((data) => {
|
||||
if (data.error) throw data.error; // This endpoint returns HTTP 200 even on failure
|
||||
dispatch({ type: CHANGE_PASSWORD_SUCCESS, data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHANGE_PASSWORD_FAIL, error, skipAlert: true });
|
||||
throw error;
|
||||
|
@ -128,9 +128,9 @@ const changeEmail = (email: string, password: string) =>
|
|||
return api(getState).post('/api/pleroma/change_email', {
|
||||
email,
|
||||
password,
|
||||
}).then(response => {
|
||||
if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure
|
||||
dispatch({ type: CHANGE_EMAIL_SUCCESS, email, response });
|
||||
}).then((response) => response.json()).then((data) => {
|
||||
if (data.error) throw data.error; // This endpoint returns HTTP 200 even on failure
|
||||
dispatch({ type: CHANGE_EMAIL_SUCCESS, email, data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHANGE_EMAIL_FAIL, email, error, skipAlert: true });
|
||||
throw error;
|
||||
|
@ -148,9 +148,9 @@ const deleteAccount = (password: string) =>
|
|||
dispatch({ type: DELETE_ACCOUNT_REQUEST });
|
||||
return api(getState).post('/api/pleroma/delete_account', {
|
||||
password,
|
||||
}).then(response => {
|
||||
if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure
|
||||
dispatch({ type: DELETE_ACCOUNT_SUCCESS, response });
|
||||
}).then((response) => response.json()).then((data) => {
|
||||
if (data.error) throw data.error; // This endpoint returns HTTP 200 even on failure
|
||||
dispatch({ type: DELETE_ACCOUNT_SUCCESS, data });
|
||||
dispatch({ type: AUTH_LOGGED_OUT, account });
|
||||
toast.success(messages.loggedOut);
|
||||
}).catch(error => {
|
||||
|
@ -165,9 +165,9 @@ const moveAccount = (targetAccount: string, password: string) =>
|
|||
return api(getState).post('/api/pleroma/move_account', {
|
||||
password,
|
||||
target_account: targetAccount,
|
||||
}).then(response => {
|
||||
if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure
|
||||
dispatch({ type: MOVE_ACCOUNT_SUCCESS, response });
|
||||
}).then((response) => response.json()).then((data) => {
|
||||
if (data.error) throw data.error; // This endpoint returns HTTP 200 even on failure
|
||||
dispatch({ type: MOVE_ACCOUNT_SUCCESS, data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: MOVE_ACCOUNT_FAIL, error, skipAlert: true });
|
||||
throw error;
|
||||
|
|
|
@ -33,7 +33,7 @@ const fetchFrontendConfigurations = () =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
api(getState)
|
||||
.get('/api/pleroma/frontend_configurations')
|
||||
.then(({ data }) => data);
|
||||
.then((response) => response.json());
|
||||
|
||||
/** Conditionally fetches Soapbox config depending on backend features */
|
||||
const fetchSoapboxConfig = (host: string | null = null) =>
|
||||
|
@ -41,7 +41,7 @@ const fetchSoapboxConfig = (host: string | null = null) =>
|
|||
const features = getFeatures(getState().instance);
|
||||
|
||||
if (features.frontendConfigurations) {
|
||||
return dispatch(fetchFrontendConfigurations()).then(data => {
|
||||
return dispatch(fetchFrontendConfigurations()).then((data) => {
|
||||
if (data.soapbox_fe) {
|
||||
dispatch(importSoapboxConfig(data.soapbox_fe, host));
|
||||
return data.soapbox_fe;
|
||||
|
|
|
@ -1,158 +0,0 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers.tsx';
|
||||
import { StatusListRecord } from 'soapbox/reducers/status-lists.ts';
|
||||
|
||||
import { fetchStatusQuotes, expandStatusQuotes } from './status-quotes.ts';
|
||||
|
||||
const status = {
|
||||
account: {
|
||||
id: 'ABDSjI3Q0R8aDaz1U0',
|
||||
},
|
||||
content: 'quoast',
|
||||
id: 'AJsajx9hY4Q7IKQXEe',
|
||||
pleroma: {
|
||||
quote: {
|
||||
content: '<p>10</p>',
|
||||
id: 'AJmoVikzI3SkyITyim',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const statusId = 'AJmoVikzI3SkyITyim';
|
||||
|
||||
describe('fetchStatusQuotes()', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
const state = { ...rootState, me: '1234' };
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(async () => {
|
||||
const quotes = await import('soapbox/__fixtures__/status-quotes.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).reply(200, quotes, {
|
||||
link: `<https://example.com/api/v1/pleroma/statuses/${statusId}/quotes?since_id=1>; rel='prev'`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch quotes from the API', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_FETCH_REQUEST', statusId },
|
||||
{ type: 'POLLS_IMPORT', polls: [] },
|
||||
{ type: 'ACCOUNTS_IMPORT', accounts: [status.account] },
|
||||
{ type: 'STATUSES_IMPORT', statuses: [status], expandSpoilers: false },
|
||||
{ type: 'STATUS_QUOTES_FETCH_SUCCESS', statusId, statuses: [status], next: null },
|
||||
];
|
||||
await store.dispatch(fetchStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch failed action', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_FETCH_REQUEST', statusId },
|
||||
{ type: 'STATUS_QUOTES_FETCH_FAIL', statusId, error: new Error('Network Error') },
|
||||
];
|
||||
await store.dispatch(fetchStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('expandStatusQuotes()', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
describe('without a url', () => {
|
||||
beforeEach(() => {
|
||||
const state = {
|
||||
...rootState,
|
||||
me: '1234',
|
||||
status_lists: ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: null }) }),
|
||||
};
|
||||
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
it('should do nothing', async() => {
|
||||
await store.dispatch(expandStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a url', () => {
|
||||
beforeEach(() => {
|
||||
const state = {
|
||||
...rootState,
|
||||
status_lists: ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: 'example' }) }),
|
||||
me: '1234',
|
||||
};
|
||||
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(async () => {
|
||||
const quotes = await import('soapbox/__fixtures__/status-quotes.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet('example').reply(200, quotes, {
|
||||
link: `<https://example.com/api/v1/pleroma/statuses/${statusId}/quotes?since_id=1>; rel='prev'`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch quotes from the API', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId },
|
||||
{ type: 'POLLS_IMPORT', polls: [] },
|
||||
{ type: 'ACCOUNTS_IMPORT', accounts: [status.account] },
|
||||
{ type: 'STATUSES_IMPORT', statuses: [status], expandSpoilers: false },
|
||||
{ type: 'STATUS_QUOTES_EXPAND_SUCCESS', statusId, statuses: [status], next: null },
|
||||
];
|
||||
await store.dispatch(expandStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('example').networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch failed action', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId },
|
||||
{ type: 'STATUS_QUOTES_EXPAND_FAIL', statusId, error: new Error('Network Error') },
|
||||
];
|
||||
await store.dispatch(expandStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import { importFetchedStatuses } from './importer/index.ts';
|
||||
|
||||
|
@ -25,14 +25,15 @@ export const fetchStatusQuotes = (statusId: string) =>
|
|||
type: STATUS_QUOTES_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
return api(getState).get(`/api/v1/pleroma/statuses/${statusId}/quotes`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
return api(getState).get(`/api/v1/pleroma/statuses/${statusId}/quotes`).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedStatuses(data));
|
||||
return dispatch({
|
||||
type: STATUS_QUOTES_FETCH_SUCCESS,
|
||||
statusId,
|
||||
statuses: response.data,
|
||||
next: next ? next.uri : null,
|
||||
statuses: data,
|
||||
next,
|
||||
});
|
||||
}).catch(error => {
|
||||
dispatch({
|
||||
|
@ -56,14 +57,14 @@ export const expandStatusQuotes = (statusId: string) =>
|
|||
statusId,
|
||||
});
|
||||
|
||||
return api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
return api(getState).get(url).then(async (response) => {
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedStatuses(data));
|
||||
dispatch({
|
||||
type: STATUS_QUOTES_EXPAND_SUCCESS,
|
||||
statusId,
|
||||
statuses: response.data,
|
||||
next: next ? next.uri : null,
|
||||
statuses: data,
|
||||
next: response.next(),
|
||||
});
|
||||
}).catch(error => {
|
||||
dispatch({
|
||||
|
|
|
@ -1,162 +0,0 @@
|
|||
import { fromJS, Map as ImmutableMap } from 'immutable';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { STATUSES_IMPORT } from 'soapbox/actions/importer/index.ts';
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers.tsx';
|
||||
import { normalizeStatus } from 'soapbox/normalizers/status.ts';
|
||||
|
||||
import { deleteStatus, fetchContext } from './statuses.ts';
|
||||
|
||||
describe('fetchContext()', () => {
|
||||
it('handles Mitra context', async () => {
|
||||
const statuses = await import('soapbox/__fixtures__/mitra-context.json');
|
||||
|
||||
__stub(mock => {
|
||||
mock.onGet('/api/v1/statuses/017ed505-5926-392f-256a-f86d5075df70/context')
|
||||
.reply(200, statuses);
|
||||
});
|
||||
|
||||
const store = mockStore(rootState);
|
||||
await store.dispatch(fetchContext('017ed505-5926-392f-256a-f86d5075df70'));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions[3].type).toEqual(STATUSES_IMPORT);
|
||||
expect(actions[3].statuses[0].id).toEqual('017ed503-bc96-301a-e871-2c23b30ddd05');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteStatus()', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
describe('if logged out', () => {
|
||||
beforeEach(() => {
|
||||
const state = { ...rootState, me: null };
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
it('should do nothing', async() => {
|
||||
await store.dispatch(deleteStatus('1'));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if logged in', () => {
|
||||
const statusId = 'AHU2RrX0wdcwzCYjFQ';
|
||||
const cachedStatus = normalizeStatus({
|
||||
id: statusId,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const state = {
|
||||
...rootState,
|
||||
me: '1234',
|
||||
statuses: fromJS({
|
||||
[statusId]: cachedStatus,
|
||||
}) as any,
|
||||
};
|
||||
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
let status: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
status = await import('soapbox/__fixtures__/pleroma-status-deleted.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onDelete(`/api/v1/statuses/${statusId}`).reply(200, status);
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete the status from the API', async() => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: 'STATUS_DELETE_REQUEST',
|
||||
params: cachedStatus,
|
||||
},
|
||||
{ type: 'STATUS_DELETE_SUCCESS', id: statusId },
|
||||
{
|
||||
type: 'TIMELINE_DELETE',
|
||||
id: statusId,
|
||||
accountId: null,
|
||||
references: ImmutableMap({}),
|
||||
reblogOf: null,
|
||||
},
|
||||
];
|
||||
await store.dispatch(deleteStatus(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
|
||||
it('should handle redraft', async() => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: 'STATUS_DELETE_REQUEST',
|
||||
params: cachedStatus,
|
||||
},
|
||||
{ type: 'STATUS_DELETE_SUCCESS', id: statusId },
|
||||
{
|
||||
type: 'TIMELINE_DELETE',
|
||||
id: statusId,
|
||||
accountId: null,
|
||||
references: ImmutableMap({}),
|
||||
reblogOf: null,
|
||||
},
|
||||
{
|
||||
type: 'COMPOSE_SET_STATUS',
|
||||
status: cachedStatus,
|
||||
rawText: status.text,
|
||||
explicitAddressing: false,
|
||||
spoilerText: '',
|
||||
contentType: 'text/markdown',
|
||||
v: {
|
||||
build: undefined,
|
||||
compatVersion: '0.0.0',
|
||||
software: 'Mastodon',
|
||||
version: '0.0.0',
|
||||
},
|
||||
withRedraft: true,
|
||||
id: 'compose-modal',
|
||||
},
|
||||
{ type: 'MODAL_CLOSE', modalType: 'COMPOSE', modalProps: undefined },
|
||||
{ type: 'MODAL_OPEN', modalType: 'COMPOSE', modalProps: undefined },
|
||||
];
|
||||
await store.dispatch(deleteStatus(statusId, true));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onDelete(`/api/v1/statuses/${statusId}`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch failed action', async() => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: 'STATUS_DELETE_REQUEST',
|
||||
params: cachedStatus,
|
||||
},
|
||||
{
|
||||
type: 'STATUS_DELETE_FAIL',
|
||||
params: cachedStatus,
|
||||
error: new Error('Network Error'),
|
||||
},
|
||||
];
|
||||
await store.dispatch(deleteStatus(statusId, true));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -3,7 +3,7 @@ import { isLoggedIn } from 'soapbox/utils/auth.ts';
|
|||
import { getFeatures } from 'soapbox/utils/features.ts';
|
||||
import { shouldHaveCard } from 'soapbox/utils/status.ts';
|
||||
|
||||
import api, { getNextLink } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import { setComposeToStatus } from './compose-status.ts';
|
||||
import { fetchGroupRelationships } from './groups.ts';
|
||||
|
@ -60,12 +60,13 @@ const createStatus = (params: Record<string, any>, idempotencyKey: string, statu
|
|||
return (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: STATUS_CREATE_REQUEST, params, idempotencyKey, editing: !!statusId });
|
||||
|
||||
return api(getState).request({
|
||||
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
|
||||
method: statusId === null ? 'post' : 'put',
|
||||
data: params,
|
||||
headers: { 'Idempotency-Key': idempotencyKey },
|
||||
}).then(({ data: status }) => {
|
||||
const method = statusId === null ? 'POST' : 'PUT';
|
||||
const path = statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`;
|
||||
const headers = { 'Idempotency-Key': idempotencyKey };
|
||||
|
||||
return api(getState).request(method, path, params, { headers }).then(async (response) => {
|
||||
const status = await response.json();
|
||||
|
||||
// The backend might still be processing the rich media attachment
|
||||
if (!status.card && shouldHaveCard(status)) {
|
||||
status.expectsCard = true;
|
||||
|
@ -79,9 +80,9 @@ const createStatus = (params: Record<string, any>, idempotencyKey: string, statu
|
|||
const delay = 1000;
|
||||
|
||||
const poll = (retries = 5) => {
|
||||
api(getState).get(`/api/v1/statuses/${status.id}`).then(response => {
|
||||
if (response.data?.card) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
api(getState).get(`/api/v1/statuses/${status.id}`).then((response) => response.json()).then((data) => {
|
||||
if (data?.card) {
|
||||
dispatch(importFetchedStatus(data));
|
||||
} else if (retries > 0 && response.status === 200) {
|
||||
setTimeout(() => poll(retries - 1), delay);
|
||||
}
|
||||
|
@ -108,9 +109,9 @@ const editStatus = (id: string) => (dispatch: AppDispatch, getState: () => RootS
|
|||
|
||||
dispatch({ type: STATUS_FETCH_SOURCE_REQUEST });
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
|
||||
api(getState).get(`/api/v1/statuses/${id}/source`).then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS });
|
||||
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.content_type, false));
|
||||
dispatch(setComposeToStatus(status, data.text, data.spoiler_text, data.content_type, false));
|
||||
dispatch(openModal('COMPOSE'));
|
||||
}).catch(error => {
|
||||
dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error });
|
||||
|
@ -124,7 +125,7 @@ const fetchStatus = (id: string) => {
|
|||
|
||||
dispatch({ type: STATUS_FETCH_REQUEST, id, skipLoading });
|
||||
|
||||
return api(getState).get(`/api/v1/statuses/${id}`).then(({ data: status }) => {
|
||||
return api(getState).get(`/api/v1/statuses/${id}`).then((response) => response.json()).then((status) => {
|
||||
dispatch(importFetchedStatus(status));
|
||||
if (status.group) {
|
||||
dispatch(fetchGroupRelationships([status.group.id]));
|
||||
|
@ -151,12 +152,12 @@ const deleteStatus = (id: string, withRedraft = false) => {
|
|||
|
||||
return api(getState)
|
||||
.delete(`/api/v1/statuses/${id}`)
|
||||
.then(response => {
|
||||
.then((response) => response.json()).then((data) => {
|
||||
dispatch({ type: STATUS_DELETE_SUCCESS, id });
|
||||
dispatch(deleteFromTimelines(id));
|
||||
|
||||
if (withRedraft) {
|
||||
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.pleroma?.content_type, withRedraft));
|
||||
dispatch(setComposeToStatus(status, data.text, data.spoiler_text, data.pleroma?.content_type, withRedraft));
|
||||
dispatch(openModal('COMPOSE'));
|
||||
}
|
||||
})
|
||||
|
@ -173,7 +174,7 @@ const fetchContext = (id: string) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: CONTEXT_FETCH_REQUEST, id });
|
||||
|
||||
return api(getState).get(`/api/v1/statuses/${id}/context`).then(({ data: context }) => {
|
||||
return api(getState).get(`/api/v1/statuses/${id}/context`).then((response) => response.json()).then((context) => {
|
||||
if (Array.isArray(context)) {
|
||||
// Mitra: returns a list of statuses
|
||||
dispatch(importFetchedStatuses(context));
|
||||
|
@ -199,29 +200,33 @@ const fetchContext = (id: string) =>
|
|||
const fetchNext = (statusId: string, next: string) =>
|
||||
async(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const response = await api(getState).get(next);
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
const data = await response.json();
|
||||
|
||||
dispatch(importFetchedStatuses(data));
|
||||
|
||||
dispatch({
|
||||
type: CONTEXT_FETCH_SUCCESS,
|
||||
id: statusId,
|
||||
ancestors: [],
|
||||
descendants: response.data,
|
||||
descendants: data,
|
||||
});
|
||||
|
||||
return { next: getNextLink(response) };
|
||||
return { next: response.pagination().next };
|
||||
};
|
||||
|
||||
const fetchAncestors = (id: string) =>
|
||||
async(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const response = await api(getState).get(`/api/v1/statuses/${id}/context/ancestors`);
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedStatuses(data));
|
||||
return response;
|
||||
};
|
||||
|
||||
const fetchDescendants = (id: string) =>
|
||||
async(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const response = await api(getState).get(`/api/v1/statuses/${id}/context/descendants`);
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
const data = await response.json();
|
||||
dispatch(importFetchedStatuses(data));
|
||||
return response;
|
||||
};
|
||||
|
||||
|
@ -231,7 +236,8 @@ const fetchStatusWithContext = (id: string) =>
|
|||
|
||||
if (features.paginatedContext) {
|
||||
await dispatch(fetchStatus(id));
|
||||
const responses = await Promise.all([
|
||||
|
||||
const [ancestors, descendants] = await Promise.all([
|
||||
dispatch(fetchAncestors(id)),
|
||||
dispatch(fetchDescendants(id)),
|
||||
]);
|
||||
|
@ -239,18 +245,17 @@ const fetchStatusWithContext = (id: string) =>
|
|||
dispatch({
|
||||
type: CONTEXT_FETCH_SUCCESS,
|
||||
id,
|
||||
ancestors: responses[0].data,
|
||||
descendants: responses[1].data,
|
||||
ancestors: await ancestors.json(),
|
||||
descendants: await descendants.json(),
|
||||
});
|
||||
|
||||
const next = getNextLink(responses[1]);
|
||||
return { next };
|
||||
return descendants.pagination();
|
||||
} else {
|
||||
await Promise.all([
|
||||
dispatch(fetchContext(id)),
|
||||
dispatch(fetchStatus(id)),
|
||||
]);
|
||||
return { next: undefined };
|
||||
return { next: null, prev: null };
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -323,11 +328,11 @@ const translateStatus = (id: string, lang?: string) => (dispatch: AppDispatch, g
|
|||
api(getState).post(`/api/v1/statuses/${id}/translate`, {
|
||||
lang, // Mastodon API
|
||||
target_language: lang, // HACK: Rebased and Pleroma compatibility
|
||||
}).then(response => {
|
||||
}).then((response) => response.json()).then((data) => {
|
||||
dispatch({
|
||||
type: STATUS_TRANSLATE_SUCCESS,
|
||||
id,
|
||||
translation: response.data,
|
||||
translation: data,
|
||||
});
|
||||
}).catch(error => {
|
||||
dispatch({
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { isLoggedIn } from 'soapbox/utils/auth.ts';
|
||||
import { getFeatures } from 'soapbox/utils/features.ts';
|
||||
|
||||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import { fetchRelationships } from './accounts.ts';
|
||||
import { importFetchedAccounts } from './importer/index.ts';
|
||||
|
@ -23,7 +23,7 @@ const SUGGESTIONS_V2_FETCH_FAIL = 'SUGGESTIONS_V2_FETCH_FAIL';
|
|||
const fetchSuggestionsV1 = (params: Record<string, any> = {}) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: SUGGESTIONS_FETCH_REQUEST, skipLoading: true });
|
||||
return api(getState).get('/api/v1/suggestions', { params }).then(({ data: accounts }) => {
|
||||
return api(getState).get('/api/v1/suggestions', { searchParams: params }).then((response) => response.json()).then((accounts) => {
|
||||
dispatch(importFetchedAccounts(accounts));
|
||||
dispatch({ type: SUGGESTIONS_FETCH_SUCCESS, accounts, skipLoading: true });
|
||||
return accounts;
|
||||
|
@ -39,10 +39,10 @@ const fetchSuggestionsV2 = (params: Record<string, any> = {}) =>
|
|||
|
||||
dispatch({ type: SUGGESTIONS_V2_FETCH_REQUEST, skipLoading: true });
|
||||
|
||||
return api(getState).get(next ? next : '/api/v2/suggestions', next ? {} : { params }).then((response) => {
|
||||
const suggestions: APIEntity[] = response.data;
|
||||
return api(getState).get(next ?? '/api/v2/suggestions', next ? {} : { searchParams: params }).then(async (response) => {
|
||||
const suggestions: APIEntity[] = await response.json();
|
||||
const accounts = suggestions.map(({ account }) => account);
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next')?.uri;
|
||||
const next = response.next();
|
||||
|
||||
dispatch(importFetchedAccounts(accounts));
|
||||
dispatch({ type: SUGGESTIONS_V2_FETCH_SUCCESS, suggestions, next, skipLoading: true });
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store.ts';
|
||||
import type { APIEntity } from 'soapbox/types/entities.ts';
|
||||
|
@ -26,7 +26,7 @@ const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL';
|
|||
const fetchHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchHashtagRequest());
|
||||
|
||||
api(getState).get(`/api/v1/tags/${name}`).then(({ data }) => {
|
||||
api(getState).get(`/api/v1/tags/${name}`).then((response) => response.json()).then((data) => {
|
||||
dispatch(fetchHashtagSuccess(name, data));
|
||||
}).catch(err => {
|
||||
dispatch(fetchHashtagFail(err));
|
||||
|
@ -51,7 +51,7 @@ const fetchHashtagFail = (error: unknown) => ({
|
|||
const followHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(followHashtagRequest(name));
|
||||
|
||||
api(getState).post(`/api/v1/tags/${name}/follow`).then(({ data }) => {
|
||||
api(getState).post(`/api/v1/tags/${name}/follow`).then((response) => response.json()).then((data) => {
|
||||
dispatch(followHashtagSuccess(name, data));
|
||||
}).catch(err => {
|
||||
dispatch(followHashtagFail(name, err));
|
||||
|
@ -78,7 +78,7 @@ const followHashtagFail = (name: string, error: unknown) => ({
|
|||
const unfollowHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(unfollowHashtagRequest(name));
|
||||
|
||||
api(getState).post(`/api/v1/tags/${name}/unfollow`).then(({ data }) => {
|
||||
api(getState).post(`/api/v1/tags/${name}/unfollow`).then((response) => response.json()).then((data) => {
|
||||
dispatch(unfollowHashtagSuccess(name, data));
|
||||
}).catch(err => {
|
||||
dispatch(unfollowHashtagFail(name, err));
|
||||
|
@ -105,9 +105,10 @@ const unfollowHashtagFail = (name: string, error: unknown) => ({
|
|||
const fetchFollowedHashtags = () => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchFollowedHashtagsRequest());
|
||||
|
||||
api(getState).get('/api/v1/followed_tags').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null));
|
||||
api(getState).get('/api/v1/followed_tags').then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(fetchFollowedHashtagsSuccess(data, next));
|
||||
}).catch(err => {
|
||||
dispatch(fetchFollowedHashtagsFail(err));
|
||||
});
|
||||
|
@ -137,9 +138,10 @@ const expandFollowedHashtags = () => (dispatch: AppDispatch, getState: () => Roo
|
|||
|
||||
dispatch(expandFollowedHashtagsRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null));
|
||||
api(getState).get(url).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
dispatch(expandFollowedHashtagsSuccess(data, next));
|
||||
}).catch(error => {
|
||||
dispatch(expandFollowedHashtagsFail(error));
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ import { getSettings } from 'soapbox/actions/settings.ts';
|
|||
import { normalizeStatus } from 'soapbox/normalizers/index.ts';
|
||||
import { shouldFilter } from 'soapbox/utils/timelines.ts';
|
||||
|
||||
import api, { getNextLink, getPrevLink } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import { fetchGroupRelationships } from './groups.ts';
|
||||
import { importFetchedStatus, importFetchedStatuses } from './importer/index.ts';
|
||||
|
@ -169,17 +169,20 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
|
|||
|
||||
dispatch(expandTimelineRequest(timelineId, isLoadingMore));
|
||||
|
||||
return api(getState).get(path, { params }).then(response => {
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
return api(getState).get(path, { searchParams: params }).then(async (response) => {
|
||||
const { next, prev } = response.pagination();
|
||||
const data: APIEntity[] = await response.json();
|
||||
|
||||
const statusesFromGroups = (response.data as Status[]).filter((status) => !!status.group);
|
||||
dispatch(importFetchedStatuses(data));
|
||||
|
||||
const statusesFromGroups = (data as Status[]).filter((status) => !!status.group);
|
||||
dispatch(fetchGroupRelationships(statusesFromGroups.map((status: any) => status.group?.id)));
|
||||
|
||||
dispatch(expandTimelineSuccess(
|
||||
timelineId,
|
||||
response.data,
|
||||
getNextLink(response),
|
||||
getPrevLink(response),
|
||||
data,
|
||||
next,
|
||||
prev,
|
||||
response.status === 206,
|
||||
isLoadingRecent,
|
||||
isLoadingMore,
|
||||
|
@ -267,8 +270,8 @@ const expandTimelineRequest = (timeline: string, isLoadingMore: boolean) => ({
|
|||
const expandTimelineSuccess = (
|
||||
timeline: string,
|
||||
statuses: APIEntity[],
|
||||
next: string | undefined,
|
||||
prev: string | undefined,
|
||||
next: string | null,
|
||||
prev: string | null,
|
||||
partial: boolean,
|
||||
isLoadingRecent: boolean,
|
||||
isLoadingMore: boolean,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { APIEntity } from 'soapbox/types/entities.ts';
|
||||
import { getFeatures } from 'soapbox/utils/features.ts';
|
||||
|
||||
import api, { getLinks } from '../api/index.ts';
|
||||
import api from '../api/index.ts';
|
||||
|
||||
import { importFetchedStatuses } from './importer/index.ts';
|
||||
|
||||
|
@ -23,13 +23,14 @@ const fetchTrendingStatuses = () =>
|
|||
if (!features.trendingStatuses) return;
|
||||
|
||||
dispatch({ type: TRENDING_STATUSES_FETCH_REQUEST });
|
||||
return api(getState).get('/api/v1/trends/statuses').then((response) => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
return api(getState).get('/api/v1/trends/statuses').then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
const statuses = response.data;
|
||||
const statuses = data;
|
||||
|
||||
dispatch(importFetchedStatuses(statuses));
|
||||
dispatch(fetchTrendingStatusesSuccess(statuses, next ? next.uri : null));
|
||||
dispatch(fetchTrendingStatusesSuccess(statuses, next));
|
||||
return statuses;
|
||||
}).catch(error => {
|
||||
dispatch(fetchTrendingStatusesFail(error));
|
||||
|
@ -50,13 +51,14 @@ const fetchTrendingStatusesFail = (error: unknown) => ({
|
|||
|
||||
const expandTrendingStatuses = (path: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
api(getState).get(path).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
api(getState).get(path).then(async (response) => {
|
||||
const next = response.next();
|
||||
const data = await response.json();
|
||||
|
||||
const statuses = response.data;
|
||||
const statuses = data;
|
||||
|
||||
dispatch(importFetchedStatuses(statuses));
|
||||
dispatch(expandTrendingStatusesSuccess(statuses, next ? next.uri : null));
|
||||
dispatch(expandTrendingStatusesSuccess(statuses, next));
|
||||
}).catch(error => {
|
||||
dispatch(expandTrendingStatusesFail(error));
|
||||
});
|
||||
|
|
|
@ -11,8 +11,8 @@ const fetchTrends = () =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchTrendsRequest());
|
||||
|
||||
api(getState).get('/api/v1/trends').then(response => {
|
||||
dispatch(fetchTrendsSuccess(response.data));
|
||||
api(getState).get('/api/v1/trends').then((response) => response.json()).then(data => {
|
||||
dispatch(fetchTrendsSuccess(data));
|
||||
}).catch(error => dispatch(fetchTrendsFail(error)));
|
||||
};
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { MastodonResponse } from 'soapbox/api/MastodonResponse.ts';
|
||||
|
||||
export class HTTPError extends Error {
|
||||
|
||||
response: Response;
|
||||
response: MastodonResponse;
|
||||
request: Request;
|
||||
|
||||
constructor(response: Response, request: Request) {
|
||||
constructor(response: MastodonResponse, request: Request) {
|
||||
super(response.statusText);
|
||||
this.response = response;
|
||||
this.request = request;
|
||||
|
|
|
@ -2,7 +2,8 @@ import { HTTPError } from './HTTPError.ts';
|
|||
import { MastodonResponse } from './MastodonResponse.ts';
|
||||
|
||||
interface Opts {
|
||||
searchParams?: URLSearchParams | Record<string, string | number | boolean>;
|
||||
searchParams?: URLSearchParams | Record<string, string | number | boolean | string[] | number[] | boolean[] | null | undefined>;
|
||||
onUploadProgress?: (e: ProgressEvent) => void;
|
||||
headers?: Record<string, string>;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
@ -56,7 +57,16 @@ export class MastodonClient {
|
|||
? opts.searchParams
|
||||
: Object
|
||||
.entries(opts.searchParams)
|
||||
.map(([key, value]) => ([key, String(value)]));
|
||||
.reduce<[string, string][]>((acc, [key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
for (const v of value) {
|
||||
acc.push([`${key}[]`, String(v)]);
|
||||
}
|
||||
} else if (value !== undefined && value !== null) {
|
||||
acc.push([key, String(value)]);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
url.search = new URLSearchParams(params).toString();
|
||||
}
|
||||
|
@ -70,7 +80,6 @@ export class MastodonClient {
|
|||
let body: BodyInit | undefined;
|
||||
|
||||
if (data instanceof FormData) {
|
||||
headers.set('Content-Type', 'multipart/form-data');
|
||||
body = data;
|
||||
} else if (data !== undefined) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
|
@ -84,19 +93,77 @@ export class MastodonClient {
|
|||
body,
|
||||
});
|
||||
|
||||
const response = await this.fetch(request);
|
||||
const response = opts.onUploadProgress
|
||||
? await this.xhr(request, opts)
|
||||
: MastodonResponse.fromResponse(await this.fetch(request));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new HTTPError(response, request);
|
||||
}
|
||||
|
||||
// Fix for non-compliant browsers.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Response/body
|
||||
if (response.status === 204 || request.method === 'HEAD') {
|
||||
return new MastodonResponse(null, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform an XHR request from the native `Request` object and get back a `MastodonResponse`.
|
||||
* This is needed because unfortunately `fetch` does not support upload progress.
|
||||
*/
|
||||
private async xhr(request: Request, opts: Opts = {}): Promise<MastodonResponse> {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const { resolve, reject, promise } = Promise.withResolvers<MastodonResponse>();
|
||||
|
||||
xhr.responseType = 'arraybuffer';
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState !== XMLHttpRequest.DONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = new Headers(
|
||||
xhr.getAllResponseHeaders()
|
||||
.trim()
|
||||
.split(/[\r\n]+/)
|
||||
.map((line): [string, string] => {
|
||||
const [name, ...rest] = line.split(': ');
|
||||
const value = rest.join(': ');
|
||||
return [name, value];
|
||||
}),
|
||||
);
|
||||
|
||||
const response = new MastodonResponse(xhr.response, {
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText,
|
||||
headers,
|
||||
});
|
||||
|
||||
resolve(response);
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
reject(new TypeError('Network request failed'));
|
||||
};
|
||||
|
||||
xhr.onabort = () => {
|
||||
reject(new DOMException('The request was aborted', 'AbortError'));
|
||||
};
|
||||
|
||||
if (opts.onUploadProgress) {
|
||||
xhr.upload.onprogress = opts.onUploadProgress;
|
||||
}
|
||||
|
||||
return new MastodonResponse(response.body, response);
|
||||
if (opts.signal) {
|
||||
opts.signal.addEventListener('abort', () => xhr.abort(), { once: true });
|
||||
}
|
||||
|
||||
xhr.open(request.method, request.url, true);
|
||||
|
||||
for (const [name, value] of request.headers) {
|
||||
xhr.setRequestHeader(name, value);
|
||||
}
|
||||
|
||||
xhr.send(await request.arrayBuffer());
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,16 +1,87 @@
|
|||
import LinkHeader from 'http-link-header';
|
||||
import { z } from 'zod';
|
||||
|
||||
/** Mastodon JSON error response. */
|
||||
export interface MastodonError {
|
||||
/** Error message in plaintext, to be displayed in the UI. */
|
||||
error: string;
|
||||
/** Map of field validation errors. See: https://github.com/mastodon/mastodon/pull/15803 */
|
||||
detail?: Record<string, { error: string; description: string }[]>;
|
||||
}
|
||||
|
||||
/** Parsed Mastodon `Link` header. */
|
||||
export interface MastodonLink {
|
||||
rel: string;
|
||||
uri: string;
|
||||
}
|
||||
|
||||
export class MastodonResponse extends Response {
|
||||
|
||||
/** Parses the `Link` header and returns URLs for the `prev` and `next` pages of this response, if any. */
|
||||
pagination(): { prev?: string; next?: string } {
|
||||
/** Construct a `MastodonResponse` from a regular `Response` object. */
|
||||
static fromResponse(response: Response): MastodonResponse {
|
||||
// Fix for non-compliant browsers.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Response/body
|
||||
if (response.status === 204) {
|
||||
return new MastodonResponse(null, response);
|
||||
}
|
||||
|
||||
return new MastodonResponse(response.body, response);
|
||||
}
|
||||
|
||||
/** Parses the `Link` header and returns an array of URLs and their rel values. */
|
||||
links(): MastodonLink[] {
|
||||
const header = this.headers.get('link');
|
||||
const links = header ? new LinkHeader(header) : undefined;
|
||||
|
||||
if (header) {
|
||||
return new LinkHeader(header).refs;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses the `Link` header and returns URLs for the `prev` and `next` pages of this response, if any. */
|
||||
pagination(): { prev: string | null; next: string | null } {
|
||||
const links = this.links();
|
||||
|
||||
return {
|
||||
next: links?.refs.find((link) => link.rel === 'next')?.uri,
|
||||
prev: links?.refs.find((link) => link.rel === 'prev')?.uri,
|
||||
next: links.find((link) => link.rel === 'next')?.uri ?? null,
|
||||
prev: links.find((link) => link.rel === 'prev')?.uri ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns the `next` URI from the `Link` header, if applicable. */
|
||||
next(): string | null {
|
||||
const links = this.links();
|
||||
return links.find((link) => link.rel === 'next')?.uri ?? null;
|
||||
}
|
||||
|
||||
/** Returns the `prev` URI from the `Link` header, if applicable. */
|
||||
prev(): string | null {
|
||||
const links = this.links();
|
||||
return links.find((link) => link.rel === 'prev')?.uri ?? null;
|
||||
}
|
||||
|
||||
/** Extracts the error JSON from the response body, if possible. Otherwise returns `null`. */
|
||||
async error(): Promise<MastodonError | null> {
|
||||
const data = await this.json();
|
||||
const result = MastodonResponse.errorSchema().safeParse(data);
|
||||
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Validates the error response schema. */
|
||||
private static errorSchema(): z.ZodType<MastodonError> {
|
||||
return z.object({
|
||||
error: z.string(),
|
||||
detail: z.record(
|
||||
z.string(),
|
||||
z.object({ error: z.string(), description: z.string() }).array(),
|
||||
).optional(),
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import LinkHeader from 'http-link-header';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import type { AxiosInstance, AxiosResponse } from 'axios';
|
||||
|
||||
const api = await vi.importActual('../index') as Record<string, Function>;
|
||||
let mocks: Array<Function> = [];
|
||||
|
||||
export const __stub = (func: (mock: MockAdapter) => void) => mocks.push(func);
|
||||
export const __clear = (): Function[] => mocks = [];
|
||||
|
||||
const setupMock = (axios: AxiosInstance) => {
|
||||
const mock = new MockAdapter(axios, { onNoMatch: 'throwException' });
|
||||
mocks.map(func => func(mock));
|
||||
};
|
||||
|
||||
export const getLinks = (response: AxiosResponse): LinkHeader => {
|
||||
return new LinkHeader(response.headers?.link);
|
||||
};
|
||||
|
||||
export const getNextLink = (response: AxiosResponse) => {
|
||||
const nextLink = new LinkHeader(response.headers?.link);
|
||||
return nextLink.refs.find(link => link.rel === 'next')?.uri;
|
||||
};
|
||||
|
||||
export const getPrevLink = (response: AxiosResponse) => {
|
||||
const prevLink = new LinkHeader(response.headers?.link);
|
||||
return prevLink.refs.find(link => link.rel === 'prev')?.uri;
|
||||
};
|
||||
|
||||
export const baseClient = (...params: any[]) => {
|
||||
const axios = api.baseClient(...params);
|
||||
setupMock(axios);
|
||||
return axios;
|
||||
};
|
||||
|
||||
export default (...params: any[]) => {
|
||||
const axios = api.default(...params);
|
||||
setupMock(axios);
|
||||
return axios;
|
||||
};
|
|
@ -1,43 +0,0 @@
|
|||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { buildGroup } from 'soapbox/jest/factory.ts';
|
||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers.tsx';
|
||||
|
||||
import { useGroup } from './useGroup.ts';
|
||||
|
||||
const group = buildGroup({ id: '1', display_name: 'soapbox' });
|
||||
|
||||
describe('useGroup hook', () => {
|
||||
describe('with a successful request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/groups/${group.id}`).reply(200, group);
|
||||
});
|
||||
});
|
||||
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(() => useGroup(group.id));
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.group?.id).toBe(group.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful query', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/groups/${group.id}`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('is has error state', async() => {
|
||||
const { result } = renderHook(() => useGroup(group.id));
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.group).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,50 +0,0 @@
|
|||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { buildGroup } from 'soapbox/jest/factory.ts';
|
||||
import { renderHook, rootState, waitFor } from 'soapbox/jest/test-helpers.tsx';
|
||||
|
||||
import { useGroupLookup } from './useGroupLookup.ts';
|
||||
|
||||
const group = buildGroup({ id: '1', slug: 'soapbox' });
|
||||
const state = {
|
||||
...rootState,
|
||||
instance: {
|
||||
...rootState.instance,
|
||||
version: '3.4.1 (compatible; TruthSocial 1.0.0)',
|
||||
},
|
||||
};
|
||||
|
||||
describe('useGroupLookup hook', () => {
|
||||
describe('with a successful request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/groups/lookup?name=${group.slug}`).reply(200, group);
|
||||
});
|
||||
});
|
||||
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(() => useGroupLookup(group.slug), undefined, state);
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.entity?.id).toBe(group.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful query', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/groups/lookup?name=${group.slug}`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('is has error state', async() => {
|
||||
const { result } = renderHook(() => useGroupLookup(group.slug), undefined, state);
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.entity).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,46 +0,0 @@
|
|||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { buildStatus } from 'soapbox/jest/factory.ts';
|
||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers.tsx';
|
||||
|
||||
import { useGroupMedia } from './useGroupMedia.ts';
|
||||
|
||||
const status = buildStatus();
|
||||
const groupId = '1';
|
||||
|
||||
describe('useGroupMedia hook', () => {
|
||||
describe('with a successful request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/timelines/group/${groupId}?only_media=true`).reply(200, [status]);
|
||||
});
|
||||
});
|
||||
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(() => useGroupMedia(groupId));
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.entities.length).toBe(1);
|
||||
expect(result.current.entities[0].id).toBe(status.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful query', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/timelines/group/${groupId}?only_media=true`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('is has error state', async() => {
|
||||
const { result } = renderHook(() => useGroupMedia(groupId));
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.entities.length).toBe(0);
|
||||
expect(result.current.isError).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,47 +0,0 @@
|
|||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { buildGroupMember } from 'soapbox/jest/factory.ts';
|
||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers.tsx';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member.ts';
|
||||
|
||||
import { useGroupMembers } from './useGroupMembers.ts';
|
||||
|
||||
const groupMember = buildGroupMember();
|
||||
const groupId = '1';
|
||||
|
||||
describe('useGroupMembers hook', () => {
|
||||
describe('with a successful request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/groups/${groupId}/memberships?role=${GroupRoles.ADMIN}`).reply(200, [groupMember]);
|
||||
});
|
||||
});
|
||||
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(() => useGroupMembers(groupId, GroupRoles.ADMIN));
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.groupMembers.length).toBe(1);
|
||||
expect(result.current.groupMembers[0].id).toBe(groupMember.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful query', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/groups/${groupId}/memberships?role=${GroupRoles.ADMIN}`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('is has error state', async() => {
|
||||
const { result } = renderHook(() => useGroupMembers(groupId, GroupRoles.ADMIN));
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.groupMembers.length).toBe(0);
|
||||
expect(result.current.isError).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,49 +0,0 @@
|
|||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { buildGroup } from 'soapbox/jest/factory.ts';
|
||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers.tsx';
|
||||
import { instanceV1Schema } from 'soapbox/schemas/instance.ts';
|
||||
|
||||
import { useGroups } from './useGroups.ts';
|
||||
|
||||
const group = buildGroup({ id: '1', display_name: 'soapbox' });
|
||||
const store = {
|
||||
instance: instanceV1Schema.parse({
|
||||
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
|
||||
}),
|
||||
};
|
||||
|
||||
describe('useGroups hook', () => {
|
||||
describe('with a successful request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/groups').reply(200, [group]);
|
||||
});
|
||||
});
|
||||
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(useGroups, undefined, store);
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.groups).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful query', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/groups').networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('is has error state', async() => {
|
||||
const { result } = renderHook(useGroups, undefined, store);
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.groups).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,66 +0,0 @@
|
|||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { Entities } from 'soapbox/entity-store/entities.ts';
|
||||
import { buildAccount, buildGroup } from 'soapbox/jest/factory.ts';
|
||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers.tsx';
|
||||
import { instanceV1Schema } from 'soapbox/schemas/instance.ts';
|
||||
|
||||
import { usePendingGroups } from './usePendingGroups.ts';
|
||||
|
||||
const id = '1';
|
||||
const group = buildGroup({ id, display_name: 'soapbox' });
|
||||
const store = {
|
||||
instance: instanceV1Schema.parse({
|
||||
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
|
||||
}),
|
||||
me: '1',
|
||||
entities: {
|
||||
[Entities.ACCOUNTS]: {
|
||||
store: {
|
||||
[id]: buildAccount({
|
||||
id,
|
||||
acct: 'tiger',
|
||||
display_name: 'Tiger',
|
||||
avatar: 'test.jpg',
|
||||
verified: true,
|
||||
}),
|
||||
},
|
||||
lists: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('usePendingGroups hook', () => {
|
||||
describe('with a successful request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/groups').reply(200, [group]);
|
||||
});
|
||||
});
|
||||
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(usePendingGroups, undefined, store);
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.groups).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful query', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/groups').networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('is has error state', async() => {
|
||||
const { result } = renderHook(usePendingGroups, undefined, store);
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.groups).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,107 +1,39 @@
|
|||
/**
|
||||
* API: HTTP client and utilities.
|
||||
* @see {@link https://github.com/axios/axios}
|
||||
* @module soapbox/api
|
||||
*/
|
||||
|
||||
import axios, { type AxiosInstance, type AxiosResponse } from 'axios';
|
||||
import LinkHeader from 'http-link-header';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { MastodonClient } from 'soapbox/api/MastodonClient.ts';
|
||||
import * as BuildConfig from 'soapbox/build-config.ts';
|
||||
import { selectAccount } from 'soapbox/selectors/index.ts';
|
||||
import { RootState } from 'soapbox/store.ts';
|
||||
import { getAccessToken, getAppToken, isURL, parseBaseURL } from 'soapbox/utils/auth.ts';
|
||||
|
||||
import type MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
/**
|
||||
Parse Link headers, mostly for pagination.
|
||||
@see {@link https://www.npmjs.com/package/http-link-header}
|
||||
@param {object} response - Axios response object
|
||||
@returns {object} Link object
|
||||
*/
|
||||
export const getLinks = (response: AxiosResponse): LinkHeader => {
|
||||
return new LinkHeader(response.headers?.link);
|
||||
};
|
||||
|
||||
export const getNextLink = (response: AxiosResponse): string | undefined => {
|
||||
return getLinks(response).refs.find(link => link.rel === 'next')?.uri;
|
||||
};
|
||||
|
||||
export const getPrevLink = (response: AxiosResponse): string | undefined => {
|
||||
return getLinks(response).refs.find(link => link.rel === 'prev')?.uri;
|
||||
};
|
||||
import { getAccessToken, getAppToken, parseBaseURL } from 'soapbox/utils/auth.ts';
|
||||
|
||||
const getToken = (state: RootState, authType: string) => {
|
||||
return authType === 'app' ? getAppToken(state) : getAccessToken(state);
|
||||
};
|
||||
|
||||
const maybeParseJSON = (data: string) => {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (Exception) {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
const getAuthBaseURL = createSelector([
|
||||
(state: RootState, me: string | false | null) => me ? selectAccount(state, me)?.url : undefined,
|
||||
(state: RootState, _me: string | false | null) => state.auth.me,
|
||||
], (accountUrl, authUserUrl) => {
|
||||
const baseURL = parseBaseURL(accountUrl) || parseBaseURL(authUserUrl);
|
||||
return baseURL !== window.location.origin ? baseURL : '';
|
||||
return parseBaseURL(accountUrl) || parseBaseURL(authUserUrl);
|
||||
});
|
||||
|
||||
/**
|
||||
* Base client for HTTP requests.
|
||||
* @param {string} accessToken
|
||||
* @param {string} baseURL
|
||||
* @returns {object} Axios instance
|
||||
*/
|
||||
/** Base client for HTTP requests. */
|
||||
export const baseClient = (
|
||||
accessToken?: string | null,
|
||||
baseURL: string = '',
|
||||
nostrSign = false,
|
||||
): AxiosInstance => {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
if (nostrSign) {
|
||||
headers['X-Nostr-Sign'] = 'true';
|
||||
}
|
||||
|
||||
return axios.create({
|
||||
// When BACKEND_URL is set, always use it.
|
||||
baseURL: isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : baseURL,
|
||||
headers,
|
||||
transformResponse: [maybeParseJSON],
|
||||
});
|
||||
baseURL?: string,
|
||||
): MastodonClient => {
|
||||
return new MastodonClient(baseURL || location.origin, accessToken || undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stateful API client.
|
||||
* Uses credentials from the Redux store if available.
|
||||
* @param {function} getState - Must return the Redux state
|
||||
* @param {string} authType - Either 'user' or 'app'
|
||||
* @returns {object} Axios instance
|
||||
*/
|
||||
export default (getState: () => RootState, authType: string = 'user'): AxiosInstance => {
|
||||
* Stateful API client.
|
||||
* Uses credentials from the Redux store if available.
|
||||
*/
|
||||
export default (getState: () => RootState, authType: string = 'user'): MastodonClient => {
|
||||
const state = getState();
|
||||
const accessToken = getToken(state, authType);
|
||||
const me = state.me;
|
||||
const baseURL = me ? getAuthBaseURL(state, me) : '';
|
||||
const baseURL = BuildConfig.BACKEND_URL || (me ? getAuthBaseURL(state, me) : undefined) || location.origin;
|
||||
|
||||
const relayUrl = state.instance?.nostr?.relay;
|
||||
const pubkey = state.instance?.nostr?.pubkey;
|
||||
const nostrSign = Boolean(relayUrl && pubkey);
|
||||
|
||||
return baseClient(accessToken, baseURL, nostrSign);
|
||||
return baseClient(accessToken, baseURL);
|
||||
};
|
||||
|
||||
// The Jest mock exports these, so they're needed for TypeScript.
|
||||
export const __stub = (_func: (mock: MockAdapter) => void) => 0;
|
||||
export const __clear = (): Function[] => [];
|
||||
|
|
|
@ -32,7 +32,7 @@ interface EntityCallbacks<Value, Error = unknown> {
|
|||
|
||||
/**
|
||||
* Passed into hooks to make requests.
|
||||
* Must return an Axios response.
|
||||
* Must return a Response object.
|
||||
*/
|
||||
type EntityFn<T> = (value: T) => Promise<Response>
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import userCheckIcon from '@tabler/icons/outline/user-check.svg';
|
|||
import userXIcon from '@tabler/icons/outline/user-x.svg';
|
||||
import userIcon from '@tabler/icons/outline/user.svg';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
@ -33,6 +32,7 @@ import { initMuteModal } from 'soapbox/actions/mutes.ts';
|
|||
import { initReport, ReportableEntities } from 'soapbox/actions/reports.ts';
|
||||
import { setSearchAccount } from 'soapbox/actions/search.ts';
|
||||
import { getSettings } from 'soapbox/actions/settings.ts';
|
||||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||
import { useFollow } from 'soapbox/api/hooks/index.ts';
|
||||
import Badge from 'soapbox/components/badge.tsx';
|
||||
import DropdownMenu, { Menu } from 'soapbox/components/dropdown-menu/index.ts';
|
||||
|
@ -121,9 +121,10 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
|
||||
const createAndNavigateToChat = useMutation({
|
||||
mutationFn: (accountId: string) => getOrCreateChatByAccountId(accountId),
|
||||
onError: (error: AxiosError) => {
|
||||
const data = error.response?.data as any;
|
||||
toast.error(data?.error);
|
||||
onError: (error) => {
|
||||
if (error instanceof HTTPError) {
|
||||
toast.showAlertForError(error);
|
||||
}
|
||||
},
|
||||
onSuccess: async (response) => {
|
||||
const data = await response.json();
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { AxiosError } from 'axios';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
|
@ -150,7 +149,8 @@ const ManageDittoServer: React.FC = () => {
|
|||
|
||||
try {
|
||||
const response = await dispatch(uploadMedia(data));
|
||||
const attachment = normalizeAttachment(response.data);
|
||||
const json = await response.json();
|
||||
const attachment = normalizeAttachment(json);
|
||||
|
||||
if (attachment.type !== 'image') {
|
||||
throw new Error('Only images supported.');
|
||||
|
@ -166,11 +166,9 @@ const ManageDittoServer: React.FC = () => {
|
|||
setThumbnailLoading(false);
|
||||
e.target.value = '';
|
||||
|
||||
if (err instanceof AxiosError) {
|
||||
toast.error(err.response?.data?.error || 'An error occurred');
|
||||
return;
|
||||
if (err instanceof HTTPError) {
|
||||
toast.showAlertForError(err);
|
||||
}
|
||||
toast.error((err as Error)?.message || 'An error occurred');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -268,7 +266,8 @@ const ScreenshotInput: StreamfieldComponent<Screenshot> = ({ value, onChange })
|
|||
|
||||
try {
|
||||
const response = await dispatch(uploadMedia(data));
|
||||
const attachment = normalizeAttachment(response.data);
|
||||
const json = await response.json();
|
||||
const attachment = normalizeAttachment(json);
|
||||
|
||||
if (attachment.type !== 'image') {
|
||||
throw new Error('Only images supported.');
|
||||
|
@ -289,11 +288,9 @@ const ScreenshotInput: StreamfieldComponent<Screenshot> = ({ value, onChange })
|
|||
setLoading(false);
|
||||
e.target.value = '';
|
||||
|
||||
if (err instanceof AxiosError) {
|
||||
toast.error(err.response?.data?.error || 'An error occurred');
|
||||
return;
|
||||
if (err instanceof HTTPError) {
|
||||
toast.showAlertForError(err);
|
||||
}
|
||||
toast.error((err as Error)?.message || 'An error occurred');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -27,21 +27,21 @@ const Dashboard: React.FC = () => {
|
|||
const { account } = useOwnAccount();
|
||||
|
||||
const handleSubscribersClick: React.MouseEventHandler = e => {
|
||||
dispatch(getSubscribersCsv()).then(({ data }) => {
|
||||
dispatch(getSubscribersCsv()).then((response) => response.json()).then((data) => {
|
||||
download(data, 'subscribers.csv');
|
||||
}).catch(() => {});
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleUnsubscribersClick: React.MouseEventHandler = e => {
|
||||
dispatch(getUnsubscribersCsv()).then(({ data }) => {
|
||||
dispatch(getUnsubscribersCsv()).then((response) => response.json()).then((data) => {
|
||||
download(data, 'unsubscribers.csv');
|
||||
}).catch(() => {});
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleCombinedClick: React.MouseEventHandler = e => {
|
||||
dispatch(getCombinedCsv()).then(({ data }) => {
|
||||
dispatch(getCombinedCsv()).then((response) => response.json()).then((data) => {
|
||||
download(data, 'combined.csv');
|
||||
}).catch(() => {});
|
||||
e.preventDefault();
|
||||
|
|
|
@ -8,8 +8,6 @@ import Stack from 'soapbox/components/ui/stack.tsx';
|
|||
import Text from 'soapbox/components/ui/text.tsx';
|
||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||
|
||||
import type { AxiosResponse } from 'axios';
|
||||
|
||||
const noOp = () => {};
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -44,8 +42,8 @@ const CaptchaField: React.FC<ICaptchaField> = ({
|
|||
const [refresh, setRefresh] = useState<NodeJS.Timeout | undefined>(undefined);
|
||||
|
||||
const getCaptcha = () => {
|
||||
dispatch(fetchCaptcha()).then((response: AxiosResponse) => {
|
||||
const captcha = ImmutableMap<string, any>(response.data);
|
||||
dispatch(fetchCaptcha()).then((response) => response.json()).then((data) => {
|
||||
const captcha = ImmutableMap<string, any>(data);
|
||||
setCaptcha(captcha);
|
||||
onFetch(captcha);
|
||||
}).catch((error: Error) => {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useState } from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth.ts';
|
||||
import { logIn, verifyCredentials, switchAccount, MfaRequiredError } from 'soapbox/actions/auth.ts';
|
||||
import { fetchInstance } from 'soapbox/actions/instance.ts';
|
||||
import { closeModal, openModal } from 'soapbox/actions/modals.ts';
|
||||
import { BigCard } from 'soapbox/components/big-card.tsx';
|
||||
|
@ -16,8 +16,6 @@ import ConsumersList from './consumers-list.tsx';
|
|||
import LoginForm from './login-form.tsx';
|
||||
import OtpAuthForm from './otp-auth-form.tsx';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const LoginPage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -53,11 +51,10 @@ const LoginPage = () => {
|
|||
} else {
|
||||
setShouldRedirect(true);
|
||||
}
|
||||
}).catch((error: AxiosError) => {
|
||||
const data: any = error.response?.data;
|
||||
if (data?.error === 'mfa_required') {
|
||||
}).catch((error) => {
|
||||
if (error instanceof MfaRequiredError) {
|
||||
setMfaAuthNeeded(true);
|
||||
setMfaToken(data.mfa_token);
|
||||
setMfaToken(error.token);
|
||||
}
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
import { Route, Switch } from 'react-router-dom';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { fireEvent, render, screen, waitFor } from 'soapbox/jest/test-helpers.tsx';
|
||||
|
||||
import PasswordResetConfirm from './password-reset-confirm.tsx';
|
||||
|
||||
const TestableComponent = () => (
|
||||
<Switch>
|
||||
<Route path='/edit' exact><PasswordResetConfirm /></Route>
|
||||
<Route path='/' exact><span data-testid='home'>Homepage</span></Route> {/* eslint-disable-line formatjs/no-literal-string-in-jsx */}
|
||||
</Switch>
|
||||
);
|
||||
|
||||
describe('<PasswordResetConfirm />', () => {
|
||||
it('handles successful responses from the API', async() => {
|
||||
__stub(mock => {
|
||||
mock.onPost('/api/v1/truth/password_reset/confirm')
|
||||
.reply(200, {});
|
||||
});
|
||||
|
||||
render(
|
||||
<TestableComponent />,
|
||||
{},
|
||||
null,
|
||||
{ initialEntries: ['/edit'] },
|
||||
);
|
||||
|
||||
fireEvent.submit(
|
||||
screen.getByTestId('form'), {
|
||||
preventDefault: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('home')).toHaveTextContent('Homepage');
|
||||
expect(screen.queryByTestId('form-group-error')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles failed responses from the API', async() => {
|
||||
__stub(mock => {
|
||||
mock.onPost('/api/v1/truth/password_reset/confirm')
|
||||
.reply(403, {});
|
||||
});
|
||||
|
||||
render(
|
||||
<TestableComponent />,
|
||||
{},
|
||||
null,
|
||||
{ initialEntries: ['/edit'] },
|
||||
);
|
||||
|
||||
await fireEvent.submit(
|
||||
screen.getByTestId('form'), {
|
||||
preventDefault: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('home')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('form-group-error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,6 +1,5 @@
|
|||
import atIcon from '@tabler/icons/outline/at.svg';
|
||||
import checkIcon from '@tabler/icons/outline/check.svg';
|
||||
import axios from 'axios';
|
||||
import { debounce } from 'es-toolkit';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
|
@ -71,12 +70,12 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
const [passwordConfirmation, setPasswordConfirmation] = useState('');
|
||||
const [passwordMismatch, setPasswordMismatch] = useState(false);
|
||||
|
||||
const source = useRef(axios.CancelToken.source());
|
||||
const controllerRef = useRef(new AbortController());
|
||||
|
||||
const refreshCancelToken = () => {
|
||||
source.current.cancel();
|
||||
source.current = axios.CancelToken.source();
|
||||
return source.current;
|
||||
const refreshController = () => {
|
||||
controllerRef.current.abort();
|
||||
controllerRef.current = new AbortController();
|
||||
return controllerRef.current;
|
||||
};
|
||||
|
||||
const updateParams = (map: any) => {
|
||||
|
@ -90,7 +89,6 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
const onUsernameChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
updateParams({ username: e.target.value });
|
||||
setUsernameUnavailable(false);
|
||||
source.current.cancel();
|
||||
|
||||
const domain = params.get('domain');
|
||||
usernameAvailable(e.target.value, domain ? domains!.find(({ id }) => id === domain)?.domain : undefined);
|
||||
|
@ -99,7 +97,6 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
const onDomainChange: React.ChangeEventHandler<HTMLSelectElement> = e => {
|
||||
updateParams({ domain: e.target.value || null });
|
||||
setUsernameUnavailable(false);
|
||||
source.current.cancel();
|
||||
|
||||
const username = params.get('username');
|
||||
if (username) {
|
||||
|
@ -188,9 +185,9 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
const usernameAvailable = useCallback(debounce((username, domain?: string) => {
|
||||
if (!supportsAccountLookup) return;
|
||||
|
||||
const source = refreshCancelToken();
|
||||
const controller = refreshController();
|
||||
|
||||
dispatch(accountLookup(`${username}${domain ? `@${domain}` : ''}`, source.token))
|
||||
dispatch(accountLookup(`${username}${domain ? `@${domain}` : ''}`, controller.signal))
|
||||
.then(account => {
|
||||
setUsernameUnavailable(!!account);
|
||||
})
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { ChatContext } from 'soapbox/contexts/chat-context.tsx';
|
||||
import { buildAccount, buildInstance } from 'soapbox/jest/factory.ts';
|
||||
import { queryClient, render, rootState, screen, waitFor } from 'soapbox/jest/test-helpers.tsx';
|
||||
import { normalizeChatMessage } from 'soapbox/normalizers/index.ts';
|
||||
import { IChat } from 'soapbox/queries/chats.ts';
|
||||
import { ChatMessage } from 'soapbox/types/entities.ts';
|
||||
|
||||
import ChatMessageList from './chat-message-list.tsx';
|
||||
|
||||
const chat: IChat = {
|
||||
accepted: true,
|
||||
account: buildAccount({
|
||||
username: 'username',
|
||||
verified: true,
|
||||
id: '1',
|
||||
acct: 'acct',
|
||||
avatar: 'avatar',
|
||||
avatar_static: 'avatar',
|
||||
display_name: 'my name',
|
||||
}),
|
||||
chat_type: 'direct',
|
||||
created_at: '2020-06-10T02:05:06.000Z',
|
||||
created_by_account: '2',
|
||||
discarded_at: null,
|
||||
id: '14',
|
||||
last_message: null,
|
||||
latest_read_message_by_account: [],
|
||||
latest_read_message_created_at: null,
|
||||
message_expiration: 1209600,
|
||||
unread: 5,
|
||||
};
|
||||
|
||||
const chatMessages: ChatMessage[] = [
|
||||
normalizeChatMessage({
|
||||
account_id: '1',
|
||||
chat_id: '14',
|
||||
content: 'this is the first chat',
|
||||
created_at: '2022-09-09T16:02:26.186Z',
|
||||
emoji_reactions: null,
|
||||
expiration: 1209600,
|
||||
id: '1',
|
||||
unread: false,
|
||||
pending: false,
|
||||
}),
|
||||
normalizeChatMessage({
|
||||
account_id: '2',
|
||||
chat_id: '14',
|
||||
content: 'this is the second chat',
|
||||
created_at: '2022-09-09T16:04:26.186Z',
|
||||
emoji_reactions: null,
|
||||
expiration: 1209600,
|
||||
id: '2',
|
||||
unread: true,
|
||||
pending: false,
|
||||
}),
|
||||
];
|
||||
|
||||
// Mock scrollIntoView function.
|
||||
window.HTMLElement.prototype.scrollIntoView = function () { };
|
||||
Object.assign(navigator, {
|
||||
clipboard: {
|
||||
writeText: () => { },
|
||||
},
|
||||
});
|
||||
|
||||
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 }}>
|
||||
<ChatContext.Provider value={{ chat }}>
|
||||
<ChatMessageList chat={chat} />
|
||||
</ChatContext.Provider>
|
||||
</VirtuosoMockContext.Provider>,
|
||||
undefined,
|
||||
store,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
describe('<ChatMessageList />', () => {
|
||||
describe('when the query is loading', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/pleroma/chats/${chat.id}/messages`).reply(200, chatMessages, {
|
||||
link: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('displays the skeleton loader', async () => {
|
||||
renderComponentWithChatContext();
|
||||
|
||||
expect(screen.queryAllByTestId('placeholder-chat-message')).toHaveLength(5);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('chat-message-list-intro')).toBeInTheDocument();
|
||||
expect(screen.queryAllByTestId('placeholder-chat-message')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the query is finished loading', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/pleroma/chats/${chat.id}/messages`).reply(200, chatMessages, {
|
||||
link: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('displays the intro', async () => {
|
||||
renderComponentWithChatContext();
|
||||
|
||||
expect(screen.queryAllByTestId('chat-message-list-intro')).toHaveLength(0);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('chat-message-list-intro')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays the messages', async () => {
|
||||
renderComponentWithChatContext();
|
||||
|
||||
expect(screen.queryAllByTestId('chat-message')).toHaveLength(0);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('chat-message')).toHaveLength(chatMessages.length);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays the correct menu options depending on the owner of the message', async () => {
|
||||
renderComponentWithChatContext();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('chat-message-menu')).toHaveLength(2);
|
||||
});
|
||||
|
||||
// my message
|
||||
await userEvent.click(screen.queryAllByTestId('chat-message-menu')[0].querySelector('button') as any);
|
||||
|
||||
// other user message
|
||||
await userEvent.click(screen.queryAllByTestId('chat-message-menu')[1].querySelector('button') as any);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,65 +0,0 @@
|
|||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { ChatContext } from 'soapbox/contexts/chat-context.tsx';
|
||||
import { StatProvider } from 'soapbox/contexts/stat-context.tsx';
|
||||
import chats from 'soapbox/jest/fixtures/chats.json';
|
||||
import { render, screen, waitFor } from 'soapbox/jest/test-helpers.tsx';
|
||||
|
||||
import ChatPane from './chat-pane.tsx';
|
||||
|
||||
const renderComponentWithChatContext = (store = {}) => render(
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 300, itemHeight: 100 }}>
|
||||
<StatProvider>
|
||||
<ChatContext.Provider value={{ isOpen: true }}>
|
||||
<ChatPane />
|
||||
</ChatContext.Provider>
|
||||
</StatProvider>
|
||||
</VirtuosoMockContext.Provider>,
|
||||
undefined,
|
||||
store,
|
||||
);
|
||||
|
||||
describe('<ChatPane />', () => {
|
||||
// describe('when there are no chats', () => {
|
||||
// let store: ReturnType<typeof mockStore>;
|
||||
|
||||
// beforeEach(() => {
|
||||
// const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.2.0)');
|
||||
// store = mockStore(state);
|
||||
|
||||
// __stub((mock) => {
|
||||
// mock.onGet('/api/v1/pleroma/chats').reply(200, [], {
|
||||
// link: null,
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
// it('renders the blankslate', async () => {
|
||||
// renderComponentWithChatContext(store);
|
||||
|
||||
// await waitFor(() => {
|
||||
// expect(screen.getByTestId('chat-pane-blankslate')).toBeInTheDocument();
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('when the software is not Truth Social', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/pleroma/chats').reply(200, chats, {
|
||||
link: '<https://example.com/api/v1/pleroma/chats?since_id=2>; rel=\'prev\'',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render the search input', async () => {
|
||||
renderComponentWithChatContext();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('chat-search-input')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,69 +0,0 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { ChatProvider } from 'soapbox/contexts/chat-context.tsx';
|
||||
import { render, screen, waitFor } from 'soapbox/jest/test-helpers.tsx';
|
||||
|
||||
import ChatSearch from './chat-search.tsx';
|
||||
|
||||
const renderComponent = () => render(
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 300, itemHeight: 100 }}>
|
||||
<ChatProvider>
|
||||
<ChatSearch />
|
||||
</ChatProvider>, {/* eslint-disable-line formatjs/no-literal-string-in-jsx */}
|
||||
</VirtuosoMockContext.Provider>,
|
||||
);
|
||||
|
||||
describe('<ChatSearch />', () => {
|
||||
beforeEach(async() => {
|
||||
renderComponent();
|
||||
});
|
||||
|
||||
it('renders the search input', () => {
|
||||
expect(screen.getByTestId('search')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when searching', () => {
|
||||
describe('with results', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/accounts/search').reply(200, [{
|
||||
id: '1',
|
||||
avatar: 'url',
|
||||
verified: false,
|
||||
display_name: 'steve',
|
||||
acct: 'sjobs',
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders accounts', async() => {
|
||||
const user = userEvent.setup();
|
||||
await user.type(screen.getByTestId('search'), 'ste');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('account')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without results', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/accounts/search').reply(200, []);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders accounts', async() => {
|
||||
const user = userEvent.setup();
|
||||
await user.type(screen.getByTestId('search'), 'ste');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('no-results')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,11 +1,11 @@
|
|||
import searchIcon from '@tabler/icons/outline/search.svg';
|
||||
import xIcon from '@tabler/icons/outline/x.svg';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||
import Icon from 'soapbox/components/ui/icon.tsx';
|
||||
import Input from 'soapbox/components/ui/input.tsx';
|
||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||
|
@ -49,9 +49,10 @@ const ChatSearch = (props: IChatSearch) => {
|
|||
|
||||
const handleClickOnSearchResult = useMutation({
|
||||
mutationFn: (accountId: string) => getOrCreateChatByAccountId(accountId),
|
||||
onError: (error: AxiosError) => {
|
||||
const data = error.response?.data as any;
|
||||
toast.error(data?.error);
|
||||
onError: (error) => {
|
||||
if (error instanceof HTTPError) {
|
||||
toast.showAlertForError(error);
|
||||
}
|
||||
},
|
||||
onSuccess: async (response) => {
|
||||
const data = await response.json();
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { AxiosError } from 'axios';
|
||||
import clsx from 'clsx';
|
||||
import { MutableRefObject, useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { uploadMedia } from 'soapbox/actions/media.ts';
|
||||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||
|
@ -71,10 +71,12 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
|
|||
onSuccess: () => {
|
||||
setErrorMessage(undefined);
|
||||
},
|
||||
onError: (error: AxiosError<{ error: string }>, _variables, context) => {
|
||||
const message = error.response?.data?.error;
|
||||
setErrorMessage(message || intl.formatMessage(messages.failedToSend));
|
||||
setContent(context.prevContent as string);
|
||||
onError: async (error: unknown, _variables, context) => {
|
||||
if (error instanceof HTTPError) {
|
||||
const data = await error.response.error();
|
||||
setErrorMessage(data?.error || intl.formatMessage(messages.failedToSend));
|
||||
setContent(context.prevContent as string);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -158,7 +160,8 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
|
|||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
const response = await dispatch(uploadMedia(data, onUploadProgress));
|
||||
return normalizeAttachment(response.data);
|
||||
const json = await response.json();
|
||||
return normalizeAttachment(json);
|
||||
});
|
||||
|
||||
return Promise.all(promises)
|
||||
|
|
|
@ -60,7 +60,7 @@ const SettingsStore: React.FC = () => {
|
|||
pleroma_settings_store: {
|
||||
[FE_NAME]: settings,
|
||||
},
|
||||
})).then(response => {
|
||||
})).then(() => {
|
||||
dispatch({ type: SETTINGS_UPDATE, settings });
|
||||
setLoading(false);
|
||||
}).catch(error => {
|
||||
|
|
|
@ -37,8 +37,8 @@ const EmailConfirmation = () => {
|
|||
.catch((error) => {
|
||||
setStatus(Statuses.FAIL);
|
||||
|
||||
if (error.response.data.error) {
|
||||
const message = buildErrorMessage(error.response.data.error);
|
||||
if (error.data.error) {
|
||||
const message = buildErrorMessage(error.data.error);
|
||||
toast.error(message);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -135,7 +135,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
|||
};
|
||||
|
||||
const handleExportClick = () => {
|
||||
dispatch(fetchEventIcs(status.id)).then(({ data }) => {
|
||||
dispatch(fetchEventIcs(status.id)).then((response) => response.json()).then((data) => {
|
||||
download(data, 'calendar.ics');
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
|
|
@ -51,7 +51,7 @@ const EventDiscussion: React.FC<IEventDiscussion> = (props) => {
|
|||
});
|
||||
|
||||
const [isLoaded, setIsLoaded] = useState<boolean>(!!status);
|
||||
const [next, setNext] = useState<string>();
|
||||
const [next, setNext] = useState<string | null>(null);
|
||||
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
const scroller = useRef<VirtuosoHandle>(null);
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
|||
import { useIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
|
||||
import { externalLogin, loginWithCode } from 'soapbox/actions/external-auth.ts';
|
||||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||
import Button from 'soapbox/components/ui/button.tsx';
|
||||
import FormActions from 'soapbox/components/ui/form-actions.tsx';
|
||||
import FormGroup from 'soapbox/components/ui/form-group.tsx';
|
||||
|
@ -11,8 +12,6 @@ import Spinner from 'soapbox/components/ui/spinner.tsx';
|
|||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||
import toast from 'soapbox/toast.tsx';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
instanceLabel: { id: 'login.fields.instance_label', defaultMessage: 'Instance' },
|
||||
instancePlaceholder: { id: 'login.fields.instance_placeholder', defaultMessage: 'example.com' },
|
||||
|
@ -41,13 +40,14 @@ const ExternalLoginForm: React.FC = () => {
|
|||
|
||||
dispatch(externalLogin(host))
|
||||
.then(() => setLoading(false))
|
||||
.catch((error: AxiosError) => {
|
||||
.catch((error: unknown) => {
|
||||
console.error(error);
|
||||
const status = error.response?.status;
|
||||
|
||||
const status = error instanceof HTTPError ? error.response.status : undefined;
|
||||
|
||||
if (status) {
|
||||
toast.error(intl.formatMessage(messages.instanceFailed));
|
||||
} else if (!status && error.code === 'ERR_NETWORK') {
|
||||
} else if (error && typeof error === 'object' && 'code' in error && error.code === 'ERR_NETWORK') {
|
||||
toast.error(intl.formatMessage(messages.networkFailed));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,320 +0,0 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { buildGroup, buildGroupMember, buildGroupRelationship } from 'soapbox/jest/factory.ts';
|
||||
import { render, screen, waitFor } from 'soapbox/jest/test-helpers.tsx';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member.ts';
|
||||
|
||||
import GroupMemberListItem from './group-member-list-item.tsx';
|
||||
|
||||
describe('<GroupMemberListItem />', () => {
|
||||
describe('account rendering', () => {
|
||||
const accountId = '4';
|
||||
const groupMember = buildGroupMember({}, {
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the users avatar', async () => {
|
||||
const group = buildGroup({
|
||||
relationship: buildGroupRelationship(),
|
||||
});
|
||||
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('group-member-list-item')).toHaveTextContent(groupMember.account.display_name);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('role badge', () => {
|
||||
const accountId = '4';
|
||||
const group = buildGroup();
|
||||
|
||||
describe('when the user is an Owner', () => {
|
||||
const groupMember = buildGroupMember({ role: GroupRoles.OWNER }, {
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the correct badge', async () => {
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('role-badge')).toHaveTextContent('owner');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user is an Admin', () => {
|
||||
const groupMember = buildGroupMember({ role: GroupRoles.ADMIN }, {
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the correct badge', async () => {
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('role-badge')).toHaveTextContent('admin');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user is an User', () => {
|
||||
const groupMember = buildGroupMember({ role: GroupRoles.USER }, {
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render no correct badge', async () => {
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('role-badge')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as a Group owner', () => {
|
||||
const group = buildGroup({
|
||||
relationship: buildGroupRelationship({
|
||||
role: GroupRoles.OWNER,
|
||||
member: true,
|
||||
}),
|
||||
});
|
||||
|
||||
describe('when the user has role of "user"', () => {
|
||||
const accountId = '4';
|
||||
const groupMember = buildGroupMember({}, {
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
username: 'tiger',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when "canPromoteToAdmin is true', () => {
|
||||
it('should render dropdown with correct Owner actions', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(async() => {
|
||||
await user.click(screen.getByTestId('icon-button'));
|
||||
});
|
||||
|
||||
const dropdownMenu = screen.getByTestId('dropdown-menu');
|
||||
expect(dropdownMenu).toHaveTextContent('Assign admin role');
|
||||
expect(dropdownMenu).toHaveTextContent('Kick @tiger from group');
|
||||
expect(dropdownMenu).toHaveTextContent('Ban from group');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when "canPromoteToAdmin is false', () => {
|
||||
it('should prevent promoting user to Admin', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin={false} />);
|
||||
|
||||
await waitFor(async() => {
|
||||
await user.click(screen.getByTestId('icon-button'));
|
||||
await user.click(screen.getByTitle('Assign admin role'));
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('toast')).toHaveTextContent('Admin limit reached');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user has role of "admin"', () => {
|
||||
const accountId = '4';
|
||||
const groupMember = buildGroupMember(
|
||||
{
|
||||
role: GroupRoles.ADMIN,
|
||||
},
|
||||
{
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
username: 'tiger',
|
||||
},
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render dropdown with correct Owner actions', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(async() => {
|
||||
await user.click(screen.getByTestId('icon-button'));
|
||||
});
|
||||
|
||||
const dropdownMenu = screen.getByTestId('dropdown-menu');
|
||||
expect(dropdownMenu).toHaveTextContent('Remove admin role');
|
||||
expect(dropdownMenu).toHaveTextContent('Kick @tiger from group');
|
||||
expect(dropdownMenu).toHaveTextContent('Ban from group');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as a Group admin', () => {
|
||||
const group = buildGroup({
|
||||
relationship: buildGroupRelationship({
|
||||
role: GroupRoles.ADMIN,
|
||||
member: true,
|
||||
}),
|
||||
});
|
||||
|
||||
describe('when the user has role of "user"', () => {
|
||||
const accountId = '4';
|
||||
const groupMember = buildGroupMember({}, {
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
username: 'tiger',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render dropdown with correct Admin actions', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(async() => {
|
||||
await user.click(screen.getByTestId('icon-button'));
|
||||
});
|
||||
|
||||
const dropdownMenu = screen.getByTestId('dropdown-menu');
|
||||
expect(dropdownMenu).not.toHaveTextContent('Assign admin role');
|
||||
expect(dropdownMenu).toHaveTextContent('Kick @tiger from group');
|
||||
expect(dropdownMenu).toHaveTextContent('Ban from group');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user has role of "admin"', () => {
|
||||
const accountId = '4';
|
||||
const groupMember = buildGroupMember(
|
||||
{
|
||||
role: GroupRoles.ADMIN,
|
||||
},
|
||||
{
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
username: 'tiger',
|
||||
},
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render the dropdown', async () => {
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(async() => {
|
||||
expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user has role of "owner"', () => {
|
||||
const accountId = '4';
|
||||
const groupMember = buildGroupMember(
|
||||
{
|
||||
role: GroupRoles.OWNER,
|
||||
},
|
||||
{
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
username: 'tiger',
|
||||
},
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render the dropdown', async () => {
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(async() => {
|
||||
expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('as a Group user', () => {
|
||||
const group = buildGroup({
|
||||
relationship: buildGroupRelationship({
|
||||
role: GroupRoles.USER,
|
||||
member: true,
|
||||
}),
|
||||
});
|
||||
const accountId = '4';
|
||||
const groupMember = buildGroupMember({}, {
|
||||
id: accountId,
|
||||
display_name: 'tiger woods',
|
||||
username: 'tiger',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render the dropdown', async () => {
|
||||
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
|
||||
|
||||
await waitFor(async() => {
|
||||
expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
import { AxiosError } from 'axios';
|
||||
import { useEffect } from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||
import { useGroup, useGroupMembers, useGroupMembershipRequests } from 'soapbox/api/hooks/index.ts';
|
||||
import Account from 'soapbox/components/account.tsx';
|
||||
import { AuthorizeRejectButtons } from 'soapbox/components/authorize-reject-buttons.tsx';
|
||||
|
@ -85,14 +85,17 @@ const GroupMembershipRequests: React.FC<IGroupMembershipRequests> = ({ params })
|
|||
async function handleAuthorize(account: AccountEntity) {
|
||||
return authorize(account.id)
|
||||
.then(() => Promise.resolve())
|
||||
.catch((error: AxiosError) => {
|
||||
.catch(async (error: unknown) => {
|
||||
refetch();
|
||||
|
||||
let message = intl.formatMessage(messages.authorizeFail, { name: account.username });
|
||||
if (error.response?.status === 409) {
|
||||
message = (error.response?.data as any).error;
|
||||
const message = intl.formatMessage(messages.authorizeFail, { name: account.username });
|
||||
|
||||
if (error instanceof HTTPError && error.response.status === 409) {
|
||||
const data = await error.response.error();
|
||||
toast.error(data?.error || message);
|
||||
} else {
|
||||
toast.error(message);
|
||||
}
|
||||
toast.error(message);
|
||||
|
||||
return Promise.reject();
|
||||
});
|
||||
|
@ -101,14 +104,17 @@ const GroupMembershipRequests: React.FC<IGroupMembershipRequests> = ({ params })
|
|||
async function handleReject(account: AccountEntity) {
|
||||
return reject(account.id)
|
||||
.then(() => Promise.resolve())
|
||||
.catch((error: AxiosError) => {
|
||||
.catch(async (error: unknown) => {
|
||||
refetch();
|
||||
|
||||
let message = intl.formatMessage(messages.rejectFail, { name: account.username });
|
||||
if (error.response?.status === 409) {
|
||||
message = (error.response?.data as any).error;
|
||||
const message = intl.formatMessage(messages.rejectFail, { name: account.username });
|
||||
|
||||
if (error instanceof HTTPError && error.response.status === 409) {
|
||||
const data = await error.response.error();
|
||||
toast.error(data?.error || message);
|
||||
} else {
|
||||
toast.error(message);
|
||||
}
|
||||
toast.error(message);
|
||||
|
||||
return Promise.reject();
|
||||
});
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { buildGroup } from 'soapbox/jest/factory.ts';
|
||||
import { render, screen, waitFor } from 'soapbox/jest/test-helpers.tsx';
|
||||
import { instanceV1Schema } from 'soapbox/schemas/instance.ts';
|
||||
|
||||
import Search from './search.tsx';
|
||||
|
||||
const store = {
|
||||
instance: instanceV1Schema.parse({
|
||||
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
|
||||
}),
|
||||
};
|
||||
|
||||
const renderApp = (children: React.ReactElement) => render(children, undefined, store);
|
||||
|
||||
describe('<Search />', () => {
|
||||
describe('with no results', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/groups/search').reply(200, []);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the blankslate', async () => {
|
||||
renderApp(<Search searchValue={'some-search'} onSelect={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('no-results')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with results', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/groups/search').reply(200, [
|
||||
buildGroup({
|
||||
display_name: 'Group',
|
||||
id: '1',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the results', async () => {
|
||||
renderApp(<Search searchValue={'some-search'} onSelect={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('results')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('before starting a search', () => {
|
||||
it('should render the RecentSearches component', () => {
|
||||
renderApp(<Search searchValue={''} onSelect={vi.fn()} />);
|
||||
|
||||
expect(screen.getByTestId('recent-searches')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -80,7 +80,7 @@ interface IThread {
|
|||
withMedia?: boolean;
|
||||
useWindowScroll?: boolean;
|
||||
itemClassName?: string;
|
||||
next: string | undefined;
|
||||
next?: string | null;
|
||||
handleLoadMore: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ const StatusDetails: React.FC<IStatusDetails> = (props) => {
|
|||
const status = useAppSelector((state) => getStatus(state, { id: props.params.statusId }));
|
||||
|
||||
const [isLoaded, setIsLoaded] = useState<boolean>(!!status);
|
||||
const [next, setNext] = useState<string>();
|
||||
const [next, setNext] = useState<string | null>(null);
|
||||
|
||||
/** Fetch the status (and context) from the API. */
|
||||
const fetchData = async () => {
|
||||
|
|
|
@ -36,7 +36,7 @@ const TestTimeline: React.FC = () => {
|
|||
|
||||
useEffect(() => {
|
||||
dispatch(importFetchedStatuses(MOCK_STATUSES));
|
||||
dispatch(expandTimelineSuccess(timelineId, MOCK_STATUSES, undefined, undefined, false, false, false));
|
||||
dispatch(expandTimelineSuccess(timelineId, MOCK_STATUSES, null, null, false, false, false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
|
|
@ -76,7 +76,7 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
|
|||
const actualStatus = status ? getActualStatus(status) : undefined;
|
||||
|
||||
const [isLoaded, setIsLoaded] = useState<boolean>(!!status);
|
||||
const [next, setNext] = useState<string>();
|
||||
const [next, setNext] = useState<string | null>(null);
|
||||
const [index, setIndex] = useState<number | null>(null);
|
||||
const [navigationHidden, setNavigationHidden] = useState(false);
|
||||
const [isFullScreen, setIsFullScreen] = useState(!status);
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useRef, useState } from 'react';
|
|||
import { FormattedMessage, defineMessages } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me.ts';
|
||||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||
import Avatar from 'soapbox/components/ui/avatar.tsx';
|
||||
import Button from 'soapbox/components/ui/button.tsx';
|
||||
import IconButton from 'soapbox/components/ui/icon-button.tsx';
|
||||
|
@ -18,8 +19,6 @@ import toast from 'soapbox/toast.tsx';
|
|||
import { isDefaultAvatar } from 'soapbox/utils/accounts.ts';
|
||||
import resizeImage from 'soapbox/utils/resize-image.ts';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const closeIcon = xIcon;
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -64,13 +63,13 @@ const AvatarSelectionModal: React.FC<IAvatarSelectionModal> = ({ onClose, onNext
|
|||
setDisabled(false);
|
||||
setSubmitting(false);
|
||||
onNext();
|
||||
}).catch((error: AxiosError) => {
|
||||
}).catch((error) => {
|
||||
setSubmitting(false);
|
||||
setDisabled(false);
|
||||
setSelectedFile(null);
|
||||
|
||||
if (error.response?.status === 422) {
|
||||
toast.error((error.response.data as any).error.replace('Validation failed: ', ''));
|
||||
if (error instanceof HTTPError && error.response.status === 422) {
|
||||
toast.showAlertForError(error);
|
||||
} else {
|
||||
toast.error(messages.error);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useState } from 'react';
|
|||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me.ts';
|
||||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||
import Button from 'soapbox/components/ui/button.tsx';
|
||||
import FormGroup from 'soapbox/components/ui/form-group.tsx';
|
||||
import IconButton from 'soapbox/components/ui/icon-button.tsx';
|
||||
|
@ -13,8 +14,6 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
|||
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||
import toast from 'soapbox/toast.tsx';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
bioPlaceholder: { id: 'onboarding.bio.placeholder', defaultMessage: 'Tell the world a little about yourself…' },
|
||||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
|
@ -45,11 +44,14 @@ const BioStep: React.FC<IBioStep> = ({ onClose, onNext }) => {
|
|||
.then(() => {
|
||||
setSubmitting(false);
|
||||
onNext();
|
||||
}).catch((error: AxiosError) => {
|
||||
}).catch(async (error) => {
|
||||
setSubmitting(false);
|
||||
|
||||
if (error.response?.status === 422) {
|
||||
setErrors([(error.response.data as any).error.replace('Validation failed: ', '')]);
|
||||
if (error instanceof HTTPError && error.response.status === 422) {
|
||||
const data = await error.response.error();
|
||||
if (data) {
|
||||
setErrors([data.error]);
|
||||
}
|
||||
} else {
|
||||
toast.error(messages.error);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useRef, useState } from 'react';
|
|||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me.ts';
|
||||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||
import StillImage from 'soapbox/components/still-image.tsx';
|
||||
import Avatar from 'soapbox/components/ui/avatar.tsx';
|
||||
import Button from 'soapbox/components/ui/button.tsx';
|
||||
|
@ -19,8 +20,6 @@ import toast from 'soapbox/toast.tsx';
|
|||
import { isDefaultHeader } from 'soapbox/utils/accounts.ts';
|
||||
import resizeImage from 'soapbox/utils/resize-image.ts';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const closeIcon = xIcon;
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -68,13 +67,18 @@ const CoverPhotoSelectionModal: React.FC<ICoverPhotoSelectionModal> = ({ onClose
|
|||
setDisabled(false);
|
||||
setSubmitting(false);
|
||||
onNext();
|
||||
}).catch((error: AxiosError) => {
|
||||
}).catch(async (error) => {
|
||||
setSubmitting(false);
|
||||
setDisabled(false);
|
||||
setSelectedFile(null);
|
||||
|
||||
if (error.response?.status === 422) {
|
||||
toast.error((error.response.data as any).error.replace('Validation failed: ', ''));
|
||||
if (error instanceof HTTPError && error.response?.status === 422) {
|
||||
const data = await error.response.error();
|
||||
if (data) {
|
||||
toast.error(data.error);
|
||||
} else {
|
||||
toast.error(messages.error);
|
||||
}
|
||||
} else {
|
||||
toast.error(messages.error);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useMemo, useState } from 'react';
|
|||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me.ts';
|
||||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||
import Button from 'soapbox/components/ui/button.tsx';
|
||||
import FormGroup from 'soapbox/components/ui/form-group.tsx';
|
||||
import IconButton from 'soapbox/components/ui/icon-button.tsx';
|
||||
|
@ -13,8 +14,6 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
|||
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||
import toast from 'soapbox/toast.tsx';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const closeIcon = xIcon;
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -56,11 +55,14 @@ const DisplayNameStep: React.FC<IDisplayNameStep> = ({ onClose, onNext }) => {
|
|||
.then(() => {
|
||||
setSubmitting(false);
|
||||
onNext();
|
||||
}).catch((error: AxiosError) => {
|
||||
}).catch(async (error) => {
|
||||
setSubmitting(false);
|
||||
|
||||
if (error.response?.status === 422) {
|
||||
setErrors([(error.response.data as any).error.replace('Validation failed: ', '')]);
|
||||
if (error instanceof HTTPError && error.response?.status === 422) {
|
||||
const data = await error.response.error();
|
||||
if (data) {
|
||||
setErrors([data.error]);
|
||||
}
|
||||
} else {
|
||||
toast.error(messages.error);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useRef, useState } from 'react';
|
|||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { Link, Redirect } from 'react-router-dom';
|
||||
|
||||
import { logIn, verifyCredentials } from 'soapbox/actions/auth.ts';
|
||||
import { logIn, MfaRequiredError, verifyCredentials } from 'soapbox/actions/auth.ts';
|
||||
import { fetchInstance } from 'soapbox/actions/instance.ts';
|
||||
import { openModal } from 'soapbox/actions/modals.ts';
|
||||
import { openSidebar } from 'soapbox/actions/sidebar.ts';
|
||||
|
@ -28,8 +28,6 @@ import { useSettingsNotifications } from 'soapbox/hooks/useSettingsNotifications
|
|||
|
||||
import ProfileDropdown from './profile-dropdown.tsx';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
login: { id: 'navbar.login.action', defaultMessage: 'Log in' },
|
||||
username: { id: 'navbar.login.username.placeholder', defaultMessage: 'Email or username' },
|
||||
|
@ -52,7 +50,7 @@ const Navbar = () => {
|
|||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const [username, setUsername] = useState<string>('');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
const [mfaToken, setMfaToken] = useState<boolean>(false);
|
||||
const [mfaToken, setMfaToken] = useState<string>();
|
||||
|
||||
const onOpenSidebar = () => dispatch(openSidebar());
|
||||
|
||||
|
@ -74,17 +72,18 @@ const Navbar = () => {
|
|||
.then(() => dispatch(fetchInstance()))
|
||||
);
|
||||
})
|
||||
.catch((error: AxiosError) => {
|
||||
.catch((error: unknown) => {
|
||||
setLoading(false);
|
||||
|
||||
const data: any = error.response?.data;
|
||||
if (data?.error === 'mfa_required') {
|
||||
setMfaToken(data.mfa_token);
|
||||
if (error instanceof MfaRequiredError) {
|
||||
setMfaToken(error.token);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (mfaToken) return <Redirect to={`/login?token=${encodeURIComponent(mfaToken)}`} />;
|
||||
if (mfaToken) {
|
||||
return <Redirect to={`/login?token=${encodeURIComponent(mfaToken)}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { __stub } from 'soapbox/api/index.ts';
|
||||
import { queryClient, render, screen, waitFor } from 'soapbox/jest/test-helpers.tsx';
|
||||
|
||||
import TrendsPanel from './trends-panel.tsx';
|
||||
|
||||
describe('<TrendsPanel />', () => {
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
describe('with hashtags', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/trends')
|
||||
.reply(200, [
|
||||
{
|
||||
name: 'hashtag 1',
|
||||
url: 'https://example.com',
|
||||
history: [{
|
||||
day: '1652745600',
|
||||
uses: '294',
|
||||
accounts: '180',
|
||||
}],
|
||||
},
|
||||
{ name: 'hashtag 2', url: 'https://example.com' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders trending hashtags', async() => {
|
||||
render(<TrendsPanel limit={1} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('hashtag')).toHaveTextContent(/hashtag 1/i);
|
||||
expect(screen.getByTestId('hashtag')).toHaveTextContent(/180 people talking/i);
|
||||
expect(screen.getByTestId('sparklines')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders multiple trends', async() => {
|
||||
render(<TrendsPanel limit={3} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('hashtag')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('respects the limit prop', async() => {
|
||||
render(<TrendsPanel limit={1} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('hashtag')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without hashtags', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/trends').reply(200, []);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders empty', async() => {
|
||||
render(<TrendsPanel limit={1} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('hashtag')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -75,7 +75,8 @@ const SoapboxLoad: React.FC<ISoapboxLoad> = ({ children }) => {
|
|||
if (!instance.isLoading && !nostrLoading) {
|
||||
dispatch(loadInitial()).then(() => {
|
||||
setIsLoaded(true);
|
||||
}).catch(() => {
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
setIsLoaded(true);
|
||||
});
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue