diff --git a/src/api/hooks/accounts/useAccount.ts b/src/api/hooks/accounts/useAccount.ts index 907337b8b..9db7ec217 100644 --- a/src/api/hooks/accounts/useAccount.ts +++ b/src/api/hooks/accounts/useAccount.ts @@ -30,7 +30,7 @@ function useAccount(accountId?: string, opts: UseAccountOpts = {}) { isLoading: isRelationshipLoading, } = useRelationship(accountId, { enabled: withRelationship }); - const isBlocked = entity?.relationship?.blocked_by === true; + const isBlocked = relationship?.blocked_by === true; const isUnavailable = (me === entity?.id) ? false : (isBlocked && !features.blockersVisible); const account = useMemo( diff --git a/src/api/hooks/accounts/useAccountLookup.ts b/src/api/hooks/accounts/useAccountLookup.ts index 321c8dcc1..ef9306a7d 100644 --- a/src/api/hooks/accounts/useAccountLookup.ts +++ b/src/api/hooks/accounts/useAccountLookup.ts @@ -31,7 +31,7 @@ function useAccountLookup(acct: string | undefined, opts: UseAccountLookupOpts = isLoading: isRelationshipLoading, } = useRelationship(account?.id, { enabled: withRelationship }); - const isBlocked = account?.relationship?.blocked_by === true; + const isBlocked = relationship?.blocked_by === true; const isUnavailable = (me === account?.id) ? false : (isBlocked && !features.blockersVisible); useEffect(() => { diff --git a/src/normalizers/admin-account.ts b/src/normalizers/admin-account.ts index 4e040dfb1..5f3b7d8cb 100644 --- a/src/normalizers/admin-account.ts +++ b/src/normalizers/admin-account.ts @@ -9,11 +9,10 @@ import { fromJS, } from 'immutable'; -import type { ReducerAccount } from 'soapbox/reducers/accounts'; import type { Account, EmbeddedEntity } from 'soapbox/types/entities'; export const AdminAccountRecord = ImmutableRecord({ - account: null as EmbeddedEntity, + account: null as EmbeddedEntity, approved: false, confirmed: false, created_at: new Date(), diff --git a/src/normalizers/admin-report.ts b/src/normalizers/admin-report.ts index acf6786b1..71e616e8d 100644 --- a/src/normalizers/admin-report.ts +++ b/src/normalizers/admin-report.ts @@ -9,21 +9,20 @@ import { fromJS, } from 'immutable'; -import type { ReducerAccount } from 'soapbox/reducers/accounts'; import type { Account, EmbeddedEntity, Status } from 'soapbox/types/entities'; export const AdminReportRecord = ImmutableRecord({ - account: null as EmbeddedEntity, + account: null as EmbeddedEntity, action_taken: false, - action_taken_by_account: null as EmbeddedEntity | null, - assigned_account: null as EmbeddedEntity | null, + action_taken_by_account: null as EmbeddedEntity | null, + assigned_account: null as EmbeddedEntity | null, category: '', comment: '', created_at: new Date(), id: '', rules: ImmutableList(), statuses: ImmutableList>(), - target_account: null as EmbeddedEntity, + target_account: null as EmbeddedEntity, updated_at: new Date(), }); diff --git a/src/normalizers/status-edit.ts b/src/normalizers/status-edit.ts index 26619c174..859eb3a95 100644 --- a/src/normalizers/status-edit.ts +++ b/src/normalizers/status-edit.ts @@ -17,11 +17,10 @@ import { pollSchema } from 'soapbox/schemas'; import { stripCompatibilityFeatures } from 'soapbox/utils/html'; import { makeEmojiMap } from 'soapbox/utils/normalizers'; -import type { ReducerAccount } from 'soapbox/reducers/accounts'; import type { Account, Attachment, Emoji, EmbeddedEntity, Poll } from 'soapbox/types/entities'; export const StatusEditRecord = ImmutableRecord({ - account: null as EmbeddedEntity, + account: null as EmbeddedEntity, content: '', created_at: new Date(), emojis: ImmutableList(), diff --git a/src/reducers/accounts.ts b/src/reducers/accounts.ts deleted file mode 100644 index 8dcc264a6..000000000 --- a/src/reducers/accounts.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { - Map as ImmutableMap, - List as ImmutableList, - OrderedSet as ImmutableOrderedSet, - fromJS, -} from 'immutable'; - -import { - ADMIN_USERS_FETCH_SUCCESS, - ADMIN_USERS_TAG_REQUEST, - ADMIN_USERS_TAG_SUCCESS, - ADMIN_USERS_TAG_FAIL, - ADMIN_USERS_UNTAG_REQUEST, - ADMIN_USERS_UNTAG_SUCCESS, - ADMIN_USERS_UNTAG_FAIL, - ADMIN_ADD_PERMISSION_GROUP_REQUEST, - ADMIN_ADD_PERMISSION_GROUP_SUCCESS, - ADMIN_ADD_PERMISSION_GROUP_FAIL, - ADMIN_REMOVE_PERMISSION_GROUP_REQUEST, - ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS, - ADMIN_REMOVE_PERMISSION_GROUP_FAIL, - ADMIN_USERS_DELETE_REQUEST, - ADMIN_USERS_DELETE_FAIL, - ADMIN_USERS_DEACTIVATE_REQUEST, - ADMIN_USERS_DEACTIVATE_FAIL, -} from 'soapbox/actions/admin'; -import { CHATS_FETCH_SUCCESS, CHATS_EXPAND_SUCCESS, CHAT_FETCH_SUCCESS } from 'soapbox/actions/chats'; -import { - ACCOUNT_IMPORT, - ACCOUNTS_IMPORT, - ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, -} from 'soapbox/actions/importer'; -import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming'; -import { normalizeAccount } from 'soapbox/normalizers/account'; -import { normalizeId } from 'soapbox/utils/normalizers'; - -import type { AnyAction } from 'redux'; -import type { APIEntity } from 'soapbox/types/entities'; - -type AccountRecord = ReturnType; -type AccountMap = ImmutableMap; -type APIEntities = Array; - -export interface ReducerAccount extends AccountRecord { - moved: string | null; -} - -type State = ImmutableMap; - -const initialState: State = ImmutableMap(); - -const minifyAccount = (account: AccountRecord): ReducerAccount => { - return account.mergeWith((o, n) => n || o, { - moved: normalizeId(account.getIn(['moved', 'id'])), - }) as ReducerAccount; -}; - -const fixAccount = (state: State, account: APIEntity) => { - const normalized = minifyAccount(normalizeAccount(account)); - return state.set(account.id, normalized); -}; - -const normalizeAccounts = (state: State, accounts: ImmutableList) => { - accounts.forEach(account => { - state = fixAccount(state, account); - }); - - return state; -}; - -const importAccountFromChat = ( - state: State, - chat: APIEntity, -): State => fixAccount(state, chat.account); - -const importAccountsFromChats = (state: State, chats: APIEntities): State => - state.withMutations(mutable => - chats.forEach(chat => importAccountFromChat(mutable, chat))); - -const addTags = ( - state: State, - accountIds: Array, - tags: Array, -): State => { - return state.withMutations(state => { - accountIds.forEach(id => { - state.updateIn([id, 'pleroma', 'tags'], ImmutableList(), v => - ImmutableOrderedSet(fromJS(v)).union(tags).toList(), - ); - - tags.forEach(tag => { - switch (tag) { - case 'verified': - state.setIn([id, 'verified'], true); - break; - case 'donor': - state.setIn([id, 'donor'], true); - break; - } - }); - }); - }); -}; - -const removeTags = ( - state: State, - accountIds: Array, - tags: Array, -): State => { - return state.withMutations(state => { - accountIds.forEach(id => { - state.updateIn([id, 'pleroma', 'tags'], ImmutableList(), v => - ImmutableOrderedSet(fromJS(v)).subtract(tags).toList(), - ); - - tags.forEach(tag => { - switch (tag) { - case 'verified': - state.setIn([id, 'verified'], false); - break; - case 'donor': - state.setIn([id, 'donor'], false); - break; - } - }); - }); - }); -}; - -const setActive = (state: State, accountIds: Array, active: boolean): State => { - return state.withMutations(state => { - accountIds.forEach(id => { - state.setIn([id, 'pleroma', 'is_active'], active); - }); - }); -}; - -const permissionGroupFields: Record = { - admin: 'is_admin', - moderator: 'is_moderator', -}; - -const addPermission = ( - state: State, - accountIds: Array, - permissionGroup: string, -): State => { - const field = permissionGroupFields[permissionGroup]; - if (!field) return state; - - return state.withMutations(state => { - accountIds.forEach(id => { - state.setIn([id, 'pleroma', field], true); - }); - }); -}; - -const removePermission = ( - state: State, - accountIds: Array, - permissionGroup: string, -): State => { - const field = permissionGroupFields[permissionGroup]; - if (!field) return state; - - return state.withMutations(state => { - accountIds.forEach(id => { - state.setIn([id, 'pleroma', field], false); - }); - }); -}; - -const buildAccount = (adminUser: ImmutableMap): AccountRecord => normalizeAccount({ - id: adminUser.get('id'), - username: adminUser.get('nickname').split('@')[0], - acct: adminUser.get('nickname'), - display_name: adminUser.get('display_name'), - display_name_html: adminUser.get('display_name'), - url: adminUser.get('url'), - avatar: adminUser.get('avatar'), - avatar_static: adminUser.get('avatar'), - created_at: adminUser.get('created_at'), - pleroma: { - is_active: adminUser.get('is_active'), - is_confirmed: adminUser.get('is_confirmed'), - is_admin: adminUser.getIn(['roles', 'admin']), - is_moderator: adminUser.getIn(['roles', 'moderator']), - tags: adminUser.get('tags'), - }, - source: { - pleroma: { - actor_type: adminUser.get('actor_type'), - }, - }, - should_refetch: true, -}); - -const mergeAdminUser = ( - account: AccountRecord, - adminUser: ImmutableMap, -) => { - return account.withMutations(account => { - account.set('display_name', adminUser.get('display_name')); - account.set('avatar', adminUser.get('avatar')); - account.set('avatar_static', adminUser.get('avatar')); - account.setIn(['pleroma', 'is_active'], adminUser.get('is_active')); - account.setIn(['pleroma', 'is_admin'], adminUser.getIn(['roles', 'admin'])); - account.setIn(['pleroma', 'is_moderator'], adminUser.getIn(['roles', 'moderator'])); - account.setIn(['pleroma', 'is_confirmed'], adminUser.get('is_confirmed')); - account.setIn(['pleroma', 'tags'], adminUser.get('tags')); - }); -}; - -const importAdminUser = (state: State, adminUser: ImmutableMap): State => { - const id = adminUser.get('id'); - const account = state.get(id); - - if (!account) { - return state.set(id, minifyAccount(buildAccount(adminUser))); - } else { - return state.set(id, minifyAccount(mergeAdminUser(account, adminUser))); - } -}; - -const importAdminUsers = (state: State, adminUsers: Array>): State => { - return state.withMutations((state: State) => { - adminUsers.filter(adminUser => !adminUser.account).forEach(adminUser => { - importAdminUser(state, ImmutableMap(fromJS(adminUser))); - }); - }); -}; - -export default function accounts(state: State = initialState, action: AnyAction): State { - switch (action.type) { - case ACCOUNT_IMPORT: - return fixAccount(state, action.account); - case ACCOUNTS_IMPORT: - return normalizeAccounts(state, action.accounts); - case ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP: - return fixAccount(state, { id: -1, username: action.username }); - case CHATS_FETCH_SUCCESS: - case CHATS_EXPAND_SUCCESS: - return importAccountsFromChats(state, action.chats); - case CHAT_FETCH_SUCCESS: - case STREAMING_CHAT_UPDATE: - return importAccountsFromChats(state, [action.chat]); - case ADMIN_USERS_TAG_REQUEST: - case ADMIN_USERS_TAG_SUCCESS: - case ADMIN_USERS_UNTAG_FAIL: - return addTags(state, action.accountIds, action.tags); - case ADMIN_USERS_UNTAG_REQUEST: - case ADMIN_USERS_UNTAG_SUCCESS: - case ADMIN_USERS_TAG_FAIL: - return removeTags(state, action.accountIds, action.tags); - case ADMIN_ADD_PERMISSION_GROUP_REQUEST: - case ADMIN_ADD_PERMISSION_GROUP_SUCCESS: - case ADMIN_REMOVE_PERMISSION_GROUP_FAIL: - return addPermission(state, action.accountIds, action.permissionGroup); - case ADMIN_REMOVE_PERMISSION_GROUP_REQUEST: - case ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS: - case ADMIN_ADD_PERMISSION_GROUP_FAIL: - return removePermission(state, action.accountIds, action.permissionGroup); - case ADMIN_USERS_DELETE_REQUEST: - case ADMIN_USERS_DEACTIVATE_REQUEST: - return setActive(state, action.accountIds, false); - case ADMIN_USERS_DELETE_FAIL: - case ADMIN_USERS_DEACTIVATE_FAIL: - return setActive(state, action.accountIds, true); - case ADMIN_USERS_FETCH_SUCCESS: - return importAdminUsers(state, action.users); - default: - return state; - } -} diff --git a/src/schemas/account.ts b/src/schemas/account.ts index 702b631b0..dc3605b17 100644 --- a/src/schemas/account.ts +++ b/src/schemas/account.ts @@ -7,7 +7,7 @@ import emojify from 'soapbox/features/emoji'; import { unescapeHTML } from 'soapbox/utils/html'; import { customEmojiSchema } from './custom-emoji'; -import { relationshipSchema } from './relationship'; +import { Relationship } from './relationship'; import { coerceObject, contentSchema, filteredArray, makeCustomEmojiMap } from './utils'; import type { Resolve } from 'soapbox/utils/types'; @@ -73,7 +73,7 @@ const baseAccountSchema = z.object({ birthday: birthdaySchema.nullish().catch(undefined), location: z.string().optional().catch(undefined), }).optional().catch(undefined), - pleroma: z.object({ + pleroma: coerceObject({ accepts_chat_messages: z.boolean().catch(false), accepts_email_list: z.boolean().catch(false), also_known_as: z.array(z.string().url()).catch([]), @@ -91,12 +91,11 @@ const baseAccountSchema = z.object({ is_moderator: z.boolean().catch(false), is_suggested: z.boolean().catch(false), location: z.string().optional().catch(undefined), - notification_settings: z.object({ + notification_settings: coerceObject({ block_from_strangers: z.boolean().catch(false), - }).optional().catch(undefined), - relationship: relationshipSchema.optional().catch(undefined), + }), tags: z.array(z.string()).catch([]), - }).optional().catch(undefined), + }), roles: filteredArray(roleSchema), source: z.object({ approved: z.boolean().catch(true), @@ -170,13 +169,8 @@ const transformAccount = ({ pleroma, other_setti local: pleroma?.is_local !== undefined ? pleroma.is_local : account.acct.split('@')[1] === undefined, location: account.location || pleroma?.location || other_settings?.location || '', note_emojified: DOMPurify.sanitize(emojify(account.note, customEmojiMap), { USE_PROFILES: { html: true } }), - pleroma: (() => { - if (!pleroma) return undefined; - const { relationship, ...rest } = pleroma; - return rest; - })(), + pleroma, roles: account.roles.length ? account.roles : filterBadges(pleroma?.tags), - relationship: pleroma?.relationship, staff: pleroma?.is_admin || pleroma?.is_moderator || false, suspended: account.suspended || pleroma?.deactivated || false, verified: account.verified || pleroma?.tags.includes('verified') || false, @@ -187,6 +181,9 @@ const accountSchema = baseAccountSchema.extend({ moved: baseAccountSchema.transform(transformAccount).nullable().catch(null), }).transform(transformAccount); -type Account = Resolve>; +type Account = Resolve> & { + // FIXME: decouple these in components. + relationship?: Relationship; +} export { accountSchema, type Account }; diff --git a/src/utils/accounts.test.ts b/src/utils/accounts.test.ts deleted file mode 100644 index f884729f3..000000000 --- a/src/utils/accounts.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { AccountRecord } from 'soapbox/normalizers'; - -import { - getDomain, -} from './accounts'; - -import type { ReducerAccount } from 'soapbox/reducers/accounts'; - -describe('getDomain', () => { - const account = AccountRecord({ - acct: 'alice', - url: 'https://party.com/users/alice', - }) as ReducerAccount; - it('returns the domain', () => { - expect(getDomain(account)).toEqual('party.com'); - }); -}); diff --git a/src/utils/normalizers.ts b/src/utils/normalizers.ts index b450a15d8..dbdf36379 100644 --- a/src/utils/normalizers.ts +++ b/src/utils/normalizers.ts @@ -9,7 +9,7 @@ export const makeEmojiMap = (emojis: any) => emojis.reduce((obj: any, emoji: any }, {}); /** Normalize entity ID */ -export const normalizeId = (id: any): string | null => { +export const normalizeId = (id: unknown): string | null => { return z.string().nullable().catch(null).parse(id); };