diff --git a/package.json b/package.json index 68cce9d60..d2528c0f7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/actions/accounts.test.ts b/src/actions/accounts.test.ts index fed8bb8b0..ff4517d5c 100644 --- a/src/actions/accounts.test.ts +++ b/src/actions/accounts.test.ts @@ -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: `; 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: `; 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: `; 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: `; 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'; diff --git a/src/actions/accounts.ts b/src/actions/accounts.ts index 70254c482..ad53e5aab 100644 --- a/src/actions/accounts.ts +++ b/src/actions/accounts.ts @@ -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, next: (() => Promise>) | 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, next: (() => Promise>) | 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, next: (() => Promise>) | 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, next: (() => Promise>) | 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, diff --git a/src/actions/compose.ts b/src/actions/compose.ts index ab7bd440e..fa56040bc 100644 --- a/src/actions/compose.ts +++ b/src/actions/compose.ts @@ -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) { diff --git a/src/actions/settings.ts b/src/actions/settings.ts index 66fd19769..ed60cb91b 100644 --- a/src/actions/settings.ts +++ b/src/actions/settings.ts @@ -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); }; diff --git a/src/api/hooks/index.ts b/src/api/hooks/index.ts index 0c15925f9..ba23f625e 100644 --- a/src/api/hooks/index.ts +++ b/src/api/hooks/index.ts @@ -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'; diff --git a/src/api/hooks/settings/useInteractionPolicies.ts b/src/api/hooks/settings/useInteractionPolicies.ts new file mode 100644 index 000000000..d6d57afc0 --- /dev/null +++ b/src/api/hooks/settings/useInteractionPolicies.ts @@ -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 }; diff --git a/src/components/ui/inline-multiselect/inline-multiselect.tsx b/src/components/ui/inline-multiselect/inline-multiselect.tsx new file mode 100644 index 000000000..83e7f2bc5 --- /dev/null +++ b/src/components/ui/inline-multiselect/inline-multiselect.tsx @@ -0,0 +1,47 @@ +import clsx from 'clsx'; +import React from 'react'; + +interface IInlineMultiselect { + items: Record; + value?: T[]; + onChange: ((values: T[]) => void); + disabled?: boolean; +} + +/** + * + */ +const InlineMultiselect = ({ items, value, onChange, disabled }: IInlineMultiselect) => ( +
+ {Object.entries(items).map(([key, label], i) => { + const checked = value?.includes(key as T); + + return ( + + ); + })} +
+); + +export { + InlineMultiselect, +}; diff --git a/src/features/interaction-policies/index.tsx b/src/features/interaction-policies/index.tsx new file mode 100644 index 000000000..79c0bd7e2 --- /dev/null +++ b/src/features/interaction-policies/index.tsx @@ -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 = ['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>> = { + 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('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; + + const handleChange = (visibility: Visibility, policy: Policy, rule: Rule) => (value: Array) => { + 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 ( + + + + {policy === 'can_reply' && ( + } /> + )} + + + + + items={items} + value={interactionPolicies[visibility][policy].always as Array} + onChange={handleChange(visibility, policy, 'always')} + disabled={isUpdating} + /> + + + } + onChange={handleChange(visibility, policy, 'with_approval')} + disabled={isUpdating} + /> + + + + ); + })} + + ); + + return ( + +
+ 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)} + + + + + +
+ ); +}; + +export { InteractionPolicies as default }; diff --git a/src/features/settings/index.tsx b/src/features/settings/index.tsx index 6cab8b6ac..f58c058ea 100644 --- a/src/features/settings/index.tsx +++ b/src/features/settings/index.tsx @@ -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 = () => { {(features.filters || features.filtersV2) && } {features.federating && } + {features.interactionRequests && } diff --git a/src/features/ui/index.tsx b/src/features/ui/index.tsx index edf63736a..ccdcb5531 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -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 = ({ children }) => + {features.interactionRequests && } diff --git a/src/features/ui/util/async-components.ts b/src/features/ui/util/async-components.ts index 1c84fa038..45e89ed0e 100644 --- a/src/features/ui/util/async-components.ts +++ b/src/features/ui/util/async-components.ts @@ -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')); diff --git a/src/reducers/settings.ts b/src/reducers/settings.ts index 17e810e81..677af7265 100644 --- a/src/reducers/settings.ts +++ b/src/reducers/settings.ts @@ -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: diff --git a/src/reducers/user-lists.ts b/src/reducers/user-lists.ts index a278d3a78..b1ac54fc9 100644 --- a/src/reducers/user-lists.ts +++ b/src/reducers/user-lists.ts @@ -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: diff --git a/yarn.lock b/yarn.lock index 311565f70..e07d5974e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"