pl-fe: wip valibot migration

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2024-10-16 17:30:05 +02:00
parent a521c9044d
commit 905e1626a4
29 changed files with 75 additions and 63 deletions

View File

@ -1,10 +1,10 @@
import * as v from 'valibot';
const historySchema = v.object({
const historySchema = v.array(v.object({
day: v.pipe(v.unknown(), v.transform(Number)),
accounts: v.pipe(v.unknown(), v.transform(Number)),
uses: v.pipe(v.unknown(), v.transform(Number)),
});
}));
/** @see {@link https://docs.joinmastodon.org/entities/tag} */
const tagSchema = v.object({

View File

@ -8,6 +8,7 @@
*/
import { credentialAccountSchema, PlApiClient, type CreateAccountParams, type Token } from 'pl-api';
import { defineMessages } from 'react-intl';
import * as v from 'valibot';
import { createAccount } from 'pl-fe/actions/accounts';
import { createApp } from 'pl-fe/actions/apps';
@ -157,7 +158,7 @@ const verifyCredentials = (token: string, accountUrl?: string) =>
if (error?.response?.status === 403 && error?.response?.json?.id) {
// The user is waitlisted
const account = error.response.json;
const parsedAccount = credentialAccountSchema.parse(error.response.json);
const parsedAccount = v.parse(credentialAccountSchema, error.response.json);
dispatch(importFetchedAccount(parsedAccount));
dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account: parsedAccount });
if (account.id === getState().me) dispatch(fetchMeSuccess(parsedAccount));

View File

@ -7,6 +7,7 @@
*/
import { instanceSchema, PlApiClient, type Instance } from 'pl-api';
import * as v from 'valibot';
import { createApp } from 'pl-fe/actions/apps';
import { authLoggedIn, verifyCredentials, switchAccount } from 'pl-fe/actions/auth';
@ -24,7 +25,7 @@ const fetchExternalInstance = (baseURL: string) =>
if (error.response?.status === 401) {
// Authenticated fetch is enabled.
// Continue with a limited featureset.
return instanceSchema.parse({});
return v.parse(instanceSchema, {});
} else {
throw error;
}

View File

@ -5,6 +5,7 @@ import {
type AdminCreateAnnouncementParams,
type AdminUpdateAnnouncementParams,
} from 'pl-api';
import * as v from 'valibot';
import { useClient } from 'pl-fe/hooks';
import { normalizeAnnouncement, AdminAnnouncement } from 'pl-fe/normalizers';
@ -36,7 +37,7 @@ const useAnnouncements = () => {
retry: false,
onSuccess: (data) =>
queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
[...prevResult, adminAnnouncementSchema.parse(data)],
[...prevResult, v.parse(adminAnnouncementSchema, data)],
),
onSettled: () => userAnnouncements.refetch(),
});
@ -50,7 +51,7 @@ const useAnnouncements = () => {
retry: false,
onSuccess: (data) =>
queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
prevResult.map((announcement) => announcement.id === data.id ? adminAnnouncementSchema.parse(data) : announcement),
prevResult.map((announcement) => announcement.id === data.id ? v.parse(adminAnnouncementSchema, data) : announcement),
),
onSettled: () => userAnnouncements.refetch(),
});

View File

@ -1,11 +1,12 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { announcementReactionSchema, type AnnouncementReaction } from 'pl-api';
import * as v from 'valibot';
import { useClient } from 'pl-fe/hooks';
import { type Announcement, normalizeAnnouncement } from 'pl-fe/normalizers';
import { queryClient } from 'pl-fe/queries/client';
const updateReaction = (reaction: AnnouncementReaction, count: number, me?: boolean, overwrite?: boolean) => announcementReactionSchema.parse({
const updateReaction = (reaction: AnnouncementReaction, count: number, me?: boolean, overwrite?: boolean) => v.parse(announcementReactionSchema, {
...reaction,
me: typeof me === 'boolean' ? me : reaction.me,
count: overwrite ? count : (reaction.count + count),
@ -18,7 +19,7 @@ const updateReactions = (reactions: AnnouncementReaction[], name: string, count:
reactions = reactions.map(reaction => reaction.name === name ? updateReaction(reaction, count, me, overwrite) : reaction);
}
return [...reactions, updateReaction(announcementReactionSchema.parse({ name }), count, me, overwrite)];
return [...reactions, updateReaction(v.parse(announcementReactionSchema, { name }), count, me, overwrite)];
};
const useAnnouncements = () => {

View File

@ -1,4 +1,5 @@
import { instanceSchema } from 'pl-api';
import * as v from 'valibot';
import { __stub } from 'pl-fe/api';
import { buildGroup } from 'pl-fe/jest/factory';
@ -8,7 +9,7 @@ import { useGroups } from './useGroups';
const group = buildGroup({ id: '1', display_name: 'soapbox' });
const store = {
instance: instanceSchema.parse({
instance: v.parse(instanceSchema, {
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
}),
};

View File

@ -1,10 +1,11 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { type InteractionPolicies, interactionPoliciesSchema } from 'pl-api';
import * as v from 'valibot';
import { useClient, useFeatures, useLoggedIn } from 'pl-fe/hooks';
import { queryClient } from 'pl-fe/queries/client';
const emptySchema = interactionPoliciesSchema.parse({});
const emptySchema = v.parse(interactionPoliciesSchema, {});
const useInteractionPolicies = () => {
const client = useClient();

View File

@ -1,6 +1,7 @@
import clsx from 'clsx';
import { type MediaAttachment, type PreviewCard as CardEntity, mediaAttachmentSchema } from 'pl-api';
import React, { useState, useEffect } from 'react';
import * as v from 'valibot';
import Blurhash from 'pl-fe/components/blurhash';
import { HStack, Stack, Text, Icon } from 'pl-fe/components/ui';
@ -43,7 +44,7 @@ const PreviewCard: React.FC<IPreviewCard> = ({
const trimmedDescription = trim(card.description, maxDescription);
const handlePhotoClick = () => {
const attachment = mediaAttachmentSchema.parse({
const attachment = v.parse(mediaAttachmentSchema, {
id: '',
type: 'image',
url: card.embed_url,

View File

@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import * as v from 'valibot';
import { z } from 'zod';
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';

View File

@ -3,6 +3,7 @@ import { GOTOSOCIAL, MASTODON, mediaAttachmentSchema } from 'pl-api';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import * as v from 'valibot';
import { biteAccount, blockAccount, pinAccount, removeFromFollowers, unblockAccount, unmuteAccount, unpinAccount } from 'pl-fe/actions/accounts';
import { mentionCompose, directCompose } from 'pl-fe/actions/compose';
@ -240,7 +241,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
};
const onAvatarClick = () => {
const avatar = mediaAttachmentSchema.parse({
const avatar = v.parse(mediaAttachmentSchema, {
id: '',
type: 'image',
url: account.avatar,
@ -256,7 +257,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
};
const onHeaderClick = () => {
const header = mediaAttachmentSchema.parse({
const header = v.parse(mediaAttachmentSchema, {
type: 'image',
url: account.header,
});

View File

@ -1,5 +1,6 @@
import { instanceSchema } from 'pl-api';
import React from 'react';
import * as v from 'valibot';
import { fireEvent, render, screen } from 'pl-fe/jest/test-helpers';
@ -9,7 +10,7 @@ describe('<LoginForm />', () => {
it('renders for Pleroma', () => {
const mockFn = vi.fn();
const store = {
instance: instanceSchema.parse({
instance: v.parse(instanceSchema, {
version: '2.7.2 (compatible; Pleroma 2.3.0)',
}),
};
@ -22,7 +23,7 @@ describe('<LoginForm />', () => {
it('renders for Mastodon', () => {
const mockFn = vi.fn();
const store = {
instance: instanceSchema.parse({
instance: v.parse(instanceSchema, {
version: '3.0.0',
}),
};

View File

@ -1,5 +1,6 @@
import { instanceSchema } from 'pl-api';
import React from 'react';
import * as v from 'valibot';
import { render, screen } from 'pl-fe/jest/test-helpers';
@ -8,7 +9,7 @@ import LoginPage from './login-page';
describe('<LoginPage />', () => {
it('renders correctly on load', () => {
const store = {
instance: instanceSchema.parse({
instance: v.parse(instanceSchema, {
version: '2.7.2 (compatible; Pleroma 2.3.0)',
}),
};

View File

@ -26,6 +26,7 @@ import { mediaAttachmentSchema } from 'pl-api';
import * as React from 'react';
import { Suspense, useCallback, useEffect, useRef, useState } from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import * as v from 'valibot';
import { HStack, Icon, IconButton } from 'pl-fe/components/ui';
import { useSettings } from 'pl-fe/hooks';
@ -122,7 +123,7 @@ const ImageComponent = ({
);
const previewImage = () => {
const image = mediaAttachmentSchema.parse({
const image = v.parse(mediaAttachmentSchema, {
id: '',
type: 'image',
url: src,

View File

@ -1,4 +1,5 @@
import { statusSchema } from 'pl-api';
import * as v from 'valibot';
import { Entities } from 'pl-fe/entity-store/entities';
import { normalizeStatus } from 'pl-fe/normalizers';
@ -22,7 +23,7 @@ const buildStatus = (state: RootState, draftStatus: DraftStatus) => {
const me = state.me as string;
const account = state.entities[Entities.ACCOUNTS]?.store[me];
const status = statusSchema.parse({
const status = v.parse(statusSchema, {
id: 'draft',
account,
content: draftStatus.text.replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */

View File

@ -1,6 +1,7 @@
import { mediaAttachmentSchema } from 'pl-api';
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import * as v from 'valibot';
import GroupAvatar from 'pl-fe/components/groups/group-avatar';
import { ParsedContent } from 'pl-fe/components/parsed-content';
@ -52,7 +53,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
}
const onAvatarClick = () => {
const avatar = mediaAttachmentSchema.parse({
const avatar = v.parse(mediaAttachmentSchema, {
id: '',
type: 'image',
url: group.avatar,
@ -68,7 +69,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
};
const onHeaderClick = () => {
const header = mediaAttachmentSchema.parse({
const header = v.parse(mediaAttachmentSchema, {
id: '',
type: 'image',
url: group.header,

View File

@ -1,4 +1,5 @@
import { statusSchema, type ScheduledStatus } from 'pl-api';
import * as v from 'valibot';
import { Entities } from 'pl-fe/entity-store/entities';
import { normalizeStatus } from 'pl-fe/normalizers/status';
@ -9,7 +10,7 @@ const buildStatus = (state: RootState, scheduledStatus: ScheduledStatus) => {
const me = state.me as string;
const account = state.entities[Entities.ACCOUNTS]?.store[me];
const status = statusSchema.parse({
const status = v.parse(statusSchema, {
account,
content: scheduledStatus.params.text?.replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */
created_at: scheduledStatus.params.scheduled_at,

View File

@ -29,7 +29,6 @@ const UploadButton: React.FC<IUploadButton> = ({ disabled, onSelectFile }) => {
fileElement.current?.click();
};
return (
<HStack className='size-full cursor-pointer text-primary-500 dark:text-accent-blue' space={3} alignItems='center' justifyContent='center' element='label'>
<Icon

View File

@ -157,7 +157,6 @@ const GlobalHotkeys: React.FC<IGlobalHotkeys> = ({ children, node }) => {
return handlers;
}, [account?.id]);
return (
<HotKeys keyMap={keyMap} handlers={handlers} ref={setHotkeysRef} attach={window} focused>
{children}

View File

@ -1,5 +1,6 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { statusSchema } from 'pl-api';
import * as v from 'valibot';
import { normalizeStatus } from 'pl-fe/normalizers/status';
import { makeGetAccount } from 'pl-fe/selectors';
@ -46,7 +47,7 @@ const buildStatus = (state: RootState, pendingStatus: PendingStatus, idempotency
visibility: pendingStatus.visibility,
};
return normalizeStatus(statusSchema.parse(status));
return normalizeStatus(v.parse(statusSchema, status));
};
export { buildStatus };

View File

@ -17,6 +17,7 @@ import {
type Relationship,
type Status,
} from 'pl-api';
import * as v from 'valibot';
import type { PartialDeep } from 'type-fest';
@ -24,18 +25,18 @@ import type { PartialDeep } from 'type-fest';
// This looks promising but didn't work on my first attempt: https://github.com/anatine/zod-plugins/tree/main/packages/zod-mock
const buildAccount = (props: PartialDeep<Account> = {}): Account =>
accountSchema.parse(Object.assign({
v.parse(accountSchema, Object.assign({
id: crypto.randomUUID(),
url: `https://soapbox.test/users/${crypto.randomUUID()}`,
}, props));
const buildCard = (props: PartialDeep<PreviewCard> = {}): PreviewCard =>
previewCardSchema.parse(Object.assign({
v.parse(previewCardSchema, Object.assign({
url: 'https://soapbox.test',
}, props));
const buildGroup = (props: PartialDeep<Group> = {}): Group =>
groupSchema.parse(Object.assign({
v.parse(groupSchema, Object.assign({
id: crypto.randomUUID(),
owner: {
id: crypto.randomUUID(),
@ -43,28 +44,28 @@ const buildGroup = (props: PartialDeep<Group> = {}): Group =>
}, props));
const buildGroupRelationship = (props: PartialDeep<GroupRelationship> = {}): GroupRelationship =>
groupRelationshipSchema.parse(Object.assign({
v.parse(groupRelationshipSchema, Object.assign({
id: crypto.randomUUID(),
}, props));
const buildGroupMember = (
props: PartialDeep<GroupMember> = {},
accountProps: PartialDeep<Account> = {},
): GroupMember => groupMemberSchema.parse(Object.assign({
): GroupMember => v.parse(groupMemberSchema, Object.assign({
id: crypto.randomUUID(),
account: buildAccount(accountProps),
role: GroupRoles.USER,
}, props));
const buildInstance = (props: PartialDeep<Instance> = {}) => instanceSchema.parse(props);
const buildInstance = (props: PartialDeep<Instance> = {}) => v.parse(instanceSchema, props);
const buildRelationship = (props: PartialDeep<Relationship> = {}): Relationship =>
relationshipSchema.parse(Object.assign({
v.parse(relationshipSchema, Object.assign({
id: crypto.randomUUID(),
}, props));
const buildStatus = (props: PartialDeep<Status> = {}) =>
statusSchema.parse(Object.assign({
v.parse(statusSchema, Object.assign({
id: crypto.randomUUID(),
account: buildAccount(),
}, props));

View File

@ -1,14 +1,15 @@
import { instanceSchema } from 'pl-api';
import * as v from 'valibot';
import alexJson from 'pl-fe/__fixtures__/pleroma-account.json';
import { buildAccount } from './factory';
/** Store with registrations open. */
const storeOpen = { instance: instanceSchema.parse({ registrations: true }) };
const storeOpen = { instance: v.parse(instanceSchema, { registrations: true }) };
/** Store with registrations closed. */
const storeClosed = { instance: instanceSchema.parse({ registrations: false }) };
const storeClosed = { instance: v.parse(instanceSchema, { registrations: false }) };
/** Store with a logged-in user. */
const storeLoggedIn = {

View File

@ -1,7 +1,6 @@
import escapeTextContentForBrowser from 'escape-html';
import DOMPurify from 'isomorphic-dompurify';
import emojify from 'pl-fe/features/emoji';
import { makeEmojiMap } from 'pl-fe/utils/normalizers';

View File

@ -6,6 +6,7 @@
import escapeTextContentForBrowser from 'escape-html';
import DOMPurify from 'isomorphic-dompurify';
import { type Account as BaseAccount, type Status as BaseStatus, type CustomEmoji, type MediaAttachment, mentionSchema, type Translation } from 'pl-api';
import * as v from 'valibot';
import emojify from 'pl-fe/features/emoji';
import { unescapeHTML } from 'pl-fe/utils/html';
@ -111,7 +112,7 @@ const normalizeStatus = (status: BaseStatus & {
const hasSelfMention = status.mentions.some(mention => status.account.id === mention.id);
if (isSelfReply && !hasSelfMention) {
const selfMention = mentionSchema.parse(status.account);
const selfMention = v.parse(mentionSchema, status.account);
mentions = [selfMention, ...mentions];
}

View File

@ -1,6 +1,7 @@
import { InfiniteData, keepPreviousData, useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query';
import sumBy from 'lodash/sumBy';
import { type Chat, type ChatMessage as BaseChatMessage, type PaginatedResponse, chatMessageSchema, type Relationship } from 'pl-api';
import * as v from 'valibot';
import { importFetchedAccount, importFetchedAccounts } from 'pl-fe/actions/importer';
import { ChatWidgetScreens, useChatContext } from 'pl-fe/contexts/chat-context';
@ -171,7 +172,7 @@ const useChatActions = (chatId: string) => {
...page,
items: [
normalizeChatMessage({
...chatMessageSchema.parse({
...v.parse(chatMessageSchema, {
chat_id: variables.chatId,
content: variables.content,
id: pendingId,

View File

@ -4,11 +4,11 @@
*/
import { produce } from 'immer';
import { Account, accountSchema } from 'pl-api';
import { VERIFY_CREDENTIALS_SUCCESS, AUTH_ACCOUNT_REMEMBER_SUCCESS } from 'pl-fe/actions/auth';
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'pl-fe/actions/me';
import type { Account, CredentialAccount } from 'pl-api';
import type { AnyAction } from 'redux';
interface AccountMeta {
@ -18,16 +18,8 @@ interface AccountMeta {
type State = Record<string, AccountMeta | undefined>;
const importAccount = (state: State, data: unknown): State => {
const result = accountSchema.safeParse(data);
if (!result.success) {
return state;
}
const account = result.data;
return produce(state, draft => {
const importAccount = (state: State, account: CredentialAccount): State =>
produce(state, draft => {
const existing = draft[account.id];
draft[account.id] = {
@ -35,7 +27,6 @@ const importAccount = (state: State, data: unknown): State => {
source: account.__meta.source ?? existing?.source,
};
});
};
const accounts_meta = (state: Readonly<State> = {}, action: AnyAction): State => {
switch (action.type) {

View File

@ -1,6 +1,7 @@
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
import trim from 'lodash/trim';
import { applicationSchema, PlApiClient, tokenSchema, type Application, type CredentialAccount, type Token } from 'pl-api';
import * as v from 'valibot';
import { MASTODON_PRELOAD_IMPORT } from 'pl-fe/actions/preload';
import * as BuildConfig from 'pl-fe/build-config';
@ -60,8 +61,8 @@ const getLocalState = () => {
if (!state) return undefined;
return ReducerRecord({
app: state.app && applicationSchema.parse(state.app),
tokens: ImmutableMap(Object.entries(state.tokens).map(([key, value]) => [key, tokenSchema.parse(value)])),
app: state.app && v.parse(applicationSchema, state.app),
tokens: ImmutableMap(Object.entries(state.tokens).map(([key, value]) => [key, v.parse(tokenSchema, value)])),
users: ImmutableMap(Object.entries(state.users).map(([key, value]) => [key, AuthUserRecord(value as any)])),
me: state.me,
client: new PlApiClient(parseBaseURL(state.me) || backendUrl, state.users[state.me]?.access_token),
@ -237,7 +238,7 @@ const importMastodonPreload = (state: State, data: ImmutableMap<string, any>) =>
const accessToken = data.getIn(['meta', 'access_token']) as string;
if (validId(accessToken) && validId(accountId) && isURL(accountUrl)) {
state.setIn(['tokens', accessToken], tokenSchema.parse({
state.setIn(['tokens', accessToken], v.parse(tokenSchema, {
access_token: accessToken,
account: accountId,
me: accountUrl,

View File

@ -1,6 +1,7 @@
import { produce } from 'immer';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { type Instance, instanceSchema } from 'pl-api';
import * as v from 'valibot';
import { ADMIN_CONFIG_UPDATE_REQUEST, ADMIN_CONFIG_UPDATE_SUCCESS } from 'pl-fe/actions/admin';
import { INSTANCE_FETCH_FAIL, INSTANCE_FETCH_SUCCESS, InstanceAction } from 'pl-fe/actions/instance';
@ -10,11 +11,11 @@ import ConfigDB from 'pl-fe/utils/config-db';
import type { AnyAction } from 'redux';
const initialState: Instance = instanceSchema.parse({});
const initialState: Instance = v.parse(instanceSchema, {});
const preloadImport = (state: Instance, action: Record<string, any>, path: string) => {
const instance = action.data[path];
return instance ? instanceSchema.parse(instance) : state;
return instance ? v.parse(instanceSchema, instance) : state;
};
const getConfigValue = (instanceConfig: ImmutableMap<string, any>, key: string) => {

View File

@ -1,5 +1,6 @@
import { List as ImmutableList, fromJS } from 'immutable';
import { emojiReactionSchema } from 'pl-api';
import * as v from 'valibot';
import {
simulateEmojiReact,
@ -11,7 +12,7 @@ describe('simulateEmojiReact', () => {
const emojiReacts = ImmutableList([
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
].map((react) => emojiReactionSchema.parse(react)));
].map((react) => v.parse(emojiReactionSchema, react)));
expect(simulateEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
{ 'count': 3, 'me': true, 'name': '❤', 'url': undefined },
@ -22,7 +23,7 @@ describe('simulateEmojiReact', () => {
const emojiReacts = ImmutableList([
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
].map((react) => emojiReactionSchema.parse(react)));
].map((react) => v.parse(emojiReactionSchema, react)));
expect(simulateEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
@ -34,7 +35,7 @@ describe('simulateEmojiReact', () => {
const emojiReacts = ImmutableList([
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
].map((react) => emojiReactionSchema.parse(react)));
].map((react) => v.parse(emojiReactionSchema, react)));
expect(simulateEmojiReact(emojiReacts, 'soapbox', 'https://gleasonator.com/emoji/Gleasonator/soapbox.png')).toEqual(fromJS([
{ 'count': 2, 'me': false, 'name': '👍', 'url': undefined },
{ 'count': 2, 'me': false, 'name': '❤', 'url': undefined },
@ -48,7 +49,7 @@ describe('simulateUnEmojiReact', () => {
const emojiReacts = ImmutableList([
{ 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 3, 'me': true, 'name': '❤' },
].map((react) => emojiReactionSchema.parse(react)));
].map((react) => v.parse(emojiReactionSchema, react)));
expect(simulateUnEmojiReact(emojiReacts, '❤')).toEqual(fromJS([
{ 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' },
@ -60,7 +61,7 @@ describe('simulateUnEmojiReact', () => {
{ 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' },
{ 'count': 1, 'me': true, 'name': '😯' },
].map((react) => emojiReactionSchema.parse(react)));
].map((react) => v.parse(emojiReactionSchema, react)));
expect(simulateUnEmojiReact(emojiReacts, '😯')).toEqual(fromJS([
{ 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' },
@ -72,7 +73,7 @@ describe('simulateUnEmojiReact', () => {
{ 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' },
{ 'count': 1, 'me': true, 'name': 'soapbox', 'url': 'https://gleasonator.com/emoji/Gleasonator/soapbox.png' },
].map((react) => emojiReactionSchema.parse(react)));
].map((react) => v.parse(emojiReactionSchema, react)));
expect(simulateUnEmojiReact(emojiReacts, 'soapbox')).toEqual(fromJS([
{ 'count': 2, 'me': false, 'name': '👍' },
{ 'count': 2, 'me': false, 'name': '❤' },

View File

@ -1,18 +1,19 @@
import { emojiReactionSchema, type EmojiReaction } from 'pl-api';
import * as v from 'valibot';
const simulateEmojiReact = (emojiReacts: Array<EmojiReaction>, emoji: string, url?: string) => {
const idx = emojiReacts.findIndex(e => e.name === emoji);
const emojiReact = emojiReacts[idx];
if (idx > -1 && emojiReact) {
return emojiReacts.map((reaction, id) => id === idx ? emojiReactionSchema.parse({
return emojiReacts.map((reaction, id) => id === idx ? v.parse(emojiReactionSchema, {
...emojiReact,
count: (emojiReact.count || 0) + 1,
me: true,
url,
}) : reaction);
} else {
return [...emojiReacts, emojiReactionSchema.parse({
return [...emojiReacts, v.parse(emojiReactionSchema, {
count: 1,
me: true,
name: emoji,
@ -30,7 +31,7 @@ const simulateUnEmojiReact = (emojiReacts: Array<EmojiReaction>, emoji: string)
if (emojiReact) {
const newCount = (emojiReact.count || 1) - 1;
if (newCount < 1) return emojiReacts.filter((_, id) => id !== idx);
return emojiReacts.map((reaction, id) => id === idx ? emojiReactionSchema.parse({
return emojiReacts.map((reaction, id) => id === idx ? v.parse(emojiReactionSchema, {
...emojiReact,
count: (emojiReact.count || 1) - 1,
me: false,