60
packages/pl-fe/src/api/hooks/accounts/useAccount.ts
Normal file
60
packages/pl-fe/src/api/hooks/accounts/useAccount.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { type Account as BaseAccount, accountSchema } from 'pl-api';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useAppSelector, useClient, useFeatures, useLoggedIn } from 'soapbox/hooks';
|
||||
import { type Account, normalizeAccount } from 'soapbox/normalizers';
|
||||
|
||||
import { useRelationship } from './useRelationship';
|
||||
|
||||
interface UseAccountOpts {
|
||||
withRelationship?: boolean;
|
||||
}
|
||||
|
||||
const useAccount = (accountId?: string, opts: UseAccountOpts = {}) => {
|
||||
const client = useClient();
|
||||
const history = useHistory();
|
||||
const features = useFeatures();
|
||||
const { me } = useLoggedIn();
|
||||
const { withRelationship } = opts;
|
||||
|
||||
const { entity, isUnauthorized, ...result } = useEntity<BaseAccount, Account>(
|
||||
[Entities.ACCOUNTS, accountId!],
|
||||
() => client.accounts.getAccount(accountId!),
|
||||
{ schema: accountSchema, enabled: !!accountId, transform: normalizeAccount },
|
||||
);
|
||||
|
||||
const meta = useAppSelector((state) => accountId && state.accounts_meta[accountId] || {});
|
||||
|
||||
const {
|
||||
relationship,
|
||||
isLoading: isRelationshipLoading,
|
||||
} = useRelationship(accountId, { enabled: withRelationship });
|
||||
|
||||
const isBlocked = entity?.relationship?.blocked_by === true;
|
||||
const isUnavailable = (me === entity?.id) ? false : (isBlocked && !features.blockersVisible);
|
||||
|
||||
const account = useMemo(
|
||||
() => entity ? { ...entity, relationship, __meta: { meta, ...entity.__meta } } : undefined,
|
||||
[entity, relationship],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isUnauthorized) {
|
||||
history.push('/login');
|
||||
}
|
||||
}, [isUnauthorized]);
|
||||
|
||||
return {
|
||||
...result,
|
||||
isLoading: result.isLoading,
|
||||
isRelationshipLoading,
|
||||
isUnauthorized,
|
||||
isUnavailable,
|
||||
account,
|
||||
};
|
||||
};
|
||||
|
||||
export { useAccount };
|
||||
80
packages/pl-fe/src/api/hooks/accounts/useAccountList.ts
Normal file
80
packages/pl-fe/src/api/hooks/accounts/useAccountList.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { type Account, normalizeAccount } from 'soapbox/normalizers';
|
||||
import { flattenPages } from 'soapbox/utils/queries';
|
||||
|
||||
import { useRelationships } from './useRelationships';
|
||||
|
||||
import type { PaginatedResponse, Account as BaseAccount } from 'pl-api';
|
||||
import type { EntityFn } from 'soapbox/entity-store/hooks/types';
|
||||
|
||||
const useAccountList = (listKey: string[], entityFn: EntityFn<void>) => {
|
||||
const getAccounts = async (pageParam?: Pick<PaginatedResponse<BaseAccount>, 'next'>) => {
|
||||
const response = await (pageParam?.next ? pageParam.next() : entityFn()) as PaginatedResponse<BaseAccount>;
|
||||
|
||||
return {
|
||||
...response,
|
||||
items: response.items.map(normalizeAccount),
|
||||
};
|
||||
};
|
||||
|
||||
const queryInfo = useInfiniteQuery({
|
||||
queryKey: [Entities.ACCOUNTS, ...listKey],
|
||||
queryFn: ({ pageParam }) => getAccounts(pageParam),
|
||||
enabled: true,
|
||||
initialPageParam: { next: null as (() => Promise<PaginatedResponse<BaseAccount>>) | null },
|
||||
getNextPageParam: (config) => config.next ? config : undefined,
|
||||
});
|
||||
|
||||
const data = flattenPages<Account>(queryInfo.data as any)?.toReversed() || [];
|
||||
|
||||
const { relationships } = useRelationships(
|
||||
listKey,
|
||||
data.map(({ id }) => id),
|
||||
);
|
||||
|
||||
const accounts = data.map((account) => ({
|
||||
...account,
|
||||
relationship: relationships[account.id],
|
||||
}));
|
||||
|
||||
return { accounts, ...queryInfo };
|
||||
};
|
||||
|
||||
const useBlocks = () => {
|
||||
const client = useClient();
|
||||
return useAccountList(['blocks'], () => client.filtering.getBlocks());
|
||||
};
|
||||
|
||||
const useMutes = () => {
|
||||
const client = useClient();
|
||||
return useAccountList(['mutes'], () => client.filtering.getMutes());
|
||||
};
|
||||
|
||||
const useFollowing = (accountId: string | undefined) => {
|
||||
const client = useClient();
|
||||
|
||||
return useAccountList(
|
||||
[accountId!, 'following'],
|
||||
() => client.accounts.getAccountFollowing(accountId!),
|
||||
);
|
||||
};
|
||||
|
||||
const useFollowers = (accountId: string | undefined) => {
|
||||
const client = useClient();
|
||||
|
||||
return useAccountList(
|
||||
[accountId!, 'followers'],
|
||||
() => client.accounts.getAccountFollowers(accountId!),
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
useAccountList,
|
||||
useBlocks,
|
||||
useMutes,
|
||||
useFollowing,
|
||||
useFollowers,
|
||||
};
|
||||
54
packages/pl-fe/src/api/hooks/accounts/useAccountLookup.ts
Normal file
54
packages/pl-fe/src/api/hooks/accounts/useAccountLookup.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { accountSchema, type Account as BaseAccount } from 'pl-api';
|
||||
import { useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntityLookup } from 'soapbox/entity-store/hooks';
|
||||
import { useClient, useFeatures, useLoggedIn } from 'soapbox/hooks';
|
||||
import { type Account, normalizeAccount } from 'soapbox/normalizers';
|
||||
|
||||
import { useRelationship } from './useRelationship';
|
||||
|
||||
interface UseAccountLookupOpts {
|
||||
withRelationship?: boolean;
|
||||
}
|
||||
|
||||
const useAccountLookup = (acct: string | undefined, opts: UseAccountLookupOpts = {}) => {
|
||||
const client = useClient();
|
||||
const features = useFeatures();
|
||||
const history = useHistory();
|
||||
const { me } = useLoggedIn();
|
||||
const { withRelationship } = opts;
|
||||
|
||||
const { entity: account, isUnauthorized, ...result } = useEntityLookup<BaseAccount, Account>(
|
||||
Entities.ACCOUNTS,
|
||||
(account) => account.acct.toLowerCase() === acct?.toLowerCase(),
|
||||
() => client.accounts.lookupAccount(acct!),
|
||||
{ schema: accountSchema, enabled: !!acct, transform: normalizeAccount },
|
||||
);
|
||||
|
||||
const {
|
||||
relationship,
|
||||
isLoading: isRelationshipLoading,
|
||||
} = useRelationship(account?.id, { enabled: withRelationship });
|
||||
|
||||
const isBlocked = account?.relationship?.blocked_by === true;
|
||||
const isUnavailable = (me === account?.id) ? false : (isBlocked && !features.blockersVisible);
|
||||
|
||||
useEffect(() => {
|
||||
if (isUnauthorized) {
|
||||
history.push('/login');
|
||||
}
|
||||
}, [isUnauthorized]);
|
||||
|
||||
return {
|
||||
...result,
|
||||
isLoading: result.isLoading,
|
||||
isRelationshipLoading,
|
||||
isUnauthorized,
|
||||
isUnavailable,
|
||||
account: account ? { ...account, relationship } : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export { useAccountLookup };
|
||||
85
packages/pl-fe/src/api/hooks/accounts/useFollow.ts
Normal file
85
packages/pl-fe/src/api/hooks/accounts/useFollow.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { importEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useTransaction } from 'soapbox/entity-store/hooks';
|
||||
import { useAppDispatch, useClient, useLoggedIn } from 'soapbox/hooks';
|
||||
|
||||
interface FollowOpts {
|
||||
reblogs?: boolean;
|
||||
notify?: boolean;
|
||||
languages?: string[];
|
||||
}
|
||||
|
||||
const useFollow = () => {
|
||||
const client = useClient();
|
||||
const dispatch = useAppDispatch();
|
||||
const { isLoggedIn } = useLoggedIn();
|
||||
const { transaction } = useTransaction();
|
||||
|
||||
const followEffect = (accountId: string) => {
|
||||
transaction({
|
||||
Accounts: {
|
||||
[accountId]: (account) => ({
|
||||
...account,
|
||||
followers_count: account.followers_count + 1,
|
||||
}),
|
||||
},
|
||||
Relationships: {
|
||||
[accountId]: (relationship) => ({
|
||||
...relationship,
|
||||
following: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const unfollowEffect = (accountId: string) => {
|
||||
transaction({
|
||||
Accounts: {
|
||||
[accountId]: (account) => ({
|
||||
...account,
|
||||
followers_count: Math.max(0, account.followers_count - 1),
|
||||
}),
|
||||
},
|
||||
Relationships: {
|
||||
[accountId]: (relationship) => ({
|
||||
...relationship,
|
||||
following: false,
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const follow = async (accountId: string, options: FollowOpts = {}) => {
|
||||
if (!isLoggedIn) return;
|
||||
followEffect(accountId);
|
||||
|
||||
try {
|
||||
const response = await client.accounts.followAccount(accountId, options);
|
||||
if (response.id) {
|
||||
dispatch(importEntities([response], Entities.RELATIONSHIPS));
|
||||
}
|
||||
} catch (e) {
|
||||
unfollowEffect(accountId);
|
||||
}
|
||||
};
|
||||
|
||||
const unfollow = async (accountId: string) => {
|
||||
if (!isLoggedIn) return;
|
||||
unfollowEffect(accountId);
|
||||
|
||||
try {
|
||||
await client.accounts.unfollowAccount(accountId);
|
||||
} catch (e) {
|
||||
followEffect(accountId);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
follow,
|
||||
unfollow,
|
||||
followEffect,
|
||||
unfollowEffect,
|
||||
};
|
||||
};
|
||||
|
||||
export { useFollow };
|
||||
28
packages/pl-fe/src/api/hooks/accounts/useRelationship.ts
Normal file
28
packages/pl-fe/src/api/hooks/accounts/useRelationship.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { type Relationship, relationshipSchema } from 'pl-api';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
|
||||
interface UseRelationshipOpts {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
const useRelationship = (accountId: string | undefined, opts: UseRelationshipOpts = {}) => {
|
||||
const client = useClient();
|
||||
const { enabled = false } = opts;
|
||||
|
||||
const { entity: relationship, ...result } = useEntity<Relationship>(
|
||||
[Entities.RELATIONSHIPS, accountId!],
|
||||
() => client.accounts.getRelationships([accountId!]),
|
||||
{
|
||||
enabled: enabled && !!accountId,
|
||||
schema: z.array(relationshipSchema).nonempty().transform(arr => arr[0]),
|
||||
},
|
||||
);
|
||||
|
||||
return { relationship, ...result };
|
||||
};
|
||||
|
||||
export { useRelationship };
|
||||
23
packages/pl-fe/src/api/hooks/accounts/useRelationships.ts
Normal file
23
packages/pl-fe/src/api/hooks/accounts/useRelationships.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { type Relationship, relationshipSchema } from 'pl-api';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useBatchedEntities } from 'soapbox/entity-store/hooks/useBatchedEntities';
|
||||
import { useClient, useLoggedIn } from 'soapbox/hooks';
|
||||
|
||||
const useRelationships = (listKey: string[], accountIds: string[]) => {
|
||||
const client = useClient();
|
||||
const { isLoggedIn } = useLoggedIn();
|
||||
|
||||
const fetchRelationships = (accountIds: string[]) => client.accounts.getRelationships(accountIds);
|
||||
|
||||
const { entityMap: relationships, ...result } = useBatchedEntities<Relationship>(
|
||||
[Entities.RELATIONSHIPS, ...listKey],
|
||||
accountIds,
|
||||
fetchRelationships,
|
||||
{ schema: relationshipSchema, enabled: isLoggedIn },
|
||||
);
|
||||
|
||||
return { relationships, ...result };
|
||||
};
|
||||
|
||||
export { useRelationships };
|
||||
6
packages/pl-fe/src/api/hooks/admin/index.ts
Normal file
6
packages/pl-fe/src/api/hooks/admin/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { useDomains } from './useDomains';
|
||||
export { useModerationLog } from './useModerationLog';
|
||||
export { useRelays } from './useRelays';
|
||||
export { useRules } from './useRules';
|
||||
export { useSuggest } from './useSuggest';
|
||||
export { useVerify } from './useVerify';
|
||||
91
packages/pl-fe/src/api/hooks/admin/useAnnouncements.ts
Normal file
91
packages/pl-fe/src/api/hooks/admin/useAnnouncements.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { adminAnnouncementSchema, type AdminAnnouncement } from 'soapbox/schemas';
|
||||
|
||||
import { useAnnouncements as useUserAnnouncements } from '../announcements';
|
||||
|
||||
interface CreateAnnouncementParams {
|
||||
content: string;
|
||||
starts_at?: string | null;
|
||||
ends_at?: string | null;
|
||||
all_day?: boolean;
|
||||
}
|
||||
|
||||
interface UpdateAnnouncementParams extends CreateAnnouncementParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const useAnnouncements = () => {
|
||||
const client = useClient();
|
||||
const userAnnouncements = useUserAnnouncements();
|
||||
|
||||
const getAnnouncements = async () => {
|
||||
const { json: data } = await client.request<AdminAnnouncement[]>('/api/v1/pleroma/admin/announcements');
|
||||
|
||||
const normalizedData = data.map((announcement) => adminAnnouncementSchema.parse(announcement));
|
||||
return normalizedData;
|
||||
};
|
||||
|
||||
const result = useQuery<ReadonlyArray<AdminAnnouncement>>({
|
||||
queryKey: ['admin', 'announcements'],
|
||||
queryFn: getAnnouncements,
|
||||
placeholderData: [],
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: createAnnouncement,
|
||||
isPending: isCreating,
|
||||
} = useMutation({
|
||||
mutationFn: (params: CreateAnnouncementParams) => client.request('/api/v1/pleroma/admin/announcements', {
|
||||
method: 'POST', body: params,
|
||||
}),
|
||||
retry: false,
|
||||
onSuccess: ({ json: data }) =>
|
||||
queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
|
||||
[...prevResult, adminAnnouncementSchema.parse(data)],
|
||||
),
|
||||
onSettled: () => userAnnouncements.refetch(),
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: updateAnnouncement,
|
||||
isPending: isUpdating,
|
||||
} = useMutation({
|
||||
mutationFn: ({ id, ...params }: UpdateAnnouncementParams) => client.request(`/api/v1/pleroma/admin/announcements/${id}`, {
|
||||
method: 'PATCH', body: params,
|
||||
}),
|
||||
retry: false,
|
||||
onSuccess: ({ json: data }) =>
|
||||
queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
|
||||
prevResult.map((announcement) => announcement.id === data.id ? adminAnnouncementSchema.parse(data) : announcement),
|
||||
),
|
||||
onSettled: () => userAnnouncements.refetch(),
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: deleteAnnouncement,
|
||||
isPending: isDeleting,
|
||||
} = useMutation({
|
||||
mutationFn: (id: string) => client.request(`/api/v1/pleroma/admin/announcements/${id}`, { method: 'DELETE' }),
|
||||
retry: false,
|
||||
onSuccess: (_, id) =>
|
||||
queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
|
||||
prevResult.filter(({ id: announcementId }) => announcementId !== id),
|
||||
),
|
||||
onSettled: () => userAnnouncements.refetch(),
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
createAnnouncement,
|
||||
isCreating,
|
||||
updateAnnouncement,
|
||||
isUpdating,
|
||||
deleteAnnouncement,
|
||||
isDeleting,
|
||||
};
|
||||
};
|
||||
|
||||
export { useAnnouncements };
|
||||
84
packages/pl-fe/src/api/hooks/admin/useDomains.ts
Normal file
84
packages/pl-fe/src/api/hooks/admin/useDomains.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { domainSchema, type Domain } from 'soapbox/schemas';
|
||||
|
||||
interface CreateDomainParams {
|
||||
domain: string;
|
||||
public: boolean;
|
||||
}
|
||||
|
||||
interface UpdateDomainParams {
|
||||
id: string;
|
||||
public: boolean;
|
||||
}
|
||||
|
||||
const useDomains = () => {
|
||||
const client = useClient();
|
||||
|
||||
const getDomains = async () => {
|
||||
const { json: data } = await client.request<Domain[]>('/api/v1/pleroma/admin/domains');
|
||||
|
||||
const normalizedData = data.map((domain) => domainSchema.parse(domain));
|
||||
return normalizedData;
|
||||
};
|
||||
|
||||
const result = useQuery<ReadonlyArray<Domain>>({
|
||||
queryKey: ['admin', 'domains'],
|
||||
queryFn: getDomains,
|
||||
placeholderData: [],
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: createDomain,
|
||||
isPending: isCreating,
|
||||
} = useMutation({
|
||||
mutationFn: (params: CreateDomainParams) => client.request('/api/v1/pleroma/admin/domains', {
|
||||
method: 'POST', body: params,
|
||||
}),
|
||||
retry: false,
|
||||
onSuccess: ({ data }) =>
|
||||
queryClient.setQueryData(['admin', 'domains'], (prevResult: ReadonlyArray<Domain>) =>
|
||||
[...prevResult, domainSchema.parse(data)],
|
||||
),
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: updateDomain,
|
||||
isPending: isUpdating,
|
||||
} = useMutation({
|
||||
mutationFn: ({ id, ...params }: UpdateDomainParams) => client.request(`/api/v1/pleroma/admin/domains/${id}`, {
|
||||
method: 'PATCH', body: params,
|
||||
}),
|
||||
retry: false,
|
||||
onSuccess: ({ json: data }) =>
|
||||
queryClient.setQueryData(['admin', 'domains'], (prevResult: ReadonlyArray<Domain>) =>
|
||||
prevResult.map((domain) => domain.id === data.id ? domainSchema.parse(data) : domain),
|
||||
),
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: deleteDomain,
|
||||
isPending: isDeleting,
|
||||
} = useMutation({
|
||||
mutationFn: (id: string) => client.request(`/api/v1/pleroma/admin/domains/${id}`, { method: 'DELETE' }),
|
||||
retry: false,
|
||||
onSuccess: (_, id) =>
|
||||
queryClient.setQueryData(['admin', 'domains'], (prevResult: ReadonlyArray<Domain>) =>
|
||||
prevResult.filter(({ id: domainId }) => domainId !== id),
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
createDomain,
|
||||
isCreating,
|
||||
updateDomain,
|
||||
isUpdating,
|
||||
deleteDomain,
|
||||
isDeleting,
|
||||
};
|
||||
};
|
||||
|
||||
export { useDomains };
|
||||
42
packages/pl-fe/src/api/hooks/admin/useModerationLog.ts
Normal file
42
packages/pl-fe/src/api/hooks/admin/useModerationLog.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { moderationLogEntrySchema, type ModerationLogEntry } from 'soapbox/schemas';
|
||||
|
||||
interface ModerationLogResult {
|
||||
items: ModerationLogEntry[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
const flattenPages = (pages?: ModerationLogResult[]): ModerationLogEntry[] => (pages || []).map(({ items }) => items).flat();
|
||||
|
||||
const useModerationLog = () => {
|
||||
const client = useClient();
|
||||
|
||||
const getModerationLog = async (page: number): Promise<ModerationLogResult> => {
|
||||
const { json: data } = await client.request<ModerationLogResult>('/api/v1/pleroma/admin/moderation_log', { params: { page } });
|
||||
|
||||
const normalizedData = data.items.map((domain) => moderationLogEntrySchema.parse(domain));
|
||||
|
||||
return {
|
||||
items: normalizedData,
|
||||
total: data.total,
|
||||
};
|
||||
};
|
||||
|
||||
const queryInfo = useInfiniteQuery({
|
||||
queryKey: ['admin', 'moderation_log'],
|
||||
queryFn: ({ pageParam }) => getModerationLog(pageParam),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (page, allPages) => flattenPages(allPages)!.length >= page.total ? undefined : allPages.length + 1,
|
||||
});
|
||||
|
||||
const data = flattenPages(queryInfo.data?.pages);
|
||||
|
||||
return {
|
||||
...queryInfo,
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
export { useModerationLog };
|
||||
62
packages/pl-fe/src/api/hooks/admin/useRelays.ts
Normal file
62
packages/pl-fe/src/api/hooks/admin/useRelays.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { relaySchema, type Relay } from 'soapbox/schemas';
|
||||
|
||||
const useRelays = () => {
|
||||
const client = useClient();
|
||||
|
||||
const getRelays = async () => {
|
||||
const { json: data } = await client.request<{ relays: Relay[] }>('/api/v1/pleroma/admin/relay');
|
||||
|
||||
const normalizedData = data.relays?.map((relay) => relaySchema.parse(relay));
|
||||
return normalizedData;
|
||||
};
|
||||
|
||||
const result = useQuery<ReadonlyArray<Relay>>({
|
||||
queryKey: ['admin', 'relays'],
|
||||
queryFn: getRelays,
|
||||
placeholderData: [],
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: followRelay,
|
||||
isPending: isPendingFollow,
|
||||
} = useMutation({
|
||||
mutationFn: (relayUrl: string) => client.request('/api/v1/pleroma/admin/relays', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ relay_url: relayUrl }),
|
||||
}),
|
||||
retry: false,
|
||||
onSuccess: ({ json: data }) =>
|
||||
queryClient.setQueryData(['admin', 'relays'], (prevResult: ReadonlyArray<Relay>) =>
|
||||
[...prevResult, relaySchema.parse(data)],
|
||||
),
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: unfollowRelay,
|
||||
isPending: isPendingUnfollow,
|
||||
} = useMutation({
|
||||
mutationFn: (relayUrl: string) => client.request('/api/v1/pleroma/admin/relays', {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({ relay_url: relayUrl }),
|
||||
}),
|
||||
retry: false,
|
||||
onSuccess: (_, relayUrl) =>
|
||||
queryClient.setQueryData(['admin', 'relays'], (prevResult: ReadonlyArray<Relay>) =>
|
||||
prevResult.filter(({ actor }) => actor !== relayUrl),
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
followRelay,
|
||||
isPendingFollow,
|
||||
unfollowRelay,
|
||||
isPendingUnfollow,
|
||||
};
|
||||
};
|
||||
|
||||
export { useRelays };
|
||||
87
packages/pl-fe/src/api/hooks/admin/useRules.ts
Normal file
87
packages/pl-fe/src/api/hooks/admin/useRules.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { adminRuleSchema, type AdminRule } from 'soapbox/schemas';
|
||||
|
||||
interface CreateRuleParams {
|
||||
priority?: number;
|
||||
text: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
interface UpdateRuleParams {
|
||||
id: string;
|
||||
priority?: number;
|
||||
text?: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
const useRules = () => {
|
||||
const client = useClient();
|
||||
|
||||
const getRules = async () => {
|
||||
const { json: data } = await client.request<AdminRule[]>('/api/v1/pleroma/admin/rules');
|
||||
|
||||
const normalizedData = data.map((rule) => adminRuleSchema.parse(rule));
|
||||
return normalizedData;
|
||||
};
|
||||
|
||||
const result = useQuery<ReadonlyArray<AdminRule>>({
|
||||
queryKey: ['admin', 'rules'],
|
||||
queryFn: getRules,
|
||||
placeholderData: [],
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: createRule,
|
||||
isPending: isCreating,
|
||||
} = useMutation({
|
||||
mutationFn: (params: CreateRuleParams) => client.request('/api/v1/pleroma/admin/rules', {
|
||||
method: 'POST', body: params,
|
||||
}),
|
||||
retry: false,
|
||||
onSuccess: ({ json: data }) =>
|
||||
queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray<AdminRule>) =>
|
||||
[...prevResult, adminRuleSchema.parse(data)],
|
||||
),
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: updateRule,
|
||||
isPending: isUpdating,
|
||||
} = useMutation({
|
||||
mutationFn: ({ id, ...params }: UpdateRuleParams) => client.request(`/api/v1/pleroma/admin/rules/${id}`, {
|
||||
method: 'PATCH', body: params,
|
||||
}),
|
||||
retry: false,
|
||||
onSuccess: ({ json: data }) =>
|
||||
queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray<AdminRule>) =>
|
||||
prevResult.map((rule) => rule.id === data.id ? adminRuleSchema.parse(data) : rule),
|
||||
),
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: deleteRule,
|
||||
isPending: isDeleting,
|
||||
} = useMutation({
|
||||
mutationFn: (id: string) => client.request(`/api/v1/pleroma/admin/rules/${id}`, { method: 'DELETE' }),
|
||||
retry: false,
|
||||
onSuccess: (_, id) =>
|
||||
queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray<AdminRule>) =>
|
||||
prevResult.filter(({ id: ruleId }) => ruleId !== id),
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
createRule,
|
||||
isCreating,
|
||||
updateRule,
|
||||
isUpdating,
|
||||
deleteRule,
|
||||
isDeleting,
|
||||
};
|
||||
};
|
||||
|
||||
export { useRules };
|
||||
60
packages/pl-fe/src/api/hooks/admin/useSuggest.ts
Normal file
60
packages/pl-fe/src/api/hooks/admin/useSuggest.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useTransaction } from 'soapbox/entity-store/hooks';
|
||||
import { EntityCallbacks } from 'soapbox/entity-store/hooks/types';
|
||||
import { useClient, useGetState } from 'soapbox/hooks';
|
||||
import { accountIdsToAccts } from 'soapbox/selectors';
|
||||
|
||||
import type { Account } from 'soapbox/normalizers';
|
||||
|
||||
const useSuggest = () => {
|
||||
const client = useClient();
|
||||
const getState = useGetState();
|
||||
const { transaction } = useTransaction();
|
||||
|
||||
const suggestEffect = (accountIds: string[], suggested: boolean) => {
|
||||
const updater = (account: Account): Account => {
|
||||
account.is_suggested = suggested;
|
||||
return account;
|
||||
};
|
||||
|
||||
transaction({
|
||||
Accounts: accountIds.reduce<Record<string, (account: Account) => Account>>(
|
||||
(result, id) => ({ ...result, [id]: updater }),
|
||||
{}),
|
||||
});
|
||||
};
|
||||
|
||||
const suggest = async (accountIds: string[], callbacks?: EntityCallbacks<void, unknown>) => {
|
||||
const accts = accountIdsToAccts(getState(), accountIds);
|
||||
suggestEffect(accountIds, true);
|
||||
try {
|
||||
await client.request('/api/v1/pleroma/admin/users/suggest', {
|
||||
method: 'PATCH', body: { nicknames: accts },
|
||||
});
|
||||
callbacks?.onSuccess?.();
|
||||
} catch (e) {
|
||||
callbacks?.onError?.(e);
|
||||
suggestEffect(accountIds, false);
|
||||
}
|
||||
};
|
||||
|
||||
const unsuggest = async (accountIds: string[], callbacks?: EntityCallbacks<void, unknown>) => {
|
||||
const accts = accountIdsToAccts(getState(), accountIds);
|
||||
suggestEffect(accountIds, false);
|
||||
try {
|
||||
await client.request('/api/v1/pleroma/admin/users/unsuggest', {
|
||||
method: 'PATCH', body: { nicknames: accts },
|
||||
});
|
||||
callbacks?.onSuccess?.();
|
||||
} catch (e) {
|
||||
callbacks?.onError?.(e);
|
||||
suggestEffect(accountIds, true);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
suggest,
|
||||
unsuggest,
|
||||
};
|
||||
};
|
||||
|
||||
export { useSuggest };
|
||||
69
packages/pl-fe/src/api/hooks/admin/useVerify.ts
Normal file
69
packages/pl-fe/src/api/hooks/admin/useVerify.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useTransaction } from 'soapbox/entity-store/hooks';
|
||||
import { EntityCallbacks } from 'soapbox/entity-store/hooks/types';
|
||||
import { useClient, useGetState } from 'soapbox/hooks';
|
||||
import { accountIdsToAccts } from 'soapbox/selectors';
|
||||
|
||||
import type { Account } from 'soapbox/normalizers';
|
||||
|
||||
const useVerify = () => {
|
||||
const client = useClient();
|
||||
const getState = useGetState();
|
||||
const { transaction } = useTransaction();
|
||||
|
||||
const verifyEffect = (accountIds: string[], verified: boolean) => {
|
||||
const updater = (account: Account): Account => {
|
||||
if (account.__meta.pleroma) {
|
||||
const tags = account.__meta.pleroma.tags.filter((tag: string) => tag !== 'verified');
|
||||
if (verified) {
|
||||
tags.push('verified');
|
||||
}
|
||||
account.__meta.pleroma.tags = tags;
|
||||
}
|
||||
account.verified = verified;
|
||||
return account;
|
||||
};
|
||||
|
||||
transaction({
|
||||
Accounts: accountIds.reduce<Record<string, (account: Account) => Account>>(
|
||||
(result, id) => ({ ...result, [id]: updater }),
|
||||
{}),
|
||||
});
|
||||
};
|
||||
|
||||
const verify = async (accountIds: string[], callbacks?: EntityCallbacks<void, unknown>) => {
|
||||
const accts = accountIdsToAccts(getState(), accountIds);
|
||||
verifyEffect(accountIds, true);
|
||||
try {
|
||||
await client.request('/api/v1/pleroma/admin/users/tag', {
|
||||
method: 'PUT',
|
||||
body: { nicknames: accts, tags: ['verified'] },
|
||||
});
|
||||
callbacks?.onSuccess?.();
|
||||
} catch (e) {
|
||||
callbacks?.onError?.(e);
|
||||
verifyEffect(accountIds, false);
|
||||
}
|
||||
};
|
||||
|
||||
const unverify = async (accountIds: string[], callbacks?: EntityCallbacks<void, unknown>) => {
|
||||
const accts = accountIdsToAccts(getState(), accountIds);
|
||||
verifyEffect(accountIds, false);
|
||||
try {
|
||||
await client.request('/api/v1/pleroma/admin/users/tag', {
|
||||
method: 'DELETE',
|
||||
body: { nicknames: accts, tags: ['verified'] },
|
||||
});
|
||||
callbacks?.onSuccess?.();
|
||||
} catch (e) {
|
||||
callbacks?.onError?.(e);
|
||||
verifyEffect(accountIds, true);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
verify,
|
||||
unverify,
|
||||
};
|
||||
};
|
||||
|
||||
export { useVerify };
|
||||
1
packages/pl-fe/src/api/hooks/announcements/index.ts
Normal file
1
packages/pl-fe/src/api/hooks/announcements/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useAnnouncements } from './useAnnouncements';
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { announcementReactionSchema, type AnnouncementReaction } from 'pl-api';
|
||||
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { type Announcement, normalizeAnnouncement } from 'soapbox/normalizers';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
|
||||
const updateReaction = (reaction: AnnouncementReaction, count: number, me?: boolean, overwrite?: boolean) => announcementReactionSchema.parse({
|
||||
...reaction,
|
||||
me: typeof me === 'boolean' ? me : reaction.me,
|
||||
count: overwrite ? count : (reaction.count + count),
|
||||
});
|
||||
|
||||
const updateReactions = (reactions: AnnouncementReaction[], name: string, count: number, me?: boolean, overwrite?: boolean) => {
|
||||
const idx = reactions.findIndex(reaction => reaction.name === name);
|
||||
|
||||
if (idx > -1) {
|
||||
reactions = reactions.map(reaction => reaction.name === name ? updateReaction(reaction, count, me, overwrite) : reaction);
|
||||
}
|
||||
|
||||
return [...reactions, updateReaction(announcementReactionSchema.parse({ name }), count, me, overwrite)];
|
||||
};
|
||||
|
||||
const useAnnouncements = () => {
|
||||
const client = useClient();
|
||||
|
||||
const getAnnouncements = async () => {
|
||||
const data = await client.announcements.getAnnouncements();
|
||||
return data.map(normalizeAnnouncement);
|
||||
};
|
||||
|
||||
const { data, ...result } = useQuery<ReadonlyArray<Announcement>>({
|
||||
queryKey: ['announcements'],
|
||||
queryFn: getAnnouncements,
|
||||
placeholderData: [],
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: addReaction,
|
||||
} = useMutation({
|
||||
mutationFn: ({ announcementId, name }: { announcementId: string; name: string }) =>
|
||||
client.announcements.addAnnouncementReaction(announcementId, name),
|
||||
retry: false,
|
||||
onMutate: ({ announcementId: id, name }) => {
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.map(value => value.id !== id ? value : {
|
||||
...value,
|
||||
reactions: updateReactions(value.reactions, name, 1, true),
|
||||
}),
|
||||
);
|
||||
},
|
||||
onError: (_, { announcementId: id, name }) => {
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.map(value => value.id !== id ? value : {
|
||||
...value,
|
||||
reactions: updateReactions(value.reactions, name, -1, false),
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: removeReaction,
|
||||
} = useMutation({
|
||||
mutationFn: ({ announcementId, name }: { announcementId: string; name: string }) =>
|
||||
client.announcements.deleteAnnouncementReaction(announcementId, name),
|
||||
retry: false,
|
||||
onMutate: ({ announcementId: id, name }) => {
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.map(value => value.id !== id ? value : {
|
||||
...value,
|
||||
reactions: updateReactions(value.reactions, name, -1, false),
|
||||
}),
|
||||
);
|
||||
},
|
||||
onError: (_, { announcementId: id, name }) => {
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.map(value => value.id !== id ? value : {
|
||||
...value,
|
||||
reactions: updateReactions(value.reactions, name, 1, true),
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data: data ? [...data].sort(compareAnnouncements) : undefined,
|
||||
...result,
|
||||
addReaction,
|
||||
removeReaction,
|
||||
};
|
||||
};
|
||||
|
||||
const compareAnnouncements = (a: Announcement, b: Announcement): number =>
|
||||
new Date(a.starts_at || a.published_at).getDate() - new Date(b.starts_at || b.published_at).getDate();
|
||||
|
||||
export { updateReactions, useAnnouncements };
|
||||
19
packages/pl-fe/src/api/hooks/groups/useBlockGroupMember.ts
Normal file
19
packages/pl-fe/src/api/hooks/groups/useBlockGroupMember.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useCreateEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
|
||||
import type { Group } from 'pl-api';
|
||||
import type { Account } from 'soapbox/normalizers';
|
||||
|
||||
const useBlockGroupMember = (group: Pick<Group, 'id'>, account: Pick<Account, 'id'>) => {
|
||||
const client = useClient();
|
||||
|
||||
const { createEntity } = useCreateEntity(
|
||||
[Entities.GROUP_MEMBERSHIPS, account.id],
|
||||
(accountIds: string[]) => client.experimental.groups.blockGroupUsers(group.id, accountIds),
|
||||
);
|
||||
|
||||
return createEntity;
|
||||
};
|
||||
|
||||
export { useBlockGroupMember };
|
||||
33
packages/pl-fe/src/api/hooks/groups/useCreateGroup.ts
Normal file
33
packages/pl-fe/src/api/hooks/groups/useCreateGroup.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { groupSchema, type Group as BaseGroup } from 'pl-api';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useCreateEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { normalizeGroup, type Group } from 'soapbox/normalizers';
|
||||
|
||||
interface CreateGroupParams {
|
||||
display_name: string;
|
||||
note?: string;
|
||||
avatar?: File;
|
||||
header?: File;
|
||||
group_visibility?: 'members_only' | 'everyone';
|
||||
discoverable?: boolean;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
const useCreateGroup = () => {
|
||||
const client = useClient();
|
||||
|
||||
const { createEntity, ...rest } = useCreateEntity<BaseGroup, Group, CreateGroupParams>(
|
||||
[Entities.GROUPS, 'search', ''],
|
||||
(params: CreateGroupParams) => client.experimental.groups.createGroup(params),
|
||||
{ schema: groupSchema, transform: normalizeGroup },
|
||||
);
|
||||
|
||||
return {
|
||||
createGroup: createEntity,
|
||||
...rest,
|
||||
};
|
||||
};
|
||||
|
||||
export { useCreateGroup, type CreateGroupParams };
|
||||
19
packages/pl-fe/src/api/hooks/groups/useDeleteGroup.ts
Normal file
19
packages/pl-fe/src/api/hooks/groups/useDeleteGroup.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useDeleteEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
|
||||
const useDeleteGroup = () => {
|
||||
const client = useClient();
|
||||
|
||||
const { deleteEntity, isSubmitting } = useDeleteEntity(
|
||||
Entities.GROUPS,
|
||||
(groupId: string) => client.experimental.groups.deleteGroup(groupId),
|
||||
);
|
||||
|
||||
return {
|
||||
mutate: deleteEntity,
|
||||
isSubmitting,
|
||||
};
|
||||
};
|
||||
|
||||
export { useDeleteGroup };
|
||||
20
packages/pl-fe/src/api/hooks/groups/useDeleteGroupStatus.ts
Normal file
20
packages/pl-fe/src/api/hooks/groups/useDeleteGroupStatus.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useDeleteEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
|
||||
import type { Group } from 'pl-api';
|
||||
|
||||
const useDeleteGroupStatus = (group: Pick<Group, 'id'>, statusId: string) => {
|
||||
const client = useClient();
|
||||
const { deleteEntity, isSubmitting } = useDeleteEntity(
|
||||
Entities.STATUSES,
|
||||
() => client.experimental.groups.deleteGroupStatus(group.id, statusId),
|
||||
);
|
||||
|
||||
return {
|
||||
mutate: deleteEntity,
|
||||
isSubmitting,
|
||||
};
|
||||
};
|
||||
|
||||
export { useDeleteGroupStatus };
|
||||
21
packages/pl-fe/src/api/hooks/groups/useDemoteGroupMember.ts
Normal file
21
packages/pl-fe/src/api/hooks/groups/useDemoteGroupMember.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { groupMemberSchema, type Group, type GroupMember as GroupMember, type GroupRole } from 'pl-api';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useCreateEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { normalizeGroupMember } from 'soapbox/normalizers';
|
||||
|
||||
const useDemoteGroupMember = (group: Pick<Group, 'id'>, groupMember: Pick<GroupMember, 'id'>) => {
|
||||
const client = useClient();
|
||||
|
||||
const { createEntity } = useCreateEntity(
|
||||
[Entities.GROUP_MEMBERSHIPS, groupMember.id],
|
||||
({ account_ids, role }: { account_ids: string[]; role: GroupRole }) => client.experimental.groups.demoteGroupUsers(group.id, account_ids, role),
|
||||
{ schema: z.array(groupMemberSchema).transform((arr) => arr[0]), transform: normalizeGroupMember },
|
||||
);
|
||||
|
||||
return createEntity;
|
||||
};
|
||||
|
||||
export { useDemoteGroupMember };
|
||||
41
packages/pl-fe/src/api/hooks/groups/useGroup.test.ts
Normal file
41
packages/pl-fe/src/api/hooks/groups/useGroup.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { buildGroup } from 'soapbox/jest/factory';
|
||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import { useGroup } from './useGroup';
|
||||
|
||||
const group = buildGroup({ id: '1', display_name: 'soapbox' });
|
||||
|
||||
describe('useGroup hook', () => {
|
||||
describe('with a successful request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/groups/${group.id}`).reply(200, group);
|
||||
});
|
||||
});
|
||||
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(() => useGroup(group.id));
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.group?.id).toBe(group.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful query', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/groups/${group.id}`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('is has error state', async() => {
|
||||
const { result } = renderHook(() => useGroup(group.id));
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.group).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
41
packages/pl-fe/src/api/hooks/groups/useGroup.ts
Normal file
41
packages/pl-fe/src/api/hooks/groups/useGroup.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { type Group as BaseGroup, groupSchema } from 'pl-api';
|
||||
import { useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { normalizeGroup, type Group } from 'soapbox/normalizers';
|
||||
|
||||
import { useGroupRelationship } from './useGroupRelationship';
|
||||
|
||||
const useGroup = (groupId: string, refetch = true) => {
|
||||
const client = useClient();
|
||||
const history = useHistory();
|
||||
|
||||
const { entity: group, isUnauthorized, ...result } = useEntity<BaseGroup, Group>(
|
||||
[Entities.GROUPS, groupId],
|
||||
() => client.experimental.groups.getGroup(groupId),
|
||||
{
|
||||
schema: groupSchema,
|
||||
transform: normalizeGroup,
|
||||
refetch,
|
||||
enabled: !!groupId,
|
||||
},
|
||||
);
|
||||
const { groupRelationship: relationship } = useGroupRelationship(groupId);
|
||||
|
||||
useEffect(() => {
|
||||
if (isUnauthorized) {
|
||||
history.push('/login');
|
||||
}
|
||||
}, [isUnauthorized]);
|
||||
|
||||
return {
|
||||
...result,
|
||||
isUnauthorized,
|
||||
group: group ? { ...group, relationship: relationship || null } : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export { useGroup };
|
||||
44
packages/pl-fe/src/api/hooks/groups/useGroupMedia.test.ts
Normal file
44
packages/pl-fe/src/api/hooks/groups/useGroupMedia.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { buildStatus } from 'soapbox/jest/factory';
|
||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import { useGroupMedia } from './useGroupMedia';
|
||||
|
||||
const status = buildStatus();
|
||||
const groupId = '1';
|
||||
|
||||
describe('useGroupMedia hook', () => {
|
||||
describe('with a successful request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/timelines/group/${groupId}?only_media=true`).reply(200, [status]);
|
||||
});
|
||||
});
|
||||
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(() => useGroupMedia(groupId));
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.entities.length).toBe(1);
|
||||
expect(result.current.entities[0].id).toBe(status.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful query', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/timelines/group/${groupId}?only_media=true`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('is has error state', async() => {
|
||||
const { result } = renderHook(() => useGroupMedia(groupId));
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.entities.length).toBe(0);
|
||||
expect(result.current.isError).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
18
packages/pl-fe/src/api/hooks/groups/useGroupMedia.ts
Normal file
18
packages/pl-fe/src/api/hooks/groups/useGroupMedia.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { statusSchema } from 'pl-api';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { normalizeStatus } from 'soapbox/normalizers';
|
||||
|
||||
const useGroupMedia = (groupId: string) => {
|
||||
const client = useClient();
|
||||
|
||||
return useEntities(
|
||||
[Entities.STATUSES, 'groupMedia', groupId],
|
||||
() => client.timelines.groupTimeline(groupId, { only_media: true }),
|
||||
{ schema: statusSchema, transform: normalizeStatus })
|
||||
;
|
||||
};
|
||||
|
||||
export { useGroupMedia };
|
||||
46
packages/pl-fe/src/api/hooks/groups/useGroupMembers.test.ts
Normal file
46
packages/pl-fe/src/api/hooks/groups/useGroupMembers.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { GroupRoles } from 'pl-api';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { buildGroupMember } from 'soapbox/jest/factory';
|
||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import { useGroupMembers } from './useGroupMembers';
|
||||
|
||||
const groupMember = buildGroupMember();
|
||||
const groupId = '1';
|
||||
|
||||
describe('useGroupMembers hook', () => {
|
||||
describe('with a successful request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/groups/${groupId}/memberships?role=${GroupRoles.ADMIN}`).reply(200, [groupMember]);
|
||||
});
|
||||
});
|
||||
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(() => useGroupMembers(groupId, GroupRoles.ADMIN));
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.groupMembers.length).toBe(1);
|
||||
expect(result.current.groupMembers[0].id).toBe(groupMember.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful query', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/groups/${groupId}/memberships?role=${GroupRoles.ADMIN}`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('is has error state', async() => {
|
||||
const { result } = renderHook(() => useGroupMembers(groupId, GroupRoles.ADMIN));
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.groupMembers.length).toBe(0);
|
||||
expect(result.current.isError).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
23
packages/pl-fe/src/api/hooks/groups/useGroupMembers.ts
Normal file
23
packages/pl-fe/src/api/hooks/groups/useGroupMembers.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { groupMemberSchema, type GroupMember as BaseGroupMember, type GroupRoles } from 'pl-api';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { normalizeGroupMember, type GroupMember } from 'soapbox/normalizers';
|
||||
|
||||
const useGroupMembers = (groupId: string, role: GroupRoles) => {
|
||||
const client = useClient();
|
||||
|
||||
const { entities, ...result } = useEntities<BaseGroupMember, GroupMember>(
|
||||
[Entities.GROUP_MEMBERSHIPS, groupId, role],
|
||||
() => client.experimental.groups.getGroupMemberships(groupId, role),
|
||||
{ schema: groupMemberSchema, transform: normalizeGroupMember },
|
||||
);
|
||||
|
||||
return {
|
||||
...result,
|
||||
groupMembers: entities,
|
||||
};
|
||||
};
|
||||
|
||||
export { useGroupMembers };
|
||||
@@ -0,0 +1,49 @@
|
||||
import { accountSchema, GroupRoles } from 'pl-api';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useDismissEntity, useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { normalizeAccount } from 'soapbox/normalizers';
|
||||
|
||||
import { useGroupRelationship } from './useGroupRelationship';
|
||||
|
||||
import type { ExpandedEntitiesPath } from 'soapbox/entity-store/hooks/types';
|
||||
|
||||
const useGroupMembershipRequests = (groupId: string) => {
|
||||
const client = useClient();
|
||||
const path: ExpandedEntitiesPath = [Entities.ACCOUNTS, 'membership_requests', groupId];
|
||||
|
||||
const { groupRelationship: relationship } = useGroupRelationship(groupId);
|
||||
|
||||
const { entities, invalidate, fetchEntities, ...rest } = useEntities(
|
||||
path,
|
||||
() => client.experimental.groups.getGroupMembershipRequests(groupId),
|
||||
{
|
||||
schema: accountSchema,
|
||||
transform: normalizeAccount,
|
||||
enabled: relationship?.role === GroupRoles.OWNER || relationship?.role === GroupRoles.ADMIN,
|
||||
},
|
||||
);
|
||||
|
||||
const { dismissEntity: authorize } = useDismissEntity(path, async (accountId: string) => {
|
||||
const response = await client.experimental.groups.acceptGroupMembershipRequest(groupId, accountId);
|
||||
invalidate();
|
||||
return response;
|
||||
});
|
||||
|
||||
const { dismissEntity: reject } = useDismissEntity(path, async (accountId: string) => {
|
||||
const response = await client.experimental.groups.rejectGroupMembershipRequest(groupId, accountId);
|
||||
invalidate();
|
||||
return response;
|
||||
});
|
||||
|
||||
return {
|
||||
accounts: entities,
|
||||
refetch: fetchEntities,
|
||||
authorize,
|
||||
reject,
|
||||
...rest,
|
||||
};
|
||||
};
|
||||
|
||||
export { useGroupMembershipRequests };
|
||||
26
packages/pl-fe/src/api/hooks/groups/useGroupRelationship.ts
Normal file
26
packages/pl-fe/src/api/hooks/groups/useGroupRelationship.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { type GroupRelationship, groupRelationshipSchema } from 'pl-api';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
|
||||
const useGroupRelationship = (groupId: string | undefined) => {
|
||||
const client = useClient();
|
||||
|
||||
const { entity: groupRelationship, ...result } = useEntity<GroupRelationship>(
|
||||
[Entities.GROUP_RELATIONSHIPS, groupId!],
|
||||
() => client.experimental.groups.getGroupRelationships([groupId!]),
|
||||
{
|
||||
enabled: !!groupId,
|
||||
schema: z.array(groupRelationshipSchema).nonempty().transform(arr => arr[0]),
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
groupRelationship,
|
||||
...result,
|
||||
};
|
||||
};
|
||||
|
||||
export { useGroupRelationship };
|
||||
24
packages/pl-fe/src/api/hooks/groups/useGroupRelationships.ts
Normal file
24
packages/pl-fe/src/api/hooks/groups/useGroupRelationships.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { type GroupRelationship, groupRelationshipSchema } from 'pl-api';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useBatchedEntities } from 'soapbox/entity-store/hooks/useBatchedEntities';
|
||||
import { useClient, useLoggedIn } from 'soapbox/hooks';
|
||||
|
||||
const useGroupRelationships = (listKey: string[], groupIds: string[]) => {
|
||||
const client = useClient();
|
||||
const { isLoggedIn } = useLoggedIn();
|
||||
|
||||
const fetchGroupRelationships = (groupIds: string[]) =>
|
||||
client.experimental.groups.getGroupRelationships(groupIds);
|
||||
|
||||
const { entityMap: relationships, ...result } = useBatchedEntities<GroupRelationship>(
|
||||
[Entities.RELATIONSHIPS, ...listKey],
|
||||
groupIds,
|
||||
fetchGroupRelationships,
|
||||
{ schema: groupRelationshipSchema, enabled: isLoggedIn },
|
||||
);
|
||||
|
||||
return { relationships, ...result };
|
||||
};
|
||||
|
||||
export { useGroupRelationships };
|
||||
47
packages/pl-fe/src/api/hooks/groups/useGroups.test.ts
Normal file
47
packages/pl-fe/src/api/hooks/groups/useGroups.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { buildGroup } from 'soapbox/jest/factory';
|
||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||
import { instanceSchema } from 'soapbox/schemas';
|
||||
|
||||
import { useGroups } from './useGroups';
|
||||
|
||||
const group = buildGroup({ id: '1', display_name: 'soapbox' });
|
||||
const store = {
|
||||
instance: instanceSchema.parse({
|
||||
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
|
||||
}),
|
||||
};
|
||||
|
||||
describe('useGroups hook', () => {
|
||||
describe('with a successful request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/groups').reply(200, [group]);
|
||||
});
|
||||
});
|
||||
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(useGroups, undefined, store);
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.groups).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful query', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/groups').networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('is has error state', async() => {
|
||||
const { result } = renderHook(useGroups, undefined, store);
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
expect(result.current.groups).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
36
packages/pl-fe/src/api/hooks/groups/useGroups.ts
Normal file
36
packages/pl-fe/src/api/hooks/groups/useGroups.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { groupSchema, type Group as BaseGroup } from 'pl-api';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { useFeatures } from 'soapbox/hooks/useFeatures';
|
||||
import { normalizeGroup, type Group } from 'soapbox/normalizers';
|
||||
|
||||
import { useGroupRelationships } from './useGroupRelationships';
|
||||
|
||||
const useGroups = () => {
|
||||
const client = useClient();
|
||||
const features = useFeatures();
|
||||
|
||||
const { entities, ...result } = useEntities<BaseGroup, Group>(
|
||||
[Entities.GROUPS, 'search', ''],
|
||||
() => client.experimental.groups.getGroups(),
|
||||
{ enabled: features.groups, schema: groupSchema, transform: normalizeGroup },
|
||||
);
|
||||
const { relationships } = useGroupRelationships(
|
||||
['search', ''],
|
||||
entities.map(entity => entity.id),
|
||||
);
|
||||
|
||||
const groups = entities.map((group) => ({
|
||||
...group,
|
||||
relationship: relationships[group.id] || null,
|
||||
}));
|
||||
|
||||
return {
|
||||
...result,
|
||||
groups,
|
||||
};
|
||||
};
|
||||
|
||||
export { useGroups };
|
||||
25
packages/pl-fe/src/api/hooks/groups/useJoinGroup.ts
Normal file
25
packages/pl-fe/src/api/hooks/groups/useJoinGroup.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useCreateEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
|
||||
import { useGroups } from './useGroups';
|
||||
|
||||
import type { Group } from 'pl-api';
|
||||
|
||||
const useJoinGroup = (group: Pick<Group, 'id'>) => {
|
||||
const client = useClient();
|
||||
const { invalidate } = useGroups();
|
||||
|
||||
const { createEntity, isSubmitting } = useCreateEntity(
|
||||
[Entities.GROUP_RELATIONSHIPS, group.id],
|
||||
() => client.experimental.groups.joinGroup(group.id),
|
||||
);
|
||||
|
||||
return {
|
||||
mutate: createEntity,
|
||||
isSubmitting,
|
||||
invalidate,
|
||||
};
|
||||
};
|
||||
|
||||
export { useJoinGroup };
|
||||
25
packages/pl-fe/src/api/hooks/groups/useLeaveGroup.ts
Normal file
25
packages/pl-fe/src/api/hooks/groups/useLeaveGroup.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useCreateEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
|
||||
import { useGroups } from './useGroups';
|
||||
|
||||
import type { Group } from 'pl-api';
|
||||
|
||||
const useLeaveGroup = (group: Pick<Group, 'id'>) => {
|
||||
const client = useClient();
|
||||
const { invalidate } = useGroups();
|
||||
|
||||
const { createEntity, isSubmitting } = useCreateEntity(
|
||||
[Entities.GROUP_RELATIONSHIPS, group.id],
|
||||
() => client.experimental.groups.leaveGroup(group.id),
|
||||
);
|
||||
|
||||
return {
|
||||
mutate: createEntity,
|
||||
isSubmitting,
|
||||
invalidate,
|
||||
};
|
||||
};
|
||||
|
||||
export { useLeaveGroup };
|
||||
23
packages/pl-fe/src/api/hooks/groups/usePromoteGroupMember.ts
Normal file
23
packages/pl-fe/src/api/hooks/groups/usePromoteGroupMember.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { groupMemberSchema } from 'pl-api';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useCreateEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { normalizeGroupMember } from 'soapbox/normalizers';
|
||||
|
||||
import type { Group, GroupMember, GroupRole } from 'pl-api';
|
||||
|
||||
const usePromoteGroupMember = (group: Pick<Group, 'id'>, groupMember: Pick<GroupMember, 'id'>) => {
|
||||
const client = useClient();
|
||||
|
||||
const { createEntity } = useCreateEntity(
|
||||
[Entities.GROUP_MEMBERSHIPS, groupMember.id],
|
||||
({ account_ids, role }: { account_ids: string[]; role: GroupRole }) => client.experimental.groups.promoteGroupUsers(group.id, account_ids, role),
|
||||
{ schema: z.array(groupMemberSchema).transform((arr) => arr[0]), transform: normalizeGroupMember },
|
||||
);
|
||||
|
||||
return createEntity;
|
||||
};
|
||||
|
||||
export { usePromoteGroupMember };
|
||||
32
packages/pl-fe/src/api/hooks/groups/useUpdateGroup.ts
Normal file
32
packages/pl-fe/src/api/hooks/groups/useUpdateGroup.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { groupSchema } from 'pl-api';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useCreateEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { normalizeGroup } from 'soapbox/normalizers';
|
||||
|
||||
interface UpdateGroupParams {
|
||||
display_name?: string;
|
||||
note?: string;
|
||||
avatar?: File | '';
|
||||
header?: File | '';
|
||||
group_visibility?: string;
|
||||
discoverable?: boolean;
|
||||
}
|
||||
|
||||
const useUpdateGroup = (groupId: string) => {
|
||||
const client = useClient();
|
||||
|
||||
const { createEntity, ...rest } = useCreateEntity(
|
||||
[Entities.GROUPS],
|
||||
(params: UpdateGroupParams) => client.experimental.groups.updateGroup(groupId, params),
|
||||
{ schema: groupSchema, transform: normalizeGroup },
|
||||
);
|
||||
|
||||
return {
|
||||
updateGroup: createEntity,
|
||||
...rest,
|
||||
};
|
||||
};
|
||||
|
||||
export { useUpdateGroup };
|
||||
55
packages/pl-fe/src/api/hooks/index.ts
Normal file
55
packages/pl-fe/src/api/hooks/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
// Accounts
|
||||
export { useAccount } from './accounts/useAccount';
|
||||
export { useAccountLookup } from './accounts/useAccountLookup';
|
||||
export {
|
||||
useBlocks,
|
||||
useMutes,
|
||||
useFollowers,
|
||||
useFollowing,
|
||||
} from './accounts/useAccountList';
|
||||
export { useFollow } from './accounts/useFollow';
|
||||
export { useRelationships } from './accounts/useRelationships';
|
||||
|
||||
// Groups
|
||||
export { useBlockGroupMember } from './groups/useBlockGroupMember';
|
||||
export { useCreateGroup, type CreateGroupParams } from './groups/useCreateGroup';
|
||||
export { useDeleteGroup } from './groups/useDeleteGroup';
|
||||
export { useDemoteGroupMember } from './groups/useDemoteGroupMember';
|
||||
export { useGroup } from './groups/useGroup';
|
||||
export { useGroupMedia } from './groups/useGroupMedia';
|
||||
export { useGroupMembers } from './groups/useGroupMembers';
|
||||
export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests';
|
||||
export { useGroupRelationship } from './groups/useGroupRelationship';
|
||||
export { useGroupRelationships } from './groups/useGroupRelationships';
|
||||
export { useGroups } from './groups/useGroups';
|
||||
export { useJoinGroup } from './groups/useJoinGroup';
|
||||
export { useLeaveGroup } from './groups/useLeaveGroup';
|
||||
export { usePromoteGroupMember } from './groups/usePromoteGroupMember';
|
||||
export { useUpdateGroup } from './groups/useUpdateGroup';
|
||||
|
||||
// Instance
|
||||
export { useTranslationLanguages } from './instance/useTranslationLanguages';
|
||||
|
||||
// Settings
|
||||
export { useInteractionPolicies } from './settings/useInteractionPolicies';
|
||||
|
||||
// Statuses
|
||||
export { useBookmarkFolders } from './statuses/useBookmarkFolders';
|
||||
export { useBookmarkFolder } from './statuses/useBookmarkFolder';
|
||||
export { useCreateBookmarkFolder } from './statuses/useCreateBookmarkFolder';
|
||||
export { useDeleteBookmarkFolder } from './statuses/useDeleteBookmarkFolder';
|
||||
export { useUpdateBookmarkFolder } from './statuses/useUpdateBookmarkFolder';
|
||||
|
||||
// Streaming
|
||||
export { useUserStream } from './streaming/useUserStream';
|
||||
export { useCommunityStream } from './streaming/useCommunityStream';
|
||||
export { usePublicStream } from './streaming/usePublicStream';
|
||||
export { useDirectStream } from './streaming/useDirectStream';
|
||||
export { useHashtagStream } from './streaming/useHashtagStream';
|
||||
export { useListStream } from './streaming/useListStream';
|
||||
export { useGroupStream } from './streaming/useGroupStream';
|
||||
export { useRemoteStream } from './streaming/useRemoteStream';
|
||||
|
||||
// Trends
|
||||
export { useTrendingLinks } from './trends/useTrendingLinks';
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useClient, useFeatures, useInstance, useLoggedIn } from 'soapbox/hooks';
|
||||
|
||||
const useTranslationLanguages = () => {
|
||||
const client = useClient();
|
||||
const { isLoggedIn } = useLoggedIn();
|
||||
const features = useFeatures();
|
||||
const instance = useInstance();
|
||||
|
||||
const getTranslationLanguages = async () => {
|
||||
const metadata = instance.pleroma.metadata;
|
||||
|
||||
if (metadata.translation.source_languages?.length) {
|
||||
return Object.fromEntries(metadata.translation.source_languages.map(source => [
|
||||
source,
|
||||
metadata.translation.target_languages!.filter(lang => lang !== source),
|
||||
]));
|
||||
}
|
||||
|
||||
return client.instance.getInstanceTranslationLanguages();
|
||||
};
|
||||
|
||||
const { data, ...result } = useQuery({
|
||||
queryKey: ['translationLanguages'],
|
||||
queryFn: getTranslationLanguages,
|
||||
placeholderData: {},
|
||||
enabled: isLoggedIn && features.translations,
|
||||
});
|
||||
|
||||
return {
|
||||
translationLanguages: data || {},
|
||||
...result,
|
||||
};
|
||||
};
|
||||
|
||||
export { useTranslationLanguages };
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { type InteractionPolicies, interactionPoliciesSchema } from 'pl-api';
|
||||
|
||||
import { useClient, useFeatures, useLoggedIn } from 'soapbox/hooks';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
|
||||
const emptySchema = interactionPoliciesSchema.parse({});
|
||||
|
||||
const useInteractionPolicies = () => {
|
||||
const client = useClient();
|
||||
const { isLoggedIn } = useLoggedIn();
|
||||
const features = useFeatures();
|
||||
|
||||
const { data, ...result } = useQuery({
|
||||
queryKey: ['interactionPolicies'],
|
||||
queryFn: client.settings.getInteractionPolicies,
|
||||
placeholderData: emptySchema,
|
||||
enabled: isLoggedIn && features.interactionRequests,
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: updateInteractionPolicies,
|
||||
isPending: isUpdating,
|
||||
} = useMutation({
|
||||
mutationFn: (policy: InteractionPolicies) =>
|
||||
client.settings.updateInteractionPolicies(policy),
|
||||
retry: false,
|
||||
onSuccess: (policy) => {
|
||||
queryClient.setQueryData(['interactionPolicies'], policy);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
interactionPolicies: data || emptySchema,
|
||||
updateInteractionPolicies,
|
||||
isUpdating,
|
||||
...result,
|
||||
};
|
||||
};
|
||||
|
||||
export { useInteractionPolicies };
|
||||
32
packages/pl-fe/src/api/hooks/statuses/useBookmarkFolder.ts
Normal file
32
packages/pl-fe/src/api/hooks/statuses/useBookmarkFolder.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { selectEntity } from 'soapbox/entity-store/selectors';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import { useBookmarkFolders } from './useBookmarkFolders';
|
||||
|
||||
import type{ BookmarkFolder } from 'pl-api';
|
||||
|
||||
const useBookmarkFolder = (folderId?: string) => {
|
||||
const {
|
||||
isError,
|
||||
isFetched,
|
||||
isFetching,
|
||||
isLoading,
|
||||
invalidate,
|
||||
} = useBookmarkFolders();
|
||||
|
||||
const bookmarkFolder = useAppSelector(state => folderId
|
||||
? selectEntity<BookmarkFolder>(state, Entities.BOOKMARK_FOLDERS, folderId)
|
||||
: undefined);
|
||||
|
||||
return {
|
||||
bookmarkFolder,
|
||||
isError,
|
||||
isFetched,
|
||||
isFetching,
|
||||
isLoading,
|
||||
invalidate,
|
||||
};
|
||||
};
|
||||
|
||||
export { useBookmarkFolder };
|
||||
26
packages/pl-fe/src/api/hooks/statuses/useBookmarkFolders.ts
Normal file
26
packages/pl-fe/src/api/hooks/statuses/useBookmarkFolders.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { bookmarkFolderSchema, type BookmarkFolder } from 'pl-api';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
import { useFeatures } from 'soapbox/hooks/useFeatures';
|
||||
|
||||
const useBookmarkFolders = () => {
|
||||
const client = useClient();
|
||||
const features = useFeatures();
|
||||
|
||||
const { entities, ...result } = useEntities<BookmarkFolder>(
|
||||
[Entities.BOOKMARK_FOLDERS],
|
||||
() => client.myAccount.getBookmarkFolders(),
|
||||
{ enabled: features.bookmarkFolders, schema: bookmarkFolderSchema },
|
||||
);
|
||||
|
||||
const bookmarkFolders = entities;
|
||||
|
||||
return {
|
||||
...result,
|
||||
bookmarkFolders,
|
||||
};
|
||||
};
|
||||
|
||||
export { useBookmarkFolders };
|
||||
@@ -0,0 +1,28 @@
|
||||
import { bookmarkFolderSchema } from 'pl-api';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useCreateEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
|
||||
interface CreateBookmarkFolderParams {
|
||||
name: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
const useCreateBookmarkFolder = () => {
|
||||
const client = useClient();
|
||||
|
||||
const { createEntity, ...rest } = useCreateEntity(
|
||||
[Entities.BOOKMARK_FOLDERS],
|
||||
(params: CreateBookmarkFolderParams) =>
|
||||
client.myAccount.createBookmarkFolder(params),
|
||||
{ schema: bookmarkFolderSchema },
|
||||
);
|
||||
|
||||
return {
|
||||
createBookmarkFolder: createEntity,
|
||||
...rest,
|
||||
};
|
||||
};
|
||||
|
||||
export { useCreateBookmarkFolder };
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useDeleteEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
|
||||
const useDeleteBookmarkFolder = () => {
|
||||
const client = useClient();
|
||||
|
||||
const { deleteEntity, isSubmitting } = useDeleteEntity(
|
||||
Entities.BOOKMARK_FOLDERS,
|
||||
(folderId: string) => client.myAccount.deleteBookmarkFolder(folderId),
|
||||
);
|
||||
|
||||
return {
|
||||
deleteBookmarkFolder: deleteEntity,
|
||||
isSubmitting,
|
||||
};
|
||||
};
|
||||
|
||||
export { useDeleteBookmarkFolder };
|
||||
@@ -0,0 +1,28 @@
|
||||
import { bookmarkFolderSchema } from 'pl-api';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useCreateEntity } from 'soapbox/entity-store/hooks';
|
||||
import { useClient } from 'soapbox/hooks';
|
||||
|
||||
interface UpdateBookmarkFolderParams {
|
||||
name: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
const useUpdateBookmarkFolder = (folderId: string) => {
|
||||
const client = useClient();
|
||||
|
||||
const { createEntity, ...rest } = useCreateEntity(
|
||||
[Entities.BOOKMARK_FOLDERS],
|
||||
(params: UpdateBookmarkFolderParams) =>
|
||||
client.myAccount.updateBookmarkFolder(folderId, params),
|
||||
{ schema: bookmarkFolderSchema },
|
||||
);
|
||||
|
||||
return {
|
||||
updateBookmarkFolder: createEntity,
|
||||
...rest,
|
||||
};
|
||||
};
|
||||
|
||||
export { useUpdateBookmarkFolder };
|
||||
11
packages/pl-fe/src/api/hooks/streaming/useCommunityStream.ts
Normal file
11
packages/pl-fe/src/api/hooks/streaming/useCommunityStream.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useTimelineStream } from './useTimelineStream';
|
||||
|
||||
interface UseCommunityStreamOpts {
|
||||
onlyMedia?: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
const useCommunityStream = ({ onlyMedia, enabled }: UseCommunityStreamOpts = {}) =>
|
||||
useTimelineStream(`public:local${onlyMedia ? ':media' : ''}`, {}, enabled);
|
||||
|
||||
export { useCommunityStream };
|
||||
@@ -0,0 +1,5 @@
|
||||
import { useTimelineStream } from './useTimelineStream';
|
||||
|
||||
const useDirectStream = () => useTimelineStream('direct');
|
||||
|
||||
export { useDirectStream };
|
||||
5
packages/pl-fe/src/api/hooks/streaming/useGroupStream.ts
Normal file
5
packages/pl-fe/src/api/hooks/streaming/useGroupStream.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { useTimelineStream } from './useTimelineStream';
|
||||
|
||||
const useGroupStream = (groupId: string) => useTimelineStream('group', { group: groupId } as any);
|
||||
|
||||
export { useGroupStream };
|
||||
@@ -0,0 +1,5 @@
|
||||
import { useTimelineStream } from './useTimelineStream';
|
||||
|
||||
const useHashtagStream = (tag: string) => useTimelineStream('hashtag', { tag });
|
||||
|
||||
export { useHashtagStream };
|
||||
11
packages/pl-fe/src/api/hooks/streaming/useListStream.ts
Normal file
11
packages/pl-fe/src/api/hooks/streaming/useListStream.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useLoggedIn } from 'soapbox/hooks';
|
||||
|
||||
import { useTimelineStream } from './useTimelineStream';
|
||||
|
||||
const useListStream = (listId: string) => {
|
||||
const { isLoggedIn } = useLoggedIn();
|
||||
|
||||
return useTimelineStream('list', { list: listId }, isLoggedIn);
|
||||
};
|
||||
|
||||
export { useListStream };
|
||||
10
packages/pl-fe/src/api/hooks/streaming/usePublicStream.ts
Normal file
10
packages/pl-fe/src/api/hooks/streaming/usePublicStream.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useTimelineStream } from './useTimelineStream';
|
||||
|
||||
interface UsePublicStreamOpts {
|
||||
onlyMedia?: boolean;
|
||||
}
|
||||
|
||||
const usePublicStream = ({ onlyMedia }: UsePublicStreamOpts = {}) =>
|
||||
useTimelineStream(`public${onlyMedia ? ':media' : ''}`);
|
||||
|
||||
export { usePublicStream };
|
||||
11
packages/pl-fe/src/api/hooks/streaming/useRemoteStream.ts
Normal file
11
packages/pl-fe/src/api/hooks/streaming/useRemoteStream.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useTimelineStream } from './useTimelineStream';
|
||||
|
||||
interface UseRemoteStreamOpts {
|
||||
instance: string;
|
||||
onlyMedia?: boolean;
|
||||
}
|
||||
|
||||
const useRemoteStream = ({ instance, onlyMedia }: UseRemoteStreamOpts) =>
|
||||
useTimelineStream(`public:remote${onlyMedia ? ':media' : ''}`, { instance } as any);
|
||||
|
||||
export { useRemoteStream };
|
||||
87
packages/pl-fe/src/api/hooks/streaming/useTimelineStream.ts
Normal file
87
packages/pl-fe/src/api/hooks/streaming/useTimelineStream.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { useAppSelector, useClient, useInstance } from 'soapbox/hooks';
|
||||
import { getAccessToken } from 'soapbox/utils/auth';
|
||||
|
||||
import type { StreamingEvent } from 'pl-api';
|
||||
|
||||
const useTimelineStream = (stream: string, params: { list?: string; tag?: string } = {}, enabled = true, listener?: (event: StreamingEvent) => any) => {
|
||||
const firstUpdate = useRef(true);
|
||||
|
||||
const client = useClient();
|
||||
|
||||
const instance = useInstance();
|
||||
const socket = useRef<({
|
||||
listen: (listener: any, stream?: string) => number;
|
||||
unlisten: (listener: any) => void;
|
||||
subscribe: (stream: string, params?: {
|
||||
list?: string;
|
||||
tag?: string;
|
||||
}) => void;
|
||||
unsubscribe: (stream: string, params?: {
|
||||
list?: string;
|
||||
tag?: string;
|
||||
}) => void;
|
||||
close: () => void;
|
||||
}) | null>(null);
|
||||
|
||||
const accessToken = useAppSelector(getAccessToken);
|
||||
const streamingUrl = instance.configuration.urls.streaming;
|
||||
|
||||
const connect = async () => {
|
||||
if (!socket.current && streamingUrl) {
|
||||
socket.current = client.streaming.connect();
|
||||
|
||||
socket.current.subscribe(stream, params);
|
||||
if (listener) socket.current.listen(listener);
|
||||
}
|
||||
};
|
||||
|
||||
const disconnect = () => {
|
||||
if (socket.current) {
|
||||
socket.current.close();
|
||||
socket.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
socket.current?.subscribe(stream, params);
|
||||
|
||||
return () => socket.current?.unsubscribe(stream, params);
|
||||
}, [stream, params.list, params.tag, enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
if (listener) socket.current?.unlisten(listener);
|
||||
};
|
||||
}
|
||||
}, [enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (firstUpdate.current) {
|
||||
firstUpdate.current = false;
|
||||
} else {
|
||||
disconnect();
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
if (listener) socket.current?.unlisten(listener);
|
||||
};
|
||||
}
|
||||
}, [accessToken, streamingUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
disconnect();
|
||||
}
|
||||
}, [enabled]);
|
||||
|
||||
return {
|
||||
disconnect,
|
||||
};
|
||||
};
|
||||
|
||||
export { useTimelineStream };
|
||||
180
packages/pl-fe/src/api/hooks/streaming/useUserStream.ts
Normal file
180
packages/pl-fe/src/api/hooks/streaming/useUserStream.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
|
||||
import { announcementSchema, type Announcement, type AnnouncementReaction, type FollowRelationshipUpdate, type Relationship, type StreamingEvent } from 'pl-api';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { updateConversations } from 'soapbox/actions/conversations';
|
||||
import { fetchFilters } from 'soapbox/actions/filters';
|
||||
import { updateNotificationsQueue } from 'soapbox/actions/notifications';
|
||||
import { getLocale, getSettings } from 'soapbox/actions/settings';
|
||||
import { updateStatus } from 'soapbox/actions/statuses';
|
||||
import { deleteFromTimelines, processTimelineUpdate } from 'soapbox/actions/timelines';
|
||||
import { useStatContext } from 'soapbox/contexts/stat-context';
|
||||
import { importEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { selectEntity } from 'soapbox/entity-store/selectors';
|
||||
import { useAppDispatch, useLoggedIn } from 'soapbox/hooks';
|
||||
import messages from 'soapbox/messages';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { getUnreadChatsCount, updateChatListItem } from 'soapbox/utils/chats';
|
||||
import { play, soundCache } from 'soapbox/utils/sounds';
|
||||
|
||||
import { updateReactions } from '../announcements/useAnnouncements';
|
||||
|
||||
import { useTimelineStream } from './useTimelineStream';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
|
||||
const updateAnnouncementReactions = ({ announcement_id: id, name }: AnnouncementReaction) => {
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.map(value => {
|
||||
if (value.id !== id) return value;
|
||||
|
||||
return {
|
||||
...value,
|
||||
reactions: updateReactions(value.reactions, name, -1, true),
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const updateAnnouncement = (announcement: Announcement) =>
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => {
|
||||
let updated = false;
|
||||
|
||||
const result = prevResult.map(value => value.id === announcement.id
|
||||
? (updated = true, announcementSchema.parse(announcement))
|
||||
: value);
|
||||
|
||||
if (!updated) return [announcementSchema.parse(announcement), ...result];
|
||||
});
|
||||
|
||||
const deleteAnnouncement = (announcementId: string) =>
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.filter(value => value.id !== announcementId),
|
||||
);
|
||||
|
||||
const followStateToRelationship = (followState: FollowRelationshipUpdate['state']) => {
|
||||
switch (followState) {
|
||||
case 'follow_pending':
|
||||
return { following: false, requested: true };
|
||||
case 'follow_accept':
|
||||
return { following: true, requested: false };
|
||||
case 'follow_reject':
|
||||
return { following: false, requested: false };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const updateFollowRelationships = (update: FollowRelationshipUpdate) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
|
||||
const me = state.me;
|
||||
const relationship = selectEntity<Relationship>(state, Entities.RELATIONSHIPS, update.following.id);
|
||||
|
||||
if (update.follower.id === me && relationship) {
|
||||
const updated = {
|
||||
...relationship,
|
||||
...followStateToRelationship(update.state),
|
||||
};
|
||||
|
||||
// Add a small delay to deal with API race conditions.
|
||||
setTimeout(() => dispatch(importEntities([updated], Entities.RELATIONSHIPS)), 300);
|
||||
}
|
||||
};
|
||||
|
||||
const getTimelineFromStream = (stream: Array<string>) => {
|
||||
switch (stream[0]) {
|
||||
case 'user':
|
||||
return 'home';
|
||||
case 'hashtag':
|
||||
case 'hashtag:local':
|
||||
case 'list':
|
||||
return `${stream[0]}:${stream[1]}`;
|
||||
default:
|
||||
return stream[0];
|
||||
}
|
||||
};
|
||||
|
||||
const useUserStream = () => {
|
||||
const { isLoggedIn } = useLoggedIn();
|
||||
const dispatch = useAppDispatch();
|
||||
const statContext = useStatContext();
|
||||
|
||||
const listener = useCallback((event: StreamingEvent) => {
|
||||
switch (event.event) {
|
||||
case 'update':
|
||||
dispatch(processTimelineUpdate(getTimelineFromStream(event.stream), event.payload));
|
||||
break;
|
||||
case 'status.update':
|
||||
dispatch(updateStatus(event.payload));
|
||||
break;
|
||||
case 'delete':
|
||||
dispatch(deleteFromTimelines(event.payload));
|
||||
break;
|
||||
case 'notification':
|
||||
dispatch((dispatch, getState) => {
|
||||
const locale = getLocale(getState());
|
||||
messages[locale]().then(messages => {
|
||||
dispatch(
|
||||
updateNotificationsQueue(
|
||||
event.payload,
|
||||
messages,
|
||||
locale,
|
||||
window.location.pathname,
|
||||
),
|
||||
);
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 'conversation':
|
||||
dispatch(updateConversations(event.payload));
|
||||
break;
|
||||
case 'filters_changed':
|
||||
dispatch(fetchFilters());
|
||||
break;
|
||||
case 'chat_update':
|
||||
dispatch((_dispatch, getState) => {
|
||||
const chat = event.payload;
|
||||
const me = getState().me;
|
||||
const messageOwned = chat.last_message?.account_id === me;
|
||||
const settings = getSettings(getState());
|
||||
|
||||
// Don't update own messages from streaming
|
||||
if (!messageOwned) {
|
||||
updateChatListItem(chat);
|
||||
|
||||
if (settings.getIn(['chats', 'sound'])) {
|
||||
play(soundCache.chat);
|
||||
}
|
||||
|
||||
// Increment unread counter
|
||||
statContext?.setUnreadChatsCount(getUnreadChatsCount());
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'follow_relationships_update':
|
||||
dispatch(updateFollowRelationships(event.payload));
|
||||
break;
|
||||
case 'announcement':
|
||||
updateAnnouncement(event.payload);
|
||||
break;
|
||||
case 'announcement.reaction':
|
||||
updateAnnouncementReactions(event.payload);
|
||||
break;
|
||||
case 'announcement.delete':
|
||||
deleteAnnouncement(event.payload);
|
||||
break;
|
||||
// case 'marker':
|
||||
// dispatch({ type: MARKER_FETCH_SUCCESS, marker: JSON.parse(data.payload) });
|
||||
// break;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return useTimelineStream('user', {}, isLoggedIn, listener);
|
||||
};
|
||||
|
||||
export { useUserStream };
|
||||
20
packages/pl-fe/src/api/hooks/trends/useTrendingLinks.ts
Normal file
20
packages/pl-fe/src/api/hooks/trends/useTrendingLinks.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { trendsLinkSchema } from 'pl-api';
|
||||
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useClient, useFeatures } from 'soapbox/hooks';
|
||||
|
||||
const useTrendingLinks = () => {
|
||||
const client = useClient();
|
||||
const features = useFeatures();
|
||||
|
||||
const { entities, ...rest } = useEntities(
|
||||
[Entities.TRENDS_LINKS],
|
||||
() => client.trends.getTrendingLinks(),
|
||||
{ schema: trendsLinkSchema, enabled: features.trendingLinks },
|
||||
);
|
||||
|
||||
return { trendingLinks: entities, ...rest };
|
||||
};
|
||||
|
||||
export { useTrendingLinks };
|
||||
Reference in New Issue
Block a user