mirror of
https://github.com/hexastack/hexabot
synced 2025-04-10 15:55:55 +00:00
Merge pull request #377 from Hexastack/376-issue-unify-postback-type
update: unify postback types
This commit is contained in:
commit
16eff916f5
@ -70,6 +70,8 @@ export enum FileType {
|
||||
export enum PayloadType {
|
||||
location = 'location',
|
||||
attachments = 'attachments',
|
||||
quick_reply = 'quick_reply',
|
||||
button = 'button',
|
||||
}
|
||||
|
||||
export type StdOutgoingTextMessage = { text: string };
|
||||
|
@ -7,11 +7,7 @@
|
||||
*/
|
||||
|
||||
import { IncomingAttachmentPayload } from './attachment';
|
||||
|
||||
export enum PayloadType {
|
||||
location = 'location',
|
||||
attachments = 'attachments',
|
||||
}
|
||||
import { PayloadType } from './message';
|
||||
|
||||
export type Payload =
|
||||
| {
|
||||
|
@ -13,10 +13,13 @@ import {
|
||||
import { BlockFull } from '@/chat/schemas/block.schema';
|
||||
import { FileType } from '@/chat/schemas/types/attachment';
|
||||
import { ButtonType } from '@/chat/schemas/types/button';
|
||||
import { OutgoingMessageFormat } from '@/chat/schemas/types/message';
|
||||
import {
|
||||
OutgoingMessageFormat,
|
||||
PayloadType,
|
||||
} from '@/chat/schemas/types/message';
|
||||
import { BlockOptions, ContentOptions } from '@/chat/schemas/types/options';
|
||||
import { Pattern } from '@/chat/schemas/types/pattern';
|
||||
import { PayloadType, QuickReplyType } from '@/chat/schemas/types/quick-reply';
|
||||
import { QuickReplyType } from '@/chat/schemas/types/quick-reply';
|
||||
import { CaptureVar } from '@/chat/validation-rules/is-valid-capture';
|
||||
|
||||
import { modelInstance } from './misc';
|
||||
|
@ -300,6 +300,7 @@
|
||||
"from_channels": "Target channels",
|
||||
"simple_text": "Simple Text",
|
||||
"quick_replies": "Quick Replies",
|
||||
"button": "Button",
|
||||
"buttons": "Buttons",
|
||||
"web_url": "Web URL",
|
||||
"payload": "Payload",
|
||||
|
@ -300,6 +300,7 @@
|
||||
"from_channels": "Cibler les canaux",
|
||||
"simple_text": "Texte simple",
|
||||
"quick_replies": "Réponses rapides",
|
||||
"button": "Boutton",
|
||||
"buttons": "Boutons",
|
||||
"web_url": "URL Web",
|
||||
"payload": "Payload",
|
||||
|
@ -1,113 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import AutoCompleteSelect from "@/app-components/inputs/AutoCompleteSelect";
|
||||
import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useGetFromCache } from "@/hooks/crud/useGet";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { EntityType, Format } from "@/services/types";
|
||||
import { IBlock, PayloadPattern } from "@/types/block.types";
|
||||
|
||||
import { useBlock } from "../../BlockFormProvider";
|
||||
|
||||
type ContentPayloadOption = {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type ContentPostbackInputProps = {
|
||||
value?: string | null;
|
||||
onChange: (pattern: PayloadPattern) => void;
|
||||
};
|
||||
|
||||
export const ContentPostbackInput = ({
|
||||
value,
|
||||
onChange,
|
||||
}: ContentPostbackInputProps) => {
|
||||
const { t } = useTranslate();
|
||||
const block = useBlock();
|
||||
const { data: contents } = useFind(
|
||||
{ entity: EntityType.CONTENT, format: Format.FULL },
|
||||
{
|
||||
hasCount: false,
|
||||
},
|
||||
);
|
||||
const getBlockFromCache = useGetFromCache(EntityType.BLOCK);
|
||||
const options = useMemo(
|
||||
() =>
|
||||
(block?.previousBlocks || [])
|
||||
.map((bId) => getBlockFromCache(bId))
|
||||
.filter((b) => {
|
||||
return (
|
||||
b &&
|
||||
b.options?.content?.entity &&
|
||||
b.options.content.buttons.length > 0
|
||||
);
|
||||
})
|
||||
.map((b) => b as IBlock)
|
||||
.map((b) => {
|
||||
const availableContents = (contents || []).filter(
|
||||
({ entity, status }) =>
|
||||
status && entity === b.options?.content?.entity,
|
||||
);
|
||||
|
||||
return (b.options?.content?.buttons || []).reduce((payloads, btn) => {
|
||||
// Return a payload for each node/button combination
|
||||
payloads.push({
|
||||
id: btn.title,
|
||||
label: btn.title,
|
||||
});
|
||||
|
||||
return availableContents.reduce((acc, n) => {
|
||||
acc.push({
|
||||
id: btn.title + ":" + n.title,
|
||||
label: btn.title + ":" + n.title,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, payloads);
|
||||
}, [] as ContentPayloadOption[]);
|
||||
})
|
||||
.flat(),
|
||||
[block?.previousBlocks, contents, getBlockFromCache],
|
||||
);
|
||||
|
||||
return (
|
||||
<AutoCompleteSelect<ContentPayloadOption, "label", false>
|
||||
value={value}
|
||||
options={options}
|
||||
labelKey="label"
|
||||
label={t("label.content")}
|
||||
multiple={false}
|
||||
onChange={(_e, content) => {
|
||||
content &&
|
||||
onChange({
|
||||
label: content.label,
|
||||
value: content.id,
|
||||
type: "content",
|
||||
} as PayloadPattern);
|
||||
}}
|
||||
groupBy={(option) => {
|
||||
const [btn] = option.label.split(":");
|
||||
|
||||
return btn;
|
||||
}}
|
||||
renderGroup={(params) => (
|
||||
<li key={params.key}>
|
||||
<Typography component="h4" p={2} fontWeight={700} color="primary">
|
||||
{params.group}
|
||||
</Typography>
|
||||
<Box>{params.children}</Box>
|
||||
</li>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
@ -10,12 +10,10 @@ import { Grid, MenuItem, TextFieldProps } from "@mui/material";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { RegisterOptions, useFormContext } from "react-hook-form";
|
||||
|
||||
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import NlpPatternSelect from "@/app-components/inputs/NlpPatternSelect";
|
||||
import { RegexInput } from "@/app-components/inputs/RegexInput";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { EntityType, Format } from "@/services/types";
|
||||
import {
|
||||
IBlockAttributes,
|
||||
IBlockFull,
|
||||
@ -24,9 +22,7 @@ import {
|
||||
PatternType,
|
||||
PayloadPattern,
|
||||
} from "@/types/block.types";
|
||||
import { IMenuItem } from "@/types/menu.types";
|
||||
|
||||
import { ContentPostbackInput } from "./ContentPostbackInput";
|
||||
import { PostbackInput } from "./PostbackInput";
|
||||
|
||||
const isRegex = (str: Pattern) => {
|
||||
@ -72,13 +68,12 @@ const PatternInput: FC<PatternInputProps> = ({
|
||||
// const getNlpEntityFromCache = useGetFromCache(EntityType.NLP_ENTITY);
|
||||
const [pattern, setPattern] = useState<Pattern>(value);
|
||||
const [patternType, setPatternType] = useState<PatternType>(getType(value));
|
||||
const isPostbackType = ["payload", "content", "menu"].includes(patternType);
|
||||
const types = [
|
||||
{ value: "text", label: t("label.match_sound") },
|
||||
{ value: "regex", label: t("label.regex") },
|
||||
{ value: "payload", label: t("label.postback") },
|
||||
{ value: "nlp", label: t("label.nlp") },
|
||||
{ value: "menu", label: t("label.menu") },
|
||||
{ value: "content", label: t("label.content") },
|
||||
];
|
||||
const registerInput = (
|
||||
errorMessage: string,
|
||||
@ -110,7 +105,7 @@ const PatternInput: FC<PatternInputProps> = ({
|
||||
<Input
|
||||
select
|
||||
label={t("label.type")}
|
||||
value={patternType}
|
||||
value={isPostbackType ? "payload" : patternType}
|
||||
onChange={(e) => {
|
||||
const selected = e.target.value as PatternType;
|
||||
|
||||
@ -151,43 +146,12 @@ const PatternInput: FC<PatternInputProps> = ({
|
||||
onChange={setPattern}
|
||||
/>
|
||||
)}
|
||||
{patternType === "menu" ? (
|
||||
<AutoCompleteEntitySelect<IMenuItem, "title", false>
|
||||
value={pattern ? (pattern as PayloadPattern).value : null}
|
||||
searchFields={["title"]}
|
||||
entity={EntityType.MENU}
|
||||
format={Format.BASIC}
|
||||
idKey="payload"
|
||||
labelKey="title"
|
||||
label={t("label.menu")}
|
||||
multiple={false}
|
||||
onChange={(_e, menuItem) => {
|
||||
menuItem &&
|
||||
setPattern({
|
||||
label: menuItem?.title,
|
||||
value: menuItem?.payload,
|
||||
type: "menu",
|
||||
} as PayloadPattern);
|
||||
}}
|
||||
preprocess={(items) => {
|
||||
return items.filter((item) => "payload" in item);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{patternType === "content" ? (
|
||||
<ContentPostbackInput
|
||||
onChange={(payload) => {
|
||||
payload && setPattern(payload);
|
||||
}}
|
||||
value={pattern ? (pattern as PayloadPattern).value : null}
|
||||
/>
|
||||
) : null}
|
||||
{patternType === "payload" ? (
|
||||
{isPostbackType ? (
|
||||
<PostbackInput
|
||||
onChange={(payload) => {
|
||||
payload && setPattern(payload);
|
||||
}}
|
||||
value={pattern ? (pattern as PayloadPattern).value : null}
|
||||
defaultValue={pattern as PayloadPattern}
|
||||
/>
|
||||
) : null}
|
||||
{typeof value === "string" && patternType === "regex" ? (
|
||||
|
@ -6,16 +6,29 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
InputAdornment,
|
||||
Skeleton,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import AutoCompleteSelect from "@/app-components/inputs/AutoCompleteSelect";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useGetFromCache } from "@/hooks/crud/useGet";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { theme } from "@/layout/themes/theme";
|
||||
import { EntityType, Format } from "@/services/types";
|
||||
import { IBlock, PayloadPattern } from "@/types/block.types";
|
||||
import {
|
||||
ButtonType,
|
||||
PayloadType,
|
||||
PostBackButton,
|
||||
QuickReplyType,
|
||||
StdOutgoingButtonsMessage,
|
||||
StdOutgoingQuickRepliesMessage,
|
||||
StdQuickReply,
|
||||
@ -23,38 +36,56 @@ import {
|
||||
|
||||
import { useBlock } from "../../BlockFormProvider";
|
||||
|
||||
type PayloadOption = {
|
||||
id: string;
|
||||
label: string;
|
||||
group?: string;
|
||||
type PayloadOption = PayloadPattern & {
|
||||
group: string;
|
||||
};
|
||||
|
||||
const isSamePostback = <T extends PayloadPattern>(a: T, b: T) =>
|
||||
a.label === b.label && a.value === b.value;
|
||||
|
||||
type PostbackInputProps = {
|
||||
value?: string | null;
|
||||
onChange: (pattern: PayloadPattern) => void;
|
||||
defaultValue?: PayloadPattern;
|
||||
onChange: (pattern: PayloadPattern | null) => void;
|
||||
};
|
||||
|
||||
export const PostbackInput = ({ value, onChange }: PostbackInputProps) => {
|
||||
export const PostbackInput = ({
|
||||
defaultValue,
|
||||
onChange,
|
||||
}: PostbackInputProps) => {
|
||||
const block = useBlock();
|
||||
const [selectedValue, setSelectedValue] = useState(defaultValue || null);
|
||||
const getBlockFromCache = useGetFromCache(EntityType.BLOCK);
|
||||
const { data: menu, isLoading: isLoadingMenu } = useFind(
|
||||
{ entity: EntityType.MENU, format: Format.FULL },
|
||||
{ hasCount: false },
|
||||
);
|
||||
const { data: contents, isLoading: isLoadingContent } = useFind(
|
||||
{ entity: EntityType.CONTENT, format: Format.FULL },
|
||||
{
|
||||
hasCount: false,
|
||||
},
|
||||
);
|
||||
const { t } = useTranslate();
|
||||
// General options
|
||||
const generalOptions = [
|
||||
{
|
||||
id: "GET_STARTED",
|
||||
label: t("label.get_started"),
|
||||
group: t("label.general"),
|
||||
value: "GET_STARTED",
|
||||
type: PayloadType.button,
|
||||
group: "general",
|
||||
},
|
||||
|
||||
{
|
||||
id: "VIEW_MORE",
|
||||
label: t("label.view_more"),
|
||||
group: t("label.general"),
|
||||
value: "VIEW_MORE",
|
||||
type: PayloadType.button,
|
||||
group: "general",
|
||||
},
|
||||
{
|
||||
id: "LOCATION",
|
||||
label: t("label.location"),
|
||||
group: t("label.general"),
|
||||
value: "LOCATION",
|
||||
type: PayloadType.location,
|
||||
group: "general",
|
||||
},
|
||||
];
|
||||
// Gather previous blocks buttons
|
||||
@ -62,28 +93,25 @@ export const PostbackInput = ({ value, onChange }: PostbackInputProps) => {
|
||||
() =>
|
||||
(block?.previousBlocks || [])
|
||||
.map((b) => getBlockFromCache(b))
|
||||
.filter((b) => {
|
||||
return b && typeof b.message === "object" && "buttons" in b.message;
|
||||
})
|
||||
.filter(
|
||||
(b) => b && typeof b.message === "object" && "buttons" in b.message,
|
||||
)
|
||||
.map((b) => b as IBlock)
|
||||
.reduce((acc, b) => {
|
||||
const postbackButtons = (
|
||||
(b.message as StdOutgoingButtonsMessage)?.buttons || []
|
||||
)
|
||||
.filter((btn) => btn.type === "postback")
|
||||
.map((btn) => {
|
||||
return { ...btn, group: b.name };
|
||||
});
|
||||
.filter((btn) => btn.type === ButtonType.postback)
|
||||
.map((btn) => ({ ...btn, group: b.name }));
|
||||
|
||||
return acc.concat(postbackButtons);
|
||||
}, [] as (PostBackButton & { group: string })[])
|
||||
.map((btn) => {
|
||||
return {
|
||||
id: btn.payload,
|
||||
label: btn.title,
|
||||
group: btn.group,
|
||||
};
|
||||
}),
|
||||
.map((btn) => ({
|
||||
label: btn.title,
|
||||
value: btn.payload,
|
||||
type: PayloadType.button,
|
||||
group: "button",
|
||||
})),
|
||||
[block?.previousBlocks, getBlockFromCache],
|
||||
);
|
||||
// Gather previous blocks quick replies
|
||||
@ -91,62 +119,157 @@ export const PostbackInput = ({ value, onChange }: PostbackInputProps) => {
|
||||
() =>
|
||||
(block?.previousBlocks || [])
|
||||
.map((b) => getBlockFromCache(b))
|
||||
.filter((b) => {
|
||||
return (
|
||||
b && typeof b.message === "object" && "quickReplies" in b.message
|
||||
);
|
||||
})
|
||||
.filter(
|
||||
(b) =>
|
||||
b && typeof b.message === "object" && "quickReplies" in b.message,
|
||||
)
|
||||
.map((b) => b as IBlock)
|
||||
.reduce((acc, b) => {
|
||||
const postbackQuickReplies = (
|
||||
(b.message as StdOutgoingQuickRepliesMessage)?.quickReplies || []
|
||||
)
|
||||
.filter((btn) => btn.content_type === "text")
|
||||
.map((btn) => {
|
||||
return { ...btn, group: b.name };
|
||||
});
|
||||
.filter(({ content_type }) => content_type === QuickReplyType.text)
|
||||
.map((btn) => ({ ...btn, group: b.name }));
|
||||
|
||||
return acc.concat(postbackQuickReplies);
|
||||
}, [] as (StdQuickReply & { group: string })[])
|
||||
.map((btn) => {
|
||||
return {
|
||||
id: btn.payload as string,
|
||||
label: btn.title as string,
|
||||
group: btn.group,
|
||||
};
|
||||
}),
|
||||
.map((btn) => ({
|
||||
label: btn.title as string,
|
||||
value: btn.payload as string,
|
||||
type: PayloadType.quick_reply,
|
||||
group: "quick_reply",
|
||||
})),
|
||||
[block?.previousBlocks],
|
||||
);
|
||||
const menuOptions = menu
|
||||
.filter(({ payload }) => payload)
|
||||
.map(({ title, payload }) => ({
|
||||
label: title,
|
||||
value: payload as string,
|
||||
type: PayloadType.menu,
|
||||
group: "menu",
|
||||
}));
|
||||
const contentOptions = useMemo(
|
||||
() =>
|
||||
(block?.previousBlocks || [])
|
||||
.map((bId) => getBlockFromCache(bId) as IBlock)
|
||||
.filter(
|
||||
(b) =>
|
||||
b &&
|
||||
b.options?.content?.entity &&
|
||||
b.options.content.buttons.length > 0,
|
||||
)
|
||||
.map((b) => {
|
||||
const availableContents = (contents || []).filter(
|
||||
({ entity, status }) =>
|
||||
status && entity === b.options?.content?.entity,
|
||||
);
|
||||
|
||||
return (b.options?.content?.buttons || []).reduce((payloads, btn) => {
|
||||
// Return a payload for each node/button combination
|
||||
payloads.push({
|
||||
label: btn.title,
|
||||
value: btn.title,
|
||||
type: PayloadType.content,
|
||||
group: "content",
|
||||
});
|
||||
|
||||
return availableContents.reduce((acc, n) => {
|
||||
acc.push({
|
||||
label: n.title,
|
||||
value: n.title,
|
||||
type: PayloadType.content,
|
||||
group: "content",
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, payloads);
|
||||
}, [] as PayloadOption[]);
|
||||
})
|
||||
.flat(),
|
||||
[block?.previousBlocks, contents, getBlockFromCache],
|
||||
);
|
||||
// Concat all previous blocks
|
||||
const options = [...generalOptions, ...btnOptions, ...qrOptions];
|
||||
const options: PayloadOption[] = [
|
||||
...generalOptions,
|
||||
...btnOptions,
|
||||
...qrOptions,
|
||||
...menuOptions,
|
||||
...contentOptions,
|
||||
];
|
||||
const isOptionsReady =
|
||||
!defaultValue || options.find((o) => isSamePostback(o, defaultValue));
|
||||
|
||||
if (!isOptionsReady || isLoadingContent || isLoadingMenu) {
|
||||
return (
|
||||
<Skeleton animation="wave" variant="rounded" width="100%" height={40} />
|
||||
);
|
||||
}
|
||||
const selected = defaultValue
|
||||
? options.find((o) => isSamePostback(o, defaultValue))
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoCompleteSelect<PayloadOption, "label", false>
|
||||
value={value}
|
||||
options={options}
|
||||
labelKey="label"
|
||||
label={t("label.postback")}
|
||||
multiple={false}
|
||||
onChange={(_e, content) => {
|
||||
content &&
|
||||
onChange({
|
||||
label: content.label,
|
||||
value: content.id,
|
||||
} as PayloadPattern);
|
||||
}}
|
||||
groupBy={(option) => {
|
||||
return option.group ?? t("label.other");
|
||||
}}
|
||||
renderGroup={(params) => (
|
||||
<li key={params.key}>
|
||||
<Typography component="h4" p={2} fontWeight={700} color="primary">
|
||||
{params.group}
|
||||
</Typography>
|
||||
<Box>{params.children}</Box>
|
||||
</li>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
defaultValue={selected}
|
||||
value={selected}
|
||||
options={options}
|
||||
multiple={false}
|
||||
onChange={(_e, value) => {
|
||||
setSelectedValue(value);
|
||||
if (value) {
|
||||
const { group: _g, ...payloadPattern } = value;
|
||||
|
||||
onChange(payloadPattern);
|
||||
} else {
|
||||
onChange(null);
|
||||
}
|
||||
}}
|
||||
groupBy={({ group }) => group ?? t("label.other")}
|
||||
getOptionLabel={({ label }) => label}
|
||||
renderGroup={({ key, group, children }) => (
|
||||
<li key={key}>
|
||||
<Typography component="h4" p={2} fontWeight={700} color="primary">
|
||||
{t(`label.${group}`)}
|
||||
</Typography>
|
||||
<Box>{children}</Box>
|
||||
</li>
|
||||
)}
|
||||
renderInput={(props) => (
|
||||
<Input
|
||||
{...props}
|
||||
label={t("label.postback")}
|
||||
InputProps={{
|
||||
...props.InputProps,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Chip
|
||||
sx={{
|
||||
left: "8px",
|
||||
height: "25px",
|
||||
fontSize: "12px",
|
||||
minWidth: "75px",
|
||||
position: "relative",
|
||||
maxHeight: "30px",
|
||||
borderRadius: "16px",
|
||||
borderColor: theme.palette.grey[400],
|
||||
}}
|
||||
color="primary"
|
||||
label={t(
|
||||
`label.${selectedValue?.type || "postback"}`,
|
||||
).toLocaleLowerCase()}
|
||||
variant="role"
|
||||
/>
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment:
|
||||
isLoadingMenu || isLoadingContent ? (
|
||||
<CircularProgress color="inherit" size={20} />
|
||||
) : null,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -66,6 +66,8 @@ export interface PayloadPattern {
|
||||
label: string;
|
||||
value: string;
|
||||
// @todo : rename 'attachment' to 'attachments'
|
||||
// @todo: If undefined, that means the payload could be either quick_reply or button
|
||||
// We will move soon so that it will be a required attribute
|
||||
type?: PayloadType;
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,8 @@ export enum PayloadType {
|
||||
attachments = "attachments",
|
||||
menu = "menu",
|
||||
content = "content",
|
||||
quick_reply = "quick_reply",
|
||||
button = "button",
|
||||
}
|
||||
|
||||
export enum FileType {
|
||||
|
Loading…
Reference in New Issue
Block a user