Merge branch 'zod-notification' into 'develop'
zod: Notification, Attachment, ChatMessage, Status See merge request soapbox-pub/soapbox!2500
This commit is contained in:
commit
c5c2378542
|
@ -1,7 +1,10 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useApi } from 'soapbox/hooks/useApi';
|
||||
import { statusSchema } from 'soapbox/schemas/status';
|
||||
import { normalizeStatus } from 'soapbox/normalizers';
|
||||
import { toSchema } from 'soapbox/utils/normalizers';
|
||||
|
||||
const statusSchema = toSchema(normalizeStatus);
|
||||
|
||||
function useGroupMedia(groupId: string) {
|
||||
const api = useApi();
|
||||
|
|
|
@ -5,7 +5,7 @@ import emojify from 'soapbox/features/emoji';
|
|||
|
||||
import { customEmojiSchema } from './custom-emoji';
|
||||
import { relationshipSchema } from './relationship';
|
||||
import { filteredArray, makeCustomEmojiMap } from './utils';
|
||||
import { contentSchema, filteredArray, makeCustomEmojiMap } from './utils';
|
||||
|
||||
const avatarMissing = require('assets/images/avatar-missing.png');
|
||||
const headerMissing = require('assets/images/header-missing.png');
|
||||
|
@ -39,7 +39,7 @@ const accountSchema = z.object({
|
|||
z.string(),
|
||||
z.null(),
|
||||
]).catch(null),
|
||||
note: z.string().catch(''),
|
||||
note: contentSchema,
|
||||
pleroma: z.any(), // TODO
|
||||
source: z.any(), // TODO
|
||||
statuses_count: z.number().catch(0),
|
||||
|
@ -121,4 +121,4 @@ const accountSchema = z.object({
|
|||
|
||||
type Account = z.infer<typeof accountSchema>;
|
||||
|
||||
export { accountSchema, Account };
|
||||
export { accountSchema, type Account };
|
|
@ -0,0 +1,89 @@
|
|||
import { isBlurhashValid } from 'blurhash';
|
||||
import { z } from 'zod';
|
||||
|
||||
const blurhashSchema = z.string().superRefine((value, ctx) => {
|
||||
const r = isBlurhashValid(value);
|
||||
|
||||
if (!r.result) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: r.errorReason,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const baseAttachmentSchema = z.object({
|
||||
blurhash: blurhashSchema.nullable().catch(null),
|
||||
description: z.string().catch(''),
|
||||
external_video_id: z.string().optional().catch(undefined), // TruthSocial
|
||||
id: z.string(),
|
||||
pleroma: z.object({
|
||||
mime_type: z.string().regex(/^\w+\/[-+.\w]+$/),
|
||||
}).optional().catch(undefined),
|
||||
preview_url: z.string().url().catch(''),
|
||||
remote_url: z.string().url().nullable().catch(null),
|
||||
type: z.string(),
|
||||
url: z.string().url(),
|
||||
});
|
||||
|
||||
const imageMetaSchema = z.object({
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
aspect: z.number().optional().catch(undefined),
|
||||
}).transform((meta) => ({
|
||||
...meta,
|
||||
aspect: typeof meta.aspect === 'number' ? meta.aspect : meta.width / meta.height,
|
||||
}));
|
||||
|
||||
const imageAttachmentSchema = baseAttachmentSchema.extend({
|
||||
type: z.literal('image'),
|
||||
meta: z.object({
|
||||
original: imageMetaSchema.optional().catch(undefined),
|
||||
}).catch({}),
|
||||
});
|
||||
|
||||
const videoAttachmentSchema = baseAttachmentSchema.extend({
|
||||
type: z.literal('video'),
|
||||
meta: z.object({
|
||||
duration: z.number().optional().catch(undefined),
|
||||
original: imageMetaSchema.optional().catch(undefined),
|
||||
}).catch({}),
|
||||
});
|
||||
|
||||
const gifvAttachmentSchema = baseAttachmentSchema.extend({
|
||||
type: z.literal('gifv'),
|
||||
meta: z.object({
|
||||
duration: z.number().optional().catch(undefined),
|
||||
original: imageMetaSchema.optional().catch(undefined),
|
||||
}).catch({}),
|
||||
});
|
||||
|
||||
const audioAttachmentSchema = baseAttachmentSchema.extend({
|
||||
type: z.literal('audio'),
|
||||
meta: z.object({
|
||||
duration: z.number().optional().catch(undefined),
|
||||
}).catch({}),
|
||||
});
|
||||
|
||||
const unknownAttachmentSchema = baseAttachmentSchema.extend({
|
||||
type: z.literal('unknown'),
|
||||
});
|
||||
|
||||
/** https://docs.joinmastodon.org/entities/attachment */
|
||||
const attachmentSchema = z.discriminatedUnion('type', [
|
||||
imageAttachmentSchema,
|
||||
videoAttachmentSchema,
|
||||
gifvAttachmentSchema,
|
||||
audioAttachmentSchema,
|
||||
unknownAttachmentSchema,
|
||||
]).transform((attachment) => {
|
||||
if (!attachment.preview_url) {
|
||||
attachment.preview_url = attachment.url;
|
||||
}
|
||||
|
||||
return attachment;
|
||||
});
|
||||
|
||||
type Attachment = z.infer<typeof attachmentSchema>;
|
||||
|
||||
export { attachmentSchema, type Attachment };
|
|
@ -0,0 +1,26 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { attachmentSchema } from './attachment';
|
||||
import { cardSchema } from './card';
|
||||
import { customEmojiSchema } from './custom-emoji';
|
||||
import { contentSchema, emojiSchema, filteredArray } from './utils';
|
||||
|
||||
const chatMessageSchema = z.object({
|
||||
account_id: z.string(),
|
||||
media_attachments: filteredArray(attachmentSchema),
|
||||
card: cardSchema.nullable().catch(null),
|
||||
chat_id: z.string(),
|
||||
content: contentSchema,
|
||||
created_at: z.string().datetime().catch(new Date().toUTCString()),
|
||||
emojis: filteredArray(customEmojiSchema),
|
||||
expiration: z.number().optional().catch(undefined),
|
||||
emoji_reactions: z.array(emojiSchema).min(1).nullable().catch(null),
|
||||
id: z.string(),
|
||||
unread: z.coerce.boolean(),
|
||||
deleting: z.coerce.boolean(),
|
||||
pending: z.coerce.boolean(),
|
||||
});
|
||||
|
||||
type ChatMessage = z.infer<typeof chatMessageSchema>;
|
||||
|
||||
export { chatMessageSchema, type ChatMessage };
|
|
@ -14,4 +14,4 @@ const customEmojiSchema = z.object({
|
|||
|
||||
type CustomEmoji = z.infer<typeof customEmojiSchema>;
|
||||
|
||||
export { customEmojiSchema, CustomEmoji };
|
||||
export { customEmojiSchema, type CustomEmoji };
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
/** Validates the string as an emoji. */
|
||||
const emojiSchema = z.string().refine((v) => /\p{Extended_Pictographic}/u.test(v));
|
||||
import { emojiSchema } from './utils';
|
||||
|
||||
/** Pleroma emoji reaction. */
|
||||
const emojiReactionSchema = z.object({
|
||||
|
@ -12,4 +11,4 @@ const emojiReactionSchema = z.object({
|
|||
|
||||
type EmojiReaction = z.infer<typeof emojiReactionSchema>;
|
||||
|
||||
export { emojiReactionSchema, EmojiReaction };
|
||||
export { emojiReactionSchema, type EmojiReaction };
|
|
@ -16,4 +16,4 @@ const groupMemberSchema = z.object({
|
|||
|
||||
type GroupMember = z.infer<typeof groupMemberSchema>;
|
||||
|
||||
export { groupMemberSchema, GroupMember, GroupRoles };
|
||||
export { groupMemberSchema, type GroupMember, GroupRoles };
|
|
@ -14,4 +14,4 @@ const groupRelationshipSchema = z.object({
|
|||
|
||||
type GroupRelationship = z.infer<typeof groupRelationshipSchema>;
|
||||
|
||||
export { groupRelationshipSchema, GroupRelationship };
|
||||
export { groupRelationshipSchema, type GroupRelationship };
|
|
@ -1,13 +1,18 @@
|
|||
export { accountSchema, type Account } from './account';
|
||||
export { attachmentSchema, type Attachment } from './attachment';
|
||||
export { cardSchema, type Card } from './card';
|
||||
export { chatMessageSchema, type ChatMessage } from './chat-message';
|
||||
export { customEmojiSchema, type CustomEmoji } from './custom-emoji';
|
||||
export { emojiReactionSchema, type EmojiReaction } from './emoji-reaction';
|
||||
export { groupSchema, type Group } from './group';
|
||||
export { groupMemberSchema, type GroupMember } from './group-member';
|
||||
export { groupRelationshipSchema, type GroupRelationship } from './group-relationship';
|
||||
export { groupTagSchema, type GroupTag } from './group-tag';
|
||||
export { mentionSchema, type Mention } from './mention';
|
||||
export { notificationSchema, type Notification } from './notification';
|
||||
export { pollSchema, type Poll, type PollOption } from './poll';
|
||||
export { relationshipSchema, type Relationship } from './relationship';
|
||||
export { statusSchema, type Status } from './status';
|
||||
export { tagSchema, type Tag } from './tag';
|
||||
|
||||
// Soapbox
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
const mentionSchema = z.object({
|
||||
acct: z.string(),
|
||||
id: z.string(),
|
||||
url: z.string().url().catch(''),
|
||||
username: z.string().catch(''),
|
||||
}).transform((mention) => {
|
||||
if (!mention.username) {
|
||||
mention.username = mention.acct.split('@')[0];
|
||||
}
|
||||
|
||||
return mention;
|
||||
});
|
||||
|
||||
type Mention = z.infer<typeof mentionSchema>;
|
||||
|
||||
export { mentionSchema, type Mention };
|
|
@ -0,0 +1,104 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { accountSchema } from './account';
|
||||
import { chatMessageSchema } from './chat-message';
|
||||
import { statusSchema } from './status';
|
||||
import { emojiSchema } from './utils';
|
||||
|
||||
const baseNotificationSchema = z.object({
|
||||
account: accountSchema,
|
||||
created_at: z.string().datetime().catch(new Date().toUTCString()),
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
total_count: z.number().optional().catch(undefined), // TruthSocial
|
||||
});
|
||||
|
||||
const mentionNotificationSchema = baseNotificationSchema.extend({
|
||||
type: z.literal('mention'),
|
||||
status: statusSchema,
|
||||
});
|
||||
|
||||
const statusNotificationSchema = baseNotificationSchema.extend({
|
||||
type: z.literal('status'),
|
||||
status: statusSchema,
|
||||
});
|
||||
|
||||
const reblogNotificationSchema = baseNotificationSchema.extend({
|
||||
type: z.literal('reblog'),
|
||||
status: statusSchema,
|
||||
});
|
||||
|
||||
const followNotificationSchema = baseNotificationSchema.extend({
|
||||
type: z.literal('follow'),
|
||||
});
|
||||
|
||||
const followRequestNotificationSchema = baseNotificationSchema.extend({
|
||||
type: z.literal('follow_request'),
|
||||
});
|
||||
|
||||
const favouriteNotificationSchema = baseNotificationSchema.extend({
|
||||
type: z.literal('favourite'),
|
||||
status: statusSchema,
|
||||
});
|
||||
|
||||
const pollNotificationSchema = baseNotificationSchema.extend({
|
||||
type: z.literal('poll'),
|
||||
status: statusSchema,
|
||||
});
|
||||
|
||||
const updateNotificationSchema = baseNotificationSchema.extend({
|
||||
type: z.literal('update'),
|
||||
status: statusSchema,
|
||||
});
|
||||
|
||||
const moveNotificationSchema = baseNotificationSchema.extend({
|
||||
type: z.literal('move'),
|
||||
target: accountSchema,
|
||||
});
|
||||
|
||||
const chatMessageNotificationSchema = baseNotificationSchema.extend({
|
||||
type: z.literal('chat_message'),
|
||||
chat_message: chatMessageSchema,
|
||||
});
|
||||
|
||||
const emojiReactionNotificationSchema = baseNotificationSchema.extend({
|
||||
type: z.literal('pleroma:emoji_reaction'),
|
||||
emoji: emojiSchema,
|
||||
emoji_url: z.string().url().optional().catch(undefined),
|
||||
});
|
||||
|
||||
const eventReminderNotificationSchema = baseNotificationSchema.extend({
|
||||
type: z.literal('pleroma:event_reminder'),
|
||||
status: statusSchema,
|
||||
});
|
||||
|
||||
const participationRequestNotificationSchema = baseNotificationSchema.extend({
|
||||
type: z.literal('pleroma:participation_request'),
|
||||
status: statusSchema,
|
||||
});
|
||||
|
||||
const participationAcceptedNotificationSchema = baseNotificationSchema.extend({
|
||||
type: z.literal('pleroma:participation_accepted'),
|
||||
status: statusSchema,
|
||||
});
|
||||
|
||||
const notificationSchema = z.discriminatedUnion('type', [
|
||||
mentionNotificationSchema,
|
||||
statusNotificationSchema,
|
||||
reblogNotificationSchema,
|
||||
followNotificationSchema,
|
||||
followRequestNotificationSchema,
|
||||
favouriteNotificationSchema,
|
||||
pollNotificationSchema,
|
||||
updateNotificationSchema,
|
||||
moveNotificationSchema,
|
||||
chatMessageNotificationSchema,
|
||||
emojiReactionNotificationSchema,
|
||||
eventReminderNotificationSchema,
|
||||
participationRequestNotificationSchema,
|
||||
participationAcceptedNotificationSchema,
|
||||
]);
|
||||
|
||||
type Notification = z.infer<typeof notificationSchema>;
|
||||
|
||||
export { notificationSchema, type Notification };
|
|
@ -1,9 +1,60 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { normalizeStatus } from 'soapbox/normalizers';
|
||||
import { toSchema } from 'soapbox/utils/normalizers';
|
||||
import { accountSchema } from './account';
|
||||
import { attachmentSchema } from './attachment';
|
||||
import { cardSchema } from './card';
|
||||
import { customEmojiSchema } from './custom-emoji';
|
||||
import { groupSchema } from './group';
|
||||
import { mentionSchema } from './mention';
|
||||
import { pollSchema } from './poll';
|
||||
import { tagSchema } from './tag';
|
||||
import { contentSchema, dateSchema, filteredArray } from './utils';
|
||||
|
||||
const statusSchema = toSchema(normalizeStatus);
|
||||
const baseStatusSchema = z.object({
|
||||
account: accountSchema,
|
||||
application: z.object({
|
||||
name: z.string(),
|
||||
website: z.string().url().nullable().catch(null),
|
||||
}).nullable().catch(null),
|
||||
bookmarked: z.coerce.boolean(),
|
||||
card: cardSchema.nullable().catch(null),
|
||||
content: contentSchema,
|
||||
created_at: dateSchema,
|
||||
disliked: z.coerce.boolean(),
|
||||
dislikes_count: z.number().catch(0),
|
||||
edited_at: z.string().datetime().nullable().catch(null),
|
||||
emojis: filteredArray(customEmojiSchema),
|
||||
favourited: z.coerce.boolean(),
|
||||
favourites_count: z.number().catch(0),
|
||||
group: groupSchema.nullable().catch(null),
|
||||
in_reply_to_account_id: z.string().nullable().catch(null),
|
||||
in_reply_to_id: z.string().nullable().catch(null),
|
||||
id: z.string(),
|
||||
language: z.string().nullable().catch(null),
|
||||
media_attachments: filteredArray(attachmentSchema),
|
||||
mentions: filteredArray(mentionSchema),
|
||||
muted: z.coerce.boolean(),
|
||||
pinned: z.coerce.boolean(),
|
||||
pleroma: z.object({}).optional().catch(undefined),
|
||||
poll: pollSchema.nullable().catch(null),
|
||||
quote: z.literal(null).catch(null),
|
||||
quotes_count: z.number().catch(0),
|
||||
reblog: z.literal(null).catch(null),
|
||||
reblogged: z.coerce.boolean(),
|
||||
reblogs_count: z.number().catch(0),
|
||||
replies_count: z.number().catch(0),
|
||||
sensitive: z.coerce.boolean(),
|
||||
spoiler_text: contentSchema,
|
||||
tags: filteredArray(tagSchema),
|
||||
uri: z.string().url().catch(''),
|
||||
url: z.string().url().catch(''),
|
||||
visibility: z.string().catch('public'),
|
||||
});
|
||||
|
||||
const statusSchema = baseStatusSchema.extend({
|
||||
quote: baseStatusSchema.nullable().catch(null),
|
||||
reblog: baseStatusSchema.nullable().catch(null),
|
||||
});
|
||||
|
||||
type Status = z.infer<typeof statusSchema>;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ const historySchema = z.object({
|
|||
uses: z.coerce.number(),
|
||||
});
|
||||
|
||||
/** // https://docs.joinmastodon.org/entities/tag */
|
||||
/** https://docs.joinmastodon.org/entities/tag */
|
||||
const tagSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
url: z.string().url().catch(''),
|
||||
|
|
|
@ -2,6 +2,12 @@ import z from 'zod';
|
|||
|
||||
import type { CustomEmoji } from './custom-emoji';
|
||||
|
||||
/** Ensure HTML content is a string, and drop empty `<p>` tags. */
|
||||
const contentSchema = z.string().catch('').transform((value) => value === '<p></p>' ? '' : value);
|
||||
|
||||
/** Validate to Mastodon's date format, or use the current date. */
|
||||
const dateSchema = z.string().datetime().catch(new Date().toUTCString());
|
||||
|
||||
/** Validates individual items in an array, dropping any that aren't valid. */
|
||||
function filteredArray<T extends z.ZodTypeAny>(schema: T) {
|
||||
return z.any().array().catch([])
|
||||
|
@ -13,6 +19,9 @@ function filteredArray<T extends z.ZodTypeAny>(schema: T) {
|
|||
));
|
||||
}
|
||||
|
||||
/** Validates the string as an emoji. */
|
||||
const emojiSchema = z.string().refine((v) => /\p{Extended_Pictographic}/u.test(v));
|
||||
|
||||
/** Map a list of CustomEmoji to their shortcodes. */
|
||||
function makeCustomEmojiMap(customEmojis: CustomEmoji[]) {
|
||||
return customEmojis.reduce<Record<string, CustomEmoji>>((result, emoji) => {
|
||||
|
@ -21,4 +30,4 @@ function makeCustomEmojiMap(customEmojis: CustomEmoji[]) {
|
|||
}, {});
|
||||
}
|
||||
|
||||
export { filteredArray, makeCustomEmojiMap };
|
||||
export { filteredArray, makeCustomEmojiMap, emojiSchema, contentSchema, dateSchema };
|
Loading…
Reference in New Issue