Allow managing interaction policies
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
40141191c6
commit
4c990c8fff
15 changed files with 280 additions and 532 deletions
|
@ -132,7 +132,7 @@
|
|||
"multiselect-react-dropdown": "^2.0.25",
|
||||
"object-to-formdata": "^4.5.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pl-api": "^0.0.18",
|
||||
"pl-api": "^0.0.20",
|
||||
"postcss": "^8.4.29",
|
||||
"process": "^0.11.10",
|
||||
"punycode": "^2.1.1",
|
||||
|
|
|
@ -10,13 +10,9 @@ import {
|
|||
authorizeFollowRequest,
|
||||
blockAccount,
|
||||
createAccount,
|
||||
expandFollowers,
|
||||
expandFollowing,
|
||||
expandFollowRequests,
|
||||
fetchAccount,
|
||||
fetchAccountByUsername,
|
||||
fetchFollowers,
|
||||
fetchFollowing,
|
||||
fetchFollowRequests,
|
||||
fetchRelationships,
|
||||
muteAccount,
|
||||
|
@ -841,322 +837,6 @@ describe('removeFromFollowers()', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('fetchFollowers()', () => {
|
||||
const id = '1';
|
||||
|
||||
describe('when logged in', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState.set('me', '123');
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${id}/followers`).reply(200, [], {
|
||||
link: `<https://example.com/api/v1/accounts/${id}/followers?since_id=1>; rel='prev'`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch the correct actions', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'FOLLOWERS_FETCH_REQUEST', id },
|
||||
{ type: 'ACCOUNTS_IMPORT', accounts: [] },
|
||||
{
|
||||
type: 'FOLLOWERS_FETCH_SUCCESS',
|
||||
id,
|
||||
accounts: [],
|
||||
next: null,
|
||||
},
|
||||
];
|
||||
await store.dispatch(fetchFollowers(id));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${id}/followers`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch the correct actions', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'FOLLOWERS_FETCH_REQUEST', id },
|
||||
{ type: 'FOLLOWERS_FETCH_FAIL', id, error: new Error('Network Error') },
|
||||
];
|
||||
await store.dispatch(fetchFollowers(id));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('expandFollowers()', () => {
|
||||
const id = '1';
|
||||
|
||||
describe('when logged out', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState.set('me', null);
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
it('should do nothing', async() => {
|
||||
await store.dispatch(expandFollowers(id));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when logged in', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState
|
||||
.set('user_lists', ReducerRecord({
|
||||
followers: ImmutableMap({
|
||||
[id]: ListRecord({
|
||||
next: 'next_url',
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
.set('me', '123');
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('when the url is null', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState
|
||||
.set('user_lists', ReducerRecord({
|
||||
followers: ImmutableMap({
|
||||
[id]: ListRecord({
|
||||
next: null,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
.set('me', '123');
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
it('should do nothing', async() => {
|
||||
await store.dispatch(expandFollowers(id));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('next_url').reply(200, [], {
|
||||
link: `<https://example.com/api/v1/accounts/${id}/followers?since_id=1>; rel='prev'`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch the correct actions', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'FOLLOWERS_EXPAND_REQUEST', id },
|
||||
{ type: 'ACCOUNTS_IMPORT', accounts: [] },
|
||||
{
|
||||
type: 'FOLLOWERS_EXPAND_SUCCESS',
|
||||
id,
|
||||
accounts: [],
|
||||
next: null,
|
||||
},
|
||||
];
|
||||
await store.dispatch(expandFollowers(id));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('next_url').networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch the correct actions', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'FOLLOWERS_EXPAND_REQUEST', id },
|
||||
{ type: 'FOLLOWERS_EXPAND_FAIL', id, error: new Error('Network Error') },
|
||||
];
|
||||
await store.dispatch(expandFollowers(id));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchFollowing()', () => {
|
||||
const id = '1';
|
||||
|
||||
describe('when logged in', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState.set('me', '123');
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${id}/following`).reply(200, [], {
|
||||
link: `<https://example.com/api/v1/accounts/${id}/following?since_id=1>; rel='prev'`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch the correct actions', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'FOLLOWING_FETCH_REQUEST', id },
|
||||
{ type: 'ACCOUNTS_IMPORT', accounts: [] },
|
||||
{
|
||||
type: 'FOLLOWING_FETCH_SUCCESS',
|
||||
id,
|
||||
accounts: [],
|
||||
next: null,
|
||||
},
|
||||
];
|
||||
await store.dispatch(fetchFollowing(id));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/accounts/${id}/following`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch the correct actions', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'FOLLOWING_FETCH_REQUEST', id },
|
||||
{ type: 'FOLLOWING_FETCH_FAIL', id, error: new Error('Network Error') },
|
||||
];
|
||||
await store.dispatch(fetchFollowing(id));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('expandFollowing()', () => {
|
||||
const id = '1';
|
||||
|
||||
describe('when logged out', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState.set('me', null);
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
it('should do nothing', async() => {
|
||||
await store.dispatch(expandFollowing(id));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when logged in', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState
|
||||
.set('user_lists', ReducerRecord({
|
||||
following: ImmutableMap({
|
||||
[id]: ListRecord({
|
||||
next: 'next_url',
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
.set('me', '123');
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('when the url is null', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState
|
||||
.set('user_lists', ReducerRecord({
|
||||
following: ImmutableMap({
|
||||
[id]: ListRecord({
|
||||
next: null,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
.set('me', '123');
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
it('should do nothing', async() => {
|
||||
await store.dispatch(expandFollowing(id));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('next_url').reply(200, [], {
|
||||
link: `<https://example.com/api/v1/accounts/${id}/following?since_id=1>; rel='prev'`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch the correct actions', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'FOLLOWING_EXPAND_REQUEST', id },
|
||||
{ type: 'ACCOUNTS_IMPORT', accounts: [] },
|
||||
{
|
||||
type: 'FOLLOWING_EXPAND_SUCCESS',
|
||||
id,
|
||||
accounts: [],
|
||||
next: null,
|
||||
},
|
||||
];
|
||||
await store.dispatch(expandFollowing(id));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('next_url').networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch the correct actions', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'FOLLOWING_EXPAND_REQUEST', id },
|
||||
{ type: 'FOLLOWING_EXPAND_FAIL', id, error: new Error('Network Error') },
|
||||
];
|
||||
await store.dispatch(expandFollowing(id));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchRelationships()', () => {
|
||||
const id = '1';
|
||||
|
||||
|
|
|
@ -62,22 +62,6 @@ const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST' as const;
|
|||
const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS' as const;
|
||||
const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL' as const;
|
||||
|
||||
const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST' as const;
|
||||
const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS' as const;
|
||||
const FOLLOWERS_FETCH_FAIL = 'FOLLOWERS_FETCH_FAIL' as const;
|
||||
|
||||
const FOLLOWERS_EXPAND_REQUEST = 'FOLLOWERS_EXPAND_REQUEST' as const;
|
||||
const FOLLOWERS_EXPAND_SUCCESS = 'FOLLOWERS_EXPAND_SUCCESS' as const;
|
||||
const FOLLOWERS_EXPAND_FAIL = 'FOLLOWERS_EXPAND_FAIL' as const;
|
||||
|
||||
const FOLLOWING_FETCH_REQUEST = 'FOLLOWING_FETCH_REQUEST' as const;
|
||||
const FOLLOWING_FETCH_SUCCESS = 'FOLLOWING_FETCH_SUCCESS' as const;
|
||||
const FOLLOWING_FETCH_FAIL = 'FOLLOWING_FETCH_FAIL' as const;
|
||||
|
||||
const FOLLOWING_EXPAND_REQUEST = 'FOLLOWING_EXPAND_REQUEST' as const;
|
||||
const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS' as const;
|
||||
const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL' as const;
|
||||
|
||||
const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST' as const;
|
||||
const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS' as const;
|
||||
const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL' as const;
|
||||
|
@ -370,147 +354,6 @@ const removeFromFollowersFail = (accountId: string, error: unknown) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
const fetchFollowers = (accountId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchFollowersRequest(accountId));
|
||||
|
||||
return getClient(getState()).accounts.getAccountFollowers(accountId)
|
||||
.then(response => {
|
||||
dispatch(importFetchedAccounts(response.items));
|
||||
dispatch(fetchFollowersSuccess(accountId, response.items, response.next));
|
||||
dispatch(fetchRelationships(response.items.map((item) => item.id)));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(fetchFollowersFail(accountId, error));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchFollowersRequest = (accountId: string) => ({
|
||||
type: FOLLOWERS_FETCH_REQUEST,
|
||||
accountId,
|
||||
});
|
||||
|
||||
const fetchFollowersSuccess = (accountId: string, accounts: Array<Account>, next: (() => Promise<PaginatedResponse<Account>>) | null) => ({
|
||||
type: FOLLOWERS_FETCH_SUCCESS,
|
||||
accountId,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const fetchFollowersFail = (accountId: string, error: unknown) => ({
|
||||
type: FOLLOWERS_FETCH_FAIL,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
const expandFollowers = (accountId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return null;
|
||||
|
||||
const next = getState().user_lists.followers.get(accountId)!.next;
|
||||
|
||||
if (next === null) return;
|
||||
|
||||
dispatch(expandFollowersRequest(accountId));
|
||||
|
||||
next().then(response => {
|
||||
dispatch(importFetchedAccounts(response.items));
|
||||
dispatch(expandFollowersSuccess(accountId, response.items, response.next));
|
||||
dispatch(fetchRelationships(response.items.map((item) => item.id)));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(expandFollowersFail(accountId, error));
|
||||
});
|
||||
};
|
||||
|
||||
const expandFollowersRequest = (accountId: string) => ({
|
||||
type: FOLLOWERS_EXPAND_REQUEST,
|
||||
accountId,
|
||||
});
|
||||
|
||||
const expandFollowersSuccess = (accountId: string, accounts: Array<Account>, next: (() => Promise<PaginatedResponse<Account>>) | null) => ({
|
||||
type: FOLLOWERS_EXPAND_SUCCESS,
|
||||
accountId,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const expandFollowersFail = (accountId: string, error: unknown) => ({
|
||||
type: FOLLOWERS_EXPAND_FAIL,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
const fetchFollowing = (accountId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(fetchFollowingRequest(accountId));
|
||||
|
||||
return getClient(getState()).accounts.getAccountFollowing(accountId)
|
||||
.then(response => {
|
||||
dispatch(importFetchedAccounts(response.items));
|
||||
dispatch(fetchFollowingSuccess(accountId, response.items, response.next));
|
||||
dispatch(fetchRelationships(response.items.map((item) => item.id)));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(fetchFollowingFail(accountId, error));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchFollowingRequest = (accountId: string) => ({
|
||||
type: FOLLOWING_FETCH_REQUEST,
|
||||
accountId,
|
||||
});
|
||||
|
||||
const fetchFollowingSuccess = (accountId: string, accounts: Array<Account>, next: (() => Promise<PaginatedResponse<Account>>) | null) => ({
|
||||
type: FOLLOWING_FETCH_SUCCESS,
|
||||
accountId,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const fetchFollowingFail = (accountId: string, error: unknown) => ({
|
||||
type: FOLLOWING_FETCH_FAIL,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
const expandFollowing = (accountId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return null;
|
||||
|
||||
const next = getState().user_lists.following.get(accountId)!.next;
|
||||
|
||||
if (next === null) return;
|
||||
|
||||
dispatch(expandFollowingRequest(accountId));
|
||||
|
||||
next().then(response => {
|
||||
dispatch(importFetchedAccounts(response.items));
|
||||
dispatch(expandFollowingSuccess(accountId, response.items, response.next));
|
||||
dispatch(fetchRelationships(response.items.map((item) => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(expandFollowingFail(accountId, error));
|
||||
});
|
||||
};
|
||||
|
||||
const expandFollowingRequest = (accountId: string) => ({
|
||||
type: FOLLOWING_EXPAND_REQUEST,
|
||||
accountId,
|
||||
});
|
||||
|
||||
const expandFollowingSuccess = (accountId: string, accounts: Array<Account>, next: (() => Promise<PaginatedResponse<Account>>) | null) => ({
|
||||
type: FOLLOWING_EXPAND_SUCCESS,
|
||||
accountId,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
const expandFollowingFail = (accountId: string, error: unknown) => ({
|
||||
type: FOLLOWING_EXPAND_FAIL,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
const fetchRelationships = (accountIds: string[]) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return null;
|
||||
|
@ -874,18 +717,6 @@ export {
|
|||
ACCOUNT_LOOKUP_REQUEST,
|
||||
ACCOUNT_LOOKUP_SUCCESS,
|
||||
ACCOUNT_LOOKUP_FAIL,
|
||||
FOLLOWERS_FETCH_REQUEST,
|
||||
FOLLOWERS_FETCH_SUCCESS,
|
||||
FOLLOWERS_FETCH_FAIL,
|
||||
FOLLOWERS_EXPAND_REQUEST,
|
||||
FOLLOWERS_EXPAND_SUCCESS,
|
||||
FOLLOWERS_EXPAND_FAIL,
|
||||
FOLLOWING_FETCH_REQUEST,
|
||||
FOLLOWING_FETCH_SUCCESS,
|
||||
FOLLOWING_FETCH_FAIL,
|
||||
FOLLOWING_EXPAND_REQUEST,
|
||||
FOLLOWING_EXPAND_SUCCESS,
|
||||
FOLLOWING_EXPAND_FAIL,
|
||||
RELATIONSHIPS_FETCH_REQUEST,
|
||||
RELATIONSHIPS_FETCH_SUCCESS,
|
||||
RELATIONSHIPS_FETCH_FAIL,
|
||||
|
@ -936,22 +767,6 @@ export {
|
|||
removeFromFollowersRequest,
|
||||
removeFromFollowersSuccess,
|
||||
removeFromFollowersFail,
|
||||
fetchFollowers,
|
||||
fetchFollowersRequest,
|
||||
fetchFollowersSuccess,
|
||||
fetchFollowersFail,
|
||||
expandFollowers,
|
||||
expandFollowersRequest,
|
||||
expandFollowersSuccess,
|
||||
expandFollowersFail,
|
||||
fetchFollowing,
|
||||
fetchFollowingRequest,
|
||||
fetchFollowingSuccess,
|
||||
fetchFollowingFail,
|
||||
expandFollowing,
|
||||
expandFollowingRequest,
|
||||
expandFollowingSuccess,
|
||||
expandFollowingFail,
|
||||
fetchRelationships,
|
||||
fetchRelationshipsRequest,
|
||||
fetchRelationshipsSuccess,
|
||||
|
|
|
@ -404,7 +404,7 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
|||
scheduled_at: compose.schedule?.toISOString(),
|
||||
language: compose.language || compose.suggested_language || undefined,
|
||||
to: to.size ? to.toArray() : undefined,
|
||||
federated: compose.federated,
|
||||
local_only: !compose.federated,
|
||||
};
|
||||
|
||||
if (compose.poll) {
|
||||
|
|
|
@ -192,12 +192,8 @@ const updateAuthAccount = (url: string, settings: any) => {
|
|||
const key = `authAccount:${url}`;
|
||||
return KVStore.getItem(key).then((oldAccount: any) => {
|
||||
if (!oldAccount) return;
|
||||
if (!oldAccount.__meta) oldAccount.__meta = { pleroma: { settings_store: {} } };
|
||||
else if (!oldAccount.__meta.pleroma) oldAccount.__meta.pleroma = { settings_store: {} };
|
||||
else if (!oldAccount.__meta.pleroma.settings_store) oldAccount.__meta.pleroma.settings_store = {};
|
||||
oldAccount.__meta.pleroma.settings_store[FE_NAME] = settings;
|
||||
// const settingsStore = oldAccount?.__meta || {};
|
||||
// settingsStore[FE_NAME] = settings;
|
||||
if (!oldAccount.settings_store) oldAccount.settings_store = {};
|
||||
oldAccount.settings_store[FE_NAME] = settings;
|
||||
KVStore.setItem(key, oldAccount);
|
||||
}).catch(console.error);
|
||||
};
|
||||
|
|
|
@ -31,6 +31,9 @@ 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';
|
||||
|
|
41
src/api/hooks/settings/useInteractionPolicies.ts
Normal file
41
src/api/hooks/settings/useInteractionPolicies.ts
Normal file
|
@ -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 };
|
47
src/components/ui/inline-multiselect/inline-multiselect.tsx
Normal file
47
src/components/ui/inline-multiselect/inline-multiselect.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
interface IInlineMultiselect<T extends string> {
|
||||
items: Record<T, string>;
|
||||
value?: T[];
|
||||
onChange: ((values: T[]) => void);
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const InlineMultiselect = <T extends string>({ items, value, onChange, disabled }: IInlineMultiselect<T>) => (
|
||||
<div className='flex w-fit overflow-hidden rounded-md border border-gray-400 bg-white black:bg-black dark:border-gray-800 dark:bg-gray-900'>
|
||||
{Object.entries(items).map(([key, label], i) => {
|
||||
const checked = value?.includes(key as T);
|
||||
|
||||
return (
|
||||
<label
|
||||
className={clsx('px-3 py-2 text-white transition-colors hover:bg-primary-700 [&:has(:focus-visible)]:bg-primary-700', {
|
||||
'cursor-pointer': !disabled,
|
||||
'opacity-75': disabled,
|
||||
'bg-gray-500': !checked,
|
||||
'bg-primary-600': checked,
|
||||
'border-l border-gray-400 dark:border-gray-800': i !== 0,
|
||||
})}
|
||||
key={key}
|
||||
>
|
||||
<input
|
||||
name={key}
|
||||
type='checkbox'
|
||||
className='sr-only'
|
||||
checked={checked}
|
||||
onChange={({ target }) => onChange((target.checked ? [...(value || []), target.name] : value?.filter(key => key !== target.name) || []) as Array<T>)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{label as string}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
export {
|
||||
InlineMultiselect,
|
||||
};
|
175
src/features/interaction-policies/index.tsx
Normal file
175
src/features/interaction-policies/index.tsx
Normal file
|
@ -0,0 +1,175 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { useInteractionPolicies } from 'soapbox/api/hooks';
|
||||
import List, { ListItem } from 'soapbox/components/list';
|
||||
import { Button, CardTitle, Column, Form, FormActions, Tabs } from 'soapbox/components/ui';
|
||||
import { InlineMultiselect } from 'soapbox/components/ui/inline-multiselect/inline-multiselect';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import Warning from '../compose/components/warning';
|
||||
|
||||
type Visibility = 'public' | 'unlisted' | 'private';
|
||||
type Policy = 'can_favourite' | 'can_reblog' | 'can_reply';
|
||||
type Rule = 'always' | 'with_approval';
|
||||
type Scope = 'followers' | 'following' | 'mentioned' | 'public';
|
||||
|
||||
const policies: Array<Policy> = ['can_favourite', 'can_reply', 'can_reblog'];
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.interaction_policies', defaultMessage: 'Interaction policies' },
|
||||
public: { id: 'interaction_policies.tabs.public', defaultMessage: 'Public' },
|
||||
unlisted: { id: 'interaction_policies.tabs.unlisted', defaultMessage: 'Unlisted' },
|
||||
private: { id: 'interaction_policies.tabs.private', defaultMessage: 'Followers-only' },
|
||||
submit: { id: 'interaction_policies.update', defaultMessage: 'Update' },
|
||||
success: { id: 'interaction_policies.success', defaultMessage: 'Updated interaction policies' },
|
||||
fail: { id: 'interaction_policies.fail', defaultMessage: 'Failed to update interaction policies' },
|
||||
});
|
||||
|
||||
const scopeMessages = defineMessages({
|
||||
followers: { id: 'interaction_policies.entry.followers', defaultMessage: 'Followers' },
|
||||
following: { id: 'interaction_policies.entry.following', defaultMessage: 'People I follow' },
|
||||
mentioned: { id: 'interaction_policies.entry.mentioned', defaultMessage: 'Mentioned' },
|
||||
public: { id: 'interaction_policies.entry.public', defaultMessage: 'Everyone' },
|
||||
});
|
||||
|
||||
const titleMessages = {
|
||||
public: defineMessages({
|
||||
can_favourite: { id: 'interaction_policies.title.public.can_favourite', defaultMessage: 'Who can like a public post?' },
|
||||
can_reply: { id: 'interaction_policies.title.public.can_reply', defaultMessage: 'Who can reply to a public post?' },
|
||||
can_reblog: { id: 'interaction_policies.title.public.can_reblog', defaultMessage: 'Who can repost a public post?' },
|
||||
}),
|
||||
unlisted: defineMessages({
|
||||
can_favourite: { id: 'interaction_policies.title.unlisted.can_favourite', defaultMessage: 'Who can like an unlisted post?' },
|
||||
can_reply: { id: 'interaction_policies.title.unlisted.can_reply', defaultMessage: 'Who can reply to an unlisted post?' },
|
||||
can_reblog: { id: 'interaction_policies.title.unlisted.can_reblog', defaultMessage: 'Who can repost an unlisted post?' },
|
||||
}),
|
||||
private: defineMessages({
|
||||
can_favourite: { id: 'interaction_policies.title.private.can_favourite', defaultMessage: 'Who can like a followers-only post?' },
|
||||
can_reply: { id: 'interaction_policies.title.private.can_reply', defaultMessage: 'Who can reply to a followers-only post?' },
|
||||
can_reblog: { id: 'interaction_policies.title.private.can_reblog', defaultMessage: 'Who can repost a followers-only post?' },
|
||||
}),
|
||||
};
|
||||
|
||||
const options: Record<Visibility, Record<Policy, Array<Scope>>> = {
|
||||
public: {
|
||||
can_favourite: ['followers', 'following', 'mentioned', 'public'],
|
||||
can_reblog: ['followers', 'following', 'mentioned', 'public'],
|
||||
can_reply: ['followers', 'following', 'public'],
|
||||
},
|
||||
unlisted: {
|
||||
can_favourite: ['followers', 'following', 'mentioned', 'public'],
|
||||
can_reblog: ['followers', 'following', 'mentioned', 'public'],
|
||||
can_reply: ['followers', 'following', 'public'],
|
||||
},
|
||||
private: {
|
||||
can_favourite: ['followers'],
|
||||
can_reblog: [],
|
||||
can_reply: ['followers'],
|
||||
},
|
||||
};
|
||||
|
||||
const InteractionPolicies = () => {
|
||||
const { interactionPolicies: initial, updateInteractionPolicies, isUpdating } = useInteractionPolicies();
|
||||
const intl = useIntl();
|
||||
const [interactionPolicies, setInteractionPolicies] = useState(initial);
|
||||
const [visibility, setVisibility] = useState<Visibility>('public');
|
||||
|
||||
useEffect(() => {
|
||||
setInteractionPolicies(initial);
|
||||
}, [initial]);
|
||||
|
||||
const getItems = (visibility: Visibility, policy: Policy) => Object.fromEntries(options[visibility][policy].map(scope => [scope, intl.formatMessage(scopeMessages[scope])])) as Record<Scope, string>;
|
||||
|
||||
const handleChange = (visibility: Visibility, policy: Policy, rule: Rule) => (value: Array<Scope>) => {
|
||||
const newPolicies = { ...interactionPolicies };
|
||||
newPolicies[visibility][policy][rule] = value;
|
||||
newPolicies[visibility][policy][rule === 'always' ? 'with_approval' : 'always'] = newPolicies[visibility][policy][rule === 'always' ? 'with_approval' : 'always'].filter(rule => !value.includes(rule as any));
|
||||
|
||||
setInteractionPolicies(newPolicies);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
updateInteractionPolicies(interactionPolicies, {
|
||||
onSuccess: () => toast.success(messages.success),
|
||||
onError: () => toast.success(messages.fail),
|
||||
});
|
||||
};
|
||||
|
||||
const renderPolicy = (visibility: 'public' | 'unlisted' | 'private') => (
|
||||
<>
|
||||
{policies.map((policy) => {
|
||||
const items = getItems(visibility, policy);
|
||||
|
||||
if (!Object.keys(items).length) return null;
|
||||
|
||||
return (
|
||||
<React.Fragment key={policy}>
|
||||
<CardTitle
|
||||
title={intl.formatMessage(titleMessages[visibility][policy])}
|
||||
/>
|
||||
|
||||
{policy === 'can_reply' && (
|
||||
<Warning message={<FormattedMessage id='interaction_policies.mentioned_warning' defaultMessage='Mentioned users can always reply.' />} />
|
||||
)}
|
||||
|
||||
<List>
|
||||
<ListItem label='Always'>
|
||||
<InlineMultiselect<Scope>
|
||||
items={items}
|
||||
value={interactionPolicies[visibility][policy].always as Array<Scope>}
|
||||
onChange={handleChange(visibility, policy, 'always')}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem label='With approval'>
|
||||
<InlineMultiselect
|
||||
items={items}
|
||||
value={interactionPolicies[visibility][policy].with_approval as Array<Scope>}
|
||||
onChange={handleChange(visibility, policy, 'with_approval')}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)} backHref='/settings'>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
text: intl.formatMessage(messages.public),
|
||||
action: () => setVisibility('public'),
|
||||
name: 'public',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.unlisted),
|
||||
action: () => setVisibility('unlisted'),
|
||||
name: 'unlisted',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.private),
|
||||
action: () => setVisibility('private'),
|
||||
name: 'private',
|
||||
}]}
|
||||
activeItem={visibility}
|
||||
/>
|
||||
|
||||
{renderPolicy(visibility)}
|
||||
|
||||
<FormActions>
|
||||
<Button type='submit' theme='primary' disabled={isUpdating}>
|
||||
{intl.formatMessage(messages.submit)}
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Form>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export { InteractionPolicies as default };
|
|
@ -26,6 +26,7 @@ const messages = defineMessages({
|
|||
exportData: { id: 'column.export_data', defaultMessage: 'Export data' },
|
||||
filters: { id: 'navigation_bar.filters', defaultMessage: 'Filters' },
|
||||
importData: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' },
|
||||
interactionPolicies: { id: 'column.interaction_policies', defaultMessage: 'Interaction policies' },
|
||||
mfaDisabled: { id: 'mfa.disabled', defaultMessage: 'Disabled' },
|
||||
mfaEnabled: { id: 'mfa.enabled', defaultMessage: 'Enabled' },
|
||||
mutes: { id: 'settings.mutes', defaultMessage: 'Mutes' },
|
||||
|
@ -82,6 +83,7 @@ const Settings = () => {
|
|||
<ListItem label={intl.formatMessage(messages.blocks)} to='/blocks' />
|
||||
{(features.filters || features.filtersV2) && <ListItem label={intl.formatMessage(messages.filters)} to='/filters' />}
|
||||
{features.federating && <ListItem label={intl.formatMessage(messages.domainBlocks)} to='/domain_blocks' />}
|
||||
{features.interactionRequests && <ListItem label={intl.formatMessage(messages.interactionPolicies)} to='/interaction_policies' />}
|
||||
</List>
|
||||
</CardBody>
|
||||
|
||||
|
|
|
@ -129,6 +129,7 @@ import {
|
|||
DraftStatuses,
|
||||
Circle,
|
||||
BubbleTimeline,
|
||||
InteractionPolicies,
|
||||
} from './util/async-components';
|
||||
import GlobalHotkeys from './util/global-hotkeys';
|
||||
import { WrappedRoute } from './util/react-router-helpers';
|
||||
|
@ -297,6 +298,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
|||
<WrappedRoute path='/settings/account' layout={DefaultLayout} component={DeleteAccount} content={children} />
|
||||
<WrappedRoute path='/settings/mfa' layout={DefaultLayout} component={MfaForm} exact />
|
||||
<WrappedRoute path='/settings/tokens' layout={DefaultLayout} component={AuthTokenList} content={children} />
|
||||
{features.interactionRequests && <WrappedRoute path='/settings/interaction_policies' layout={DefaultLayout} component={InteractionPolicies} content={children} />}
|
||||
<WrappedRoute path='/settings' layout={DefaultLayout} component={Settings} content={children} />
|
||||
<WrappedRoute path='/soapbox/config' adminOnly layout={DefaultLayout} component={SoapboxConfig} content={children} />
|
||||
|
||||
|
|
|
@ -122,4 +122,4 @@ export const Rules = lazy(() => import('soapbox/features/admin/rules'));
|
|||
export const DraftStatuses = lazy(() => import('soapbox/features/draft-statuses'));
|
||||
export const Circle = lazy(() => import('soapbox/features/circle'));
|
||||
export const BubbleTimeline = lazy(() => import('soapbox/features/bubble-timeline'));
|
||||
|
||||
export const InteractionPolicies = lazy(() => import('soapbox/features/interaction-policies'));
|
||||
|
|
|
@ -27,7 +27,7 @@ const updateFrequentLanguages = (state: State, language: string) =>
|
|||
|
||||
const importSettings = (state: State, account: APIEntity) => {
|
||||
account = fromJS(account);
|
||||
const prefs = account.getIn(['__meta', 'pleroma', 'settings_store', FE_NAME], ImmutableMap());
|
||||
const prefs = account.getIn(['settings_store', FE_NAME], ImmutableMap());
|
||||
return state.merge(prefs) as State;
|
||||
};
|
||||
|
||||
|
@ -41,7 +41,6 @@ const settings = (
|
|||
): State => {
|
||||
switch (action.type) {
|
||||
case ME_FETCH_SUCCESS:
|
||||
console.log('importing', action.me);
|
||||
return importSettings(state, action.me);
|
||||
case NOTIFICATIONS_FILTER_SET:
|
||||
case SEARCH_FILTER_SET:
|
||||
|
|
|
@ -6,10 +6,6 @@ import {
|
|||
import { AnyAction } from 'redux';
|
||||
|
||||
import {
|
||||
FOLLOWERS_FETCH_SUCCESS,
|
||||
FOLLOWERS_EXPAND_SUCCESS,
|
||||
FOLLOWING_FETCH_SUCCESS,
|
||||
FOLLOWING_EXPAND_SUCCESS,
|
||||
FOLLOW_REQUESTS_FETCH_SUCCESS,
|
||||
FOLLOW_REQUESTS_EXPAND_SUCCESS,
|
||||
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
|
||||
|
@ -138,14 +134,6 @@ const normalizeFollowRequest = (state: State, notification: Notification) =>
|
|||
|
||||
const userLists = (state = ReducerRecord(), action: DirectoryAction | AnyAction) => {
|
||||
switch (action.type) {
|
||||
case FOLLOWERS_FETCH_SUCCESS:
|
||||
return normalizeList(state, ['followers', action.accountId], action.accounts, action.next);
|
||||
case FOLLOWERS_EXPAND_SUCCESS:
|
||||
return appendToList(state, ['followers', action.accountId], action.accounts, action.next);
|
||||
case FOLLOWING_FETCH_SUCCESS:
|
||||
return normalizeList(state, ['following', action.accountId], action.accounts, action.next);
|
||||
case FOLLOWING_EXPAND_SUCCESS:
|
||||
return appendToList(state, ['following', action.accountId], action.accounts, action.next);
|
||||
case REBLOGS_FETCH_SUCCESS:
|
||||
return normalizeList(state, ['reblogged_by', action.statusId], action.accounts, action.next);
|
||||
case REBLOGS_EXPAND_SUCCESS:
|
||||
|
|
|
@ -8385,10 +8385,10 @@ pkg-types@^1.0.3:
|
|||
mlly "^1.2.0"
|
||||
pathe "^1.1.0"
|
||||
|
||||
pl-api@^0.0.18:
|
||||
version "0.0.18"
|
||||
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-0.0.18.tgz#23542323cb9450e4c084a38e411a8dc5f3e806f5"
|
||||
integrity sha512-JYNR3hKO8bHUOnMNERCXwYj6PMNToY9uqPC5lFbe0vQry77ioHzaqiGzA4F1cK1nSSYZhDR41psuBqisvIp9kg==
|
||||
pl-api@^0.0.20:
|
||||
version "0.0.20"
|
||||
resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-0.0.20.tgz#2839820d399d8018ca3c89b8529e2d4c239b77c1"
|
||||
integrity sha512-FL5eCZnJDPuazGK9zMrIHsKmM9Mb1kvcaYMR6ecbGpkzmustjNL0f8gdH86rFYL1k33zyKLA3sd7OAXB4zFoMg==
|
||||
dependencies:
|
||||
blurhash "^2.0.5"
|
||||
http-link-header "^1.1.3"
|
||||
|
|
Loading…
Reference in a new issue