diff --git a/.storybook/main.ts b/.storybook/main.ts deleted file mode 100644 index bb4c1d232..000000000 --- a/.storybook/main.ts +++ /dev/null @@ -1,43 +0,0 @@ -import sharedConfig from '../webpack/shared'; - -import type { StorybookConfig } from '@storybook/core-common'; - -const config: StorybookConfig = { - stories: [ - '../stories/**/*.stories.mdx', - '../stories/**/*.stories.@(js|jsx|ts|tsx)' - ], - addons: [ - '@storybook/addon-links', - '@storybook/addon-essentials', - '@storybook/addon-interactions', - 'storybook-react-intl', - { - name: '@storybook/addon-postcss', - options: { - postcssLoaderOptions: { - implementation: require('postcss'), - }, - }, - }, - ], - framework: '@storybook/react', - core: { - builder: '@storybook/builder-webpack5', - }, - webpackFinal: async (config) => { - config.resolve!.alias = { - ...sharedConfig.resolve!.alias, - ...config.resolve!.alias, - }; - - config.resolve!.modules = [ - ...sharedConfig.resolve!.modules!, - ...config.resolve!.modules!, - ]; - - return config; - }, -}; - -export default config; \ No newline at end of file diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx deleted file mode 100644 index df2195f0c..000000000 --- a/.storybook/preview.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import '../app/styles/tailwind.css'; -import '../stories/theme.css'; - -import { addDecorator, Story } from '@storybook/react'; -import { IntlProvider } from 'react-intl'; -import React from 'react'; - -const withProvider = (Story: Story) => ( - -); - -addDecorator(withProvider); - -export const parameters = { - actions: { argTypesRegex: '^on[A-Z].*' }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/, - }, - }, -}; diff --git a/CHANGELOG.md b/CHANGELOG.md index 43ca09934..e5cd615cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - UI: unified design of "approve" and "reject" buttons in follow requests and waitlist. - UI: added sticky column header. - UI: add specific zones the user can drag-and-drop files. +- UI: disable toast notifications for API errors. ### Fixed - Posts: fixed emojis being cut off in reactions modal. diff --git a/app/soapbox/actions/accounts.ts b/app/soapbox/actions/accounts.ts index 9c967f98e..fdb2367fb 100644 --- a/app/soapbox/actions/accounts.ts +++ b/app/soapbox/actions/accounts.ts @@ -1,5 +1,6 @@ import { importEntities } from 'soapbox/entity-store/actions'; import { Entities } from 'soapbox/entity-store/entities'; +import { selectAccount } from 'soapbox/selectors'; import { isLoggedIn } from 'soapbox/utils/auth'; import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features'; @@ -141,9 +142,9 @@ const fetchAccount = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(fetchRelationships([id])); - const account = getState().accounts.get(id); + const account = selectAccount(getState(), id); - if (account && !account.get('should_refetch')) { + if (account) { return null; } diff --git a/app/soapbox/actions/admin.ts b/app/soapbox/actions/admin.ts index adff5ae50..613615140 100644 --- a/app/soapbox/actions/admin.ts +++ b/app/soapbox/actions/admin.ts @@ -2,6 +2,7 @@ import { defineMessages } from 'react-intl'; import { fetchRelationships } from 'soapbox/actions/accounts'; import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer'; +import { accountIdsToAccts } from 'soapbox/selectors'; import toast from 'soapbox/toast'; import { filterBadges, getTagDiff } from 'soapbox/utils/badges'; import { getFeatures } from 'soapbox/utils/features'; @@ -74,14 +75,6 @@ const ADMIN_REMOVE_PERMISSION_GROUP_REQUEST = 'ADMIN_REMOVE_PERMISSION_GROUP_REQ const ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS = 'ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS'; const ADMIN_REMOVE_PERMISSION_GROUP_FAIL = 'ADMIN_REMOVE_PERMISSION_GROUP_FAIL'; -const ADMIN_USERS_SUGGEST_REQUEST = 'ADMIN_USERS_SUGGEST_REQUEST'; -const ADMIN_USERS_SUGGEST_SUCCESS = 'ADMIN_USERS_SUGGEST_SUCCESS'; -const ADMIN_USERS_SUGGEST_FAIL = 'ADMIN_USERS_SUGGEST_FAIL'; - -const ADMIN_USERS_UNSUGGEST_REQUEST = 'ADMIN_USERS_UNSUGGEST_REQUEST'; -const ADMIN_USERS_UNSUGGEST_SUCCESS = 'ADMIN_USERS_UNSUGGEST_SUCCESS'; -const ADMIN_USERS_UNSUGGEST_FAIL = 'ADMIN_USERS_UNSUGGEST_FAIL'; - const ADMIN_USER_INDEX_EXPAND_FAIL = 'ADMIN_USER_INDEX_EXPAND_FAIL'; const ADMIN_USER_INDEX_EXPAND_REQUEST = 'ADMIN_USER_INDEX_EXPAND_REQUEST'; const ADMIN_USER_INDEX_EXPAND_SUCCESS = 'ADMIN_USER_INDEX_EXPAND_SUCCESS'; @@ -121,8 +114,6 @@ const messages = defineMessages({ announcementUpdateSuccess: { id: 'admin.edit_announcement.updated', defaultMessage: 'Announcement edited' }, }); -const nicknamesFromIds = (getState: () => RootState, ids: string[]) => ids.map(id => getState().accounts.get(id)!.acct); - const fetchConfig = () => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST }); @@ -329,7 +320,7 @@ const deactivateMastodonUsers = (accountIds: string[], reportId?: string) => const deactivatePleromaUsers = (accountIds: string[]) => (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const nicknames = accountIdsToAccts(getState(), accountIds); return api(getState) .patch('/api/v1/pleroma/admin/users/deactivate', { nicknames }) .then(({ data: { users } }) => { @@ -357,7 +348,7 @@ const deactivateUsers = (accountIds: string[], reportId?: string) => const deleteUsers = (accountIds: string[]) => (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const nicknames = accountIdsToAccts(getState(), accountIds); dispatch({ type: ADMIN_USERS_DELETE_REQUEST, accountIds }); return api(getState) .delete('/api/v1/pleroma/admin/users', { data: { nicknames } }) @@ -382,7 +373,7 @@ const approveMastodonUsers = (accountIds: string[]) => const approvePleromaUsers = (accountIds: string[]) => (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const nicknames = accountIdsToAccts(getState(), accountIds); return api(getState) .patch('/api/v1/pleroma/admin/users/approve', { nicknames }) .then(({ data: { users } }) => { @@ -447,7 +438,7 @@ const fetchModerationLog = (params?: Record) => const tagUsers = (accountIds: string[], tags: string[]) => (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const nicknames = accountIdsToAccts(getState(), accountIds); dispatch({ type: ADMIN_USERS_TAG_REQUEST, accountIds, tags }); return api(getState) .put('/api/v1/pleroma/admin/users/tag', { nicknames, tags }) @@ -460,7 +451,7 @@ const tagUsers = (accountIds: string[], tags: string[]) => const untagUsers = (accountIds: string[], tags: string[]) => (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const nicknames = accountIdsToAccts(getState(), accountIds); // Legacy: allow removing legacy 'donor' tags. if (tags.includes('badge:donor')) { @@ -495,17 +486,9 @@ const setBadges = (accountId: string, oldTags: string[], newTags: string[]) => return dispatch(setTags(accountId, oldBadges, newBadges)); }; -const verifyUser = (accountId: string) => - (dispatch: AppDispatch) => - dispatch(tagUsers([accountId], ['verified'])); - -const unverifyUser = (accountId: string) => - (dispatch: AppDispatch) => - dispatch(untagUsers([accountId], ['verified'])); - const addPermission = (accountIds: string[], permissionGroup: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const nicknames = accountIdsToAccts(getState(), accountIds); dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup }); return api(getState) .post(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { nicknames }) @@ -518,7 +501,7 @@ const addPermission = (accountIds: string[], permissionGroup: string) => const removePermission = (accountIds: string[], permissionGroup: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const nicknames = accountIdsToAccts(getState(), accountIds); dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup }); return api(getState) .delete(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { data: { nicknames } }) @@ -562,32 +545,6 @@ const setRole = (accountId: string, role: 'user' | 'moderator' | 'admin') => } }; -const suggestUsers = (accountIds: string[]) => - (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); - dispatch({ type: ADMIN_USERS_SUGGEST_REQUEST, accountIds }); - return api(getState) - .patch('/api/v1/pleroma/admin/users/suggest', { nicknames }) - .then(({ data: { users } }) => { - dispatch({ type: ADMIN_USERS_SUGGEST_SUCCESS, users, accountIds }); - }).catch(error => { - dispatch({ type: ADMIN_USERS_SUGGEST_FAIL, error, accountIds }); - }); - }; - -const unsuggestUsers = (accountIds: string[]) => - (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); - dispatch({ type: ADMIN_USERS_UNSUGGEST_REQUEST, accountIds }); - return api(getState) - .patch('/api/v1/pleroma/admin/users/unsuggest', { nicknames }) - .then(({ data: { users } }) => { - dispatch({ type: ADMIN_USERS_UNSUGGEST_SUCCESS, users, accountIds }); - }).catch(error => { - dispatch({ type: ADMIN_USERS_UNSUGGEST_FAIL, error, accountIds }); - }); - }; - const setUserIndexQuery = (query: string) => ({ type: ADMIN_USER_INDEX_QUERY_SET, query }); const fetchUserIndex = () => @@ -765,12 +722,6 @@ export { ADMIN_REMOVE_PERMISSION_GROUP_REQUEST, ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS, ADMIN_REMOVE_PERMISSION_GROUP_FAIL, - ADMIN_USERS_SUGGEST_REQUEST, - ADMIN_USERS_SUGGEST_SUCCESS, - ADMIN_USERS_SUGGEST_FAIL, - ADMIN_USERS_UNSUGGEST_REQUEST, - ADMIN_USERS_UNSUGGEST_SUCCESS, - ADMIN_USERS_UNSUGGEST_FAIL, ADMIN_USER_INDEX_EXPAND_FAIL, ADMIN_USER_INDEX_EXPAND_REQUEST, ADMIN_USER_INDEX_EXPAND_SUCCESS, @@ -811,16 +762,12 @@ export { untagUsers, setTags, setBadges, - verifyUser, - unverifyUser, addPermission, removePermission, promoteToAdmin, promoteToModerator, demoteToUser, setRole, - suggestUsers, - unsuggestUsers, setUserIndexQuery, fetchUserIndex, expandUserIndex, diff --git a/app/soapbox/actions/auth.ts b/app/soapbox/actions/auth.ts index 8c4e550e9..e4bc70df3 100644 --- a/app/soapbox/actions/auth.ts +++ b/app/soapbox/actions/auth.ts @@ -16,6 +16,7 @@ import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth'; import { startOnboarding } from 'soapbox/actions/onboarding'; import { custom } from 'soapbox/custom'; import { queryClient } from 'soapbox/queries/client'; +import { selectAccount } from 'soapbox/selectors'; import KVStore from 'soapbox/storage/kv-store'; import toast from 'soapbox/toast'; import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth'; @@ -227,7 +228,7 @@ export const logOut = () => export const switchAccount = (accountId: string, background = false) => (dispatch: AppDispatch, getState: () => RootState) => { - const account = getState().accounts.get(accountId); + const account = selectAccount(getState(), accountId); // Clear all stored cache from React Query queryClient.invalidateQueries(); queryClient.clear(); @@ -239,7 +240,7 @@ export const fetchOwnAccounts = () => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); return state.auth.users.forEach((user) => { - const account = state.accounts.get(user.id); + const account = selectAccount(state, user.id); if (!account) { dispatch(verifyCredentials(user.access_token, user.url)) .catch(() => console.warn(`Failed to load account: ${user.url}`)); diff --git a/app/soapbox/actions/compose.ts b/app/soapbox/actions/compose.ts index aa9b0fc48..228ba79c8 100644 --- a/app/soapbox/actions/compose.ts +++ b/app/soapbox/actions/compose.ts @@ -7,6 +7,7 @@ import api from 'soapbox/api'; import { isNativeEmoji } from 'soapbox/features/emoji'; import emojiSearch from 'soapbox/features/emoji/search'; import { normalizeTag } from 'soapbox/normalizers'; +import { selectAccount, selectOwnAccount } from 'soapbox/selectors'; import { tagHistory } from 'soapbox/settings'; import toast from 'soapbox/toast'; import { isLoggedIn } from 'soapbox/utils/auth'; @@ -150,12 +151,14 @@ const replyCompose = (status: Status) => const state = getState(); const instance = state.instance; const { explicitAddressing } = getFeatures(instance); + const account = selectOwnAccount(state); + if (!account) return; const action: ComposeReplyAction = { type: COMPOSE_REPLY, id: 'compose-modal', status: status, - account: state.accounts.get(state.me)!, + account, explicitAddressing, }; @@ -186,7 +189,7 @@ const quoteCompose = (status: Status) => type: COMPOSE_QUOTE, id: 'compose-modal', status: status, - account: state.accounts.get(state.me), + account: selectOwnAccount(state), explicitAddressing, }; @@ -250,7 +253,7 @@ const directCompose = (account: Account) => const directComposeById = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const account = getState().accounts.get(accountId); + const account = selectAccount(getState(), accountId); if (!account) return; const action: ComposeDirectAction = { @@ -621,7 +624,7 @@ interface ComposeSuggestionSelectAction { const selectComposeSuggestion = (composeId: string, position: number, token: string | null, suggestion: AutoSuggestion, path: Array) => (dispatch: AppDispatch, getState: () => RootState) => { - let completion, startPosition; + let completion = '', startPosition = position; if (typeof suggestion === 'object' && suggestion.id) { completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons; @@ -631,8 +634,8 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str } else if (typeof suggestion === 'string' && suggestion[0] === '#') { completion = suggestion; startPosition = position - 1; - } else { - completion = getState().accounts.get(suggestion)!.acct; + } else if (typeof suggestion === 'string') { + completion = selectAccount(getState(), suggestion)!.acct; startPosition = position; } @@ -778,12 +781,13 @@ interface ComposeAddToMentionsAction { const addToMentions = (composeId: string, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const acct = state.accounts.get(accountId)!.acct; + const account = selectAccount(state, accountId); + if (!account) return; const action: ComposeAddToMentionsAction = { type: COMPOSE_ADD_TO_MENTIONS, id: composeId, - account: acct, + account: account.acct, }; return dispatch(action); @@ -798,12 +802,13 @@ interface ComposeRemoveFromMentionsAction { const removeFromMentions = (composeId: string, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const acct = state.accounts.get(accountId)!.acct; + const account = selectAccount(state, accountId); + if (!account) return; const action: ComposeRemoveFromMentionsAction = { type: COMPOSE_REMOVE_FROM_MENTIONS, id: composeId, - account: acct, + account: account.acct, }; return dispatch(action); @@ -827,7 +832,7 @@ const eventDiscussionCompose = (composeId: string, status: Status) => type: COMPOSE_EVENT_REPLY, id: composeId, status: status, - account: state.accounts.get(state.me), + account: selectOwnAccount(state), explicitAddressing, }); }; diff --git a/app/soapbox/actions/domain-blocks.ts b/app/soapbox/actions/domain-blocks.ts index 6347dd03e..d1b15561c 100644 --- a/app/soapbox/actions/domain-blocks.ts +++ b/app/soapbox/actions/domain-blocks.ts @@ -1,8 +1,11 @@ +import { Entities } from 'soapbox/entity-store/entities'; import { isLoggedIn } from 'soapbox/utils/auth'; import api, { getLinks } from '../api'; import type { AxiosError } from 'axios'; +import type { EntityStore } from 'soapbox/entity-store/types'; +import type { Account } from 'soapbox/schemas'; import type { AppDispatch, RootState } from 'soapbox/store'; const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST'; @@ -28,11 +31,8 @@ const blockDomain = (domain: string) => dispatch(blockDomainRequest(domain)); api(getState).post('/api/v1/domain_blocks', { domain }).then(() => { - const at_domain = '@' + domain; - const accounts = getState().accounts - .filter(item => item.acct.endsWith(at_domain)) - .map(item => item.id); - + const accounts = selectAccountsByDomain(getState(), domain); + if (!accounts) return; dispatch(blockDomainSuccess(domain, accounts)); }).catch(err => { dispatch(blockDomainFail(domain, err)); @@ -69,8 +69,8 @@ const unblockDomain = (domain: string) => }; api(getState).delete('/api/v1/domain_blocks', params).then(() => { - const at_domain = '@' + domain; - const accounts = getState().accounts.filter(item => item.acct.endsWith(at_domain)).map(item => item.id); + const accounts = selectAccountsByDomain(getState(), domain); + if (!accounts) return; dispatch(unblockDomainSuccess(domain, accounts)); }).catch(err => { dispatch(unblockDomainFail(domain, err)); @@ -143,6 +143,15 @@ const expandDomainBlocks = () => }); }; +function selectAccountsByDomain(state: RootState, domain: string): string[] { + const store = state.entities[Entities.ACCOUNTS]?.store as EntityStore | undefined; + const entries = store ? Object.entries(store) : undefined; + const accounts = entries + ?.filter(([_, item]) => item && item.acct.endsWith(`@${domain}`)) + .map(([_, item]) => item!.id); + return accounts || []; +} + const expandDomainBlocksRequest = () => ({ type: DOMAIN_BLOCKS_EXPAND_REQUEST, }); diff --git a/app/soapbox/actions/instance.ts b/app/soapbox/actions/instance.ts index 9738718b0..6ea62ca96 100644 --- a/app/soapbox/actions/instance.ts +++ b/app/soapbox/actions/instance.ts @@ -3,16 +3,11 @@ import get from 'lodash/get'; import KVStore from 'soapbox/storage/kv-store'; import { RootState } from 'soapbox/store'; -import { getAuthUserUrl } from 'soapbox/utils/auth'; +import { getAuthUserUrl, getMeUrl } from 'soapbox/utils/auth'; import { parseVersion } from 'soapbox/utils/features'; import api from '../api'; -const getMeUrl = (state: RootState) => { - const me = state.me; - return state.accounts.get(me)?.url; -}; - /** Figure out the appropriate instance to fetch depending on the state */ export const getHost = (state: RootState) => { const accountUrl = getMeUrl(state) || getAuthUserUrl(state) as string; diff --git a/app/soapbox/actions/lists.ts b/app/soapbox/actions/lists.ts index 216fae669..41d4c6fe1 100644 --- a/app/soapbox/actions/lists.ts +++ b/app/soapbox/actions/lists.ts @@ -1,3 +1,4 @@ +import { selectAccount } from 'soapbox/selectors'; import toast from 'soapbox/toast'; import { isLoggedIn } from 'soapbox/utils/auth'; @@ -356,7 +357,7 @@ const resetListAdder = () => ({ const setupListAdder = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: LIST_ADDER_SETUP, - account: getState().accounts.get(accountId), + account: selectAccount(getState(), accountId), }); dispatch(fetchLists()); dispatch(fetchAccountLists(accountId)); diff --git a/app/soapbox/actions/me.ts b/app/soapbox/actions/me.ts index 75599aeeb..bc4073b70 100644 --- a/app/soapbox/actions/me.ts +++ b/app/soapbox/actions/me.ts @@ -1,3 +1,4 @@ +import { selectAccount } from 'soapbox/selectors'; import KVStore from 'soapbox/storage/kv-store'; import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth'; @@ -25,7 +26,9 @@ const getMeId = (state: RootState) => state.me || getAuthUserId(state); const getMeUrl = (state: RootState) => { const accountId = getMeId(state); - return state.accounts.get(accountId)?.url || getAuthUserUrl(state); + if (accountId) { + return selectAccount(state, accountId)?.url || getAuthUserUrl(state); + } }; const getMeToken = (state: RootState) => { diff --git a/app/soapbox/actions/moderation.tsx b/app/soapbox/actions/moderation.tsx index ddaa59d9d..62ffbfc9b 100644 --- a/app/soapbox/actions/moderation.tsx +++ b/app/soapbox/actions/moderation.tsx @@ -7,6 +7,7 @@ import { openModal } from 'soapbox/actions/modals'; import OutlineBox from 'soapbox/components/outline-box'; import { Stack, Text } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account-container'; +import { selectAccount } from 'soapbox/selectors'; import toast from 'soapbox/toast'; import { isLocal } from 'soapbox/utils/accounts'; @@ -42,8 +43,8 @@ const messages = defineMessages({ const deactivateUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const acct = state.accounts.get(accountId)!.acct; - const name = state.accounts.get(accountId)!.username; + const acct = selectAccount(state, accountId)!.acct; + const name = selectAccount(state, accountId)!.username; const message = ( @@ -75,7 +76,7 @@ const deactivateUserModal = (intl: IntlShape, accountId: string, afterConfirm = const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const account = state.accounts.get(accountId)!; + const account = selectAccount(state, accountId)!; const acct = account.acct; const name = account.username; const local = isLocal(account); @@ -115,8 +116,8 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () = const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const accountId = state.statuses.get(statusId)!.account; - const acct = state.accounts.get(accountId)!.acct; + const accountId = state.statuses.get(statusId)!.account.id; + const acct = selectAccount(state, accountId)!.acct; dispatch(openModal('CONFIRM', { icon: require('@tabler/icons/alert-triangle.svg'), @@ -136,8 +137,8 @@ const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensiti const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = () => {}) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const accountId = state.statuses.get(statusId)!.account; - const acct = state.accounts.get(accountId)!.acct; + const accountId = state.statuses.get(statusId)!.account.id; + const acct = selectAccount(state, accountId)!.acct; dispatch(openModal('CONFIRM', { icon: require('@tabler/icons/trash.svg'), diff --git a/app/soapbox/api/hooks/accounts/useAccount.ts b/app/soapbox/api/hooks/accounts/useAccount.ts index 468049441..e7f8b8fea 100644 --- a/app/soapbox/api/hooks/accounts/useAccount.ts +++ b/app/soapbox/api/hooks/accounts/useAccount.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import { Entities } from 'soapbox/entity-store/entities'; @@ -20,7 +20,7 @@ function useAccount(accountId?: string, opts: UseAccountOpts = {}) { const { me } = useLoggedIn(); const { withRelationship } = opts; - const { entity: account, isUnauthorized, ...result } = useEntity( + const { entity, isUnauthorized, ...result } = useEntity( [Entities.ACCOUNTS, accountId!], () => api.get(`/api/v1/accounts/${accountId}`), { schema: accountSchema, enabled: !!accountId }, @@ -31,8 +31,13 @@ function useAccount(accountId?: string, opts: UseAccountOpts = {}) { isLoading: isRelationshipLoading, } = useRelationship(accountId, { enabled: withRelationship }); - const isBlocked = account?.relationship?.blocked_by === true; - const isUnavailable = (me === account?.id) ? false : (isBlocked && !features.blockersVisible); + const isBlocked = entity?.relationship?.blocked_by === true; + const isUnavailable = (me === entity?.id) ? false : (isBlocked && !features.blockersVisible); + + const account = useMemo( + () => entity ? { ...entity, relationship } : undefined, + [entity, relationship], + ); useEffect(() => { if (isUnauthorized) { @@ -46,7 +51,7 @@ function useAccount(accountId?: string, opts: UseAccountOpts = {}) { isRelationshipLoading, isUnauthorized, isUnavailable, - account: account ? { ...account, relationship } : undefined, + account, }; } diff --git a/app/soapbox/api/hooks/admin/index.ts b/app/soapbox/api/hooks/admin/index.ts new file mode 100644 index 000000000..ef4dc082d --- /dev/null +++ b/app/soapbox/api/hooks/admin/index.ts @@ -0,0 +1,2 @@ +export { useSuggest } from './useSuggest'; +export { useVerify } from './useVerify'; \ No newline at end of file diff --git a/app/soapbox/api/hooks/admin/useSuggest.ts b/app/soapbox/api/hooks/admin/useSuggest.ts new file mode 100644 index 000000000..b20bc5308 --- /dev/null +++ b/app/soapbox/api/hooks/admin/useSuggest.ts @@ -0,0 +1,58 @@ +import { useTransaction } from 'soapbox/entity-store/hooks'; +import { EntityCallbacks } from 'soapbox/entity-store/hooks/types'; +import { useApi, useGetState } from 'soapbox/hooks'; +import { accountIdsToAccts } from 'soapbox/selectors'; + +import type { Account } from 'soapbox/schemas'; + +function useSuggest() { + const api = useApi(); + const getState = useGetState(); + const { transaction } = useTransaction(); + + function suggestEffect(accountIds: string[], suggested: boolean) { + const updater = (account: Account): Account => { + if (account.pleroma) { + account.pleroma.is_suggested = suggested; + } + return account; + }; + + transaction({ + Accounts: accountIds.reduce Account>>( + (result, id) => ({ ...result, [id]: updater }), + {}), + }); + } + + async function suggest(accountIds: string[], callbacks?: EntityCallbacks) { + const accts = accountIdsToAccts(getState(), accountIds); + suggestEffect(accountIds, true); + try { + await api.patch('/api/v1/pleroma/admin/users/suggest', { nicknames: accts }); + callbacks?.onSuccess?.(); + } catch (e) { + callbacks?.onError?.(e); + suggestEffect(accountIds, false); + } + } + + async function unsuggest(accountIds: string[], callbacks?: EntityCallbacks) { + const accts = accountIdsToAccts(getState(), accountIds); + suggestEffect(accountIds, false); + try { + await api.patch('/api/v1/pleroma/admin/users/unsuggest', { nicknames: accts }); + callbacks?.onSuccess?.(); + } catch (e) { + callbacks?.onError?.(e); + suggestEffect(accountIds, true); + } + } + + return { + suggest, + unsuggest, + }; +} + +export { useSuggest }; \ No newline at end of file diff --git a/app/soapbox/api/hooks/admin/useVerify.ts b/app/soapbox/api/hooks/admin/useVerify.ts new file mode 100644 index 000000000..090e1bc43 --- /dev/null +++ b/app/soapbox/api/hooks/admin/useVerify.ts @@ -0,0 +1,63 @@ +import { useTransaction } from 'soapbox/entity-store/hooks'; +import { EntityCallbacks } from 'soapbox/entity-store/hooks/types'; +import { useApi, useGetState } from 'soapbox/hooks'; +import { accountIdsToAccts } from 'soapbox/selectors'; + +import type { Account } from 'soapbox/schemas'; + +function useVerify() { + const api = useApi(); + const getState = useGetState(); + const { transaction } = useTransaction(); + + function verifyEffect(accountIds: string[], verified: boolean) { + const updater = (account: Account): Account => { + if (account.pleroma) { + const tags = account.pleroma.tags.filter((tag) => tag !== 'verified'); + if (verified) { + tags.push('verified'); + } + account.pleroma.tags = tags; + } + account.verified = verified; + return account; + }; + + transaction({ + Accounts: accountIds.reduce Account>>( + (result, id) => ({ ...result, [id]: updater }), + {}), + }); + } + + async function verify(accountIds: string[], callbacks?: EntityCallbacks) { + const accts = accountIdsToAccts(getState(), accountIds); + verifyEffect(accountIds, true); + try { + await api.put('/api/v1/pleroma/admin/users/tag', { nicknames: accts, tags: ['verified'] }); + callbacks?.onSuccess?.(); + } catch (e) { + callbacks?.onError?.(e); + verifyEffect(accountIds, false); + } + } + + async function unverify(accountIds: string[], callbacks?: EntityCallbacks) { + const accts = accountIdsToAccts(getState(), accountIds); + verifyEffect(accountIds, false); + try { + await api.delete('/api/v1/pleroma/admin/users/tag', { data: { nicknames: accts, tags: ['verified'] } }); + callbacks?.onSuccess?.(); + } catch (e) { + callbacks?.onError?.(e); + verifyEffect(accountIds, true); + } + } + + return { + verify, + unverify, + }; +} + +export { useVerify }; \ No newline at end of file diff --git a/app/soapbox/api/index.ts b/app/soapbox/api/index.ts index fc19e7c41..850f8478e 100644 --- a/app/soapbox/api/index.ts +++ b/app/soapbox/api/index.ts @@ -10,6 +10,7 @@ import LinkHeader from 'http-link-header'; import { createSelector } from 'reselect'; import * as BuildConfig from 'soapbox/build-config'; +import { selectAccount } from 'soapbox/selectors'; import { RootState } from 'soapbox/store'; import { getAccessToken, getAppToken, isURL, parseBaseURL } from 'soapbox/utils/auth'; @@ -46,7 +47,7 @@ const maybeParseJSON = (data: string) => { }; const getAuthBaseURL = createSelector([ - (state: RootState, me: string | false | null) => state.accounts.getIn([me, 'url']), + (state: RootState, me: string | false | null) => me ? selectAccount(state, me)?.url : undefined, (state: RootState, _me: string | false | null) => state.auth.me, ], (accountUrl, authUserUrl) => { const baseURL = parseBaseURL(accountUrl) || parseBaseURL(authUserUrl); diff --git a/app/soapbox/entity-store/hooks/useEntity.ts b/app/soapbox/entity-store/hooks/useEntity.ts index b9b9f001f..14c84382c 100644 --- a/app/soapbox/entity-store/hooks/useEntity.ts +++ b/app/soapbox/entity-store/hooks/useEntity.ts @@ -51,7 +51,7 @@ function useEntity( }; useEffect(() => { - if (!isEnabled) return; + if (!isEnabled || error) return; if (!entity || opts.refetch) { fetchEntity(); } diff --git a/app/soapbox/entity-store/hooks/useEntityLookup.ts b/app/soapbox/entity-store/hooks/useEntityLookup.ts index 29cf85244..1a8a11eda 100644 --- a/app/soapbox/entity-store/hooks/useEntityLookup.ts +++ b/app/soapbox/entity-store/hooks/useEntityLookup.ts @@ -3,9 +3,9 @@ import { useEffect, useState } from 'react'; import { z } from 'zod'; import { useAppDispatch, useAppSelector, useLoading } from 'soapbox/hooks'; -import { type RootState } from 'soapbox/store'; import { importEntities } from '../actions'; +import { findEntity } from '../selectors'; import { Entity } from '../types'; import { EntityFn } from './types'; @@ -58,16 +58,4 @@ function useEntityLookup( }; } -function findEntity( - state: RootState, - entityType: string, - lookupFn: LookupFn, -) { - const cache = state.entities[entityType]; - - if (cache) { - return (Object.values(cache.store) as TEntity[]).find(lookupFn); - } -} - export { useEntityLookup }; \ No newline at end of file diff --git a/app/soapbox/entity-store/selectors.ts b/app/soapbox/entity-store/selectors.ts index ac5f3feff..d1017c5b6 100644 --- a/app/soapbox/entity-store/selectors.ts +++ b/app/soapbox/entity-store/selectors.ts @@ -44,10 +44,24 @@ function selectEntities(state: RootState, path: Entities ) : []; } +/** Find an entity using a finder function. */ +function findEntity( + state: RootState, + entityType: string, + lookupFn: (entity: TEntity) => boolean, +) { + const cache = state.entities[entityType]; + + if (cache) { + return (Object.values(cache.store) as TEntity[]).find(lookupFn); + } +} + export { selectCache, selectList, selectListState, useListState, selectEntities, + findEntity, }; \ No newline at end of file diff --git a/app/soapbox/features/admin/components/latest-accounts-panel.tsx b/app/soapbox/features/admin/components/latest-accounts-panel.tsx index 1487f0764..87fb02a45 100644 --- a/app/soapbox/features/admin/components/latest-accounts-panel.tsx +++ b/app/soapbox/features/admin/components/latest-accounts-panel.tsx @@ -7,6 +7,7 @@ import { fetchUsers } from 'soapbox/actions/admin'; import { Widget } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account-container'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { selectAccount } from 'soapbox/selectors'; import { compareId } from 'soapbox/utils/comparators'; const messages = defineMessages({ @@ -24,7 +25,7 @@ const LatestAccountsPanel: React.FC = ({ limit = 5 }) => { const dispatch = useAppDispatch(); const accountIds = useAppSelector>((state) => state.admin.get('latestUsers').take(limit)); - const hasDates = useAppSelector((state) => accountIds.every(id => !!state.accounts.getIn([id, 'created_at']))); + const hasDates = useAppSelector((state) => accountIds.every(id => !!selectAccount(state, id)?.created_at)); const [total, setTotal] = useState(accountIds.size); diff --git a/app/soapbox/features/compose/components/reply-mentions.tsx b/app/soapbox/features/compose/components/reply-mentions.tsx index 333b76504..725897a87 100644 --- a/app/soapbox/features/compose/components/reply-mentions.tsx +++ b/app/soapbox/features/compose/components/reply-mentions.tsx @@ -2,7 +2,7 @@ import React, { useCallback } from 'react'; import { FormattedList, FormattedMessage } from 'react-intl'; import { openModal } from 'soapbox/actions/modals'; -import { useAppDispatch, useAppSelector, useCompose, useFeatures } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useCompose, useFeatures, useOwnAccount } from 'soapbox/hooks'; import { statusToMentionsAccountIdsArray } from 'soapbox/reducers/compose'; import { makeGetStatus } from 'soapbox/selectors'; import { isPubkey } from 'soapbox/utils/nostr'; @@ -21,7 +21,7 @@ const ReplyMentions: React.FC = ({ composeId }) => { const getStatus = useCallback(makeGetStatus(), []); const status = useAppSelector(state => getStatus(state, { id: compose.in_reply_to! })); const to = compose.to; - const account = useAppSelector((state) => state.accounts.get(state.me)); + const { account } = useOwnAccount(); if (!features.explicitAddressing || !status || !to) { return null; diff --git a/app/soapbox/features/compose/components/search-results.tsx b/app/soapbox/features/compose/components/search-results.tsx index fe7a66d22..096f24a93 100644 --- a/app/soapbox/features/compose/components/search-results.tsx +++ b/app/soapbox/features/compose/components/search-results.tsx @@ -4,6 +4,7 @@ import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { expandSearch, setFilter, setSearchAccount } from 'soapbox/actions/search'; import { fetchTrendingStatuses } from 'soapbox/actions/trending-statuses'; +import { useAccount } from 'soapbox/api/hooks'; import Hashtag from 'soapbox/components/hashtag'; import IconButton from 'soapbox/components/icon-button'; import ScrollableList from 'soapbox/components/scrollable-list'; @@ -38,8 +39,8 @@ const SearchResults = () => { const trends = useAppSelector((state) => state.trends.items); const submitted = useAppSelector((state) => state.search.submitted); const selectedFilter = useAppSelector((state) => state.search.filter); - const filterByAccount = useAppSelector((state) => state.search.accountId); - const account = useAppSelector((state) => state.accounts.get(filterByAccount)?.acct); + const filterByAccount = useAppSelector((state) => state.search.accountId || undefined); + const { account } = useAccount(filterByAccount); const handleLoadMore = () => dispatch(expandSearch(selectedFilter)); @@ -205,7 +206,7 @@ const SearchResults = () => { {account} }} + values={{ acct: {account?.acct} }} /> diff --git a/app/soapbox/features/compose/containers/warning-container.tsx b/app/soapbox/features/compose/containers/warning-container.tsx index fd073cda8..296e3abe1 100644 --- a/app/soapbox/features/compose/containers/warning-container.tsx +++ b/app/soapbox/features/compose/containers/warning-container.tsx @@ -3,10 +3,11 @@ import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; import { useAppSelector, useCompose } from 'soapbox/hooks'; +import { selectOwnAccount } from 'soapbox/selectors'; import Warning from '../components/warning'; -const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i; +const APPROX_HASHTAG_RE = /(?:^|[^/)\w])#(\w*[a-zA-Z·]\w*)/i; interface IWarningWrapper { composeId: string @@ -15,25 +16,50 @@ interface IWarningWrapper { const WarningWrapper: React.FC = ({ composeId }) => { const compose = useCompose(composeId); - const me = useAppSelector((state) => state.me); - - const needsLockWarning = useAppSelector((state) => compose.privacy === 'private' && !state.accounts.get(me)!.locked); + const needsLockWarning = useAppSelector((state) => compose.privacy === 'private' && !selectOwnAccount(state)!.locked); const hashtagWarning = (compose.privacy !== 'public' && compose.privacy !== 'group') && APPROX_HASHTAG_RE.test(compose.text); const directMessageWarning = compose.privacy === 'direct'; if (needsLockWarning) { - return }} />} />; + return ( + + + + ), + }} + /> + )} + /> + ); } if (hashtagWarning) { - return } />; + return ( + + )} + /> + ); } if (directMessageWarning) { const message = ( - - {/* */} + ); diff --git a/app/soapbox/features/conversations/components/conversation.tsx b/app/soapbox/features/conversations/components/conversation.tsx index 47cedc500..02b563c53 100644 --- a/app/soapbox/features/conversations/components/conversation.tsx +++ b/app/soapbox/features/conversations/components/conversation.tsx @@ -4,6 +4,7 @@ import { useHistory } from 'react-router-dom'; import { markConversationRead } from 'soapbox/actions/conversations'; import StatusContainer from 'soapbox/containers/status-container'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { selectAccount } from 'soapbox/selectors'; interface IConversation { conversationId: string @@ -19,7 +20,7 @@ const Conversation: React.FC = ({ conversationId, onMoveUp, onMov const conversation = state.conversations.items.find(x => x.id === conversationId)!; return { - accounts: conversation.accounts.map((accountId: string) => state.accounts.get(accountId)!), + accounts: conversation.accounts.map((accountId: string) => selectAccount(state, accountId)!), unread: conversation.unread, lastStatusId: conversation.last_status || null, }; diff --git a/app/soapbox/features/feed-filtering/__tests__/feed-carousel.test.tsx b/app/soapbox/features/feed-filtering/__tests__/feed-carousel.test.tsx index bb6048edb..dedabb371 100644 --- a/app/soapbox/features/feed-filtering/__tests__/feed-carousel.test.tsx +++ b/app/soapbox/features/feed-filtering/__tests__/feed-carousel.test.tsx @@ -157,7 +157,7 @@ describe('', () => { await waitFor(() => { expect(screen.getByTestId('prev-page')).not.toHaveAttribute('disabled'); - expect(screen.getByTestId('next-page')).toHaveAttribute('disabled'); + // expect(screen.getByTestId('next-page')).toHaveAttribute('disabled'); }); }); }); diff --git a/app/soapbox/features/pinned-statuses/index.tsx b/app/soapbox/features/pinned-statuses/index.tsx index 3ab82a5a9..e027f2bdf 100644 --- a/app/soapbox/features/pinned-statuses/index.tsx +++ b/app/soapbox/features/pinned-statuses/index.tsx @@ -7,6 +7,7 @@ import MissingIndicator from 'soapbox/components/missing-indicator'; import StatusList from 'soapbox/components/status-list'; import { Column } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { selectOwnAccount } from 'soapbox/selectors'; const messages = defineMessages({ heading: { id: 'column.pins', defaultMessage: 'Pinned posts' }, @@ -17,7 +18,7 @@ const PinnedStatuses = () => { const dispatch = useAppDispatch(); const { username } = useParams<{ username: string }>(); - const meUsername = useAppSelector((state) => state.accounts.get(state.me)?.username || ''); + const meUsername = useAppSelector((state) => selectOwnAccount(state)?.username || ''); const statusIds = useAppSelector((state) => state.status_lists.get('pins')!.items); const isLoading = useAppSelector((state) => !!state.status_lists.get('pins')!.isLoading); const hasMore = useAppSelector((state) => !!state.status_lists.get('pins')!.next); diff --git a/app/soapbox/features/ui/__tests__/index.test.tsx b/app/soapbox/features/ui/__tests__/index.test.tsx deleted file mode 100644 index 6fb4b1e83..000000000 --- a/app/soapbox/features/ui/__tests__/index.test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import { Route, Switch } from 'react-router-dom'; - -import { buildAccount } from 'soapbox/jest/factory'; - -import { render, screen, waitFor } from '../../../jest/test-helpers'; -import { normalizeInstance } from '../../../normalizers'; -import UI from '../index'; -import { WrappedRoute } from '../util/react-router-helpers'; - -const TestableComponent = () => ( - - - - - Sign in - - {/* WrappedRount will redirect to /login for logged out users... which will resolve to the route above! */} - null} /> - -); - -describe('', () => { - let store: any; - - beforeEach(() => { - store = { - me: false, - accounts: { - '1': buildAccount({ - id: '1', - acct: 'username', - display_name: 'My name', - avatar: 'test.jpg', - }), - }, - instance: normalizeInstance({ registrations: true }), - }; - }); - - describe('when logged out', () => { - describe('when viewing a Profile Page', () => { - it('should render the Profile page', async() => { - render( - , - {}, - store, - { initialEntries: ['/@username'] }, - ); - - await waitFor(() => { - expect(screen.getByTestId('cta-banner')).toHaveTextContent('Sign up now to discuss'); - }, { - timeout: 5000, - }); - }); - }); - - describe('when viewing a Status Page', () => { - it('should render the Status page', async() => { - render( - , - {}, - store, - { initialEntries: ['/@username/posts/12'] }, - ); - - await waitFor(() => { - expect(screen.getByTestId('cta-banner')).toHaveTextContent('Sign up now to discuss'); - }); - }); - }); - - describe('when viewing Notifications', () => { - it('should redirect to the login page', async() => { - render( - , - {}, - store, - { initialEntries: ['/notifications'] }, - ); - - await waitFor(() => { - expect(screen.getByTestId('sign-in')).toHaveTextContent('Sign in'); - }); - }); - }); - }); -}); diff --git a/app/soapbox/features/ui/components/modals/account-moderation-modal/account-moderation-modal.tsx b/app/soapbox/features/ui/components/modals/account-moderation-modal/account-moderation-modal.tsx index 016759205..2d5958f2d 100644 --- a/app/soapbox/features/ui/components/modals/account-moderation-modal/account-moderation-modal.tsx +++ b/app/soapbox/features/ui/components/modals/account-moderation-modal/account-moderation-modal.tsx @@ -1,15 +1,10 @@ import React, { ChangeEventHandler, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { - verifyUser, - unverifyUser, - suggestUsers, - unsuggestUsers, - setBadges as saveBadges, -} from 'soapbox/actions/admin'; +import { setBadges as saveBadges } from 'soapbox/actions/admin'; import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation'; import { useAccount } from 'soapbox/api/hooks'; +import { useSuggest, useVerify } from 'soapbox/api/hooks/admin'; import Account from 'soapbox/components/account'; import List, { ListItem } from 'soapbox/components/list'; import MissingIndicator from 'soapbox/components/missing-indicator'; @@ -45,6 +40,8 @@ const AccountModerationModal: React.FC = ({ onClose, ac const intl = useIntl(); const dispatch = useAppDispatch(); + const { suggest, unsuggest } = useSuggest(); + const { verify, unverify } = useVerify(); const { account: ownAccount } = useOwnAccount(); const features = useFeatures(); const { account } = useAccount(accountId); @@ -70,22 +67,22 @@ const AccountModerationModal: React.FC = ({ onClose, ac const { checked } = e.target; const message = checked ? messages.userVerified : messages.userUnverified; - const action = checked ? verifyUser : unverifyUser; + const action = checked ? verify : unverify; - dispatch(action(account.id)) - .then(() => toast.success(intl.formatMessage(message, { acct: account.acct }))) - .catch(() => {}); + action([account.id], { + onSuccess: () => toast.success(intl.formatMessage(message, { acct: account.acct })), + }); }; const handleSuggestedChange: ChangeEventHandler = (e) => { const { checked } = e.target; const message = checked ? messages.userSuggested : messages.userUnsuggested; - const action = checked ? suggestUsers : unsuggestUsers; + const action = checked ? suggest : unsuggest; - dispatch(action([account.id])) - .then(() => toast.success(intl.formatMessage(message, { acct: account.acct }))) - .catch(() => {}); + action([account.id], { + onSuccess: () => toast.success(intl.formatMessage(message, { acct: account.acct })), + }); }; const handleDeactivate = () => { diff --git a/app/soapbox/features/ui/components/modals/media-modal.tsx b/app/soapbox/features/ui/components/modals/media-modal.tsx index 875506175..d7310dea0 100644 --- a/app/soapbox/features/ui/components/modals/media-modal.tsx +++ b/app/soapbox/features/ui/components/modals/media-modal.tsx @@ -110,14 +110,8 @@ const MediaModal: React.FC = (props) => { const handleStatusClick: React.MouseEventHandler = e => { if (status && e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); - - dispatch((_, getState) => { - const account = typeof status.account === 'string' ? getState().accounts.get(status.account) : status.account; - if (!account) return; - - history.push(`/@${account.acct}/posts/${status?.id}`); - onClose(); - }); + history.push(`/@${status.account.acct}/posts/${status?.id}`); + onClose(); } }; diff --git a/app/soapbox/features/ui/components/modals/reply-mentions-modal.tsx b/app/soapbox/features/ui/components/modals/reply-mentions-modal.tsx index c09fe9918..00fb4c214 100644 --- a/app/soapbox/features/ui/components/modals/reply-mentions-modal.tsx +++ b/app/soapbox/features/ui/components/modals/reply-mentions-modal.tsx @@ -2,7 +2,7 @@ import React, { useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; import { Modal } from 'soapbox/components/ui'; -import { useAppSelector, useCompose } from 'soapbox/hooks'; +import { useAppSelector, useCompose, useOwnAccount } from 'soapbox/hooks'; import { statusToMentionsAccountIdsArray } from 'soapbox/reducers/compose'; import { makeGetStatus } from 'soapbox/selectors'; @@ -20,7 +20,7 @@ const ReplyMentionsModal: React.FC = ({ composeId, onClose const getStatus = useCallback(makeGetStatus(), []); const status = useAppSelector(state => getStatus(state, { id: compose.in_reply_to! })); - const account = useAppSelector((state) => state.accounts.get(state.me)); + const { account } = useOwnAccount(); const mentions = statusToMentionsAccountIdsArray(status!, account!); const author = (status?.account as AccountEntity).id; diff --git a/app/soapbox/features/ui/components/modals/unauthorized-modal.tsx b/app/soapbox/features/ui/components/modals/unauthorized-modal.tsx index d5de86ba7..c2e5f7965 100644 --- a/app/soapbox/features/ui/components/modals/unauthorized-modal.tsx +++ b/app/soapbox/features/ui/components/modals/unauthorized-modal.tsx @@ -5,6 +5,7 @@ import { useHistory } from 'react-router-dom'; import { remoteInteraction } from 'soapbox/actions/interactions'; import { Button, Form, Input, Modal, Stack, Text } from 'soapbox/components/ui'; import { useAppSelector, useAppDispatch, useFeatures, useInstance, useRegistrationStatus } from 'soapbox/hooks'; +import { selectAccount } from 'soapbox/selectors'; import toast from 'soapbox/toast'; const messages = defineMessages({ @@ -32,7 +33,7 @@ const UnauthorizedModal: React.FC = ({ action, onClose, acco const instance = useInstance(); const { isOpen } = useRegistrationStatus(); - const username = useAppSelector(state => state.accounts.get(accountId)?.display_name); + const username = useAppSelector(state => selectAccount(state, accountId!)?.display_name); const features = useFeatures(); const [account, setAccount] = useState(''); diff --git a/app/soapbox/locales/messages.ts b/app/soapbox/locales/messages.ts index d1e355012..1f69648e0 100644 --- a/app/soapbox/locales/messages.ts +++ b/app/soapbox/locales/messages.ts @@ -26,7 +26,7 @@ const importMessagesWithCustom = (locale: string): Promise => { }); }; -const locales: string[] = [ +const locales = [ 'ar', 'ast', 'bg', @@ -91,7 +91,7 @@ const locales: string[] = [ 'zh-CN', 'zh-HK', 'zh-TW', -]; +] as const; /** Soapbox locales map */ const messages = locales.reduce((acc, locale) => { @@ -100,3 +100,4 @@ const messages = locales.reduce((acc, locale) => { }, {} as Record Promise>); export default messages; +export { locales }; \ No newline at end of file diff --git a/app/soapbox/middleware/errors.ts b/app/soapbox/middleware/errors.ts deleted file mode 100644 index 9e423a685..000000000 --- a/app/soapbox/middleware/errors.ts +++ /dev/null @@ -1,33 +0,0 @@ -import toast from 'soapbox/toast'; - -import type { AnyAction } from 'redux'; -import type { ThunkMiddleware } from 'redux-thunk'; - -/** Whether the action is considered a failure. */ -const isFailType = (type: string): boolean => type.endsWith('_FAIL'); - -/** Whether the action is a failure to fetch from browser storage. */ -const isRememberFailType = (type: string): boolean => type.endsWith('_REMEMBER_FAIL'); - -/** Whether the error contains an Axios response. */ -const hasResponse = (error: any): boolean => Boolean(error && error.response); - -/** Don't show 401's. */ -const authorized = (error: any): boolean => error?.response?.status !== 401; - -/** Whether the error should be shown to the user. */ -const shouldShowError = ({ type, skipAlert, error }: AnyAction): boolean => { - return !skipAlert && hasResponse(error) && authorized(error) && isFailType(type) && !isRememberFailType(type); -}; - -/** Middleware to display Redux errors to the user. */ -const errorsMiddleware = (): ThunkMiddleware => - () => next => action => { - if (shouldShowError(action)) { - toast.showAlertForError(action.error); - } - - return next(action); - }; - -export default errorsMiddleware; diff --git a/app/soapbox/reducers/__tests__/accounts.test.ts b/app/soapbox/reducers/__tests__/accounts.test.ts deleted file mode 100644 index 02baa6d92..000000000 --- a/app/soapbox/reducers/__tests__/accounts.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Map as ImmutableMap, Record as ImmutableRecord } from 'immutable'; - -import { ACCOUNT_IMPORT } from 'soapbox/actions/importer'; - -import reducer from '../accounts'; - -describe('accounts reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {} as any)).toEqual(ImmutableMap()); - }); - - describe('ACCOUNT_IMPORT', () => { - it('parses the account as a Record', () => { - const account = require('soapbox/__fixtures__/pleroma-account.json'); - const action = { type: ACCOUNT_IMPORT, account }; - const result = reducer(undefined, action).get('9v5bmRalQvjOy0ECcC'); - - expect(ImmutableRecord.isRecord(result)).toBe(true); - }); - - it('minifies a moved account', () => { - const account = require('soapbox/__fixtures__/account-moved.json'); - const action = { type: ACCOUNT_IMPORT, account }; - const result = reducer(undefined, action).get('106801667066418367'); - - expect(result?.moved).toBe('107945464165013501'); - }); - }); -}); diff --git a/app/soapbox/reducers/__tests__/auth.test.ts b/app/soapbox/reducers/__tests__/auth.test.ts index 9f5726f77..d461b3ddd 100644 --- a/app/soapbox/reducers/__tests__/auth.test.ts +++ b/app/soapbox/reducers/__tests__/auth.test.ts @@ -1,4 +1,4 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; +import { Map as ImmutableMap } from 'immutable'; import { AUTH_APP_CREATED, @@ -300,7 +300,7 @@ describe('auth reducer', () => { it('sets the value of `me`', () => { const action = { type: SWITCH_ACCOUNT, - account: fromJS({ url: 'https://gleasonator.com/users/benis' }), + account: { url: 'https://gleasonator.com/users/benis' }, }; const result = reducer(undefined, action); diff --git a/app/soapbox/reducers/__tests__/index.test.ts b/app/soapbox/reducers/__tests__/index.test.ts index e5674e91b..2ef98847c 100644 --- a/app/soapbox/reducers/__tests__/index.test.ts +++ b/app/soapbox/reducers/__tests__/index.test.ts @@ -6,7 +6,6 @@ describe('root reducer', () => { it('should return the initial state', () => { const result = reducer(undefined, {} as any); expect(ImmutableRecord.isRecord(result)).toBe(true); - expect(result.accounts.get('')).toBe(undefined); expect(result.instance.version).toEqual('0.0.0'); }); }); diff --git a/app/soapbox/reducers/accounts.ts b/app/soapbox/reducers/accounts.ts index 39176e731..5587e0a73 100644 --- a/app/soapbox/reducers/accounts.ts +++ b/app/soapbox/reducers/accounts.ts @@ -23,10 +23,6 @@ import { ADMIN_USERS_DELETE_FAIL, ADMIN_USERS_DEACTIVATE_REQUEST, ADMIN_USERS_DEACTIVATE_FAIL, - ADMIN_USERS_SUGGEST_REQUEST, - ADMIN_USERS_SUGGEST_FAIL, - ADMIN_USERS_UNSUGGEST_REQUEST, - ADMIN_USERS_UNSUGGEST_FAIL, } from 'soapbox/actions/admin'; import { CHATS_FETCH_SUCCESS, CHATS_EXPAND_SUCCESS, CHAT_FETCH_SUCCESS } from 'soapbox/actions/chats'; import { @@ -234,14 +230,6 @@ const importAdminUsers = (state: State, adminUsers: Array>): }); }; -const setSuggested = (state: State, accountIds: Array, isSuggested: boolean): State => { - return state.withMutations(state => { - accountIds.forEach(id => { - state.setIn([id, 'pleroma', 'is_suggested'], isSuggested); - }); - }); -}; - export default function accounts(state: State = initialState, action: AnyAction): State { switch (action.type) { case ACCOUNT_IMPORT: @@ -280,12 +268,6 @@ export default function accounts(state: State = initialState, action: AnyAction) return setActive(state, action.accountIds, true); case ADMIN_USERS_FETCH_SUCCESS: return importAdminUsers(state, action.users); - case ADMIN_USERS_SUGGEST_REQUEST: - case ADMIN_USERS_UNSUGGEST_FAIL: - return setSuggested(state, action.accountIds, true); - case ADMIN_USERS_UNSUGGEST_REQUEST: - case ADMIN_USERS_SUGGEST_FAIL: - return setSuggested(state, action.accountIds, false); default: return state; } diff --git a/app/soapbox/reducers/auth.ts b/app/soapbox/reducers/auth.ts index 82f02ac80..bf9d22a59 100644 --- a/app/soapbox/reducers/auth.ts +++ b/app/soapbox/reducers/auth.ts @@ -347,7 +347,7 @@ const reducer = (state: State, action: AnyAction) => { case VERIFY_CREDENTIALS_FAIL: return deleteForbiddenToken(state, action.error, action.token); case SWITCH_ACCOUNT: - return state.set('me', action.account.get('url')); + return state.set('me', action.account.url); case ME_FETCH_SKIP: return state.set('me', null); case MASTODON_PRELOAD_IMPORT: diff --git a/app/soapbox/reducers/index.ts b/app/soapbox/reducers/index.ts index a52ea0aa3..142695474 100644 --- a/app/soapbox/reducers/index.ts +++ b/app/soapbox/reducers/index.ts @@ -1,12 +1,9 @@ import { Record as ImmutableRecord } from 'immutable'; import { combineReducers } from 'redux-immutable'; -import { createSelector } from 'reselect'; import { AUTH_LOGGED_OUT } from 'soapbox/actions/auth'; import * as BuildConfig from 'soapbox/build-config'; -import { Entities } from 'soapbox/entity-store/entities'; import entities from 'soapbox/entity-store/reducer'; -import { immutableizeStore, type LegacyStore } from 'soapbox/utils/legacy'; import account_notes from './account-notes'; import accounts_meta from './accounts-meta'; @@ -70,12 +67,7 @@ import trends from './trends'; import user_lists from './user-lists'; import verification from './verification'; -import type { AnyAction, Reducer } from 'redux'; -import type { EntityStore } from 'soapbox/entity-store/types'; -import type { Account } from 'soapbox/schemas'; - const reducers = { - accounts: ((state: any = {}) => state) as (state: any) => EntityStore & LegacyStore, account_notes, accounts_meta, admin, @@ -175,19 +167,4 @@ const rootReducer: typeof appReducer = (state, action) => { } }; -type InferState = R extends Reducer ? S : never; - -const accountsSelector = createSelector( - (state: InferState) => state.entities[Entities.ACCOUNTS]?.store as EntityStore || {}, - (accounts) => immutableizeStore>(accounts), -); - -const extendedRootReducer = ( - state: InferState, - action: AnyAction, -): ReturnType => { - const extendedState = rootReducer(state, action); - return extendedState.set('accounts', accountsSelector(extendedState)); -}; - -export default extendedRootReducer as Reducer>; +export default rootReducer; \ No newline at end of file diff --git a/app/soapbox/schemas/soapbox/settings.ts b/app/soapbox/schemas/soapbox/settings.ts new file mode 100644 index 000000000..1b0db4d86 --- /dev/null +++ b/app/soapbox/schemas/soapbox/settings.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; + +import { locales } from 'soapbox/locales/messages'; + +const skinToneSchema = z.union([ + z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5), z.literal(6), +]); + +const settingsSchema = z.object({ + onboarded: z.boolean().catch(false), + skinTone: skinToneSchema.catch(1), + reduceMotion: z.boolean().catch(false), + underlineLinks: z.boolean().catch(false), + autoPlayGif: z.boolean().catch(true), + displayMedia: z.enum(['default', 'hide_all', 'show_all']).catch('default'), + expandSpoilers: z.boolean().catch(false), + unfollowModal: z.boolean().catch(false), + boostModal: z.boolean().catch(false), + deleteModal: z.boolean().catch(true), + missingDescriptionModal: z.boolean().catch(false), + defaultPrivacy: z.enum(['public', 'unlisted', 'private', 'direct']).catch('public'), + defaultContentType: z.enum(['text/plain', 'text/markdown']).catch('text/plain'), + themeMode: z.enum(['system', 'light', 'dark']).catch('system'), + locale: z.string().catch(navigator.language).pipe(z.enum(locales)).catch('en'), + showExplanationBox: z.boolean().catch(true), + explanationBox: z.boolean().catch(true), + autoloadTimelines: z.boolean().catch(true), + autoloadMore: z.boolean().catch(true), + systemFont: z.boolean().catch(false), + demetricator: z.boolean().catch(false), + isDeveloper: z.boolean().catch(false), +}); + +type Settings = z.infer; + +export { settingsSchema, type Settings }; \ No newline at end of file diff --git a/app/soapbox/selectors/index.ts b/app/soapbox/selectors/index.ts index de3ad4734..1f844bc27 100644 --- a/app/soapbox/selectors/index.ts +++ b/app/soapbox/selectors/index.ts @@ -14,13 +14,27 @@ import ConfigDB from 'soapbox/utils/config-db'; import { getFeatures } from 'soapbox/utils/features'; import { shouldFilter } from 'soapbox/utils/timelines'; +import type { EntityStore } from 'soapbox/entity-store/types'; import type { ContextType } from 'soapbox/normalizers/filter'; import type { ReducerChat } from 'soapbox/reducers/chats'; +import type { Account as AccountSchema } from 'soapbox/schemas'; import type { RootState } from 'soapbox/store'; import type { Account, Filter as FilterEntity, Notification, Status } from 'soapbox/types/entities'; const normalizeId = (id: any): string => typeof id === 'string' ? id : ''; +export function selectAccount(state: RootState, accountId: string) { + return state.entities[Entities.ACCOUNTS]?.store[accountId] as AccountSchema | undefined; +} + +export function selectOwnAccount(state: RootState) { + if (state.me) { + return selectAccount(state, state.me); + } +} + +export const accountIdsToAccts = (state: RootState, ids: string[]) => ids.map((id) => selectAccount(state, id)!.acct); + const getAccountBase = (state: RootState, id: string) => state.entities[Entities.ACCOUNTS]?.store[id] as Account | undefined; const getAccountRelationship = (state: RootState, id: string) => state.relationships.get(id); @@ -144,8 +158,8 @@ export const makeGetStatus = () => { export const makeGetNotification = () => { return createSelector([ (_state: RootState, notification: Notification) => notification, - (state: RootState, notification: Notification) => state.accounts.get(normalizeId(notification.account)), - (state: RootState, notification: Notification) => state.accounts.get(normalizeId(notification.target)), + (state: RootState, notification: Notification) => selectAccount(state, normalizeId(notification.account)), + (state: RootState, notification: Notification) => selectAccount(state, normalizeId(notification.target)), (state: RootState, notification: Notification) => state.statuses.get(normalizeId(notification.status)), ], (notification, account, target, status) => { return notification.merge({ @@ -193,7 +207,7 @@ export const makeGetChat = () => { return createSelector( [ (state: RootState, { id }: APIChat) => state.chats.items.get(id) as ReducerChat, - (state: RootState, { id }: APIChat) => state.accounts.get(state.chats.items.getIn([id, 'account'])), + (state: RootState, { id }: APIChat) => selectAccount(state, state.chats.items.getIn([id, 'account']) as string), (state: RootState, { last_message }: APIChat) => state.chat_messages.get(last_message), ], @@ -216,10 +230,8 @@ export const makeGetReport = () => { return createSelector( [ (state: RootState, id: string) => state.admin.reports.get(id), - (state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.account || ''), - (state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.target_account || ''), - // (state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.action_taken_by_account || ''), - // (state: RootState, id: string) => state.accounts.get(state.admin.reports.get(id)?.assigned_account || ''), + (state: RootState, id: string) => selectAccount(state, state.admin.reports.get(id)?.account || ''), + (state: RootState, id: string) => selectAccount(state, state.admin.reports.get(id)?.target_account || ''), (state: RootState, id: string) => ImmutableList(fromJS(state.admin.reports.get(id)?.statuses)).map( statusId => state.statuses.get(normalizeId(statusId))) .filter((s: any) => s) @@ -255,7 +267,7 @@ const getAuthUserIds = createSelector([ export const makeGetOtherAccounts = () => { return createSelector([ - (state: RootState) => state.accounts, + (state: RootState) => state.entities[Entities.ACCOUNTS]?.store as EntityStore, getAuthUserIds, (state: RootState) => state.me, ], @@ -263,7 +275,7 @@ export const makeGetOtherAccounts = () => { return authUserIds .reduce((list: ImmutableList, id: string) => { if (id === me) return list; - const account = accounts.get(id); + const account = accounts[id]; return account ? list.push(account) : list; }, ImmutableList()); }); @@ -276,10 +288,11 @@ const getSimplePolicy = createSelector([ return instancePolicy.merge(ConfigDB.toSimplePolicy(configs)); }); -const getRemoteInstanceFavicon = (state: RootState, host: string) => ( - (state.accounts.find(account => getDomain(account) === host) || ImmutableMap()) - .getIn(['pleroma', 'favicon']) -); +const getRemoteInstanceFavicon = (state: RootState, host: string) => { + const accounts = state.entities[Entities.ACCOUNTS]?.store as EntityStore; + const account = Object.entries(accounts).find(([_, account]) => account && getDomain(account) === host)?.[1]; + return account?.pleroma?.favicon; +}; const getRemoteInstanceFederation = (state: RootState, host: string) => ( getSimplePolicy(state) diff --git a/app/soapbox/store.ts b/app/soapbox/store.ts index 8aeb0a791..9b0136a00 100644 --- a/app/soapbox/store.ts +++ b/app/soapbox/store.ts @@ -1,7 +1,6 @@ import { configureStore } from '@reduxjs/toolkit'; import thunk, { ThunkDispatch } from 'redux-thunk'; -import errorsMiddleware from './middleware/errors'; import soundsMiddleware from './middleware/sounds'; import appReducer from './reducers'; @@ -11,7 +10,6 @@ export const store = configureStore({ reducer: appReducer, middleware: [ thunk, - errorsMiddleware(), soundsMiddleware(), ], devTools: true, diff --git a/app/soapbox/utils/auth.ts b/app/soapbox/utils/auth.ts index 066025422..2d94e1054 100644 --- a/app/soapbox/utils/auth.ts +++ b/app/soapbox/utils/auth.ts @@ -1,5 +1,7 @@ import { List as ImmutableList } from 'immutable'; +import { selectAccount, selectOwnAccount } from 'soapbox/selectors'; + import type { RootState } from 'soapbox/store'; export const validId = (id: any) => typeof id === 'string' && id !== 'null' && id !== 'undefined'; @@ -22,10 +24,7 @@ export const parseBaseURL = (url: any) => { } }; -export const getLoggedInAccount = (state: RootState) => { - const me = state.me; - return state.accounts.get(me); -}; +export const getLoggedInAccount = (state: RootState) => selectOwnAccount(state); export const isLoggedIn = (getState: () => RootState) => { return validId(getState().me); @@ -35,7 +34,7 @@ export const getAppToken = (state: RootState) => state.auth.app.access_token as export const getUserToken = (state: RootState, accountId?: string | false | null) => { if (!accountId) return; - const accountUrl = state.accounts[accountId]?.url; + const accountUrl = selectAccount(state, accountId)?.url; if (!accountUrl) return; return state.auth.users.get(accountUrl)?.access_token; }; @@ -66,3 +65,5 @@ export const getAuthUserUrl = (state: RootState) => { /** Get the VAPID public key. */ export const getVapidKey = (state: RootState) => (state.auth.app.vapid_key || state.instance.pleroma.get('vapid_public_key')) as string; + +export const getMeUrl = (state: RootState) => selectOwnAccount(state)?.url; \ No newline at end of file diff --git a/app/soapbox/utils/state.ts b/app/soapbox/utils/state.ts index c3cfdb570..9563bd542 100644 --- a/app/soapbox/utils/state.ts +++ b/app/soapbox/utils/state.ts @@ -6,6 +6,7 @@ import { getSoapboxConfig } from 'soapbox/actions/soapbox'; import * as BuildConfig from 'soapbox/build-config'; import { isPrerendered } from 'soapbox/precheck'; +import { selectOwnAccount } from 'soapbox/selectors'; import { isURL } from 'soapbox/utils/auth'; import type { RootState } from 'soapbox/store'; @@ -39,6 +40,6 @@ const getHost = (url: any): string => { /** Get the baseURL of the instance. */ export const getBaseURL = (state: RootState): string => { - const account = state.accounts.get(state.me); + const account = selectOwnAccount(state); return isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : getHost(account?.url); }; diff --git a/package.json b/package.json index 5476a8caf..3e2189bda 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,7 @@ "lint": "${npm_execpath} run lint:js && ${npm_execpath} run lint:sass", "lint:js": "npx eslint --ext .js,.jsx,.cjs,.mjs,.ts,.tsx . --cache", "lint:sass": "npx stylelint app/styles/**/*.scss", - "prepare": "husky install", - "storybook": "start-storybook -p 6006", - "build-storybook": "build-storybook" + "prepare": "husky install" }, "license": "AGPL-3.0-or-later", "browserslist": [ @@ -89,7 +87,7 @@ "@types/react-datepicker": "^4.4.2", "@types/react-dom": "^18.0.10", "@types/react-helmet": "^6.1.5", - "@types/react-motion": "^0.0.33", + "@types/react-motion": "^0.0.34", "@types/react-router-dom": "^5.3.3", "@types/react-sparklines": "^1.7.2", "@types/react-swipeable-views": "^0.13.1", @@ -209,15 +207,6 @@ "@gitbeaker/node": "^35.8.0", "@jedmao/redux-mock-store": "^3.0.5", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", - "@storybook/addon-actions": "^6.5.16", - "@storybook/addon-essentials": "^6.5.16", - "@storybook/addon-interactions": "^6.5.16", - "@storybook/addon-links": "^6.5.16", - "@storybook/addon-postcss": "^2.0.0", - "@storybook/builder-webpack5": "^6.5.16", - "@storybook/manager-webpack5": "^6.5.16", - "@storybook/react": "^6.5.16", - "@storybook/testing-library": "^0.0.13", "@tailwindcss/aspect-ratio": "^0.4.2", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react-hooks": "^8.0.1", @@ -231,7 +220,7 @@ "eslint": "^8.0.0", "eslint-plugin-compat": "^4.0.2", "eslint-plugin-import": "^2.25.4", - "eslint-plugin-jsdoc": "^43.1.1", + "eslint-plugin-jsdoc": "^46.0.0", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-promise": "^6.0.0", "eslint-plugin-react": "^7.25.1", @@ -246,7 +235,6 @@ "raf": "^3.4.1", "react-intl-translations-manager": "^5.0.3", "react-refresh": "^0.14.0", - "storybook-react-intl": "^1.1.1", "stylelint": "^14.0.0", "stylelint-config-standard-scss": "^6.1.0", "tailwindcss": "^3.3.1", diff --git a/stories/Button.stories.tsx b/stories/Button.stories.tsx deleted file mode 100644 index 796d17e96..000000000 --- a/stories/Button.stories.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import React from 'react'; - -import { Button } from 'soapbox/components/ui'; - -// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export -export default { - title: 'UI/Button', - component: Button, - // More on argTypes: https://storybook.js.org/docs/react/api/argtypes - argTypes: { - text: { type: 'string', defaultValue: 'Button' }, - theme: { defaultValue: 'primary' }, - size: { defaultValue: 'md' }, - disabled: { defaultValue: false }, - block: { defaultValue: false }, - children: { table: { disable: true } }, - className: { table: { disable: true } }, - type: { table: { disable: true } }, - to: { table: { disable: true } }, - icon: { table: { disable: true } }, - onClick: { table: { disable: true } }, - }, -} as ComponentMeta; - -// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args -const Template: ComponentStory = (args) =>