FIX THE TYPE ERRORS

This commit is contained in:
Alex Gleason 2023-06-20 14:24:39 -05:00
parent 011f2eb298
commit 412fe84d13
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
67 changed files with 261 additions and 236 deletions

View File

@ -1,15 +1,12 @@
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { __stub } from 'soapbox/api'; import { __stub } from 'soapbox/api';
import { buildRelationship } from 'soapbox/jest/factory'; import { buildAccount, buildRelationship } from 'soapbox/jest/factory';
import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { ReducerRecord, EditRecord } from 'soapbox/reducers/account-notes'; import { ReducerRecord, EditRecord } from 'soapbox/reducers/account-notes';
import { normalizeAccount } from '../../normalizers';
import { changeAccountNoteComment, initAccountNoteModal, submitAccountNote } from '../account-notes'; import { changeAccountNoteComment, initAccountNoteModal, submitAccountNote } from '../account-notes';
import type { Account } from 'soapbox/types/entities';
describe('submitAccountNote()', () => { describe('submitAccountNote()', () => {
let store: ReturnType<typeof mockStore>; let store: ReturnType<typeof mockStore>;
@ -72,13 +69,13 @@ describe('initAccountNoteModal()', () => {
}); });
it('dispatches the proper actions', async() => { it('dispatches the proper actions', async() => {
const account = normalizeAccount({ const account = buildAccount({
id: '1', id: '1',
acct: 'justin-username', acct: 'justin-username',
display_name: 'Justin L', display_name: 'Justin L',
avatar: 'test.jpg', avatar: 'test.jpg',
verified: true, verified: true,
}) as Account; });
const expectedActions = [ const expectedActions = [
{ type: 'ACCOUNT_NOTE_INIT_MODAL', account, comment: 'hello' }, { type: 'ACCOUNT_NOTE_INIT_MODAL', account, comment: 'hello' },
{ type: 'MODAL_CLOSE', modalType: 'ACCOUNT_NOTE' }, { type: 'MODAL_CLOSE', modalType: 'ACCOUNT_NOTE' },

View File

@ -76,9 +76,14 @@ describe('fetchAccount()', () => {
}); });
const state = rootState const state = rootState
.set('accounts', ImmutableMap({ .set('entities', {
'ACCOUNTS': {
store: {
[id]: account, [id]: account,
}) as any); },
lists: {},
},
});
store = mockStore(state); store = mockStore(state);
@ -168,9 +173,14 @@ describe('fetchAccountByUsername()', () => {
}); });
state = rootState state = rootState
.set('accounts', ImmutableMap({ .set('entities', {
'ACCOUNTS': {
store: {
[id]: account, [id]: account,
})); },
lists: {},
},
});
store = mockStore(state); store = mockStore(state);

View File

@ -1,13 +1,11 @@
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { __stub } from 'soapbox/api'; import { __stub } from 'soapbox/api';
import { buildAccount } from 'soapbox/jest/factory';
import { mockStore, rootState } from 'soapbox/jest/test-helpers'; import { mockStore, rootState } from 'soapbox/jest/test-helpers';
import { AccountRecord } from 'soapbox/normalizers'; import { AuthUserRecord, ReducerRecord } from 'soapbox/reducers/auth';
import { AuthUserRecord, ReducerRecord } from '../../reducers/auth'; import { fetchMe, patchMe } from '../me';
import {
fetchMe, patchMe,
} from '../me';
jest.mock('../../storage/kv-store', () => ({ jest.mock('../../storage/kv-store', () => ({
__esModule: true, __esModule: true,
@ -48,11 +46,15 @@ describe('fetchMe()', () => {
}), }),
}), }),
})) }))
.set('accounts', ImmutableMap({ .set('entities', {
[accountUrl]: AccountRecord({ 'ACCOUNTS': {
url: accountUrl, store: {
}), [accountUrl]: buildAccount({ url: accountUrl }),
}) as any); },
lists: {},
},
});
store = mockStore(state); store = mockStore(state);
}); });

View File

@ -1,4 +1,6 @@
import { rootState } from '../../jest/test-helpers'; import { rootState } from 'soapbox/jest/test-helpers';
import { RootState } from 'soapbox/store';
import { getSoapboxConfig } from '../soapbox'; import { getSoapboxConfig } from '../soapbox';
const ASCII_HEART = '❤'; // '\u2764\uFE0F' const ASCII_HEART = '❤'; // '\u2764\uFE0F'
@ -6,13 +8,13 @@ const RED_HEART_RGI = '❤️'; // '\u2764'
describe('getSoapboxConfig()', () => { describe('getSoapboxConfig()', () => {
it('returns RGI heart on Pleroma > 2.3', () => { it('returns RGI heart on Pleroma > 2.3', () => {
const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.3.0)'); const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.3.0)') as RootState;
expect(getSoapboxConfig(state).allowedEmoji.includes(RED_HEART_RGI)).toBe(true); expect(getSoapboxConfig(state).allowedEmoji.includes(RED_HEART_RGI)).toBe(true);
expect(getSoapboxConfig(state).allowedEmoji.includes(ASCII_HEART)).toBe(false); expect(getSoapboxConfig(state).allowedEmoji.includes(ASCII_HEART)).toBe(false);
}); });
it('returns an ASCII heart on Pleroma < 2.3', () => { it('returns an ASCII heart on Pleroma < 2.3', () => {
const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.0.0)'); const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.0.0)') as RootState;
expect(getSoapboxConfig(state).allowedEmoji.includes(ASCII_HEART)).toBe(true); expect(getSoapboxConfig(state).allowedEmoji.includes(ASCII_HEART)).toBe(true);
expect(getSoapboxConfig(state).allowedEmoji.includes(RED_HEART_RGI)).toBe(false); expect(getSoapboxConfig(state).allowedEmoji.includes(RED_HEART_RGI)).toBe(false);
}); });

View File

@ -4,8 +4,8 @@ import { openModal, closeModal } from './modals';
import type { AxiosError } from 'axios'; import type { AxiosError } from 'axios';
import type { AnyAction } from 'redux'; import type { AnyAction } from 'redux';
import type { Account } from 'soapbox/schemas';
import type { AppDispatch, RootState } from 'soapbox/store'; import type { AppDispatch, RootState } from 'soapbox/store';
import type { Account } from 'soapbox/types/entities';
const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST'; const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS'; const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';

View File

@ -111,7 +111,7 @@ const addToAliases = (account: Account) =>
dispatch(addToAliasesRequest()); dispatch(addToAliasesRequest());
api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, account.pleroma.get('ap_id')] }) api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, account.pleroma?.ap_id] })
.then((response => { .then((response => {
toast.success(messages.createSuccess); toast.success(messages.createSuccess);
dispatch(addToAliasesSuccess); dispatch(addToAliasesSuccess);

View File

@ -3,7 +3,6 @@ import { isLoggedIn } from 'soapbox/utils/auth';
import api, { getLinks } from '../api'; import api, { getLinks } from '../api';
import type { AxiosError } from 'axios'; import type { AxiosError } from 'axios';
import type { List as ImmutableList } from 'immutable';
import type { AppDispatch, RootState } from 'soapbox/store'; import type { AppDispatch, RootState } from 'soapbox/store';
const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST'; const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
@ -30,8 +29,11 @@ const blockDomain = (domain: string) =>
api(getState).post('/api/v1/domain_blocks', { domain }).then(() => { api(getState).post('/api/v1/domain_blocks', { domain }).then(() => {
const at_domain = '@' + domain; const at_domain = '@' + domain;
const accounts = getState().accounts.filter(item => item.acct.endsWith(at_domain)).valueSeq().map(item => item.id); const accounts = getState().accounts
dispatch(blockDomainSuccess(domain, accounts.toList())); .filter(item => item.acct.endsWith(at_domain))
.map(item => item.id);
dispatch(blockDomainSuccess(domain, accounts));
}).catch(err => { }).catch(err => {
dispatch(blockDomainFail(domain, err)); dispatch(blockDomainFail(domain, err));
}); });
@ -42,7 +44,7 @@ const blockDomainRequest = (domain: string) => ({
domain, domain,
}); });
const blockDomainSuccess = (domain: string, accounts: ImmutableList<string>) => ({ const blockDomainSuccess = (domain: string, accounts: string[]) => ({
type: DOMAIN_BLOCK_SUCCESS, type: DOMAIN_BLOCK_SUCCESS,
domain, domain,
accounts, accounts,
@ -68,8 +70,8 @@ const unblockDomain = (domain: string) =>
api(getState).delete('/api/v1/domain_blocks', params).then(() => { api(getState).delete('/api/v1/domain_blocks', params).then(() => {
const at_domain = '@' + domain; const at_domain = '@' + domain;
const accounts = getState().accounts.filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id')); const accounts = getState().accounts.filter(item => item.acct.endsWith(at_domain)).map(item => item.id);
dispatch(unblockDomainSuccess(domain, accounts.toList())); dispatch(unblockDomainSuccess(domain, accounts));
}).catch(err => { }).catch(err => {
dispatch(unblockDomainFail(domain, err)); dispatch(unblockDomainFail(domain, err));
}); });
@ -80,7 +82,7 @@ const unblockDomainRequest = (domain: string) => ({
domain, domain,
}); });
const unblockDomainSuccess = (domain: string, accounts: ImmutableList<string>) => ({ const unblockDomainSuccess = (domain: string, accounts: string[]) => ({
type: DOMAIN_UNBLOCK_SUCCESS, type: DOMAIN_UNBLOCK_SUCCESS,
domain, domain,
accounts, accounts,

View File

@ -3,8 +3,9 @@ import api from '../api';
import { openModal } from './modals'; import { openModal } from './modals';
import type { AxiosError } from 'axios'; import type { AxiosError } from 'axios';
import type { Account } from 'soapbox/schemas';
import type { AppDispatch, RootState } from 'soapbox/store'; import type { AppDispatch, RootState } from 'soapbox/store';
import type { Account, ChatMessage, Group, Status } from 'soapbox/types/entities'; import type { ChatMessage, Group, Status } from 'soapbox/types/entities';
const REPORT_INIT = 'REPORT_INIT'; const REPORT_INIT = 'REPORT_INIT';
const REPORT_CANCEL = 'REPORT_CANCEL'; const REPORT_CANCEL = 'REPORT_CANCEL';

View File

@ -40,7 +40,7 @@ const processTimelineUpdate = (timeline: string, status: APIEntity, accept: ((st
const hasPendingStatuses = !getState().pending_statuses.isEmpty(); const hasPendingStatuses = !getState().pending_statuses.isEmpty();
const columnSettings = getSettings(getState()).get(timeline, ImmutableMap()); const columnSettings = getSettings(getState()).get(timeline, ImmutableMap());
const shouldSkipQueue = shouldFilter(normalizeStatus(status) as Status, columnSettings); const shouldSkipQueue = shouldFilter(normalizeStatus(status) as Status, columnSettings as any);
if (ownStatus && hasPendingStatuses) { if (ownStatus && hasPendingStatuses) {
// WebSockets push statuses without the Idempotency-Key, // WebSockets push statuses without the Idempotency-Key,

View File

@ -1,20 +1,19 @@
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import React from 'react'; import React from 'react';
import { render, screen } from '../../jest/test-helpers'; import { buildAccount } from 'soapbox/jest/factory';
import { normalizeAccount } from '../../normalizers';
import Account from '../account';
import type { ReducerAccount } from 'soapbox/reducers/accounts'; import { render, screen } from '../../jest/test-helpers';
import Account from '../account';
describe('<Account />', () => { describe('<Account />', () => {
it('renders account name and username', () => { it('renders account name and username', () => {
const account = normalizeAccount({ const account = buildAccount({
id: '1', id: '1',
acct: 'justin-username', acct: 'justin-username',
display_name: 'Justin L', display_name: 'Justin L',
avatar: 'test.jpg', avatar: 'test.jpg',
}) as ReducerAccount; });
const store = { const store = {
accounts: ImmutableMap({ accounts: ImmutableMap({
@ -29,13 +28,13 @@ describe('<Account />', () => {
describe('verification badge', () => { describe('verification badge', () => {
it('renders verification badge', () => { it('renders verification badge', () => {
const account = normalizeAccount({ const account = buildAccount({
id: '1', id: '1',
acct: 'justin-username', acct: 'justin-username',
display_name: 'Justin L', display_name: 'Justin L',
avatar: 'test.jpg', avatar: 'test.jpg',
verified: true, verified: true,
}) as ReducerAccount; });
const store = { const store = {
accounts: ImmutableMap({ accounts: ImmutableMap({
@ -48,13 +47,13 @@ describe('<Account />', () => {
}); });
it('does not render verification badge', () => { it('does not render verification badge', () => {
const account = normalizeAccount({ const account = buildAccount({
id: '1', id: '1',
acct: 'justin-username', acct: 'justin-username',
display_name: 'Justin L', display_name: 'Justin L',
avatar: 'test.jpg', avatar: 'test.jpg',
verified: false, verified: false,
}) as ReducerAccount; });
const store = { const store = {
accounts: ImmutableMap({ accounts: ImmutableMap({

View File

@ -1,15 +1,13 @@
import React from 'react'; import React from 'react';
import { normalizeAccount } from 'soapbox/normalizers'; import { buildAccount } from 'soapbox/jest/factory';
import { render, screen } from '../../jest/test-helpers'; import { render, screen } from '../../jest/test-helpers';
import DisplayName from '../display-name'; import DisplayName from '../display-name';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
describe('<DisplayName />', () => { describe('<DisplayName />', () => {
it('renders display name + account name', () => { it('renders display name + account name', () => {
const account = normalizeAccount({ acct: 'bar@baz' }) as ReducerAccount; const account = buildAccount({ acct: 'bar@baz' });
render(<DisplayName account={account} />); render(<DisplayName account={account} />);
expect(screen.getByTestId('display-name')).toHaveTextContent('bar@baz'); expect(screen.getByTestId('display-name')).toHaveTextContent('bar@baz');

View File

@ -8,10 +8,10 @@ import { getAcct } from '../utils/accounts';
import { HStack, Text } from './ui'; import { HStack, Text } from './ui';
import VerificationBadge from './verification-badge'; import VerificationBadge from './verification-badge';
import type { Account } from 'soapbox/types/entities'; import type { Account } from 'soapbox/schemas';
interface IDisplayName { interface IDisplayName {
account: Account account: Pick<Account, 'id' | 'acct' | 'fqn' | 'verified' | 'display_name_html'>
withSuffix?: boolean withSuffix?: boolean
children?: React.ReactNode children?: React.ReactNode
} }
@ -37,7 +37,7 @@ const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = t
return ( return (
<span className='display-name' data-testid='display-name'> <span className='display-name' data-testid='display-name'>
<HoverRefWrapper accountId={account.get('id')} inline> <HoverRefWrapper accountId={account.id} inline>
{displayName} {displayName}
</HoverRefWrapper> </HoverRefWrapper>
{withSuffix && suffix} {withSuffix && suffix}

View File

@ -96,7 +96,7 @@ const SoapboxMount = () => {
const features = useFeatures(); const features = useFeatures();
const { pepeEnabled } = useRegistrationStatus(); const { pepeEnabled } = useRegistrationStatus();
const waitlisted = account && !account.source.get('approved', true); const waitlisted = account && account.source?.approved === false;
const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding); const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding);
const showOnboarding = account && !waitlisted && needsOnboarding; const showOnboarding = account && !waitlisted && needsOnboarding;
const { redirectRootNoLogin } = soapboxConfig; const { redirectRootNoLogin } = soapboxConfig;

View File

@ -26,7 +26,7 @@ function useEntityActions<TEntity extends Entity = Entity, Data = any>(
const { entityType, path } = parseEntitiesPath(expandedPath); const { entityType, path } = parseEntitiesPath(expandedPath);
const { deleteEntity, isSubmitting: deleteSubmitting } = const { deleteEntity, isSubmitting: deleteSubmitting } =
useDeleteEntity(entityType, (entityId) => api.delete(endpoints.delete!.replaceAll(':id', entityId))); useDeleteEntity(entityType, (entityId) => api.delete(endpoints.delete!.replace(/:id/g, entityId)));
const { createEntity, isSubmitting: createSubmitting } = const { createEntity, isSubmitting: createSubmitting } =
useCreateEntity<TEntity, Data>(path, (data) => api.post(endpoints.post!, data), opts); useCreateEntity<TEntity, Data>(path, (data) => api.post(endpoints.post!, data), opts);

View File

@ -615,7 +615,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
return ( return (
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6'> <div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6'>
{(account.moved && typeof account.moved === 'object') && ( {(account.moved && typeof account.moved === 'object') && (
<MovedNote from={account} to={account.moved} /> <MovedNote from={account} to={account.moved as Account} />
)} )}
<div> <div>

View File

@ -27,7 +27,7 @@ const UnapprovedAccount: React.FC<IUnapprovedAccount> = ({ accountId }) => {
<HStack space={4} justifyContent='between'> <HStack space={4} justifyContent='between'>
<Stack space={1}> <Stack space={1}>
<Text weight='semibold'> <Text weight='semibold'>
@{account.get('acct')} @{account.acct}
</Text> </Text>
<Text tag='blockquote' size='sm'> <Text tag='blockquote' size='sm'>
{adminAccount?.invite_request || ''} {adminAccount?.invite_request || ''}

View File

@ -8,15 +8,13 @@ import { HStack } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors'; import { makeGetAccount } from 'soapbox/selectors';
import type { List as ImmutableList } from 'immutable';
const messages = defineMessages({ const messages = defineMessages({
add: { id: 'aliases.account.add', defaultMessage: 'Create alias' }, add: { id: 'aliases.account.add', defaultMessage: 'Create alias' },
}); });
interface IAccount { interface IAccount {
accountId: string accountId: string
aliases: ImmutableList<string> aliases: string[]
} }
const Account: React.FC<IAccount> = ({ accountId, aliases }) => { const Account: React.FC<IAccount> = ({ accountId, aliases }) => {
@ -30,8 +28,9 @@ const Account: React.FC<IAccount> = ({ accountId, aliases }) => {
const added = useAppSelector((state) => { const added = useAppSelector((state) => {
const account = getAccount(state, accountId); const account = getAccount(state, accountId);
const apId = account?.pleroma.get('ap_id'); const apId = account?.pleroma?.ap_id;
const name = features.accountMoving ? account?.acct : apId; const name = features.accountMoving ? account?.acct : apId;
if (!name) return false;
return aliases.includes(name); return aliases.includes(name);
}); });

View File

@ -1,4 +1,3 @@
import { List as ImmutableList } from 'immutable';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@ -28,11 +27,11 @@ const Aliases = () => {
const aliases = useAppSelector((state) => { const aliases = useAppSelector((state) => {
if (features.accountMoving) { if (features.accountMoving) {
return state.aliases.aliases.items; return state.aliases.aliases.items.toArray();
} else { } else {
return account!.pleroma.get('also_known_as'); return account?.pleroma?.also_known_as ?? [];
} }
}) as ImmutableList<string>; });
const searchAccountIds = useAppSelector((state) => state.aliases.suggestions.items); const searchAccountIds = useAppSelector((state) => state.aliases.suggestions.items);
const loaded = useAppSelector((state) => state.aliases.suggestions.loaded); const loaded = useAppSelector((state) => state.aliases.suggestions.loaded);

View File

@ -23,7 +23,7 @@ const Account: React.FC<IAccount> = ({ accountId }) => {
if (!account) return null; if (!account) return null;
const birthday = account.birthday; const birthday = account.pleroma?.birthday;
if (!birthday) return null; if (!birthday) return null;
const formattedBirthday = intl.formatDate(birthday, { day: 'numeric', month: 'short', year: 'numeric' }); const formattedBirthday = intl.formatDate(birthday, { day: 'numeric', month: 'short', year: 'numeric' });

View File

@ -4,8 +4,8 @@ import { VirtuosoMockContext } from 'react-virtuoso';
import { ChatContext } from 'soapbox/contexts/chat-context'; import { ChatContext } from 'soapbox/contexts/chat-context';
import { buildAccount } from 'soapbox/jest/factory';
import { normalizeChatMessage, normalizeInstance } from 'soapbox/normalizers'; import { normalizeChatMessage, normalizeInstance } from 'soapbox/normalizers';
import { IAccount } from 'soapbox/queries/accounts';
import { ChatMessage } from 'soapbox/types/entities'; import { ChatMessage } from 'soapbox/types/entities';
import { __stub } from '../../../../api'; import { __stub } from '../../../../api';
@ -15,7 +15,7 @@ import ChatMessageList from '../chat-message-list';
const chat: IChat = { const chat: IChat = {
accepted: true, accepted: true,
account: { account: buildAccount({
username: 'username', username: 'username',
verified: true, verified: true,
id: '1', id: '1',
@ -23,7 +23,7 @@ const chat: IChat = {
avatar: 'avatar', avatar: 'avatar',
avatar_static: 'avatar', avatar_static: 'avatar',
display_name: 'my name', display_name: 'my name',
} as IAccount, }),
chat_type: 'direct', chat_type: 'direct',
created_at: '2020-06-10T02:05:06.000Z', created_at: '2020-06-10T02:05:06.000Z',
created_by_account: '2', created_by_account: '2',

View File

@ -1,26 +1,32 @@
import { Map as ImmutableMap } from 'immutable';
import React from 'react'; import React from 'react';
import { Route, Switch } from 'react-router-dom'; import { Route, Switch } from 'react-router-dom';
import { normalizeAccount } from 'soapbox/normalizers'; import { buildAccount } from 'soapbox/jest/factory';
import { render, rootState } from '../../../../jest/test-helpers'; import { render, rootState } from '../../../../jest/test-helpers';
import ChatWidget from '../chat-widget/chat-widget'; import ChatWidget from '../chat-widget/chat-widget';
const id = '1'; const id = '1';
const account = normalizeAccount({ const account = buildAccount({
id, id,
acct: 'justin-username', acct: 'justin-username',
display_name: 'Justin L', display_name: 'Justin L',
avatar: 'test.jpg', avatar: 'test.jpg',
source: {
chats_onboarded: true, chats_onboarded: true,
},
}); });
const store = rootState const store = rootState
.set('me', id) .set('me', id)
.set('accounts', ImmutableMap({ .set('entities', {
'ACCOUNTS': {
store: {
[id]: account, [id]: account,
}) as any); },
lists: {},
},
});
describe('<ChatWidget />', () => { describe('<ChatWidget />', () => {
describe('when on the /chats endpoint', () => { describe('when on the /chats endpoint', () => {
@ -45,16 +51,23 @@ describe('<ChatWidget />', () => {
describe('when the user has not onboarded chats', () => { describe('when the user has not onboarded chats', () => {
it('hides the widget', async () => { it('hides the widget', async () => {
const accountWithoutChats = normalizeAccount({ const accountWithoutChats = buildAccount({
id, id,
acct: 'justin-username', acct: 'justin-username',
display_name: 'Justin L', display_name: 'Justin L',
avatar: 'test.jpg', avatar: 'test.jpg',
source: {
chats_onboarded: false, chats_onboarded: false,
},
}); });
const newStore = store.set('accounts', ImmutableMap({ const newStore = store.set('entities', {
'ACCOUNTS': {
store: {
[id]: accountWithoutChats, [id]: accountWithoutChats,
}) as any); },
lists: {},
},
});
const screen = render( const screen = render(
<ChatWidget />, <ChatWidget />,

View File

@ -13,7 +13,6 @@ import emojify from 'soapbox/features/emoji';
import Bundle from 'soapbox/features/ui/components/bundle'; import Bundle from 'soapbox/features/ui/components/bundle';
import { MediaGallery } from 'soapbox/features/ui/util/async-components'; import { MediaGallery } from 'soapbox/features/ui/util/async-components';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import { normalizeAccount } from 'soapbox/normalizers';
import { ChatKeys, IChat, useChatActions } from 'soapbox/queries/chats'; import { ChatKeys, IChat, useChatActions } from 'soapbox/queries/chats';
import { queryClient } from 'soapbox/queries/client'; import { queryClient } from 'soapbox/queries/client';
import { stripHTML } from 'soapbox/utils/html'; import { stripHTML } from 'soapbox/utils/html';
@ -24,7 +23,7 @@ import ChatMessageReactionWrapper from './chat-message-reaction-wrapper/chat-mes
import type { Menu as IMenu } from 'soapbox/components/dropdown-menu'; import type { Menu as IMenu } from 'soapbox/components/dropdown-menu';
import type { IMediaGallery } from 'soapbox/components/media-gallery'; import type { IMediaGallery } from 'soapbox/components/media-gallery';
import type { Account, ChatMessage as ChatMessageEntity } from 'soapbox/types/entities'; import type { ChatMessage as ChatMessageEntity } from 'soapbox/types/entities';
const messages = defineMessages({ const messages = defineMessages({
copy: { id: 'chats.actions.copy', defaultMessage: 'Copy' }, copy: { id: 'chats.actions.copy', defaultMessage: 'Copy' },
@ -178,7 +177,7 @@ const ChatMessage = (props: IChatMessage) => {
if (features.reportChats) { if (features.reportChats) {
menu.push({ menu.push({
text: intl.formatMessage(messages.report), text: intl.formatMessage(messages.report),
action: () => dispatch(initReport(ReportableEntities.CHAT_MESSAGE, normalizeAccount(chat.account) as Account, { chatMessage })), action: () => dispatch(initReport(ReportableEntities.CHAT_MESSAGE, chat.account, { chatMessage })),
icon: require('@tabler/icons/flag.svg'), icon: require('@tabler/icons/flag.svg'),
}); });
} }

View File

@ -19,7 +19,7 @@ const ChatPage: React.FC<IChatPage> = ({ chatId }) => {
const account = useOwnAccount(); const account = useOwnAccount();
const history = useHistory(); const history = useHistory();
const isOnboarded = account?.chats_onboarded; const isOnboarded = account?.source?.chats_onboarded ?? true;
const path = history.location.pathname; const path = history.location.pathname;
const isSidebarHidden = matchPath(path, { const isSidebarHidden = matchPath(path, {

View File

@ -33,7 +33,7 @@ const ChatPageSettings = () => {
const [data, setData] = useState<FormData>({ const [data, setData] = useState<FormData>({
chats_onboarded: true, chats_onboarded: true,
accepts_chat_messages: account?.accepts_chat_messages, accepts_chat_messages: account?.pleroma?.accepts_chat_messages === true,
}); });
const onToggleChange = (key: string[], checked: boolean) => { const onToggleChange = (key: string[], checked: boolean) => {

View File

@ -26,7 +26,7 @@ const Welcome = () => {
const [data, setData] = useState<FormData>({ const [data, setData] = useState<FormData>({
chats_onboarded: true, chats_onboarded: true,
accepts_chat_messages: account?.accepts_chat_messages, accepts_chat_messages: account?.pleroma?.accepts_chat_messages === true,
}); });
const handleSubmit = (event: React.FormEvent) => { const handleSubmit = (event: React.FormEvent) => {

View File

@ -11,9 +11,10 @@ const ChatWidget = () => {
const history = useHistory(); const history = useHistory();
const path = history.location.pathname; const path = history.location.pathname;
const shouldHideWidget = Boolean(path.match(/^\/chats/)); const isChatsPath = Boolean(path.match(/^\/chats/));
const isOnboarded = account?.source?.chats_onboarded ?? true;
if (!account?.chats_onboarded || shouldHideWidget) { if (!isOnboarded || isChatsPath) {
return null; return null;
} }

View File

@ -19,7 +19,7 @@ const Conversation: React.FC<IConversation> = ({ conversationId, onMoveUp, onMov
const conversation = state.conversations.items.find(x => x.id === conversationId)!; const conversation = state.conversations.items.find(x => x.id === conversationId)!;
return { return {
accounts: conversation.accounts.map((accountId: string) => state.accounts.get(accountId, null)!), accounts: conversation.accounts.map((accountId: string) => state.accounts.get(accountId)!),
unread: conversation.unread, unread: conversation.unread,
lastStatusId: conversation.last_status || null, lastStatusId: conversation.last_status || null,
}; };

View File

@ -87,10 +87,10 @@ const AccountCard: React.FC<IAccountCard> = ({ id }) => {
<Stack> <Stack>
<Text theme='primary' size='md' weight='medium'> <Text theme='primary' size='md' weight='medium'>
{account.last_status_at === null ? ( {account.last_status_at ? (
<FormattedMessage id='account.never_active' defaultMessage='Never' />
) : (
<RelativeTimestamp theme='inherit' timestamp={account.last_status_at} /> <RelativeTimestamp theme='inherit' timestamp={account.last_status_at} />
) : (
<FormattedMessage id='account.never_active' defaultMessage='Never' />
)} )}
</Text> </Text>

View File

@ -8,7 +8,7 @@ import { useSoapboxConfig } from 'soapbox/hooks';
import type { Account } from 'soapbox/types/entities'; import type { Account } from 'soapbox/types/entities';
interface IProfilePreview { interface IProfilePreview {
account: Account account: Pick<Account, 'acct' | 'fqn' | 'avatar' | 'header' | 'verified' | 'display_name_html'>
} }
/** Displays a preview of the user's account, including avatar, banner, etc. */ /** Displays a preview of the user's account, including avatar, banner, etc. */

View File

@ -1,4 +1,3 @@
import { List as ImmutableList } from 'immutable';
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
@ -20,22 +19,26 @@ import {
Toggle, Toggle,
} from 'soapbox/components/ui'; } from 'soapbox/components/ui';
import { useAppDispatch, useOwnAccount, useFeatures, useInstance } from 'soapbox/hooks'; import { useAppDispatch, useOwnAccount, useFeatures, useInstance } from 'soapbox/hooks';
import { normalizeAccount } from 'soapbox/normalizers'; import { accountSchema } from 'soapbox/schemas';
import toast from 'soapbox/toast'; import toast from 'soapbox/toast';
import resizeImage from 'soapbox/utils/resize-image'; import resizeImage from 'soapbox/utils/resize-image';
import ProfilePreview from './components/profile-preview'; import ProfilePreview from './components/profile-preview';
import type { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield'; import type { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
import type { Account } from 'soapbox/types/entities'; import type { Account } from 'soapbox/schemas';
/** /**
* Whether the user is hiding their follows and/or followers. * Whether the user is hiding their follows and/or followers.
* Pleroma's config is granular, but we simplify it into one setting. * Pleroma's config is granular, but we simplify it into one setting.
*/ */
const hidesNetwork = (account: Account): boolean => { const hidesNetwork = ({ pleroma }: Account): boolean => {
const { hide_followers, hide_follows, hide_followers_count, hide_follows_count } = account.pleroma.toJS(); return Boolean(
return Boolean(hide_followers && hide_follows && hide_followers_count && hide_follows_count); pleroma?.hide_followers &&
pleroma?.hide_follows &&
pleroma?.hide_followers_count &&
pleroma?.hide_follows_count,
);
}; };
const messages = defineMessages({ const messages = defineMessages({
@ -124,18 +127,18 @@ const accountToCredentials = (account: Account): AccountCredentials => {
discoverable: account.discoverable, discoverable: account.discoverable,
bot: account.bot, bot: account.bot,
display_name: account.display_name, display_name: account.display_name,
note: account.source.get('note', ''), note: account.source?.note ?? '',
locked: account.locked, locked: account.locked,
fields_attributes: [...account.source.get<Iterable<AccountCredentialsField>>('fields', ImmutableList()).toJS()], fields_attributes: [...account.source?.fields ?? []],
stranger_notifications: account.getIn(['pleroma', 'notification_settings', 'block_from_strangers']) === true, stranger_notifications: account.pleroma?.notification_settings?.block_from_strangers === true,
accepts_email_list: account.getIn(['pleroma', 'accepts_email_list']) === true, accepts_email_list: account.pleroma?.accepts_email_list === true,
hide_followers: hideNetwork, hide_followers: hideNetwork,
hide_follows: hideNetwork, hide_follows: hideNetwork,
hide_followers_count: hideNetwork, hide_followers_count: hideNetwork,
hide_follows_count: hideNetwork, hide_follows_count: hideNetwork,
website: account.website, website: account.website,
location: account.location, location: account.location,
birthday: account.birthday, birthday: account.pleroma?.birthday ?? undefined,
}; };
}; };
@ -299,12 +302,13 @@ const EditProfile: React.FC = () => {
/** Preview account data. */ /** Preview account data. */
const previewAccount = useMemo(() => { const previewAccount = useMemo(() => {
return normalizeAccount({ return accountSchema.parse({
...account?.toJS(), id: '1',
...account,
...data, ...data,
avatar: avatarUrl, avatar: avatarUrl,
header: headerUrl, header: headerUrl,
}) as Account; });
}, [account?.id, data.display_name, avatarUrl, headerUrl]); }, [account?.id, data.display_name, avatarUrl, headerUrl]);
return ( return (

View File

@ -18,7 +18,7 @@ const BioStep = ({ onNext }: { onNext: () => void }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const account = useOwnAccount(); const account = useOwnAccount();
const [value, setValue] = React.useState<string>(account?.source.get('note') || ''); const [value, setValue] = React.useState<string>(account?.source?.note ?? '');
const [isSubmitting, setSubmitting] = React.useState<boolean>(false); const [isSubmitting, setSubmitting] = React.useState<boolean>(false);
const [errors, setErrors] = React.useState<string[]>([]); const [errors, setErrors] = React.useState<string[]>([]);

View File

@ -29,7 +29,7 @@ const MessagesSettings = () => {
label={intl.formatMessage(messages.label)} label={intl.formatMessage(messages.label)}
> >
<Toggle <Toggle
checked={account.accepts_chat_messages} checked={account.pleroma?.accepts_chat_messages}
onChange={handleChange} onChange={handleChange}
/> />
</ListItem> </ListItem>

View File

@ -1,13 +1,10 @@
import React from 'react'; import React from 'react';
import { buildRelationship } from 'soapbox/jest/factory'; import { buildAccount, buildRelationship } from 'soapbox/jest/factory';
import { render, screen } from 'soapbox/jest/test-helpers'; import { render, screen } from 'soapbox/jest/test-helpers';
import { normalizeAccount } from 'soapbox/normalizers';
import SubscribeButton from '../subscription-button'; import SubscribeButton from '../subscription-button';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
const justin = { const justin = {
id: '1', id: '1',
acct: 'justin-username', acct: 'justin-username',
@ -20,7 +17,7 @@ describe('<SubscribeButton />', () => {
describe('with "accountNotifies" disabled', () => { describe('with "accountNotifies" disabled', () => {
it('renders nothing', () => { it('renders nothing', () => {
const account = normalizeAccount({ ...justin, relationship: buildRelationship({ following: true }) }) as ReducerAccount; const account = buildAccount({ ...justin, relationship: buildRelationship({ following: true }) });
render(<SubscribeButton account={account} />, undefined, store); render(<SubscribeButton account={account} />, undefined, store);
expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);

View File

@ -6,7 +6,7 @@ import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import { Stack, Text } from 'soapbox/components/ui'; import { Stack, Text } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks'; import { useAppSelector } from 'soapbox/hooks';
import type { ReducerAccount } from 'soapbox/reducers/accounts'; import type { Account } from 'soapbox/schemas';
const messages = defineMessages({ const messages = defineMessages({
accountEntity: { id: 'report.confirmation.entity.account', defaultMessage: 'account' }, accountEntity: { id: 'report.confirmation.entity.account', defaultMessage: 'account' },
@ -15,8 +15,8 @@ const messages = defineMessages({
content: { id: 'report.confirmation.content', defaultMessage: 'If we find that this {entity} is violating the {link} we will take further action on the matter.' }, content: { id: 'report.confirmation.content', defaultMessage: 'If we find that this {entity} is violating the {link} we will take further action on the matter.' },
}); });
interface IOtherActionsStep { interface IConfirmationStep {
account: ReducerAccount account?: Account
} }
const termsOfServiceText = (<FormattedMessage const termsOfServiceText = (<FormattedMessage
@ -34,7 +34,7 @@ const renderTermsOfServiceLink = (href: string) => (
</a> </a>
); );
const ConfirmationStep = ({ account }: IOtherActionsStep) => { const ConfirmationStep: React.FC<IConfirmationStep> = () => {
const intl = useIntl(); const intl = useIntl();
const links = useAppSelector((state) => getSoapboxConfig(state).get('links') as any); const links = useAppSelector((state) => getSoapboxConfig(state).get('links') as any);
const entityType = useAppSelector((state) => state.reports.new.entityType); const entityType = useAppSelector((state) => state.reports.new.entityType);

View File

@ -9,7 +9,7 @@ import StatusCheckBox from 'soapbox/features/report/components/status-check-box'
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import { isRemote, getDomain } from 'soapbox/utils/accounts'; import { isRemote, getDomain } from 'soapbox/utils/accounts';
import type { ReducerAccount } from 'soapbox/reducers/accounts'; import type { Account } from 'soapbox/schemas';
const messages = defineMessages({ const messages = defineMessages({
addAdditionalStatuses: { id: 'report.otherActions.addAdditional', defaultMessage: 'Would you like to add additional statuses to this report?' }, addAdditionalStatuses: { id: 'report.otherActions.addAdditional', defaultMessage: 'Would you like to add additional statuses to this report?' },
@ -20,7 +20,7 @@ const messages = defineMessages({
}); });
interface IOtherActionsStep { interface IOtherActionsStep {
account: ReducerAccount account: Account
} }
const OtherActionsStep = ({ account }: IOtherActionsStep) => { const OtherActionsStep = ({ account }: IOtherActionsStep) => {
@ -104,7 +104,7 @@ const OtherActionsStep = ({ account }: IOtherActionsStep) => {
/> />
<Text theme='muted' tag='label' size='sm' htmlFor='report-block'> <Text theme='muted' tag='label' size='sm' htmlFor='report-block'>
<FormattedMessage id='report.block' defaultMessage='Block {target}' values={{ target: `@${account.get('acct')}` }} /> <FormattedMessage id='report.block' defaultMessage='Block {target}' values={{ target: `@${account.acct}` }} />
</Text> </Text>
</HStack> </HStack>
</FormGroup> </FormGroup>

View File

@ -7,7 +7,7 @@ import { fetchRules } from 'soapbox/actions/rules';
import { FormGroup, Stack, Text, Textarea } from 'soapbox/components/ui'; import { FormGroup, Stack, Text, Textarea } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import type { ReducerAccount } from 'soapbox/reducers/accounts'; import type { Account } from 'soapbox/schemas';
const messages = defineMessages({ const messages = defineMessages({
placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' }, placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
@ -15,12 +15,12 @@ const messages = defineMessages({
}); });
interface IReasonStep { interface IReasonStep {
account: ReducerAccount account?: Account
} }
const RULES_HEIGHT = 385; const RULES_HEIGHT = 385;
const ReasonStep = (_props: IReasonStep) => { const ReasonStep: React.FC<IReasonStep> = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const intl = useIntl(); const intl = useIntl();

View File

@ -7,7 +7,7 @@ import { HStack, Icon } from 'soapbox/components/ui';
import BundleContainer from 'soapbox/features/ui/containers/bundle-container'; import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
import { CryptoAddress } from 'soapbox/features/ui/util/async-components'; import { CryptoAddress } from 'soapbox/features/ui/util/async-components';
import type { Field } from 'soapbox/types/entities'; import type { Account } from 'soapbox/schemas';
const getTicker = (value: string): string => (value.match(/\$([a-zA-Z]*)/i) || [])[1]; const getTicker = (value: string): string => (value.match(/\$([a-zA-Z]*)/i) || [])[1];
const isTicker = (value: string): boolean => Boolean(getTicker(value)); const isTicker = (value: string): boolean => Boolean(getTicker(value));
@ -26,7 +26,7 @@ const dateFormatOptions: FormatDateOptions = {
}; };
interface IProfileField { interface IProfileField {
field: Field field: Account['fields'][number]
} }
/** Renders a single profile field. */ /** Renders a single profile field. */

View File

@ -86,7 +86,7 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
}; };
const renderBirthday = (): React.ReactNode => { const renderBirthday = (): React.ReactNode => {
const birthday = account.birthday; const birthday = account.pleroma?.birthday;
if (!birthday) return null; if (!birthday) return null;
const formattedBirthday = intl.formatDate(birthday, { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' }); const formattedBirthday = intl.formatDate(birthday, { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' });
@ -131,7 +131,7 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
} }
const content = { __html: account.note_emojified }; const content = { __html: account.note_emojified };
const deactivated = !account.pleroma.get('is_active', true) === true; const deactivated = account.pleroma?.deactivated ?? false;
const displayNameHtml = deactivated ? { __html: intl.formatMessage(messages.deactivated) } : { __html: account.display_name_html }; const displayNameHtml = deactivated ? { __html: intl.formatMessage(messages.deactivated) } : { __html: account.display_name_html };
const memberSinceDate = intl.formatDate(account.created_at, { month: 'long', year: 'numeric' }); const memberSinceDate = intl.formatDate(account.created_at, { month: 'long', year: 'numeric' });
const badges = getBadges(); const badges = getBadges();
@ -229,7 +229,7 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
<ProfileFamiliarFollowers account={account} /> <ProfileFamiliarFollowers account={account} />
</Stack> </Stack>
{account.fields.size > 0 && ( {account.fields.length > 0 && (
<Stack space={2} className='mt-4 xl:hidden'> <Stack space={2} className='mt-4 xl:hidden'>
{account.fields.map((field, i) => ( {account.fields.map((field, i) => (
<ProfileField field={field} key={i} /> <ProfileField field={field} key={i} />

View File

@ -22,7 +22,7 @@ const messages = defineMessages({
}); });
interface ISubscriptionButton { interface ISubscriptionButton {
account: AccountEntity account: Pick<AccountEntity, 'id' | 'username' | 'relationship'>
} }
const SubscriptionButton = ({ account }: ISubscriptionButton) => { const SubscriptionButton = ({ account }: ISubscriptionButton) => {
@ -36,8 +36,8 @@ const SubscriptionButton = ({ account }: ISubscriptionButton) => {
? account.relationship?.notifying ? account.relationship?.notifying
: account.relationship?.subscribing; : account.relationship?.subscribing;
const title = isSubscribed const title = isSubscribed
? intl.formatMessage(messages.unsubscribe, { name: account.get('username') }) ? intl.formatMessage(messages.unsubscribe, { name: account.username })
: intl.formatMessage(messages.subscribe, { name: account.get('username') }); : intl.formatMessage(messages.subscribe, { name: account.username });
const onSubscribeSuccess = () => const onSubscribeSuccess = () =>
toast.success(intl.formatMessage(messages.subscribeSuccess)); toast.success(intl.formatMessage(messages.subscribeSuccess));
@ -53,11 +53,11 @@ const SubscriptionButton = ({ account }: ISubscriptionButton) => {
const onNotifyToggle = () => { const onNotifyToggle = () => {
if (account.relationship?.notifying) { if (account.relationship?.notifying) {
dispatch(followAccount(account.get('id'), { notify: false } as any)) dispatch(followAccount(account.id, { notify: false } as any))
?.then(() => onUnsubscribeSuccess()) ?.then(() => onUnsubscribeSuccess())
.catch(() => onUnsubscribeFailure()); .catch(() => onUnsubscribeFailure());
} else { } else {
dispatch(followAccount(account.get('id'), { notify: true } as any)) dispatch(followAccount(account.id, { notify: true } as any))
?.then(() => onSubscribeSuccess()) ?.then(() => onSubscribeSuccess())
.catch(() => onSubscribeFailure()); .catch(() => onSubscribeFailure());
} }
@ -65,11 +65,11 @@ const SubscriptionButton = ({ account }: ISubscriptionButton) => {
const onSubscriptionToggle = () => { const onSubscriptionToggle = () => {
if (account.relationship?.subscribing) { if (account.relationship?.subscribing) {
dispatch(unsubscribeAccount(account.get('id'))) dispatch(unsubscribeAccount(account.id))
?.then(() => onUnsubscribeSuccess()) ?.then(() => onUnsubscribeSuccess())
.catch(() => onUnsubscribeFailure()); .catch(() => onUnsubscribeFailure());
} else { } else {
dispatch(subscribeAccount(account.get('id'))) dispatch(subscribeAccount(account.id))
?.then(() => onSubscribeSuccess()) ?.then(() => onSubscribeSuccess())
.catch(() => onSubscribeFailure()); .catch(() => onSubscribeFailure());
} }

View File

@ -14,7 +14,7 @@ const WaitlistPage = () => {
const instance = useInstance(); const instance = useInstance();
const me = useOwnAccount(); const me = useOwnAccount();
const isSmsVerified = me?.source.get('sms_verified'); const isSmsVerified = me?.source?.sms_verified ?? true;
const onClickLogOut: React.MouseEventHandler = (event) => { const onClickLogOut: React.MouseEventHandler = (event) => {
event.preventDefault(); event.preventDefault();

View File

@ -1,6 +1,5 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { normalizeStatus } from 'soapbox/normalizers';
import { import {
accountSchema, accountSchema,
adSchema, adSchema,
@ -10,6 +9,7 @@ import {
groupSchema, groupSchema,
groupTagSchema, groupTagSchema,
relationshipSchema, relationshipSchema,
statusSchema,
type Account, type Account,
type Ad, type Ad,
type Card, type Card,
@ -22,22 +22,24 @@ import {
} from 'soapbox/schemas'; } from 'soapbox/schemas';
import { GroupRoles } from 'soapbox/schemas/group-member'; import { GroupRoles } from 'soapbox/schemas/group-member';
import type { PartialDeep } from 'type-fest';
// TODO: there's probably a better way to create these factory functions. // TODO: there's probably a better way to create these factory functions.
// This looks promising but didn't work on my first attempt: https://github.com/anatine/zod-plugins/tree/main/packages/zod-mock // This looks promising but didn't work on my first attempt: https://github.com/anatine/zod-plugins/tree/main/packages/zod-mock
function buildAccount(props: Partial<Account> = {}): Account { function buildAccount(props: PartialDeep<Account> = {}): Account {
return accountSchema.parse(Object.assign({ return accountSchema.parse(Object.assign({
id: uuidv4(), id: uuidv4(),
}, props)); }, props));
} }
function buildCard(props: Partial<Card> = {}): Card { function buildCard(props: PartialDeep<Card> = {}): Card {
return cardSchema.parse(Object.assign({ return cardSchema.parse(Object.assign({
url: 'https://soapbox.test', url: 'https://soapbox.test',
}, props)); }, props));
} }
function buildGroup(props: Partial<Group> = {}): Group { function buildGroup(props: PartialDeep<Group> = {}): Group {
return groupSchema.parse(Object.assign({ return groupSchema.parse(Object.assign({
id: uuidv4(), id: uuidv4(),
owner: { owner: {
@ -46,13 +48,13 @@ function buildGroup(props: Partial<Group> = {}): Group {
}, props)); }, props));
} }
function buildGroupRelationship(props: Partial<GroupRelationship> = {}): GroupRelationship { function buildGroupRelationship(props: PartialDeep<GroupRelationship> = {}): GroupRelationship {
return groupRelationshipSchema.parse(Object.assign({ return groupRelationshipSchema.parse(Object.assign({
id: uuidv4(), id: uuidv4(),
}, props)); }, props));
} }
function buildGroupTag(props: Partial<GroupTag> = {}): GroupTag { function buildGroupTag(props: PartialDeep<GroupTag> = {}): GroupTag {
return groupTagSchema.parse(Object.assign({ return groupTagSchema.parse(Object.assign({
id: uuidv4(), id: uuidv4(),
name: uuidv4(), name: uuidv4(),
@ -60,8 +62,8 @@ function buildGroupTag(props: Partial<GroupTag> = {}): GroupTag {
} }
function buildGroupMember( function buildGroupMember(
props: Partial<GroupMember> = {}, props: PartialDeep<GroupMember> = {},
accountProps: Partial<Account> = {}, accountProps: PartialDeep<Account> = {},
): GroupMember { ): GroupMember {
return groupMemberSchema.parse(Object.assign({ return groupMemberSchema.parse(Object.assign({
id: uuidv4(), id: uuidv4(),
@ -70,25 +72,26 @@ function buildGroupMember(
}, props)); }, props));
} }
function buildAd(props: Partial<Ad> = {}): Ad { function buildAd(props: PartialDeep<Ad> = {}): Ad {
return adSchema.parse(Object.assign({ return adSchema.parse(Object.assign({
card: buildCard(), card: buildCard(),
}, props)); }, props));
} }
function buildRelationship(props: Partial<Relationship> = {}): Relationship { function buildRelationship(props: PartialDeep<Relationship> = {}): Relationship {
return relationshipSchema.parse(Object.assign({ return relationshipSchema.parse(Object.assign({
id: uuidv4(), id: uuidv4(),
}, props)); }, props));
} }
function buildStatus(props: Partial<Status> = {}) { function buildStatus(props: PartialDeep<Status> = {}) {
return normalizeStatus(Object.assign({ return statusSchema.parse(Object.assign({
id: uuidv4(), id: uuidv4(),
}, props)); }, props));
} }
export { export {
buildAccount,
buildAd, buildAd,
buildCard, buildCard,
buildGroup, buildGroup,

View File

@ -1,10 +1,9 @@
import { Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable'; import { Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
import type { Account, EmbeddedEntity } from 'soapbox/types/entities'; import type { Account, EmbeddedEntity } from 'soapbox/types/entities';
export const ChatRecord = ImmutableRecord({ export const ChatRecord = ImmutableRecord({
account: null as EmbeddedEntity<Account | ReducerAccount>, account: null as EmbeddedEntity<Account>,
id: '', id: '',
unread: 0, unread: 0,
last_message: '' as string || null, last_message: '' as string || null,

View File

@ -13,9 +13,8 @@ import {
import { normalizeAttachment } from 'soapbox/normalizers/attachment'; import { normalizeAttachment } from 'soapbox/normalizers/attachment';
import { normalizeEmoji } from 'soapbox/normalizers/emoji'; import { normalizeEmoji } from 'soapbox/normalizers/emoji';
import { normalizeMention } from 'soapbox/normalizers/mention'; import { normalizeMention } from 'soapbox/normalizers/mention';
import { cardSchema, pollSchema, tombstoneSchema } from 'soapbox/schemas'; import { accountSchema, cardSchema, pollSchema, tombstoneSchema } from 'soapbox/schemas';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
import type { Account, Attachment, Card, Emoji, Group, Mention, Poll, EmbeddedEntity } from 'soapbox/types/entities'; import type { Account, Attachment, Card, Emoji, Group, Mention, Poll, EmbeddedEntity } from 'soapbox/types/entities';
export type StatusApprovalStatus = 'pending' | 'approval' | 'rejected'; export type StatusApprovalStatus = 'pending' | 'approval' | 'rejected';
@ -42,7 +41,7 @@ interface Tombstone {
// https://docs.joinmastodon.org/entities/status/ // https://docs.joinmastodon.org/entities/status/
export const StatusRecord = ImmutableRecord({ export const StatusRecord = ImmutableRecord({
account: null as EmbeddedEntity<Account | ReducerAccount>, account: null as unknown as Account,
application: null as ImmutableMap<string, any> | null, application: null as ImmutableMap<string, any> | null,
approval_status: 'approved' as StatusApprovalStatus, approval_status: 'approved' as StatusApprovalStatus,
bookmarked: false, bookmarked: false,
@ -244,6 +243,15 @@ const normalizeDislikes = (status: ImmutableMap<string, any>) => {
return status; return status;
}; };
const parseAccount = (status: ImmutableMap<string, any>) => {
try {
const account = accountSchema.parse(status.get('account').toJS());
return status.set('account', account);
} catch (_e) {
return status.set('account', null);
}
};
export const normalizeStatus = (status: Record<string, any>) => { export const normalizeStatus = (status: Record<string, any>) => {
return StatusRecord( return StatusRecord(
ImmutableMap(fromJS(status)).withMutations(status => { ImmutableMap(fromJS(status)).withMutations(status => {
@ -261,6 +269,7 @@ export const normalizeStatus = (status: Record<string, any>) => {
normalizeFilterResults(status); normalizeFilterResults(status);
normalizeDislikes(status); normalizeDislikes(status);
normalizeTombstone(status); normalizeTombstone(status);
parseAccount(status);
}), }),
); );
}; };

View File

@ -71,7 +71,7 @@ const ProfilePage: React.FC<IProfilePage> = ({ params, children }) => {
if (account) { if (account) {
const ownAccount = account.id === me; const ownAccount = account.id === me;
if (ownAccount || !account.pleroma.get('hide_favorites', true)) { if (ownAccount || account.pleroma?.hide_favorites !== true) {
tabItems.push({ tabItems.push({
text: <FormattedMessage id='navigation_bar.favourites' defaultMessage='Likes' />, text: <FormattedMessage id='navigation_bar.favourites' defaultMessage='Likes' />,
to: `/@${account.acct}/favorites`, to: `/@${account.acct}/favorites`,
@ -129,7 +129,7 @@ const ProfilePage: React.FC<IProfilePage> = ({ params, children }) => {
<BundleContainer fetchComponent={ProfileMediaPanel}> <BundleContainer fetchComponent={ProfileMediaPanel}>
{Component => <Component account={account} />} {Component => <Component account={account} />}
</BundleContainer> </BundleContainer>
{account && !account.fields.isEmpty() && ( {account && !account.fields.length && (
<BundleContainer fetchComponent={ProfileFieldsPanel}> <BundleContainer fetchComponent={ProfileFieldsPanel}>
{Component => <Component account={account} />} {Component => <Component account={account} />}
</BundleContainer> </BundleContainer>

View File

@ -3,19 +3,18 @@ import sumBy from 'lodash/sumBy';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { __stub } from 'soapbox/api'; import { __stub } from 'soapbox/api';
import { buildRelationship } from 'soapbox/jest/factory'; import { buildAccount, buildRelationship } from 'soapbox/jest/factory';
import { createTestStore, mockStore, queryClient, renderHook, rootState, waitFor } from 'soapbox/jest/test-helpers'; import { createTestStore, mockStore, queryClient, renderHook, rootState, waitFor } from 'soapbox/jest/test-helpers';
import { normalizeChatMessage } from 'soapbox/normalizers'; import { normalizeChatMessage } from 'soapbox/normalizers';
import { Store } from 'soapbox/store'; import { Store } from 'soapbox/store';
import { ChatMessage } from 'soapbox/types/entities'; import { ChatMessage } from 'soapbox/types/entities';
import { flattenPages } from 'soapbox/utils/queries'; import { flattenPages } from 'soapbox/utils/queries';
import { IAccount } from '../accounts';
import { ChatKeys, IChat, isLastMessage, useChat, useChatActions, useChatMessages, useChats } from '../chats'; import { ChatKeys, IChat, isLastMessage, useChat, useChatActions, useChatMessages, useChats } from '../chats';
const chat: IChat = { const chat: IChat = {
accepted: true, accepted: true,
account: { account: buildAccount({
username: 'username', username: 'username',
verified: true, verified: true,
id: '1', id: '1',
@ -23,7 +22,7 @@ const chat: IChat = {
avatar: 'avatar', avatar: 'avatar',
avatar_static: 'avatar', avatar_static: 'avatar',
display_name: 'my name', display_name: 'my name',
} as IAccount, }),
chat_type: 'direct', chat_type: 'direct',
created_at: '2020-06-10T02:05:06.000Z', created_at: '2020-06-10T02:05:06.000Z',
created_by_account: '1', created_by_account: '1',

View File

@ -41,8 +41,8 @@ const useUpdateCredentials = () => {
return useMutation((data: UpdateCredentialsData) => api.patch('/api/v1/accounts/update_credentials', data), { return useMutation((data: UpdateCredentialsData) => api.patch('/api/v1/accounts/update_credentials', data), {
onMutate(variables) { onMutate(variables) {
const cachedAccount = account?.toJS(); const cachedAccount = account;
dispatch(patchMeSuccess({ ...cachedAccount, ...variables })); dispatch(patchMeSuccess({ ...account, ...variables }));
return { cachedAccount }; return { cachedAccount };
}, },

View File

@ -15,7 +15,7 @@ import { flattenPages, PaginatedResult, updatePageItem } from 'soapbox/utils/que
import { queryClient } from './client'; import { queryClient } from './client';
import { useFetchRelationships } from './relationships'; import { useFetchRelationships } from './relationships';
import type { IAccount } from './accounts'; import type { Account } from 'soapbox/schemas';
export const messageExpirationOptions = [604800, 1209600, 2592000, 7776000]; export const messageExpirationOptions = [604800, 1209600, 2592000, 7776000];
@ -28,7 +28,7 @@ export enum MessageExpirationValues {
export interface IChat { export interface IChat {
accepted: boolean accepted: boolean
account: IAccount account: Account
chat_type: 'channel' | 'direct' chat_type: 'channel' | 'direct'
created_at: string created_at: string
created_by_account: string created_by_account: string

View File

@ -272,8 +272,8 @@ const deleteToken = (state: State, token: string) => {
}); });
}; };
const deleteUser = (state: State, account: AccountEntity) => { const deleteUser = (state: State, account: Pick<AccountEntity, 'url'>) => {
const accountUrl = account.get('url'); const accountUrl = account.url;
return state.withMutations(state => { return state.withMutations(state => {
state.update('users', users => users.delete(accountUrl)); state.update('users', users => users.delete(accountUrl));

View File

@ -20,7 +20,6 @@ type ChatRecord = ReturnType<typeof normalizeChat>;
type APIEntities = Array<APIEntity>; type APIEntities = Array<APIEntity>;
export interface ReducerChat extends ChatRecord { export interface ReducerChat extends ChatRecord {
account: string | null
last_message: string | null last_message: string | null
} }
@ -34,7 +33,6 @@ type State = ReturnType<typeof ReducerRecord>;
const minifyChat = (chat: ChatRecord): ReducerChat => { const minifyChat = (chat: ChatRecord): ReducerChat => {
return chat.mergeWith((o, n) => n || o, { return chat.mergeWith((o, n) => n || o, {
account: normalizeId(chat.getIn(['account', 'id'])),
last_message: normalizeId(chat.getIn(['last_message', 'id'])), last_message: normalizeId(chat.getIn(['last_message', 'id'])),
}) as ReducerChat; }) as ReducerChat;
}; };

View File

@ -126,9 +126,9 @@ export const statusToMentionsArray = (status: ImmutableMap<string, any>, account
const author = status.getIn(['account', 'acct']) as string; const author = status.getIn(['account', 'acct']) as string;
const mentions = status.get('mentions')?.map((m: ImmutableMap<string, any>) => m.get('acct')) || []; const mentions = status.get('mentions')?.map((m: ImmutableMap<string, any>) => m.get('acct')) || [];
return ImmutableOrderedSet([author]) return ImmutableOrderedSet<string>([author])
.concat(mentions) .concat(mentions)
.delete(account.get('acct')) as ImmutableOrderedSet<string>; .delete(account.acct) as ImmutableOrderedSet<string>;
}; };
export const statusToMentionsAccountIdsArray = (status: StatusEntity, account: AccountEntity) => { export const statusToMentionsAccountIdsArray = (status: StatusEntity, account: AccountEntity) => {

View File

@ -17,8 +17,8 @@ import {
} from '../actions/statuses'; } from '../actions/statuses';
import { TIMELINE_DELETE } from '../actions/timelines'; import { TIMELINE_DELETE } from '../actions/timelines';
import type { ReducerStatus } from './statuses';
import type { AnyAction } from 'redux'; import type { AnyAction } from 'redux';
import type { Status } from 'soapbox/schemas';
export const ReducerRecord = ImmutableRecord({ export const ReducerRecord = ImmutableRecord({
inReplyTos: ImmutableMap<string, string>(), inReplyTos: ImmutableMap<string, string>(),
@ -163,10 +163,10 @@ const filterContexts = (
state: State, state: State,
relationship: { id: string }, relationship: { id: string },
/** The entire statuses map from the store. */ /** The entire statuses map from the store. */
statuses: ImmutableMap<string, ReducerStatus>, statuses: ImmutableMap<string, Status>,
): State => { ): State => {
const ownedStatusIds = statuses const ownedStatusIds = statuses
.filter(status => status.account === relationship.id) .filter(status => status.account.id === relationship.id)
.map(status => status.id) .map(status => status.id)
.toList() .toList()
.toArray(); .toArray();

View File

@ -1,4 +1,4 @@
import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import get from 'lodash/get'; import get from 'lodash/get';
import { STREAMING_FOLLOW_RELATIONSHIPS_UPDATE } from 'soapbox/actions/streaming'; import { STREAMING_FOLLOW_RELATIONSHIPS_UPDATE } from 'soapbox/actions/streaming';
@ -50,7 +50,7 @@ const normalizeRelationships = (state: State, relationships: APIEntities) => {
return state; return state;
}; };
const setDomainBlocking = (state: State, accounts: ImmutableList<string>, blocking: boolean) => { const setDomainBlocking = (state: State, accounts: string[], blocking: boolean) => {
return state.withMutations(map => { return state.withMutations(map => {
accounts.forEach(id => { accounts.forEach(id => {
map.setIn([id, 'domain_blocking'], blocking); map.setIn([id, 'domain_blocking'], blocking);

View File

@ -56,7 +56,6 @@ type APIEntities = Array<APIEntity>;
type State = ImmutableMap<string, ReducerStatus>; type State = ImmutableMap<string, ReducerStatus>;
export interface ReducerStatus extends StatusRecord { export interface ReducerStatus extends StatusRecord {
account: string | null
reblog: string | null reblog: string | null
poll: string | null poll: string | null
quote: string | null quote: string | null
@ -65,7 +64,6 @@ export interface ReducerStatus extends StatusRecord {
const minifyStatus = (status: StatusRecord): ReducerStatus => { const minifyStatus = (status: StatusRecord): ReducerStatus => {
return status.mergeWith((o, n) => n || o, { return status.mergeWith((o, n) => n || o, {
account: normalizeId(status.getIn(['account', 'id'])),
reblog: normalizeId(status.getIn(['reblog', 'id'])), reblog: normalizeId(status.getIn(['reblog', 'id'])),
poll: normalizeId(status.getIn(['poll', 'id'])), poll: normalizeId(status.getIn(['poll', 'id'])),
quote: normalizeId(status.getIn(['quote', 'id'])), quote: normalizeId(status.getIn(['quote', 'id'])),

View File

@ -68,7 +68,7 @@ const dismissAccount = (state: State, accountId: string) => {
return state.update('items', items => items.filterNot(item => item.account === accountId)); return state.update('items', items => items.filterNot(item => item.account === accountId));
}; };
const dismissAccounts = (state: State, accountIds: Array<string>) => { const dismissAccounts = (state: State, accountIds: string[]) => {
return state.update('items', items => items.filterNot(item => accountIds.includes(item.account))); return state.update('items', items => items.filterNot(item => accountIds.includes(item.account)));
}; };

View File

@ -215,7 +215,7 @@ const filterTimelines = (state: State, relationship: APIEntity, statuses: Immuta
statuses.forEach(status => { statuses.forEach(status => {
if (status.get('account') !== relationship.id) return; if (status.get('account') !== relationship.id) return;
const references = buildReferencesTo(statuses, status); const references = buildReferencesTo(statuses, status);
deleteStatus(state, status.get('id'), status.get('account') as string, references, relationship.id); deleteStatus(state, status.id, status.account!.id, references, relationship.id);
}); });
}); });
}; };

View File

@ -5,6 +5,7 @@ import emojify from 'soapbox/features/emoji';
import { unescapeHTML } from 'soapbox/utils/html'; import { unescapeHTML } from 'soapbox/utils/html';
import { customEmojiSchema } from './custom-emoji'; import { customEmojiSchema } from './custom-emoji';
import { relationshipSchema } from './relationship';
import { contentSchema, filteredArray, makeCustomEmojiMap } from './utils'; import { contentSchema, filteredArray, makeCustomEmojiMap } from './utils';
import type { Resolve } from 'soapbox/utils/types'; import type { Resolve } from 'soapbox/utils/types';
@ -54,6 +55,8 @@ const baseAccountSchema = z.object({
pleroma: z.object({ pleroma: z.object({
accepts_chat_messages: z.boolean().catch(false), accepts_chat_messages: z.boolean().catch(false),
accepts_email_list: z.boolean().catch(false), accepts_email_list: z.boolean().catch(false),
also_known_as: z.array(z.string().url()).catch([]),
ap_id: z.string().url().optional().catch(undefined),
birthday: birthdaySchema.nullish().catch(undefined), birthday: birthdaySchema.nullish().catch(undefined),
deactivated: z.boolean().catch(false), deactivated: z.boolean().catch(false),
favicon: z.string().url().optional().catch(undefined), favicon: z.string().url().optional().catch(undefined),
@ -69,6 +72,7 @@ const baseAccountSchema = z.object({
notification_settings: z.object({ notification_settings: z.object({
block_from_strangers: z.boolean().catch(false), block_from_strangers: z.boolean().catch(false),
}).optional().catch(undefined), }).optional().catch(undefined),
relationship: relationshipSchema.optional().catch(undefined),
tags: z.array(z.string()).catch([]), tags: z.array(z.string()).catch([]),
}).optional().catch(undefined), }).optional().catch(undefined),
source: z.object({ source: z.object({
@ -133,7 +137,7 @@ const transformAccount = <T extends TransformableAccount>({ pleroma, other_setti
location: account.location || pleroma?.location || other_settings?.location || '', location: account.location || pleroma?.location || other_settings?.location || '',
note_emojified: emojify(account.note, customEmojiMap), note_emojified: emojify(account.note, customEmojiMap),
pleroma, pleroma,
relationship: undefined, relationship: relationshipSchema.parse({ id: account.id, ...pleroma?.relationship }),
staff: pleroma?.is_admin || pleroma?.is_moderator || false, staff: pleroma?.is_admin || pleroma?.is_moderator || false,
suspended: account.suspended || pleroma?.deactivated || false, suspended: account.suspended || pleroma?.deactivated || false,
verified: account.verified || pleroma?.tags.includes('verified') || false, verified: account.verified || pleroma?.tags.includes('verified') || false,

View File

@ -394,7 +394,7 @@ export const makeGetStatusIds = () => createSelector([
(state: RootState, { type, prefix }: ColumnQuery) => getSettings(state).get(prefix || type, ImmutableMap()), (state: RootState, { type, prefix }: ColumnQuery) => getSettings(state).get(prefix || type, ImmutableMap()),
(state: RootState, { type }: ColumnQuery) => state.timelines.get(type)?.items || ImmutableOrderedSet(), (state: RootState, { type }: ColumnQuery) => state.timelines.get(type)?.items || ImmutableOrderedSet(),
(state: RootState) => state.statuses, (state: RootState) => state.statuses,
], (columnSettings, statusIds: ImmutableOrderedSet<string>, statuses) => { ], (columnSettings: any, statusIds: ImmutableOrderedSet<string>, statuses) => {
return statusIds.filter((id: string) => { return statusIds.filter((id: string) => {
const status = statuses.get(id); const status = statuses.get(id);
if (!status) return true; if (!status) return true;

View File

@ -1,7 +1,6 @@
import { import {
AdminAccountRecord, AdminAccountRecord,
AdminReportRecord, AdminReportRecord,
AccountRecord,
AnnouncementRecord, AnnouncementRecord,
AnnouncementReactionRecord, AnnouncementReactionRecord,
AttachmentRecord, AttachmentRecord,
@ -23,8 +22,10 @@ import {
TagRecord, TagRecord,
} from 'soapbox/normalizers'; } from 'soapbox/normalizers';
import { LogEntryRecord } from 'soapbox/reducers/admin-log'; import { LogEntryRecord } from 'soapbox/reducers/admin-log';
import { Account as SchemaAccount } from 'soapbox/schemas';
import type { Record as ImmutableRecord } from 'immutable'; import type { Record as ImmutableRecord } from 'immutable';
import type { LegacyMap } from 'soapbox/utils/legacy';
type AdminAccount = ReturnType<typeof AdminAccountRecord>; type AdminAccount = ReturnType<typeof AdminAccountRecord>;
type AdminLog = ReturnType<typeof LogEntryRecord>; type AdminLog = ReturnType<typeof LogEntryRecord>;
@ -48,11 +49,7 @@ type Notification = ReturnType<typeof NotificationRecord>;
type StatusEdit = ReturnType<typeof StatusEditRecord>; type StatusEdit = ReturnType<typeof StatusEditRecord>;
type Tag = ReturnType<typeof TagRecord>; type Tag = ReturnType<typeof TagRecord>;
interface Account extends ReturnType<typeof AccountRecord> { type Account = SchemaAccount & LegacyMap;
// HACK: we can't do a circular reference in the Record definition itself,
// so do it here.
moved: EmbeddedEntity<Account>
}
interface Status extends ReturnType<typeof StatusRecord> { interface Status extends ReturnType<typeof StatusRecord> {
// HACK: same as above // HACK: same as above
@ -65,10 +62,10 @@ type APIEntity = Record<string, any>;
type EmbeddedEntity<T extends object> = null | string | ReturnType<ImmutableRecord.Factory<T>>; type EmbeddedEntity<T extends object> = null | string | ReturnType<ImmutableRecord.Factory<T>>;
export { export {
Account,
AdminAccount, AdminAccount,
AdminLog, AdminLog,
AdminReport, AdminReport,
Account,
Announcement, Announcement,
AnnouncementReaction, AnnouncementReaction,
Attachment, Attachment,

View File

@ -1,4 +1,4 @@
import { normalizeAccount } from 'soapbox/normalizers'; import { buildAccount } from 'soapbox/jest/factory';
import { import {
tagToBadge, tagToBadge,
@ -8,8 +8,6 @@ import {
getBadges, getBadges,
} from '../badges'; } from '../badges';
import type { Account } from 'soapbox/types/entities';
test('tagToBadge', () => { test('tagToBadge', () => {
expect(tagToBadge('yolo')).toEqual('badge:yolo'); expect(tagToBadge('yolo')).toEqual('badge:yolo');
}); });
@ -38,6 +36,6 @@ test('getTagDiff', () => {
}); });
test('getBadges', () => { test('getBadges', () => {
const account = normalizeAccount({ id: '1', pleroma: { tags: ['a', 'b', 'badge:c'] } }) as Account; const account = buildAccount({ id: '1', pleroma: { tags: ['a', 'b', 'badge:c'] } });
expect(getBadges(account)).toEqual(['badge:c']); expect(getBadges(account)).toEqual(['badge:c']);
}); });

View File

@ -1,5 +1,5 @@
import { buildAccount } from 'soapbox/jest/factory';
import { normalizeChatMessage } from 'soapbox/normalizers'; import { normalizeChatMessage } from 'soapbox/normalizers';
import { IAccount } from 'soapbox/queries/accounts';
import { ChatKeys, IChat } from 'soapbox/queries/chats'; import { ChatKeys, IChat } from 'soapbox/queries/chats';
import { queryClient } from 'soapbox/queries/client'; import { queryClient } from 'soapbox/queries/client';
@ -7,7 +7,7 @@ import { updateChatMessage } from '../chats';
const chat: IChat = { const chat: IChat = {
accepted: true, accepted: true,
account: { account: buildAccount({
username: 'username', username: 'username',
verified: true, verified: true,
id: '1', id: '1',
@ -15,7 +15,7 @@ const chat: IChat = {
avatar: 'avatar', avatar: 'avatar',
avatar_static: 'avatar', avatar_static: 'avatar',
display_name: 'my name', display_name: 'my name',
} as IAccount, }),
chat_type: 'direct', chat_type: 'direct',
created_at: '2020-06-10T02:05:06.000Z', created_at: '2020-06-10T02:05:06.000Z',
created_by_account: '1', created_by_account: '1',

View File

@ -1,16 +1,14 @@
import { normalizeStatus } from 'soapbox/normalizers/status'; import { buildStatus } from 'soapbox/jest/factory';
import { import {
hasIntegerMediaIds, hasIntegerMediaIds,
defaultMediaVisibility, defaultMediaVisibility,
} from '../status'; } from '../status';
import type { ReducerStatus } from 'soapbox/reducers/statuses';
describe('hasIntegerMediaIds()', () => { describe('hasIntegerMediaIds()', () => {
it('returns true for a Pleroma deleted status', () => { it('returns true for a Pleroma deleted status', () => {
const status = normalizeStatus(require('soapbox/__fixtures__/pleroma-status-deleted.json')) as ReducerStatus; const status = buildStatus(require('soapbox/__fixtures__/pleroma-status-deleted.json'));
expect(hasIntegerMediaIds(status)).toBe(true); expect(hasIntegerMediaIds(status)).toBe(true);
}); });
}); });
@ -21,17 +19,17 @@ describe('defaultMediaVisibility()', () => {
}); });
it('hides sensitive media by default', () => { it('hides sensitive media by default', () => {
const status = normalizeStatus({ sensitive: true }) as ReducerStatus; const status = buildStatus({ sensitive: true });
expect(defaultMediaVisibility(status, 'default')).toBe(false); expect(defaultMediaVisibility(status, 'default')).toBe(false);
}); });
it('hides media when displayMedia is hide_all', () => { it('hides media when displayMedia is hide_all', () => {
const status = normalizeStatus({}) as ReducerStatus; const status = buildStatus({});
expect(defaultMediaVisibility(status, 'hide_all')).toBe(false); expect(defaultMediaVisibility(status, 'hide_all')).toBe(false);
}); });
it('shows sensitive media when displayMedia is show_all', () => { it('shows sensitive media when displayMedia is show_all', () => {
const status = normalizeStatus({ sensitive: true }) as ReducerStatus; const status = buildStatus({ sensitive: true });
expect(defaultMediaVisibility(status, 'show_all')).toBe(true); expect(defaultMediaVisibility(status, 'show_all')).toBe(true);
}); });
}); });

View File

@ -1,75 +1,73 @@
import { fromJS } from 'immutable'; import { fromJS } from 'immutable';
import { normalizeStatus } from 'soapbox/normalizers/status'; import { buildStatus } from 'soapbox/jest/factory';
import { shouldFilter } from '../timelines'; import { shouldFilter } from '../timelines';
import type { ReducerStatus } from 'soapbox/reducers/statuses';
describe('shouldFilter', () => { describe('shouldFilter', () => {
it('returns false under normal circumstances', () => { it('returns false under normal circumstances', () => {
const columnSettings = fromJS({}); const columnSettings = fromJS({});
const status = normalizeStatus({}) as ReducerStatus; const status = buildStatus({});
expect(shouldFilter(status, columnSettings)).toBe(false); expect(shouldFilter(status, columnSettings)).toBe(false);
}); });
it('reblog: returns true when `shows.reblog == false`', () => { it('reblog: returns true when `shows.reblog == false`', () => {
const columnSettings = fromJS({ shows: { reblog: false } }); const columnSettings = fromJS({ shows: { reblog: false } });
const status = normalizeStatus({ reblog: {} }) as ReducerStatus; const status = buildStatus({ reblog: {} });
expect(shouldFilter(status, columnSettings)).toBe(true); expect(shouldFilter(status, columnSettings)).toBe(true);
}); });
it('reblog: returns false when `shows.reblog == true`', () => { it('reblog: returns false when `shows.reblog == true`', () => {
const columnSettings = fromJS({ shows: { reblog: true } }); const columnSettings = fromJS({ shows: { reblog: true } });
const status = normalizeStatus({ reblog: {} }) as ReducerStatus; const status = buildStatus({ reblog: {} });
expect(shouldFilter(status, columnSettings)).toBe(false); expect(shouldFilter(status, columnSettings)).toBe(false);
}); });
it('reply: returns true when `shows.reply == false`', () => { it('reply: returns true when `shows.reply == false`', () => {
const columnSettings = fromJS({ shows: { reply: false } }); const columnSettings = fromJS({ shows: { reply: false } });
const status = normalizeStatus({ in_reply_to_id: '1234' }) as ReducerStatus; const status = buildStatus({ in_reply_to_id: '1234' });
expect(shouldFilter(status, columnSettings)).toBe(true); expect(shouldFilter(status, columnSettings)).toBe(true);
}); });
it('reply: returns false when `shows.reply == true`', () => { it('reply: returns false when `shows.reply == true`', () => {
const columnSettings = fromJS({ shows: { reply: true } }); const columnSettings = fromJS({ shows: { reply: true } });
const status = normalizeStatus({ in_reply_to_id: '1234' }) as ReducerStatus; const status = buildStatus({ in_reply_to_id: '1234' });
expect(shouldFilter(status, columnSettings)).toBe(false); expect(shouldFilter(status, columnSettings)).toBe(false);
}); });
it('direct: returns true when `shows.direct == false`', () => { it('direct: returns true when `shows.direct == false`', () => {
const columnSettings = fromJS({ shows: { direct: false } }); const columnSettings = fromJS({ shows: { direct: false } });
const status = normalizeStatus({ visibility: 'direct' }) as ReducerStatus; const status = buildStatus({ visibility: 'direct' });
expect(shouldFilter(status, columnSettings)).toBe(true); expect(shouldFilter(status, columnSettings)).toBe(true);
}); });
it('direct: returns false when `shows.direct == true`', () => { it('direct: returns false when `shows.direct == true`', () => {
const columnSettings = fromJS({ shows: { direct: true } }); const columnSettings = fromJS({ shows: { direct: true } });
const status = normalizeStatus({ visibility: 'direct' }) as ReducerStatus; const status = buildStatus({ visibility: 'direct' });
expect(shouldFilter(status, columnSettings)).toBe(false); expect(shouldFilter(status, columnSettings)).toBe(false);
}); });
it('direct: returns false for a public post when `shows.direct == false`', () => { it('direct: returns false for a public post when `shows.direct == false`', () => {
const columnSettings = fromJS({ shows: { direct: false } }); const columnSettings = fromJS({ shows: { direct: false } });
const status = normalizeStatus({ visibility: 'public' }) as ReducerStatus; const status = buildStatus({ visibility: 'public' });
expect(shouldFilter(status, columnSettings)).toBe(false); expect(shouldFilter(status, columnSettings)).toBe(false);
}); });
it('multiple settings', () => { it('multiple settings', () => {
const columnSettings = fromJS({ shows: { reblog: false, reply: false, direct: false } }); const columnSettings = fromJS({ shows: { reblog: false, reply: false, direct: false } });
const status = normalizeStatus({ reblog: null, in_reply_to_id: null, visibility: 'direct' }) as ReducerStatus; const status = buildStatus({ reblog: null, in_reply_to_id: null, visibility: 'direct' });
expect(shouldFilter(status, columnSettings)).toBe(true); expect(shouldFilter(status, columnSettings)).toBe(true);
}); });
it('multiple settings', () => { it('multiple settings', () => {
const columnSettings = fromJS({ shows: { reblog: false, reply: true, direct: false } }); const columnSettings = fromJS({ shows: { reblog: false, reply: true, direct: false } });
const status = normalizeStatus({ reblog: null, in_reply_to_id: '1234', visibility: 'public' }) as ReducerStatus; const status = buildStatus({ reblog: null, in_reply_to_id: '1234', visibility: 'public' });
expect(shouldFilter(status, columnSettings)).toBe(false); expect(shouldFilter(status, columnSettings)).toBe(false);
}); });
it('multiple settings', () => { it('multiple settings', () => {
const columnSettings = fromJS({ shows: { reblog: true, reply: false, direct: true } }); const columnSettings = fromJS({ shows: { reblog: true, reply: false, direct: true } });
const status = normalizeStatus({ reblog: {}, in_reply_to_id: '1234', visibility: 'direct' }) as ReducerStatus; const status = buildStatus({ reblog: {}, in_reply_to_id: '1234', visibility: 'direct' });
expect(shouldFilter(status, columnSettings)).toBe(true); expect(shouldFilter(status, columnSettings)).toBe(true);
}); });
}); });

View File

@ -33,8 +33,8 @@ const filterBadges = (tags: string[]): string[] => {
}; };
/** Get badges from an account. */ /** Get badges from an account. */
const getBadges = (account: Account) => { const getBadges = (account: Pick<Account, 'pleroma'>) => {
const tags = Array.from(account?.getIn(['pleroma', 'tags']) as Iterable<string> || []); const tags = account?.pleroma?.tags ?? [];
return filterBadges(tags); return filterBadges(tags);
}; };

View File

@ -1,18 +1,15 @@
import { isIntegerId } from 'soapbox/utils/numbers'; import { isIntegerId } from 'soapbox/utils/numbers';
import type { IntlShape } from 'react-intl'; import type { IntlShape } from 'react-intl';
import type { Status } from 'soapbox/types/entities'; import type { Status } from 'soapbox/schemas';
/** Get the initial visibility of media attachments from user settings. */ /** Get the initial visibility of media attachments from user settings. */
export const defaultMediaVisibility = ( export const defaultMediaVisibility = <T extends { reblog: T | string | null } & Pick<Status, 'visibility' | 'sensitive'>>(
status: Pick<Status, 'reblog' | 'visibility' | 'sensitive'> | undefined | null, status: T | undefined | null,
displayMedia: string, displayMedia: string,
): boolean => { ): boolean => {
if (!status) return false; if (!status) return false;
status = getActualStatus(status);
if (status.reblog && typeof status.reblog === 'object') {
status = status.reblog;
}
const isUnderReview = status.visibility === 'self'; const isUnderReview = status.visibility === 'self';
@ -73,14 +70,9 @@ export const textForScreenReader = (
}; };
/** Get reblogged status if any, otherwise return the original status. */ /** Get reblogged status if any, otherwise return the original status. */
// @ts-ignore The type seems right, but TS doesn't like it. export const getActualStatus = <T extends { reblog: T | string | null }>(status: T): T => {
export const getActualStatus: {
<T extends Pick<Status, 'reblog'>>(status: T): T
(status: undefined): undefined
(status: null): null
} = <T extends Pick<Status, 'reblog'>>(status: T | null | undefined) => {
if (status?.reblog && typeof status?.reblog === 'object') { if (status?.reblog && typeof status?.reblog === 'object') {
return status.reblog as Status; return status.reblog;
} else { } else {
return status; return status;
} }

View File

@ -1,8 +1,11 @@
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap, type Collection } from 'immutable';
import type { Status as StatusEntity } from 'soapbox/types/entities'; import type { Status } from 'soapbox/schemas';
export const shouldFilter = (status: StatusEntity, columnSettings: any) => { export const shouldFilter = (
status: Pick<Status, 'in_reply_to_id' | 'visibility'> & { reblog: unknown },
columnSettings: Collection<any, any>,
) => {
const shows = ImmutableMap({ const shows = ImmutableMap({
reblog: status.reblog !== null, reblog: status.reblog !== null,
reply: status.in_reply_to_id !== null, reply: status.in_reply_to_id !== null,

View File

@ -181,6 +181,7 @@
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"twemoji": "https://github.com/twitter/twemoji#v14.0.2", "twemoji": "https://github.com/twitter/twemoji#v14.0.2",
"type-fest": "^3.12.0",
"typescript": "^5.1.3", "typescript": "^5.1.3",
"util": "^0.12.4", "util": "^0.12.4",
"uuid": "^9.0.0", "uuid": "^9.0.0",

View File

@ -17111,6 +17111,11 @@ type-fest@^0.8.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
type-fest@^3.12.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.12.0.tgz#4ce26edc1ccc59fc171e495887ef391fe1f5280e"
integrity sha512-qj9wWsnFvVEMUDbESiilKeXeHL7FwwiFcogfhfyjmvT968RXSvnl23f1JOClTHYItsi7o501C/7qVllscUP3oA==
type-is@~1.6.18: type-is@~1.6.18:
version "1.6.18" version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"