Merge branch 'zod-poll' into 'develop'
zod: Poll See merge request soapbox-pub/soapbox!2495
This commit is contained in:
commit
da69cf140b
|
@ -1,10 +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 { 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, normalizeRelationship } from '../../normalizers';
|
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';
|
import type { Account } from 'soapbox/types/entities';
|
||||||
|
@ -66,7 +67,7 @@ describe('initAccountNoteModal()', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const state = rootState
|
const state = rootState
|
||||||
.set('relationships', ImmutableMap({ '1': normalizeRelationship({ note: 'hello' }) }));
|
.set('relationships', ImmutableMap({ '1': buildRelationship({ note: 'hello' }) }));
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,10 +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 { buildRelationship } from 'soapbox/jest/factory';
|
||||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||||
import { ListRecord, ReducerRecord } from 'soapbox/reducers/user-lists';
|
import { ListRecord, ReducerRecord } from 'soapbox/reducers/user-lists';
|
||||||
|
|
||||||
import { normalizeAccount, normalizeInstance, normalizeRelationship } from '../../normalizers';
|
import { normalizeAccount, normalizeInstance } from '../../normalizers';
|
||||||
import {
|
import {
|
||||||
authorizeFollowRequest,
|
authorizeFollowRequest,
|
||||||
blockAccount,
|
blockAccount,
|
||||||
|
@ -1340,7 +1341,7 @@ describe('fetchRelationships()', () => {
|
||||||
describe('without newAccountIds', () => {
|
describe('without newAccountIds', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const state = rootState
|
const state = rootState
|
||||||
.set('relationships', ImmutableMap({ [id]: normalizeRelationship({}) }))
|
.set('relationships', ImmutableMap({ [id]: buildRelationship() }))
|
||||||
.set('me', '123');
|
.set('me', '123');
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
});
|
});
|
||||||
|
|
|
@ -74,7 +74,7 @@ const importFetchedGroup = (group: APIEntity) =>
|
||||||
importFetchedGroups([group]);
|
importFetchedGroups([group]);
|
||||||
|
|
||||||
const importFetchedGroups = (groups: APIEntity[]) => {
|
const importFetchedGroups = (groups: APIEntity[]) => {
|
||||||
const entities = filteredArray(groupSchema).catch([]).parse(groups);
|
const entities = filteredArray(groupSchema).parse(groups);
|
||||||
return importGroups(entities);
|
return importGroups(entities);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,14 +4,22 @@ import { IntlProvider } from 'react-intl';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import { __stub } from 'soapbox/api';
|
import { __stub } from 'soapbox/api';
|
||||||
import { normalizePoll } from 'soapbox/normalizers/poll';
|
import { mockStore, render, screen, rootState } from 'soapbox/jest/test-helpers';
|
||||||
|
import { type Poll } from 'soapbox/schemas';
|
||||||
|
|
||||||
import { mockStore, render, screen, rootState } from '../../../jest/test-helpers';
|
|
||||||
import PollFooter from '../poll-footer';
|
import PollFooter from '../poll-footer';
|
||||||
|
|
||||||
let poll = normalizePoll({
|
let poll: Poll = {
|
||||||
id: 1,
|
id: '1',
|
||||||
options: [{ title: 'Apples', votes_count: 0 }],
|
options: [{
|
||||||
|
title: 'Apples',
|
||||||
|
votes_count: 0,
|
||||||
|
title_emojified: 'Apples',
|
||||||
|
}, {
|
||||||
|
title: 'Oranges',
|
||||||
|
votes_count: 0,
|
||||||
|
title_emojified: 'Oranges',
|
||||||
|
}],
|
||||||
emojis: [],
|
emojis: [],
|
||||||
expired: false,
|
expired: false,
|
||||||
expires_at: '2020-03-24T19:33:06.000Z',
|
expires_at: '2020-03-24T19:33:06.000Z',
|
||||||
|
@ -20,7 +28,7 @@ let poll = normalizePoll({
|
||||||
votes_count: 0,
|
votes_count: 0,
|
||||||
own_votes: null,
|
own_votes: null,
|
||||||
voted: false,
|
voted: false,
|
||||||
});
|
};
|
||||||
|
|
||||||
describe('<PollFooter />', () => {
|
describe('<PollFooter />', () => {
|
||||||
describe('with "showResults" enabled', () => {
|
describe('with "showResults" enabled', () => {
|
||||||
|
@ -62,10 +70,10 @@ describe('<PollFooter />', () => {
|
||||||
|
|
||||||
describe('when the Poll has not expired', () => {
|
describe('when the Poll has not expired', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
poll = normalizePoll({
|
poll = {
|
||||||
...poll.toJS(),
|
...poll,
|
||||||
expired: false,
|
expired: false,
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders time remaining', () => {
|
it('renders time remaining', () => {
|
||||||
|
@ -77,10 +85,10 @@ describe('<PollFooter />', () => {
|
||||||
|
|
||||||
describe('when the Poll has expired', () => {
|
describe('when the Poll has expired', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
poll = normalizePoll({
|
poll = {
|
||||||
...poll.toJS(),
|
...poll,
|
||||||
expired: true,
|
expired: true,
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders closed', () => {
|
it('renders closed', () => {
|
||||||
|
@ -100,10 +108,10 @@ describe('<PollFooter />', () => {
|
||||||
|
|
||||||
describe('when the Poll is multiple', () => {
|
describe('when the Poll is multiple', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
poll = normalizePoll({
|
poll = {
|
||||||
...poll.toJS(),
|
...poll,
|
||||||
multiple: true,
|
multiple: true,
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the Vote button', () => {
|
it('renders the Vote button', () => {
|
||||||
|
@ -115,10 +123,10 @@ describe('<PollFooter />', () => {
|
||||||
|
|
||||||
describe('when the Poll is not multiple', () => {
|
describe('when the Poll is not multiple', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
poll = normalizePoll({
|
poll = {
|
||||||
...poll.toJS(),
|
...poll,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render the Vote button', () => {
|
it('does not render the Vote button', () => {
|
||||||
|
|
|
@ -40,21 +40,21 @@ const PollFooter: React.FC<IPollFooter> = ({ poll, showResults, selected }): JSX
|
||||||
let votesCount = null;
|
let votesCount = null;
|
||||||
|
|
||||||
if (poll.voters_count !== null && poll.voters_count !== undefined) {
|
if (poll.voters_count !== null && poll.voters_count !== undefined) {
|
||||||
votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get('voters_count') }} />;
|
votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.voters_count }} />;
|
||||||
} else {
|
} else {
|
||||||
votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />;
|
votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.votes_count }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack space={4} data-testid='poll-footer'>
|
<Stack space={4} data-testid='poll-footer'>
|
||||||
{(!showResults && poll?.multiple) && (
|
{(!showResults && poll.multiple) && (
|
||||||
<Button onClick={handleVote} theme='primary' block>
|
<Button onClick={handleVote} theme='primary' block>
|
||||||
<FormattedMessage id='poll.vote' defaultMessage='Vote' />
|
<FormattedMessage id='poll.vote' defaultMessage='Vote' />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<HStack space={1.5} alignItems='center' wrap>
|
<HStack space={1.5} alignItems='center' wrap>
|
||||||
{poll.pleroma.get('non_anonymous') && (
|
{poll.pleroma?.non_anonymous && (
|
||||||
<>
|
<>
|
||||||
<Tooltip text={intl.formatMessage(messages.nonAnonymous)}>
|
<Tooltip text={intl.formatMessage(messages.nonAnonymous)}>
|
||||||
<Text theme='muted' weight='medium'>
|
<Text theme='muted' weight='medium'>
|
||||||
|
|
|
@ -112,10 +112,13 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
||||||
|
|
||||||
const pollVotesCount = poll.voters_count || poll.votes_count;
|
const pollVotesCount = poll.voters_count || poll.votes_count;
|
||||||
const percent = pollVotesCount === 0 ? 0 : (option.votes_count / pollVotesCount) * 100;
|
const percent = pollVotesCount === 0 ? 0 : (option.votes_count / pollVotesCount) * 100;
|
||||||
const leading = poll.options.filterNot(other => other.title === option.title).every(other => option.votes_count >= other.votes_count);
|
|
||||||
const voted = poll.own_votes?.includes(index);
|
const voted = poll.own_votes?.includes(index);
|
||||||
const message = intl.formatMessage(messages.votes, { votes: option.votes_count });
|
const message = intl.formatMessage(messages.votes, { votes: option.votes_count });
|
||||||
|
|
||||||
|
const leading = poll.options
|
||||||
|
.filter(other => other.title !== option.title)
|
||||||
|
.every(other => option.votes_count >= other.votes_count);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={option.title}>
|
<div key={option.title}>
|
||||||
{showResults ? (
|
{showResults ? (
|
||||||
|
|
|
@ -6,7 +6,6 @@ import type { Card } from 'soapbox/types/entities';
|
||||||
/** Map of available provider modules. */
|
/** Map of available provider modules. */
|
||||||
const PROVIDERS: Record<string, () => Promise<AdProvider>> = {
|
const PROVIDERS: Record<string, () => Promise<AdProvider>> = {
|
||||||
soapbox: async() => (await import(/* webpackChunkName: "features/ads/soapbox" */'./soapbox-config')).default,
|
soapbox: async() => (await import(/* webpackChunkName: "features/ads/soapbox" */'./soapbox-config')).default,
|
||||||
rumble: async() => (await import(/* webpackChunkName: "features/ads/rumble" */'./rumble')).default,
|
|
||||||
truth: async() => (await import(/* webpackChunkName: "features/ads/truth" */'./truth')).default,
|
truth: async() => (await import(/* webpackChunkName: "features/ads/truth" */'./truth')).default,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
import { getSettings } from 'soapbox/actions/settings';
|
|
||||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
|
||||||
import { normalizeAd, normalizeCard } from 'soapbox/normalizers';
|
|
||||||
|
|
||||||
import type { AdProvider } from '.';
|
|
||||||
|
|
||||||
/** Rumble ad API entity. */
|
|
||||||
interface RumbleAd {
|
|
||||||
type: number
|
|
||||||
impression: string
|
|
||||||
click: string
|
|
||||||
asset: string
|
|
||||||
expires: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Response from Rumble ad server. */
|
|
||||||
interface RumbleApiResponse {
|
|
||||||
count: number
|
|
||||||
ads: RumbleAd[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Provides ads from Soapbox Config. */
|
|
||||||
const RumbleAdProvider: AdProvider = {
|
|
||||||
getAds: async(getState) => {
|
|
||||||
const state = getState();
|
|
||||||
const settings = getSettings(state);
|
|
||||||
const soapboxConfig = getSoapboxConfig(state);
|
|
||||||
const endpoint = soapboxConfig.extensions.getIn(['ads', 'endpoint']) as string | undefined;
|
|
||||||
|
|
||||||
if (endpoint) {
|
|
||||||
try {
|
|
||||||
const { data } = await axios.get<RumbleApiResponse>(endpoint, {
|
|
||||||
headers: {
|
|
||||||
'Accept-Language': settings.get('locale', '*') as string,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return data.ads.map(item => normalizeAd({
|
|
||||||
impression: item.impression,
|
|
||||||
card: normalizeCard({
|
|
||||||
type: item.type === 1 ? 'link' : 'rich',
|
|
||||||
image: item.asset,
|
|
||||||
url: item.click,
|
|
||||||
}),
|
|
||||||
expires_at: new Date(item.expires * 1000),
|
|
||||||
}));
|
|
||||||
} catch (e) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RumbleAdProvider;
|
|
|
@ -1,18 +1,19 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { getSettings } from 'soapbox/actions/settings';
|
import { getSettings } from 'soapbox/actions/settings';
|
||||||
import { normalizeCard } from 'soapbox/normalizers';
|
import { cardSchema } from 'soapbox/schemas/card';
|
||||||
|
import { filteredArray } from 'soapbox/schemas/utils';
|
||||||
|
|
||||||
import type { AdProvider } from '.';
|
import type { AdProvider } from '.';
|
||||||
import type { Card } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
/** TruthSocial ad API entity. */
|
/** TruthSocial ad API entity. */
|
||||||
interface TruthAd {
|
const truthAdSchema = z.object({
|
||||||
impression: string
|
impression: z.string(),
|
||||||
card: Card
|
card: cardSchema,
|
||||||
expires_at: string
|
expires_at: z.string(),
|
||||||
reason: string
|
reason: z.string().catch(''),
|
||||||
}
|
});
|
||||||
|
|
||||||
/** Provides ads from the TruthSocial API. */
|
/** Provides ads from the TruthSocial API. */
|
||||||
const TruthAdProvider: AdProvider = {
|
const TruthAdProvider: AdProvider = {
|
||||||
|
@ -21,16 +22,13 @@ const TruthAdProvider: AdProvider = {
|
||||||
const settings = getSettings(state);
|
const settings = getSettings(state);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get<TruthAd[]>('/api/v2/truth/ads?device=desktop', {
|
const { data } = await axios.get('/api/v2/truth/ads?device=desktop', {
|
||||||
headers: {
|
headers: {
|
||||||
'Accept-Language': settings.get('locale', '*') as string,
|
'Accept-Language': z.string().catch('*').parse(settings.get('locale')),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return data.map(item => ({
|
return filteredArray(truthAdSchema).parse(data);
|
||||||
...item,
|
|
||||||
card: normalizeCard(item.card),
|
|
||||||
}));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { normalizeEmojiReaction } from 'soapbox/normalizers/emoji-reaction';
|
|
||||||
|
|
||||||
import { render, screen } from '../../../../jest/test-helpers';
|
import { render, screen } from '../../../../jest/test-helpers';
|
||||||
import ChatMessageReaction from '../chat-message-reaction';
|
import ChatMessageReaction from '../chat-message-reaction';
|
||||||
|
|
||||||
const emojiReaction = normalizeEmojiReaction({
|
const emojiReaction = ({
|
||||||
name: '👍',
|
name: '👍',
|
||||||
count: 1,
|
count: 1,
|
||||||
me: false,
|
me: false,
|
||||||
|
@ -56,7 +54,7 @@ describe('<ChatMessageReaction />', () => {
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<ChatMessageReaction
|
<ChatMessageReaction
|
||||||
emojiReaction={normalizeEmojiReaction({
|
emojiReaction={({
|
||||||
name: '👍',
|
name: '👍',
|
||||||
count: 1,
|
count: 1,
|
||||||
me: true,
|
me: true,
|
||||||
|
|
|
@ -312,7 +312,7 @@ const ChatMessage = (props: IChatMessage) => {
|
||||||
</Stack>
|
</Stack>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{(chatMessage.emoji_reactions?.size) ? (
|
{(chatMessage.emoji_reactions?.length) ? (
|
||||||
<div
|
<div
|
||||||
className={clsx({
|
className={clsx({
|
||||||
'space-y-1': true,
|
'space-y-1': true,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
|
|
||||||
import { buildGroup, buildGroupRelationship } from 'soapbox/jest/factory';
|
import { buildGroup, buildGroupRelationship } from 'soapbox/jest/factory';
|
||||||
import { render, screen } from 'soapbox/jest/test-helpers';
|
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||||
|
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||||
import { Group } from 'soapbox/types/entities';
|
import { Group } from 'soapbox/types/entities';
|
||||||
|
|
||||||
import GroupActionButton from '../group-action-button';
|
import GroupActionButton from '../group-action-button';
|
||||||
|
@ -45,7 +46,7 @@ describe('<GroupActionButton />', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
group = buildGroup({
|
group = buildGroup({
|
||||||
relationship: buildGroupRelationship({
|
relationship: buildGroupRelationship({
|
||||||
member: null,
|
member: false,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -98,7 +99,7 @@ describe('<GroupActionButton />', () => {
|
||||||
relationship: buildGroupRelationship({
|
relationship: buildGroupRelationship({
|
||||||
requested: false,
|
requested: false,
|
||||||
member: true,
|
member: true,
|
||||||
role: 'owner',
|
role: GroupRoles.OWNER,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -116,7 +117,7 @@ describe('<GroupActionButton />', () => {
|
||||||
relationship: buildGroupRelationship({
|
relationship: buildGroupRelationship({
|
||||||
requested: false,
|
requested: false,
|
||||||
member: true,
|
member: true,
|
||||||
role: 'user',
|
role: GroupRoles.USER,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,7 +17,7 @@ describe('<GroupOptionsButton />', () => {
|
||||||
requested: false,
|
requested: false,
|
||||||
member: true,
|
member: true,
|
||||||
blocked_by: true,
|
blocked_by: true,
|
||||||
role: 'user',
|
role: GroupRoles.USER,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
// import { Map as ImmutableMap } from 'immutable';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { render, screen } from '../../../../jest/test-helpers';
|
import { buildRelationship } from 'soapbox/jest/factory';
|
||||||
import { normalizeAccount, normalizeRelationship } from '../../../../normalizers';
|
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';
|
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||||
|
@ -19,162 +20,10 @@ describe('<SubscribeButton />', () => {
|
||||||
|
|
||||||
describe('with "accountNotifies" disabled', () => {
|
describe('with "accountNotifies" disabled', () => {
|
||||||
it('renders nothing', () => {
|
it('renders nothing', () => {
|
||||||
const account = normalizeAccount({ ...justin, relationship: normalizeRelationship({ following: true }) }) as ReducerAccount;
|
const account = normalizeAccount({ ...justin, relationship: buildRelationship({ following: true }) }) as ReducerAccount;
|
||||||
|
|
||||||
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// describe('with "accountNotifies" enabled', () => {
|
|
||||||
// beforeEach(() => {
|
|
||||||
// store = {
|
|
||||||
// ...store,
|
|
||||||
// instance: normalizeInstance({
|
|
||||||
// version: '3.4.1 (compatible; TruthSocial 1.0.0)',
|
|
||||||
// software: 'TRUTHSOCIAL',
|
|
||||||
// pleroma: ImmutableMap({}),
|
|
||||||
// }),
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
|
|
||||||
// describe('when the relationship is requested', () => {
|
|
||||||
// beforeEach(() => {
|
|
||||||
// account = normalizeAccount({ ...account, relationship: normalizeRelationship({ requested: true }) });
|
|
||||||
|
|
||||||
// store = {
|
|
||||||
// ...store,
|
|
||||||
// accounts: ImmutableMap({
|
|
||||||
// '1': account,
|
|
||||||
// }),
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it('renders the button', () => {
|
|
||||||
// render(<SubscribeButton account={account} />, null, store);
|
|
||||||
// expect(screen.getByTestId('icon-button')).toBeInTheDocument();
|
|
||||||
// });
|
|
||||||
|
|
||||||
// describe('when the user "isSubscribed"', () => {
|
|
||||||
// beforeEach(() => {
|
|
||||||
// account = normalizeAccount({
|
|
||||||
// ...account,
|
|
||||||
// relationship: normalizeRelationship({ requested: true, notifying: true }),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// store = {
|
|
||||||
// ...store,
|
|
||||||
// accounts: ImmutableMap({
|
|
||||||
// '1': account,
|
|
||||||
// }),
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it('renders the unsubscribe button', () => {
|
|
||||||
// render(<SubscribeButton account={account} />, null, store);
|
|
||||||
// expect(screen.getByTestId('icon-button').title).toEqual(`Unsubscribe to notifications from @${account.acct}`);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
// describe('when the user is not "isSubscribed"', () => {
|
|
||||||
// beforeEach(() => {
|
|
||||||
// account = normalizeAccount({
|
|
||||||
// ...account,
|
|
||||||
// relationship: normalizeRelationship({ requested: true, notifying: false }),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// store = {
|
|
||||||
// ...store,
|
|
||||||
// accounts: ImmutableMap({
|
|
||||||
// '1': account,
|
|
||||||
// }),
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it('renders the unsubscribe button', () => {
|
|
||||||
// render(<SubscribeButton account={account} />, null, store);
|
|
||||||
// expect(screen.getByTestId('icon-button').title).toEqual(`Subscribe to notifications from @${account.acct}`);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
// describe('when the user is not following the account', () => {
|
|
||||||
// beforeEach(() => {
|
|
||||||
// account = normalizeAccount({ ...account, relationship: normalizeRelationship({ following: false }) });
|
|
||||||
|
|
||||||
// store = {
|
|
||||||
// ...store,
|
|
||||||
// accounts: ImmutableMap({
|
|
||||||
// '1': account,
|
|
||||||
// }),
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it('renders nothing', () => {
|
|
||||||
// render(<SubscribeButton account={account} />, null, store);
|
|
||||||
// expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
// describe('when the user is following the account', () => {
|
|
||||||
// beforeEach(() => {
|
|
||||||
// account = normalizeAccount({ ...account, relationship: normalizeRelationship({ following: true }) });
|
|
||||||
|
|
||||||
// store = {
|
|
||||||
// ...store,
|
|
||||||
// accounts: ImmutableMap({
|
|
||||||
// '1': account,
|
|
||||||
// }),
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it('renders the button', () => {
|
|
||||||
// render(<SubscribeButton account={account} />, null, store);
|
|
||||||
// expect(screen.getByTestId('icon-button')).toBeInTheDocument();
|
|
||||||
// });
|
|
||||||
|
|
||||||
// describe('when the user "isSubscribed"', () => {
|
|
||||||
// beforeEach(() => {
|
|
||||||
// account = normalizeAccount({
|
|
||||||
// ...account,
|
|
||||||
// relationship: normalizeRelationship({ requested: true, notifying: true }),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// store = {
|
|
||||||
// ...store,
|
|
||||||
// accounts: ImmutableMap({
|
|
||||||
// '1': account,
|
|
||||||
// }),
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it('renders the unsubscribe button', () => {
|
|
||||||
// render(<SubscribeButton account={account} />, null, store);
|
|
||||||
// expect(screen.getByTestId('icon-button').title).toEqual(`Unsubscribe to notifications from @${account.acct}`);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
// describe('when the user is not "isSubscribed"', () => {
|
|
||||||
// beforeEach(() => {
|
|
||||||
// account = normalizeAccount({
|
|
||||||
// ...account,
|
|
||||||
// relationship: normalizeRelationship({ requested: true, notifying: false }),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// store = {
|
|
||||||
// ...store,
|
|
||||||
// accounts: ImmutableMap({
|
|
||||||
// '1': account,
|
|
||||||
// }),
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it('renders the unsubscribe button', () => {
|
|
||||||
// render(<SubscribeButton account={account} />, null, store);
|
|
||||||
// expect(screen.getByTestId('icon-button').title).toEqual(`Subscribe to notifications from @${account.acct}`);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,33 +1,64 @@
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
adSchema,
|
||||||
|
cardSchema,
|
||||||
groupSchema,
|
groupSchema,
|
||||||
groupRelationshipSchema,
|
groupRelationshipSchema,
|
||||||
groupTagSchema,
|
groupTagSchema,
|
||||||
|
relationshipSchema,
|
||||||
|
type Ad,
|
||||||
|
type Card,
|
||||||
type Group,
|
type Group,
|
||||||
type GroupRelationship,
|
type GroupRelationship,
|
||||||
type GroupTag,
|
type GroupTag,
|
||||||
|
type Relationship,
|
||||||
} from 'soapbox/schemas';
|
} from 'soapbox/schemas';
|
||||||
|
|
||||||
// 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 buildGroup(props: Record<string, any> = {}): Group {
|
function buildCard(props: Partial<Card> = {}): Card {
|
||||||
|
return cardSchema.parse(Object.assign({
|
||||||
|
url: 'https://soapbox.test',
|
||||||
|
}, props));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGroup(props: Partial<Group> = {}): Group {
|
||||||
return groupSchema.parse(Object.assign({
|
return groupSchema.parse(Object.assign({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
}, props));
|
}, props));
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildGroupRelationship(props: Record<string, any> = {}): GroupRelationship {
|
function buildGroupRelationship(props: Partial<GroupRelationship> = {}): GroupRelationship {
|
||||||
return groupRelationshipSchema.parse(Object.assign({
|
return groupRelationshipSchema.parse(Object.assign({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
}, props));
|
}, props));
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildGroupTag(props: Record<string, any> = {}): GroupTag {
|
function buildGroupTag(props: Partial<GroupTag> = {}): GroupTag {
|
||||||
return groupTagSchema.parse(Object.assign({
|
return groupTagSchema.parse(Object.assign({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
}, props));
|
}, props));
|
||||||
}
|
}
|
||||||
|
|
||||||
export { buildGroup, buildGroupRelationship, buildGroupTag };
|
function buildAd(props: Partial<Ad> = {}): Ad {
|
||||||
|
return adSchema.parse(Object.assign({
|
||||||
|
card: buildCard(),
|
||||||
|
}, props));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRelationship(props: Partial<Relationship> = {}): Relationship {
|
||||||
|
return relationshipSchema.parse(Object.assign({
|
||||||
|
id: uuidv4(),
|
||||||
|
}, props));
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
buildCard,
|
||||||
|
buildGroup,
|
||||||
|
buildGroupRelationship,
|
||||||
|
buildGroupTag,
|
||||||
|
buildAd,
|
||||||
|
buildRelationship,
|
||||||
|
};
|
|
@ -1,14 +0,0 @@
|
||||||
import { Record as ImmutableRecord } from 'immutable';
|
|
||||||
|
|
||||||
import { normalizeCard } from '../card';
|
|
||||||
|
|
||||||
describe('normalizeCard()', () => {
|
|
||||||
it('adds base fields', () => {
|
|
||||||
const card = {};
|
|
||||||
const result = normalizeCard(card);
|
|
||||||
|
|
||||||
expect(ImmutableRecord.isRecord(result)).toBe(true);
|
|
||||||
expect(result.type).toEqual('link');
|
|
||||||
expect(result.url).toEqual('');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { Record as ImmutableRecord } from 'immutable';
|
|
||||||
|
|
||||||
import { normalizePoll } from '../poll';
|
|
||||||
|
|
||||||
describe('normalizePoll()', () => {
|
|
||||||
it('adds base fields', () => {
|
|
||||||
const poll = { options: [{ title: 'Apples' }] };
|
|
||||||
const result = normalizePoll(poll);
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
options: [{ title: 'Apples', votes_count: 0 }],
|
|
||||||
emojis: [],
|
|
||||||
expired: false,
|
|
||||||
multiple: false,
|
|
||||||
voters_count: 0,
|
|
||||||
votes_count: 0,
|
|
||||||
own_votes: null,
|
|
||||||
voted: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(ImmutableRecord.isRecord(result)).toBe(true);
|
|
||||||
expect(ImmutableRecord.isRecord(result.options.get(0))).toBe(true);
|
|
||||||
expect(result.toJS()).toMatchObject(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('normalizes a Pleroma logged-out poll', () => {
|
|
||||||
const { poll } = require('soapbox/__fixtures__/pleroma-status-with-poll.json');
|
|
||||||
const result = normalizePoll(poll);
|
|
||||||
|
|
||||||
// Adds logged-in fields
|
|
||||||
expect(result.voted).toBe(false);
|
|
||||||
expect(result.own_votes).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('normalizes poll with emojis', () => {
|
|
||||||
const { poll } = require('soapbox/__fixtures__/pleroma-status-with-poll-with-emojis.json');
|
|
||||||
const result = normalizePoll(poll);
|
|
||||||
|
|
||||||
// Emojifies poll options
|
|
||||||
expect(result.options.get(1)?.title_emojified)
|
|
||||||
.toContain('emojione');
|
|
||||||
|
|
||||||
// Parses emojis as Immutable.Record's
|
|
||||||
expect(ImmutableRecord.isRecord(result.emojis.get(0))).toBe(true);
|
|
||||||
expect(result.emojis.get(1)?.shortcode).toEqual('soapbox');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -146,12 +146,16 @@ describe('normalizeStatus()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('normalizes poll and poll options', () => {
|
it('normalizes poll and poll options', () => {
|
||||||
const status = { poll: { options: [{ title: 'Apples' }] } };
|
const status = { poll: { id: '1', options: [{ title: 'Apples' }, { title: 'Oranges' }] } };
|
||||||
const result = normalizeStatus(status);
|
const result = normalizeStatus(status);
|
||||||
const poll = result.poll as Poll;
|
const poll = result.poll as Poll;
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
options: [{ title: 'Apples', votes_count: 0 }],
|
id: '1',
|
||||||
|
options: [
|
||||||
|
{ title: 'Apples', votes_count: 0 },
|
||||||
|
{ title: 'Oranges', votes_count: 0 },
|
||||||
|
],
|
||||||
emojis: [],
|
emojis: [],
|
||||||
expired: false,
|
expired: false,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
|
@ -161,9 +165,7 @@ describe('normalizeStatus()', () => {
|
||||||
voted: false,
|
voted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(ImmutableRecord.isRecord(poll)).toBe(true);
|
expect(poll).toMatchObject(expected);
|
||||||
expect(ImmutableRecord.isRecord(poll.options.get(0))).toBe(true);
|
|
||||||
expect(poll.toJS()).toMatchObject(expected);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('normalizes a Pleroma logged-out poll', () => {
|
it('normalizes a Pleroma logged-out poll', () => {
|
||||||
|
@ -182,12 +184,10 @@ describe('normalizeStatus()', () => {
|
||||||
const poll = result.poll as Poll;
|
const poll = result.poll as Poll;
|
||||||
|
|
||||||
// Emojifies poll options
|
// Emojifies poll options
|
||||||
expect(poll.options.get(1)?.title_emojified)
|
expect(poll.options[1].title_emojified)
|
||||||
.toContain('emojione');
|
.toContain('emojione');
|
||||||
|
|
||||||
// Parses emojis as Immutable.Record's
|
expect(poll.emojis[1].shortcode).toEqual('soapbox');
|
||||||
expect(ImmutableRecord.isRecord(poll.emojis.get(0))).toBe(true);
|
|
||||||
expect(poll.emojis.get(1)?.shortcode).toEqual('soapbox');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('normalizes a card', () => {
|
it('normalizes a card', () => {
|
||||||
|
@ -195,7 +195,6 @@ describe('normalizeStatus()', () => {
|
||||||
const result = normalizeStatus(status);
|
const result = normalizeStatus(status);
|
||||||
const card = result.card as Card;
|
const card = result.card as Card;
|
||||||
|
|
||||||
expect(ImmutableRecord.isRecord(card)).toBe(true);
|
|
||||||
expect(card.type).toEqual('link');
|
expect(card.type).toEqual('link');
|
||||||
expect(card.provider_url).toEqual('https://soapbox.pub');
|
expect(card.provider_url).toEqual('https://soapbox.pub');
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
/**
|
|
||||||
* Card normalizer:
|
|
||||||
* Converts API cards into our internal format.
|
|
||||||
* @see {@link https://docs.joinmastodon.org/entities/card/}
|
|
||||||
*/
|
|
||||||
import punycode from 'punycode';
|
|
||||||
|
|
||||||
import { Record as ImmutableRecord, Map as ImmutableMap, fromJS } from 'immutable';
|
|
||||||
|
|
||||||
import { groupSchema, type Group } from 'soapbox/schemas';
|
|
||||||
import { mergeDefined } from 'soapbox/utils/normalizers';
|
|
||||||
|
|
||||||
// https://docs.joinmastodon.org/entities/card/
|
|
||||||
export const CardRecord = ImmutableRecord({
|
|
||||||
author_name: '',
|
|
||||||
author_url: '',
|
|
||||||
blurhash: null as string | null,
|
|
||||||
description: '',
|
|
||||||
embed_url: '',
|
|
||||||
group: null as null | Group,
|
|
||||||
height: 0,
|
|
||||||
html: '',
|
|
||||||
image: null as string | null,
|
|
||||||
provider_name: '',
|
|
||||||
provider_url: '',
|
|
||||||
title: '',
|
|
||||||
type: 'link',
|
|
||||||
url: '',
|
|
||||||
width: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const IDNA_PREFIX = 'xn--';
|
|
||||||
|
|
||||||
const decodeIDNA = (domain: string): string => {
|
|
||||||
return domain
|
|
||||||
.split('.')
|
|
||||||
.map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part)
|
|
||||||
.join('.');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getHostname = (url: string): string => {
|
|
||||||
const parser = document.createElement('a');
|
|
||||||
parser.href = url;
|
|
||||||
return parser.hostname;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Fall back to Pleroma's OG data */
|
|
||||||
const normalizePleromaOpengraph = (card: ImmutableMap<string, any>) => {
|
|
||||||
const opengraph = ImmutableMap({
|
|
||||||
width: card.getIn(['pleroma', 'opengraph', 'width']),
|
|
||||||
height: card.getIn(['pleroma', 'opengraph', 'height']),
|
|
||||||
html: card.getIn(['pleroma', 'opengraph', 'html']),
|
|
||||||
image: card.getIn(['pleroma', 'opengraph', 'thumbnail_url']),
|
|
||||||
});
|
|
||||||
|
|
||||||
return card.mergeWith(mergeDefined, opengraph);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Set provider from URL if not found */
|
|
||||||
const normalizeProviderName = (card: ImmutableMap<string, any>) => {
|
|
||||||
const providerName = card.get('provider_name') || decodeIDNA(getHostname(card.get('url')));
|
|
||||||
return card.set('provider_name', providerName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeGroup = (card: ImmutableMap<string, any>) => {
|
|
||||||
try {
|
|
||||||
const group = groupSchema.parse(card.get('group').toJS());
|
|
||||||
return card.set('group', group);
|
|
||||||
} catch (_e) {
|
|
||||||
return card.set('group', null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const normalizeCard = (card: Record<string, any>) => {
|
|
||||||
return CardRecord(
|
|
||||||
ImmutableMap(fromJS(card)).withMutations(card => {
|
|
||||||
normalizePleromaOpengraph(card);
|
|
||||||
normalizeProviderName(card);
|
|
||||||
normalizeGroup(card);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -6,8 +6,8 @@ import {
|
||||||
} from 'immutable';
|
} from 'immutable';
|
||||||
|
|
||||||
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
|
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
|
||||||
|
import { emojiReactionSchema } from 'soapbox/schemas';
|
||||||
import { normalizeEmojiReaction } from './emoji-reaction';
|
import { filteredArray } from 'soapbox/schemas/utils';
|
||||||
|
|
||||||
import type { Attachment, Card, Emoji, EmojiReaction } from 'soapbox/types/entities';
|
import type { Attachment, Card, Emoji, EmojiReaction } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ export const ChatMessageRecord = ImmutableRecord({
|
||||||
created_at: '',
|
created_at: '',
|
||||||
emojis: ImmutableList<Emoji>(),
|
emojis: ImmutableList<Emoji>(),
|
||||||
expiration: null as number | null,
|
expiration: null as number | null,
|
||||||
emoji_reactions: null as ImmutableList<EmojiReaction> | null,
|
emoji_reactions: null as readonly EmojiReaction[] | null,
|
||||||
id: '',
|
id: '',
|
||||||
unread: false,
|
unread: false,
|
||||||
deleting: false,
|
deleting: false,
|
||||||
|
@ -41,13 +41,8 @@ const normalizeMedia = (status: ImmutableMap<string, any>) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeChatMessageEmojiReaction = (chatMessage: ImmutableMap<string, any>) => {
|
const normalizeChatMessageEmojiReaction = (chatMessage: ImmutableMap<string, any>) => {
|
||||||
const emojiReactions = chatMessage.get('emoji_reactions');
|
const emojiReactions = ImmutableList(chatMessage.get('emoji_reactions') || []);
|
||||||
|
return chatMessage.set('emoji_reactions', filteredArray(emojiReactionSchema).parse(emojiReactions.toJS()));
|
||||||
if (emojiReactions) {
|
|
||||||
return chatMessage.set('emoji_reactions', ImmutableList(emojiReactions.map(normalizeEmojiReaction)));
|
|
||||||
} else {
|
|
||||||
return chatMessage;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Rewrite `<p></p>` to empty string. */
|
/** Rewrite `<p></p>` to empty string. */
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
|
|
||||||
|
|
||||||
// https://docs.joinmastodon.org/entities/emoji/
|
|
||||||
export const EmojiReactionRecord = ImmutableRecord({
|
|
||||||
name: '',
|
|
||||||
count: null as number | null,
|
|
||||||
me: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const normalizeEmojiReaction = (emojiReaction: Record<string, any>) => {
|
|
||||||
return EmojiReactionRecord(
|
|
||||||
ImmutableMap(fromJS(emojiReaction)),
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -4,11 +4,9 @@ export { AdminReportRecord, normalizeAdminReport } from './admin-report';
|
||||||
export { AnnouncementRecord, normalizeAnnouncement } from './announcement';
|
export { AnnouncementRecord, normalizeAnnouncement } from './announcement';
|
||||||
export { AnnouncementReactionRecord, normalizeAnnouncementReaction } from './announcement-reaction';
|
export { AnnouncementReactionRecord, normalizeAnnouncementReaction } from './announcement-reaction';
|
||||||
export { AttachmentRecord, normalizeAttachment } from './attachment';
|
export { AttachmentRecord, normalizeAttachment } from './attachment';
|
||||||
export { CardRecord, normalizeCard } from './card';
|
|
||||||
export { ChatRecord, normalizeChat } from './chat';
|
export { ChatRecord, normalizeChat } from './chat';
|
||||||
export { ChatMessageRecord, normalizeChatMessage } from './chat-message';
|
export { ChatMessageRecord, normalizeChatMessage } from './chat-message';
|
||||||
export { EmojiRecord, normalizeEmoji } from './emoji';
|
export { EmojiRecord, normalizeEmoji } from './emoji';
|
||||||
export { EmojiReactionRecord } from './emoji-reaction';
|
|
||||||
export { FilterRecord, normalizeFilter } from './filter';
|
export { FilterRecord, normalizeFilter } from './filter';
|
||||||
export { FilterKeywordRecord, normalizeFilterKeyword } from './filter-keyword';
|
export { FilterKeywordRecord, normalizeFilterKeyword } from './filter-keyword';
|
||||||
export { FilterStatusRecord, normalizeFilterStatus } from './filter-status';
|
export { FilterStatusRecord, normalizeFilterStatus } from './filter-status';
|
||||||
|
@ -20,11 +18,8 @@ export { ListRecord, normalizeList } from './list';
|
||||||
export { LocationRecord, normalizeLocation } from './location';
|
export { LocationRecord, normalizeLocation } from './location';
|
||||||
export { MentionRecord, normalizeMention } from './mention';
|
export { MentionRecord, normalizeMention } from './mention';
|
||||||
export { NotificationRecord, normalizeNotification } from './notification';
|
export { NotificationRecord, normalizeNotification } from './notification';
|
||||||
export { PollRecord, PollOptionRecord, normalizePoll } from './poll';
|
|
||||||
export { RelationshipRecord, normalizeRelationship } from './relationship';
|
|
||||||
export { StatusRecord, normalizeStatus } from './status';
|
export { StatusRecord, normalizeStatus } from './status';
|
||||||
export { StatusEditRecord, normalizeStatusEdit } from './status-edit';
|
export { StatusEditRecord, normalizeStatusEdit } from './status-edit';
|
||||||
export { TagRecord, normalizeTag } from './tag';
|
export { TagRecord, normalizeTag } from './tag';
|
||||||
|
|
||||||
export { AdRecord, normalizeAd } from './soapbox/ad';
|
|
||||||
export { SoapboxConfigRecord, normalizeSoapboxConfig } from './soapbox/soapbox-config';
|
export { SoapboxConfigRecord, normalizeSoapboxConfig } from './soapbox/soapbox-config';
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
/**
|
|
||||||
* Poll normalizer:
|
|
||||||
* Converts API polls into our internal format.
|
|
||||||
* @see {@link https://docs.joinmastodon.org/entities/poll/}
|
|
||||||
*/
|
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
|
||||||
import {
|
|
||||||
Map as ImmutableMap,
|
|
||||||
List as ImmutableList,
|
|
||||||
Record as ImmutableRecord,
|
|
||||||
fromJS,
|
|
||||||
} from 'immutable';
|
|
||||||
|
|
||||||
import emojify from 'soapbox/features/emoji';
|
|
||||||
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
|
||||||
import { makeEmojiMap } from 'soapbox/utils/normalizers';
|
|
||||||
|
|
||||||
import type { Emoji, PollOption } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
// https://docs.joinmastodon.org/entities/poll/
|
|
||||||
export const PollRecord = ImmutableRecord({
|
|
||||||
emojis: ImmutableList<Emoji>(),
|
|
||||||
expired: false,
|
|
||||||
expires_at: '',
|
|
||||||
id: '',
|
|
||||||
multiple: false,
|
|
||||||
options: ImmutableList<PollOption>(),
|
|
||||||
voters_count: 0,
|
|
||||||
votes_count: 0,
|
|
||||||
own_votes: null as ImmutableList<number> | null,
|
|
||||||
voted: false,
|
|
||||||
pleroma: ImmutableMap<string, any>(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sub-entity of Poll
|
|
||||||
export const PollOptionRecord = ImmutableRecord({
|
|
||||||
title: '',
|
|
||||||
votes_count: 0,
|
|
||||||
|
|
||||||
// Internal fields
|
|
||||||
title_emojified: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Normalize emojis
|
|
||||||
const normalizeEmojis = (entity: ImmutableMap<string, any>) => {
|
|
||||||
return entity.update('emojis', ImmutableList(), emojis => {
|
|
||||||
return emojis.map(normalizeEmoji);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizePollOption = (option: ImmutableMap<string, any> | string, emojis: ImmutableList<ImmutableMap<string, string>> = ImmutableList()) => {
|
|
||||||
const emojiMap = makeEmojiMap(emojis);
|
|
||||||
|
|
||||||
if (typeof option === 'string') {
|
|
||||||
const titleEmojified = emojify(escapeTextContentForBrowser(option), emojiMap);
|
|
||||||
|
|
||||||
return PollOptionRecord({
|
|
||||||
title: option,
|
|
||||||
title_emojified: titleEmojified,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
|
|
||||||
|
|
||||||
return PollOptionRecord(
|
|
||||||
option.set('title_emojified', titleEmojified),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Normalize poll options
|
|
||||||
const normalizePollOptions = (poll: ImmutableMap<string, any>) => {
|
|
||||||
const emojis = poll.get('emojis');
|
|
||||||
|
|
||||||
return poll.update('options', (options: ImmutableList<ImmutableMap<string, any>>) => {
|
|
||||||
return options.map(option => normalizePollOption(option, emojis));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Normalize own_votes to `null` if empty (like Mastodon)
|
|
||||||
const normalizePollOwnVotes = (poll: ImmutableMap<string, any>) => {
|
|
||||||
return poll.update('own_votes', ownVotes => {
|
|
||||||
return ownVotes?.size > 0 ? ownVotes : null;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Whether the user voted in the poll
|
|
||||||
const normalizePollVoted = (poll: ImmutableMap<string, any>) => {
|
|
||||||
return poll.update('voted', voted => {
|
|
||||||
return typeof voted === 'boolean' ? voted : poll.get('own_votes')?.size > 0;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const normalizePoll = (poll: Record<string, any>) => {
|
|
||||||
return PollRecord(
|
|
||||||
ImmutableMap(fromJS(poll)).withMutations((poll: ImmutableMap<string, any>) => {
|
|
||||||
normalizeEmojis(poll);
|
|
||||||
normalizePollOptions(poll);
|
|
||||||
normalizePollOwnVotes(poll);
|
|
||||||
normalizePollVoted(poll);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,35 +0,0 @@
|
||||||
/**
|
|
||||||
* Relationship normalizer:
|
|
||||||
* Converts API relationships into our internal format.
|
|
||||||
* @see {@link https://docs.joinmastodon.org/entities/relationship/}
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
Map as ImmutableMap,
|
|
||||||
Record as ImmutableRecord,
|
|
||||||
fromJS,
|
|
||||||
} from 'immutable';
|
|
||||||
|
|
||||||
// https://docs.joinmastodon.org/entities/relationship/
|
|
||||||
// https://api.pleroma.social/#operation/AccountController.relationships
|
|
||||||
export const RelationshipRecord = ImmutableRecord({
|
|
||||||
blocked_by: false,
|
|
||||||
blocking: false,
|
|
||||||
domain_blocking: false,
|
|
||||||
endorsed: false,
|
|
||||||
followed_by: false,
|
|
||||||
following: false,
|
|
||||||
id: '',
|
|
||||||
muting: false,
|
|
||||||
muting_notifications: false,
|
|
||||||
note: '',
|
|
||||||
notifying: false,
|
|
||||||
requested: false,
|
|
||||||
showing_reblogs: false,
|
|
||||||
subscribing: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const normalizeRelationship = (relationship: Record<string, any>) => {
|
|
||||||
return RelationshipRecord(
|
|
||||||
ImmutableMap(fromJS(relationship)),
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,28 +0,0 @@
|
||||||
import {
|
|
||||||
Map as ImmutableMap,
|
|
||||||
Record as ImmutableRecord,
|
|
||||||
fromJS,
|
|
||||||
} from 'immutable';
|
|
||||||
|
|
||||||
import { CardRecord, normalizeCard } from '../card';
|
|
||||||
|
|
||||||
import type { Ad } from 'soapbox/features/ads/providers';
|
|
||||||
|
|
||||||
export const AdRecord = ImmutableRecord<Ad>({
|
|
||||||
card: CardRecord(),
|
|
||||||
impression: undefined as string | undefined,
|
|
||||||
expires_at: undefined as string | undefined,
|
|
||||||
reason: undefined as string | undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Normalizes an ad from Soapbox Config. */
|
|
||||||
export const normalizeAd = (ad: Record<string, any>) => {
|
|
||||||
const map = ImmutableMap<string, any>(fromJS(ad));
|
|
||||||
const card = normalizeCard(map.get('card'));
|
|
||||||
const expiresAt = map.get('expires_at') || map.get('expires');
|
|
||||||
|
|
||||||
return AdRecord(map.merge({
|
|
||||||
card,
|
|
||||||
expires_at: expiresAt,
|
|
||||||
}));
|
|
||||||
};
|
|
|
@ -6,12 +6,12 @@ import {
|
||||||
} from 'immutable';
|
} from 'immutable';
|
||||||
import trimStart from 'lodash/trimStart';
|
import trimStart from 'lodash/trimStart';
|
||||||
|
|
||||||
|
import { adSchema } from 'soapbox/schemas';
|
||||||
|
import { filteredArray } from 'soapbox/schemas/utils';
|
||||||
import { normalizeUsername } from 'soapbox/utils/input';
|
import { normalizeUsername } from 'soapbox/utils/input';
|
||||||
import { toTailwind } from 'soapbox/utils/tailwind';
|
import { toTailwind } from 'soapbox/utils/tailwind';
|
||||||
import { generateAccent } from 'soapbox/utils/theme';
|
import { generateAccent } from 'soapbox/utils/theme';
|
||||||
|
|
||||||
import { normalizeAd } from './ad';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Ad,
|
Ad,
|
||||||
PromoPanelItem,
|
PromoPanelItem,
|
||||||
|
@ -125,8 +125,12 @@ export const SoapboxConfigRecord = ImmutableRecord({
|
||||||
type SoapboxConfigMap = ImmutableMap<string, any>;
|
type SoapboxConfigMap = ImmutableMap<string, any>;
|
||||||
|
|
||||||
const normalizeAds = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => {
|
const normalizeAds = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => {
|
||||||
const ads = ImmutableList<Record<string, any>>(soapboxConfig.get('ads'));
|
if (soapboxConfig.has('ads')) {
|
||||||
return soapboxConfig.set('ads', ads.map(normalizeAd));
|
const ads = filteredArray(adSchema).parse(soapboxConfig.get('ads').toJS());
|
||||||
|
return soapboxConfig.set('ads', ads);
|
||||||
|
} else {
|
||||||
|
return soapboxConfig;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeCryptoAddress = (address: unknown): CryptoAddress => {
|
const normalizeCryptoAddress = (address: unknown): CryptoAddress => {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
import emojify from 'soapbox/features/emoji';
|
import emojify from 'soapbox/features/emoji';
|
||||||
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 { normalizePoll } from 'soapbox/normalizers/poll';
|
import { pollSchema } from 'soapbox/schemas';
|
||||||
import { stripCompatibilityFeatures } from 'soapbox/utils/html';
|
import { stripCompatibilityFeatures } from 'soapbox/utils/html';
|
||||||
import { makeEmojiMap } from 'soapbox/utils/normalizers';
|
import { makeEmojiMap } from 'soapbox/utils/normalizers';
|
||||||
|
|
||||||
|
@ -50,9 +50,10 @@ const normalizeEmojis = (entity: ImmutableMap<string, any>) => {
|
||||||
|
|
||||||
// Normalize the poll in the status, if applicable
|
// Normalize the poll in the status, if applicable
|
||||||
const normalizeStatusPoll = (statusEdit: ImmutableMap<string, any>) => {
|
const normalizeStatusPoll = (statusEdit: ImmutableMap<string, any>) => {
|
||||||
if (statusEdit.hasIn(['poll', 'options'])) {
|
try {
|
||||||
return statusEdit.update('poll', ImmutableMap(), normalizePoll);
|
const poll = pollSchema.parse(statusEdit.get('poll').toJS());
|
||||||
} else {
|
return statusEdit.set('poll', poll);
|
||||||
|
} catch (_e) {
|
||||||
return statusEdit.set('poll', null);
|
return statusEdit.set('poll', null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,10 +11,9 @@ import {
|
||||||
} from 'immutable';
|
} from 'immutable';
|
||||||
|
|
||||||
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
|
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
|
||||||
import { normalizeCard } from 'soapbox/normalizers/card';
|
|
||||||
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 { normalizePoll } from 'soapbox/normalizers/poll';
|
import { cardSchema, pollSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
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';
|
||||||
|
@ -109,18 +108,20 @@ const normalizeEmojis = (entity: ImmutableMap<string, any>) => {
|
||||||
|
|
||||||
// Normalize the poll in the status, if applicable
|
// Normalize the poll in the status, if applicable
|
||||||
const normalizeStatusPoll = (status: ImmutableMap<string, any>) => {
|
const normalizeStatusPoll = (status: ImmutableMap<string, any>) => {
|
||||||
if (status.hasIn(['poll', 'options'])) {
|
try {
|
||||||
return status.update('poll', ImmutableMap(), normalizePoll);
|
const poll = pollSchema.parse(status.get('poll').toJS());
|
||||||
} else {
|
return status.set('poll', poll);
|
||||||
|
} catch (_e) {
|
||||||
return status.set('poll', null);
|
return status.set('poll', null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Normalize card
|
// Normalize card
|
||||||
const normalizeStatusCard = (status: ImmutableMap<string, any>) => {
|
const normalizeStatusCard = (status: ImmutableMap<string, any>) => {
|
||||||
if (status.get('card')) {
|
try {
|
||||||
return status.update('card', ImmutableMap(), normalizeCard);
|
const card = cardSchema.parse(status.get('card').toJS());
|
||||||
} else {
|
return status.set('card', card);
|
||||||
|
} catch (e) {
|
||||||
return status.set('card', null);
|
return status.set('card', null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
import sumBy from 'lodash/sumBy';
|
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 { 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, normalizeRelationship } from 'soapbox/normalizers';
|
import { normalizeChatMessage } from 'soapbox/normalizers';
|
||||||
import { normalizeEmojiReaction } from 'soapbox/normalizers/emoji-reaction';
|
|
||||||
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';
|
||||||
|
@ -120,7 +120,7 @@ describe('useChatMessages', () => {
|
||||||
const state = rootState
|
const state = rootState
|
||||||
.set(
|
.set(
|
||||||
'relationships',
|
'relationships',
|
||||||
ImmutableMap({ '1': normalizeRelationship({ blocked_by: true }) }),
|
ImmutableMap({ '1': buildRelationship({ blocked_by: true }) }),
|
||||||
);
|
);
|
||||||
store = mockStore(state);
|
store = mockStore(state);
|
||||||
});
|
});
|
||||||
|
@ -239,7 +239,7 @@ describe('useChat()', () => {
|
||||||
mock.onGet(`/api/v1/pleroma/chats/${chat.id}`).reply(200, chat);
|
mock.onGet(`/api/v1/pleroma/chats/${chat.id}`).reply(200, chat);
|
||||||
mock
|
mock
|
||||||
.onGet(`/api/v1/accounts/relationships?id[]=${chat.account.id}`)
|
.onGet(`/api/v1/accounts/relationships?id[]=${chat.account.id}`)
|
||||||
.reply(200, [normalizeRelationship({ id: relationshipId, blocked_by: true })]);
|
.reply(200, [buildRelationship({ id: relationshipId, blocked_by: true })]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -425,11 +425,11 @@ describe('useChatActions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedChatMessage = (queryClient.getQueryData(ChatKeys.chatMessages(chat.id)) as any).pages[0].result[0] as ChatMessage;
|
const updatedChatMessage = (queryClient.getQueryData(ChatKeys.chatMessages(chat.id)) as any).pages[0].result[0] as ChatMessage;
|
||||||
expect(updatedChatMessage.emoji_reactions).toEqual(ImmutableList([normalizeEmojiReaction({
|
expect(updatedChatMessage.emoji_reactions).toEqual([{
|
||||||
name: '👍',
|
name: '👍',
|
||||||
count: 1,
|
count: 1,
|
||||||
me: true,
|
me: true,
|
||||||
})]));
|
}]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
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 { createTestStore, queryClient, renderHook, rootState, waitFor } from 'soapbox/jest/test-helpers';
|
import { createTestStore, queryClient, renderHook, rootState, waitFor } from 'soapbox/jest/test-helpers';
|
||||||
import { normalizeRelationship } from 'soapbox/normalizers';
|
|
||||||
import { Store } from 'soapbox/store';
|
import { Store } from 'soapbox/store';
|
||||||
|
|
||||||
import { useFetchRelationships } from '../relationships';
|
import { useFetchRelationships } from '../relationships';
|
||||||
|
@ -25,7 +25,7 @@ describe('useFetchRelationships()', () => {
|
||||||
__stub((mock) => {
|
__stub((mock) => {
|
||||||
mock
|
mock
|
||||||
.onGet(`/api/v1/accounts/relationships?id[]=${id}`)
|
.onGet(`/api/v1/accounts/relationships?id[]=${id}`)
|
||||||
.reply(200, [normalizeRelationship({ id, blocked_by: true })]);
|
.reply(200, [buildRelationship({ id, blocked_by: true })]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ describe('useFetchRelationships()', () => {
|
||||||
__stub((mock) => {
|
__stub((mock) => {
|
||||||
mock
|
mock
|
||||||
.onGet(`/api/v1/accounts/relationships?id[]=${ids[0]}&id[]=${ids[1]}`)
|
.onGet(`/api/v1/accounts/relationships?id[]=${ids[0]}&id[]=${ids[1]}`)
|
||||||
.reply(200, ids.map((id) => normalizeRelationship({ id, blocked_by: true })));
|
.reply(200, ids.map((id) => buildRelationship({ id, blocked_by: true })));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,8 @@ import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { Ad, getProvider } from 'soapbox/features/ads/providers';
|
import { Ad, getProvider } from 'soapbox/features/ads/providers';
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
import { normalizeAd } from 'soapbox/normalizers';
|
import { adSchema } from 'soapbox/schemas';
|
||||||
|
import { filteredArray } from 'soapbox/schemas/utils';
|
||||||
import { isExpired } from 'soapbox/utils/ads';
|
import { isExpired } from 'soapbox/utils/ads';
|
||||||
|
|
||||||
const AdKeys = {
|
const AdKeys = {
|
||||||
|
@ -28,7 +29,9 @@ function useAds() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter out expired ads.
|
// Filter out expired ads.
|
||||||
const data = result.data?.map(normalizeAd).filter(ad => !isExpired(ad));
|
const data = filteredArray(adSchema)
|
||||||
|
.parse(result.data)
|
||||||
|
.filter(ad => !isExpired(ad));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
|
|
|
@ -11,14 +11,17 @@ describe('polls reducer', () => {
|
||||||
|
|
||||||
describe('POLLS_IMPORT', () => {
|
describe('POLLS_IMPORT', () => {
|
||||||
it('normalizes the poll', () => {
|
it('normalizes the poll', () => {
|
||||||
const polls = [{ id: '3', options: [{ title: 'Apples' }] }];
|
const polls = [{ id: '3', options: [{ title: 'Apples' }, { title: 'Oranges' }] }];
|
||||||
const action = { type: POLLS_IMPORT, polls };
|
const action = { type: POLLS_IMPORT, polls };
|
||||||
|
|
||||||
const result = reducer(undefined, action);
|
const result = reducer(undefined, action);
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
'3': {
|
'3': {
|
||||||
options: [{ title: 'Apples', votes_count: 0 }],
|
options: [
|
||||||
|
{ title: 'Apples', votes_count: 0 },
|
||||||
|
{ title: 'Oranges', votes_count: 0 },
|
||||||
|
],
|
||||||
emojis: [],
|
emojis: [],
|
||||||
expired: false,
|
expired: false,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { List as ImmutableList, 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';
|
||||||
import { normalizeRelationship } from 'soapbox/normalizers/relationship';
|
import { type Relationship, relationshipSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
import { ACCOUNT_NOTE_SUBMIT_SUCCESS } from '../actions/account-notes';
|
import { ACCOUNT_NOTE_SUBMIT_SUCCESS } from '../actions/account-notes';
|
||||||
import {
|
import {
|
||||||
|
@ -35,13 +35,16 @@ import {
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
import type { APIEntity } from 'soapbox/types/entities';
|
import type { APIEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
type Relationship = ReturnType<typeof normalizeRelationship>;
|
|
||||||
type State = ImmutableMap<string, Relationship>;
|
type State = ImmutableMap<string, Relationship>;
|
||||||
type APIEntities = Array<APIEntity>;
|
type APIEntities = Array<APIEntity>;
|
||||||
|
|
||||||
const normalizeRelationships = (state: State, relationships: APIEntities) => {
|
const normalizeRelationships = (state: State, relationships: APIEntities) => {
|
||||||
relationships.forEach(relationship => {
|
relationships.forEach(relationship => {
|
||||||
state = state.set(relationship.id, normalizeRelationship(relationship));
|
try {
|
||||||
|
state = state.set(relationship.id, relationshipSchema.parse(relationship));
|
||||||
|
} catch (_e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
|
@ -84,8 +87,12 @@ const followStateToRelationship = (followState: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateFollowRelationship = (state: State, id: string, followState: string) => {
|
const updateFollowRelationship = (state: State, id: string, followState: string) => {
|
||||||
const map = followStateToRelationship(followState);
|
const relationship = state.get(id) || relationshipSchema.parse({ id });
|
||||||
return state.update(id, normalizeRelationship({}), relationship => relationship.merge(map));
|
|
||||||
|
return state.set(id, {
|
||||||
|
...relationship,
|
||||||
|
...followStateToRelationship(followState),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function relationships(state: State = ImmutableMap<string, Relationship>(), action: AnyAction) {
|
export default function relationships(state: State = ImmutableMap<string, Relationship>(), action: AnyAction) {
|
||||||
|
|
|
@ -74,11 +74,11 @@ const minifyStatus = (status: StatusRecord): ReducerStatus => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gets titles of poll options from status
|
// Gets titles of poll options from status
|
||||||
const getPollOptionTitles = ({ poll }: StatusRecord): ImmutableList<string> => {
|
const getPollOptionTitles = ({ poll }: StatusRecord): readonly string[] => {
|
||||||
if (poll && typeof poll === 'object') {
|
if (poll && typeof poll === 'object') {
|
||||||
return poll.options.map(({ title }) => title);
|
return poll.options.map(({ title }) => title);
|
||||||
} else {
|
} else {
|
||||||
return ImmutableList();
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { cardSchema } from '../card';
|
||||||
|
|
||||||
|
describe('cardSchema', () => {
|
||||||
|
it('adds base fields', () => {
|
||||||
|
const card = { url: 'https://soapbox.test' };
|
||||||
|
const result = cardSchema.parse(card);
|
||||||
|
|
||||||
|
expect(result.type).toEqual('link');
|
||||||
|
expect(result.url).toEqual(card.url);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { pollSchema } from '../poll';
|
||||||
|
|
||||||
|
describe('normalizePoll()', () => {
|
||||||
|
it('adds base fields', () => {
|
||||||
|
const poll = { id: '1', options: [{ title: 'Apples' }, { title: 'Oranges' }] };
|
||||||
|
const result = pollSchema.parse(poll);
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
options: [
|
||||||
|
{ title: 'Apples', votes_count: 0 },
|
||||||
|
{ title: 'Oranges', votes_count: 0 },
|
||||||
|
],
|
||||||
|
emojis: [],
|
||||||
|
expired: false,
|
||||||
|
multiple: false,
|
||||||
|
voters_count: 0,
|
||||||
|
votes_count: 0,
|
||||||
|
own_votes: null,
|
||||||
|
voted: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(result).toMatchObject(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes a Pleroma logged-out poll', () => {
|
||||||
|
const { poll } = require('soapbox/__fixtures__/pleroma-status-with-poll.json');
|
||||||
|
const result = pollSchema.parse(poll);
|
||||||
|
|
||||||
|
// Adds logged-in fields
|
||||||
|
expect(result.voted).toBe(false);
|
||||||
|
expect(result.own_votes).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes poll with emojis', () => {
|
||||||
|
const { poll } = require('soapbox/__fixtures__/pleroma-status-with-poll-with-emojis.json');
|
||||||
|
const result = pollSchema.parse(poll);
|
||||||
|
|
||||||
|
// Emojifies poll options
|
||||||
|
expect(result.options[1]?.title_emojified)
|
||||||
|
.toContain('emojione');
|
||||||
|
|
||||||
|
expect(result.emojis[1]?.shortcode).toEqual('soapbox');
|
||||||
|
});
|
||||||
|
});
|
|
@ -22,7 +22,7 @@ const accountSchema = z.object({
|
||||||
created_at: z.string().datetime().catch(new Date().toUTCString()),
|
created_at: z.string().datetime().catch(new Date().toUTCString()),
|
||||||
discoverable: z.boolean().catch(false),
|
discoverable: z.boolean().catch(false),
|
||||||
display_name: z.string().catch(''),
|
display_name: z.string().catch(''),
|
||||||
emojis: filteredArray(customEmojiSchema).catch([]),
|
emojis: filteredArray(customEmojiSchema),
|
||||||
favicon: z.string().catch(''),
|
favicon: z.string().catch(''),
|
||||||
fields: z.any(), // TODO
|
fields: z.any(), // TODO
|
||||||
followers_count: z.number().catch(0),
|
followers_count: z.number().catch(0),
|
||||||
|
@ -43,8 +43,8 @@ const accountSchema = z.object({
|
||||||
pleroma: z.any(), // TODO
|
pleroma: z.any(), // TODO
|
||||||
source: z.any(), // TODO
|
source: z.any(), // TODO
|
||||||
statuses_count: z.number().catch(0),
|
statuses_count: z.number().catch(0),
|
||||||
uri: z.string().catch(''),
|
uri: z.string().url().catch(''),
|
||||||
url: z.string().catch(''),
|
url: z.string().url().catch(''),
|
||||||
username: z.string().catch(''),
|
username: z.string().catch(''),
|
||||||
verified: z.boolean().default(false),
|
verified: z.boolean().default(false),
|
||||||
website: z.string().catch(''),
|
website: z.string().catch(''),
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import punycode from 'punycode';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { groupSchema } from './group';
|
||||||
|
|
||||||
|
const IDNA_PREFIX = 'xn--';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Card (aka link preview).
|
||||||
|
* https://docs.joinmastodon.org/entities/card/
|
||||||
|
*/
|
||||||
|
const cardSchema = z.object({
|
||||||
|
author_name: z.string().catch(''),
|
||||||
|
author_url: z.string().url().catch(''),
|
||||||
|
blurhash: z.string().nullable().catch(null),
|
||||||
|
description: z.string().catch(''),
|
||||||
|
embed_url: z.string().url().catch(''),
|
||||||
|
group: groupSchema.nullable().catch(null), // TruthSocial
|
||||||
|
height: z.number().catch(0),
|
||||||
|
html: z.string().catch(''),
|
||||||
|
image: z.string().nullable().catch(null),
|
||||||
|
pleroma: z.object({
|
||||||
|
opengraph: z.object({
|
||||||
|
width: z.number(),
|
||||||
|
height: z.number(),
|
||||||
|
html: z.string(),
|
||||||
|
thumbnail_url: z.string().url(),
|
||||||
|
}).optional().catch(undefined),
|
||||||
|
}).optional().catch(undefined),
|
||||||
|
provider_name: z.string().catch(''),
|
||||||
|
provider_url: z.string().url().catch(''),
|
||||||
|
title: z.string().catch(''),
|
||||||
|
type: z.enum(['link', 'photo', 'video', 'rich']).catch('link'),
|
||||||
|
url: z.string().url(),
|
||||||
|
width: z.number().catch(0),
|
||||||
|
}).transform(({ pleroma, ...card }) => {
|
||||||
|
if (!card.provider_name) {
|
||||||
|
card.provider_name = decodeIDNA(new URL(card.url).hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pleroma?.opengraph) {
|
||||||
|
if (!card.width && !card.height) {
|
||||||
|
card.width = pleroma.opengraph.width;
|
||||||
|
card.height = pleroma.opengraph.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!card.html) {
|
||||||
|
card.html = pleroma.opengraph.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!card.image) {
|
||||||
|
card.image = pleroma.opengraph.thumbnail_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
|
});
|
||||||
|
|
||||||
|
const decodeIDNA = (domain: string): string => {
|
||||||
|
return domain
|
||||||
|
.split('.')
|
||||||
|
.map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part)
|
||||||
|
.join('.');
|
||||||
|
};
|
||||||
|
|
||||||
|
type Card = z.infer<typeof cardSchema>;
|
||||||
|
|
||||||
|
export { cardSchema, type Card };
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/** Validates the string as an emoji. */
|
||||||
|
const emojiSchema = z.string().refine((v) => /\p{Extended_Pictographic}/u.test(v));
|
||||||
|
|
||||||
|
/** Pleroma emoji reaction. */
|
||||||
|
const emojiReactionSchema = z.object({
|
||||||
|
name: emojiSchema,
|
||||||
|
count: z.number().nullable().catch(null),
|
||||||
|
me: z.boolean().catch(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
type EmojiReaction = z.infer<typeof emojiReactionSchema>;
|
||||||
|
|
||||||
|
export { emojiReactionSchema, EmojiReaction };
|
|
@ -19,7 +19,7 @@ const groupSchema = z.object({
|
||||||
deleted_at: z.string().datetime().or(z.null()).catch(null),
|
deleted_at: z.string().datetime().or(z.null()).catch(null),
|
||||||
display_name: z.string().catch(''),
|
display_name: z.string().catch(''),
|
||||||
domain: z.string().catch(''),
|
domain: z.string().catch(''),
|
||||||
emojis: filteredArray(customEmojiSchema).catch([]),
|
emojis: filteredArray(customEmojiSchema),
|
||||||
group_visibility: z.string().catch(''), // TruthSocial
|
group_visibility: z.string().catch(''), // TruthSocial
|
||||||
header: z.string().catch(headerMissing),
|
header: z.string().catch(headerMissing),
|
||||||
header_static: z.string().catch(''),
|
header_static: z.string().catch(''),
|
||||||
|
|
|
@ -1,21 +1,14 @@
|
||||||
/**
|
export { accountSchema, type Account } from './account';
|
||||||
* Schemas
|
export { cardSchema, type Card } from './card';
|
||||||
*/
|
export { customEmojiSchema, type CustomEmoji } from './custom-emoji';
|
||||||
export { accountSchema } from './account';
|
export { emojiReactionSchema, type EmojiReaction } from './emoji-reaction';
|
||||||
export { customEmojiSchema } from './custom-emoji';
|
export { groupSchema, type Group } from './group';
|
||||||
export { groupSchema } from './group';
|
export { groupMemberSchema, type GroupMember } from './group-member';
|
||||||
export { groupMemberSchema } from './group-member';
|
export { groupRelationshipSchema, type GroupRelationship } from './group-relationship';
|
||||||
export { groupRelationshipSchema } from './group-relationship';
|
export { groupTagSchema, type GroupTag } from './group-tag';
|
||||||
export { groupTagSchema } from './group-tag';
|
export { pollSchema, type Poll, type PollOption } from './poll';
|
||||||
export { relationshipSchema } from './relationship';
|
export { relationshipSchema, type Relationship } from './relationship';
|
||||||
|
export { tagSchema, type Tag } from './tag';
|
||||||
|
|
||||||
/**
|
// Soapbox
|
||||||
* Entity Types
|
export { adSchema, type Ad } from './soapbox/ad';
|
||||||
*/
|
|
||||||
export type { Account } from './account';
|
|
||||||
export type { CustomEmoji } from './custom-emoji';
|
|
||||||
export type { Group } from './group';
|
|
||||||
export type { GroupMember } from './group-member';
|
|
||||||
export type { GroupRelationship } from './group-relationship';
|
|
||||||
export type { GroupTag } from './group-tag';
|
|
||||||
export type { Relationship } from './relationship';
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import emojify from 'soapbox/features/emoji';
|
||||||
|
|
||||||
|
import { customEmojiSchema } from './custom-emoji';
|
||||||
|
import { filteredArray, makeCustomEmojiMap } from './utils';
|
||||||
|
|
||||||
|
const pollOptionSchema = z.object({
|
||||||
|
title: z.string().catch(''),
|
||||||
|
votes_count: z.number().catch(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
const pollSchema = z.object({
|
||||||
|
emojis: filteredArray(customEmojiSchema),
|
||||||
|
expired: z.boolean().catch(false),
|
||||||
|
expires_at: z.string().datetime().catch(new Date().toUTCString()),
|
||||||
|
id: z.string(),
|
||||||
|
multiple: z.boolean().catch(false),
|
||||||
|
options: z.array(pollOptionSchema).min(2),
|
||||||
|
voters_count: z.number().catch(0),
|
||||||
|
votes_count: z.number().catch(0),
|
||||||
|
own_votes: z.array(z.number()).nonempty().nullable().catch(null),
|
||||||
|
voted: z.boolean().catch(false),
|
||||||
|
pleroma: z.object({
|
||||||
|
non_anonymous: z.boolean().catch(false),
|
||||||
|
}).optional().catch(undefined),
|
||||||
|
}).transform((poll) => {
|
||||||
|
const emojiMap = makeCustomEmojiMap(poll.emojis);
|
||||||
|
|
||||||
|
const emojifiedOptions = poll.options.map((option) => ({
|
||||||
|
...option,
|
||||||
|
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// If the user has votes, they have certainly voted.
|
||||||
|
if (poll.own_votes?.length) {
|
||||||
|
poll.voted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...poll,
|
||||||
|
options: emojifiedOptions,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
type Poll = z.infer<typeof pollSchema>;
|
||||||
|
type PollOption = Poll['options'][number];
|
||||||
|
|
||||||
|
export { pollSchema, type Poll, type PollOption };
|
|
@ -19,4 +19,4 @@ const relationshipSchema = z.object({
|
||||||
|
|
||||||
type Relationship = z.infer<typeof relationshipSchema>;
|
type Relationship = z.infer<typeof relationshipSchema>;
|
||||||
|
|
||||||
export { relationshipSchema, Relationship };
|
export { relationshipSchema, type Relationship };
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { cardSchema } from '../card';
|
||||||
|
|
||||||
|
const adSchema = z.object({
|
||||||
|
card: cardSchema,
|
||||||
|
impression: z.string().optional().catch(undefined),
|
||||||
|
expires_at: z.string().datetime().optional().catch(undefined),
|
||||||
|
reason: z.string().optional().catch(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Ad = z.infer<typeof adSchema>;
|
||||||
|
|
||||||
|
export { adSchema, type Ad };
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const historySchema = z.object({
|
||||||
|
accounts: z.coerce.number(),
|
||||||
|
uses: z.coerce.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** // https://docs.joinmastodon.org/entities/tag */
|
||||||
|
const tagSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
url: z.string().url().catch(''),
|
||||||
|
history: z.array(historySchema).nullable().catch(null),
|
||||||
|
following: z.boolean().catch(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Tag = z.infer<typeof tagSchema>;
|
||||||
|
|
||||||
|
export { tagSchema, type Tag };
|
|
@ -4,7 +4,7 @@ import type { CustomEmoji } from './custom-emoji';
|
||||||
|
|
||||||
/** Validates individual items in an array, dropping any that aren't valid. */
|
/** Validates individual items in an array, dropping any that aren't valid. */
|
||||||
function filteredArray<T extends z.ZodTypeAny>(schema: T) {
|
function filteredArray<T extends z.ZodTypeAny>(schema: T) {
|
||||||
return z.any().array()
|
return z.any().array().catch([])
|
||||||
.transform((arr) => (
|
.transform((arr) => (
|
||||||
arr.map((item) => {
|
arr.map((item) => {
|
||||||
const parsed = schema.safeParse(item);
|
const parsed = schema.safeParse(item);
|
||||||
|
|
|
@ -5,11 +5,9 @@ import {
|
||||||
AnnouncementRecord,
|
AnnouncementRecord,
|
||||||
AnnouncementReactionRecord,
|
AnnouncementReactionRecord,
|
||||||
AttachmentRecord,
|
AttachmentRecord,
|
||||||
CardRecord,
|
|
||||||
ChatRecord,
|
ChatRecord,
|
||||||
ChatMessageRecord,
|
ChatMessageRecord,
|
||||||
EmojiRecord,
|
EmojiRecord,
|
||||||
EmojiReactionRecord,
|
|
||||||
FieldRecord,
|
FieldRecord,
|
||||||
FilterRecord,
|
FilterRecord,
|
||||||
FilterKeywordRecord,
|
FilterKeywordRecord,
|
||||||
|
@ -20,9 +18,6 @@ import {
|
||||||
LocationRecord,
|
LocationRecord,
|
||||||
MentionRecord,
|
MentionRecord,
|
||||||
NotificationRecord,
|
NotificationRecord,
|
||||||
PollRecord,
|
|
||||||
PollOptionRecord,
|
|
||||||
RelationshipRecord,
|
|
||||||
StatusEditRecord,
|
StatusEditRecord,
|
||||||
StatusRecord,
|
StatusRecord,
|
||||||
TagRecord,
|
TagRecord,
|
||||||
|
@ -37,11 +32,9 @@ type AdminReport = ReturnType<typeof AdminReportRecord>;
|
||||||
type Announcement = ReturnType<typeof AnnouncementRecord>;
|
type Announcement = ReturnType<typeof AnnouncementRecord>;
|
||||||
type AnnouncementReaction = ReturnType<typeof AnnouncementReactionRecord>;
|
type AnnouncementReaction = ReturnType<typeof AnnouncementReactionRecord>;
|
||||||
type Attachment = ReturnType<typeof AttachmentRecord>;
|
type Attachment = ReturnType<typeof AttachmentRecord>;
|
||||||
type Card = ReturnType<typeof CardRecord>;
|
|
||||||
type Chat = ReturnType<typeof ChatRecord>;
|
type Chat = ReturnType<typeof ChatRecord>;
|
||||||
type ChatMessage = ReturnType<typeof ChatMessageRecord>;
|
type ChatMessage = ReturnType<typeof ChatMessageRecord>;
|
||||||
type Emoji = ReturnType<typeof EmojiRecord>;
|
type Emoji = ReturnType<typeof EmojiRecord>;
|
||||||
type EmojiReaction = ReturnType<typeof EmojiReactionRecord>;
|
|
||||||
type Field = ReturnType<typeof FieldRecord>;
|
type Field = ReturnType<typeof FieldRecord>;
|
||||||
type Filter = ReturnType<typeof FilterRecord>;
|
type Filter = ReturnType<typeof FilterRecord>;
|
||||||
type FilterKeyword = ReturnType<typeof FilterKeywordRecord>;
|
type FilterKeyword = ReturnType<typeof FilterKeywordRecord>;
|
||||||
|
@ -52,9 +45,6 @@ type List = ReturnType<typeof ListRecord>;
|
||||||
type Location = ReturnType<typeof LocationRecord>;
|
type Location = ReturnType<typeof LocationRecord>;
|
||||||
type Mention = ReturnType<typeof MentionRecord>;
|
type Mention = ReturnType<typeof MentionRecord>;
|
||||||
type Notification = ReturnType<typeof NotificationRecord>;
|
type Notification = ReturnType<typeof NotificationRecord>;
|
||||||
type Poll = ReturnType<typeof PollRecord>;
|
|
||||||
type PollOption = ReturnType<typeof PollOptionRecord>;
|
|
||||||
type Relationship = ReturnType<typeof RelationshipRecord>;
|
|
||||||
type StatusEdit = ReturnType<typeof StatusEditRecord>;
|
type StatusEdit = ReturnType<typeof StatusEditRecord>;
|
||||||
type Tag = ReturnType<typeof TagRecord>;
|
type Tag = ReturnType<typeof TagRecord>;
|
||||||
|
|
||||||
|
@ -82,11 +72,9 @@ export {
|
||||||
Announcement,
|
Announcement,
|
||||||
AnnouncementReaction,
|
AnnouncementReaction,
|
||||||
Attachment,
|
Attachment,
|
||||||
Card,
|
|
||||||
Chat,
|
Chat,
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
Emoji,
|
Emoji,
|
||||||
EmojiReaction,
|
|
||||||
Field,
|
Field,
|
||||||
Filter,
|
Filter,
|
||||||
FilterKeyword,
|
FilterKeyword,
|
||||||
|
@ -97,9 +85,6 @@ export {
|
||||||
Location,
|
Location,
|
||||||
Mention,
|
Mention,
|
||||||
Notification,
|
Notification,
|
||||||
Poll,
|
|
||||||
PollOption,
|
|
||||||
Relationship,
|
|
||||||
Status,
|
Status,
|
||||||
StatusEdit,
|
StatusEdit,
|
||||||
Tag,
|
Tag,
|
||||||
|
@ -110,7 +95,12 @@ export {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
|
Card,
|
||||||
|
EmojiReaction,
|
||||||
Group,
|
Group,
|
||||||
GroupMember,
|
GroupMember,
|
||||||
GroupRelationship,
|
GroupRelationship,
|
||||||
|
Poll,
|
||||||
|
PollOption,
|
||||||
|
Relationship,
|
||||||
} from 'soapbox/schemas';
|
} from 'soapbox/schemas';
|
|
@ -1,4 +1,3 @@
|
||||||
import { AdRecord } from 'soapbox/normalizers/soapbox/ad';
|
|
||||||
import {
|
import {
|
||||||
PromoPanelItemRecord,
|
PromoPanelItemRecord,
|
||||||
FooterItemRecord,
|
FooterItemRecord,
|
||||||
|
@ -8,7 +7,6 @@ import {
|
||||||
|
|
||||||
type Me = string | null | false | undefined;
|
type Me = string | null | false | undefined;
|
||||||
|
|
||||||
type Ad = ReturnType<typeof AdRecord>;
|
|
||||||
type PromoPanelItem = ReturnType<typeof PromoPanelItemRecord>;
|
type PromoPanelItem = ReturnType<typeof PromoPanelItemRecord>;
|
||||||
type FooterItem = ReturnType<typeof FooterItemRecord>;
|
type FooterItem = ReturnType<typeof FooterItemRecord>;
|
||||||
type CryptoAddress = ReturnType<typeof CryptoAddressRecord>;
|
type CryptoAddress = ReturnType<typeof CryptoAddressRecord>;
|
||||||
|
@ -16,9 +14,12 @@ type SoapboxConfig = ReturnType<typeof SoapboxConfigRecord>;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Me,
|
Me,
|
||||||
Ad,
|
|
||||||
PromoPanelItem,
|
PromoPanelItem,
|
||||||
FooterItem,
|
FooterItem,
|
||||||
CryptoAddress,
|
CryptoAddress,
|
||||||
SoapboxConfig,
|
SoapboxConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type {
|
||||||
|
Ad,
|
||||||
|
} from 'soapbox/schemas';
|
|
@ -1,4 +1,4 @@
|
||||||
import { normalizeAd } from 'soapbox/normalizers';
|
import { buildAd } from 'soapbox/jest/factory';
|
||||||
|
|
||||||
import { isExpired } from '../ads';
|
import { isExpired } from '../ads';
|
||||||
|
|
||||||
|
@ -14,10 +14,10 @@ test('isExpired()', () => {
|
||||||
const epoch = now.getTime();
|
const epoch = now.getTime();
|
||||||
|
|
||||||
// Sanity tests.
|
// Sanity tests.
|
||||||
expect(isExpired(normalizeAd({ expires_at: iso }))).toBe(true);
|
expect(isExpired(buildAd({ expires_at: iso }))).toBe(true);
|
||||||
expect(isExpired(normalizeAd({ expires_at: new Date(epoch + 999999).toISOString() }))).toBe(false);
|
expect(isExpired(buildAd({ expires_at: new Date(epoch + 999999).toISOString() }))).toBe(false);
|
||||||
|
|
||||||
// Testing the 5-minute mark.
|
// Testing the 5-minute mark.
|
||||||
expect(isExpired(normalizeAd({ expires_at: new Date(epoch + threeMins).toISOString() }), fiveMins)).toBe(true);
|
expect(isExpired(buildAd({ expires_at: new Date(epoch + threeMins).toISOString() }), fiveMins)).toBe(true);
|
||||||
expect(isExpired(normalizeAd({ expires_at: new Date(epoch + fiveMins + 1000).toISOString() }), fiveMins)).toBe(false);
|
expect(isExpired(buildAd({ expires_at: new Date(epoch + fiveMins + 1000).toISOString() }), fiveMins)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Ad } from 'soapbox/types/soapbox';
|
import type { Ad } from 'soapbox/schemas';
|
||||||
|
|
||||||
/** Time (ms) window to not display an ad if it's about to expire. */
|
/** Time (ms) window to not display an ad if it's about to expire. */
|
||||||
const AD_EXPIRY_THRESHOLD = 5 * 60 * 1000;
|
const AD_EXPIRY_THRESHOLD = 5 * 60 * 1000;
|
||||||
|
|
|
@ -261,7 +261,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
/**
|
/**
|
||||||
* Ability to add reactions to chat messages.
|
* Ability to add reactions to chat messages.
|
||||||
*/
|
*/
|
||||||
chatEmojiReactions: v.software === TRUTHSOCIAL && v.build === UNRELEASED,
|
chatEmojiReactions: v.software === TRUTHSOCIAL,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pleroma chats API.
|
* Pleroma chats API.
|
||||||
|
|
Loading…
Reference in New Issue