diff --git a/app/soapbox/actions/aliases.ts b/app/soapbox/actions/aliases.ts index b7856cbe0..e485ea14e 100644 --- a/app/soapbox/actions/aliases.ts +++ b/app/soapbox/actions/aliases.ts @@ -10,8 +10,8 @@ import { importFetchedAccounts } from './importer'; import { patchMeSuccess } from './me'; import type { AxiosError } from 'axios'; +import type { Account } from 'soapbox/schemas'; import type { AppDispatch, RootState } from 'soapbox/store'; -import type { APIEntity, Account } from 'soapbox/types/entities'; const ALIASES_FETCH_REQUEST = 'ALIASES_FETCH_REQUEST'; const ALIASES_FETCH_SUCCESS = 'ALIASES_FETCH_SUCCESS'; @@ -56,7 +56,7 @@ const fetchAliasesRequest = () => ({ type: ALIASES_FETCH_REQUEST, }); -const fetchAliasesSuccess = (aliases: APIEntity[]) => ({ +const fetchAliasesSuccess = (aliases: unknown[]) => ({ type: ALIASES_FETCH_SUCCESS, value: aliases, }); @@ -82,7 +82,7 @@ const fetchAliasesSuggestions = (q: string) => }).catch(error => toast.showAlertForError(error)); }; -const fetchAliasesSuggestionsReady = (query: string, accounts: APIEntity[]) => ({ +const fetchAliasesSuggestionsReady = (query: string, accounts: unknown[]) => ({ type: ALIASES_SUGGESTIONS_READY, query, accounts, diff --git a/app/soapbox/api/hooks/accounts/useAccount.ts b/app/soapbox/api/hooks/accounts/useAccount.ts index 2442ad642..222a7c94f 100644 --- a/app/soapbox/api/hooks/accounts/useAccount.ts +++ b/app/soapbox/api/hooks/accounts/useAccount.ts @@ -3,22 +3,25 @@ import { useEntity } from 'soapbox/entity-store/hooks'; import { useApi } from 'soapbox/hooks/useApi'; import { type Account, accountSchema } from 'soapbox/schemas'; - import { useRelationships } from './useRelationships'; -function useAccount(id: string) { +function useAccount(accountId?: string) { const api = useApi(); const { entity: account, ...result } = useEntity( - [Entities.ACCOUNTS, id], - () => api.get(`/api/v1/accounts/${id}`), - { schema: accountSchema }, + [Entities.ACCOUNTS, accountId || ''], + () => api.get(`/api/v1/accounts/${accountId}`), + { schema: accountSchema, enabled: !!accountId }, ); - const { relationships, isLoading } = useRelationships([account?.id as string]); + const { + relationships, + isLoading: isRelationshipLoading, + } = useRelationships(accountId ? [accountId] : []); return { ...result, - isLoading: result.isLoading || isLoading, + isLoading: result.isLoading, + isRelationshipLoading, account: account ? { ...account, relationship: relationships[0] || null } : undefined, }; } diff --git a/app/soapbox/api/hooks/accounts/usePatronUser.ts b/app/soapbox/api/hooks/accounts/usePatronUser.ts new file mode 100644 index 000000000..283f02b3d --- /dev/null +++ b/app/soapbox/api/hooks/accounts/usePatronUser.ts @@ -0,0 +1,18 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntity } from 'soapbox/entity-store/hooks'; +import { useApi } from 'soapbox/hooks/useApi'; +import { type PatronUser, patronUserSchema } from 'soapbox/schemas'; + +function usePatronUser(url?: string) { + const api = useApi(); + + const { entity: patronUser, ...result } = useEntity( + [Entities.PATRON_USERS, url || ''], + () => api.get(`/api/patron/v1/accounts/${encodeURIComponent(url!)}`), + { schema: patronUserSchema, enabled: !!url }, + ); + + return { patronUser, ...result }; +} + +export { usePatronUser }; \ No newline at end of file diff --git a/app/soapbox/api/hooks/index.ts b/app/soapbox/api/hooks/index.ts index ade03f799..e411e2133 100644 --- a/app/soapbox/api/hooks/index.ts +++ b/app/soapbox/api/hooks/index.ts @@ -3,6 +3,7 @@ * Accounts */ export { useAccount } from './accounts/useAccount'; +export { usePatronUser } from './accounts/usePatronUser'; /** * Groups diff --git a/app/soapbox/components/profile-hover-card.tsx b/app/soapbox/components/profile-hover-card.tsx index d0c8a93d4..98487f4ac 100644 --- a/app/soapbox/components/profile-hover-card.tsx +++ b/app/soapbox/components/profile-hover-card.tsx @@ -9,32 +9,30 @@ import { closeProfileHoverCard, updateProfileHoverCard, } from 'soapbox/actions/profile-hover-card'; +import { useAccount, usePatronUser } from 'soapbox/api/hooks'; import Badge from 'soapbox/components/badge'; import ActionButton from 'soapbox/features/ui/components/action-button'; import BundleContainer from 'soapbox/features/ui/containers/bundle-container'; import { UserPanel } from 'soapbox/features/ui/util/async-components'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; import { isLocal } from 'soapbox/utils/accounts'; import { showProfileHoverCard } from './hover-ref-wrapper'; import { Card, CardBody, HStack, Icon, Stack, Text } from './ui'; +import type { Account, PatronUser } from 'soapbox/schemas'; import type { AppDispatch } from 'soapbox/store'; -import type { Account } from 'soapbox/types/entities'; -const getAccount = makeGetAccount(); - -const getBadges = (account: Account): JSX.Element[] => { +const getBadges = (account?: Account, patronUser?: PatronUser): JSX.Element[] => { const badges = []; - if (account.admin) { + if (account?.admin) { badges.push(); - } else if (account.moderator) { + } else if (account?.moderator) { badges.push(); } - if (account.getIn(['patron', 'is_patron'])) { + if (patronUser?.is_patron) { badges.push(); } @@ -67,9 +65,10 @@ export const ProfileHoverCard: React.FC = ({ visible = true } const me = useAppSelector(state => state.me); const accountId: string | undefined = useAppSelector(state => state.profile_hover_card.accountId || undefined); - const account = useAppSelector(state => accountId && getAccount(state, accountId)); + const { account } = useAccount(accountId); + const { patronUser } = usePatronUser(account?.url); const targetRef = useAppSelector(state => state.profile_hover_card.ref?.current); - const badges = account ? getBadges(account) : []; + const badges = getBadges(account, patronUser); useEffect(() => { if (accountId) dispatch(fetchRelationships([accountId])); @@ -112,7 +111,7 @@ export const ProfileHoverCard: React.FC = ({ visible = true } {Component => ( } badges={badges} /> diff --git a/app/soapbox/components/sidebar-menu.tsx b/app/soapbox/components/sidebar-menu.tsx index 241e3e837..f77e9f5ed 100644 --- a/app/soapbox/components/sidebar-menu.tsx +++ b/app/soapbox/components/sidebar-menu.tsx @@ -1,17 +1,18 @@ /* eslint-disable jsx-a11y/interactive-supports-focus */ import clsx from 'clsx'; -import React from 'react'; +import React, { useCallback } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { Link, NavLink } from 'react-router-dom'; import { fetchOwnAccounts, logOut, switchAccount } from 'soapbox/actions/auth'; import { getSettings } from 'soapbox/actions/settings'; import { closeSidebar } from 'soapbox/actions/sidebar'; +import { useAccount } from 'soapbox/api/hooks'; import Account from 'soapbox/components/account'; import { Stack } from 'soapbox/components/ui'; import ProfileStats from 'soapbox/features/ui/components/profile-stats'; import { useAppDispatch, useAppSelector, useGroupsPath, useFeatures } from 'soapbox/hooks'; -import { makeGetAccount, makeGetOtherAccounts } from 'soapbox/selectors'; +import { makeGetOtherAccounts } from 'soapbox/selectors'; import { Divider, HStack, Icon, IconButton, Text } from './ui'; @@ -76,16 +77,14 @@ const SidebarLink: React.FC = ({ href, to, icon, text, onClick }) ); }; -const getOtherAccounts = makeGetOtherAccounts(); - const SidebarMenu: React.FC = (): JSX.Element | null => { const intl = useIntl(); const dispatch = useAppDispatch(); + const getOtherAccounts = useCallback(makeGetOtherAccounts(), []); const features = useFeatures(); - const getAccount = makeGetAccount(); const me = useAppSelector((state) => state.me); - const account = useAppSelector((state) => me ? getAccount(state, me) : null); + const { account } = useAccount(me || undefined); const otherAccounts: ImmutableList = useAppSelector((state) => getOtherAccounts(state)); const sidebarOpen = useAppSelector((state) => state.sidebar.sidebarOpen); const settings = useAppSelector((state) => getSettings(state)); diff --git a/app/soapbox/containers/account-container.tsx b/app/soapbox/containers/account-container.tsx index 54c6db64e..6b41aef87 100644 --- a/app/soapbox/containers/account-container.tsx +++ b/app/soapbox/containers/account-container.tsx @@ -1,17 +1,14 @@ -import React, { useCallback } from 'react'; +import React from 'react'; -import { useAppSelector } from 'soapbox/hooks'; - -import Account, { IAccount } from '../components/account'; -import { makeGetAccount } from '../selectors'; +import { useAccount } from 'soapbox/api/hooks'; +import Account, { IAccount } from 'soapbox/components/account'; interface IAccountContainer extends Omit { id: string } const AccountContainer: React.FC = ({ id, ...props }) => { - const getAccount = useCallback(makeGetAccount(), []); - const account = useAppSelector(state => getAccount(state, id)); + const { account } = useAccount(id); return ( diff --git a/app/soapbox/entity-store/entities.ts b/app/soapbox/entity-store/entities.ts index 9878cbbf2..b8129940e 100644 --- a/app/soapbox/entity-store/entities.ts +++ b/app/soapbox/entity-store/entities.ts @@ -4,6 +4,7 @@ export enum Entities { GROUP_MEMBERSHIPS = 'GroupMemberships', GROUP_RELATIONSHIPS = 'GroupRelationships', GROUP_TAGS = 'GroupTags', + PATRON_USERS = 'PatronUsers', RELATIONSHIPS = 'Relationships', STATUSES = 'Statuses' } \ No newline at end of file diff --git a/app/soapbox/features/admin/components/unapproved-account.tsx b/app/soapbox/features/admin/components/unapproved-account.tsx index 9aa1ba4fe..519d2cd6a 100644 --- a/app/soapbox/features/admin/components/unapproved-account.tsx +++ b/app/soapbox/features/admin/components/unapproved-account.tsx @@ -1,10 +1,10 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { approveUsers, deleteUsers } from 'soapbox/actions/admin'; +import { useAccount } from 'soapbox/api/hooks'; import { AuthorizeRejectButtons } from 'soapbox/components/authorize-reject-buttons'; import { Stack, HStack, Text } from 'soapbox/components/ui'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; interface IUnapprovedAccount { accountId: string @@ -13,9 +13,8 @@ interface IUnapprovedAccount { /** Displays an unapproved account for moderation purposes. */ const UnapprovedAccount: React.FC = ({ accountId }) => { const dispatch = useAppDispatch(); - const getAccount = useCallback(makeGetAccount(), []); - const account = useAppSelector(state => getAccount(state, accountId)); + const { account } = useAccount(accountId); const adminAccount = useAppSelector(state => state.admin.users.get(accountId)); if (!account) return null; diff --git a/app/soapbox/features/aliases/components/account.tsx b/app/soapbox/features/aliases/components/account.tsx index f0aa77e8c..741b36add 100644 --- a/app/soapbox/features/aliases/components/account.tsx +++ b/app/soapbox/features/aliases/components/account.tsx @@ -1,12 +1,12 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { addToAliases } from 'soapbox/actions/aliases'; +import { useAccount } from 'soapbox/api/hooks'; import AccountComponent from 'soapbox/components/account'; import IconButton from 'soapbox/components/icon-button'; import { HStack } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; const messages = defineMessages({ add: { id: 'aliases.account.add', defaultMessage: 'Create alias' }, @@ -22,18 +22,12 @@ const Account: React.FC = ({ accountId, aliases }) => { const dispatch = useAppDispatch(); const features = useFeatures(); - const getAccount = useCallback(makeGetAccount(), []); - const account = useAppSelector((state) => getAccount(state, accountId)); const me = useAppSelector((state) => state.me); + const { account } = useAccount(accountId); - const added = useAppSelector((state) => { - const account = getAccount(state, accountId); - const apId = account?.pleroma?.ap_id; - const name = features.accountMoving ? account?.acct : apId; - if (!name) return false; - - return aliases.includes(name); - }); + const apId = account?.pleroma?.ap_id; + const name = features.accountMoving ? account?.acct : apId; + const added = name ? aliases.includes(name) : false; const handleOnAdd = () => dispatch(addToAliases(account!)); diff --git a/app/soapbox/features/birthdays/account.tsx b/app/soapbox/features/birthdays/account.tsx index 21aec8cd2..ba37ece9e 100644 --- a/app/soapbox/features/birthdays/account.tsx +++ b/app/soapbox/features/birthdays/account.tsx @@ -1,11 +1,10 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; +import { useAccount } from 'soapbox/api/hooks'; import AccountComponent from 'soapbox/components/account'; import Icon from 'soapbox/components/icon'; import { HStack } from 'soapbox/components/ui'; -import { useAppSelector } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; const messages = defineMessages({ birthday: { id: 'account.birthday', defaultMessage: 'Born {date}' }, @@ -17,9 +16,7 @@ interface IAccount { const Account: React.FC = ({ accountId }) => { const intl = useIntl(); - const getAccount = useCallback(makeGetAccount(), []); - - const account = useAppSelector((state) => getAccount(state, accountId)); + const { account } = useAccount(accountId); if (!account) return null; diff --git a/app/soapbox/features/compose/components/autosuggest-account.tsx b/app/soapbox/features/compose/components/autosuggest-account.tsx index 345459b71..6c87c6dc7 100644 --- a/app/soapbox/features/compose/components/autosuggest-account.tsx +++ b/app/soapbox/features/compose/components/autosuggest-account.tsx @@ -1,17 +1,14 @@ -import React, { useCallback } from 'react'; +import React from 'react'; +import { useAccount } from 'soapbox/api/hooks'; import Account from 'soapbox/components/account'; -import { useAppSelector } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; interface IAutosuggestAccount { id: string } const AutosuggestAccount: React.FC = ({ id }) => { - const getAccount = useCallback(makeGetAccount(), []); - const account = useAppSelector((state) => getAccount(state, id)); - + const { account } = useAccount(id); if (!account) return null; return ; diff --git a/app/soapbox/features/directory/components/account-card.tsx b/app/soapbox/features/directory/components/account-card.tsx index 0a5707c4c..9e1ff92ae 100644 --- a/app/soapbox/features/directory/components/account-card.tsx +++ b/app/soapbox/features/directory/components/account-card.tsx @@ -3,24 +3,22 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import { getSettings } from 'soapbox/actions/settings'; +import { useAccount } from 'soapbox/api/hooks'; import Account from 'soapbox/components/account'; import Badge from 'soapbox/components/badge'; import RelativeTimestamp from 'soapbox/components/relative-timestamp'; import { Stack, Text } from 'soapbox/components/ui'; import ActionButton from 'soapbox/features/ui/components/action-button'; import { useAppSelector } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; import { shortNumberFormat } from 'soapbox/utils/numbers'; -const getAccount = makeGetAccount(); - interface IAccountCard { id: string } const AccountCard: React.FC = ({ id }) => { const me = useAppSelector((state) => state.me); - const account = useAppSelector((state) => getAccount(state, id)); + const { account } = useAccount(id); const autoPlayGif = useAppSelector((state) => getSettings(state).get('autoPlayGif')); if (!account) return null; diff --git a/app/soapbox/features/feed-suggestions/feed-suggestions.tsx b/app/soapbox/features/feed-suggestions/feed-suggestions.tsx index ce86d1432..cac53d103 100644 --- a/app/soapbox/features/feed-suggestions/feed-suggestions.tsx +++ b/app/soapbox/features/feed-suggestions/feed-suggestions.tsx @@ -2,21 +2,25 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { Link } from 'react-router-dom'; +import { useAccount } from 'soapbox/api/hooks'; import VerificationBadge from 'soapbox/components/verification-badge'; -import { useAccount, useAppSelector } from 'soapbox/hooks'; +import { useAppSelector } from 'soapbox/hooks'; import { Card, CardBody, CardTitle, HStack, Stack, Text } from '../../components/ui'; import ActionButton from '../ui/components/action-button'; -import type { Account } from 'soapbox/types/entities'; - const messages = defineMessages({ heading: { id: 'feed_suggestions.heading', defaultMessage: 'Suggested Profiles' }, viewAll: { id: 'feed_suggestions.view_all', defaultMessage: 'View all' }, }); -const SuggestionItem = ({ accountId }: { accountId: string }) => { - const account = useAccount(accountId) as Account; +interface ISuggestionItem { + accountId: string +} + +const SuggestionItem: React.FC = ({ accountId }) => { + const { account } = useAccount(accountId); + if (!account) return null; return ( diff --git a/app/soapbox/features/follow-requests/components/account-authorize.tsx b/app/soapbox/features/follow-requests/components/account-authorize.tsx index 9e1387ba2..3eead9df8 100644 --- a/app/soapbox/features/follow-requests/components/account-authorize.tsx +++ b/app/soapbox/features/follow-requests/components/account-authorize.tsx @@ -1,10 +1,10 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { authorizeFollowRequest, rejectFollowRequest } from 'soapbox/actions/accounts'; +import { useAccount } from 'soapbox/api/hooks'; import Account from 'soapbox/components/account'; import { AuthorizeRejectButtons } from 'soapbox/components/authorize-reject-buttons'; -import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; +import { useAppDispatch } from 'soapbox/hooks'; interface IAccountAuthorize { id: string @@ -12,9 +12,7 @@ interface IAccountAuthorize { const AccountAuthorize: React.FC = ({ id }) => { const dispatch = useAppDispatch(); - - const getAccount = useCallback(makeGetAccount(), []); - const account = useAppSelector((state) => getAccount(state, id)); + const { account } = useAccount(id); const onAuthorize = () => dispatch(authorizeFollowRequest(id)); const onReject = () => dispatch(rejectFollowRequest(id)); diff --git a/app/soapbox/features/group/group-blocked-members.tsx b/app/soapbox/features/group/group-blocked-members.tsx index f45e8259f..d6f994168 100644 --- a/app/soapbox/features/group/group-blocked-members.tsx +++ b/app/soapbox/features/group/group-blocked-members.tsx @@ -1,13 +1,12 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { fetchGroupBlocks, groupUnblock } from 'soapbox/actions/groups'; -import { useGroup } from 'soapbox/api/hooks'; +import { useAccount, useGroup } from 'soapbox/api/hooks'; import Account from 'soapbox/components/account'; import ScrollableList from 'soapbox/components/scrollable-list'; import { Button, Column, HStack, Spinner } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; import toast from 'soapbox/toast'; import ColumnForbidden from '../ui/components/column-forbidden'; @@ -28,10 +27,7 @@ interface IBlockedMember { const BlockedMember: React.FC = ({ accountId, groupId }) => { const intl = useIntl(); const dispatch = useAppDispatch(); - - const getAccount = useCallback(makeGetAccount(), []); - - const account = useAppSelector((state) => getAccount(state, accountId)); + const { account } = useAccount(accountId); if (!account) return null; diff --git a/app/soapbox/features/reply-mentions/account.tsx b/app/soapbox/features/reply-mentions/account.tsx index be6b61166..108d71b54 100644 --- a/app/soapbox/features/reply-mentions/account.tsx +++ b/app/soapbox/features/reply-mentions/account.tsx @@ -1,13 +1,13 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { fetchAccount } from 'soapbox/actions/accounts'; import { addToMentions, removeFromMentions } from 'soapbox/actions/compose'; +import { useAccount } from 'soapbox/api/hooks'; import AccountComponent from 'soapbox/components/account'; import IconButton from 'soapbox/components/icon-button'; import { HStack } from 'soapbox/components/ui'; -import { useAppDispatch, useAppSelector, useCompose } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; +import { useAppDispatch, useCompose } from 'soapbox/hooks'; const messages = defineMessages({ remove: { id: 'reply_mentions.account.remove', defaultMessage: 'Remove from mentions' }, @@ -23,11 +23,9 @@ interface IAccount { const Account: React.FC = ({ composeId, accountId, author }) => { const intl = useIntl(); const dispatch = useAppDispatch(); - const getAccount = useCallback(makeGetAccount(), []); const compose = useCompose(composeId); - - const account = useAppSelector((state) => getAccount(state, accountId)); + const { account } = useAccount(accountId); const added = !!account && compose.to?.includes(account.acct); const onRemove = () => dispatch(removeFromMentions(composeId, accountId)); diff --git a/app/soapbox/features/scheduled-statuses/builder.tsx b/app/soapbox/features/scheduled-statuses/builder.tsx index dbd40f101..95ab0d878 100644 --- a/app/soapbox/features/scheduled-statuses/builder.tsx +++ b/app/soapbox/features/scheduled-statuses/builder.tsx @@ -1,18 +1,15 @@ import { Map as ImmutableMap } from 'immutable'; +import { Entities } from 'soapbox/entity-store/entities'; import { normalizeStatus } from 'soapbox/normalizers/status'; import { calculateStatus } from 'soapbox/reducers/statuses'; -import { makeGetAccount } from 'soapbox/selectors'; import type { ScheduledStatus } from 'soapbox/reducers/scheduled-statuses'; import type { RootState } from 'soapbox/store'; export const buildStatus = (state: RootState, scheduledStatus: ScheduledStatus) => { - const getAccount = makeGetAccount(); - const me = state.me as string; - - const account = getAccount(state, me); + const account = state.entities[Entities.ACCOUNTS]?.store[me]; const status = ImmutableMap({ account, 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 258653295..f7f1606aa 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 @@ -9,13 +9,13 @@ import { setBadges as saveBadges, } from 'soapbox/actions/admin'; import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation'; +import { useAccount } from 'soapbox/api/hooks'; import Account from 'soapbox/components/account'; import List, { ListItem } from 'soapbox/components/list'; import MissingIndicator from 'soapbox/components/missing-indicator'; import OutlineBox from 'soapbox/components/outline-box'; import { Button, Text, HStack, Modal, Stack, Toggle } from 'soapbox/components/ui'; -import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; +import { useAppDispatch, useFeatures, useOwnAccount } from 'soapbox/hooks'; import toast from 'soapbox/toast'; import { isLocal } from 'soapbox/utils/accounts'; import { getBadges } from 'soapbox/utils/badges'; @@ -23,8 +23,6 @@ import { getBadges } from 'soapbox/utils/badges'; import BadgeInput from './badge-input'; import StaffRolePicker from './staff-role-picker'; -const getAccount = makeGetAccount(); - const messages = defineMessages({ userVerified: { id: 'admin.users.user_verified_message', defaultMessage: '@{acct} was verified' }, userUnverified: { id: 'admin.users.user_unverified_message', defaultMessage: '@{acct} was unverified' }, @@ -49,7 +47,7 @@ const AccountModerationModal: React.FC = ({ onClose, ac const ownAccount = useOwnAccount(); const features = useFeatures(); - const account = useAppSelector(state => getAccount(state, accountId)); + const { account } = useAccount(accountId); const accountBadges = account ? getBadges(account) : []; const [badges, setBadges] = useState(accountBadges); @@ -138,7 +136,7 @@ const AccountModerationModal: React.FC = ({ onClose, ac {features.suggestionsV2 && ( }> diff --git a/app/soapbox/features/ui/components/modals/account-moderation-modal/staff-role-picker.tsx b/app/soapbox/features/ui/components/modals/account-moderation-modal/staff-role-picker.tsx index 3d8ea9993..2aa972fe6 100644 --- a/app/soapbox/features/ui/components/modals/account-moderation-modal/staff-role-picker.tsx +++ b/app/soapbox/features/ui/components/modals/account-moderation-modal/staff-role-picker.tsx @@ -6,13 +6,13 @@ import { SelectDropdown } from 'soapbox/features/forms'; import { useAppDispatch } from 'soapbox/hooks'; import toast from 'soapbox/toast'; -import type { Account as AccountEntity } from 'soapbox/types/entities'; +import type { Account as AccountEntity } from 'soapbox/schemas'; /** Staff role. */ type AccountRole = 'user' | 'moderator' | 'admin'; /** Get the highest staff role associated with the account. */ -const getRole = (account: AccountEntity): AccountRole => { +const getRole = (account: Pick): AccountRole => { if (account.admin) { return 'admin'; } else if (account.moderator) { @@ -34,7 +34,7 @@ const messages = defineMessages({ interface IStaffRolePicker { /** Account whose role to change. */ - account: AccountEntity + account: Pick } /** Picker for setting the staff role of an account. */ diff --git a/app/soapbox/features/ui/components/modals/account-note-modal.tsx b/app/soapbox/features/ui/components/modals/account-note-modal.tsx index b34e5e132..95afc614e 100644 --- a/app/soapbox/features/ui/components/modals/account-note-modal.tsx +++ b/app/soapbox/features/ui/components/modals/account-note-modal.tsx @@ -3,23 +3,22 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { changeAccountNoteComment, submitAccountNote } from 'soapbox/actions/account-notes'; import { closeModal } from 'soapbox/actions/modals'; +import { useAccount } from 'soapbox/api/hooks'; import { Modal, Text } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; const messages = defineMessages({ placeholder: { id: 'account_note.placeholder', defaultMessage: 'No comment provided' }, save: { id: 'account_note.save', defaultMessage: 'Save' }, }); -const getAccount = makeGetAccount(); - const AccountNoteModal = () => { const intl = useIntl(); const dispatch = useAppDispatch(); const isSubmitting = useAppSelector((state) => state.account_notes.edit.isSubmitting); - const account = useAppSelector((state) => getAccount(state, state.account_notes.edit.account!)); + const accountId = useAppSelector((state) => state.account_notes.edit.account); + const { account } = useAccount(accountId || undefined); const comment = useAppSelector((state) => state.account_notes.edit.comment); const onClose = () => { diff --git a/app/soapbox/features/ui/components/modals/mute-modal.tsx b/app/soapbox/features/ui/components/modals/mute-modal.tsx index 90f32ec35..aa2209fd0 100644 --- a/app/soapbox/features/ui/components/modals/mute-modal.tsx +++ b/app/soapbox/features/ui/components/modals/mute-modal.tsx @@ -4,17 +4,16 @@ import { FormattedMessage } from 'react-intl'; import { muteAccount } from 'soapbox/actions/accounts'; import { closeModal } from 'soapbox/actions/modals'; import { toggleHideNotifications, changeMuteDuration } from 'soapbox/actions/mutes'; +import { useAccount } from 'soapbox/api/hooks'; import { Modal, HStack, Stack, Text, Toggle } from 'soapbox/components/ui'; import DurationSelector from 'soapbox/features/compose/components/polls/duration-selector'; import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; - -const getAccount = makeGetAccount(); const MuteModal = () => { const dispatch = useAppDispatch(); - const account = useAppSelector((state) => getAccount(state, state.mutes.new.accountId!)); + const accountId = useAppSelector((state) => state.mutes.new.accountId); + const { account } = useAccount(accountId || undefined); const notifications = useAppSelector((state) => state.mutes.new.notifications); const duration = useAppSelector((state) => state.mutes.new.duration); const mutesDuration = useFeatures().mutesDuration; diff --git a/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx b/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx index 566e75981..4b599c445 100644 --- a/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx +++ b/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx @@ -4,13 +4,14 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { blockAccount } from 'soapbox/actions/accounts'; import { submitReport, submitReportSuccess, submitReportFail, ReportableEntities } from 'soapbox/actions/reports'; import { expandAccountTimeline } from 'soapbox/actions/timelines'; +import { useAccount } from 'soapbox/api/hooks'; import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; import GroupCard from 'soapbox/components/group-card'; import List, { ListItem } from 'soapbox/components/list'; import StatusContent from 'soapbox/components/status-content'; import { Avatar, HStack, Icon, Modal, ProgressBar, Stack, Text } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account-container'; -import { useAccount, useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import ConfirmationStep from './steps/confirmation-step'; import OtherActionsStep from './steps/other-actions-step'; @@ -100,7 +101,7 @@ const ReportModal = ({ onClose }: IReportModal) => { const intl = useIntl(); const accountId = useAppSelector((state) => state.reports.new.account_id); - const account = useAccount(accountId as string); + const { account } = useAccount(accountId || undefined); const entityType = useAppSelector((state) => state.reports.new.entityType); const isBlocked = useAppSelector((state) => state.reports.new.block); diff --git a/app/soapbox/features/ui/components/profile-info-panel.tsx b/app/soapbox/features/ui/components/profile-info-panel.tsx index 9cb4e7135..cdc4d66b3 100644 --- a/app/soapbox/features/ui/components/profile-info-panel.tsx +++ b/app/soapbox/features/ui/components/profile-info-panel.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; +import { usePatronUser } from 'soapbox/api/hooks'; import Badge from 'soapbox/components/badge'; import Markup from 'soapbox/components/markup'; import { Icon, HStack, Stack, Text } from 'soapbox/components/ui'; @@ -35,7 +36,7 @@ const messages = defineMessages({ }); interface IProfileInfoPanel { - account: Account + account?: Account /** Username from URL params, in case the account isn't found. */ username: string } @@ -44,6 +45,7 @@ interface IProfileInfoPanel { const ProfileInfoPanel: React.FC = ({ account, username }) => { const intl = useIntl(); const { displayFqn } = useSoapboxConfig(); + const { patronUser } = usePatronUser(account?.url); const getStaffBadge = (): React.ReactNode => { if (account?.admin) { @@ -56,7 +58,7 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => }; const getCustomBadges = (): React.ReactNode[] => { - const badges = getAccountBadges(account); + const badges = account ? getAccountBadges(account) : []; return badges.map(badge => ( = ({ account, username }) => const getBadges = (): React.ReactNode[] => { const custom = getCustomBadges(); const staffBadge = getStaffBadge(); - const isPatron = account.getIn(['patron', 'is_patron']) === true; + const isPatron = patronUser?.is_patron === true; const badges = []; @@ -86,7 +88,7 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => }; const renderBirthday = (): React.ReactNode => { - const birthday = account.pleroma?.birthday; + const birthday = account?.pleroma?.birthday; if (!birthday) return null; const formattedBirthday = intl.formatDate(birthday, { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' }); diff --git a/app/soapbox/features/ui/components/profile-stats.tsx b/app/soapbox/features/ui/components/profile-stats.tsx index d02542b2f..6cb02a173 100644 --- a/app/soapbox/features/ui/components/profile-stats.tsx +++ b/app/soapbox/features/ui/components/profile-stats.tsx @@ -5,7 +5,7 @@ import { NavLink } from 'react-router-dom'; import { HStack, Text } from 'soapbox/components/ui'; import { shortNumberFormat } from 'soapbox/utils/numbers'; -import type { Account } from 'soapbox/types/entities'; +import type { Account } from 'soapbox/schemas'; const messages = defineMessages({ followers: { id: 'account.followers', defaultMessage: 'Followers' }, @@ -13,7 +13,7 @@ const messages = defineMessages({ }); interface IProfileStats { - account: Account | undefined + account: Pick | undefined onClickHandler?: React.MouseEventHandler } diff --git a/app/soapbox/features/ui/components/user-panel.tsx b/app/soapbox/features/ui/components/user-panel.tsx index 08642d801..bfbba1b3f 100644 --- a/app/soapbox/features/ui/components/user-panel.tsx +++ b/app/soapbox/features/ui/components/user-panel.tsx @@ -2,17 +2,15 @@ import React from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { Link } from 'react-router-dom'; +import { useAccount } from 'soapbox/api/hooks'; import StillImage from 'soapbox/components/still-image'; import { Avatar, HStack, Stack, Text } from 'soapbox/components/ui'; import VerificationBadge from 'soapbox/components/verification-badge'; import { useAppSelector } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; import { getAcct } from 'soapbox/utils/accounts'; import { shortNumberFormat } from 'soapbox/utils/numbers'; import { displayFqn } from 'soapbox/utils/state'; -const getAccount = makeGetAccount(); - interface IUserPanel { accountId: string action?: JSX.Element @@ -22,7 +20,7 @@ interface IUserPanel { const UserPanel: React.FC = ({ accountId, action, badges, domain }) => { const intl = useIntl(); - const account = useAppSelector((state) => getAccount(state, accountId)); + const { account } = useAccount(accountId); const fqn = useAppSelector((state) => displayFqn(state)); if (!account) return null; diff --git a/app/soapbox/hooks/index.ts b/app/soapbox/hooks/index.ts index 0bd63eb21..4bab834e3 100644 --- a/app/soapbox/hooks/index.ts +++ b/app/soapbox/hooks/index.ts @@ -1,4 +1,3 @@ -export { useAccount } from './useAccount'; export { useApi } from './useApi'; export { useAppDispatch } from './useAppDispatch'; export { useAppSelector } from './useAppSelector'; diff --git a/app/soapbox/hooks/useAccount.ts b/app/soapbox/hooks/useAccount.ts deleted file mode 100644 index e8dfff152..000000000 --- a/app/soapbox/hooks/useAccount.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { useAppSelector } from 'soapbox/hooks'; -import { makeGetAccount } from 'soapbox/selectors'; - -export const useAccount = (id: string) => { - const getAccount = makeGetAccount(); - - return useAppSelector((state) => getAccount(state, id)); -}; diff --git a/app/soapbox/reducers/mutes.ts b/app/soapbox/reducers/mutes.ts index 4c0b08c39..0034a6c56 100644 --- a/app/soapbox/reducers/mutes.ts +++ b/app/soapbox/reducers/mutes.ts @@ -10,7 +10,7 @@ import type { AnyAction } from 'redux'; const NewMuteRecord = ImmutableRecord({ isSubmitting: false, - accountId: null, + accountId: null as string | null, notifications: true, duration: 0, }); diff --git a/app/soapbox/schemas/index.ts b/app/soapbox/schemas/index.ts index 1fa8ac5b4..2c99ef8b8 100644 --- a/app/soapbox/schemas/index.ts +++ b/app/soapbox/schemas/index.ts @@ -10,6 +10,7 @@ export { groupRelationshipSchema, type GroupRelationship } from './group-relatio export { groupTagSchema, type GroupTag } from './group-tag'; export { mentionSchema, type Mention } from './mention'; export { notificationSchema, type Notification } from './notification'; +export { patronUserSchema, type PatronUser } from './patron'; export { pollSchema, type Poll, type PollOption } from './poll'; export { relationshipSchema, type Relationship } from './relationship'; export { statusSchema, type Status } from './status'; diff --git a/app/soapbox/schemas/patron.ts b/app/soapbox/schemas/patron.ts new file mode 100644 index 000000000..c7aa5a569 --- /dev/null +++ b/app/soapbox/schemas/patron.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +const patronUserSchema = z.object({ + is_patron: z.boolean().catch(false), + url: z.string().url(), +}).transform((patron) => { + return { + id: patron.url, + ...patron, + }; +}); + +type PatronUser = z.infer; + +export { patronUserSchema, type PatronUser }; \ No newline at end of file