Merge branch 'mastodon-client' into 'main'

Use a custom API client for fetching data

See merge request soapbox-pub/soapbox!3146
environments/review-main-yi2y9f/deployments/4887
Alex Gleason 2024-10-11 03:47:12 +00:00
commit c3655d085c
47 zmienionych plików z 357 dodań i 203 usunięć

Wyświetl plik

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

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -3,8 +3,7 @@ import { useHistory } from 'react-router-dom';
import { Entities } from 'soapbox/entity-store/entities';
import { useEntity } from 'soapbox/entity-store/hooks';
import { useFeatures, useLoggedIn } from 'soapbox/hooks';
import { useApi } from 'soapbox/hooks/useApi';
import { useApi, useFeatures, useLoggedIn } from 'soapbox/hooks';
import { type Account, accountSchema } from 'soapbox/schemas';
import { useRelationship } from './useRelationship';

Wyświetl plik

@ -3,8 +3,7 @@ import { useHistory } from 'react-router-dom';
import { Entities } from 'soapbox/entity-store/entities';
import { useEntityLookup } from 'soapbox/entity-store/hooks';
import { useFeatures, useLoggedIn } from 'soapbox/hooks';
import { useApi } from 'soapbox/hooks/useApi';
import { useApi, useFeatures, useLoggedIn } from 'soapbox/hooks';
import { type Account, accountSchema } from 'soapbox/schemas';
import { useRelationship } from './useRelationship';

Wyświetl plik

@ -1,8 +1,7 @@
import { importEntities } from 'soapbox/entity-store/actions';
import { Entities } from 'soapbox/entity-store/entities';
import { useTransaction } from 'soapbox/entity-store/hooks';
import { useAppDispatch, useLoggedIn } from 'soapbox/hooks';
import { useApi } from 'soapbox/hooks/useApi';
import { useApi, useAppDispatch, useLoggedIn } from 'soapbox/hooks';
import { relationshipSchema } from 'soapbox/schemas';
interface FollowOpts {
@ -57,7 +56,7 @@ function useFollow() {
try {
const response = await api.post(`/api/v1/accounts/${accountId}/follow`, options);
const result = relationshipSchema.safeParse(response.data);
const result = relationshipSchema.safeParse(await response.json());
if (result.success) {
dispatch(importEntities([result.data], Entities.RELATIONSHIPS));
}

Wyświetl plik

@ -1,6 +1,6 @@
import { Entities } from 'soapbox/entity-store/entities';
import { useEntity } from 'soapbox/entity-store/hooks';
import { useApi } from 'soapbox/hooks/useApi';
import { useApi } from 'soapbox/hooks';
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig';
import { type PatronUser, patronUserSchema } from 'soapbox/schemas';

Wyświetl plik

@ -1,7 +1,6 @@
import { Entities } from 'soapbox/entity-store/entities';
import { useBatchedEntities } from 'soapbox/entity-store/hooks/useBatchedEntities';
import { useLoggedIn } from 'soapbox/hooks';
import { useApi } from 'soapbox/hooks/useApi';
import { useApi, useLoggedIn } from 'soapbox/hooks';
import { type Relationship, relationshipSchema } from 'soapbox/schemas';
function useRelationships(listKey: string[], ids: string[]) {

Wyświetl plik

@ -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,8 @@ const useAnnouncements = () => {
const userAnnouncements = useUserAnnouncements();
const getAnnouncements = async () => {
const { data } = await api.get<AdminAnnouncement[]>('/api/v1/pleroma/admin/announcements');
const response = await api.get('/api/v1/pleroma/admin/announcements');
const data: AdminAnnouncement[] = await response.json();
const normalizedData = data.map((announcement) => adminAnnouncementSchema.parse(announcement));
return normalizedData;
@ -42,10 +41,12 @@ const useAnnouncements = () => {
} = useMutation({
mutationFn: (params: CreateAnnouncementParams) => api.post('/api/v1/pleroma/admin/announcements', params),
retry: false,
onSuccess: ({ data }: AxiosResponse) =>
queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
onSuccess: async (response: Response) => {
const data = await response.json();
return queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
[...prevResult, adminAnnouncementSchema.parse(data)],
),
);
},
onSettled: () => userAnnouncements.refetch(),
});
@ -55,10 +56,12 @@ const useAnnouncements = () => {
} = useMutation({
mutationFn: ({ id, ...params }: UpdateAnnouncementParams) => api.patch(`/api/v1/pleroma/admin/announcements/${id}`, params),
retry: false,
onSuccess: ({ data }: AxiosResponse) =>
queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
onSuccess: async (response: Response) => {
const data = await response.json();
return queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
prevResult.map((announcement) => announcement.id === data.id ? adminAnnouncementSchema.parse(data) : announcement),
),
);
},
onSettled: () => userAnnouncements.refetch(),
});

Wyświetl plik

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

Wyświetl plik

@ -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,8 @@ const useDomains = () => {
const api = useApi();
const getDomains = async () => {
const { data } = await api.get<Domain[]>('/api/v1/pleroma/admin/domains');
const response = await api.get('/api/v1/pleroma/admin/domains');
const data: Domain[] = await response.json();
const normalizedData = data.map((domain) => domainSchema.parse(domain));
return normalizedData;
@ -38,10 +37,12 @@ const useDomains = () => {
} = useMutation({
mutationFn: (params: CreateDomainParams) => api.post('/api/v1/pleroma/admin/domains', params),
retry: false,
onSuccess: ({ data }: AxiosResponse) =>
queryClient.setQueryData(['admin', 'domains'], (prevResult: ReadonlyArray<Domain>) =>
onSuccess: async (response: Response) => {
const data = await response.json();
return queryClient.setQueryData(['admin', 'domains'], (prevResult: ReadonlyArray<Domain>) =>
[...prevResult, domainSchema.parse(data)],
),
);
},
});
const {
@ -50,10 +51,12 @@ const useDomains = () => {
} = useMutation({
mutationFn: ({ id, ...params }: UpdateDomainParams) => api.patch(`/api/v1/pleroma/admin/domains/${id}`, params),
retry: false,
onSuccess: ({ data }: AxiosResponse) =>
queryClient.setQueryData(['admin', 'domains'], (prevResult: ReadonlyArray<Domain>) =>
onSuccess: async (response: Response) => {
const data = await response.json();
return queryClient.setQueryData(['admin', 'domains'], (prevResult: ReadonlyArray<Domain>) =>
prevResult.map((domain) => domain.id === data.id ? domainSchema.parse(data) : domain),
),
);
},
});
const {

Wyświetl plik

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

Wyświetl plik

@ -14,7 +14,8 @@ 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 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));

Wyświetl plik

@ -4,15 +4,14 @@ 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 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;
};
@ -28,19 +27,21 @@ const useRelays = () => {
} = useMutation({
mutationFn: (relayUrl: string) => api.post('/api/v1/pleroma/admin/relays', { relay_url: relayUrl }),
retry: false,
onSuccess: ({ data }: AxiosResponse) =>
queryClient.setQueryData(['admin', 'relays'], (prevResult: ReadonlyArray<Relay>) =>
onSuccess: async (response: Response) => {
const data = await response.json();
return queryClient.setQueryData(['admin', 'relays'], (prevResult: ReadonlyArray<Relay>) =>
[...prevResult, relaySchema.parse(data)],
),
);
},
});
const {
mutate: unfollowRelay,
isPending: isPendingUnfollow,
} = useMutation({
mutationFn: (relayUrl: string) => api.delete('/api/v1/pleroma/admin/relays', {
data: { relay_url: relayUrl },
}),
mutationFn: async (relayUrl: string) => {
await api.request('DELETE', '/api/v1/pleroma/admin/relays', { relay_url: relayUrl });
},
retry: false,
onSuccess: (_, relayUrl) =>
queryClient.setQueryData(['admin', 'relays'], (prevResult: ReadonlyArray<Relay>) =>

Wyświetl plik

@ -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,8 @@ const useRules = () => {
const api = useApi();
const getRules = async () => {
const { data } = await api.get<AdminRule[]>('/api/v1/pleroma/admin/rules');
const response = await api.get('/api/v1/pleroma/admin/rules');
const data: AdminRule[] = await response.json();
const normalizedData = data.map((rule) => adminRuleSchema.parse(rule));
return normalizedData;
@ -41,10 +40,12 @@ const useRules = () => {
} = useMutation({
mutationFn: (params: CreateRuleParams) => api.post('/api/v1/pleroma/admin/rules', params),
retry: false,
onSuccess: ({ data }: AxiosResponse) =>
queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray<AdminRule>) =>
onSuccess: async (response: Response) => {
const data = await response.json();
return queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray<AdminRule>) =>
[...prevResult, adminRuleSchema.parse(data)],
),
);
},
});
const {
@ -53,10 +54,12 @@ const useRules = () => {
} = useMutation({
mutationFn: ({ id, ...params }: UpdateRuleParams) => api.patch(`/api/v1/pleroma/admin/rules/${id}`, params),
retry: false,
onSuccess: ({ data }: AxiosResponse) =>
queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray<AdminRule>) =>
onSuccess: async (response: Response) => {
const data = await response.json();
return queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray<AdminRule>) =>
prevResult.map((rule) => rule.id === data.id ? adminRuleSchema.parse(data) : rule),
),
);
},
});
const {

Wyświetl plik

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

Wyświetl plik

@ -46,7 +46,7 @@ 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.request('DELETE', '/api/v1/pleroma/admin/users/tag', { nicknames: accts, tags: ['verified'] });
callbacks?.onSuccess?.();
} catch (e) {
callbacks?.onError?.(e);

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,6 +1,6 @@
import { Entities } from 'soapbox/entity-store/entities';
import { useDismissEntity, useEntities } from 'soapbox/entity-store/hooks';
import { useApi } from 'soapbox/hooks/useApi';
import { useApi } from 'soapbox/hooks';
import { accountSchema } from 'soapbox/schemas';
import { GroupRoles } from 'soapbox/schemas/group-member';

Wyświetl plik

@ -14,7 +14,7 @@ function useGroupSearch(search: string) {
const { entities, ...result } = useEntities<Group>(
[Entities.GROUPS, 'discover', 'search', search],
() => api.get('/api/v1/groups/search', {
params: {
searchParams: {
q: search,
},
}),

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -11,7 +11,7 @@ function usePendingGroups() {
const { entities, ...result } = useEntities<Group>(
[Entities.GROUPS, account?.id!, 'pending'],
() => api.get('/api/v1/groups', {
params: {
searchParams: {
pending: true,
},
}),

Wyświetl plik

@ -13,12 +13,7 @@ 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',
},
}),
(params: UpdateBookmarkFolderParams) => api.patch(`/api/v1/pleroma/bookmark_folders/${folderId}`, params),
{ schema: bookmarkFolderSchema },
);

Wyświetl plik

@ -30,15 +30,14 @@ const useZapSplit = (status: StatusEntity | undefined, account: AccountEntity) =
const [zapArrays, setZapArrays] = useState<ZapSplitData[]>([]);
const [zapSplitData, setZapSplitData] = useState<{splitAmount: number; receiveAmount: number; splitValues: SplitValue[]}>({ splitAmount: Number(), receiveAmount: Number(), splitValues: [] });
const fetchZapSplit = async (id: string) => {
return await api.get(`/api/v1/ditto/${id}/zap_splits`);
};
const fetchZapSplit = (id: string) => api.get(`/api/v1/ditto/${id}/zap_splits`);
const loadZapSplitData = async () => {
if (status) {
const data = (await fetchZapSplit(status.id)).data;
const response = await fetchZapSplit(status.id);
const data: ZapSplitData[] = await response.json();
if (data) {
const normalizedData = data.map((dataSplit: ZapSplitData) => baseZapAccountSchema.parse(dataSplit));
const normalizedData = data.map((dataSplit) => baseZapAccountSchema.parse(dataSplit));
setZapArrays(normalizedData);
}
}
@ -53,7 +52,7 @@ const useZapSplit = (status: StatusEntity | undefined, account: AccountEntity) =
const receiveAmount = (zapAmount: number) => {
if (zapArrays.length > 0) {
const zapAmountPrincipal = zapArrays.find((zapSplit: ZapSplitData) => zapSplit.account.id === account.id);
const formattedZapAmountPrincipal = {
const formattedZapAmountPrincipal = {
account: zapAmountPrincipal?.account,
message: zapAmountPrincipal?.message,
weight: zapArrays.filter((zapSplit: ZapSplitData) => zapSplit.account.id === account.id).reduce((acc:number, zapData: ZapSplitData) => acc + zapData.weight, 0),

Wyświetl plik

@ -1,5 +1,4 @@
import type { Entity } from '../types';
import type { AxiosResponse } from 'axios';
import type z from 'zod';
type EntitySchema<TEntity extends Entity = Entity> = z.ZodType<TEntity, z.ZodTypeDef, any>;
@ -35,7 +34,7 @@ interface EntityCallbacks<Value, Error = unknown> {
* Passed into hooks to make requests.
* Must return an Axios response.
*/
type EntityFn<T> = (value: T) => Promise<AxiosResponse>
type EntityFn<T> = (value: T) => Promise<Response>
export type {
EntitySchema,

Wyświetl plik

@ -54,7 +54,8 @@ function useBatchedEntities<TEntity extends Entity>(
dispatch(entitiesFetchRequest(entityType, listKey));
try {
const response = await entityFn(filteredIds);
const entities = filteredArray(schema).parse(response.data);
const json = await response.json();
const entities = filteredArray(schema).parse(json);
dispatch(entitiesFetchSuccess(entities, entityType, listKey, 'end', {
next: undefined,
prev: undefined,

Wyświetl plik

@ -1,6 +1,6 @@
import { AxiosError } from 'axios';
import { z } from 'zod';
import { HTTPError } from 'soapbox/api/HTTPError';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch';
import { useLoading } from 'soapbox/hooks/useLoading';
@ -25,11 +25,11 @@ function useCreateEntity<TEntity extends Entity = Entity, Data = unknown>(
const [isSubmitting, setPromise] = useLoading();
const { entityType, listKey } = parseEntitiesPath(expandedPath);
async function createEntity(data: Data, callbacks: EntityCallbacks<TEntity, AxiosError> = {}): Promise<void> {
async function createEntity(data: Data, callbacks: EntityCallbacks<TEntity, HTTPError> = {}): Promise<void> {
try {
const result = await setPromise(entityFn(data));
const schema = opts.schema || z.custom<TEntity>();
const entity = schema.parse(result.data);
const entity = schema.parse(await result.json());
// TODO: optimistic updating
dispatch(importEntities([entity], entityType, listKey, 'start'));
@ -38,7 +38,7 @@ function useCreateEntity<TEntity extends Entity = Entity, Data = unknown>(
callbacks.onSuccess(entity);
}
} catch (error) {
if (error instanceof AxiosError) {
if (error instanceof HTTPError) {
if (callbacks.onError) {
callbacks.onError(error);
}

Wyświetl plik

@ -1,7 +1,7 @@
import LinkHeader from 'http-link-header';
import { useEffect } from 'react';
import z from 'zod';
import { getNextLink, getPrevLink } from 'soapbox/api';
import { useApi } from 'soapbox/hooks/useApi';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch';
import { useAppSelector } from 'soapbox/hooks/useAppSelector';
@ -66,13 +66,16 @@ function useEntities<TEntity extends Entity>(
dispatch(entitiesFetchRequest(entityType, listKey));
try {
const response = await req();
const entities = filteredArray(schema).parse(response.data);
const parsedCount = realNumberSchema.safeParse(response.headers['x-total-count']);
const json = await response.json();
const entities = filteredArray(schema).parse(json);
const parsedCount = realNumberSchema.safeParse(response.headers.get('x-total-count'));
const totalCount = parsedCount.success ? parsedCount.data : undefined;
const linkHeader = response.headers.get('link');
const links = linkHeader ? new LinkHeader(linkHeader) : undefined;
dispatch(entitiesFetchSuccess(entities, entityType, listKey, pos, {
next: getNextLink(response),
prev: getPrevLink(response),
next: links?.refs.find((link) => link.rel === 'next')?.uri,
prev: links?.refs.find((link) => link.rel === 'prev')?.uri,
totalCount: Number(totalCount) >= entities.length ? totalCount : undefined,
fetching: false,
fetched: true,

Wyświetl plik

@ -1,7 +1,7 @@
import { AxiosError } from 'axios';
import { useEffect, useState } from 'react';
import z from 'zod';
import { HTTPError } from 'soapbox/api/HTTPError';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch';
import { useAppSelector } from 'soapbox/hooks/useAppSelector';
import { useLoading } from 'soapbox/hooks/useLoading';
@ -46,7 +46,8 @@ function useEntity<TEntity extends Entity>(
const fetchEntity = async () => {
try {
const response = await setPromise(entityFn());
const entity = schema.parse(response.data);
const json = await response.json();
const entity = schema.parse(json);
dispatch(importEntities([entity], entityType));
} catch (e) {
setError(e);
@ -67,8 +68,8 @@ function useEntity<TEntity extends Entity>(
isLoading,
isLoaded,
error,
isUnauthorized: error instanceof AxiosError && error.response?.status === 401,
isForbidden: error instanceof AxiosError && error.response?.status === 403,
isUnauthorized: error instanceof HTTPError && error.response.status === 401,
isForbidden: error instanceof HTTPError && error.response.status === 403,
};
}

Wyświetl plik

@ -1,7 +1,7 @@
import { AxiosError } from 'axios';
import { useEffect, useState } from 'react';
import { z } from 'zod';
import { HTTPError } from 'soapbox/api/HTTPError';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch';
import { useAppSelector } from 'soapbox/hooks/useAppSelector';
import { useLoading } from 'soapbox/hooks/useLoading';
@ -36,7 +36,8 @@ function useEntityLookup<TEntity extends Entity>(
const fetchEntity = async () => {
try {
const response = await setPromise(entityFn());
const entity = schema.parse(response.data);
const json = await response.json();
const entity = schema.parse(json);
setFetchedEntity(entity);
dispatch(importEntities([entity], entityType));
} catch (e) {
@ -57,8 +58,8 @@ function useEntityLookup<TEntity extends Entity>(
fetchEntity,
isFetching,
isLoading,
isUnauthorized: error instanceof AxiosError && error.response?.status === 401,
isForbidden: error instanceof AxiosError && error.response?.status === 403,
isUnauthorized: error instanceof HTTPError && error.response.status === 401,
isForbidden: error instanceof HTTPError && error.response.status === 403,
};
}

Wyświetl plik

@ -101,8 +101,9 @@ const Header: React.FC<IHeader> = ({ account }) => {
const data = error.response?.data as any;
toast.error(data?.error);
},
onSuccess: (response) => {
history.push(`/chats/${response.data.id}`);
onSuccess: async (response) => {
const data = await response.json();
history.push(`/chats/${data.id}`);
queryClient.invalidateQueries({
queryKey: ChatKeys.chatSearch(),
});

Wyświetl plik

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

Wyświetl plik

@ -49,11 +49,13 @@ const ChatSearch = (props: IChatSearch) => {
const data = error.response?.data as any;
toast.error(data?.error);
},
onSuccess: (response) => {
onSuccess: async (response) => {
const data = await response.json();
if (isMainPage) {
history.push(`/chats/${response.data.id}`);
history.push(`/chats/${data.id}`);
} else {
changeScreen(ChatWidgetScreens.CHAT, response.data.id);
changeScreen(ChatWidgetScreens.CHAT, data.id);
}
queryClient.invalidateQueries({ queryKey: ChatKeys.chatSearch() });

Wyświetl plik

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

Wyświetl plik

@ -53,8 +53,8 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
: intl.formatMessage(messages.joinSuccess),
);
},
onError(error) {
const message = (error.response?.data as any).error;
async onError(error) {
const message = (await error.response.json() as any).error;
if (message) {
toast.error(message);
}

Wyświetl plik

@ -67,10 +67,10 @@ const EditGroup: React.FC<IEditGroup> = ({ params: { groupId } }) => {
invalidate();
toast.success(intl.formatMessage(messages.groupSaved));
},
onError(error) {
const message = (error.response?.data as any)?.error;
async onError(error) {
const message = (await error.response.json() as any)?.error;
if (error.response?.status === 422 && typeof message !== 'undefined') {
if (error.response.status === 422 && message) {
toast.error(message);
}
},

Wyświetl plik

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

Wyświetl plik

@ -1,9 +1,12 @@
import api from 'soapbox/api';
import { MastodonClient } from 'soapbox/api/MastodonClient';
import { useGetState } from './useGetState';
import { useAppSelector } from './useAppSelector';
import { useOwnAccount } from './useOwnAccount';
/** Use stateful Axios client with auth from Redux. */
export const useApi = () => {
const getState = useGetState();
return api(getState);
};
export function useApi(): MastodonClient {
const { account } = useOwnAccount();
const accessToken = useAppSelector((state) => account ? state.auth.users.get(account.url)?.access_token : undefined);
const baseUrl = account ? new URL(account.url).origin : location.origin;
return new MastodonClient(baseUrl, accessToken);
}

Wyświetl plik

@ -47,8 +47,8 @@ const useUpdateCredentials = () => {
return { cachedAccount };
},
onSuccess(response) {
dispatch(patchMeSuccess(response.data));
async onSuccess(response) {
dispatch(patchMeSuccess(await response.json()));
toast.success('Chat Settings updated successfully');
},
onError(_error, _variables, context: any) {

Wyświetl plik

@ -2,7 +2,6 @@ import { InfiniteData, keepPreviousData, useInfiniteQuery, useMutation, useQuery
import sumBy from 'lodash/sumBy';
import { importFetchedAccount, importFetchedAccounts } from 'soapbox/actions/importer';
import { getNextLink } from 'soapbox/api';
import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context';
import { useStatContext } from 'soapbox/contexts/stat-context';
import { useApi, useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
@ -10,6 +9,7 @@ import { normalizeChatMessage } from 'soapbox/normalizers';
import toast from 'soapbox/toast';
import { ChatMessage } from 'soapbox/types/entities';
import { reOrderChatListItems, updateChatMessage } from 'soapbox/utils/chats';
import { getPagination } from 'soapbox/utils/pagination';
import { flattenPages, PaginatedResult, updatePageItem } from 'soapbox/utils/queries';
import { queryClient } from './client';
@ -84,16 +84,16 @@ const useChatMessages = (chat: IChat) => {
const getChatMessages = async (chatId: string, pageParam?: any): Promise<PaginatedResult<ChatMessage>> => {
const nextPageLink = pageParam?.link;
const uri = nextPageLink || `/api/v1/pleroma/chats/${chatId}/messages`;
const response = await api.get<any[]>(uri);
const { data } = response;
const response = await api.get(uri);
const data = await response.json();
const link = getNextLink(response);
const hasMore = !!link;
const { next } = getPagination(response);
const hasMore = !!next;
const result = data.map(normalizeChatMessage);
return {
result,
link,
link: next,
hasMore,
};
};
@ -133,17 +133,17 @@ const useChats = (search?: string) => {
const endpoint = features.chatsV2 ? '/api/v2/pleroma/chats' : '/api/v1/pleroma/chats';
const nextPageLink = pageParam?.link;
const uri = nextPageLink || endpoint;
const response = await api.get<IChat[]>(uri, {
params: search ? {
const response = await api.get(uri, {
searchParams: search ? {
search,
} : undefined,
});
const { data } = response;
const data: IChat[] = await response.json();
const link = getNextLink(response);
const hasMore = !!link;
const { next } = getPagination(response);
const hasMore = !!next;
setUnreadChatsCount(Number(response.headers['x-unread-messages-count']) || sumBy(data, (chat) => chat.unread));
setUnreadChatsCount(Number(response.headers.get('x-unread-messages-count')) || sumBy(data, (chat) => chat.unread));
// Set the relationships to these users in the redux store.
fetchRelationships.mutate({ accountIds: data.map((item) => item.account.id) });
@ -152,7 +152,7 @@ const useChats = (search?: string) => {
return {
result: data,
hasMore,
link,
link: next,
};
};
@ -178,7 +178,7 @@ const useChats = (search?: string) => {
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 };
};
@ -190,7 +190,8 @@ const useChat = (chatId?: string) => {
const getChat = async () => {
if (chatId) {
const { data } = await api.get<IChat>(`/api/v1/pleroma/chats/${chatId}`);
const response = await api.get(`/api/v1/pleroma/chats/${chatId}`);
const data: IChat = await response.json();
fetchRelationships.mutate({ accountIds: [data.account.id] });
dispatch(importFetchedAccount(data.account));
@ -217,8 +218,9 @@ const useChatActions = (chatId: string) => {
const { chat, changeScreen } = useChatContext();
const markChatAsRead = async (lastReadId: string) => {
return api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId })
.then(({ data }) => {
return api.post(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId })
.then(async (response) => {
const data = await response.json();
updatePageItem(ChatKeys.chatSearch(), data, (o, n) => o.id === n.id);
const queryData = queryClient.getQueryData<InfiniteData<PaginatedResult<unknown>>>(ChatKeys.chatSearch());
@ -239,12 +241,13 @@ const useChatActions = (chatId: string) => {
};
const createChatMessage = useMutation({
mutationFn: ({ chatId, content, mediaIds }: { chatId: string; content: string; mediaIds?: string[] }) => {
return api.post<ChatMessage>(`/api/v1/pleroma/chats/${chatId}/messages`, {
mutationFn: async ({ chatId, content, mediaIds }: { chatId: string; content: string; mediaIds?: string[] }) => {
const response = await api.post(`/api/v1/pleroma/chats/${chatId}/messages`, {
content,
media_id: (mediaIds && mediaIds.length === 1) ? mediaIds[0] : undefined, // Pleroma backwards-compat
media_ids: mediaIds,
});
return response.json();
},
retry: false,
onMutate: async (variables) => {
@ -291,12 +294,13 @@ const useChatActions = (chatId: string) => {
onError: (_error: any, variables, context: any) => {
queryClient.setQueryData(['chats', 'messages', variables.chatId], context.prevChatMessages);
},
onSuccess: (response: any, variables, context) => {
const nextChat = { ...chat, last_message: response.data };
onSuccess: async (response, variables, context) => {
const data = await response.json();
const nextChat = { ...chat, last_message: data };
updatePageItem(ChatKeys.chatSearch(), nextChat, (o, n) => o.id === n.id);
updatePageItem(
ChatKeys.chatMessages(variables.chatId),
normalizeChatMessage(response.data),
normalizeChatMessage(data),
(o) => o.id === context.pendingId,
);
reOrderChatListItems();
@ -304,7 +308,7 @@ const useChatActions = (chatId: string) => {
});
const updateChat = useMutation({
mutationFn: (data: UpdateChatVariables) => api.patch<IChat>(`/api/v1/pleroma/chats/${chatId}`, data),
mutationFn: (data: UpdateChatVariables) => api.patch(`/api/v1/pleroma/chats/${chatId}`, data),
onMutate: async (data) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries({
@ -334,12 +338,13 @@ 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({
mutationFn: () => api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/accept`),
onSuccess(response) {
changeScreen(ChatWidgetScreens.CHAT, response.data.id);
mutationFn: () => api.post(`/api/v1/pleroma/chats/${chatId}/accept`),
async onSuccess(response) {
const data = await response.json();
changeScreen(ChatWidgetScreens.CHAT, data.id);
queryClient.invalidateQueries({ queryKey: ChatKeys.chat(chatId) });
queryClient.invalidateQueries({ queryKey: ChatKeys.chatMessages(chatId) });
queryClient.invalidateQueries({ queryKey: ChatKeys.chatSearch() });
@ -347,7 +352,7 @@ const useChatActions = (chatId: string) => {
});
const deleteChat = useMutation({
mutationFn: () => api.delete<IChat>(`/api/v1/pleroma/chats/${chatId}`),
mutationFn: () => api.delete(`/api/v1/pleroma/chats/${chatId}`),
onSuccess() {
changeScreen(ChatWidgetScreens.INBOX);
queryClient.invalidateQueries({ queryKey: ChatKeys.chatMessages(chatId) });
@ -357,11 +362,13 @@ const useChatActions = (chatId: string) => {
const createReaction = useMutation({
mutationFn: (data: CreateReactionVariables) => api.post(`/api/v1/pleroma/chats/${chatId}/messages/${data.messageId}/reactions`, {
emoji: data.emoji,
json: {
emoji: data.emoji,
},
}),
// TODO: add optimistic updates
onSuccess(response) {
updateChatMessage(response.data);
async onSuccess(response) {
updateChatMessage(await response.json());
},
});

Wyświetl plik

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

Wyświetl plik

@ -13,8 +13,8 @@ const useFetchRelationships = () => {
return api.get(`/api/v1/accounts/relationships?${ids}`);
},
onSuccess(response) {
dispatch(fetchRelationshipsSuccess(response.data));
async onSuccess(response) {
dispatch(fetchRelationshipsSuccess(await response.json()));
},
onError(error) {
dispatch(fetchRelationshipsFail(error));

Wyświetl plik

@ -1,32 +1,32 @@
import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query';
import { getNextLink } from 'soapbox/api';
import { useApi } from 'soapbox/hooks';
import { Account } from 'soapbox/types/entities';
import { getPagination } from 'soapbox/utils/pagination';
import { flattenPages, PaginatedResult } from 'soapbox/utils/queries';
export default function useAccountSearch(q: string) {
const api = useApi();
const getAccountSearch = async(q: string, pageParam: { link?: string }): Promise<PaginatedResult<Account>> => {
const getAccountSearch = async (q: string, pageParam: { link?: string }): Promise<PaginatedResult<Account>> => {
const nextPageLink = pageParam?.link;
const uri = nextPageLink || '/api/v1/accounts/search';
const response = await api.get(uri, {
params: {
searchParams: {
q,
limit: 10,
followers: true,
},
});
const { data } = response;
const data = await response.json();
const link = getNextLink(response);
const hasMore = !!link;
const { next } = getPagination(response);
const hasMore = !!next;
return {
result: data,
link,
link: next,
hasMore,
};
};

Wyświetl plik

@ -2,8 +2,8 @@ import { useInfiniteQuery, useMutation, keepPreviousData } from '@tanstack/react
import { fetchRelationships } from 'soapbox/actions/accounts';
import { importFetchedAccounts } from 'soapbox/actions/importer';
import { getLinks } from 'soapbox/api';
import { useApi, useAppDispatch } from 'soapbox/hooks';
import { getPagination } from 'soapbox/utils/pagination';
import { PaginatedResult, removePageItem } from '../utils/queries';
@ -32,18 +32,19 @@ const useSuggestions = () => {
const getV2Suggestions = async (pageParam: PageParam): Promise<PaginatedResult<Result>> => {
const endpoint = pageParam?.link || '/api/v2/suggestions';
const response = await api.get<Suggestion[]>(endpoint);
const hasMore = !!response.headers.link;
const nextLink = getLinks(response).refs.find(link => link.rel === 'next')?.uri;
const response = await api.get(endpoint);
const { next } = getPagination(response);
const hasMore = !!next;
const accounts = response.data.map(({ account }) => account);
const data: Suggestion[] = await response.json();
const accounts = data.map(({ account }) => account);
const accountIds = accounts.map((account) => account.id);
dispatch(importFetchedAccounts(accounts));
dispatch(fetchRelationships(accountIds));
return {
result: response.data.map(x => ({ ...x, account: x.account.id })),
link: nextLink,
result: data.map(x => ({ ...x, account: x.account.id })),
link: next,
hasMore,
};
};
@ -90,18 +91,19 @@ function useOnboardingSuggestions() {
const getV2Suggestions = async (pageParam: any): Promise<{ data: Suggestion[]; link: string | undefined; hasMore: boolean }> => {
const link = pageParam?.link || '/api/v2/suggestions';
const response = await api.get<Suggestion[]>(link);
const hasMore = !!response.headers.link;
const nextLink = getLinks(response).refs.find(link => link.rel === 'next')?.uri;
const response = await api.get(link);
const { next } = getPagination(response);
const hasMore = !!next;
const accounts = response.data.map(({ account }) => account);
const data: Suggestion[] = await response.json();
const accounts = data.map(({ account }) => account);
const accountIds = accounts.map((account) => account.id);
dispatch(importFetchedAccounts(accounts));
dispatch(fetchRelationships(accountIds));
return {
data: response.data,
link: nextLink,
data: data,
link: next,
hasMore,
};
};

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,16 @@
import LinkHeader from 'http-link-header';
interface Pagination {
next?: string;
prev?: string;
}
export function getPagination(response: Response): Pagination {
const header = response.headers.get('link');
const links = header ? new LinkHeader(header) : undefined;
return {
next: links?.refs.find((link) => link.rel === 'next')?.uri,
prev: links?.refs.find((link) => link.rel === 'prev')?.uri,
};
}