Remove ky, create a custom fetch client

This commit is contained in:
Alex Gleason 2024-10-10 22:15:21 -05:00
parent 82b6ff1743
commit 87ee06fd1f
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
38 changed files with 230 additions and 135 deletions

View File

@ -115,7 +115,6 @@
"intl-messageformat": "10.5.11", "intl-messageformat": "10.5.11",
"intl-pluralrules": "^2.0.0", "intl-pluralrules": "^2.0.0",
"isomorphic-dompurify": "^2.3.0", "isomorphic-dompurify": "^2.3.0",
"ky": "^1.7.2",
"leaflet": "^1.8.0", "leaflet": "^1.8.0",
"lexical": "^0.18.0", "lexical": "^0.18.0",
"line-awesome": "^1.3.0", "line-awesome": "^1.3.0",

10
src/api/HTTPError.ts Normal file
View File

@ -0,0 +1,10 @@
export class HTTPError extends Error {
response: Response;
constructor(response: Response) {
super(response.statusText);
this.response = response;
}
}

93
src/api/MastodonClient.ts Normal file
View File

@ -0,0 +1,93 @@
import { HTTPError } from './HTTPError';
interface Opts {
searchParams?: Record<string, string | number | boolean>;
headers?: Record<string, string>;
signal?: AbortSignal;
}
export class MastodonClient {
readonly baseUrl: string;
private fetch: typeof fetch;
private accessToken?: string;
constructor(baseUrl: string, accessToken?: string, fetch = globalThis.fetch.bind(globalThis)) {
this.fetch = fetch;
this.baseUrl = baseUrl;
this.accessToken = accessToken;
}
async get(path: string, opts: Opts = {}): Promise<Response> {
return this.request('GET', path, undefined, opts);
}
async post(path: string, data?: unknown, opts: Opts = {}): Promise<Response> {
return this.request('POST', path, data, opts);
}
async put(path: string, data?: unknown, opts: Opts = {}): Promise<Response> {
return this.request('PUT', path, data, opts);
}
async delete(path: string, opts: Opts = {}): Promise<Response> {
return this.request('DELETE', path, undefined, opts);
}
async patch(path: string, data: unknown, opts: Opts = {}): Promise<Response> {
return this.request('PATCH', path, data, opts);
}
async head(path: string, opts: Opts = {}): Promise<Response> {
return this.request('HEAD', path, undefined, opts);
}
async options(path: string, opts: Opts = {}): Promise<Response> {
return this.request('OPTIONS', path, undefined, opts);
}
async request(method: string, path: string, data: unknown, opts: Opts = {}): Promise<Response> {
const url = new URL(path, this.baseUrl);
if (opts.searchParams) {
const params = Object
.entries(opts.searchParams)
.map(([key, value]) => ([key, String(value)]));
url.search = new URLSearchParams(params).toString();
}
const headers = new Headers(opts.headers);
if (this.accessToken) {
headers.set('Authorization', `Bearer ${this.accessToken}`);
}
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');
body = JSON.stringify(data);
}
const request = new Request(url, {
method,
headers,
signal: opts.signal,
body,
});
const response = await this.fetch(request);
if (!response.ok) {
throw new HTTPError(response);
}
return response;
}
}

View File

@ -55,7 +55,7 @@ function useFollow() {
followEffect(accountId); followEffect(accountId);
try { try {
const response = await api.post(`/api/v1/accounts/${accountId}/follow`, { json: options }); const response = await api.post(`/api/v1/accounts/${accountId}/follow`, options);
const result = relationshipSchema.safeParse(await response.json()); const result = relationshipSchema.safeParse(await response.json());
if (result.success) { if (result.success) {
dispatch(importEntities([result.data], Entities.RELATIONSHIPS)); dispatch(importEntities([result.data], Entities.RELATIONSHIPS));

View File

@ -22,7 +22,8 @@ const useAnnouncements = () => {
const userAnnouncements = useUserAnnouncements(); const userAnnouncements = useUserAnnouncements();
const getAnnouncements = async () => { const getAnnouncements = async () => {
const data = await api.get<AdminAnnouncement[]>('/api/v1/pleroma/admin/announcements').json(); const response = await api.get('/api/v1/pleroma/admin/announcements');
const data: AdminAnnouncement[] = await response.json();
const normalizedData = data.map((announcement) => adminAnnouncementSchema.parse(announcement)); const normalizedData = data.map((announcement) => adminAnnouncementSchema.parse(announcement));
return normalizedData; return normalizedData;
@ -38,7 +39,7 @@ const useAnnouncements = () => {
mutate: createAnnouncement, mutate: createAnnouncement,
isPending: isCreating, isPending: isCreating,
} = useMutation({ } = useMutation({
mutationFn: (params: CreateAnnouncementParams) => api.post('/api/v1/pleroma/admin/announcements', { json: params }), mutationFn: (params: CreateAnnouncementParams) => api.post('/api/v1/pleroma/admin/announcements', params),
retry: false, retry: false,
onSuccess: async (response: Response) => { onSuccess: async (response: Response) => {
const data = await response.json(); const data = await response.json();
@ -53,7 +54,7 @@ const useAnnouncements = () => {
mutate: updateAnnouncement, mutate: updateAnnouncement,
isPending: isUpdating, isPending: isUpdating,
} = useMutation({ } = useMutation({
mutationFn: ({ id, ...params }: UpdateAnnouncementParams) => api.patch(`/api/v1/pleroma/admin/announcements/${id}`, { json: params }), mutationFn: ({ id, ...params }: UpdateAnnouncementParams) => api.patch(`/api/v1/pleroma/admin/announcements/${id}`, params),
retry: false, retry: false,
onSuccess: async (response: Response) => { onSuccess: async (response: Response) => {
const data = await response.json(); const data = await response.json();

View File

@ -11,8 +11,11 @@ interface CreateDomainParams {
const useCreateDomain = () => { const useCreateDomain = () => {
const api = useApi(); const api = useApi();
const { createEntity, ...rest } = useCreateEntity([Entities.DOMAINS], (params: CreateDomainParams) => const { createEntity, ...rest } = useCreateEntity(
api.post('/api/v1/pleroma/admin/domains', { json: params }), { schema: domainSchema }); [Entities.DOMAINS],
(params: CreateDomainParams) => api.post('/api/v1/pleroma/admin/domains', params),
{ schema: domainSchema },
);
return { return {
createDomain: createEntity, createDomain: createEntity,

View File

@ -18,7 +18,8 @@ const useDomains = () => {
const api = useApi(); const api = useApi();
const getDomains = async () => { const getDomains = async () => {
const data = await api.get<Domain[]>('/api/v1/pleroma/admin/domains').json(); const response = await api.get('/api/v1/pleroma/admin/domains');
const data: Domain[] = await response.json();
const normalizedData = data.map((domain) => domainSchema.parse(domain)); const normalizedData = data.map((domain) => domainSchema.parse(domain));
return normalizedData; return normalizedData;
@ -34,7 +35,7 @@ const useDomains = () => {
mutate: createDomain, mutate: createDomain,
isPending: isCreating, isPending: isCreating,
} = useMutation({ } = useMutation({
mutationFn: (params: CreateDomainParams) => api.post('/api/v1/pleroma/admin/domains', { json: params }), mutationFn: (params: CreateDomainParams) => api.post('/api/v1/pleroma/admin/domains', params),
retry: false, retry: false,
onSuccess: async (response: Response) => { onSuccess: async (response: Response) => {
const data = await response.json(); const data = await response.json();
@ -48,7 +49,7 @@ const useDomains = () => {
mutate: updateDomain, mutate: updateDomain,
isPending: isUpdating, isPending: isUpdating,
} = useMutation({ } = useMutation({
mutationFn: ({ id, ...params }: UpdateDomainParams) => api.patch(`/api/v1/pleroma/admin/domains/${id}`, { json: params }), mutationFn: ({ id, ...params }: UpdateDomainParams) => api.patch(`/api/v1/pleroma/admin/domains/${id}`, params),
retry: false, retry: false,
onSuccess: async (response: Response) => { onSuccess: async (response: Response) => {
const data = await response.json(); const data = await response.json();

View File

@ -32,7 +32,8 @@ export const useManageZapSplit = () => {
*/ */
const fetchZapSplitData = async () => { const fetchZapSplitData = async () => {
try { try {
const data = await api.get<ZapSplitData[]>('/api/v1/ditto/zap_splits').json(); const response = await api.get('/api/v1/ditto/zap_splits');
const data: ZapSplitData[] = await response.json();
if (data) { if (data) {
const normalizedData = data.map((dataSplit) => baseZapAccountSchema.parse(dataSplit)); const normalizedData = data.map((dataSplit) => baseZapAccountSchema.parse(dataSplit));
setFormattedData(normalizedData); setFormattedData(normalizedData);
@ -132,9 +133,7 @@ export const useManageZapSplit = () => {
* @param accountId - The ID of the account to be removed. * @param accountId - The ID of the account to be removed.
*/ */
const removeAccount = async (accountId: string) => { const removeAccount = async (accountId: string) => {
const isToDelete = [(formattedData.find(item => item.account.id === accountId))?.account.id]; await api.request('DELETE', '/api/v1/admin/ditto/zap_splits', [accountId]);
await api.delete('/api/v1/admin/ditto/zap_splits/', { json: isToDelete });
await fetchZapSplitData(); await fetchZapSplitData();
}; };

View File

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

View File

@ -8,9 +8,10 @@ const useRelays = () => {
const api = useApi(); const api = useApi();
const getRelays = async () => { const getRelays = async () => {
const data = await api.get<{ relays: Relay[] }>('/api/v1/pleroma/admin/relay').json(); const response = await api.get('/api/v1/pleroma/admin/relay');
const relays: Relay[] = await response.json();
const normalizedData = data.relays?.map((relay) => relaySchema.parse(relay)); const normalizedData = relays?.map((relay) => relaySchema.parse(relay));
return normalizedData; return normalizedData;
}; };
@ -24,7 +25,7 @@ const useRelays = () => {
mutate: followRelay, mutate: followRelay,
isPending: isPendingFollow, isPending: isPendingFollow,
} = useMutation({ } = useMutation({
mutationFn: (relayUrl: string) => api.post('/api/v1/pleroma/admin/relays', { json: { relay_url: relayUrl } }), mutationFn: (relayUrl: string) => api.post('/api/v1/pleroma/admin/relays', { relay_url: relayUrl }),
retry: false, retry: false,
onSuccess: async (response: Response) => { onSuccess: async (response: Response) => {
const data = await response.json(); const data = await response.json();
@ -38,9 +39,9 @@ const useRelays = () => {
mutate: unfollowRelay, mutate: unfollowRelay,
isPending: isPendingUnfollow, isPending: isPendingUnfollow,
} = useMutation({ } = useMutation({
mutationFn: (relayUrl: string) => api.delete('/api/v1/pleroma/admin/relays', { mutationFn: async (relayUrl: string) => {
json: { relay_url: relayUrl }, await api.request('DELETE', '/api/v1/pleroma/admin/relays', { relay_url: relayUrl });
}), },
retry: false, retry: false,
onSuccess: (_, relayUrl) => onSuccess: (_, relayUrl) =>
queryClient.setQueryData(['admin', 'relays'], (prevResult: ReadonlyArray<Relay>) => queryClient.setQueryData(['admin', 'relays'], (prevResult: ReadonlyArray<Relay>) =>

View File

@ -21,7 +21,8 @@ const useRules = () => {
const api = useApi(); const api = useApi();
const getRules = async () => { const getRules = async () => {
const data = await api.get<AdminRule[]>('/api/v1/pleroma/admin/rules').json(); const response = await api.get('/api/v1/pleroma/admin/rules');
const data: AdminRule[] = await response.json();
const normalizedData = data.map((rule) => adminRuleSchema.parse(rule)); const normalizedData = data.map((rule) => adminRuleSchema.parse(rule));
return normalizedData; return normalizedData;
@ -37,7 +38,7 @@ const useRules = () => {
mutate: createRule, mutate: createRule,
isPending: isCreating, isPending: isCreating,
} = useMutation({ } = useMutation({
mutationFn: (params: CreateRuleParams) => api.post('/api/v1/pleroma/admin/rules', { json: params }), mutationFn: (params: CreateRuleParams) => api.post('/api/v1/pleroma/admin/rules', params),
retry: false, retry: false,
onSuccess: async (response: Response) => { onSuccess: async (response: Response) => {
const data = await response.json(); const data = await response.json();
@ -51,7 +52,7 @@ const useRules = () => {
mutate: updateRule, mutate: updateRule,
isPending: isUpdating, isPending: isUpdating,
} = useMutation({ } = useMutation({
mutationFn: ({ id, ...params }: UpdateRuleParams) => api.patch(`/api/v1/pleroma/admin/rules/${id}`, { json: params }), mutationFn: ({ id, ...params }: UpdateRuleParams) => api.patch(`/api/v1/pleroma/admin/rules/${id}`, params),
retry: false, retry: false,
onSuccess: async (response: Response) => { onSuccess: async (response: Response) => {
const data = await response.json(); const data = await response.json();

View File

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

View File

@ -8,8 +8,11 @@ import type { CreateDomainParams } from './useCreateDomain';
const useUpdateDomain = (id: string) => { const useUpdateDomain = (id: string) => {
const api = useApi(); const api = useApi();
const { createEntity, ...rest } = useCreateEntity([Entities.DOMAINS], (params: Omit<CreateDomainParams, 'domain'>) => const { createEntity, ...rest } = useCreateEntity(
api.patch(`/api/v1/pleroma/admin/domains/${id}`, { json: params }), { schema: domainSchema }); [Entities.DOMAINS],
(params: Omit<CreateDomainParams, 'domain'>) => api.patch(`/api/v1/pleroma/admin/domains/${id}`, params),
{ schema: domainSchema },
);
return { return {
updateDomain: createEntity, updateDomain: createEntity,

View File

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

View File

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

View File

@ -35,14 +35,11 @@ const useCaptcha = () => {
try { try {
const topI = getRandomNumber(0, (356 - 61)); const topI = getRandomNumber(0, (356 - 61));
const leftI = getRandomNumber(0, (330 - 61)); const leftI = getRandomNumber(0, (330 - 61));
const { data } = await api.get('/api/v1/ditto/captcha'); const response = await api.get('/api/v1/ditto/captcha');
if (data) { const data = captchaSchema.parse(await response.json());
const normalizedData = captchaSchema.parse(data); setCaptcha(data);
setCaptcha(normalizedData); setYPosition(topI);
setYPosition(topI); setXPosition(leftI);
setXPosition(leftI);
}
} catch (error) { } catch (error) {
toast.error('Error loading captcha:'); toast.error('Error loading captcha:');
} }

View File

@ -17,8 +17,7 @@ function useCreateGroup() {
const api = useApi(); const api = useApi();
const { createEntity, ...rest } = useCreateEntity([Entities.GROUPS, 'search', ''], (params: CreateGroupParams) => { const { createEntity, ...rest } = useCreateEntity([Entities.GROUPS, 'search', ''], (params: CreateGroupParams) => {
return api.post('/api/v1/groups', { return api.post('/api/v1/groups', params, {
json: params,
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },

View File

@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { HTTPError } from 'ky';
import { HTTPError } from 'soapbox/api/HTTPError';
import { useApi } from 'soapbox/hooks/useApi'; import { useApi } from 'soapbox/hooks/useApi';
import { useFeatures } from 'soapbox/hooks/useFeatures'; import { useFeatures } from 'soapbox/hooks/useFeatures';
@ -19,9 +19,8 @@ function useGroupValidation(name: string = '') {
const getValidation = async () => { const getValidation = async () => {
try { try {
return api.get<Validation>('/api/v1/groups/validate', { const response = await api.get('/api/v1/groups/validate', { searchParams: { name } });
searchParams: { name }, return response.json();
}).json();
} catch (e) { } catch (e) {
if (e instanceof HTTPError && e.response.status === 422) { if (e instanceof HTTPError && e.response.status === 422) {
return e.response.json(); return e.response.json();

View File

@ -17,8 +17,7 @@ function useUpdateGroup(groupId: string) {
const api = useApi(); const api = useApi();
const { createEntity, ...rest } = useCreateEntity([Entities.GROUPS], (params: UpdateGroupParams) => { const { createEntity, ...rest } = useCreateEntity([Entities.GROUPS], (params: UpdateGroupParams) => {
return api.put(`/api/v1/groups/${groupId}`, { return api.put(`/api/v1/groups/${groupId}`, params, {
json: params,
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },

View File

@ -14,8 +14,7 @@ function useCreateBookmarkFolder() {
const { createEntity, ...rest } = useCreateEntity( const { createEntity, ...rest } = useCreateEntity(
[Entities.BOOKMARK_FOLDERS], [Entities.BOOKMARK_FOLDERS],
(params: CreateBookmarkFolderParams) => (params: CreateBookmarkFolderParams) =>
api.post('/api/v1/pleroma/bookmark_folders', { api.post('/api/v1/pleroma/bookmark_folders', params, {
json: params,
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },

View File

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

View File

@ -30,13 +30,12 @@ const useZapSplit = (status: StatusEntity | undefined, account: AccountEntity) =
const [zapArrays, setZapArrays] = useState<ZapSplitData[]>([]); const [zapArrays, setZapArrays] = useState<ZapSplitData[]>([]);
const [zapSplitData, setZapSplitData] = useState<{splitAmount: number; receiveAmount: number; splitValues: SplitValue[]}>({ splitAmount: Number(), receiveAmount: Number(), splitValues: [] }); const [zapSplitData, setZapSplitData] = useState<{splitAmount: number; receiveAmount: number; splitValues: SplitValue[]}>({ splitAmount: Number(), receiveAmount: Number(), splitValues: [] });
const fetchZapSplit = (id: string) => { const fetchZapSplit = (id: string) => api.get(`/api/v1/ditto/${id}/zap_splits`);
return api.get<ZapSplitData[]>(`/api/v1/ditto/${id}/zap_splits`);
};
const loadZapSplitData = async () => { const loadZapSplitData = async () => {
if (status) { if (status) {
const data = await fetchZapSplit(status.id).json(); const response = await fetchZapSplit(status.id);
const data: ZapSplitData[] = await response.json();
if (data) { if (data) {
const normalizedData = data.map((dataSplit) => baseZapAccountSchema.parse(dataSplit)); const normalizedData = data.map((dataSplit) => baseZapAccountSchema.parse(dataSplit));
setZapArrays(normalizedData); setZapArrays(normalizedData);

View File

@ -1,6 +1,6 @@
import { HTTPError } from 'ky';
import { z } from 'zod'; import { z } from 'zod';
import { HTTPError } from 'soapbox/api/HTTPError';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch';
import { useLoading } from 'soapbox/hooks/useLoading'; import { useLoading } from 'soapbox/hooks/useLoading';

View File

@ -1,7 +1,7 @@
import { HTTPError } from 'ky';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import z from 'zod'; import z from 'zod';
import { HTTPError } from 'soapbox/api/HTTPError';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch';
import { useAppSelector } from 'soapbox/hooks/useAppSelector'; import { useAppSelector } from 'soapbox/hooks/useAppSelector';
import { useLoading } from 'soapbox/hooks/useLoading'; import { useLoading } from 'soapbox/hooks/useLoading';
@ -68,8 +68,8 @@ function useEntity<TEntity extends Entity>(
isLoading, isLoading,
isLoaded, isLoaded,
error, error,
isUnauthorized: error instanceof HTTPError && error.response?.status === 401, isUnauthorized: error instanceof HTTPError && error.response.status === 401,
isForbidden: error instanceof HTTPError && error.response?.status === 403, isForbidden: error instanceof HTTPError && error.response.status === 403,
}; };
} }

View File

@ -29,10 +29,10 @@ function useEntityActions<TEntity extends Entity = Entity, Data = any>(
useDeleteEntity(entityType, (entityId) => api.delete(endpoints.delete!.replace(/:id/g, entityId))); useDeleteEntity(entityType, (entityId) => api.delete(endpoints.delete!.replace(/:id/g, entityId)));
const { createEntity, isSubmitting: createSubmitting } = const { createEntity, isSubmitting: createSubmitting } =
useCreateEntity<TEntity, Data>(path, (data) => api.post(endpoints.post!, { json: data }), opts); useCreateEntity<TEntity, Data>(path, (data) => api.post(endpoints.post!, data), opts);
const { createEntity: updateEntity, isSubmitting: updateSubmitting } = const { createEntity: updateEntity, isSubmitting: updateSubmitting } =
useCreateEntity<TEntity, Data>(path, (data) => api.patch(endpoints.patch!, { json: data }), opts); useCreateEntity<TEntity, Data>(path, (data) => api.patch(endpoints.patch!, data), opts);
return { return {
createEntity, createEntity,

View File

@ -1,7 +1,7 @@
import { HTTPError } from 'ky';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { z } from 'zod'; import { z } from 'zod';
import { HTTPError } from 'soapbox/api/HTTPError';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch';
import { useAppSelector } from 'soapbox/hooks/useAppSelector'; import { useAppSelector } from 'soapbox/hooks/useAppSelector';
import { useLoading } from 'soapbox/hooks/useLoading'; import { useLoading } from 'soapbox/hooks/useLoading';
@ -58,8 +58,8 @@ function useEntityLookup<TEntity extends Entity>(
fetchEntity, fetchEntity,
isFetching, isFetching,
isLoading, isLoading,
isUnauthorized: error instanceof HTTPError && error.response?.status === 401, isUnauthorized: error instanceof HTTPError && error.response.status === 401,
isForbidden: error instanceof HTTPError && error.response?.status === 403, isForbidden: error instanceof HTTPError && error.response.status === 403,
}; };
} }

View File

@ -14,7 +14,8 @@ export function useAdminNostrRelays() {
return useQuery({ return useQuery({
queryKey: ['NostrRelay'], queryKey: ['NostrRelay'],
queryFn: async () => { queryFn: async () => {
const data = await api.get('/api/v1/admin/ditto/relays').json(); const response = await api.get('/api/v1/admin/ditto/relays');
const data = await response.json();
return relayEntitySchema.array().parse(data); return relayEntitySchema.array().parse(data);
}, },
}); });

View File

@ -20,7 +20,7 @@ const AdminNostrRelays: React.FC = () => {
const [relays, setRelays] = useState<RelayData[]>(result.data ?? []); const [relays, setRelays] = useState<RelayData[]>(result.data ?? []);
const mutation = useMutation({ const mutation = useMutation({
mutationFn: async () => api.put('/api/v1/admin/ditto/relays', { json: relays }), mutationFn: async () => api.put('/api/v1/admin/ditto/relays', relays),
}); });
const handleSubmit = () => { const handleSubmit = () => {

View File

@ -187,7 +187,7 @@ function useRequestName() {
const api = useApi(); const api = useApi();
return useMutation({ return useMutation({
mutationFn: (data: NameRequestData) => api.post('/api/v1/ditto/names', { json: data }), mutationFn: (data: NameRequestData) => api.post('/api/v1/ditto/names', data),
}); });
} }
@ -197,7 +197,8 @@ function useNames() {
return useQuery({ return useQuery({
queryKey: ['names', 'approved'], queryKey: ['names', 'approved'],
queryFn: async () => { queryFn: async () => {
const data = await api.get('/api/v1/ditto/names?approved=true').json(); const response = await api.get('/api/v1/ditto/names?approved=true');
const data = await response.json();
return adminAccountSchema.array().parse(data); return adminAccountSchema.array().parse(data);
}, },
placeholderData: [], placeholderData: [],
@ -210,7 +211,8 @@ function usePendingNames() {
return useQuery({ return useQuery({
queryKey: ['names', 'pending'], queryKey: ['names', 'pending'],
queryFn: async () => { queryFn: async () => {
const data = await api.get('/api/v1/ditto/names?approved=false').json(); const response = await api.get('/api/v1/ditto/names?approved=false');
const data = await response.json();
return adminAccountSchema.array().parse(data); return adminAccountSchema.array().parse(data);
}, },
placeholderData: [], placeholderData: [],

View File

@ -1,8 +1,8 @@
import { HTTPError } from 'ky';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { z } from 'zod'; import { z } from 'zod';
import { HTTPError } from 'soapbox/api/HTTPError';
import { useCreateGroup, useGroupValidation, type CreateGroupParams } from 'soapbox/api/hooks'; import { useCreateGroup, useGroupValidation, type CreateGroupParams } from 'soapbox/api/hooks';
import { Modal, Stack } from 'soapbox/components/ui'; import { Modal, Stack } from 'soapbox/components/ui';
import { useDebounce } from 'soapbox/hooks'; import { useDebounce } from 'soapbox/hooks';
@ -72,9 +72,13 @@ const CreateGroupModal: React.FC<ICreateGroupModal> = ({ onClose }) => {
}, },
async onError(error) { async onError(error) {
if (error instanceof HTTPError) { if (error instanceof HTTPError) {
const msg = z.object({ error: z.string() }).safeParse(await error.response.json()); try {
if (msg.success) { const data = await error.response.json();
toast.error(msg.data.error); const msg = z.object({ error: z.string() }).parse(data);
toast.error(msg.error);
} catch {
// Do nothing
} }
} }
}, },

View File

@ -1,20 +1,12 @@
import ky, { KyInstance } from 'ky'; import { MastodonClient } from 'soapbox/api/MastodonClient';
import { useAppSelector } from './useAppSelector'; import { useAppSelector } from './useAppSelector';
import { useOwnAccount } from './useOwnAccount'; import { useOwnAccount } from './useOwnAccount';
export function useApi(): KyInstance { export function useApi(): MastodonClient {
const { account } = useOwnAccount(); const { account } = useOwnAccount();
const accessToken = useAppSelector((state) => account ? state.auth.users.get(account.url)?.access_token : undefined); const accessToken = useAppSelector((state) => account ? state.auth.users.get(account.url)?.access_token : undefined);
const baseUrl = account ? new URL(account.url).origin : location.origin;
const headers: Record<string, string> = {}; return new MastodonClient(baseUrl, accessToken);
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
return ky.create({
prefixUrl: account ? new URL(account.url).origin : undefined,
headers,
});
} }

View File

@ -40,7 +40,7 @@ const useUpdateCredentials = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
return useMutation({ return useMutation({
mutationFn: (data: UpdateCredentialsData) => api.patch('/api/v1/accounts/update_credentials', { json: data }), mutationFn: (data: UpdateCredentialsData) => api.patch('/api/v1/accounts/update_credentials', data),
onMutate(variables) { onMutate(variables) {
const cachedAccount = account; const cachedAccount = account;
dispatch(patchMeSuccess({ ...account, ...variables })); dispatch(patchMeSuccess({ ...account, ...variables }));

View File

@ -84,7 +84,7 @@ const useChatMessages = (chat: IChat) => {
const getChatMessages = async (chatId: string, pageParam?: any): Promise<PaginatedResult<ChatMessage>> => { const getChatMessages = async (chatId: string, pageParam?: any): Promise<PaginatedResult<ChatMessage>> => {
const nextPageLink = pageParam?.link; const nextPageLink = pageParam?.link;
const uri = nextPageLink || `/api/v1/pleroma/chats/${chatId}/messages`; const uri = nextPageLink || `/api/v1/pleroma/chats/${chatId}/messages`;
const response = await api.get<any[]>(uri); const response = await api.get(uri);
const data = await response.json(); const data = await response.json();
const { next } = getPagination(response); const { next } = getPagination(response);
@ -133,14 +133,12 @@ const useChats = (search?: string) => {
const endpoint = features.chatsV2 ? '/api/v2/pleroma/chats' : '/api/v1/pleroma/chats'; const endpoint = features.chatsV2 ? '/api/v2/pleroma/chats' : '/api/v1/pleroma/chats';
const nextPageLink = pageParam?.link; const nextPageLink = pageParam?.link;
const uri = nextPageLink || endpoint; const uri = nextPageLink || endpoint;
const response = await api.get<IChat[]>(uri, { const response = await api.get(uri, {
json: { searchParams: search ? {
params: search ? { search,
search, } : undefined,
} : undefined,
},
}); });
const data = await response.json(); const data: IChat[] = await response.json();
const { next } = getPagination(response); const { next } = getPagination(response);
const hasMore = !!next; const hasMore = !!next;
@ -180,7 +178,7 @@ const useChats = (search?: string) => {
data, data,
}; };
const getOrCreateChatByAccountId = (accountId: string) => api.post<IChat>(`/api/v1/pleroma/chats/by-account-id/${accountId}`); const getOrCreateChatByAccountId = (accountId: string) => api.post(`/api/v1/pleroma/chats/by-account-id/${accountId}`);
return { chatsQuery, getOrCreateChatByAccountId }; return { chatsQuery, getOrCreateChatByAccountId };
}; };
@ -192,7 +190,8 @@ const useChat = (chatId?: string) => {
const getChat = async () => { const getChat = async () => {
if (chatId) { if (chatId) {
const data = await api.get<IChat>(`/api/v1/pleroma/chats/${chatId}`).json(); const response = await api.get(`/api/v1/pleroma/chats/${chatId}`);
const data: IChat = await response.json();
fetchRelationships.mutate({ accountIds: [data.account.id] }); fetchRelationships.mutate({ accountIds: [data.account.id] });
dispatch(importFetchedAccount(data.account)); dispatch(importFetchedAccount(data.account));
@ -219,7 +218,7 @@ const useChatActions = (chatId: string) => {
const { chat, changeScreen } = useChatContext(); const { chat, changeScreen } = useChatContext();
const markChatAsRead = async (lastReadId: string) => { const markChatAsRead = async (lastReadId: string) => {
return api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/read`, { json: { last_read_id: lastReadId } }) return api.post(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId })
.then(async (response) => { .then(async (response) => {
const data = await response.json(); const data = await response.json();
updatePageItem(ChatKeys.chatSearch(), data, (o, n) => o.id === n.id); updatePageItem(ChatKeys.chatSearch(), data, (o, n) => o.id === n.id);
@ -242,14 +241,13 @@ const useChatActions = (chatId: string) => {
}; };
const createChatMessage = useMutation({ const createChatMessage = useMutation({
mutationFn: ({ chatId, content, mediaIds }: { chatId: string; content: string; mediaIds?: string[] }) => { mutationFn: async ({ chatId, content, mediaIds }: { chatId: string; content: string; mediaIds?: string[] }) => {
return api.post<ChatMessage>(`/api/v1/pleroma/chats/${chatId}/messages`, { const response = await api.post(`/api/v1/pleroma/chats/${chatId}/messages`, {
json: { content,
content, media_id: (mediaIds && mediaIds.length === 1) ? mediaIds[0] : undefined, // Pleroma backwards-compat
media_id: (mediaIds && mediaIds.length === 1) ? mediaIds[0] : undefined, // Pleroma backwards-compat media_ids: mediaIds,
media_ids: mediaIds,
},
}); });
return response.json();
}, },
retry: false, retry: false,
onMutate: async (variables) => { onMutate: async (variables) => {
@ -310,7 +308,7 @@ const useChatActions = (chatId: string) => {
}); });
const updateChat = useMutation({ const updateChat = useMutation({
mutationFn: (data: UpdateChatVariables) => api.patch<IChat>(`/api/v1/pleroma/chats/${chatId}`, { json: data }), mutationFn: (data: UpdateChatVariables) => api.patch(`/api/v1/pleroma/chats/${chatId}`, data),
onMutate: async (data) => { onMutate: async (data) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update) // Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries({ await queryClient.cancelQueries({
@ -340,10 +338,10 @@ const useChatActions = (chatId: string) => {
}, },
}); });
const deleteChatMessage = (chatMessageId: string) => api.delete<IChat>(`/api/v1/pleroma/chats/${chatId}/messages/${chatMessageId}`); const deleteChatMessage = (chatMessageId: string) => api.delete(`/api/v1/pleroma/chats/${chatId}/messages/${chatMessageId}`);
const acceptChat = useMutation({ const acceptChat = useMutation({
mutationFn: () => api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/accept`), mutationFn: () => api.post(`/api/v1/pleroma/chats/${chatId}/accept`),
async onSuccess(response) { async onSuccess(response) {
const data = await response.json(); const data = await response.json();
changeScreen(ChatWidgetScreens.CHAT, data.id); changeScreen(ChatWidgetScreens.CHAT, data.id);
@ -354,7 +352,7 @@ const useChatActions = (chatId: string) => {
}); });
const deleteChat = useMutation({ const deleteChat = useMutation({
mutationFn: () => api.delete<IChat>(`/api/v1/pleroma/chats/${chatId}`), mutationFn: () => api.delete(`/api/v1/pleroma/chats/${chatId}`),
onSuccess() { onSuccess() {
changeScreen(ChatWidgetScreens.INBOX); changeScreen(ChatWidgetScreens.INBOX);
queryClient.invalidateQueries({ queryKey: ChatKeys.chatMessages(chatId) }); queryClient.invalidateQueries({ queryKey: ChatKeys.chatMessages(chatId) });

View File

@ -22,7 +22,8 @@ export default function useEmbed(url: string) {
const api = useApi(); const api = useApi();
const getEmbed = async (): Promise<Embed> => { const getEmbed = async (): Promise<Embed> => {
return api.get<Embed>('/api/oembed', { searchParams: { url } }).json(); const response = await api.get('/api/oembed', { searchParams: { url } });
return response.json();
}; };
return useQuery<Embed>({ return useQuery<Embed>({

View File

@ -12,13 +12,11 @@ export default function useAccountSearch(q: string) {
const nextPageLink = pageParam?.link; const nextPageLink = pageParam?.link;
const uri = nextPageLink || '/api/v1/accounts/search'; const uri = nextPageLink || '/api/v1/accounts/search';
const response = await api.get<Account[]>(uri, { const response = await api.get(uri, {
json: { searchParams: {
params: { q,
q, limit: 10,
limit: 10, followers: true,
followers: true,
},
}, },
}); });
const data = await response.json(); const data = await response.json();

View File

@ -32,11 +32,11 @@ const useSuggestions = () => {
const getV2Suggestions = async (pageParam: PageParam): Promise<PaginatedResult<Result>> => { const getV2Suggestions = async (pageParam: PageParam): Promise<PaginatedResult<Result>> => {
const endpoint = pageParam?.link || '/api/v2/suggestions'; const endpoint = pageParam?.link || '/api/v2/suggestions';
const response = await api.get<Suggestion[]>(endpoint); const response = await api.get(endpoint);
const { next } = getPagination(response); const { next } = getPagination(response);
const hasMore = !!next; const hasMore = !!next;
const data = await response.json(); const data: Suggestion[] = await response.json();
const accounts = data.map(({ account }) => account); const accounts = data.map(({ account }) => account);
const accountIds = accounts.map((account) => account.id); const accountIds = accounts.map((account) => account.id);
dispatch(importFetchedAccounts(accounts)); dispatch(importFetchedAccounts(accounts));
@ -91,11 +91,11 @@ function useOnboardingSuggestions() {
const getV2Suggestions = async (pageParam: any): Promise<{ data: Suggestion[]; link: string | undefined; hasMore: boolean }> => { const getV2Suggestions = async (pageParam: any): Promise<{ data: Suggestion[]; link: string | undefined; hasMore: boolean }> => {
const link = pageParam?.link || '/api/v2/suggestions'; const link = pageParam?.link || '/api/v2/suggestions';
const response = await api.get<Suggestion[]>(link); const response = await api.get(link);
const { next } = getPagination(response); const { next } = getPagination(response);
const hasMore = !!next; const hasMore = !!next;
const data = await response.json(); const data: Suggestion[] = await response.json();
const accounts = data.map(({ account }) => account); const accounts = data.map(({ account }) => account);
const accountIds = accounts.map((account) => account.id); const accountIds = accounts.map((account) => account.id);
dispatch(importFetchedAccounts(accounts)); dispatch(importFetchedAccounts(accounts));

View File

@ -11,7 +11,8 @@ export default function useTrends() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const getTrends = async() => { const getTrends = async() => {
const data = await api.get<any[]>('/api/v1/trends').json(); const response = await api.get('/api/v1/trends');
const data: Tag[] = await response.json();
dispatch(fetchTrendsSuccess(data)); dispatch(fetchTrendsSuccess(data));

View File

@ -5956,11 +5956,6 @@ known-css-properties@^0.29.0:
resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.29.0.tgz#e8ba024fb03886f23cb882e806929f32d814158f" resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.29.0.tgz#e8ba024fb03886f23cb882e806929f32d814158f"
integrity sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ== integrity sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==
ky@^1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/ky/-/ky-1.7.2.tgz#b97d9b997ba51ff1e152f0815d3d27b86513eb1c"
integrity sha512-OzIvbHKKDpi60TnF9t7UUVAF1B4mcqc02z5PIvrm08Wyb+yOcz63GRvEuVxNT18a9E1SrNouhB4W2NNLeD7Ykg==
kysely@^0.27.3: kysely@^0.27.3:
version "0.27.3" version "0.27.3"
resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.27.3.tgz#6cc6c757040500b43c4ac596cdbb12be400ee276" resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.27.3.tgz#6cc6c757040500b43c4ac596cdbb12be400ee276"