kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'zod-notification' into 'develop'
zod: Notification, Attachment, ChatMessage, Status See merge request soapbox-pub/soapbox!2500environments/review-develop-3zknud/deployments/3352
commit
c5c2378542
|
@ -1,7 +1,10 @@
|
||||||
import { Entities } from 'soapbox/entity-store/entities';
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
import { useApi } from 'soapbox/hooks/useApi';
|
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) {
|
function useGroupMedia(groupId: string) {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|
|
@ -5,7 +5,7 @@ import emojify from 'soapbox/features/emoji';
|
||||||
|
|
||||||
import { customEmojiSchema } from './custom-emoji';
|
import { customEmojiSchema } from './custom-emoji';
|
||||||
import { relationshipSchema } from './relationship';
|
import { relationshipSchema } from './relationship';
|
||||||
import { filteredArray, makeCustomEmojiMap } from './utils';
|
import { contentSchema, filteredArray, makeCustomEmojiMap } from './utils';
|
||||||
|
|
||||||
const avatarMissing = require('assets/images/avatar-missing.png');
|
const avatarMissing = require('assets/images/avatar-missing.png');
|
||||||
const headerMissing = require('assets/images/header-missing.png');
|
const headerMissing = require('assets/images/header-missing.png');
|
||||||
|
@ -39,7 +39,7 @@ const accountSchema = z.object({
|
||||||
z.string(),
|
z.string(),
|
||||||
z.null(),
|
z.null(),
|
||||||
]).catch(null),
|
]).catch(null),
|
||||||
note: z.string().catch(''),
|
note: contentSchema,
|
||||||
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),
|
||||||
|
@ -121,4 +121,4 @@ const accountSchema = z.object({
|
||||||
|
|
||||||
type Account = z.infer<typeof accountSchema>;
|
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>;
|
type CustomEmoji = z.infer<typeof customEmojiSchema>;
|
||||||
|
|
||||||
export { customEmojiSchema, CustomEmoji };
|
export { customEmojiSchema, type CustomEmoji };
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
/** Validates the string as an emoji. */
|
import { emojiSchema } from './utils';
|
||||||
const emojiSchema = z.string().refine((v) => /\p{Extended_Pictographic}/u.test(v));
|
|
||||||
|
|
||||||
/** Pleroma emoji reaction. */
|
/** Pleroma emoji reaction. */
|
||||||
const emojiReactionSchema = z.object({
|
const emojiReactionSchema = z.object({
|
||||||
|
@ -12,4 +11,4 @@ const emojiReactionSchema = z.object({
|
||||||
|
|
||||||
type EmojiReaction = z.infer<typeof emojiReactionSchema>;
|
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>;
|
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>;
|
type GroupRelationship = z.infer<typeof groupRelationshipSchema>;
|
||||||
|
|
||||||
export { groupRelationshipSchema, GroupRelationship };
|
export { groupRelationshipSchema, type GroupRelationship };
|
|
@ -1,13 +1,18 @@
|
||||||
export { accountSchema, type Account } from './account';
|
export { accountSchema, type Account } from './account';
|
||||||
|
export { attachmentSchema, type Attachment } from './attachment';
|
||||||
export { cardSchema, type Card } from './card';
|
export { cardSchema, type Card } from './card';
|
||||||
|
export { chatMessageSchema, type ChatMessage } from './chat-message';
|
||||||
export { customEmojiSchema, type CustomEmoji } from './custom-emoji';
|
export { customEmojiSchema, type CustomEmoji } from './custom-emoji';
|
||||||
export { emojiReactionSchema, type EmojiReaction } from './emoji-reaction';
|
export { emojiReactionSchema, type EmojiReaction } from './emoji-reaction';
|
||||||
export { groupSchema, type Group } from './group';
|
export { groupSchema, type Group } from './group';
|
||||||
export { groupMemberSchema, type GroupMember } from './group-member';
|
export { groupMemberSchema, type GroupMember } from './group-member';
|
||||||
export { groupRelationshipSchema, type GroupRelationship } from './group-relationship';
|
export { groupRelationshipSchema, type GroupRelationship } from './group-relationship';
|
||||||
export { groupTagSchema, type GroupTag } from './group-tag';
|
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 { pollSchema, type Poll, type PollOption } from './poll';
|
||||||
export { relationshipSchema, type Relationship } from './relationship';
|
export { relationshipSchema, type Relationship } from './relationship';
|
||||||
|
export { statusSchema, type Status } from './status';
|
||||||
export { tagSchema, type Tag } from './tag';
|
export { tagSchema, type Tag } from './tag';
|
||||||
|
|
||||||
// Soapbox
|
// 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 { z } from 'zod';
|
||||||
|
|
||||||
import { normalizeStatus } from 'soapbox/normalizers';
|
import { accountSchema } from './account';
|
||||||
import { toSchema } from 'soapbox/utils/normalizers';
|
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>;
|
type Status = z.infer<typeof statusSchema>;
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ const historySchema = z.object({
|
||||||
uses: z.coerce.number(),
|
uses: z.coerce.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/** // https://docs.joinmastodon.org/entities/tag */
|
/** https://docs.joinmastodon.org/entities/tag */
|
||||||
const tagSchema = z.object({
|
const tagSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
url: z.string().url().catch(''),
|
url: z.string().url().catch(''),
|
||||||
|
|
|
@ -2,6 +2,12 @@ import z from 'zod';
|
||||||
|
|
||||||
import type { CustomEmoji } from './custom-emoji';
|
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. */
|
/** 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().catch([])
|
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. */
|
/** Map a list of CustomEmoji to their shortcodes. */
|
||||||
function makeCustomEmojiMap(customEmojis: CustomEmoji[]) {
|
function makeCustomEmojiMap(customEmojis: CustomEmoji[]) {
|
||||||
return customEmojis.reduce<Record<string, CustomEmoji>>((result, emoji) => {
|
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 };
|
Ładowanie…
Reference in New Issue