Replace axios with fetch

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2024-05-11 23:37:37 +02:00
parent 2f57d0a5bd
commit f3165877f2
144 changed files with 1146 additions and 2754 deletions

View File

@@ -2,43 +2,41 @@ 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));
};
// const setupMock = (axios: AxiosInstance) => {
// const mock = new MockAdapter(axios, { onNoMatch: 'throwException' });
// mocks.map(func => func(mock));
// };
export const staticClient = api.staticClient;
export const getLinks = (response: AxiosResponse): LinkHeader => {
return new LinkHeader(response.headers?.link);
export const getLinks = (response: Response): LinkHeader => {
return new LinkHeader(response.headers?.get('link') || undefined);
};
export const getNextLink = (response: AxiosResponse) => {
const nextLink = new LinkHeader(response.headers?.link);
export const getNextLink = (response: Response) => {
const nextLink = new LinkHeader(response.headers?.get('link') || undefined);
return nextLink.refs.find(link => link.rel === 'next')?.uri;
};
export const getPrevLink = (response: AxiosResponse) => {
const prevLink = new LinkHeader(response.headers?.link);
export const getPrevLink = (response: Response) => {
const prevLink = new LinkHeader(response.headers?.get('link') || undefined);
return prevLink.refs.find(link => link.rel === 'prev')?.uri;
};
export const baseClient = (...params: any[]) => {
const axios = api.baseClient(...params);
setupMock(axios);
// setupMock(axios);
return axios;
};
export default (...params: any[]) => {
const axios = api.default(...params);
setupMock(axios);
// setupMock(axios);
return axios;
};

View File

@@ -22,7 +22,7 @@ function useAccount(accountId?: string, opts: UseAccountOpts = {}) {
const { entity, isUnauthorized, ...result } = useEntity<Account>(
[Entities.ACCOUNTS, accountId!],
() => api.get(`/api/v1/accounts/${accountId}`),
() => api(`/api/v1/accounts/${accountId}`),
{ schema: accountSchema, enabled: !!accountId },
);

View File

@@ -33,12 +33,12 @@ function useAccountList(listKey: string[], entityFn: EntityFn<void>, opts: useAc
function useBlocks() {
const api = useApi();
return useAccountList(['blocks'], () => api.get('/api/v1/blocks'));
return useAccountList(['blocks'], () => api('/api/v1/blocks'));
}
function useMutes() {
const api = useApi();
return useAccountList(['mutes'], () => api.get('/api/v1/mutes'));
return useAccountList(['mutes'], () => api('/api/v1/mutes'));
}
function useFollowing(accountId: string | undefined) {
@@ -46,7 +46,7 @@ function useFollowing(accountId: string | undefined) {
return useAccountList(
[accountId!, 'following'],
() => api.get(`/api/v1/accounts/${accountId}/following`),
() => api(`/api/v1/accounts/${accountId}/following`),
{ enabled: !!accountId },
);
}
@@ -56,7 +56,7 @@ function useFollowers(accountId: string | undefined) {
return useAccountList(
[accountId!, 'followers'],
() => api.get(`/api/v1/accounts/${accountId}/followers`),
() => api(`/api/v1/accounts/${accountId}/followers`),
{ enabled: !!accountId },
);
}

View File

@@ -23,7 +23,7 @@ function useAccountLookup(acct: string | undefined, opts: UseAccountLookupOpts =
const { entity: account, isUnauthorized, ...result } = useEntityLookup<Account>(
Entities.ACCOUNTS,
(account) => account.acct.toLowerCase() === acct?.toLowerCase(),
() => api.get(`/api/v1/accounts/lookup?acct=${acct}`),
() => api(`/api/v1/accounts/lookup?acct=${acct}`),
{ schema: accountSchema, enabled: !!acct },
);

View File

@@ -56,8 +56,11 @@ function useFollow() {
followEffect(accountId);
try {
const response = await api.post(`/api/v1/accounts/${accountId}/follow`, options);
const result = relationshipSchema.safeParse(response.data);
const response = await api(`/api/v1/accounts/${accountId}/follow`, {
method: 'POST',
body: JSON.stringify(options),
});
const result = relationshipSchema.safeParse(response.json);
if (result.success) {
dispatch(importEntities([result.data], Entities.RELATIONSHIPS));
}
@@ -71,7 +74,7 @@ function useFollow() {
unfollowEffect(accountId);
try {
await api.post(`/api/v1/accounts/${accountId}/unfollow`);
await api(`/api/v1/accounts/${accountId}/unfollow`, { method: 'POST' });
} catch (e) {
followEffect(accountId);
}

View File

@@ -8,7 +8,7 @@ function usePatronUser(url?: string) {
const { entity: patronUser, ...result } = useEntity<PatronUser>(
[Entities.PATRON_USERS, url || ''],
() => api.get(`/api/patron/v1/accounts/${encodeURIComponent(url!)}`),
() => api(`/api/patron/v1/accounts/${encodeURIComponent(url!)}`),
{ schema: patronUserSchema, enabled: !!url },
);

View File

@@ -15,7 +15,7 @@ function useRelationship(accountId: string | undefined, opts: UseRelationshipOpt
const { entity: relationship, ...result } = useEntity<Relationship>(
[Entities.RELATIONSHIPS, accountId!],
() => api.get(`/api/v1/accounts/relationships?id[]=${accountId}`),
() => api(`/api/v1/accounts/relationships?id[]=${accountId}`),
{
enabled: enabled && !!accountId,
schema: z.array(relationshipSchema).nonempty().transform(arr => arr[0]),

View File

@@ -9,8 +9,7 @@ function useRelationships(listKey: string[], ids: string[]) {
const { isLoggedIn } = useLoggedIn();
function fetchRelationships(ids: string[]) {
const q = ids.map((id) => `id[]=${id}`).join('&');
return api.get(`/api/v1/accounts/relationships?${q}`);
return api('/api/v1/accounts/relationships', { params: { ids } });
}
const { entityMap: relationships, ...result } = useBatchedEntities<Relationship>(

View File

@@ -6,8 +6,6 @@ import { adminAnnouncementSchema, type AdminAnnouncement } from 'soapbox/schemas
import { useAnnouncements as useUserAnnouncements } from '../announcements';
import type { AxiosResponse } from 'axios';
interface CreateAnnouncementParams {
content: string;
starts_at?: string | null;
@@ -24,7 +22,7 @@ const useAnnouncements = () => {
const userAnnouncements = useUserAnnouncements();
const getAnnouncements = async () => {
const { data } = await api.get<AdminAnnouncement[]>('/api/v1/pleroma/admin/announcements');
const { json: data } = await api<AdminAnnouncement[]>('/api/v1/pleroma/admin/announcements');
const normalizedData = data.map((announcement) => adminAnnouncementSchema.parse(announcement));
return normalizedData;
@@ -40,9 +38,12 @@ const useAnnouncements = () => {
mutate: createAnnouncement,
isPending: isCreating,
} = useMutation({
mutationFn: (params: CreateAnnouncementParams) => api.post('/api/v1/pleroma/admin/announcements', params),
mutationFn: (params: CreateAnnouncementParams) => api('/api/v1/pleroma/admin/announcements', {
method: 'POST',
body: JSON.stringify(params),
}),
retry: false,
onSuccess: ({ data }: AxiosResponse) =>
onSuccess: ({ json: data }) =>
queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
[...prevResult, adminAnnouncementSchema.parse(data)],
),
@@ -53,9 +54,12 @@ const useAnnouncements = () => {
mutate: updateAnnouncement,
isPending: isUpdating,
} = useMutation({
mutationFn: ({ id, ...params }: UpdateAnnouncementParams) => api.patch(`/api/v1/pleroma/admin/announcements/${id}`, params),
mutationFn: ({ id, ...params }: UpdateAnnouncementParams) => api(`/api/v1/pleroma/admin/announcements/${id}`, {
method: 'PATCH',
body: JSON.stringify(params),
}),
retry: false,
onSuccess: ({ data }: AxiosResponse) =>
onSuccess: ({ json: data }) =>
queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
prevResult.map((announcement) => announcement.id === data.id ? adminAnnouncementSchema.parse(data) : announcement),
),
@@ -66,7 +70,7 @@ const useAnnouncements = () => {
mutate: deleteAnnouncement,
isPending: isDeleting,
} = useMutation({
mutationFn: (id: string) => api.delete(`/api/v1/pleroma/admin/announcements/${id}`),
mutationFn: (id: string) => api(`/api/v1/pleroma/admin/announcements/${id}`, { method: 'DELETE' }),
retry: false,
onSuccess: (_, id) =>
queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>

View File

@@ -4,8 +4,6 @@ import { useApi } from 'soapbox/hooks';
import { queryClient } from 'soapbox/queries/client';
import { domainSchema, type Domain } from 'soapbox/schemas';
import type { AxiosResponse } from 'axios';
interface CreateDomainParams {
domain: string;
public: boolean;
@@ -20,7 +18,7 @@ const useDomains = () => {
const api = useApi();
const getDomains = async () => {
const { data } = await api.get<Domain[]>('/api/v1/pleroma/admin/domains');
const { json: data } = await api<Domain[]>('/api/v1/pleroma/admin/domains');
const normalizedData = data.map((domain) => domainSchema.parse(domain));
return normalizedData;
@@ -36,9 +34,12 @@ const useDomains = () => {
mutate: createDomain,
isPending: isCreating,
} = useMutation({
mutationFn: (params: CreateDomainParams) => api.post('/api/v1/pleroma/admin/domains', params),
mutationFn: (params: CreateDomainParams) => api('/api/v1/pleroma/admin/domains', {
method: 'POST',
body: JSON.stringify(params),
}),
retry: false,
onSuccess: ({ data }: AxiosResponse) =>
onSuccess: ({ data }) =>
queryClient.setQueryData(['admin', 'domains'], (prevResult: ReadonlyArray<Domain>) =>
[...prevResult, domainSchema.parse(data)],
),
@@ -48,9 +49,12 @@ const useDomains = () => {
mutate: updateDomain,
isPending: isUpdating,
} = useMutation({
mutationFn: ({ id, ...params }: UpdateDomainParams) => api.patch(`/api/v1/pleroma/admin/domains/${id}`, params),
mutationFn: ({ id, ...params }: UpdateDomainParams) => api(`/api/v1/pleroma/admin/domains/${id}`, {
method: 'PATCH',
body: JSON.stringify(params),
}),
retry: false,
onSuccess: ({ data }: AxiosResponse) =>
onSuccess: ({ json: data }) =>
queryClient.setQueryData(['admin', 'domains'], (prevResult: ReadonlyArray<Domain>) =>
prevResult.map((domain) => domain.id === data.id ? domainSchema.parse(data) : domain),
),
@@ -60,7 +64,7 @@ const useDomains = () => {
mutate: deleteDomain,
isPending: isDeleting,
} = useMutation({
mutationFn: (id: string) => api.delete(`/api/v1/pleroma/admin/domains/${id}`),
mutationFn: (id: string) => api(`/api/v1/pleroma/admin/domains/${id}`, { method: 'DELETE' }),
retry: false,
onSuccess: (_, id) =>
queryClient.setQueryData(['admin', 'domains'], (prevResult: ReadonlyArray<Domain>) =>

View File

@@ -14,7 +14,7 @@ const useModerationLog = () => {
const api = useApi();
const getModerationLog = async (page: number): Promise<ModerationLogResult> => {
const { data } = await api.get<ModerationLogResult>('/api/v1/pleroma/admin/moderation_log', { params: { page } });
const { json: data } = await api<ModerationLogResult>('/api/v1/pleroma/admin/moderation_log', { params: { page } });
const normalizedData = data.items.map((domain) => moderationLogEntrySchema.parse(domain));

View File

@@ -4,13 +4,11 @@ import { useApi } from 'soapbox/hooks';
import { queryClient } from 'soapbox/queries/client';
import { relaySchema, type Relay } from 'soapbox/schemas';
import type { AxiosResponse } from 'axios';
const useRelays = () => {
const api = useApi();
const getRelays = async () => {
const { data } = await api.get<{ relays: Relay[] }>('/api/v1/pleroma/admin/relay');
const { json: data } = await api<{ relays: Relay[] }>('/api/v1/pleroma/admin/relay');
const normalizedData = data.relays?.map((relay) => relaySchema.parse(relay));
return normalizedData;
@@ -26,9 +24,12 @@ const useRelays = () => {
mutate: followRelay,
isPending: isPendingFollow,
} = useMutation({
mutationFn: (relayUrl: string) => api.post('/api/v1/pleroma/admin/relays', { relay_url: relayUrl }),
mutationFn: (relayUrl: string) => api('/api/v1/pleroma/admin/relays', {
method: 'POST',
body: JSON.stringify({ relay_url: relayUrl }),
}),
retry: false,
onSuccess: ({ data }: AxiosResponse) =>
onSuccess: ({ json: data }) =>
queryClient.setQueryData(['admin', 'relays'], (prevResult: ReadonlyArray<Relay>) =>
[...prevResult, relaySchema.parse(data)],
),
@@ -38,8 +39,9 @@ const useRelays = () => {
mutate: unfollowRelay,
isPending: isPendingUnfollow,
} = useMutation({
mutationFn: (relayUrl: string) => api.delete('/api/v1/pleroma/admin/relays', {
data: { relay_url: relayUrl },
mutationFn: (relayUrl: string) => api('/api/v1/pleroma/admin/relays', {
method: 'DELETE',
body: JSON.stringify({ relay_url: relayUrl }),
}),
retry: false,
onSuccess: (_, relayUrl) =>

View File

@@ -4,8 +4,6 @@ import { useApi } from 'soapbox/hooks';
import { queryClient } from 'soapbox/queries/client';
import { adminRuleSchema, type AdminRule } from 'soapbox/schemas';
import type { AxiosResponse } from 'axios';
interface CreateRuleParams {
priority?: number;
text: string;
@@ -23,7 +21,7 @@ const useRules = () => {
const api = useApi();
const getRules = async () => {
const { data } = await api.get<AdminRule[]>('/api/v1/pleroma/admin/rules');
const { json: data } = await api<AdminRule[]>('/api/v1/pleroma/admin/rules');
const normalizedData = data.map((rule) => adminRuleSchema.parse(rule));
return normalizedData;
@@ -39,9 +37,12 @@ const useRules = () => {
mutate: createRule,
isPending: isCreating,
} = useMutation({
mutationFn: (params: CreateRuleParams) => api.post('/api/v1/pleroma/admin/rules', params),
mutationFn: (params: CreateRuleParams) => api('/api/v1/pleroma/admin/rules', {
method: 'POST',
body: JSON.stringify(params),
}),
retry: false,
onSuccess: ({ data }: AxiosResponse) =>
onSuccess: ({ json: data }) =>
queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray<AdminRule>) =>
[...prevResult, adminRuleSchema.parse(data)],
),
@@ -51,9 +52,12 @@ const useRules = () => {
mutate: updateRule,
isPending: isUpdating,
} = useMutation({
mutationFn: ({ id, ...params }: UpdateRuleParams) => api.patch(`/api/v1/pleroma/admin/rules/${id}`, params),
mutationFn: ({ id, ...params }: UpdateRuleParams) => api(`/api/v1/pleroma/admin/rules/${id}`, {
method: 'PATCH',
body: JSON.stringify(params),
}),
retry: false,
onSuccess: ({ data }: AxiosResponse) =>
onSuccess: ({ json: data }) =>
queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray<AdminRule>) =>
prevResult.map((rule) => rule.id === data.id ? adminRuleSchema.parse(data) : rule),
),
@@ -63,7 +67,7 @@ const useRules = () => {
mutate: deleteRule,
isPending: isDeleting,
} = useMutation({
mutationFn: (id: string) => api.delete(`/api/v1/pleroma/admin/rules/${id}`),
mutationFn: (id: string) => api(`/api/v1/pleroma/admin/rules/${id}`, { method: 'DELETE' }),
retry: false,
onSuccess: (_, id) =>
queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray<AdminRule>) =>

View File

@@ -29,7 +29,10 @@ function useSuggest() {
const accts = accountIdsToAccts(getState(), accountIds);
suggestEffect(accountIds, true);
try {
await api.patch('/api/v1/pleroma/admin/users/suggest', { nicknames: accts });
await api('/api/v1/pleroma/admin/users/suggest', {
method: 'PATCH',
body: JSON.stringify({ nicknames: accts }),
});
callbacks?.onSuccess?.();
} catch (e) {
callbacks?.onError?.(e);
@@ -41,7 +44,10 @@ function useSuggest() {
const accts = accountIdsToAccts(getState(), accountIds);
suggestEffect(accountIds, false);
try {
await api.patch('/api/v1/pleroma/admin/users/unsuggest', { nicknames: accts });
await api('/api/v1/pleroma/admin/users/unsuggest', {
method: 'PATCH',
body: JSON.stringify({ nicknames: accts }),
});
callbacks?.onSuccess?.();
} catch (e) {
callbacks?.onError?.(e);

View File

@@ -34,7 +34,10 @@ function useVerify() {
const accts = accountIdsToAccts(getState(), accountIds);
verifyEffect(accountIds, true);
try {
await api.put('/api/v1/pleroma/admin/users/tag', { nicknames: accts, tags: ['verified'] });
await api('/api/v1/pleroma/admin/users/tag', {
method: 'PUT',
body: JSON.stringify({ nicknames: accts, tags: ['verified'] }),
});
callbacks?.onSuccess?.();
} catch (e) {
callbacks?.onError?.(e);
@@ -46,7 +49,10 @@ function useVerify() {
const accts = accountIdsToAccts(getState(), accountIds);
verifyEffect(accountIds, false);
try {
await api.delete('/api/v1/pleroma/admin/users/tag', { data: { nicknames: accts, tags: ['verified'] } });
await api('/api/v1/pleroma/admin/users/tag', {
method: 'DELETE',
body: JSON.stringify({ nicknames: accts, tags: ['verified'] }),
});
callbacks?.onSuccess?.();
} catch (e) {
callbacks?.onError?.(e);

View File

@@ -24,7 +24,7 @@ const useAnnouncements = () => {
const api = useApi();
const getAnnouncements = async () => {
const { data } = await api.get<Announcement[]>('/api/v1/announcements');
const { json: data } = await api<Announcement[]>('/api/v1/announcements');
const normalizedData = data?.map((announcement) => announcementSchema.parse(announcement));
return normalizedData;
@@ -40,7 +40,7 @@ const useAnnouncements = () => {
mutate: addReaction,
} = useMutation({
mutationFn: ({ announcementId, name }: { announcementId: string; name: string }) =>
api.put<Announcement>(`/api/v1/announcements/${announcementId}/reactions/${name}`),
api(`/api/v1/announcements/${announcementId}/reactions/${name}`, { method: 'PUT' }),
retry: false,
onMutate: ({ announcementId: id, name }) => {
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
@@ -64,7 +64,7 @@ const useAnnouncements = () => {
mutate: removeReaction,
} = useMutation({
mutationFn: ({ announcementId, name }: { announcementId: string; name: string }) =>
api.delete<Announcement>(`/api/v1/announcements/${announcementId}/reactions/${name}`),
api<Announcement>(`/api/v1/announcements/${announcementId}/reactions/${name}`, { method: 'DELETE' }),
retry: false,
onMutate: ({ announcementId: id, name }) => {
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>

View File

@@ -10,7 +10,7 @@ function useCancelMembershipRequest(group: Group) {
const { createEntity, isSubmitting } = useCreateEntity(
[Entities.GROUP_RELATIONSHIPS],
() => api.post(`/api/v1/groups/${group.id}/membership_requests/${me?.id}/reject`),
() => api(`/api/v1/groups/${group.id}/membership_requests/${me?.id}/reject`, { method: 'POST' }),
);
return {

View File

@@ -17,10 +17,16 @@ function useCreateGroup() {
const api = useApi();
const { createEntity, ...rest } = useCreateEntity([Entities.GROUPS, 'search', ''], (params: CreateGroupParams) => {
return api.post('/api/v1/groups', params, {
const formData = new FormData();
Object.entries(params).forEach(([key, value]) => formData.append(key, value));
return api('/api/v1/groups', {
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data',
},
body: formData,
});
}, { schema: groupSchema });

View File

@@ -8,7 +8,7 @@ function useDeleteGroupStatus(group: Group, statusId: string) {
const api = useApi();
const { deleteEntity, isSubmitting } = useDeleteEntity(
Entities.STATUSES,
() => api.delete(`/api/v1/groups/${group.id}/statuses/${statusId}`),
() => api(`/api/v1/groups/${group.id}/statuses/${statusId}`, { method: 'DELETE' }),
);
return {

View File

@@ -14,7 +14,7 @@ function useGroup(groupId: string, refetch = true) {
const { entity: group, isUnauthorized, ...result } = useEntity<Group>(
[Entities.GROUPS, groupId],
() => api.get(`/api/v1/groups/${groupId}`),
() => api(`/api/v1/groups/${groupId}`),
{
schema: groupSchema,
refetch,

View File

@@ -10,7 +10,7 @@ function useGroupMedia(groupId: string) {
const api = useApi();
return useEntities([Entities.STATUSES, 'groupMedia', groupId], () => {
return api.get(`/api/v1/timelines/group/${groupId}?only_media=true`);
return api(`/api/v1/timelines/group/${groupId}?only_media=true`);
}, { schema: statusSchema });
}

View File

@@ -10,7 +10,7 @@ function useGroupMembers(groupId: string, role: GroupRoles) {
const { entities, ...result } = useEntities<GroupMember>(
[Entities.GROUP_MEMBERSHIPS, groupId, role],
() => api.get(`/api/v1/groups/${groupId}/memberships?role=${role}`),
() => api(`/api/v1/groups/${groupId}/memberships?role=${role}`),
{ schema: groupMemberSchema },
);

View File

@@ -16,7 +16,7 @@ function useGroupMembershipRequests(groupId: string) {
const { entities, invalidate, fetchEntities, ...rest } = useEntities(
path,
() => api.get(`/api/v1/groups/${groupId}/membership_requests`),
() => api(`/api/v1/groups/${groupId}/membership_requests`),
{
schema: accountSchema,
enabled: relationship?.role === GroupRoles.OWNER || relationship?.role === GroupRoles.ADMIN,
@@ -24,13 +24,13 @@ function useGroupMembershipRequests(groupId: string) {
);
const { dismissEntity: authorize } = useDismissEntity(path, async (accountId: string) => {
const response = await api.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`);
const response = await api(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`, { method: 'POST' });
invalidate();
return response;
});
const { dismissEntity: reject } = useDismissEntity(path, async (accountId: string) => {
const response = await api.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`);
const response = await api(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`, { method: 'POST' });
invalidate();
return response;
});

View File

@@ -10,7 +10,7 @@ function useGroupRelationship(groupId: string | undefined) {
const { entity: groupRelationship, ...result } = useEntity<GroupRelationship>(
[Entities.GROUP_RELATIONSHIPS, groupId!],
() => api.get(`/api/v1/groups/relationships?id[]=${groupId}`),
() => api(`/api/v1/groups/relationships?id[]=${groupId}`),
{
enabled: !!groupId,
schema: z.array(groupRelationshipSchema).nonempty().transform(arr => arr[0]),

View File

@@ -8,8 +8,7 @@ function useGroupRelationships(listKey: string[], ids: string[]) {
const { isLoggedIn } = useLoggedIn();
function fetchGroupRelationships(ids: string[]) {
const q = ids.map((id) => `id[]=${id}`).join('&');
return api.get(`/api/v1/groups/relationships?${q}`);
return api('/api/v1/groups/relationships', { params: { ids } });
}
const { entityMap: relationships, ...result } = useBatchedEntities<GroupRelationship>(

View File

@@ -12,7 +12,7 @@ function useGroups() {
const { entities, ...result } = useEntities<Group>(
[Entities.GROUPS, 'search', ''],
() => api.get('/api/v1/groups'),
() => api('/api/v1/groups'),
{ enabled: features.groups, schema: groupSchema },
);
const { relationships } = useGroupRelationships(

View File

@@ -16,10 +16,16 @@ function useUpdateGroup(groupId: string) {
const api = useApi();
const { createEntity, ...rest } = useCreateEntity([Entities.GROUPS], (params: UpdateGroupParams) => {
return api.put(`/api/v1/groups/${groupId}`, params, {
const formData = new FormData();
Object.entries(params).forEach(([key, value]) => formData.append(key, value));
return api(`/api/v1/groups/${groupId}`, {
method: 'PUT',
headers: {
'Content-Type': 'multipart/form-data',
},
body: formData,
});
}, { schema: groupSchema });

View File

@@ -10,7 +10,7 @@ function useBookmarkFolders() {
const { entities, ...result } = useEntities<BookmarkFolder>(
[Entities.BOOKMARK_FOLDERS],
() => api.get('/api/v1/pleroma/bookmark_folders'),
() => api('/api/v1/pleroma/bookmark_folders'),
{ enabled: features.bookmarkFolders, schema: bookmarkFolderSchema },
);

View File

@@ -14,10 +14,9 @@ function useCreateBookmarkFolder() {
const { createEntity, ...rest } = useCreateEntity(
[Entities.BOOKMARK_FOLDERS],
(params: CreateBookmarkFolderParams) =>
api.post('/api/v1/pleroma/bookmark_folders', params, {
headers: {
'Content-Type': 'multipart/form-data',
},
api('/api/v1/pleroma/bookmark_folders', {
method: 'POST',
body: JSON.stringify(params),
}),
{ schema: bookmarkFolderSchema },
);

View File

@@ -14,10 +14,9 @@ function useUpdateBookmarkFolder(folderId: string) {
const { createEntity, ...rest } = useCreateEntity(
[Entities.BOOKMARK_FOLDERS],
(params: UpdateBookmarkFolderParams) =>
api.patch(`/api/v1/pleroma/bookmark_folders/${folderId}`, params, {
headers: {
'Content-Type': 'multipart/form-data',
},
api(`/api/v1/pleroma/bookmark_folders/${folderId}`, {
method: 'PATCH',
body: JSON.stringify(params),
}),
{ schema: bookmarkFolderSchema },
);

View File

@@ -1,10 +1,7 @@
/**
* 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';
@@ -12,24 +9,22 @@ import * as BuildConfig from 'soapbox/build-config';
import { selectAccount } from 'soapbox/selectors';
import { RootState } from 'soapbox/store';
import { getAccessToken, getAppToken, isURL, parseBaseURL } from 'soapbox/utils/auth';
import type MockAdapter from 'axios-mock-adapter';
import { buildFullPath } from 'soapbox/utils/url';
/**
Parse Link headers, mostly for pagination.
@see {@link https://www.npmjs.com/package/http-link-header}
@param {object} response - Axios response object
@param {object} response - Fetch API response object
@returns {object} Link object
*/
export const getLinks = (response: AxiosResponse): LinkHeader => {
return new LinkHeader(response.headers?.link);
export const getLinks = (response: Pick<Response, 'headers'>): LinkHeader => {
return new LinkHeader(response.headers?.get('link') || undefined);
};
export const getNextLink = (response: AxiosResponse): string | undefined => {
export const getNextLink = (response: Pick<Response, 'headers'>): string | undefined => {
return getLinks(response).refs.find(link => link.rel === 'next')?.uri;
};
export const getPrevLink = (response: AxiosResponse): string | undefined => {
export const getPrevLink = (response: Pick<Response, 'headers'>): string | undefined => {
return getLinks(response).refs.find(link => link.rel === 'prev')?.uri;
};
@@ -37,14 +32,6 @@ 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,
@@ -53,39 +40,55 @@ const getAuthBaseURL = createSelector([
return baseURL !== window.location.origin ? baseURL : '';
});
/**
* Base client for HTTP requests.
* @param {string} accessToken
* @param {string} baseURL
* @returns {object} Axios instance
*/
export const baseClient = (
accessToken?: string | null,
baseURL: string = '',
): AxiosInstance => {
const headers: Record<string, string> = {};
export const getFetch = (accessToken?: string | null, baseURL: string = '') =>
<T = any>(input: URL | RequestInfo, init?: RequestInit & { params?: Record<string, any>} | undefined) => {
const fullPath = buildFullPath(input.toString(), isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : baseURL, init?.params);
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
const headers = new Headers(init?.headers);
const contentType = headers.get('Content-Type') || 'application/json';
return axios.create({
// When BACKEND_URL is set, always use it.
baseURL: isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : baseURL,
headers,
transformResponse: [maybeParseJSON],
});
};
if (accessToken) {
headers.set('Authorization', `Bearer ${accessToken}`);
}
headers.set('Content-Type', contentType);
return fetch(fullPath, {
...init,
headers,
}).then(async (response) => {
if (!response.ok) throw { response };
const data = await response.text();
let json: T = undefined!;
try {
json = JSON.parse(data);
} catch (e) {
//
}
return { ...response, data, json };
});
};
/**
* Dumb client for grabbing static files.
* It uses FE_SUBDIRECTORY and parses JSON if possible.
* No authorization is needed.
*/
export const staticClient = axios.create({
baseURL: BuildConfig.FE_SUBDIRECTORY,
transformResponse: [maybeParseJSON],
});
export const staticFetch = (input: URL | RequestInfo, init?: RequestInit | undefined) => {
const fullPath = buildFullPath(input.toString(), BuildConfig.FE_SUBDIRECTORY);
return fetch(fullPath, init).then(async (response) => {
if (!response.ok) throw { response };
const data = await response.text();
let json: any = undefined!;
try {
json = JSON.parse(data);
} catch (e) {
//
}
return { ...response, data, json };
});
};
/**
* Stateful API client.
@@ -94,15 +97,13 @@ export const staticClient = axios.create({
* @param {string} authType - Either 'user' or 'app'
* @returns {object} Axios instance
*/
export default (getState: () => RootState, authType: string = 'user'): AxiosInstance => {
export const api = (getState: () => RootState, authType: string = 'user') => {
const state = getState();
const accessToken = getToken(state, authType);
const me = state.me;
const baseURL = me ? getAuthBaseURL(state, me) : '';
return baseClient(accessToken, baseURL);
return getFetch(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[] => [];
export default api;