Ai Tone Editor: Support custom AI tone editing (#6965)
This commit is contained in:
parent
d82240b06c
commit
ac713094f8
@ -27,7 +27,8 @@ type Limit =
|
||||
| 'chatlist_joined_limit'
|
||||
| 'recommended_channels_limit'
|
||||
| 'saved_dialogs_pinned_limit'
|
||||
| 'reactions_user_max';
|
||||
| 'reactions_user_max'
|
||||
| 'aicompose_tone_saved_limit';
|
||||
type LimitKey = `${Limit}_${LimitType}`;
|
||||
type LimitsConfig = Record<LimitKey, number>;
|
||||
|
||||
@ -135,8 +136,6 @@ export interface GramJsAppConfig extends LimitsConfig {
|
||||
aicompose_tone_examples_num?: number;
|
||||
aicompose_tone_title_length_max?: number;
|
||||
aicompose_tone_prompt_length_max?: number;
|
||||
aicompose_tone_saved_limit_default?: number;
|
||||
aicompose_tone_saved_limit_premium?: number;
|
||||
}
|
||||
|
||||
function buildEmojiSounds(appConfig: GramJsAppConfig) {
|
||||
@ -217,6 +216,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
|
||||
savedDialogsPinned: getLimit(appConfig, 'saved_dialogs_pinned_limit', 'savedDialogsPinned'),
|
||||
maxReactions: getLimit(appConfig, 'reactions_user_max', 'maxReactions'),
|
||||
moreAccounts: DEFAULT_LIMITS.moreAccounts,
|
||||
aiComposeToneSaved: getLimit(appConfig, 'aicompose_tone_saved_limit', 'aiComposeToneSaved'),
|
||||
},
|
||||
contactNoteLimit: appConfig.contact_note_length_limit,
|
||||
hash,
|
||||
@ -284,8 +284,6 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
|
||||
aiComposeToneExamplesNum: appConfig.aicompose_tone_examples_num,
|
||||
aiComposeToneTitleLengthMax: appConfig.aicompose_tone_title_length_max,
|
||||
aiComposeTonePromptLengthMax: appConfig.aicompose_tone_prompt_length_max,
|
||||
aiComposeToneSavedLimitDefault: appConfig.aicompose_tone_saved_limit_default,
|
||||
aiComposeToneSavedLimitPremium: appConfig.aicompose_tone_saved_limit_premium,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1004,6 +1004,10 @@ export function buildWebPage(webPage: GramJs.TypeWebPage): ApiWebPage | undefine
|
||||
};
|
||||
}
|
||||
|
||||
const attributeAiTone = attributes?.find((a): a is GramJs.WebPageAttributeAiComposeTone => (
|
||||
a instanceof GramJs.WebPageAttributeAiComposeTone
|
||||
));
|
||||
|
||||
return {
|
||||
mediaType: 'webpage',
|
||||
webpageType: 'full',
|
||||
@ -1026,6 +1030,7 @@ export function buildWebPage(webPage: GramJs.TypeWebPage): ApiWebPage | undefine
|
||||
gift,
|
||||
auction,
|
||||
stickers,
|
||||
aiComposeToneEmojiId: attributeAiTone?.emojiId.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -64,7 +64,9 @@ import {
|
||||
buildApiSponsoredMessageReportResult,
|
||||
buildThreadReadState,
|
||||
} from '../apiBuilders/chats';
|
||||
import { buildApiAiComposeTone, buildApiComposedMessageWithAI, buildApiFormattedText } from '../apiBuilders/common';
|
||||
import {
|
||||
buildApiAiComposeTone, buildApiAiComposeToneExample, buildApiComposedMessageWithAI, buildApiFormattedText,
|
||||
} from '../apiBuilders/common';
|
||||
import { buildApiTopicWithState } from '../apiBuilders/forums';
|
||||
import {
|
||||
buildMessageMediaContent, buildMessagePollFromMedia, buildMessageTextContent,
|
||||
@ -2853,3 +2855,110 @@ export async function fetchAiComposeTones({
|
||||
hash: result.hash.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function createAiTone({
|
||||
title,
|
||||
emojiId,
|
||||
prompt,
|
||||
shouldDisplayAuthor,
|
||||
}: {
|
||||
title: string;
|
||||
emojiId: string;
|
||||
prompt: string;
|
||||
shouldDisplayAuthor?: boolean;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.aicompose.CreateTone({
|
||||
title,
|
||||
prompt,
|
||||
emojiId: BigInt(emojiId),
|
||||
displayAuthor: shouldDisplayAuthor || undefined,
|
||||
}));
|
||||
|
||||
if (!result) return undefined;
|
||||
|
||||
return buildApiAiComposeTone(result);
|
||||
}
|
||||
|
||||
export async function deleteAiTone({
|
||||
tone,
|
||||
}: {
|
||||
tone: ApiInputAiComposeTone;
|
||||
}) {
|
||||
return invokeRequest(new GramJs.aicompose.DeleteTone({
|
||||
tone: buildInputAiComposeTone(tone),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function updateAiTone({
|
||||
tone,
|
||||
title,
|
||||
emojiId,
|
||||
prompt,
|
||||
shouldDisplayAuthor,
|
||||
}: {
|
||||
tone: ApiInputAiComposeTone;
|
||||
title?: string;
|
||||
emojiId?: string;
|
||||
prompt?: string;
|
||||
shouldDisplayAuthor?: boolean;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.aicompose.UpdateTone({
|
||||
tone: buildInputAiComposeTone(tone),
|
||||
title,
|
||||
prompt,
|
||||
emojiId: emojiId ? BigInt(emojiId) : undefined,
|
||||
displayAuthor: shouldDisplayAuthor,
|
||||
}));
|
||||
|
||||
if (!result) return undefined;
|
||||
|
||||
return buildApiAiComposeTone(result);
|
||||
}
|
||||
|
||||
export async function fetchAiTone({
|
||||
tone,
|
||||
}: {
|
||||
tone: ApiInputAiComposeTone;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.aicompose.GetTone({
|
||||
tone: buildInputAiComposeTone(tone),
|
||||
}));
|
||||
|
||||
if (!result || !('tones' in result)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
tones: result.tones.map(buildApiAiComposeTone),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchAiToneExample({
|
||||
tone,
|
||||
num,
|
||||
}: {
|
||||
tone: ApiInputAiComposeTone;
|
||||
num: number;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.aicompose.GetToneExample({
|
||||
tone: buildInputAiComposeTone(tone),
|
||||
num,
|
||||
}));
|
||||
|
||||
if (!result) return undefined;
|
||||
|
||||
return buildApiAiComposeToneExample(result);
|
||||
}
|
||||
|
||||
export async function saveAiTone({
|
||||
tone,
|
||||
unsave,
|
||||
}: {
|
||||
tone: ApiInputAiComposeTone;
|
||||
unsave?: boolean;
|
||||
}) {
|
||||
return invokeRequest(new GramJs.aicompose.SaveTone({
|
||||
tone: buildInputAiComposeTone(tone),
|
||||
unsave: Boolean(unsave),
|
||||
}));
|
||||
}
|
||||
|
||||
@ -426,6 +426,7 @@ export interface ApiWebPageFull {
|
||||
gift?: ApiStarGiftUnique;
|
||||
auction?: ApiWebPageAuctionData;
|
||||
stickers?: ApiWebPageStickerData;
|
||||
aiComposeToneEmojiId?: string;
|
||||
hasLargeMedia?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@ -337,8 +337,6 @@ export interface ApiAppConfig {
|
||||
aiComposeToneExamplesNum?: number;
|
||||
aiComposeToneTitleLengthMax?: number;
|
||||
aiComposeTonePromptLengthMax?: number;
|
||||
aiComposeToneSavedLimitDefault?: number;
|
||||
aiComposeToneSavedLimitPremium?: number;
|
||||
}
|
||||
|
||||
export interface ApiConfig {
|
||||
@ -464,15 +462,17 @@ export type ApiLimitType =
|
||||
| 'recommendedChannels'
|
||||
| 'savedDialogsPinned'
|
||||
| 'maxReactions'
|
||||
| 'moreAccounts';
|
||||
| 'moreAccounts'
|
||||
| 'aiComposeToneSaved';
|
||||
|
||||
export type ApiLimitTypeWithModal = Exclude<ApiLimitType, (
|
||||
'captionLength' | 'aboutLength' | 'stickersFaved' | 'savedGifs' | 'recommendedChannels' | 'moreAccounts'
|
||||
| 'maxReactions'
|
||||
| 'maxReactions' | 'aiComposeToneSaved'
|
||||
)>;
|
||||
|
||||
export type ApiLimitTypeForPromo = Exclude<ApiLimitType,
|
||||
'uploadMaxFileparts' | 'chatlistInvites' | 'chatlistJoined' | 'savedDialogsPinned' | 'maxReactions'
|
||||
'uploadMaxFileparts' | 'chatlistInvites' | 'chatlistJoined' | 'savedDialogsPinned' | 'maxReactions'
|
||||
| 'aiComposeToneSaved'
|
||||
>;
|
||||
|
||||
export type ApiPeerNotifySettings = {
|
||||
|
||||
1
src/assets/font-icons/reload-arrows.svg
Normal file
1
src/assets/font-icons/reload-arrows.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M22.609 22.136a.226.226 0 0 0-.276.04A8.82 8.82 0 0 1 16 24.859c-4.885 0-8.858-3.975-8.858-8.86 0-.237.034-.466.052-.698a16 16 0 0 0-.73-.13c-.258-.03-.516-.06-.774-.081q-.26 0-.501-.066a11 11 0 0 0-.047.976c0 5.988 4.87 10.859 10.858 10.859 3.183 0 6.045-1.382 8.032-3.572.1-.11.07-.289-.059-.363zM24.77 17.184c.19-.075.388-.12.6-.113q.124 0 .246.012.308.033.614.083a1.6 1.6 0 0 1 .329.093c.068.028.135.06.203.087.057-.442.097-.89.097-1.346C26.859 10.012 21.987 5.14 16 5.14a10.82 10.82 0 0 0-8.032 3.572c-.1.11-.07.289.06.363l1.363.788c.09.052.203.035.276-.04A8.82 8.82 0 0 1 16 7.141c4.885 0 8.859 3.974 8.859 8.859 0 .402-.037.796-.09 1.184"/><path d="M6.241 12.481 1.8 17.451a.185.185 0 0 0 .138.308h8.606c.16 0 .245-.189.138-.308zM25.759 19.519l4.441-4.97a.185.185 0 0 0-.138-.308h-8.606c-.16 0-.245.189-.138.308z"/></svg>
|
||||
|
After Width: | Height: | Size: 897 B |
@ -1910,6 +1910,7 @@
|
||||
"ViewButtonStickerset" = "VIEW STICKERS";
|
||||
"ViewButtonEmojiset" = "VIEW EMOJI";
|
||||
"ViewButtonGiftUnique" = "VIEW COLLECTIBLE";
|
||||
"ViewButtonAiStyle" = "VIEW STYLE";
|
||||
"AuthContinueOnThisLanguage" = "Continue in English";
|
||||
"Share" = "Share";
|
||||
"GiftSortByDate" = "Sort by Date";
|
||||
@ -2889,6 +2890,32 @@
|
||||
"AiMessageEditorApply" = "Apply";
|
||||
"AiMessageEditorEmojify" = "emojify";
|
||||
"AiMessageEditorTranslation" = "Translation";
|
||||
"AiToneEditorNewStyle" = "New Style";
|
||||
"AiToneEditorTitle" = "Create AI Tone";
|
||||
"AiToneEditorNamePlaceholder" = "Style Name (for example: Pirate)";
|
||||
"AiToneEditorPromptPlaceholder" = "Instructions (for example write in bold, nautical tone, light slang, vivid sea imagery, playful swagger)";
|
||||
"AiToneEditorDisplayAuthor" = "Add a link to my account";
|
||||
"AiToneEditorSelectEmoji" = "Select Emoji";
|
||||
"AiToneCreated" = "{title} style created!";
|
||||
"AiToneCreatedHint" = "Press and hold a style to edit or share it.";
|
||||
"AiToneEditStyle" = "Edit Style";
|
||||
"AiToneShareStyle" = "Share Style";
|
||||
"AiToneDeleteStyle" = "Delete Style";
|
||||
"AiToneDeleteStyleConfirmOwn" = "Are you sure you want to delete this style? It will be removed for everyone who installed it.";
|
||||
"AiToneDeleteStyleConfirm" = "Are you sure you want to remove this style?";
|
||||
"AiToneEditorEditTitle" = "Edit AI Style";
|
||||
"AiTonePreviewSubtitle" = "Add this style to instantly rewrite your messages.";
|
||||
"AiTonePreviewBefore" = "Before";
|
||||
"AiTonePreviewAnotherExample" = "Another Example";
|
||||
"AiTonePreviewAfter" = "After";
|
||||
"AiTonePreviewAddStyle" = "Add Style";
|
||||
"AiTonePreviewRemoveStyle" = "Remove Style";
|
||||
"AiTonePreviewStyleAdded" = "Style added";
|
||||
"AiToneLimitReached" = "You have reached the limit of custom styles.";
|
||||
"AiToneLimitReachedPremium" = "You have reached the limit of **{limit}** custom styles.";
|
||||
"AiTonePreviewUsedBy" = "Used by {count} people.";
|
||||
"AiTonePreviewCreatedBy" = "Created by {author}.";
|
||||
"AiTonePreviewUsedByCreatedBy" = "{usedBy} {createdBy}";
|
||||
"TextShowMore" = "more";
|
||||
"TextShowLess" = "less";
|
||||
"AiMessageEditorFrom" = "From";
|
||||
|
||||
@ -73,6 +73,10 @@ export { default as ReactionPicker } from '../components/middle/message/reaction
|
||||
|
||||
export { default as AiMessageEditorModal }
|
||||
from '../components/middle/composer/AiMessageEditorModal/AiMessageEditorModal';
|
||||
export { default as AiToneEmojiPickerModal }
|
||||
from '../components/middle/composer/AiMessageEditorModal/AiToneEmojiPickerModal';
|
||||
export { default as AiTonePreviewModal }
|
||||
from '../components/modals/aiTonePreview/AiTonePreviewModal';
|
||||
|
||||
export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal';
|
||||
export { default as PollModal } from '../components/modals/poll/PollModal';
|
||||
|
||||
@ -68,6 +68,7 @@ type OwnProps = {
|
||||
isStatusPicker?: boolean;
|
||||
isReactionPicker?: boolean;
|
||||
isTranslucent?: boolean;
|
||||
noAddButton?: boolean;
|
||||
onCustomEmojiSelect: (sticker: ApiSticker) => void;
|
||||
onReactionSelect?: (reaction: ApiReactionWithPaid) => void;
|
||||
onReactionContext?: (reaction: ApiReactionWithPaid) => void;
|
||||
@ -130,6 +131,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
isReactionPicker,
|
||||
isStatusPicker,
|
||||
isTranslucent,
|
||||
noAddButton,
|
||||
isSavedMessages,
|
||||
isCurrentUserPremium,
|
||||
withDefaultTopicIcons,
|
||||
@ -451,6 +453,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
isSavedMessages={isSavedMessages}
|
||||
isStatusPicker={isStatusPicker}
|
||||
isReactionPicker={isReactionPicker}
|
||||
noAddButton={noAddButton}
|
||||
shouldHideHeader={shouldHideHeader}
|
||||
withDefaultTopicIcon={withDefaultTopicIcons && stickerSet.id === RECENT_SYMBOL_SET_ID}
|
||||
withDefaultStatusIcon={isStatusPicker && stickerSet.id === RECENT_SYMBOL_SET_ID}
|
||||
|
||||
@ -62,6 +62,7 @@ type OwnProps = {
|
||||
isChatStickerSet?: boolean;
|
||||
isTranslucent?: boolean;
|
||||
noContextMenus?: boolean;
|
||||
noAddButton?: boolean;
|
||||
forcePlayback?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
observeIntersectionForPlayingItems: ObserveFn;
|
||||
@ -106,6 +107,7 @@ const StickerSet = ({
|
||||
isChatStickerSet,
|
||||
isTranslucent,
|
||||
noContextMenus,
|
||||
noAddButton,
|
||||
forcePlayback,
|
||||
collectibleStatuses,
|
||||
observeIntersection,
|
||||
@ -260,7 +262,7 @@ const StickerSet = ({
|
||||
const collectibleEmojiIdsSet = useMemo(() => (
|
||||
collectibleStatuses ? new Set(collectibleStatuses.map(({ documentId }) => documentId)) : undefined
|
||||
), [collectibleStatuses]);
|
||||
const withAddSetButton = !shouldHideHeader && !isRecent && !isStatusCollectible
|
||||
const withAddSetButton = !noAddButton && !shouldHideHeader && !isRecent && !isStatusCollectible
|
||||
&& isEmoji && !isPopular && !isChatEmojiSet
|
||||
&& (!isInstalled || (!isCurrentUserPremium && !isSavedMessages));
|
||||
const addSetButtonText = useMemo(() => {
|
||||
|
||||
@ -143,6 +143,11 @@
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.stickyFooter {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.withHeader {
|
||||
mask-image:
|
||||
linear-gradient(
|
||||
|
||||
@ -43,6 +43,10 @@ export type ModalProps = {
|
||||
height?: ModalHeight;
|
||||
noBackdrop?: boolean;
|
||||
noLightDismiss?: boolean;
|
||||
noScrollable?: boolean;
|
||||
noContentInlinePadding?: boolean;
|
||||
keepMounted?: boolean;
|
||||
stickyFooter?: TeactNode;
|
||||
ariaLabel?: string;
|
||||
noContainment?: boolean;
|
||||
onClose: NoneToVoidFunction;
|
||||
@ -114,10 +118,15 @@ const Modal = ({
|
||||
height = 'regular',
|
||||
noBackdrop,
|
||||
noLightDismiss,
|
||||
noScrollable,
|
||||
noContentInlinePadding,
|
||||
keepMounted,
|
||||
stickyFooter,
|
||||
ariaLabel,
|
||||
noContainment,
|
||||
onClose,
|
||||
}: ModalProps) => {
|
||||
const [hasEverOpened, setHasEverOpened] = useState(Boolean(isOpen));
|
||||
const [shouldRender, setShouldRender] = useState(Boolean(isOpen));
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const [hasTitle, setHasTitle] = useState(false);
|
||||
@ -134,11 +143,14 @@ const Modal = ({
|
||||
const frozenProps = useFrozenProps({
|
||||
header,
|
||||
children,
|
||||
stickyFooter,
|
||||
dialogClassName,
|
||||
contentClassName,
|
||||
width,
|
||||
height,
|
||||
noBackdrop,
|
||||
noScrollable,
|
||||
noContentInlinePadding,
|
||||
ariaLabel,
|
||||
noContainment,
|
||||
}, !isOpen);
|
||||
@ -193,6 +205,12 @@ const Modal = ({
|
||||
titleId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && !hasEverOpened) {
|
||||
setHasEverOpened(true);
|
||||
}
|
||||
}, [hasEverOpened, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
cleanupCloseAnimation();
|
||||
@ -322,7 +340,7 @@ const Modal = ({
|
||||
handleRequestClose();
|
||||
});
|
||||
|
||||
if (!shouldRender) {
|
||||
if (!shouldRender && !(keepMounted && hasEverOpened)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -355,7 +373,8 @@ const Modal = ({
|
||||
)}
|
||||
|
||||
<Surface
|
||||
scrollable
|
||||
scrollable={!frozenProps.noScrollable}
|
||||
noPadding={frozenProps.noContentInlinePadding}
|
||||
className={buildClassName(
|
||||
styles.content,
|
||||
shouldShowHeader && styles.withHeader,
|
||||
@ -366,6 +385,11 @@ const Modal = ({
|
||||
{frozenProps.children}
|
||||
</div>
|
||||
</Surface>
|
||||
{Boolean(frozenProps.stickyFooter) && (
|
||||
<div className={styles.stickyFooter}>
|
||||
{frozenProps.stickyFooter}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</dialog>
|
||||
</ModalContext.Provider>
|
||||
|
||||
9
src/components/gili/templates/CheckboxField.module.scss
Normal file
9
src/components/gili/templates/CheckboxField.module.scss
Normal file
@ -0,0 +1,9 @@
|
||||
@layer ui.templates {
|
||||
.centered {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.centeredControl {
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
import { memo } from '../../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import Control, {
|
||||
ControlDescription,
|
||||
ControlLabel,
|
||||
@ -7,11 +9,14 @@ import Control, {
|
||||
import Interactive from '../layout/Interactive';
|
||||
import Checkbox from '../primitives/Checkbox';
|
||||
|
||||
import styles from './CheckboxField.module.scss';
|
||||
|
||||
type Props = Omit<React.ComponentProps<typeof Checkbox>, 'className' | 'disabled'> & {
|
||||
label: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
isCentered?: boolean;
|
||||
className?: string;
|
||||
controlClassName?: string;
|
||||
labelClassName?: string;
|
||||
@ -24,6 +29,7 @@ const CheckboxField = ({
|
||||
description,
|
||||
disabled,
|
||||
loading,
|
||||
isCentered,
|
||||
className,
|
||||
controlClassName,
|
||||
labelClassName,
|
||||
@ -38,9 +44,9 @@ const CheckboxField = ({
|
||||
ripple={ripple}
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
className={className}
|
||||
className={buildClassName(className, isCentered && styles.centered)}
|
||||
>
|
||||
<Control className={controlClassName}>
|
||||
<Control className={buildClassName(controlClassName, isCentered && styles.centeredControl)}>
|
||||
<Checkbox {...checkboxProps} />
|
||||
<ControlLabel className={labelClassName}>{label}</ControlLabel>
|
||||
{description !== undefined ? (
|
||||
|
||||
@ -64,6 +64,7 @@ const Dialogs = ({ dialogs, currentMessageList }: StateProps) => {
|
||||
onClose={closeModal}
|
||||
className="confirm"
|
||||
title={lang('ShareYouPhoneNumberTitle')}
|
||||
isNativeDialog
|
||||
onCloseAnimationEnd={dismissDialog}
|
||||
>
|
||||
{lang(
|
||||
@ -94,6 +95,7 @@ const Dialogs = ({ dialogs, currentMessageList }: StateProps) => {
|
||||
onCloseAnimationEnd={dismissDialog}
|
||||
className="error"
|
||||
title={title}
|
||||
isNativeDialog
|
||||
>
|
||||
{renderedText}
|
||||
<div className="dialog-buttons mt-2">
|
||||
|
||||
@ -32,7 +32,9 @@
|
||||
.resultArea {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.resultAreaAnimated {
|
||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||
transition: height 0.1s;
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ type AiEditorResultAreaProps = {
|
||||
isLoading?: boolean;
|
||||
transitionKey?: number;
|
||||
className?: string;
|
||||
loadingElement?: TeactNode;
|
||||
children: TeactNode;
|
||||
};
|
||||
|
||||
@ -30,6 +31,7 @@ export const AiEditorResultArea = memo(({
|
||||
isLoading,
|
||||
transitionKey,
|
||||
className,
|
||||
loadingElement,
|
||||
children,
|
||||
}: AiEditorResultAreaProps) => {
|
||||
const contentRef = useRef<HTMLDivElement>();
|
||||
@ -45,15 +47,16 @@ export const AiEditorResultArea = memo(({
|
||||
});
|
||||
}, [children, isLoading, transitionKey]);
|
||||
|
||||
const displayHeight = height ?? MIN_HEIGHT;
|
||||
const hasInitialized = height !== undefined;
|
||||
const displayHeight = hasInitialized ? height : (isLoading ? MIN_HEIGHT : undefined);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(styles.resultArea, className)}
|
||||
style={`height: ${displayHeight}px`}
|
||||
className={buildClassName(styles.resultArea, hasInitialized && styles.resultAreaAnimated, className)}
|
||||
style={displayHeight !== undefined ? `height: ${displayHeight}px` : undefined}
|
||||
>
|
||||
<div className={buildClassName(styles.loadingContainer, !isLoading && styles.hidden)}>
|
||||
<TextLoadingPlaceholder lines={6} />
|
||||
{loadingElement || <TextLoadingPlaceholder lines={6} />}
|
||||
</div>
|
||||
<Transition
|
||||
name="fade"
|
||||
|
||||
@ -77,6 +77,7 @@
|
||||
padding: 0 1rem 1rem;
|
||||
|
||||
&::before {
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
|
||||
@ -24,6 +24,38 @@
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.tabListWrapper {
|
||||
position: relative;
|
||||
min-height: 4.125rem;
|
||||
}
|
||||
|
||||
.tabListSkeleton {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
align-items: center;
|
||||
|
||||
padding: 0.625rem 0;
|
||||
|
||||
opacity: 1;
|
||||
background: var(--color-background);
|
||||
|
||||
transition: opacity 0.2s ease-out;
|
||||
}
|
||||
|
||||
.tabListSkeletonHidden {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tabSkeleton {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
.textLabel,
|
||||
.resultLabel {
|
||||
font-size: 0.875rem;
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { memo, useMemo } from '../../../../lib/teact/teact';
|
||||
import { memo, useMemo, useState } from '../../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../../global';
|
||||
|
||||
import type {
|
||||
ApiAiComposeToneType, ApiComposedMessageWithAI, ApiFormattedText, ApiInputAiComposeTone,
|
||||
} from '../../../../api/types';
|
||||
import type { MenuItemContextAction } from '../../../ui/ListItem';
|
||||
import type { TabWithProperties } from '../../../ui/TabList';
|
||||
|
||||
import { TME_LINK_PREFIX } from '../../../../config';
|
||||
import { selectTabState } from '../../../../global/selectors';
|
||||
import { compareAiTones, getInputTone } from '../../../../util/aiComposeTones';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../../util/memo';
|
||||
@ -15,9 +18,12 @@ import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
|
||||
import CheckboxField from '../../../gili/templates/CheckboxField';
|
||||
import ConfirmDialog from '../../../ui/ConfirmDialog';
|
||||
import Skeleton from '../../../ui/placeholder/Skeleton';
|
||||
import TabList from '../../../ui/TabList';
|
||||
import Transition from '../../../ui/Transition';
|
||||
import { AiEditorCopyButton, AiEditorErrorMessage, AiEditorResultArea } from './AiEditorShared';
|
||||
import AiToneEditorModal from './AiToneEditorModal';
|
||||
|
||||
import sharedStyles from './AiEditorShared.module.scss';
|
||||
import modalStyles from './AiMessageEditorModal.module.scss';
|
||||
@ -35,6 +41,7 @@ type OwnProps = {
|
||||
|
||||
type StateProps = {
|
||||
tones: ApiAiComposeToneType[];
|
||||
isAiToneEditorOpen?: boolean;
|
||||
};
|
||||
|
||||
const AiTextStyleEditor = ({
|
||||
@ -46,28 +53,97 @@ const AiTextStyleEditor = ({
|
||||
error,
|
||||
isPremium,
|
||||
tones,
|
||||
isAiToneEditorOpen,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
setAiMessageEditorStyleOptions,
|
||||
composeWithAiMessageEditor,
|
||||
openAiToneEditorModal,
|
||||
closeAiMessageEditorModal,
|
||||
deleteAiTone,
|
||||
openChatWithDraft,
|
||||
} = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const [toneToDelete, setToneToDelete] = useState<ApiInputAiComposeTone>();
|
||||
const [isCreatorDelete, setIsCreatorDelete] = useState(false);
|
||||
|
||||
const handleConfirmDelete = useLastCallback(() => {
|
||||
if (!toneToDelete) return;
|
||||
deleteAiTone({ tone: toneToDelete });
|
||||
setToneToDelete(undefined);
|
||||
});
|
||||
|
||||
const handleCloseDeleteConfirm = useLastCallback(() => {
|
||||
setToneToDelete(undefined);
|
||||
});
|
||||
|
||||
const hasResult = Boolean(result?.resultText);
|
||||
const hasRequest = Boolean(selectedTone) || shouldEmojify;
|
||||
const shouldShowError = Boolean(error) && hasRequest;
|
||||
|
||||
const styleTabs = useMemo((): TabWithProperties[] => tones.map((entry) => ({
|
||||
customEmojiDocumentId: entry.emojiId,
|
||||
title: entry.title,
|
||||
})), [tones]);
|
||||
const buildContextActions = useLastCallback((entry: ApiAiComposeToneType): MenuItemContextAction[] | undefined => {
|
||||
if (!('id' in entry)) return undefined;
|
||||
|
||||
const tone = getInputTone(entry);
|
||||
const actions: MenuItemContextAction[] = [];
|
||||
|
||||
if (entry.isCreator) {
|
||||
actions.push({
|
||||
title: lang('AiToneEditStyle'),
|
||||
icon: 'edit',
|
||||
handler: () => {
|
||||
openAiToneEditorModal({ toneToEdit: entry });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
actions.push({
|
||||
title: lang('AiToneShareStyle'),
|
||||
icon: 'forward',
|
||||
handler: () => {
|
||||
closeAiMessageEditorModal();
|
||||
openChatWithDraft({ text: { text: `${TME_LINK_PREFIX}addstyle/${entry.slug}` } });
|
||||
},
|
||||
});
|
||||
|
||||
actions.push({
|
||||
title: lang('AiToneDeleteStyle'),
|
||||
icon: 'delete',
|
||||
destructive: true,
|
||||
handler: () => {
|
||||
setToneToDelete(tone);
|
||||
setIsCreatorDelete(Boolean(entry.isCreator));
|
||||
},
|
||||
});
|
||||
|
||||
return actions;
|
||||
});
|
||||
|
||||
const styleTabs = useMemo((): TabWithProperties[] => {
|
||||
const tabs: TabWithProperties[] = tones.map((entry) => ({
|
||||
customEmojiDocumentId: entry.emojiId,
|
||||
title: entry.title,
|
||||
contextActions: buildContextActions(entry),
|
||||
}));
|
||||
|
||||
if (tones.length) {
|
||||
tabs.push({ icon: 'add', title: lang('AiToneEditorNewStyle') });
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}, [tones, lang, buildContextActions]);
|
||||
|
||||
const activeStyleIndex = tones.findIndex(
|
||||
(entry) => compareAiTones(selectedTone, getInputTone(entry)),
|
||||
);
|
||||
|
||||
const handleStyleSelect = useLastCallback((index: number) => {
|
||||
if (index === tones.length) {
|
||||
openAiToneEditorModal();
|
||||
return;
|
||||
}
|
||||
const tone = getInputTone(tones[index]);
|
||||
setAiMessageEditorStyleOptions({ selectedTone: tone });
|
||||
composeWithAiMessageEditor({ tone, isEmojify: shouldEmojify });
|
||||
@ -105,15 +181,26 @@ const AiTextStyleEditor = ({
|
||||
|
||||
return (
|
||||
<div className={buildClassName(modalStyles.editorBlock, styles.styleBlock)}>
|
||||
<TabList
|
||||
tabs={styleTabs}
|
||||
activeTab={activeStyleIndex}
|
||||
onSwitchTab={handleStyleSelect}
|
||||
className={styles.tabList}
|
||||
tabClassName={styles.tab}
|
||||
indicatorClassName={styles.tabListIndicator}
|
||||
itemAlignment="vertical"
|
||||
/>
|
||||
<div className={styles.tabListWrapper}>
|
||||
{styleTabs.length > 0 && (
|
||||
<TabList
|
||||
tabs={styleTabs}
|
||||
activeTab={activeStyleIndex}
|
||||
onSwitchTab={handleStyleSelect}
|
||||
className={styles.tabList}
|
||||
tabClassName={styles.tab}
|
||||
indicatorClassName={styles.tabListIndicator}
|
||||
itemAlignment="vertical"
|
||||
/>
|
||||
)}
|
||||
<div className={buildClassName(styles.tabListSkeleton, styleTabs.length && styles.tabListSkeletonHidden)}>
|
||||
<Skeleton className={styles.tabSkeleton} variant="round" animation="wave" />
|
||||
<Skeleton className={styles.tabSkeleton} variant="round" animation="wave" />
|
||||
<Skeleton className={styles.tabSkeleton} variant="round" animation="wave" />
|
||||
<Skeleton className={styles.tabSkeleton} variant="round" animation="wave" />
|
||||
<Skeleton className={styles.tabSkeleton} variant="round" animation="wave" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={sharedStyles.separator} />
|
||||
|
||||
@ -144,6 +231,16 @@ const AiTextStyleEditor = ({
|
||||
textToCopy={displayText?.text}
|
||||
isHidden={isLoading || shouldShowError || !displayText?.text}
|
||||
/>
|
||||
<AiToneEditorModal isOpen={Boolean(isAiToneEditorOpen)} />
|
||||
<ConfirmDialog
|
||||
isOpen={Boolean(toneToDelete)}
|
||||
title={lang('AiToneDeleteStyle')}
|
||||
text={lang(isCreatorDelete ? 'AiToneDeleteStyleConfirmOwn' : 'AiToneDeleteStyleConfirm')}
|
||||
confirmLabel={lang('Delete')}
|
||||
confirmIsDestructive
|
||||
onClose={handleCloseDeleteConfirm}
|
||||
confirmHandler={handleConfirmDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -152,6 +249,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
(global): Complete<StateProps> => {
|
||||
return {
|
||||
tones: global.aiComposeTones?.tones ?? MEMO_EMPTY_ARRAY,
|
||||
isAiToneEditorOpen: Boolean(selectTabState(global).aiToneEditorModal),
|
||||
};
|
||||
},
|
||||
)(AiTextStyleEditor));
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
.emojiRow {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.emojiButton {
|
||||
--custom-emoji-size: 3rem;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
|
||||
background: var(--color-background);
|
||||
outline: none;
|
||||
|
||||
transition: opacity 0.15s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
.emojiPlaceholderIcon {
|
||||
font-size: 2rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.input {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.promptInput {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.promptInput :global(.form-control) {
|
||||
min-height: 5.625rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
|
||||
.createButton {
|
||||
width: 100%;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
@ -0,0 +1,243 @@
|
||||
import { memo, useEffect, useMemo, useState } from '../../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../../global';
|
||||
|
||||
import type { ApiAiComposeTone } from '../../../../api/types';
|
||||
|
||||
import { selectTabState } from '../../../../global/selectors';
|
||||
import { getInputTone } from '../../../../util/aiComposeTones';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
|
||||
import useFlag from '../../../../hooks/useFlag';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
|
||||
import CustomEmoji from '../../../common/CustomEmoji';
|
||||
import Icon from '../../../common/icons/Icon';
|
||||
import CheckboxField from '../../../gili/templates/CheckboxField';
|
||||
import Button from '../../../ui/Button';
|
||||
import ConfirmDialog from '../../../ui/ConfirmDialog';
|
||||
import InputText from '../../../ui/InputText';
|
||||
import TextArea from '../../../ui/TextArea';
|
||||
import AiToneEmojiPickerModal from './AiToneEmojiPickerModal.async';
|
||||
import Island from '@gili/layout/Island';
|
||||
import Modal, { ModalCloseButton, ModalHeader, ModalTitle } from '@gili/modal/Modal';
|
||||
|
||||
import styles from './AiToneEditorModal.module.scss';
|
||||
|
||||
const EMOJI_SIZE = 48;
|
||||
const DEFAULT_TITLE_MAX_LENGTH = 12;
|
||||
const DEFAULT_PROMPT_MAX_LENGTH = 1024;
|
||||
|
||||
type OwnProps = {
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
toneToEdit?: ApiAiComposeTone;
|
||||
titleMaxLength?: number;
|
||||
promptMaxLength?: number;
|
||||
};
|
||||
|
||||
const AiToneEditorModal = ({
|
||||
isOpen,
|
||||
toneToEdit,
|
||||
titleMaxLength = DEFAULT_TITLE_MAX_LENGTH,
|
||||
promptMaxLength = DEFAULT_PROMPT_MAX_LENGTH,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
closeAiToneEditorModal,
|
||||
createAiTone,
|
||||
updateAiTone,
|
||||
deleteAiTone,
|
||||
} = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const isEditMode = Boolean(toneToEdit);
|
||||
|
||||
const [emojiId, setEmojiId] = useState<string | undefined>();
|
||||
const [title, setTitle] = useState('');
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [shouldDisplayAuthor, setShouldDisplayAuthor] = useState(false);
|
||||
const [isEmojiPickerOpen, openEmojiPicker, closeEmojiPicker] = useFlag();
|
||||
const [isDeleteConfirmOpen, openDeleteConfirm, closeDeleteConfirm] = useFlag();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
if (toneToEdit) {
|
||||
setEmojiId(toneToEdit.emojiId);
|
||||
setTitle(toneToEdit.title);
|
||||
setPrompt(toneToEdit.prompt || '');
|
||||
setShouldDisplayAuthor(Boolean(toneToEdit.authorId));
|
||||
} else {
|
||||
setEmojiId(undefined);
|
||||
setTitle('');
|
||||
setPrompt('');
|
||||
setShouldDisplayAuthor(false);
|
||||
}
|
||||
}, [isOpen, toneToEdit]);
|
||||
|
||||
const canSubmit = Boolean(emojiId && title.trim() && prompt.trim());
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
closeAiToneEditorModal();
|
||||
closeEmojiPicker();
|
||||
closeDeleteConfirm();
|
||||
});
|
||||
|
||||
const handleSubmit = useLastCallback(() => {
|
||||
if (!canSubmit) return;
|
||||
|
||||
if (isEditMode) {
|
||||
const tone = getInputTone(toneToEdit);
|
||||
updateAiTone({
|
||||
tone,
|
||||
title: title.trim(),
|
||||
emojiId: emojiId!,
|
||||
prompt: prompt.trim(),
|
||||
shouldDisplayAuthor: shouldDisplayAuthor || undefined,
|
||||
});
|
||||
} else {
|
||||
createAiTone({
|
||||
title: title.trim(),
|
||||
emojiId: emojiId!,
|
||||
prompt: prompt.trim(),
|
||||
shouldDisplayAuthor: shouldDisplayAuthor || undefined,
|
||||
});
|
||||
}
|
||||
handleClose();
|
||||
});
|
||||
|
||||
const handleDelete = useLastCallback(() => {
|
||||
if (!toneToEdit) return;
|
||||
const tone = getInputTone(toneToEdit);
|
||||
deleteAiTone({ tone });
|
||||
closeDeleteConfirm();
|
||||
handleClose();
|
||||
});
|
||||
|
||||
const handleTitleChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(e.target.value);
|
||||
});
|
||||
|
||||
const handlePromptChange = useLastCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setPrompt(e.target.value);
|
||||
});
|
||||
|
||||
const handleEmojiSelect = useLastCallback((emojiDocumentId: string) => {
|
||||
setEmojiId(emojiDocumentId);
|
||||
closeEmojiPicker();
|
||||
});
|
||||
|
||||
const modalTitle = lang(isEditMode ? 'AiToneEditorEditTitle' : 'AiToneEditorTitle');
|
||||
|
||||
const renderHeader = useMemo(() => (
|
||||
<ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalTitle>{modalTitle}</ModalTitle>
|
||||
</ModalHeader>
|
||||
), [modalTitle]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
header={renderHeader}
|
||||
ariaLabel={modalTitle}
|
||||
width="slim"
|
||||
>
|
||||
<div className={styles.emojiRow}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.emojiButton}
|
||||
onClick={openEmojiPicker}
|
||||
>
|
||||
{emojiId ? (
|
||||
<CustomEmoji documentId={emojiId} size={EMOJI_SIZE} />
|
||||
) : (
|
||||
<Icon name="smile" className={styles.emojiPlaceholderIcon} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Island>
|
||||
<InputText
|
||||
className={styles.input}
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
placeholder={lang('AiToneEditorNamePlaceholder')}
|
||||
maxLength={titleMaxLength}
|
||||
hasLengthIndicator
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
className={buildClassName(styles.input, styles.promptInput)}
|
||||
value={prompt}
|
||||
onChange={handlePromptChange}
|
||||
placeholder={lang('AiToneEditorPromptPlaceholder')}
|
||||
maxLength={promptMaxLength}
|
||||
hasLengthIndicator
|
||||
noReplaceNewlines
|
||||
/>
|
||||
</Island>
|
||||
|
||||
<Island>
|
||||
<CheckboxField
|
||||
label={lang('AiToneEditorDisplayAuthor')}
|
||||
checked={shouldDisplayAuthor}
|
||||
isRound
|
||||
isCentered
|
||||
onChange={setShouldDisplayAuthor}
|
||||
/>
|
||||
</Island>
|
||||
|
||||
<div className={styles.footer}>
|
||||
{isEditMode && (
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
onClick={openDeleteConfirm}
|
||||
color="danger"
|
||||
isText
|
||||
>
|
||||
{lang('AiToneDeleteStyle')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className={styles.createButton}
|
||||
onClick={handleSubmit}
|
||||
disabled={!canSubmit}
|
||||
>
|
||||
{lang(isEditMode ? 'Save' : 'Create')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<AiToneEmojiPickerModal
|
||||
isOpen={isEmojiPickerOpen}
|
||||
onEmojiSelect={handleEmojiSelect}
|
||||
onClose={closeEmojiPicker}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={isDeleteConfirmOpen}
|
||||
title={lang('AiToneDeleteStyle')}
|
||||
text={lang('AiToneDeleteStyleConfirm')}
|
||||
confirmLabel={lang('Delete')}
|
||||
confirmIsDestructive
|
||||
onClose={closeDeleteConfirm}
|
||||
confirmHandler={handleDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): Complete<StateProps> => {
|
||||
return {
|
||||
toneToEdit: selectTabState(global).aiToneEditorModal?.toneToEdit,
|
||||
titleMaxLength: global.appConfig.aiComposeToneTitleLengthMax,
|
||||
promptMaxLength: global.appConfig.aiComposeTonePromptLengthMax,
|
||||
};
|
||||
},
|
||||
)(AiToneEditorModal));
|
||||
@ -0,0 +1,14 @@
|
||||
import type { OwnProps } from './AiToneEmojiPickerModal';
|
||||
|
||||
import { Bundles } from '../../../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../../../hooks/useModuleLoader';
|
||||
|
||||
const AiToneEmojiPickerModalAsync = (props: OwnProps) => {
|
||||
const { isOpen } = props;
|
||||
const AiToneEmojiPickerModal = useModuleLoader(Bundles.Extra, 'AiToneEmojiPickerModal', !isOpen);
|
||||
|
||||
return AiToneEmojiPickerModal ? <AiToneEmojiPickerModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default AiToneEmojiPickerModalAsync;
|
||||
@ -0,0 +1,9 @@
|
||||
.content {
|
||||
--modal-content-block-padding: 0rem;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.picker {
|
||||
height: var(--symbol-menu-height);
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
import { memo, useMemo } from '../../../../lib/teact/teact';
|
||||
|
||||
import type { ApiSticker } from '../../../../api/types';
|
||||
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
|
||||
import CustomEmojiPicker from '../../../common/CustomEmojiPicker';
|
||||
import Modal, { ModalCloseButton, ModalHeader, ModalTitle } from '@gili/modal/Modal';
|
||||
|
||||
import styles from './AiToneEmojiPickerModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen: boolean;
|
||||
onEmojiSelect: (emojiId: string) => void;
|
||||
onClose: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const AiToneEmojiPickerModal = ({
|
||||
isOpen,
|
||||
onEmojiSelect,
|
||||
onClose,
|
||||
}: OwnProps) => {
|
||||
const lang = useLang();
|
||||
|
||||
const handleEmojiSelect = useLastCallback((sticker: ApiSticker) => {
|
||||
onEmojiSelect(sticker.id);
|
||||
});
|
||||
|
||||
const renderHeader = useMemo(() => (
|
||||
<ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalTitle>{lang('AiToneEditorSelectEmoji')}</ModalTitle>
|
||||
</ModalHeader>
|
||||
), [lang]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
header={renderHeader}
|
||||
ariaLabel={lang('AiToneEditorSelectEmoji')}
|
||||
width="slim"
|
||||
noScrollable
|
||||
noContentInlinePadding
|
||||
keepMounted
|
||||
contentClassName={styles.content}
|
||||
>
|
||||
<div className={styles.picker}>
|
||||
<CustomEmojiPicker
|
||||
idPrefix="ai-tone-icon-"
|
||||
loadAndPlay={isOpen}
|
||||
isHidden={!isOpen}
|
||||
noAddButton
|
||||
onCustomEmojiSelect={handleEmojiSelect}
|
||||
onDismiss={onClose}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AiToneEmojiPickerModal);
|
||||
@ -197,6 +197,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
&--ai-tone-emoji {
|
||||
--custom-emoji-size: 3rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&--ai-tone {
|
||||
max-width: 20rem;
|
||||
|
||||
.WebPage--ai-tone-emoji {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1921px) {
|
||||
max-width: none;
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import Audio from '../../common/Audio';
|
||||
import CustomEmoji from '../../common/CustomEmoji';
|
||||
import Document from '../../common/Document';
|
||||
import EmojiIconBackground from '../../common/embedded/EmojiIconBackground';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
@ -37,6 +38,7 @@ const MAX_TEXT_LENGTH = 170; // symbols
|
||||
const WEBPAGE_STORY_TYPE = 'telegram_story';
|
||||
const WEBPAGE_GIFT_TYPE = 'telegram_nft';
|
||||
const WEBPAGE_AUCTION_TYPE = 'telegram_auction';
|
||||
const WEBPAGE_AI_TONE_TYPE = 'telegram_aicomposetone';
|
||||
const STICKER_SIZE = 80;
|
||||
const EMOJI_SIZE = 38;
|
||||
|
||||
@ -148,6 +150,7 @@ const WebPage = ({
|
||||
const isStory = type === WEBPAGE_STORY_TYPE;
|
||||
const isGift = type === WEBPAGE_GIFT_TYPE;
|
||||
const isAuction = type === WEBPAGE_AUCTION_TYPE;
|
||||
const isAiTone = type === WEBPAGE_AI_TONE_TYPE;
|
||||
const isExpiredStory = story && 'isDeleted' in story;
|
||||
|
||||
const resultType = stickers?.isEmoji ? 'telegram_emojiset' : type;
|
||||
@ -157,8 +160,9 @@ const WebPage = ({
|
||||
const quickButtonIcon = getWebpageButtonIcon(resultType);
|
||||
|
||||
const truncatedDescription = trimText(description, MAX_TEXT_LENGTH);
|
||||
const aiToneEmojiId = isAiTone ? webPage.aiComposeToneEmojiId : undefined;
|
||||
const isArticle = Boolean(truncatedDescription || title || siteName);
|
||||
let isSquarePhoto = Boolean(stickers);
|
||||
let isSquarePhoto = Boolean(stickers) || Boolean(aiToneEmojiId);
|
||||
if (isArticle && webPage?.photo && !webPage.video && !webPage.document) {
|
||||
isSquarePhoto = getIsSmallPhoto(webPage, mediaSize);
|
||||
}
|
||||
@ -173,6 +177,7 @@ const WebPage = ({
|
||||
document && 'with-document',
|
||||
quickButtonTitle && 'with-quick-button',
|
||||
(isGift || isAuction) && 'with-gift',
|
||||
isAiTone && 'WebPage--ai-tone',
|
||||
);
|
||||
|
||||
function renderQuickButton() {
|
||||
@ -323,6 +328,11 @@ const WebPage = ({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{aiToneEmojiId && (
|
||||
<div className="media-inner square-image WebPage--ai-tone-emoji">
|
||||
<CustomEmoji documentId={aiToneEmojiId} size={STICKER_SIZE} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{quickButtonTitle && renderQuickButton()}
|
||||
</PeerColorWrapper>
|
||||
|
||||
@ -44,6 +44,8 @@ export function getWebpageButtonLangKey(type?: string, auctionEndDate?: number):
|
||||
const isFinished = auctionEndDate !== undefined && auctionEndDate < getServerTime();
|
||||
return isFinished ? 'PollViewResults' : 'GiftAuctionJoin';
|
||||
}
|
||||
case 'telegram_aicomposetone':
|
||||
return 'ViewButtonAiStyle';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import WebAppsCloseConfirmationModal from '../main/WebAppsCloseConfirmationModal
|
||||
import AiMessageEditorModal from '../middle/composer/AiMessageEditorModal/AiMessageEditorModal.async';
|
||||
import AboutAdsModal from './aboutAds/AboutAdsModal.async';
|
||||
import AgeVerificationModal from './ageVerification/AgeVerificationModal.async';
|
||||
import AiTonePreviewModal from './aiTonePreview/AiTonePreviewModal.async';
|
||||
import AttachBotInstallModal from './attachBotInstall/AttachBotInstallModal.async';
|
||||
import BirthdaySetupModal from './birthday/BirthdaySetupModal.async';
|
||||
import BoostModal from './boost/BoostModal.async';
|
||||
@ -157,7 +158,8 @@ type ModalKey = keyof Pick<TabState,
|
||||
'isQuickChatPickerOpen' |
|
||||
'isCocoonModalOpen' |
|
||||
'editRankModal' |
|
||||
'rankModal'
|
||||
'rankModal' |
|
||||
'aiTonePreviewModal'
|
||||
>;
|
||||
type WrappedModalKey = 'pollModal';
|
||||
type LegacyModalKey = Exclude<ModalKey, WrappedModalKey>;
|
||||
@ -293,6 +295,7 @@ const LEGACY_MODALS: LegacyModalRegistry = {
|
||||
isCocoonModalOpen: CocoonModal,
|
||||
editRankModal: EditRankModal,
|
||||
rankModal: RankModal,
|
||||
aiTonePreviewModal: AiTonePreviewModal,
|
||||
};
|
||||
const WRAPPED_MODALS: WrappedModalRegistry = {
|
||||
pollModal: PollModal,
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
import type { OwnProps } from './AiTonePreviewModal';
|
||||
|
||||
import { Bundles } from '../../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const AiTonePreviewModalAsync = (props: OwnProps) => {
|
||||
const { modal } = props;
|
||||
const AiTonePreviewModal = useModuleLoader(Bundles.Extra, 'AiTonePreviewModal', !modal);
|
||||
|
||||
return AiTonePreviewModal ? <AiTonePreviewModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default AiTonePreviewModalAsync;
|
||||
@ -0,0 +1,116 @@
|
||||
.modal {
|
||||
--modal-header-height: 1rem;
|
||||
--modal-max-height: min(39.3125rem, 80dvh);
|
||||
}
|
||||
|
||||
.emojiRow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.emojiCircle {
|
||||
--custom-emoji-size: 3rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
max-width: 13rem;
|
||||
margin-top: 0.25rem;
|
||||
margin-inline: auto;
|
||||
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.exampleWrapper {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: 1rem;
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
.skeletonBefore {
|
||||
margin-bottom: 1.0625rem;
|
||||
}
|
||||
|
||||
.skeletonAfter {
|
||||
margin-top: 0.9375rem;
|
||||
}
|
||||
|
||||
.anotherExampleButton {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
|
||||
min-height: 1.75rem;
|
||||
margin-inline-start: auto;
|
||||
padding: 0.25rem 0.625rem;
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
|
||||
font-size: 0.8125rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1.25rem;
|
||||
color: var(--color-primary);
|
||||
|
||||
background: var(--color-background-secondary);
|
||||
outline: none;
|
||||
|
||||
transition: opacity 0.15s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
.anotherExampleIcon {
|
||||
font-size: 0.9375rem;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 0 1rem 1rem;
|
||||
|
||||
&::before {
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 1.125rem;
|
||||
|
||||
background: linear-gradient(to top, var(--color-background-secondary) 0%, transparent 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.addButton {
|
||||
flex: 1;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
277
src/components/modals/aiTonePreview/AiTonePreviewModal.tsx
Normal file
277
src/components/modals/aiTonePreview/AiTonePreviewModal.tsx
Normal file
@ -0,0 +1,277 @@
|
||||
import { memo, useMemo, useRef, useState } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiUser } from '../../../api/types';
|
||||
import type { TabState } from '../../../global/types';
|
||||
|
||||
import { selectUser } from '../../../global/selectors';
|
||||
import { getInputTone } from '../../../util/aiComposeTones';
|
||||
import calcTextLineHeightAndCount from '../../../util/element/calcTextLineHeightAndCount';
|
||||
import formatUsername from '../../common/helpers/formatUsername';
|
||||
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import CustomEmoji from '../../common/CustomEmoji';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import { AiEditorResultArea } from '../../middle/composer/AiMessageEditorModal/AiEditorShared';
|
||||
import Button from '../../ui/Button';
|
||||
import Link from '../../ui/Link';
|
||||
import TextLoadingPlaceholder from '../../ui/placeholder/TextLoadingPlaceholder';
|
||||
import Modal, { ModalCloseButton, ModalHeader } from '@gili/modal/Modal';
|
||||
|
||||
import sharedStyles from '../../middle/composer/AiMessageEditorModal/AiEditorShared.module.scss';
|
||||
import styles from './AiTonePreviewModal.module.scss';
|
||||
|
||||
const DEFAULT_MAX_EXAMPLES = 3;
|
||||
const DEFAULT_LINES = 4;
|
||||
const EMOJI_SIZE = 48;
|
||||
|
||||
export type OwnProps = {
|
||||
modal: TabState['aiTonePreviewModal'];
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
author?: ApiUser;
|
||||
maxExamples?: number;
|
||||
};
|
||||
|
||||
const AiTonePreviewModal = ({ modal, author, maxExamples = DEFAULT_MAX_EXAMPLES }: OwnProps & StateProps) => {
|
||||
const {
|
||||
closeAiTonePreview,
|
||||
saveAiTone,
|
||||
loadAiTonePreviewExample,
|
||||
openChat,
|
||||
} = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const isOpen = Boolean(modal);
|
||||
const tone = modal?.tone;
|
||||
const example = modal?.example;
|
||||
const isAlreadyAdded = modal?.isAlreadyAdded;
|
||||
const slug = modal?.slug;
|
||||
const hasExampleError = modal?.hasExampleError;
|
||||
|
||||
const [exampleNum, setExampleNum] = useState(0);
|
||||
const spinCountRef = useRef(0);
|
||||
const beforeRef = useRef<HTMLDivElement>();
|
||||
const afterRef = useRef<HTMLDivElement>();
|
||||
const prevLinesRef = useRef({ before: DEFAULT_LINES, after: DEFAULT_LINES });
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
closeAiTonePreview();
|
||||
setExampleNum(0);
|
||||
spinCountRef.current = 0;
|
||||
prevLinesRef.current = { before: DEFAULT_LINES, after: DEFAULT_LINES };
|
||||
});
|
||||
|
||||
const handleAdd = useLastCallback(() => {
|
||||
if (!tone) return;
|
||||
saveAiTone({ tone: getInputTone(tone) });
|
||||
});
|
||||
|
||||
const handleRemove = useLastCallback(() => {
|
||||
if (!tone) return;
|
||||
saveAiTone({ tone: getInputTone(tone), unsave: true });
|
||||
});
|
||||
|
||||
const handleAnotherExample = useLastCallback(() => {
|
||||
if (!slug) return;
|
||||
|
||||
if (beforeRef.current && afterRef.current) {
|
||||
prevLinesRef.current = {
|
||||
before: Math.max(DEFAULT_LINES, calcTextLineHeightAndCount(beforeRef.current).totalLines),
|
||||
after: Math.max(DEFAULT_LINES, calcTextLineHeightAndCount(afterRef.current).totalLines),
|
||||
};
|
||||
}
|
||||
|
||||
const nextNum = (exampleNum + 1) % maxExamples;
|
||||
spinCountRef.current += 1;
|
||||
setExampleNum(nextNum);
|
||||
loadAiTonePreviewExample({
|
||||
tone: { type: 'slug', slug },
|
||||
num: nextNum,
|
||||
});
|
||||
});
|
||||
|
||||
const handleAuthorClick = useLastCallback(() => {
|
||||
if (!tone?.authorId) return;
|
||||
handleClose();
|
||||
openChat({ id: tone.authorId });
|
||||
});
|
||||
|
||||
const renderHeader = useMemo(() => (
|
||||
<ModalHeader>
|
||||
<ModalCloseButton />
|
||||
</ModalHeader>
|
||||
), []);
|
||||
|
||||
function renderFooterInfo() {
|
||||
if (!tone) return undefined;
|
||||
|
||||
const installsCount = tone.installsCount || 0;
|
||||
const authorName = author?.usernames?.[0]?.username;
|
||||
|
||||
if (!installsCount && !authorName) return undefined;
|
||||
|
||||
const authorLink = authorName
|
||||
? <Link isPrimary onClick={handleAuthorClick}>{formatUsername(authorName)}</Link>
|
||||
: undefined;
|
||||
|
||||
const usedByText = installsCount
|
||||
? lang('AiTonePreviewUsedBy', { count: lang.number(installsCount) })
|
||||
: undefined;
|
||||
const createdByText = authorLink
|
||||
? lang('AiTonePreviewCreatedBy', { author: authorLink }, { withNodes: true })
|
||||
: undefined;
|
||||
|
||||
if (usedByText && createdByText) {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{lang('AiTonePreviewUsedByCreatedBy', {
|
||||
usedBy: usedByText,
|
||||
createdBy: createdByText,
|
||||
}, { withNodes: true })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{usedByText || createdByText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { before: beforeLines, after: afterLines } = prevLinesRef.current;
|
||||
|
||||
const exampleLoadingElement = useMemo(() => (
|
||||
<>
|
||||
<TextLoadingPlaceholder lines={beforeLines} className={styles.skeletonBefore} />
|
||||
<div className={sharedStyles.separator} />
|
||||
<div className={sharedStyles.labelRow}>
|
||||
<span className={sharedStyles.label}>{lang('AiTonePreviewAfter')}</span>
|
||||
</div>
|
||||
<TextLoadingPlaceholder lines={afterLines} className={styles.skeletonAfter} />
|
||||
</>
|
||||
), [lang, beforeLines, afterLines]);
|
||||
|
||||
function renderExampleContent() {
|
||||
if (hasExampleError) {
|
||||
return (
|
||||
<div className={sharedStyles.errorMessage}>
|
||||
{lang('AiMessageEditorGenericError')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!example) return undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={beforeRef} className={sharedStyles.resultContent}>
|
||||
{renderTextWithEntities(example.from)}
|
||||
</div>
|
||||
<div className={sharedStyles.separator} />
|
||||
<div className={sharedStyles.labelRow}>
|
||||
<span className={sharedStyles.label}>{lang('AiTonePreviewAfter')}</span>
|
||||
</div>
|
||||
<div ref={afterRef} className={sharedStyles.resultContent}>
|
||||
{renderTextWithEntities(example.to)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const renderFooter = useMemo(() => {
|
||||
if (!tone) return undefined;
|
||||
return (
|
||||
<div className={styles.footer}>
|
||||
{isAlreadyAdded ? (
|
||||
<Button
|
||||
className={styles.addButton}
|
||||
onClick={tone.isCreator ? handleClose : handleRemove}
|
||||
color={tone.isCreator ? undefined : 'danger'}
|
||||
isText={!tone.isCreator}
|
||||
noForcedUpperCase
|
||||
>
|
||||
{lang(tone.isCreator ? 'Done' : 'AiTonePreviewRemoveStyle')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={styles.addButton}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
{lang('AiTonePreviewAddStyle')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [tone, isAlreadyAdded, lang, handleClose, handleRemove, handleAdd]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
header={renderHeader}
|
||||
stickyFooter={renderFooter}
|
||||
dialogClassName={styles.modal}
|
||||
width="slim"
|
||||
height="regular"
|
||||
>
|
||||
{tone && (
|
||||
<>
|
||||
{tone.emojiId && (
|
||||
<div className={styles.emojiRow}>
|
||||
<div className={styles.emojiCircle}>
|
||||
<CustomEmoji documentId={tone.emojiId} size={EMOJI_SIZE} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.title}>{tone.title}</div>
|
||||
<div className={styles.subtitle}>{lang('AiTonePreviewSubtitle')}</div>
|
||||
|
||||
<div className={styles.exampleWrapper}>
|
||||
<div className={sharedStyles.labelRow}>
|
||||
<span className={sharedStyles.label}>{lang('AiTonePreviewBefore')}</span>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.anotherExampleButton}
|
||||
onClick={handleAnotherExample}
|
||||
>
|
||||
<Icon
|
||||
name="reload-arrows"
|
||||
className={styles.anotherExampleIcon}
|
||||
style={`transform: rotate(${spinCountRef.current * 180}deg)`}
|
||||
/>
|
||||
{lang('AiTonePreviewAnotherExample')}
|
||||
</button>
|
||||
</div>
|
||||
<AiEditorResultArea
|
||||
isLoading={!example && !hasExampleError}
|
||||
transitionKey={exampleNum}
|
||||
loadingElement={exampleLoadingElement}
|
||||
>
|
||||
{renderExampleContent()}
|
||||
</AiEditorResultArea>
|
||||
</div>
|
||||
|
||||
{renderFooterInfo()}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { modal }): Complete<StateProps> => {
|
||||
const authorId = modal?.tone?.authorId;
|
||||
return {
|
||||
author: authorId ? selectUser(global, authorId) : undefined,
|
||||
maxExamples: global.appConfig.aiComposeToneExamplesNum,
|
||||
};
|
||||
},
|
||||
)(AiTonePreviewModal));
|
||||
@ -9,6 +9,8 @@ import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import AnimatedCounter from '../common/AnimatedCounter';
|
||||
|
||||
type OwnProps = {
|
||||
ref?: ElementRef<HTMLInputElement>;
|
||||
id?: string;
|
||||
@ -22,6 +24,7 @@ type OwnProps = {
|
||||
placeholder?: string;
|
||||
autoComplete?: string;
|
||||
maxLength?: number;
|
||||
hasLengthIndicator?: boolean;
|
||||
tabIndex?: number;
|
||||
title?: string;
|
||||
autoFocus?: boolean;
|
||||
@ -51,6 +54,7 @@ const InputText = ({
|
||||
autoComplete = 'off',
|
||||
inputMode,
|
||||
maxLength,
|
||||
hasLengthIndicator,
|
||||
tabIndex,
|
||||
title,
|
||||
autoFocus,
|
||||
@ -109,6 +113,11 @@ const InputText = ({
|
||||
{labelText && (
|
||||
<label htmlFor={id}>{labelText}</label>
|
||||
)}
|
||||
{hasLengthIndicator && maxLength !== undefined && (
|
||||
<div className="max-length-indicator">
|
||||
<AnimatedCounter text={Math.max(0, maxLength - (value || '').length).toString()} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -12,6 +12,8 @@ import buildClassName from '../../util/buildClassName';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import AnimatedCounter from '../common/AnimatedCounter';
|
||||
|
||||
type OwnProps = {
|
||||
ref?: ElementRef<HTMLTextAreaElement>;
|
||||
id?: string;
|
||||
@ -26,6 +28,7 @@ type OwnProps = {
|
||||
autoComplete?: string;
|
||||
maxLength?: number;
|
||||
maxLengthIndicator?: string;
|
||||
hasLengthIndicator?: boolean;
|
||||
tabIndex?: number;
|
||||
inputMode?: 'text' | 'none' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
|
||||
onChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
@ -52,6 +55,7 @@ const TextArea: FC<OwnProps> = ({
|
||||
inputMode,
|
||||
maxLength,
|
||||
maxLengthIndicator,
|
||||
hasLengthIndicator,
|
||||
tabIndex,
|
||||
onChange,
|
||||
onInput,
|
||||
@ -135,8 +139,12 @@ const TextArea: FC<OwnProps> = ({
|
||||
{labelText && (
|
||||
<label htmlFor={id}>{labelText}</label>
|
||||
)}
|
||||
{maxLengthIndicator && (
|
||||
<div className="max-length-indicator">{maxLengthIndicator}</div>
|
||||
{(maxLengthIndicator || (hasLengthIndicator && maxLength !== undefined)) && (
|
||||
<div className="max-length-indicator">
|
||||
<AnimatedCounter
|
||||
text={maxLengthIndicator || Math.max(0, maxLength! - (value || '').length).toString()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { ApiInputAiComposeTone } from '../../../api/types';
|
||||
import type { ActionReturnType, GlobalState } from '../../types';
|
||||
|
||||
import { compareAiTones, getToneCacheKey } from '../../../util/aiComposeTones';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
@ -6,6 +7,36 @@ import { callApi } from '../../../api/gramjs';
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import { selectTabState } from '../../selectors';
|
||||
import { selectCurrentLimit } from '../../selectors/limits';
|
||||
import { selectIsCurrentUserPremium } from '../../selectors/users';
|
||||
|
||||
export function showToneLimitNotification<T extends GlobalState>(
|
||||
global: T,
|
||||
actions: { showNotification: AnyFunction },
|
||||
tabId: number,
|
||||
): boolean {
|
||||
const isPremium = selectIsCurrentUserPremium(global);
|
||||
const limit = selectCurrentLimit(global, 'aiComposeToneSaved');
|
||||
|
||||
const customToneCount = (global.aiComposeTones?.tones || []).filter((t) => 'id' in t).length;
|
||||
if (customToneCount < limit) return false;
|
||||
|
||||
if (isPremium) {
|
||||
actions.showNotification({
|
||||
message: { key: 'AiToneLimitReachedPremium', variables: { limit: limit.toString() } },
|
||||
tabId,
|
||||
});
|
||||
} else {
|
||||
actions.showNotification({
|
||||
message: { key: 'AiToneLimitReached' },
|
||||
action: { action: 'openPremiumModal', payload: { tabId } },
|
||||
actionText: { key: 'PremiumMore' },
|
||||
tabId,
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildStyleCacheKey(tone?: ApiInputAiComposeTone, emojify?: boolean) {
|
||||
return `${tone ? getToneCacheKey(tone) : ''}_${emojify ? '1' : '0'}`;
|
||||
@ -148,6 +179,166 @@ addActionHandler('composeWithAiMessageEditor', async (global, actions, payload):
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('createAiTone', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
title, emojiId, prompt, shouldDisplayAuthor,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
if (showToneLimitNotification(global, actions, tabId)) return;
|
||||
|
||||
const result = await callApi('createAiTone', {
|
||||
title, emojiId, prompt, shouldDisplayAuthor,
|
||||
});
|
||||
|
||||
if (!result) return;
|
||||
|
||||
actions.closeAiToneEditorModal({ tabId });
|
||||
actions.loadAiComposeTones();
|
||||
actions.showNotification({
|
||||
title: { key: 'AiToneCreated', variables: { title } },
|
||||
message: { key: 'AiToneCreatedHint' },
|
||||
customEmojiIconId: emojiId,
|
||||
tabId,
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('deleteAiTone', async (global, actions, payload): Promise<void> => {
|
||||
const { tone, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const result = await callApi('deleteAiTone', { tone });
|
||||
|
||||
if (!result) {
|
||||
actions.showNotification({ message: { key: 'ErrorUnspecified' }, tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
actions.loadAiComposeTones();
|
||||
});
|
||||
|
||||
addActionHandler('updateAiTone', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
tone, title, emojiId, prompt, shouldDisplayAuthor,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
const updatedTone = await callApi('updateAiTone', {
|
||||
tone, title, emojiId, prompt, shouldDisplayAuthor,
|
||||
});
|
||||
|
||||
if (!updatedTone) return;
|
||||
|
||||
global = getGlobal();
|
||||
const currentTones = global.aiComposeTones?.tones || [];
|
||||
const updatedTones = 'id' in updatedTone
|
||||
? currentTones.map((t) => ('id' in t && t.id === updatedTone.id ? updatedTone : t))
|
||||
: currentTones;
|
||||
|
||||
global = {
|
||||
...global,
|
||||
aiComposeTones: {
|
||||
...global.aiComposeTones,
|
||||
tones: updatedTones,
|
||||
hash: global.aiComposeTones?.hash || '',
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
|
||||
actions.closeAiToneEditorModal({ tabId });
|
||||
actions.loadAiComposeTones();
|
||||
});
|
||||
|
||||
addActionHandler('openAiTonePreview', async (global, actions, payload): Promise<void> => {
|
||||
const { slug, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const result = await callApi('fetchAiTone', {
|
||||
tone: { type: 'slug', slug },
|
||||
});
|
||||
|
||||
if (!result?.tones.length) {
|
||||
actions.showNotification({ message: { key: 'ErrorUnspecified' }, tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
const tone = result.tones[0];
|
||||
if (!('id' in tone)) return;
|
||||
|
||||
const example = await callApi('fetchAiToneExample', {
|
||||
tone: { type: 'slug', slug },
|
||||
num: 0,
|
||||
});
|
||||
|
||||
global = getGlobal();
|
||||
const currentTones = global.aiComposeTones?.tones || [];
|
||||
const isAlreadyAdded = tone.isCreator || currentTones.some((t) => 'id' in t && t.id === tone.id);
|
||||
global = updateTabState(global, {
|
||||
aiTonePreviewModal: {
|
||||
slug,
|
||||
tone,
|
||||
isAlreadyAdded,
|
||||
example,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('closeAiTonePreview', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
return updateTabState(global, {
|
||||
aiTonePreviewModal: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('saveAiTone', async (global, actions, payload): Promise<void> => {
|
||||
const { tone, unsave, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
if (!unsave && showToneLimitNotification(global, actions, tabId)) return;
|
||||
|
||||
const result = await callApi('saveAiTone', { tone, unsave });
|
||||
if (!result) return;
|
||||
|
||||
actions.loadAiComposeTones();
|
||||
actions.closeAiTonePreview({ tabId });
|
||||
|
||||
if (!unsave) {
|
||||
actions.showNotification({
|
||||
message: { key: 'AiTonePreviewStyleAdded' },
|
||||
tabId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('loadAiTonePreviewExample', async (global, actions, payload): Promise<void> => {
|
||||
const { tone, num, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
// Clear current example to trigger loading state
|
||||
const currentModal = selectTabState(global, tabId).aiTonePreviewModal;
|
||||
if (currentModal) {
|
||||
global = updateTabState(global, {
|
||||
aiTonePreviewModal: { ...currentModal, example: undefined, hasExampleError: undefined },
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
}
|
||||
|
||||
const example = await callApi('fetchAiToneExample', { tone, num });
|
||||
|
||||
global = getGlobal();
|
||||
const previewModal = selectTabState(global, tabId).aiTonePreviewModal;
|
||||
if (!previewModal) return;
|
||||
|
||||
const openModalTone: ApiInputAiComposeTone = { type: 'slug', slug: previewModal.slug };
|
||||
if (!compareAiTones(openModalTone, tone)) return;
|
||||
|
||||
global = updateTabState(global, {
|
||||
aiTonePreviewModal: {
|
||||
...previewModal,
|
||||
example,
|
||||
hasExampleError: !example,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadAiComposeTones', async (global): Promise<void> => {
|
||||
const hash = global.aiComposeTones?.hash;
|
||||
const result = await callApi('fetchAiComposeTones', { hash });
|
||||
|
||||
@ -1754,6 +1754,12 @@ addActionHandler('openTelegramLink', async (global, actions, payload): Promise<v
|
||||
return;
|
||||
}
|
||||
|
||||
if (part1 === 'addstyle') {
|
||||
if (!part2) return;
|
||||
actions.openAiTonePreview({ slug: part2, tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (part1 === 'share') {
|
||||
const text = formatShareText(params.url, params.text);
|
||||
openChatWithDraft({ text, tabId });
|
||||
|
||||
@ -6,6 +6,7 @@ import { updateTabState } from '../../reducers/tabs';
|
||||
import { selectTabState } from '../../selectors';
|
||||
import { selectCurrentMessageList } from '../../selectors/messages';
|
||||
import { selectTranslationLanguage } from '../../selectors/settings';
|
||||
import { showToneLimitNotification } from '../api/ai';
|
||||
|
||||
addActionHandler('openAiMessageEditorModal', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
@ -187,3 +188,23 @@ addActionHandler('clearAiMessageEditorPendingResult', (global, actions, payload)
|
||||
aiMessageEditorPendingResult: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('openAiToneEditorModal', (global, actions, payload): ActionReturnType => {
|
||||
const { toneToEdit, tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
if (!toneToEdit && showToneLimitNotification(global, actions, tabId)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return updateTabState(global, {
|
||||
aiToneEditorModal: { toneToEdit },
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closeAiToneEditorModal', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
return updateTabState(global, {
|
||||
aiToneEditorModal: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type {
|
||||
ApiAiComposeTone,
|
||||
ApiAttachBot,
|
||||
ApiAttachment,
|
||||
ApiBirthday,
|
||||
@ -2694,6 +2695,38 @@ export interface ActionPayloads {
|
||||
scheduleRepeatPeriod?: number;
|
||||
} & WithTabId) | undefined;
|
||||
clearAiMessageEditorPendingResult: WithTabId | undefined;
|
||||
openAiToneEditorModal: {
|
||||
toneToEdit?: ApiAiComposeTone;
|
||||
} & WithTabId | undefined;
|
||||
closeAiToneEditorModal: WithTabId | undefined;
|
||||
createAiTone: {
|
||||
title: string;
|
||||
emojiId: string;
|
||||
prompt: string;
|
||||
shouldDisplayAuthor?: boolean;
|
||||
} & WithTabId;
|
||||
updateAiTone: {
|
||||
tone: ApiInputAiComposeTone;
|
||||
title?: string;
|
||||
emojiId?: string;
|
||||
prompt?: string;
|
||||
shouldDisplayAuthor?: boolean;
|
||||
} & WithTabId;
|
||||
deleteAiTone: {
|
||||
tone: ApiInputAiComposeTone;
|
||||
} & WithTabId;
|
||||
openAiTonePreview: {
|
||||
slug: string;
|
||||
} & WithTabId;
|
||||
closeAiTonePreview: WithTabId | undefined;
|
||||
saveAiTone: {
|
||||
tone: ApiInputAiComposeTone;
|
||||
unsave?: boolean;
|
||||
} & WithTabId;
|
||||
loadAiTonePreviewExample: {
|
||||
tone: ApiInputAiComposeTone;
|
||||
num: number;
|
||||
} & WithTabId;
|
||||
|
||||
openGiveawayModal: ({
|
||||
chatId: string;
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import type {
|
||||
ApiAiComposeTone,
|
||||
ApiAiComposeToneExample,
|
||||
ApiAttachBot,
|
||||
ApiBirthday,
|
||||
ApiBoost,
|
||||
@ -694,6 +696,18 @@ export type TabState = {
|
||||
};
|
||||
};
|
||||
|
||||
aiToneEditorModal?: {
|
||||
toneToEdit?: ApiAiComposeTone;
|
||||
};
|
||||
|
||||
aiTonePreviewModal?: {
|
||||
slug: string;
|
||||
tone?: ApiAiComposeTone;
|
||||
example?: ApiAiComposeToneExample;
|
||||
isAlreadyAdded?: boolean;
|
||||
hasExampleError?: boolean;
|
||||
};
|
||||
|
||||
aiMessageEditorPendingResult?: {
|
||||
text?: ApiFormattedText;
|
||||
shouldClear?: boolean;
|
||||
|
||||
@ -34,6 +34,7 @@ export const DEFAULT_LIMITS: Record<ApiLimitType, readonly [number, number]> = {
|
||||
savedDialogsPinned: [5, 100],
|
||||
maxReactions: [1, 3],
|
||||
moreAccounts: [3, MULTIACCOUNT_MAX_SLOTS],
|
||||
aiComposeToneSaved: [5, 20],
|
||||
};
|
||||
|
||||
export const DEFAULT_MAX_MESSAGE_LENGTH = 4096;
|
||||
@ -58,6 +59,7 @@ export const DEFAULT_APP_CONFIG: ApiAppConfig = {
|
||||
savedDialogsPinned: DEFAULT_LIMITS.savedDialogsPinned,
|
||||
moreAccounts: DEFAULT_LIMITS.moreAccounts,
|
||||
maxReactions: DEFAULT_LIMITS.maxReactions,
|
||||
aiComposeToneSaved: DEFAULT_LIMITS.aiComposeToneSaved,
|
||||
},
|
||||
autologinDomains: [
|
||||
'instantview.telegram.org',
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src: url("./icons.woff2?e9a7be346eaac71f831beea8710b79fd") format("woff2"),
|
||||
url("./icons.woff?e9a7be346eaac71f831beea8710b79fd") format("woff");
|
||||
src: url("./icons.woff2?2ae8509ab058d7ef62dd8b67cc2b2b75") format("woff2"),
|
||||
url("./icons.woff?2ae8509ab058d7ef62dd8b67cc2b2b75") format("woff");
|
||||
}
|
||||
|
||||
.icon-char::before {
|
||||
@ -720,324 +720,327 @@ url("./icons.woff?e9a7be346eaac71f831beea8710b79fd") format("woff");
|
||||
.icon-reload::before {
|
||||
content: "\f1e9";
|
||||
}
|
||||
.icon-remove::before {
|
||||
.icon-reload-arrows::before {
|
||||
content: "\f1ea";
|
||||
}
|
||||
.icon-remove-quote::before {
|
||||
.icon-remove::before {
|
||||
content: "\f1eb";
|
||||
}
|
||||
.icon-reopen-topic::before {
|
||||
.icon-remove-quote::before {
|
||||
content: "\f1ec";
|
||||
}
|
||||
.icon-reorder-tabs::before {
|
||||
.icon-reopen-topic::before {
|
||||
content: "\f1ed";
|
||||
}
|
||||
.icon-replace::before {
|
||||
.icon-reorder-tabs::before {
|
||||
content: "\f1ee";
|
||||
}
|
||||
.icon-replace-round::before {
|
||||
.icon-replace::before {
|
||||
content: "\f1ef";
|
||||
}
|
||||
.icon-replies::before {
|
||||
.icon-replace-round::before {
|
||||
content: "\f1f0";
|
||||
}
|
||||
.icon-reply::before {
|
||||
.icon-replies::before {
|
||||
content: "\f1f1";
|
||||
}
|
||||
.icon-reply-filled::before {
|
||||
.icon-reply::before {
|
||||
content: "\f1f2";
|
||||
}
|
||||
.icon-revenue-split::before {
|
||||
.icon-reply-filled::before {
|
||||
content: "\f1f3";
|
||||
}
|
||||
.icon-revote::before {
|
||||
.icon-revenue-split::before {
|
||||
content: "\f1f4";
|
||||
}
|
||||
.icon-rotate::before {
|
||||
.icon-revote::before {
|
||||
content: "\f1f5";
|
||||
}
|
||||
.icon-save-story::before {
|
||||
.icon-rotate::before {
|
||||
content: "\f1f6";
|
||||
}
|
||||
.icon-saved-messages::before {
|
||||
.icon-save-story::before {
|
||||
content: "\f1f7";
|
||||
}
|
||||
.icon-schedule::before {
|
||||
.icon-saved-messages::before {
|
||||
content: "\f1f8";
|
||||
}
|
||||
.icon-scheduled::before {
|
||||
.icon-schedule::before {
|
||||
content: "\f1f9";
|
||||
}
|
||||
.icon-sd-photo::before {
|
||||
.icon-scheduled::before {
|
||||
content: "\f1fa";
|
||||
}
|
||||
.icon-search::before {
|
||||
.icon-sd-photo::before {
|
||||
content: "\f1fb";
|
||||
}
|
||||
.icon-select::before {
|
||||
.icon-search::before {
|
||||
content: "\f1fc";
|
||||
}
|
||||
.icon-select-filled::before {
|
||||
.icon-select::before {
|
||||
content: "\f1fd";
|
||||
}
|
||||
.icon-sell::before {
|
||||
.icon-select-filled::before {
|
||||
content: "\f1fe";
|
||||
}
|
||||
.icon-sell-outline::before {
|
||||
.icon-sell::before {
|
||||
content: "\f1ff";
|
||||
}
|
||||
.icon-send::before {
|
||||
.icon-sell-outline::before {
|
||||
content: "\f200";
|
||||
}
|
||||
.icon-send-outline::before {
|
||||
.icon-send::before {
|
||||
content: "\f201";
|
||||
}
|
||||
.icon-settings::before {
|
||||
.icon-send-outline::before {
|
||||
content: "\f202";
|
||||
}
|
||||
.icon-settings-filled::before {
|
||||
.icon-settings::before {
|
||||
content: "\f203";
|
||||
}
|
||||
.icon-share-filled::before {
|
||||
.icon-settings-filled::before {
|
||||
content: "\f204";
|
||||
}
|
||||
.icon-share-screen::before {
|
||||
.icon-share-filled::before {
|
||||
content: "\f205";
|
||||
}
|
||||
.icon-share-screen-outlined::before {
|
||||
.icon-share-screen::before {
|
||||
content: "\f206";
|
||||
}
|
||||
.icon-share-screen-stop::before {
|
||||
.icon-share-screen-outlined::before {
|
||||
content: "\f207";
|
||||
}
|
||||
.icon-show-message::before {
|
||||
.icon-share-screen-stop::before {
|
||||
content: "\f208";
|
||||
}
|
||||
.icon-sidebar::before {
|
||||
.icon-show-message::before {
|
||||
content: "\f209";
|
||||
}
|
||||
.icon-skip-next::before {
|
||||
.icon-sidebar::before {
|
||||
content: "\f20a";
|
||||
}
|
||||
.icon-skip-previous::before {
|
||||
.icon-skip-next::before {
|
||||
content: "\f20b";
|
||||
}
|
||||
.icon-smallscreen::before {
|
||||
.icon-skip-previous::before {
|
||||
content: "\f20c";
|
||||
}
|
||||
.icon-smile::before {
|
||||
.icon-smallscreen::before {
|
||||
content: "\f20d";
|
||||
}
|
||||
.icon-sort::before {
|
||||
.icon-smile::before {
|
||||
content: "\f20e";
|
||||
}
|
||||
.icon-sort-by-date::before {
|
||||
.icon-sort::before {
|
||||
content: "\f20f";
|
||||
}
|
||||
.icon-sort-by-number::before {
|
||||
.icon-sort-by-date::before {
|
||||
content: "\f210";
|
||||
}
|
||||
.icon-sort-by-price::before {
|
||||
.icon-sort-by-number::before {
|
||||
content: "\f211";
|
||||
}
|
||||
.icon-speaker::before {
|
||||
.icon-sort-by-price::before {
|
||||
content: "\f212";
|
||||
}
|
||||
.icon-speaker-muted-story::before {
|
||||
.icon-speaker::before {
|
||||
content: "\f213";
|
||||
}
|
||||
.icon-speaker-outline::before {
|
||||
.icon-speaker-muted-story::before {
|
||||
content: "\f214";
|
||||
}
|
||||
.icon-speaker-story::before {
|
||||
.icon-speaker-outline::before {
|
||||
content: "\f215";
|
||||
}
|
||||
.icon-spoiler::before {
|
||||
.icon-speaker-story::before {
|
||||
content: "\f216";
|
||||
}
|
||||
.icon-spoiler-disable::before {
|
||||
.icon-spoiler::before {
|
||||
content: "\f217";
|
||||
}
|
||||
.icon-sport::before {
|
||||
.icon-spoiler-disable::before {
|
||||
content: "\f218";
|
||||
}
|
||||
.icon-star::before {
|
||||
.icon-sport::before {
|
||||
content: "\f219";
|
||||
}
|
||||
.icon-stars-lock::before {
|
||||
.icon-star::before {
|
||||
content: "\f21a";
|
||||
}
|
||||
.icon-stars-refund::before {
|
||||
.icon-stars-lock::before {
|
||||
content: "\f21b";
|
||||
}
|
||||
.icon-stats::before {
|
||||
.icon-stars-refund::before {
|
||||
content: "\f21c";
|
||||
}
|
||||
.icon-stealth-future::before {
|
||||
.icon-stats::before {
|
||||
content: "\f21d";
|
||||
}
|
||||
.icon-stealth-past::before {
|
||||
.icon-stealth-future::before {
|
||||
content: "\f21e";
|
||||
}
|
||||
.icon-stickers::before {
|
||||
.icon-stealth-past::before {
|
||||
content: "\f21f";
|
||||
}
|
||||
.icon-stop::before {
|
||||
.icon-stickers::before {
|
||||
content: "\f220";
|
||||
}
|
||||
.icon-stop-raising-hand::before {
|
||||
.icon-stop::before {
|
||||
content: "\f221";
|
||||
}
|
||||
.icon-story-caption::before {
|
||||
.icon-stop-raising-hand::before {
|
||||
content: "\f222";
|
||||
}
|
||||
.icon-story-expired::before {
|
||||
.icon-story-caption::before {
|
||||
content: "\f223";
|
||||
}
|
||||
.icon-story-priority::before {
|
||||
.icon-story-expired::before {
|
||||
content: "\f224";
|
||||
}
|
||||
.icon-story-reply::before {
|
||||
.icon-story-priority::before {
|
||||
content: "\f225";
|
||||
}
|
||||
.icon-strikethrough::before {
|
||||
.icon-story-reply::before {
|
||||
content: "\f226";
|
||||
}
|
||||
.icon-tag::before {
|
||||
.icon-strikethrough::before {
|
||||
content: "\f227";
|
||||
}
|
||||
.icon-tag-add::before {
|
||||
.icon-tag::before {
|
||||
content: "\f228";
|
||||
}
|
||||
.icon-tag-crossed::before {
|
||||
.icon-tag-add::before {
|
||||
content: "\f229";
|
||||
}
|
||||
.icon-tag-filter::before {
|
||||
.icon-tag-crossed::before {
|
||||
content: "\f22a";
|
||||
}
|
||||
.icon-tag-name::before {
|
||||
.icon-tag-filter::before {
|
||||
content: "\f22b";
|
||||
}
|
||||
.icon-timer::before {
|
||||
.icon-tag-name::before {
|
||||
content: "\f22c";
|
||||
}
|
||||
.icon-timer-filled::before {
|
||||
.icon-timer::before {
|
||||
content: "\f22d";
|
||||
}
|
||||
.icon-toncoin::before {
|
||||
.icon-timer-filled::before {
|
||||
content: "\f22e";
|
||||
}
|
||||
.icon-tone::before {
|
||||
.icon-toncoin::before {
|
||||
content: "\f22f";
|
||||
}
|
||||
.icon-tools::before {
|
||||
.icon-tone::before {
|
||||
content: "\f230";
|
||||
}
|
||||
.icon-topic-new::before {
|
||||
.icon-tools::before {
|
||||
content: "\f231";
|
||||
}
|
||||
.icon-trade::before {
|
||||
.icon-topic-new::before {
|
||||
content: "\f232";
|
||||
}
|
||||
.icon-transcribe::before {
|
||||
.icon-trade::before {
|
||||
content: "\f233";
|
||||
}
|
||||
.icon-truck::before {
|
||||
.icon-transcribe::before {
|
||||
content: "\f234";
|
||||
}
|
||||
.icon-unarchive::before {
|
||||
.icon-truck::before {
|
||||
content: "\f235";
|
||||
}
|
||||
.icon-underlined::before {
|
||||
.icon-unarchive::before {
|
||||
content: "\f236";
|
||||
}
|
||||
.icon-understood::before {
|
||||
.icon-underlined::before {
|
||||
content: "\f237";
|
||||
}
|
||||
.icon-undo::before {
|
||||
.icon-understood::before {
|
||||
content: "\f238";
|
||||
}
|
||||
.icon-unique-profile::before {
|
||||
.icon-undo::before {
|
||||
content: "\f239";
|
||||
}
|
||||
.icon-unlist::before {
|
||||
.icon-unique-profile::before {
|
||||
content: "\f23a";
|
||||
}
|
||||
.icon-unlist-outline::before {
|
||||
.icon-unlist::before {
|
||||
content: "\f23b";
|
||||
}
|
||||
.icon-unlock::before {
|
||||
.icon-unlist-outline::before {
|
||||
content: "\f23c";
|
||||
}
|
||||
.icon-unlock-badge::before {
|
||||
.icon-unlock::before {
|
||||
content: "\f23d";
|
||||
}
|
||||
.icon-unmute::before {
|
||||
.icon-unlock-badge::before {
|
||||
content: "\f23e";
|
||||
}
|
||||
.icon-unpin::before {
|
||||
.icon-unmute::before {
|
||||
content: "\f23f";
|
||||
}
|
||||
.icon-unread::before {
|
||||
.icon-unpin::before {
|
||||
content: "\f240";
|
||||
}
|
||||
.icon-up::before {
|
||||
.icon-unread::before {
|
||||
content: "\f241";
|
||||
}
|
||||
.icon-user::before {
|
||||
.icon-up::before {
|
||||
content: "\f242";
|
||||
}
|
||||
.icon-user-filled::before {
|
||||
.icon-user::before {
|
||||
content: "\f243";
|
||||
}
|
||||
.icon-user-online::before {
|
||||
.icon-user-filled::before {
|
||||
content: "\f244";
|
||||
}
|
||||
.icon-user-stars::before {
|
||||
.icon-user-online::before {
|
||||
content: "\f245";
|
||||
}
|
||||
.icon-user-tag::before {
|
||||
.icon-user-stars::before {
|
||||
content: "\f246";
|
||||
}
|
||||
.icon-video::before {
|
||||
.icon-user-tag::before {
|
||||
content: "\f247";
|
||||
}
|
||||
.icon-video-outlined::before {
|
||||
.icon-video::before {
|
||||
content: "\f248";
|
||||
}
|
||||
.icon-video-stop::before {
|
||||
.icon-video-outlined::before {
|
||||
content: "\f249";
|
||||
}
|
||||
.icon-view-once::before {
|
||||
.icon-video-stop::before {
|
||||
content: "\f24a";
|
||||
}
|
||||
.icon-voice-chat::before {
|
||||
.icon-view-once::before {
|
||||
content: "\f24b";
|
||||
}
|
||||
.icon-volume-1::before {
|
||||
.icon-voice-chat::before {
|
||||
content: "\f24c";
|
||||
}
|
||||
.icon-volume-2::before {
|
||||
.icon-volume-1::before {
|
||||
content: "\f24d";
|
||||
}
|
||||
.icon-volume-3::before {
|
||||
.icon-volume-2::before {
|
||||
content: "\f24e";
|
||||
}
|
||||
.icon-warning::before {
|
||||
.icon-volume-3::before {
|
||||
content: "\f24f";
|
||||
}
|
||||
.icon-web::before {
|
||||
.icon-warning::before {
|
||||
content: "\f250";
|
||||
}
|
||||
.icon-webapp::before {
|
||||
.icon-web::before {
|
||||
content: "\f251";
|
||||
}
|
||||
.icon-word-wrap::before {
|
||||
.icon-webapp::before {
|
||||
content: "\f252";
|
||||
}
|
||||
.icon-zoom-in::before {
|
||||
.icon-word-wrap::before {
|
||||
content: "\f253";
|
||||
}
|
||||
.icon-zoom-out::before {
|
||||
.icon-zoom-in::before {
|
||||
content: "\f254";
|
||||
}
|
||||
.icon-zoom-out::before {
|
||||
content: "\f255";
|
||||
}
|
||||
|
||||
@ -249,111 +249,112 @@ $icons-map: (
|
||||
"redo": "\f1e7",
|
||||
"refund": "\f1e8",
|
||||
"reload": "\f1e9",
|
||||
"remove": "\f1ea",
|
||||
"remove-quote": "\f1eb",
|
||||
"reopen-topic": "\f1ec",
|
||||
"reorder-tabs": "\f1ed",
|
||||
"replace": "\f1ee",
|
||||
"replace-round": "\f1ef",
|
||||
"replies": "\f1f0",
|
||||
"reply": "\f1f1",
|
||||
"reply-filled": "\f1f2",
|
||||
"revenue-split": "\f1f3",
|
||||
"revote": "\f1f4",
|
||||
"rotate": "\f1f5",
|
||||
"save-story": "\f1f6",
|
||||
"saved-messages": "\f1f7",
|
||||
"schedule": "\f1f8",
|
||||
"scheduled": "\f1f9",
|
||||
"sd-photo": "\f1fa",
|
||||
"search": "\f1fb",
|
||||
"select": "\f1fc",
|
||||
"select-filled": "\f1fd",
|
||||
"sell": "\f1fe",
|
||||
"sell-outline": "\f1ff",
|
||||
"send": "\f200",
|
||||
"send-outline": "\f201",
|
||||
"settings": "\f202",
|
||||
"settings-filled": "\f203",
|
||||
"share-filled": "\f204",
|
||||
"share-screen": "\f205",
|
||||
"share-screen-outlined": "\f206",
|
||||
"share-screen-stop": "\f207",
|
||||
"show-message": "\f208",
|
||||
"sidebar": "\f209",
|
||||
"skip-next": "\f20a",
|
||||
"skip-previous": "\f20b",
|
||||
"smallscreen": "\f20c",
|
||||
"smile": "\f20d",
|
||||
"sort": "\f20e",
|
||||
"sort-by-date": "\f20f",
|
||||
"sort-by-number": "\f210",
|
||||
"sort-by-price": "\f211",
|
||||
"speaker": "\f212",
|
||||
"speaker-muted-story": "\f213",
|
||||
"speaker-outline": "\f214",
|
||||
"speaker-story": "\f215",
|
||||
"spoiler": "\f216",
|
||||
"spoiler-disable": "\f217",
|
||||
"sport": "\f218",
|
||||
"star": "\f219",
|
||||
"stars-lock": "\f21a",
|
||||
"stars-refund": "\f21b",
|
||||
"stats": "\f21c",
|
||||
"stealth-future": "\f21d",
|
||||
"stealth-past": "\f21e",
|
||||
"stickers": "\f21f",
|
||||
"stop": "\f220",
|
||||
"stop-raising-hand": "\f221",
|
||||
"story-caption": "\f222",
|
||||
"story-expired": "\f223",
|
||||
"story-priority": "\f224",
|
||||
"story-reply": "\f225",
|
||||
"strikethrough": "\f226",
|
||||
"tag": "\f227",
|
||||
"tag-add": "\f228",
|
||||
"tag-crossed": "\f229",
|
||||
"tag-filter": "\f22a",
|
||||
"tag-name": "\f22b",
|
||||
"timer": "\f22c",
|
||||
"timer-filled": "\f22d",
|
||||
"toncoin": "\f22e",
|
||||
"tone": "\f22f",
|
||||
"tools": "\f230",
|
||||
"topic-new": "\f231",
|
||||
"trade": "\f232",
|
||||
"transcribe": "\f233",
|
||||
"truck": "\f234",
|
||||
"unarchive": "\f235",
|
||||
"underlined": "\f236",
|
||||
"understood": "\f237",
|
||||
"undo": "\f238",
|
||||
"unique-profile": "\f239",
|
||||
"unlist": "\f23a",
|
||||
"unlist-outline": "\f23b",
|
||||
"unlock": "\f23c",
|
||||
"unlock-badge": "\f23d",
|
||||
"unmute": "\f23e",
|
||||
"unpin": "\f23f",
|
||||
"unread": "\f240",
|
||||
"up": "\f241",
|
||||
"user": "\f242",
|
||||
"user-filled": "\f243",
|
||||
"user-online": "\f244",
|
||||
"user-stars": "\f245",
|
||||
"user-tag": "\f246",
|
||||
"video": "\f247",
|
||||
"video-outlined": "\f248",
|
||||
"video-stop": "\f249",
|
||||
"view-once": "\f24a",
|
||||
"voice-chat": "\f24b",
|
||||
"volume-1": "\f24c",
|
||||
"volume-2": "\f24d",
|
||||
"volume-3": "\f24e",
|
||||
"warning": "\f24f",
|
||||
"web": "\f250",
|
||||
"webapp": "\f251",
|
||||
"word-wrap": "\f252",
|
||||
"zoom-in": "\f253",
|
||||
"zoom-out": "\f254",
|
||||
"reload-arrows": "\f1ea",
|
||||
"remove": "\f1eb",
|
||||
"remove-quote": "\f1ec",
|
||||
"reopen-topic": "\f1ed",
|
||||
"reorder-tabs": "\f1ee",
|
||||
"replace": "\f1ef",
|
||||
"replace-round": "\f1f0",
|
||||
"replies": "\f1f1",
|
||||
"reply": "\f1f2",
|
||||
"reply-filled": "\f1f3",
|
||||
"revenue-split": "\f1f4",
|
||||
"revote": "\f1f5",
|
||||
"rotate": "\f1f6",
|
||||
"save-story": "\f1f7",
|
||||
"saved-messages": "\f1f8",
|
||||
"schedule": "\f1f9",
|
||||
"scheduled": "\f1fa",
|
||||
"sd-photo": "\f1fb",
|
||||
"search": "\f1fc",
|
||||
"select": "\f1fd",
|
||||
"select-filled": "\f1fe",
|
||||
"sell": "\f1ff",
|
||||
"sell-outline": "\f200",
|
||||
"send": "\f201",
|
||||
"send-outline": "\f202",
|
||||
"settings": "\f203",
|
||||
"settings-filled": "\f204",
|
||||
"share-filled": "\f205",
|
||||
"share-screen": "\f206",
|
||||
"share-screen-outlined": "\f207",
|
||||
"share-screen-stop": "\f208",
|
||||
"show-message": "\f209",
|
||||
"sidebar": "\f20a",
|
||||
"skip-next": "\f20b",
|
||||
"skip-previous": "\f20c",
|
||||
"smallscreen": "\f20d",
|
||||
"smile": "\f20e",
|
||||
"sort": "\f20f",
|
||||
"sort-by-date": "\f210",
|
||||
"sort-by-number": "\f211",
|
||||
"sort-by-price": "\f212",
|
||||
"speaker": "\f213",
|
||||
"speaker-muted-story": "\f214",
|
||||
"speaker-outline": "\f215",
|
||||
"speaker-story": "\f216",
|
||||
"spoiler": "\f217",
|
||||
"spoiler-disable": "\f218",
|
||||
"sport": "\f219",
|
||||
"star": "\f21a",
|
||||
"stars-lock": "\f21b",
|
||||
"stars-refund": "\f21c",
|
||||
"stats": "\f21d",
|
||||
"stealth-future": "\f21e",
|
||||
"stealth-past": "\f21f",
|
||||
"stickers": "\f220",
|
||||
"stop": "\f221",
|
||||
"stop-raising-hand": "\f222",
|
||||
"story-caption": "\f223",
|
||||
"story-expired": "\f224",
|
||||
"story-priority": "\f225",
|
||||
"story-reply": "\f226",
|
||||
"strikethrough": "\f227",
|
||||
"tag": "\f228",
|
||||
"tag-add": "\f229",
|
||||
"tag-crossed": "\f22a",
|
||||
"tag-filter": "\f22b",
|
||||
"tag-name": "\f22c",
|
||||
"timer": "\f22d",
|
||||
"timer-filled": "\f22e",
|
||||
"toncoin": "\f22f",
|
||||
"tone": "\f230",
|
||||
"tools": "\f231",
|
||||
"topic-new": "\f232",
|
||||
"trade": "\f233",
|
||||
"transcribe": "\f234",
|
||||
"truck": "\f235",
|
||||
"unarchive": "\f236",
|
||||
"underlined": "\f237",
|
||||
"understood": "\f238",
|
||||
"undo": "\f239",
|
||||
"unique-profile": "\f23a",
|
||||
"unlist": "\f23b",
|
||||
"unlist-outline": "\f23c",
|
||||
"unlock": "\f23d",
|
||||
"unlock-badge": "\f23e",
|
||||
"unmute": "\f23f",
|
||||
"unpin": "\f240",
|
||||
"unread": "\f241",
|
||||
"up": "\f242",
|
||||
"user": "\f243",
|
||||
"user-filled": "\f244",
|
||||
"user-online": "\f245",
|
||||
"user-stars": "\f246",
|
||||
"user-tag": "\f247",
|
||||
"video": "\f248",
|
||||
"video-outlined": "\f249",
|
||||
"video-stop": "\f24a",
|
||||
"view-once": "\f24b",
|
||||
"voice-chat": "\f24c",
|
||||
"volume-1": "\f24d",
|
||||
"volume-2": "\f24e",
|
||||
"volume-3": "\f24f",
|
||||
"warning": "\f250",
|
||||
"web": "\f251",
|
||||
"webapp": "\f252",
|
||||
"word-wrap": "\f253",
|
||||
"zoom-in": "\f254",
|
||||
"zoom-out": "\f255",
|
||||
);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>icons Preview</title>
|
||||
<link rel="stylesheet" href="../icons.css?e9a7be346eaac71f831beea8710b79fd">
|
||||
<link rel="stylesheet" href="../icons.css?2ae8509ab058d7ef62dd8b67cc2b2b75">
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
@ -93,7 +93,7 @@
|
||||
<main class="page">
|
||||
<header class="header">
|
||||
<h1 class="title">icons Preview</h1>
|
||||
<p class="subtitle">340 icons</p>
|
||||
<p class="subtitle">341 icons</p>
|
||||
</header>
|
||||
<section class="grid">
|
||||
<article class="card">
|
||||
@ -1261,540 +1261,545 @@
|
||||
<div class="name">reload</div>
|
||||
<div class="code">\f1e9</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-reload-arrows"></i>
|
||||
<div class="name">reload-arrows</div>
|
||||
<div class="code">\f1ea</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-remove"></i>
|
||||
<div class="name">remove</div>
|
||||
<div class="code">\f1ea</div>
|
||||
<div class="code">\f1eb</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-remove-quote"></i>
|
||||
<div class="name">remove-quote</div>
|
||||
<div class="code">\f1eb</div>
|
||||
<div class="code">\f1ec</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-reopen-topic"></i>
|
||||
<div class="name">reopen-topic</div>
|
||||
<div class="code">\f1ec</div>
|
||||
<div class="code">\f1ed</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-reorder-tabs"></i>
|
||||
<div class="name">reorder-tabs</div>
|
||||
<div class="code">\f1ed</div>
|
||||
<div class="code">\f1ee</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-replace"></i>
|
||||
<div class="name">replace</div>
|
||||
<div class="code">\f1ee</div>
|
||||
<div class="code">\f1ef</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-replace-round"></i>
|
||||
<div class="name">replace-round</div>
|
||||
<div class="code">\f1ef</div>
|
||||
<div class="code">\f1f0</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-replies"></i>
|
||||
<div class="name">replies</div>
|
||||
<div class="code">\f1f0</div>
|
||||
<div class="code">\f1f1</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-reply"></i>
|
||||
<div class="name">reply</div>
|
||||
<div class="code">\f1f1</div>
|
||||
<div class="code">\f1f2</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-reply-filled"></i>
|
||||
<div class="name">reply-filled</div>
|
||||
<div class="code">\f1f2</div>
|
||||
<div class="code">\f1f3</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-revenue-split"></i>
|
||||
<div class="name">revenue-split</div>
|
||||
<div class="code">\f1f3</div>
|
||||
<div class="code">\f1f4</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-revote"></i>
|
||||
<div class="name">revote</div>
|
||||
<div class="code">\f1f4</div>
|
||||
<div class="code">\f1f5</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-rotate"></i>
|
||||
<div class="name">rotate</div>
|
||||
<div class="code">\f1f5</div>
|
||||
<div class="code">\f1f6</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-save-story"></i>
|
||||
<div class="name">save-story</div>
|
||||
<div class="code">\f1f6</div>
|
||||
<div class="code">\f1f7</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-saved-messages"></i>
|
||||
<div class="name">saved-messages</div>
|
||||
<div class="code">\f1f7</div>
|
||||
<div class="code">\f1f8</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-schedule"></i>
|
||||
<div class="name">schedule</div>
|
||||
<div class="code">\f1f8</div>
|
||||
<div class="code">\f1f9</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-scheduled"></i>
|
||||
<div class="name">scheduled</div>
|
||||
<div class="code">\f1f9</div>
|
||||
<div class="code">\f1fa</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-sd-photo"></i>
|
||||
<div class="name">sd-photo</div>
|
||||
<div class="code">\f1fa</div>
|
||||
<div class="code">\f1fb</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-search"></i>
|
||||
<div class="name">search</div>
|
||||
<div class="code">\f1fb</div>
|
||||
<div class="code">\f1fc</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-select"></i>
|
||||
<div class="name">select</div>
|
||||
<div class="code">\f1fc</div>
|
||||
<div class="code">\f1fd</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-select-filled"></i>
|
||||
<div class="name">select-filled</div>
|
||||
<div class="code">\f1fd</div>
|
||||
<div class="code">\f1fe</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-sell"></i>
|
||||
<div class="name">sell</div>
|
||||
<div class="code">\f1fe</div>
|
||||
<div class="code">\f1ff</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-sell-outline"></i>
|
||||
<div class="name">sell-outline</div>
|
||||
<div class="code">\f1ff</div>
|
||||
<div class="code">\f200</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-send"></i>
|
||||
<div class="name">send</div>
|
||||
<div class="code">\f200</div>
|
||||
<div class="code">\f201</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-send-outline"></i>
|
||||
<div class="name">send-outline</div>
|
||||
<div class="code">\f201</div>
|
||||
<div class="code">\f202</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-settings"></i>
|
||||
<div class="name">settings</div>
|
||||
<div class="code">\f202</div>
|
||||
<div class="code">\f203</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-settings-filled"></i>
|
||||
<div class="name">settings-filled</div>
|
||||
<div class="code">\f203</div>
|
||||
<div class="code">\f204</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-share-filled"></i>
|
||||
<div class="name">share-filled</div>
|
||||
<div class="code">\f204</div>
|
||||
<div class="code">\f205</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-share-screen"></i>
|
||||
<div class="name">share-screen</div>
|
||||
<div class="code">\f205</div>
|
||||
<div class="code">\f206</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-share-screen-outlined"></i>
|
||||
<div class="name">share-screen-outlined</div>
|
||||
<div class="code">\f206</div>
|
||||
<div class="code">\f207</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-share-screen-stop"></i>
|
||||
<div class="name">share-screen-stop</div>
|
||||
<div class="code">\f207</div>
|
||||
<div class="code">\f208</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-show-message"></i>
|
||||
<div class="name">show-message</div>
|
||||
<div class="code">\f208</div>
|
||||
<div class="code">\f209</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-sidebar"></i>
|
||||
<div class="name">sidebar</div>
|
||||
<div class="code">\f209</div>
|
||||
<div class="code">\f20a</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-skip-next"></i>
|
||||
<div class="name">skip-next</div>
|
||||
<div class="code">\f20a</div>
|
||||
<div class="code">\f20b</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-skip-previous"></i>
|
||||
<div class="name">skip-previous</div>
|
||||
<div class="code">\f20b</div>
|
||||
<div class="code">\f20c</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-smallscreen"></i>
|
||||
<div class="name">smallscreen</div>
|
||||
<div class="code">\f20c</div>
|
||||
<div class="code">\f20d</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-smile"></i>
|
||||
<div class="name">smile</div>
|
||||
<div class="code">\f20d</div>
|
||||
<div class="code">\f20e</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-sort"></i>
|
||||
<div class="name">sort</div>
|
||||
<div class="code">\f20e</div>
|
||||
<div class="code">\f20f</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-sort-by-date"></i>
|
||||
<div class="name">sort-by-date</div>
|
||||
<div class="code">\f20f</div>
|
||||
<div class="code">\f210</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-sort-by-number"></i>
|
||||
<div class="name">sort-by-number</div>
|
||||
<div class="code">\f210</div>
|
||||
<div class="code">\f211</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-sort-by-price"></i>
|
||||
<div class="name">sort-by-price</div>
|
||||
<div class="code">\f211</div>
|
||||
<div class="code">\f212</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-speaker"></i>
|
||||
<div class="name">speaker</div>
|
||||
<div class="code">\f212</div>
|
||||
<div class="code">\f213</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-speaker-muted-story"></i>
|
||||
<div class="name">speaker-muted-story</div>
|
||||
<div class="code">\f213</div>
|
||||
<div class="code">\f214</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-speaker-outline"></i>
|
||||
<div class="name">speaker-outline</div>
|
||||
<div class="code">\f214</div>
|
||||
<div class="code">\f215</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-speaker-story"></i>
|
||||
<div class="name">speaker-story</div>
|
||||
<div class="code">\f215</div>
|
||||
<div class="code">\f216</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-spoiler"></i>
|
||||
<div class="name">spoiler</div>
|
||||
<div class="code">\f216</div>
|
||||
<div class="code">\f217</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-spoiler-disable"></i>
|
||||
<div class="name">spoiler-disable</div>
|
||||
<div class="code">\f217</div>
|
||||
<div class="code">\f218</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-sport"></i>
|
||||
<div class="name">sport</div>
|
||||
<div class="code">\f218</div>
|
||||
<div class="code">\f219</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-star"></i>
|
||||
<div class="name">star</div>
|
||||
<div class="code">\f219</div>
|
||||
<div class="code">\f21a</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-stars-lock"></i>
|
||||
<div class="name">stars-lock</div>
|
||||
<div class="code">\f21a</div>
|
||||
<div class="code">\f21b</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-stars-refund"></i>
|
||||
<div class="name">stars-refund</div>
|
||||
<div class="code">\f21b</div>
|
||||
<div class="code">\f21c</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-stats"></i>
|
||||
<div class="name">stats</div>
|
||||
<div class="code">\f21c</div>
|
||||
<div class="code">\f21d</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-stealth-future"></i>
|
||||
<div class="name">stealth-future</div>
|
||||
<div class="code">\f21d</div>
|
||||
<div class="code">\f21e</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-stealth-past"></i>
|
||||
<div class="name">stealth-past</div>
|
||||
<div class="code">\f21e</div>
|
||||
<div class="code">\f21f</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-stickers"></i>
|
||||
<div class="name">stickers</div>
|
||||
<div class="code">\f21f</div>
|
||||
<div class="code">\f220</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-stop"></i>
|
||||
<div class="name">stop</div>
|
||||
<div class="code">\f220</div>
|
||||
<div class="code">\f221</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-stop-raising-hand"></i>
|
||||
<div class="name">stop-raising-hand</div>
|
||||
<div class="code">\f221</div>
|
||||
<div class="code">\f222</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-story-caption"></i>
|
||||
<div class="name">story-caption</div>
|
||||
<div class="code">\f222</div>
|
||||
<div class="code">\f223</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-story-expired"></i>
|
||||
<div class="name">story-expired</div>
|
||||
<div class="code">\f223</div>
|
||||
<div class="code">\f224</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-story-priority"></i>
|
||||
<div class="name">story-priority</div>
|
||||
<div class="code">\f224</div>
|
||||
<div class="code">\f225</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-story-reply"></i>
|
||||
<div class="name">story-reply</div>
|
||||
<div class="code">\f225</div>
|
||||
<div class="code">\f226</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-strikethrough"></i>
|
||||
<div class="name">strikethrough</div>
|
||||
<div class="code">\f226</div>
|
||||
<div class="code">\f227</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-tag"></i>
|
||||
<div class="name">tag</div>
|
||||
<div class="code">\f227</div>
|
||||
<div class="code">\f228</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-tag-add"></i>
|
||||
<div class="name">tag-add</div>
|
||||
<div class="code">\f228</div>
|
||||
<div class="code">\f229</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-tag-crossed"></i>
|
||||
<div class="name">tag-crossed</div>
|
||||
<div class="code">\f229</div>
|
||||
<div class="code">\f22a</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-tag-filter"></i>
|
||||
<div class="name">tag-filter</div>
|
||||
<div class="code">\f22a</div>
|
||||
<div class="code">\f22b</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-tag-name"></i>
|
||||
<div class="name">tag-name</div>
|
||||
<div class="code">\f22b</div>
|
||||
<div class="code">\f22c</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-timer"></i>
|
||||
<div class="name">timer</div>
|
||||
<div class="code">\f22c</div>
|
||||
<div class="code">\f22d</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-timer-filled"></i>
|
||||
<div class="name">timer-filled</div>
|
||||
<div class="code">\f22d</div>
|
||||
<div class="code">\f22e</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-toncoin"></i>
|
||||
<div class="name">toncoin</div>
|
||||
<div class="code">\f22e</div>
|
||||
<div class="code">\f22f</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-tone"></i>
|
||||
<div class="name">tone</div>
|
||||
<div class="code">\f22f</div>
|
||||
<div class="code">\f230</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-tools"></i>
|
||||
<div class="name">tools</div>
|
||||
<div class="code">\f230</div>
|
||||
<div class="code">\f231</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-topic-new"></i>
|
||||
<div class="name">topic-new</div>
|
||||
<div class="code">\f231</div>
|
||||
<div class="code">\f232</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-trade"></i>
|
||||
<div class="name">trade</div>
|
||||
<div class="code">\f232</div>
|
||||
<div class="code">\f233</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-transcribe"></i>
|
||||
<div class="name">transcribe</div>
|
||||
<div class="code">\f233</div>
|
||||
<div class="code">\f234</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-truck"></i>
|
||||
<div class="name">truck</div>
|
||||
<div class="code">\f234</div>
|
||||
<div class="code">\f235</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-unarchive"></i>
|
||||
<div class="name">unarchive</div>
|
||||
<div class="code">\f235</div>
|
||||
<div class="code">\f236</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-underlined"></i>
|
||||
<div class="name">underlined</div>
|
||||
<div class="code">\f236</div>
|
||||
<div class="code">\f237</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-understood"></i>
|
||||
<div class="name">understood</div>
|
||||
<div class="code">\f237</div>
|
||||
<div class="code">\f238</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-undo"></i>
|
||||
<div class="name">undo</div>
|
||||
<div class="code">\f238</div>
|
||||
<div class="code">\f239</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-unique-profile"></i>
|
||||
<div class="name">unique-profile</div>
|
||||
<div class="code">\f239</div>
|
||||
<div class="code">\f23a</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-unlist"></i>
|
||||
<div class="name">unlist</div>
|
||||
<div class="code">\f23a</div>
|
||||
<div class="code">\f23b</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-unlist-outline"></i>
|
||||
<div class="name">unlist-outline</div>
|
||||
<div class="code">\f23b</div>
|
||||
<div class="code">\f23c</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-unlock"></i>
|
||||
<div class="name">unlock</div>
|
||||
<div class="code">\f23c</div>
|
||||
<div class="code">\f23d</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-unlock-badge"></i>
|
||||
<div class="name">unlock-badge</div>
|
||||
<div class="code">\f23d</div>
|
||||
<div class="code">\f23e</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-unmute"></i>
|
||||
<div class="name">unmute</div>
|
||||
<div class="code">\f23e</div>
|
||||
<div class="code">\f23f</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-unpin"></i>
|
||||
<div class="name">unpin</div>
|
||||
<div class="code">\f23f</div>
|
||||
<div class="code">\f240</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-unread"></i>
|
||||
<div class="name">unread</div>
|
||||
<div class="code">\f240</div>
|
||||
<div class="code">\f241</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-up"></i>
|
||||
<div class="name">up</div>
|
||||
<div class="code">\f241</div>
|
||||
<div class="code">\f242</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-user"></i>
|
||||
<div class="name">user</div>
|
||||
<div class="code">\f242</div>
|
||||
<div class="code">\f243</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-user-filled"></i>
|
||||
<div class="name">user-filled</div>
|
||||
<div class="code">\f243</div>
|
||||
<div class="code">\f244</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-user-online"></i>
|
||||
<div class="name">user-online</div>
|
||||
<div class="code">\f244</div>
|
||||
<div class="code">\f245</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-user-stars"></i>
|
||||
<div class="name">user-stars</div>
|
||||
<div class="code">\f245</div>
|
||||
<div class="code">\f246</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-user-tag"></i>
|
||||
<div class="name">user-tag</div>
|
||||
<div class="code">\f246</div>
|
||||
<div class="code">\f247</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-video"></i>
|
||||
<div class="name">video</div>
|
||||
<div class="code">\f247</div>
|
||||
<div class="code">\f248</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-video-outlined"></i>
|
||||
<div class="name">video-outlined</div>
|
||||
<div class="code">\f248</div>
|
||||
<div class="code">\f249</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-video-stop"></i>
|
||||
<div class="name">video-stop</div>
|
||||
<div class="code">\f249</div>
|
||||
<div class="code">\f24a</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-view-once"></i>
|
||||
<div class="name">view-once</div>
|
||||
<div class="code">\f24a</div>
|
||||
<div class="code">\f24b</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-voice-chat"></i>
|
||||
<div class="name">voice-chat</div>
|
||||
<div class="code">\f24b</div>
|
||||
<div class="code">\f24c</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-volume-1"></i>
|
||||
<div class="name">volume-1</div>
|
||||
<div class="code">\f24c</div>
|
||||
<div class="code">\f24d</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-volume-2"></i>
|
||||
<div class="name">volume-2</div>
|
||||
<div class="code">\f24d</div>
|
||||
<div class="code">\f24e</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-volume-3"></i>
|
||||
<div class="name">volume-3</div>
|
||||
<div class="code">\f24e</div>
|
||||
<div class="code">\f24f</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-warning"></i>
|
||||
<div class="name">warning</div>
|
||||
<div class="code">\f24f</div>
|
||||
<div class="code">\f250</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-web"></i>
|
||||
<div class="name">web</div>
|
||||
<div class="code">\f250</div>
|
||||
<div class="code">\f251</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-webapp"></i>
|
||||
<div class="name">webapp</div>
|
||||
<div class="code">\f251</div>
|
||||
<div class="code">\f252</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-word-wrap"></i>
|
||||
<div class="name">word-wrap</div>
|
||||
<div class="code">\f252</div>
|
||||
<div class="code">\f253</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-zoom-in"></i>
|
||||
<div class="name">zoom-in</div>
|
||||
<div class="code">\f253</div>
|
||||
<div class="code">\f254</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<i class="icon icon-zoom-out"></i>
|
||||
<div class="name">zoom-out</div>
|
||||
<div class="code">\f254</div>
|
||||
<div class="code">\f255</div>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@ -232,6 +232,7 @@ export type FontIconName =
|
||||
| 'redo'
|
||||
| 'refund'
|
||||
| 'reload'
|
||||
| 'reload-arrows'
|
||||
| 'remove'
|
||||
| 'remove-quote'
|
||||
| 'reopen-topic'
|
||||
|
||||
38
src/types/language.d.ts
vendored
38
src/types/language.d.ts
vendored
@ -1533,6 +1533,7 @@ export interface LangPair {
|
||||
'ViewButtonStickerset': undefined;
|
||||
'ViewButtonEmojiset': undefined;
|
||||
'ViewButtonGiftUnique': undefined;
|
||||
'ViewButtonAiStyle': undefined;
|
||||
'AuthContinueOnThisLanguage': undefined;
|
||||
'Share': undefined;
|
||||
'GiftSortByDate': undefined;
|
||||
@ -2108,6 +2109,27 @@ export interface LangPair {
|
||||
'AiMessageEditorApply': undefined;
|
||||
'AiMessageEditorEmojify': undefined;
|
||||
'AiMessageEditorTranslation': undefined;
|
||||
'AiToneEditorNewStyle': undefined;
|
||||
'AiToneEditorTitle': undefined;
|
||||
'AiToneEditorNamePlaceholder': undefined;
|
||||
'AiToneEditorPromptPlaceholder': undefined;
|
||||
'AiToneEditorDisplayAuthor': undefined;
|
||||
'AiToneEditorSelectEmoji': undefined;
|
||||
'AiToneCreatedHint': undefined;
|
||||
'AiToneEditStyle': undefined;
|
||||
'AiToneShareStyle': undefined;
|
||||
'AiToneDeleteStyle': undefined;
|
||||
'AiToneDeleteStyleConfirmOwn': undefined;
|
||||
'AiToneDeleteStyleConfirm': undefined;
|
||||
'AiToneEditorEditTitle': undefined;
|
||||
'AiTonePreviewSubtitle': undefined;
|
||||
'AiTonePreviewBefore': undefined;
|
||||
'AiTonePreviewAnotherExample': undefined;
|
||||
'AiTonePreviewAfter': undefined;
|
||||
'AiTonePreviewAddStyle': undefined;
|
||||
'AiTonePreviewRemoveStyle': undefined;
|
||||
'AiTonePreviewStyleAdded': undefined;
|
||||
'AiToneLimitReached': undefined;
|
||||
'TextShowMore': undefined;
|
||||
'TextShowLess': undefined;
|
||||
'AiMessageEditorFrom': undefined;
|
||||
@ -3741,6 +3763,22 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'AiMessageEditorDailyLimitReached': {
|
||||
'link': V;
|
||||
};
|
||||
'AiToneCreated': {
|
||||
'title': V;
|
||||
};
|
||||
'AiToneLimitReachedPremium': {
|
||||
'limit': V;
|
||||
};
|
||||
'AiTonePreviewUsedBy': {
|
||||
'count': V;
|
||||
};
|
||||
'AiTonePreviewCreatedBy': {
|
||||
'author': V;
|
||||
};
|
||||
'AiTonePreviewUsedByCreatedBy': {
|
||||
'usedBy': V;
|
||||
'createdBy': V;
|
||||
};
|
||||
'UnofficialSecurityRisk': {
|
||||
'peer': V;
|
||||
};
|
||||
|
||||
@ -8,9 +8,9 @@ import { toChannelId } from './entities/ids';
|
||||
import { isUsernameValid } from './entities/username';
|
||||
|
||||
export type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'addemoji' |
|
||||
'setlanguage' | 'addtheme' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' | 'msg' | 'msg_url' |
|
||||
'invoice' | 'addlist' | 'boost' | 'giftcode' | 'message' | 'premium_offer' | 'premium_multigift' | 'stars_topup'
|
||||
| 'nft' | 'stars' | 'ton' | 'stargift_auction' | 'premium' | 'oauth';
|
||||
'setlanguage' | 'addtheme' | 'addstyle' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' |
|
||||
'msg' | 'msg_url' | 'invoice' | 'addlist' | 'boost' | 'giftcode' | 'message' | 'premium_offer' |
|
||||
'premium_multigift' | 'stars_topup' | 'nft' | 'stars' | 'ton' | 'stargift_auction' | 'premium' | 'oauth';
|
||||
|
||||
interface PublicMessageLink {
|
||||
type: 'publicMessageLink';
|
||||
|
||||
@ -222,6 +222,12 @@ export const processDeepLink = (url: string, linkContext?: LinkContext): boolean
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'addstyle': {
|
||||
const { set } = params;
|
||||
if (!set) return false;
|
||||
actions.openAiTonePreview({ slug: set });
|
||||
break;
|
||||
}
|
||||
case 'share':
|
||||
case 'msg':
|
||||
case 'msg_url': {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user