Composer: Support AI messages (#6826)
This commit is contained in:
parent
d7e3456550
commit
d4138b0ebd
@ -130,6 +130,7 @@ export interface GramJsAppConfig extends LimitsConfig {
|
|||||||
whitelisted_bots?: string[];
|
whitelisted_bots?: string[];
|
||||||
settings_display_passkeys?: boolean;
|
settings_display_passkeys?: boolean;
|
||||||
passkeys_account_passkeys_max?: number;
|
passkeys_account_passkeys_max?: number;
|
||||||
|
ai_compose_styles?: [string, string, string][];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEmojiSounds(appConfig: GramJsAppConfig) {
|
function buildEmojiSounds(appConfig: GramJsAppConfig) {
|
||||||
@ -161,6 +162,11 @@ function buildDiceEmojiesSuccess(appConfig: GramJsAppConfig) {
|
|||||||
}, {} as ApiAppConfig['diceEmojiesSuccess']) : {};
|
}, {} as ApiAppConfig['diceEmojiesSuccess']) : {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildAiComposeStyles(appConfig: GramJsAppConfig) {
|
||||||
|
const { ai_compose_styles } = appConfig;
|
||||||
|
return ai_compose_styles?.map(([tone, documentId, title]) => ({ tone, documentId, title }));
|
||||||
|
}
|
||||||
|
|
||||||
function getLimit(appConfig: GramJsAppConfig, key: Limit, fallbackKey: ApiLimitType) {
|
function getLimit(appConfig: GramJsAppConfig, key: Limit, fallbackKey: ApiLimitType) {
|
||||||
const defaultLimit = appConfig[`${key}_default`] || DEFAULT_LIMITS[fallbackKey][0];
|
const defaultLimit = appConfig[`${key}_default`] || DEFAULT_LIMITS[fallbackKey][0];
|
||||||
const premiumLimit = appConfig[`${key}_premium`] || DEFAULT_LIMITS[fallbackKey][1];
|
const premiumLimit = appConfig[`${key}_premium`] || DEFAULT_LIMITS[fallbackKey][1];
|
||||||
@ -272,6 +278,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
|
|||||||
passkeysMaxCount: appConfig.passkeys_account_passkeys_max,
|
passkeysMaxCount: appConfig.passkeys_account_passkeys_max,
|
||||||
diceEmojies: appConfig.emojies_send_dice,
|
diceEmojies: appConfig.emojies_send_dice,
|
||||||
diceEmojiesSuccess: buildDiceEmojiesSuccess(appConfig),
|
diceEmojiesSuccess: buildDiceEmojiesSuccess(appConfig),
|
||||||
|
aiComposeStyles: buildAiComposeStyles(appConfig),
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { Entity } from '../../../lib/gramjs/types';
|
|||||||
import { strippedPhotoToJpg } from '../../../lib/gramjs/Utils';
|
import { strippedPhotoToJpg } from '../../../lib/gramjs/Utils';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ApiComposedMessageWithAI,
|
||||||
ApiFormattedText,
|
ApiFormattedText,
|
||||||
ApiMessageEntity,
|
ApiMessageEntity,
|
||||||
ApiMessageEntityDefault,
|
ApiMessageEntityDefault,
|
||||||
@ -301,9 +302,43 @@ export function buildApiMessageEntity(entity: GramJs.TypeMessageEntity): ApiMess
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entity instanceof GramJs.MessageEntityDiffInsert) {
|
||||||
|
return {
|
||||||
|
type: ApiMessageEntityTypes.DiffInsert,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity instanceof GramJs.MessageEntityDiffReplace) {
|
||||||
|
return {
|
||||||
|
type: ApiMessageEntityTypes.DiffReplace,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
oldText: entity.oldText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity instanceof GramJs.MessageEntityDiffDelete) {
|
||||||
|
return {
|
||||||
|
type: ApiMessageEntityTypes.DiffDelete,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: type as `${ApiMessageEntityDefault['type']}`,
|
type: type as `${ApiMessageEntityDefault['type']}`,
|
||||||
offset,
|
offset,
|
||||||
length,
|
length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildApiComposedMessageWithAI(
|
||||||
|
result: GramJs.messages.ComposedMessageWithAI,
|
||||||
|
): ApiComposedMessageWithAI {
|
||||||
|
return {
|
||||||
|
resultText: buildApiFormattedText(result.resultText),
|
||||||
|
diffText: result.diffText ? buildApiFormattedText(result.diffText) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -452,6 +452,7 @@ export async function fetchCurrentUser() {
|
|||||||
|
|
||||||
export function dispatchErrorUpdate<T extends GramJs.AnyRequest>(err: Error, request: T) {
|
export function dispatchErrorUpdate<T extends GramJs.AnyRequest>(err: Error, request: T) {
|
||||||
const message = err instanceof RPCError ? err.errorMessage : err.message;
|
const message = err instanceof RPCError ? err.errorMessage : err.message;
|
||||||
|
|
||||||
const isSlowMode = message === 'FLOOD' && (
|
const isSlowMode = message === 'FLOOD' && (
|
||||||
request instanceof GramJs.messages.SendMessage
|
request instanceof GramJs.messages.SendMessage
|
||||||
|| request instanceof GramJs.messages.SendMedia
|
|| request instanceof GramJs.messages.SendMedia
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
ApiAttachment,
|
ApiAttachment,
|
||||||
ApiChat,
|
ApiChat,
|
||||||
|
ApiComposedMessageWithAI,
|
||||||
ApiError,
|
ApiError,
|
||||||
ApiFormattedText,
|
ApiFormattedText,
|
||||||
ApiGlobalMessageSearchType,
|
ApiGlobalMessageSearchType,
|
||||||
@ -60,7 +61,7 @@ import {
|
|||||||
buildApiSponsoredMessageReportResult,
|
buildApiSponsoredMessageReportResult,
|
||||||
buildThreadReadState,
|
buildThreadReadState,
|
||||||
} from '../apiBuilders/chats';
|
} from '../apiBuilders/chats';
|
||||||
import { buildApiFormattedText } from '../apiBuilders/common';
|
import { buildApiComposedMessageWithAI, buildApiFormattedText } from '../apiBuilders/common';
|
||||||
import { buildApiTopicWithState } from '../apiBuilders/forums';
|
import { buildApiTopicWithState } from '../apiBuilders/forums';
|
||||||
import {
|
import {
|
||||||
buildMessageMediaContent, buildMessageTextContent, buildPollFromMedia,
|
buildMessageMediaContent, buildMessageTextContent, buildPollFromMedia,
|
||||||
@ -126,6 +127,7 @@ type TranslateTextParams = ({
|
|||||||
messageIds: number[];
|
messageIds: number[];
|
||||||
}) & {
|
}) & {
|
||||||
toLanguageCode: string;
|
toLanguageCode: string;
|
||||||
|
tone?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SearchResults = {
|
type SearchResults = {
|
||||||
@ -2379,17 +2381,19 @@ export async function translateText(params: TranslateTextParams) {
|
|||||||
let result;
|
let result;
|
||||||
const isMessageTranslation = 'chat' in params;
|
const isMessageTranslation = 'chat' in params;
|
||||||
if (isMessageTranslation) {
|
if (isMessageTranslation) {
|
||||||
const { chat, messageIds, toLanguageCode } = params;
|
const { chat, messageIds, toLanguageCode, tone } = params;
|
||||||
result = await invokeRequest(new GramJs.messages.TranslateText({
|
result = await invokeRequest(new GramJs.messages.TranslateText({
|
||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
id: messageIds,
|
id: messageIds,
|
||||||
toLang: toLanguageCode,
|
toLang: toLanguageCode,
|
||||||
|
tone,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
const { text, toLanguageCode } = params;
|
const { text, toLanguageCode, tone } = params;
|
||||||
result = await invokeRequest(new GramJs.messages.TranslateText({
|
result = await invokeRequest(new GramJs.messages.TranslateText({
|
||||||
text: text.map((t) => buildInputTextWithEntities(t)),
|
text: text.map((t) => buildInputTextWithEntities(t)),
|
||||||
toLang: toLanguageCode,
|
toLang: toLanguageCode,
|
||||||
|
tone,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2421,14 +2425,15 @@ export async function translateText(params: TranslateTextParams) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMessageSummary({
|
export async function fetchMessageSummary({
|
||||||
chat, id, toLanguageCode,
|
chat, id, toLanguageCode, tone,
|
||||||
}: {
|
}: {
|
||||||
chat: ApiChat; id: number; toLanguageCode?: string;
|
chat: ApiChat; id: number; toLanguageCode?: string; tone?: string;
|
||||||
}) {
|
}) {
|
||||||
const result = await invokeRequest(new GramJs.messages.SummarizeText({
|
const result = await invokeRequest(new GramJs.messages.SummarizeText({
|
||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
id,
|
id,
|
||||||
toLang: toLanguageCode,
|
toLang: toLanguageCode,
|
||||||
|
tone,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!result) return undefined;
|
if (!result) return undefined;
|
||||||
@ -2661,3 +2666,41 @@ export async function fetchPreparedInlineMessage({
|
|||||||
export function incrementLocalMessagesCounter() {
|
export function incrementLocalMessagesCounter() {
|
||||||
incrementLocalMessageCounter();
|
incrementLocalMessageCounter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function composeMessageWithAI({
|
||||||
|
text,
|
||||||
|
shouldProofread,
|
||||||
|
isEmojify,
|
||||||
|
translateToLang,
|
||||||
|
changeTone,
|
||||||
|
}: {
|
||||||
|
text: ApiFormattedText;
|
||||||
|
shouldProofread?: boolean;
|
||||||
|
isEmojify?: boolean;
|
||||||
|
translateToLang?: string;
|
||||||
|
changeTone?: string;
|
||||||
|
}): Promise<{ result?: ApiComposedMessageWithAI; error?: 'floodPremium' | 'aiError' | 'generic' }> {
|
||||||
|
try {
|
||||||
|
const result = await invokeRequest(new GramJs.messages.ComposeMessageWithAI({
|
||||||
|
text: buildInputTextWithEntities(text),
|
||||||
|
proofread: shouldProofread || undefined,
|
||||||
|
emojify: isEmojify || undefined,
|
||||||
|
translateToLang,
|
||||||
|
changeTone,
|
||||||
|
}), { shouldThrow: true });
|
||||||
|
|
||||||
|
if (!result) return { error: 'generic' };
|
||||||
|
|
||||||
|
return { result: buildApiComposedMessageWithAI(result) };
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof RPCError) {
|
||||||
|
if (err.errorMessage === 'AICOMPOSE_FLOOD_PREMIUM') {
|
||||||
|
return { error: 'floodPremium' };
|
||||||
|
}
|
||||||
|
if (err.errorMessage === 'AICOMPOSE_ERROR_OCCURED') {
|
||||||
|
return { error: 'aiError' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { error: 'generic' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -510,7 +510,8 @@ export type ApiMessageEntityDefault = {
|
|||||||
`${ApiMessageEntityTypes}`,
|
`${ApiMessageEntityTypes}`,
|
||||||
`${ApiMessageEntityTypes.Pre}` | `${ApiMessageEntityTypes.TextUrl}` | `${ApiMessageEntityTypes.MentionName}` |
|
`${ApiMessageEntityTypes.Pre}` | `${ApiMessageEntityTypes.TextUrl}` | `${ApiMessageEntityTypes.MentionName}` |
|
||||||
`${ApiMessageEntityTypes.Blockquote}` | `${ApiMessageEntityTypes.CustomEmoji}` |
|
`${ApiMessageEntityTypes.Blockquote}` | `${ApiMessageEntityTypes.CustomEmoji}` |
|
||||||
`${ApiMessageEntityTypes.Timestamp}` | `${ApiMessageEntityTypes.FormattedDate}`
|
`${ApiMessageEntityTypes.Timestamp}` | `${ApiMessageEntityTypes.FormattedDate}` |
|
||||||
|
`${ApiMessageEntityTypes.DiffInsert}` | `${ApiMessageEntityTypes.DiffReplace}` | `${ApiMessageEntityTypes.DiffDelete}`
|
||||||
>;
|
>;
|
||||||
offset: number;
|
offset: number;
|
||||||
length: number;
|
length: number;
|
||||||
@ -572,9 +573,28 @@ export type ApiMessageEntityTimestamp = {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiMessageEntityDiffInsert = {
|
||||||
|
type: ApiMessageEntityTypes.DiffInsert;
|
||||||
|
offset: number;
|
||||||
|
length: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiMessageEntityDiffReplace = {
|
||||||
|
type: ApiMessageEntityTypes.DiffReplace;
|
||||||
|
offset: number;
|
||||||
|
length: number;
|
||||||
|
oldText: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiMessageEntityDiffDelete = {
|
||||||
|
type: ApiMessageEntityTypes.DiffDelete;
|
||||||
|
offset: number;
|
||||||
|
length: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiMessageEntity = ApiMessageEntityDefault | ApiMessageEntityPre | ApiMessageEntityTextUrl |
|
export type ApiMessageEntity = ApiMessageEntityDefault | ApiMessageEntityPre | ApiMessageEntityTextUrl |
|
||||||
ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp |
|
ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp |
|
||||||
ApiMessageEntityFormattedDate;
|
ApiMessageEntityFormattedDate | ApiMessageEntityDiffInsert | ApiMessageEntityDiffReplace | ApiMessageEntityDiffDelete;
|
||||||
|
|
||||||
export enum ApiMessageEntityTypes {
|
export enum ApiMessageEntityTypes {
|
||||||
Bold = 'MessageEntityBold',
|
Bold = 'MessageEntityBold',
|
||||||
@ -599,6 +619,9 @@ export enum ApiMessageEntityTypes {
|
|||||||
QuoteFocus = 'MessageEntityQuoteFocus',
|
QuoteFocus = 'MessageEntityQuoteFocus',
|
||||||
FormattedDate = 'MessageEntityFormattedDate',
|
FormattedDate = 'MessageEntityFormattedDate',
|
||||||
Unknown = 'MessageEntityUnknown',
|
Unknown = 'MessageEntityUnknown',
|
||||||
|
DiffInsert = 'MessageEntityDiffInsert',
|
||||||
|
DiffReplace = 'MessageEntityDiffReplace',
|
||||||
|
DiffDelete = 'MessageEntityDiffDelete',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiFormattedText {
|
export interface ApiFormattedText {
|
||||||
@ -610,6 +633,11 @@ export interface ApiFormattedTextWithEmojiOnlyCount extends ApiFormattedText {
|
|||||||
emojiOnlyCount?: number;
|
emojiOnlyCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiComposedMessageWithAI {
|
||||||
|
resultText: ApiFormattedText;
|
||||||
|
diffText?: ApiFormattedText;
|
||||||
|
}
|
||||||
|
|
||||||
export type MediaContent = {
|
export type MediaContent = {
|
||||||
text?: ApiFormattedTextWithEmojiOnlyCount;
|
text?: ApiFormattedTextWithEmojiOnlyCount;
|
||||||
photo?: ApiPhoto;
|
photo?: ApiPhoto;
|
||||||
|
|||||||
@ -242,6 +242,12 @@ export interface ApiCountryCode extends ApiCountry {
|
|||||||
patterns?: string[];
|
patterns?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiAiComposeStyle {
|
||||||
|
tone: string;
|
||||||
|
documentId: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiAppConfig {
|
export interface ApiAppConfig {
|
||||||
hash: number;
|
hash: number;
|
||||||
emojiSounds: Record<string, string>;
|
emojiSounds: Record<string, string>;
|
||||||
@ -331,6 +337,7 @@ export interface ApiAppConfig {
|
|||||||
value: number;
|
value: number;
|
||||||
frameStart: number;
|
frameStart: number;
|
||||||
}>;
|
}>;
|
||||||
|
aiComposeStyles?: ApiAiComposeStyle[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiConfig {
|
export interface ApiConfig {
|
||||||
|
|||||||
1
src/assets/font-icons/ai-edit.svg
Normal file
1
src/assets/font-icons/ai-edit.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="m27.044 8.987-.824-.824c-.79-.791-1.843-1.227-2.96-1.227s-2.17.435-2.96 1.227l-1.525 1.523-.002.002-.002.003L7.098 21.363a2.52 2.52 0 0 0-.731 1.635l-.19 3.38a2.517 2.517 0 0 0 2.652 2.652l3.378-.19a2.5 2.5 0 0 0 1.637-.733l13.2-13.2a4.19 4.19 0 0 0 0-5.92M12.32 26.582a.36.36 0 0 1-.233.105l-3.38.19a.32.32 0 0 1-.275-.104.35.35 0 0 1-.103-.273l.19-3.38a.36.36 0 0 1 .103-.234l10.912-10.911 3.697 3.696zm13.2-13.199-.765.764-3.696-3.697.763-.763a2.02 2.02 0 0 1 1.436-.595c.542 0 1.052.211 1.436.595l.825.825c.79.791.79 2.08 0 2.871M7.813 17.985a3.65 3.65 0 0 1 3.648-3.648v-.554a3.66 3.66 0 0 1-3.648-3.649h-.639a3.64 3.64 0 0 1-3.639 3.64v.563a3.65 3.65 0 0 1 3.648 3.648zM24.817 21.116h-.64a3.64 3.64 0 0 1-3.638 3.64v.562a3.65 3.65 0 0 1 3.648 3.649h.63a3.65 3.65 0 0 1 3.648-3.649v-.553a3.66 3.66 0 0 1-3.648-3.649M13.762 9.384h.509a2.95 2.95 0 0 1 2.95-2.951v-.448a2.96 2.96 0 0 1-2.95-2.95h-.517a2.943 2.943 0 0 1-2.943 2.942v.456a2.95 2.95 0 0 1 2.95 2.95"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
1
src/assets/font-icons/ai-fix.svg
Normal file
1
src/assets/font-icons/ai-fix.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="m28.671 27.086-5.173-5.173a11.48 11.48 0 0 0 2.634-7.347c0-3.09-1.203-5.994-3.388-8.178A11.5 11.5 0 0 0 14.565 3a11.49 11.49 0 0 0-8.177 3.388A11.49 11.49 0 0 0 3 14.566c0 3.09 1.203 5.993 3.388 8.178a11.49 11.49 0 0 0 8.177 3.388c2.713 0 5.283-.93 7.348-2.634l5.174 5.173a1.117 1.117 0 0 0 1.585 0 1.12 1.12 0 0 0 0-1.585M7.973 21.158a9.26 9.26 0 0 1-2.73-6.592c0-2.49.969-4.832 2.73-6.592a9.3 9.3 0 0 1 6.592-2.727c2.388 0 4.776.909 6.594 2.727a9.26 9.26 0 0 1 2.73 6.592c0 2.49-.97 4.831-2.73 6.592-3.636 3.636-9.552 3.635-13.186 0"/><path d="m17.915 10.455-4.58 5.843-2.209-2.207a1.12 1.12 0 1 0-1.584 1.586l2.994 2.995c.532.532 1.41.48 1.874-.113l5.269-6.72a1.12 1.12 0 1 0-1.764-1.384"/></svg>
|
||||||
|
After Width: | Height: | Size: 768 B |
1
src/assets/font-icons/ai.svg
Normal file
1
src/assets/font-icons/ai.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M12.106 12.97h.652a3.713 3.713 0 0 1 3.713-3.714v-.574a3.72 3.72 0 0 1-3.723-3.723h-.642a3.723 3.723 0 0 1-3.723 3.723v.565a3.734 3.734 0 0 1 3.723 3.722M8.232 12.351h-.52a3.01 3.01 0 0 1-3.01 3.01v.458a3.02 3.02 0 0 1 3.01 3.01h.528a3.003 3.003 0 0 1 3.002-3.002v-.465a3.01 3.01 0 0 1-3.01-3.01M18.522 12.394c-.022-.05-.061-.083-.09-.128-.038-.061-.07-.125-.122-.177-.062-.062-.137-.104-.211-.147-.035-.02-.06-.051-.098-.068q-.006 0-.012-.002a1 1 0 0 0-.276-.06c-.037-.003-.073-.02-.11-.02-.036 0-.071.016-.108.02a1 1 0 0 0-.277.06q-.006 0-.012.002c-.036.016-.06.046-.094.066-.076.043-.152.086-.214.149-.052.052-.084.115-.123.176-.028.045-.067.08-.09.13l-5.39 12.448a1 1 0 0 0 1.835.795l1.644-3.796h5.659l1.643 3.796a1 1 0 0 0 1.316.52 1 1 0 0 0 .52-1.315zm-2.881 7.448 1.963-4.533 1.962 4.533zM26.298 17.673a1 1 0 0 0-1 1v6.567a1 1 0 1 0 2 0v-6.567a1 1 0 0 0-1-1M26.298 13.948a1 1 0 0 0-1 1v.393a1 1 0 1 0 2 0v-.393a1 1 0 0 0-1-1"/></svg>
|
||||||
|
After Width: | Height: | Size: 1009 B |
@ -2386,6 +2386,8 @@
|
|||||||
"ToDoListErrorChooseTasks" = "Please enter at least one task.";
|
"ToDoListErrorChooseTasks" = "Please enter at least one task.";
|
||||||
"GiftInfoCollectibleBy" = "Collectible #{number} by **{owner}**";
|
"GiftInfoCollectibleBy" = "Collectible #{number} by **{owner}**";
|
||||||
"PremiumPreviewTodo" = "Checklists";
|
"PremiumPreviewTodo" = "Checklists";
|
||||||
|
"PremiumPreviewAiTools" = "AI Tools";
|
||||||
|
"PremiumPreviewAiToolsDescription" = "Transform your messages and entire chats in your preferred style and language.";
|
||||||
"NativeDownloadFailed" = "Failed to save file to the Downloads folder";
|
"NativeDownloadFailed" = "Failed to save file to the Downloads folder";
|
||||||
"DescriptionAboutTon" = "Offer TON to submit post suggestions to channels on Telegram.";
|
"DescriptionAboutTon" = "Offer TON to submit post suggestions to channels on Telegram.";
|
||||||
"ButtonTopUpViaFragment" = "Top Up Via Fragment";
|
"ButtonTopUpViaFragment" = "Top Up Via Fragment";
|
||||||
@ -2814,4 +2816,30 @@
|
|||||||
"ReminderSetToast" = "You set up a reminder in **Saved Messages**";
|
"ReminderSetToast" = "You set up a reminder in **Saved Messages**";
|
||||||
"NoForwardsRequestReject" = "Reject";
|
"NoForwardsRequestReject" = "Reject";
|
||||||
"NoForwardsRequestAccept" = "Accept";
|
"NoForwardsRequestAccept" = "Accept";
|
||||||
"UnofficialSecurityRisk" = "{peer} uses an unofficial Telegram client — messages to this user may be less secure."
|
"AiMessageEditor" = "AI Editor";
|
||||||
|
"AiMessageEditorTranslate" = "Translate";
|
||||||
|
"AiMessageEditorStyle" = "Style";
|
||||||
|
"AiMessageEditorFix" = "Fix";
|
||||||
|
"AiMessageEditorSelectStyle" = "Select Style";
|
||||||
|
"AiMessageEditorDailyLimitReached" = "Daily limit reached. To increase limits, get {link}.";
|
||||||
|
"AiMessageEditorDailyLimitReachedPremium" = "Daily limit reached.";
|
||||||
|
"AiMessageEditorGenericError" = "Please try again later.";
|
||||||
|
"AiMessageEditorStyleFormal" = "Formal";
|
||||||
|
"AiMessageEditorStyleShort" = "Short";
|
||||||
|
"AiMessageEditorStyleTribal" = "Tribal";
|
||||||
|
"AiMessageEditorStyleCorp" = "Corp";
|
||||||
|
"AiMessageEditorStyleBiblical" = "Biblical";
|
||||||
|
"AiMessageEditorStyleViking" = "Viking";
|
||||||
|
"AiMessageEditorStyleZen" = "Zen";
|
||||||
|
"AiMessageEditorResult" = "Result";
|
||||||
|
"AiMessageEditorOriginal" = "Original";
|
||||||
|
"AiMessageEditorApply" = "Apply";
|
||||||
|
"AiMessageEditorEmojify" = "emojify";
|
||||||
|
"AiMessageEditorTranslation" = "Translation";
|
||||||
|
"TextShowMore" = "more";
|
||||||
|
"TextShowLess" = "less";
|
||||||
|
"AiMessageEditorFrom" = "From";
|
||||||
|
"AiMessageEditorTo" = "To";
|
||||||
|
"TranslationToneNeutral" = "Neutral";
|
||||||
|
"ButtonHelp" = "Help";
|
||||||
|
"UnofficialSecurityRisk" = "{peer} uses an unofficial Telegram client — messages to this user may be less secure.";
|
||||||
|
|||||||
1
src/assets/premium/PremiumAi.svg
Normal file
1
src/assets/premium/PremiumAi.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M11.711 13.73h.765a4.36 4.36 0 0 1 4.358-4.357V8.7a4.37 4.37 0 0 1-4.37-4.37h-.753A4.37 4.37 0 0 1 7.342 8.7v.663a4.38 4.38 0 0 1 4.369 4.369M10.803 16.123a3.01 3.01 0 0 1-3.01-3.01h-.52a3.01 3.01 0 0 1-3.01 3.01v.457a3.02 3.02 0 0 1 3.01 3.01h.528a3.003 3.003 0 0 1 3.002-3.002zM17.991 13.61c-.025-.059-.072-.099-.105-.15-.047-.076-.087-.154-.151-.218-.068-.068-.15-.111-.23-.16-.05-.03-.085-.073-.139-.097l-.018-.003c-.093-.039-.193-.048-.293-.063-.056-.007-.11-.03-.166-.03-.055 0-.11.023-.165.03-.1.015-.2.024-.293.063-.006.002-.012 0-.018.003-.054.024-.09.067-.138.096-.08.05-.163.093-.231.161-.064.064-.104.141-.151.217-.033.052-.08.092-.105.15L10.77 25.194a1.2 1.2 0 0 0 2.203.954l1.46-3.37h4.911l1.46 3.37a1.202 1.202 0 0 0 2.203-.954zm-2.518 6.767 1.416-3.27 1.416 3.27zM25.737 18.359a1.2 1.2 0 0 0-1.2 1.2v6.111a1.2 1.2 0 0 0 2.4 0v-6.11a1.2 1.2 0 0 0-1.2-1.201M25.737 14.493a1.2 1.2 0 0 0-1.2 1.2v.365a1.2 1.2 0 0 0 2.4 0v-.365a1.2 1.2 0 0 0-1.2-1.2" fill="white"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@ -71,6 +71,9 @@ export { default as HeaderMenuContainer } from '../components/middle/HeaderMenuC
|
|||||||
export { default as MiddleSearch } from '../components/middle/search/MiddleSearch';
|
export { default as MiddleSearch } from '../components/middle/search/MiddleSearch';
|
||||||
export { default as ReactionPicker } from '../components/middle/message/reactions/ReactionPicker';
|
export { default as ReactionPicker } from '../components/middle/message/reactions/ReactionPicker';
|
||||||
|
|
||||||
|
export { default as AiMessageEditorModal }
|
||||||
|
from '../components/middle/composer/AiMessageEditorModal/AiMessageEditorModal';
|
||||||
|
|
||||||
export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal';
|
export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal';
|
||||||
export { default as PollModal } from '../components/middle/composer/PollModal';
|
export { default as PollModal } from '../components/middle/composer/PollModal';
|
||||||
export { default as ToDoListModal } from '../components/middle/composer/ToDoListModal';
|
export { default as ToDoListModal } from '../components/middle/composer/ToDoListModal';
|
||||||
|
|||||||
@ -418,6 +418,28 @@
|
|||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
box-shadow: 0 1px 2px var(--color-default-shadow);
|
box-shadow: 0 1px 2px var(--color-default-shadow);
|
||||||
|
|
||||||
|
.ai-composer-button {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0.4375rem;
|
||||||
|
right: 0.625rem;
|
||||||
|
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
|
||||||
|
color: var(--color-composer-button);
|
||||||
|
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
transition: opacity 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-composer-button-hidden {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
&.with-story-tweaks {
|
&.with-story-tweaks {
|
||||||
border-radius: var(--border-radius-default-small);
|
border-radius: var(--border-radius-default-small);
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
|
|||||||
@ -119,6 +119,7 @@ import { formatMediaDuration, formatVoiceRecordDuration } from '../../util/dates
|
|||||||
import { processDeepLink } from '../../util/deeplink';
|
import { processDeepLink } from '../../util/deeplink';
|
||||||
import { tryParseDeepLink } from '../../util/deepLinkParser';
|
import { tryParseDeepLink } from '../../util/deepLinkParser';
|
||||||
import deleteLastCharacterOutsideSelection from '../../util/deleteLastCharacterOutsideSelection';
|
import deleteLastCharacterOutsideSelection from '../../util/deleteLastCharacterOutsideSelection';
|
||||||
|
import calcTextLineHeightAndCount from '../../util/element/calcTextLineHeightAndCount';
|
||||||
import { processMessageInputForCustomEmoji } from '../../util/emoji/customEmojiManager';
|
import { processMessageInputForCustomEmoji } from '../../util/emoji/customEmojiManager';
|
||||||
import { isUserId } from '../../util/entities/ids';
|
import { isUserId } from '../../util/entities/ids';
|
||||||
import { fetchBlob } from '../../util/files';
|
import { fetchBlob } from '../../util/files';
|
||||||
@ -254,6 +255,7 @@ type StateProps = {
|
|||||||
forwardedMessagesCount?: number;
|
forwardedMessagesCount?: number;
|
||||||
pollModal: TabState['pollModal'];
|
pollModal: TabState['pollModal'];
|
||||||
todoListModal: TabState['todoListModal'];
|
todoListModal: TabState['todoListModal'];
|
||||||
|
aiMessageEditorPendingResult: TabState['aiMessageEditorPendingResult'];
|
||||||
botKeyboardMessageId?: number;
|
botKeyboardMessageId?: number;
|
||||||
botKeyboardPlaceholder?: string;
|
botKeyboardPlaceholder?: string;
|
||||||
withScheduledButton?: boolean;
|
withScheduledButton?: boolean;
|
||||||
@ -382,6 +384,7 @@ const Composer = ({
|
|||||||
forwardedMessagesCount,
|
forwardedMessagesCount,
|
||||||
pollModal,
|
pollModal,
|
||||||
todoListModal,
|
todoListModal,
|
||||||
|
aiMessageEditorPendingResult,
|
||||||
botKeyboardMessageId,
|
botKeyboardMessageId,
|
||||||
botKeyboardPlaceholder,
|
botKeyboardPlaceholder,
|
||||||
inputPlaceholder,
|
inputPlaceholder,
|
||||||
@ -457,11 +460,14 @@ const Composer = ({
|
|||||||
const {
|
const {
|
||||||
sendMessage,
|
sendMessage,
|
||||||
clearDraft,
|
clearDraft,
|
||||||
|
saveDraft,
|
||||||
showDialog,
|
showDialog,
|
||||||
openPollModal,
|
openPollModal,
|
||||||
closePollModal,
|
closePollModal,
|
||||||
openTodoListModal,
|
openTodoListModal,
|
||||||
closeTodoListModal,
|
closeTodoListModal,
|
||||||
|
openAiMessageEditorModal,
|
||||||
|
clearAiMessageEditorPendingResult,
|
||||||
loadScheduledHistory,
|
loadScheduledHistory,
|
||||||
openThread,
|
openThread,
|
||||||
addRecentEmoji,
|
addRecentEmoji,
|
||||||
@ -859,6 +865,25 @@ const Composer = ({
|
|||||||
updateInsertingPeerIdMention({ peerId: undefined });
|
updateInsertingPeerIdMention({ peerId: undefined });
|
||||||
}, [insertingPeerIdMention, insertMention]);
|
}, [insertingPeerIdMention, insertMention]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!aiMessageEditorPendingResult) return;
|
||||||
|
|
||||||
|
const { text, shouldClear, shouldSendWithAttachments } = aiMessageEditorPendingResult;
|
||||||
|
|
||||||
|
if (shouldSendWithAttachments) return;
|
||||||
|
|
||||||
|
if (shouldClear) {
|
||||||
|
setHtml('');
|
||||||
|
clearDraft({ chatId, threadId, isLocalOnly: true });
|
||||||
|
} else if (text) {
|
||||||
|
setHtml(getTextWithEntitiesAsHtml(text));
|
||||||
|
saveDraft({ chatId, threadId, text });
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAiMessageEditorPendingResult();
|
||||||
|
}, [aiMessageEditorPendingResult, chatId, clearDraft,
|
||||||
|
clearAiMessageEditorPendingResult, saveDraft, setHtml, threadId]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isOpen: isInlineBotTooltipOpen,
|
isOpen: isInlineBotTooltipOpen,
|
||||||
botId: inlineBotId,
|
botId: inlineBotId,
|
||||||
@ -1369,6 +1394,14 @@ const Composer = ({
|
|||||||
openTodoListModal({ chatId });
|
openTodoListModal({ chatId });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleOpenAiEditor = useLastCallback(() => {
|
||||||
|
const { text, entities } = parseHtmlAsFormattedText(getHtml());
|
||||||
|
openAiMessageEditorModal({
|
||||||
|
chatId,
|
||||||
|
text: { text, entities },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const handleClickBotMenu = useLastCallback(() => {
|
const handleClickBotMenu = useLastCallback(() => {
|
||||||
if (botMenuButton?.type !== 'webApp') {
|
if (botMenuButton?.type !== 'webApp') {
|
||||||
return;
|
return;
|
||||||
@ -1788,7 +1821,25 @@ const Composer = ({
|
|||||||
};
|
};
|
||||||
}, [isSelectModeActive, enableHover, disableHover, isReady]);
|
}, [isSelectModeActive, enableHover, disableHover, isReady]);
|
||||||
|
|
||||||
const hasText = useDerivedState(() => Boolean(getHtml()), [getHtml]);
|
const html = useDerivedState(() => getHtml(), [getHtml]);
|
||||||
|
const hasText = Boolean(html);
|
||||||
|
const [shouldShowAiButton, setShouldShowAiButton] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasAttachments) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestMeasure(() => {
|
||||||
|
const input = inputRef.current;
|
||||||
|
if (!html || !input) {
|
||||||
|
setShouldShowAiButton(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { totalLines } = calcTextLineHeightAndCount(input, true);
|
||||||
|
setShouldShowAiButton(totalLines >= 3);
|
||||||
|
});
|
||||||
|
}, [html, hasAttachments]);
|
||||||
|
|
||||||
const withBotMenuButton = isChatWithBot && botMenuButton?.type === 'webApp' && !editingMessage
|
const withBotMenuButton = isChatWithBot && botMenuButton?.type === 'webApp' && !editingMessage
|
||||||
&& messageListType === 'thread';
|
&& messageListType === 'thread';
|
||||||
@ -2012,8 +2063,11 @@ const Composer = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleSendScheduledAttachments = useLastCallback(
|
const handleSendScheduledAttachments = useLastCallback(
|
||||||
(sendCompressed: boolean, sendGrouped: boolean, isInvertedMedia?: true) => {
|
(
|
||||||
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
sendCompressed: boolean, sendGrouped: boolean, isInvertedMedia?: true,
|
||||||
|
scheduledAt?: number, scheduleRepeatPeriod?: number,
|
||||||
|
) => {
|
||||||
|
if (scheduledAt) {
|
||||||
handleActionWithPaymentConfirmation(
|
handleActionWithPaymentConfirmation(
|
||||||
handleMessageSchedule,
|
handleMessageSchedule,
|
||||||
{ sendCompressed, sendGrouped, isInvertedMedia },
|
{ sendCompressed, sendGrouped, isInvertedMedia },
|
||||||
@ -2022,7 +2076,18 @@ const Composer = ({
|
|||||||
currentMessageList!,
|
currentMessageList!,
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
} else {
|
||||||
|
requestCalendar((calendarScheduledAt, calendarRepeatPeriod) => {
|
||||||
|
handleActionWithPaymentConfirmation(
|
||||||
|
handleMessageSchedule,
|
||||||
|
{ sendCompressed, sendGrouped, isInvertedMedia },
|
||||||
|
calendarScheduledAt,
|
||||||
|
calendarRepeatPeriod,
|
||||||
|
currentMessageList!,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -2191,6 +2256,17 @@ const Composer = ({
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
faded
|
||||||
|
className={buildClassName('ai-composer-button', (!shouldShowAiButton
|
||||||
|
|| hasAttachments) && 'ai-composer-button-hidden')}
|
||||||
|
color="translucent"
|
||||||
|
ariaLabel={lang('AiMessageEditor')}
|
||||||
|
iconName="ai"
|
||||||
|
tabIndex={shouldShowAiButton && !hasAttachments ? 0 : -1}
|
||||||
|
onClick={handleOpenAiEditor}
|
||||||
|
/>
|
||||||
{isInMessageList && (
|
{isInMessageList && (
|
||||||
<>
|
<>
|
||||||
<InlineBotTooltip
|
<InlineBotTooltip
|
||||||
@ -2739,6 +2815,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
forwardedMessagesCount: isForwarding ? forwardMessageIds!.length : undefined,
|
forwardedMessagesCount: isForwarding ? forwardMessageIds!.length : undefined,
|
||||||
pollModal: tabState.pollModal,
|
pollModal: tabState.pollModal,
|
||||||
todoListModal: tabState.todoListModal,
|
todoListModal: tabState.todoListModal,
|
||||||
|
aiMessageEditorPendingResult: tabState.aiMessageEditorPendingResult,
|
||||||
stickersForEmoji: global.stickers.forEmoji.stickers,
|
stickersForEmoji: global.stickers.forEmoji.stickers,
|
||||||
customEmojiForEmoji: global.customEmojis.forEmoji.stickers,
|
customEmojiForEmoji: global.customEmojis.forEmoji.stickers,
|
||||||
chatFullInfo,
|
chatFullInfo,
|
||||||
@ -2794,7 +2871,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
paidMessagesStars,
|
paidMessagesStars,
|
||||||
shouldPaidMessageAutoApprove,
|
shouldPaidMessageAutoApprove,
|
||||||
isSilentPosting,
|
isSilentPosting,
|
||||||
isPaymentMessageConfirmDialogOpen: tabState.isPaymentMessageConfirmDialogOpen,
|
isPaymentMessageConfirmDialogOpen: tabState.isPaymentMessageConfirmDialogOpen
|
||||||
|
&& !tabState.aiMessageEditorModal,
|
||||||
starsBalance,
|
starsBalance,
|
||||||
isStarsBalanceModalOpen,
|
isStarsBalanceModalOpen,
|
||||||
shouldDisplayGiftsButton: userFullInfo?.shouldDisplayGiftsButton,
|
shouldDisplayGiftsButton: userFullInfo?.shouldDisplayGiftsButton,
|
||||||
|
|||||||
@ -120,6 +120,7 @@ const RecipientPicker = ({
|
|||||||
orderedFolderIds,
|
orderedFolderIds,
|
||||||
chatFoldersById,
|
chatFoldersById,
|
||||||
maxFolders,
|
maxFolders,
|
||||||
|
noEmoticons: true,
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -685,6 +685,25 @@ function processEntity({
|
|||||||
{renderNestedMessagePart()}
|
{renderNestedMessagePart()}
|
||||||
</FormattedDate>
|
</FormattedDate>
|
||||||
);
|
);
|
||||||
|
case ApiMessageEntityTypes.DiffInsert:
|
||||||
|
return (
|
||||||
|
<span className="text-entity-diff-insert" data-entity-type={entity.type}>
|
||||||
|
{renderNestedMessagePart()}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case ApiMessageEntityTypes.DiffReplace:
|
||||||
|
return (
|
||||||
|
<span className="text-entity-diff-replace" data-entity-type={entity.type}>
|
||||||
|
<span className="text-entity-diff-replace-old">{entity.oldText}</span>
|
||||||
|
<span className="text-entity-diff-replace-new">{renderNestedMessagePart()}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case ApiMessageEntityTypes.DiffDelete:
|
||||||
|
return (
|
||||||
|
<span className="text-entity-diff-delete" data-entity-type={entity.type}>
|
||||||
|
{renderNestedMessagePart()}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return renderNestedMessagePart();
|
return renderNestedMessagePart();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,6 +56,7 @@ export const PREMIUM_FEATURE_TITLES: Record<ApiPremiumSection, string> = {
|
|||||||
last_seen: 'PremiumPreviewLastSeen',
|
last_seen: 'PremiumPreviewLastSeen',
|
||||||
message_privacy: 'PremiumPreviewMessagePrivacy',
|
message_privacy: 'PremiumPreviewMessagePrivacy',
|
||||||
effects: 'Premium.MessageEffects',
|
effects: 'Premium.MessageEffects',
|
||||||
|
ai_compose: 'PremiumPreviewAiTools',
|
||||||
todo: 'PremiumPreviewTodo',
|
todo: 'PremiumPreviewTodo',
|
||||||
pm_noforwards: 'PremiumPreviewNoForwards',
|
pm_noforwards: 'PremiumPreviewNoForwards',
|
||||||
};
|
};
|
||||||
@ -79,6 +80,7 @@ export const PREMIUM_FEATURE_DESCRIPTIONS: Record<ApiPremiumSection, string> = {
|
|||||||
last_seen: 'PremiumPreviewLastSeenDescription',
|
last_seen: 'PremiumPreviewLastSeenDescription',
|
||||||
message_privacy: 'PremiumPreviewMessagePrivacyDescription',
|
message_privacy: 'PremiumPreviewMessagePrivacyDescription',
|
||||||
effects: 'Premium.MessageEffectsInfo',
|
effects: 'Premium.MessageEffectsInfo',
|
||||||
|
ai_compose: 'PremiumPreviewAiToolsDescription',
|
||||||
todo: 'PremiumPreviewTodoDescription',
|
todo: 'PremiumPreviewTodoDescription',
|
||||||
pm_noforwards: 'PremiumPreviewNoForwardsDescription',
|
pm_noforwards: 'PremiumPreviewNoForwardsDescription',
|
||||||
};
|
};
|
||||||
@ -310,7 +312,7 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const i = promo.videoSections.indexOf(section);
|
const i = promo.videoSections.indexOf(section);
|
||||||
const shouldUseNewLang = section === 'todo';
|
const shouldUseNewLang = section === 'todo' || section === 'ai_compose';
|
||||||
return (
|
return (
|
||||||
<div className={styles.slide}>
|
<div className={styles.slide}>
|
||||||
<div className={styles.frame}>
|
<div className={styles.frame}>
|
||||||
@ -326,7 +328,7 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
|||||||
<h1 className={styles.title}>
|
<h1 className={styles.title}>
|
||||||
{shouldUseNewLang
|
{shouldUseNewLang
|
||||||
? lang(
|
? lang(
|
||||||
PREMIUM_FEATURE_TITLES['todo'] as keyof LangPair,
|
PREMIUM_FEATURE_TITLES[section] as keyof LangPair,
|
||||||
undefined,
|
undefined,
|
||||||
{ withNodes: true, renderTextFilters: ['br'] },
|
{ withNodes: true, renderTextFilters: ['br'] },
|
||||||
)
|
)
|
||||||
@ -335,7 +337,7 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
|||||||
<div className={styles.description}>
|
<div className={styles.description}>
|
||||||
{renderText(shouldUseNewLang
|
{renderText(shouldUseNewLang
|
||||||
? lang(
|
? lang(
|
||||||
PREMIUM_FEATURE_DESCRIPTIONS['todo'] as keyof LangPair,
|
PREMIUM_FEATURE_DESCRIPTIONS[section] as keyof LangPair,
|
||||||
undefined,
|
undefined,
|
||||||
{ withNodes: true, renderTextFilters: ['br'] },
|
{ withNodes: true, renderTextFilters: ['br'] },
|
||||||
)
|
)
|
||||||
|
|||||||
@ -49,6 +49,7 @@ import PremiumSubscriptionOption from './PremiumSubscriptionOption';
|
|||||||
import styles from './PremiumMainModal.module.scss';
|
import styles from './PremiumMainModal.module.scss';
|
||||||
|
|
||||||
import PremiumAds from '../../../assets/premium/PremiumAds.svg';
|
import PremiumAds from '../../../assets/premium/PremiumAds.svg';
|
||||||
|
import PremiumAi from '../../../assets/premium/PremiumAi.svg';
|
||||||
import PremiumBadge from '../../../assets/premium/PremiumBadge.svg';
|
import PremiumBadge from '../../../assets/premium/PremiumBadge.svg';
|
||||||
import PremiumChats from '../../../assets/premium/PremiumChats.svg';
|
import PremiumChats from '../../../assets/premium/PremiumChats.svg';
|
||||||
import PremiumEffects from '../../../assets/premium/PremiumEffects.svg';
|
import PremiumEffects from '../../../assets/premium/PremiumEffects.svg';
|
||||||
@ -89,6 +90,7 @@ const PREMIUM_FEATURE_COLOR_ICONS: Record<ApiPremiumSection, string> = {
|
|||||||
last_seen: PremiumLastSeen,
|
last_seen: PremiumLastSeen,
|
||||||
message_privacy: PremiumMessagePrivacy,
|
message_privacy: PremiumMessagePrivacy,
|
||||||
effects: PremiumEffects,
|
effects: PremiumEffects,
|
||||||
|
ai_compose: PremiumAi,
|
||||||
todo: PremiumBadge,
|
todo: PremiumBadge,
|
||||||
pm_noforwards: PremiumNoforwards,
|
pm_noforwards: PremiumNoforwards,
|
||||||
};
|
};
|
||||||
@ -444,7 +446,7 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className={buildClassName(styles.list, isPremium && styles.noButton)}>
|
<div className={buildClassName(styles.list, isPremium && styles.noButton)}>
|
||||||
{filteredSections.map((section, index) => {
|
{filteredSections.map((section, index) => {
|
||||||
const shouldUseNewLang = section === 'todo' || section === 'pm_noforwards';
|
const shouldUseNewLang = section === 'todo' || section === 'pm_noforwards' || section === 'ai_compose';
|
||||||
return (
|
return (
|
||||||
<PremiumFeatureItem
|
<PremiumFeatureItem
|
||||||
key={section}
|
key={section}
|
||||||
|
|||||||
@ -0,0 +1,132 @@
|
|||||||
|
.labelRow {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelTransition {
|
||||||
|
position: relative;
|
||||||
|
width: auto;
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelSlide {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
height: 0.0625rem;
|
||||||
|
margin-block: 0.75rem;
|
||||||
|
opacity: 0.75;
|
||||||
|
background: var(--color-borders);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultArea {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||||
|
transition: height 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingContainer {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
inset: 0;
|
||||||
|
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
|
||||||
|
opacity: 1;
|
||||||
|
background: var(--color-background);
|
||||||
|
|
||||||
|
transition: opacity 150ms ease-out;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultTransition {
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 150ms ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultContent {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--color-text);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionsRow {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emojifyCheckbox {
|
||||||
|
min-height: 1.75rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
padding-inline-start: 0.25rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emojifyCheckboxControl {
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emojifyCheckboxLabel {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorMessage {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding-bottom: 0.125rem;
|
||||||
|
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyButton {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.5rem;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0 !important;
|
||||||
|
transition: opacity 150ms ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 150ms ease-out;
|
||||||
|
}
|
||||||
@ -0,0 +1,140 @@
|
|||||||
|
import type { TeactNode } from '../../../../lib/teact/teact';
|
||||||
|
import { memo, useLayoutEffect, useRef, useState } from '../../../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../../../global';
|
||||||
|
|
||||||
|
import { requestMeasure } from '../../../../lib/fasterdom/fasterdom';
|
||||||
|
import buildClassName from '../../../../util/buildClassName';
|
||||||
|
import { copyTextToClipboard } from '../../../../util/clipboard';
|
||||||
|
|
||||||
|
import useLang from '../../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import Button from '../../../ui/Button';
|
||||||
|
import Link from '../../../ui/Link';
|
||||||
|
import TextLoadingPlaceholder from '../../../ui/placeholder/TextLoadingPlaceholder';
|
||||||
|
import Transition from '../../../ui/Transition';
|
||||||
|
|
||||||
|
import styles from './AiEditorShared.module.scss';
|
||||||
|
|
||||||
|
const MIN_HEIGHT = 100;
|
||||||
|
const HEIGHT_PADDING = 4;
|
||||||
|
|
||||||
|
type AiEditorResultAreaProps = {
|
||||||
|
isLoading?: boolean;
|
||||||
|
transitionKey?: number;
|
||||||
|
className?: string;
|
||||||
|
children: TeactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AiEditorResultArea = memo(({
|
||||||
|
isLoading,
|
||||||
|
transitionKey,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: AiEditorResultAreaProps) => {
|
||||||
|
const contentRef = useRef<HTMLDivElement>();
|
||||||
|
const [height, setHeight] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (isLoading || !contentRef.current) return;
|
||||||
|
|
||||||
|
requestMeasure(() => {
|
||||||
|
if (!contentRef.current) return;
|
||||||
|
const newHeight = contentRef.current.scrollHeight + HEIGHT_PADDING;
|
||||||
|
setHeight(newHeight);
|
||||||
|
});
|
||||||
|
}, [children, isLoading, transitionKey]);
|
||||||
|
|
||||||
|
const displayHeight = height ?? MIN_HEIGHT;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={buildClassName(styles.resultArea, className)}
|
||||||
|
style={`height: ${displayHeight}px`}
|
||||||
|
>
|
||||||
|
<div className={buildClassName(styles.loadingContainer, !isLoading && styles.hidden)}>
|
||||||
|
<TextLoadingPlaceholder lines={6} />
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
name="fade"
|
||||||
|
activeKey={transitionKey ?? 0}
|
||||||
|
className={buildClassName(styles.resultTransition, isLoading && styles.hidden)}
|
||||||
|
>
|
||||||
|
<div ref={contentRef} className={styles.resultContent}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type AiEditorErrorMessageProps = {
|
||||||
|
error?: 'floodPremium' | 'aiError' | 'generic';
|
||||||
|
isPremium?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AiEditorErrorMessage = memo(({
|
||||||
|
error,
|
||||||
|
isPremium,
|
||||||
|
}: AiEditorErrorMessageProps) => {
|
||||||
|
const { openPremiumModal } = getActions();
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const handleOpenPremiumModal = useLastCallback(() => {
|
||||||
|
openPremiumModal({ initialSection: 'ai_compose' });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!error) return undefined;
|
||||||
|
|
||||||
|
const isFloodError = error === 'floodPremium';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.errorMessage}>
|
||||||
|
{isFloodError ? (
|
||||||
|
isPremium
|
||||||
|
? lang('AiMessageEditorDailyLimitReachedPremium')
|
||||||
|
: lang('AiMessageEditorDailyLimitReached', {
|
||||||
|
link: (
|
||||||
|
<Link isPrimary onClick={handleOpenPremiumModal}>
|
||||||
|
{lang('TelegramPremium')}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
}, { withNodes: true })
|
||||||
|
) : lang('AiMessageEditorGenericError')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type AiEditorCopyButtonProps = {
|
||||||
|
textToCopy?: string;
|
||||||
|
isHidden?: boolean;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AiEditorCopyButton = memo(({
|
||||||
|
textToCopy,
|
||||||
|
isHidden,
|
||||||
|
className,
|
||||||
|
}: AiEditorCopyButtonProps) => {
|
||||||
|
const { showNotification } = getActions();
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const handleCopy = useLastCallback(() => {
|
||||||
|
if (textToCopy) {
|
||||||
|
copyTextToClipboard(textToCopy);
|
||||||
|
showNotification({ message: { key: 'TextCopied' } });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={buildClassName(styles.copyButton, isHidden && styles.hidden, className)}
|
||||||
|
round
|
||||||
|
size="tiny"
|
||||||
|
color="translucent-primary"
|
||||||
|
iconName="copy"
|
||||||
|
ariaLabel={lang('Copy')}
|
||||||
|
onClick={handleCopy}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import type { OwnProps } from './AiMessageEditorModal';
|
||||||
|
|
||||||
|
import { Bundles } from '../../../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const AiMessageEditorModalAsync = (props: OwnProps) => {
|
||||||
|
const { modal } = props;
|
||||||
|
const AiMessageEditorModal = useModuleLoader(Bundles.Extra, 'AiMessageEditorModal', !modal);
|
||||||
|
|
||||||
|
return AiMessageEditorModal ? <AiMessageEditorModal {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AiMessageEditorModalAsync;
|
||||||
@ -0,0 +1,168 @@
|
|||||||
|
.modal {
|
||||||
|
max-width: 26rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalDialog {
|
||||||
|
overflow: hidden;
|
||||||
|
height: min(38rem, 80vh);
|
||||||
|
background-color: var(--color-background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transitionWrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
flex-grow: 1;
|
||||||
|
min-height: 0;
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 70vh;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabListWrapper {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
pointer-events: none;
|
||||||
|
content: "";
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
z-index: 0;
|
||||||
|
top: calc(100% - 2rem);
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
height: 3.5rem;
|
||||||
|
margin-inline: 1rem;
|
||||||
|
|
||||||
|
background: linear-gradient(to bottom, var(--color-background-secondary) 0%, transparent 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabList {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
margin: 0.5rem 1rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
gap: 0.125rem;
|
||||||
|
padding: 0.25rem 1.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabContent {
|
||||||
|
overflow: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
min-height: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
padding-top: 3.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorBlock {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
|
||||||
|
background: var(--color-background);
|
||||||
|
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
padding: 0 1rem 1rem;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
height: 1.5rem;
|
||||||
|
margin-inline: 1rem;
|
||||||
|
|
||||||
|
background: linear-gradient(to top, var(--color-background-secondary) 0%, transparent 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.applyButton {
|
||||||
|
flex: 1;
|
||||||
|
margin-inline-end: 0.5rem;
|
||||||
|
border-radius: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sendButton {
|
||||||
|
overflow: visible;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
:global(.icon) {
|
||||||
|
margin-inline-end: 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.paidStarsBadge {
|
||||||
|
position: absolute;
|
||||||
|
top: -1rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
height: auto;
|
||||||
|
margin-top: 0.625rem;
|
||||||
|
padding-block: 0.25rem;
|
||||||
|
padding-inline: 0.375rem;
|
||||||
|
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: var(--font-weight-semibold) !important;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.icon) {
|
||||||
|
margin-inline-start: 0;
|
||||||
|
margin-inline-end: 0.0625rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.paidStarsBadgeText {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpButton {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
top: 0.875rem;
|
||||||
|
right: 1.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.CustomSendMenu {
|
||||||
|
bottom: 1rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,396 @@
|
|||||||
|
import { memo, useEffect, useMemo, useRef } from '../../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../../global';
|
||||||
|
|
||||||
|
import type { ApiChat } from '../../../../api/types';
|
||||||
|
import type { TabState } from '../../../../global/types';
|
||||||
|
import type { AnimationLevel } from '../../../../types';
|
||||||
|
import type { IconName } from '../../../../types/icons';
|
||||||
|
|
||||||
|
import { SCHEDULED_WHEN_ONLINE } from '../../../../config';
|
||||||
|
import { getPeerTitle } from '../../../../global/helpers/peers';
|
||||||
|
import {
|
||||||
|
selectCanScheduleUntilOnline,
|
||||||
|
selectChat,
|
||||||
|
selectIsChatWithSelf,
|
||||||
|
selectPeerPaidMessagesStars,
|
||||||
|
selectTabState,
|
||||||
|
} from '../../../../global/selectors';
|
||||||
|
import { selectCurrentMessageList } from '../../../../global/selectors/messages';
|
||||||
|
import { selectAnimationLevel } from '../../../../global/selectors/sharedState';
|
||||||
|
import { selectIsCurrentUserPremium } from '../../../../global/selectors/users';
|
||||||
|
import buildClassName from '../../../../util/buildClassName';
|
||||||
|
import { resolveTransitionName } from '../../../../util/resolveTransitionName';
|
||||||
|
|
||||||
|
import useContextMenuHandlers from '../../../../hooks/useContextMenuHandlers';
|
||||||
|
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
|
||||||
|
import useLang from '../../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||||
|
import useSchedule from '../../../../hooks/useSchedule';
|
||||||
|
import usePaidMessageConfirmation from '../hooks/usePaidMessageConfirmation';
|
||||||
|
|
||||||
|
import AnimatedCounter from '../../../common/AnimatedCounter';
|
||||||
|
import Icon from '../../../common/icons/Icon';
|
||||||
|
import PaymentMessageConfirmDialog from '../../../common/PaymentMessageConfirmDialog';
|
||||||
|
import Button from '../../../ui/Button';
|
||||||
|
import Modal from '../../../ui/Modal';
|
||||||
|
import TabList from '../../../ui/TabList';
|
||||||
|
import Transition from '../../../ui/Transition';
|
||||||
|
import CustomSendMenu from '../CustomSendMenu.async';
|
||||||
|
import AiTextFixEditor from './AiTextFixEditor';
|
||||||
|
import AiTextStyleEditor from './AiTextStyleEditor';
|
||||||
|
import AiTextTranslateEditor from './AiTextTranslateEditor';
|
||||||
|
|
||||||
|
import styles from './AiMessageEditorModal.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
modal: TabState['aiMessageEditorModal'];
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
animationLevel: AnimationLevel;
|
||||||
|
isPremium?: boolean;
|
||||||
|
isChatWithSelf?: boolean;
|
||||||
|
canScheduleUntilOnline?: boolean;
|
||||||
|
isInScheduledList?: boolean;
|
||||||
|
chat?: ApiChat;
|
||||||
|
paidMessagesStars?: number;
|
||||||
|
isPaymentMessageConfirmDialogOpen?: boolean;
|
||||||
|
starsBalance: number;
|
||||||
|
isStarsBalanceModalOpen?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const INDEX_TO_TAB_ID = ['translate', 'style', 'fix'] as const;
|
||||||
|
|
||||||
|
const TAB_TRANSLATE = 0;
|
||||||
|
const TAB_STYLE = 1;
|
||||||
|
const TAB_FIX = 2;
|
||||||
|
|
||||||
|
const TAB_ID_TO_INDEX = Object.fromEntries(
|
||||||
|
INDEX_TO_TAB_ID.map((id, i) => [id, i]),
|
||||||
|
) as Record<typeof INDEX_TO_TAB_ID[number], number>;
|
||||||
|
|
||||||
|
const AiMessageEditorModal = ({
|
||||||
|
modal,
|
||||||
|
animationLevel,
|
||||||
|
isPremium,
|
||||||
|
isChatWithSelf,
|
||||||
|
canScheduleUntilOnline,
|
||||||
|
isInScheduledList,
|
||||||
|
chat,
|
||||||
|
paidMessagesStars,
|
||||||
|
isPaymentMessageConfirmDialogOpen,
|
||||||
|
starsBalance,
|
||||||
|
isStarsBalanceModalOpen,
|
||||||
|
}: OwnProps & StateProps) => {
|
||||||
|
const {
|
||||||
|
closeAiMessageEditorModal,
|
||||||
|
setAiMessageEditorTab,
|
||||||
|
applyAiMessageEditorResult,
|
||||||
|
sendAiMessageEditorResult,
|
||||||
|
composeWithAiMessageEditor,
|
||||||
|
openCocoonModal,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const mainButtonRef = useRef<HTMLButtonElement>();
|
||||||
|
|
||||||
|
const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isContextMenuOpen: isCustomSendMenuOpen,
|
||||||
|
handleContextMenu,
|
||||||
|
handleContextMenuClose,
|
||||||
|
handleContextMenuHide,
|
||||||
|
} = useContextMenuHandlers(mainButtonRef, !modal);
|
||||||
|
|
||||||
|
const starsForMessage = paidMessagesStars || 0;
|
||||||
|
const shouldRenderPaidBadge = Boolean(paidMessagesStars);
|
||||||
|
|
||||||
|
const {
|
||||||
|
closeConfirmDialog: closeConfirmModalPayForMessage,
|
||||||
|
handleWithConfirmation: handleActionWithPaymentConfirmation,
|
||||||
|
dialogHandler: confirmModalPayForMessageHandler,
|
||||||
|
shouldAutoApprove: shouldPaidMessageAutoApprove,
|
||||||
|
setAutoApprove: setShouldPaidMessageAutoApprove,
|
||||||
|
} = usePaidMessageConfirmation(starsForMessage, Boolean(isStarsBalanceModalOpen), starsBalance, true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isCustomSendMenuOpen) {
|
||||||
|
handleContextMenuHide();
|
||||||
|
handleContextMenuClose();
|
||||||
|
}
|
||||||
|
}, [isCustomSendMenuOpen, handleContextMenuHide, handleContextMenuClose]);
|
||||||
|
|
||||||
|
const isOpen = Boolean(modal);
|
||||||
|
const renderingModal = useCurrentOrPrev(modal);
|
||||||
|
|
||||||
|
const {
|
||||||
|
activeTab,
|
||||||
|
text,
|
||||||
|
translateTab,
|
||||||
|
styleTab,
|
||||||
|
fixTab,
|
||||||
|
} = renderingModal || {};
|
||||||
|
|
||||||
|
const currentTabState = activeTab === 'translate' ? translateTab
|
||||||
|
: activeTab === 'style' ? styleTab : fixTab;
|
||||||
|
const isLoading = currentTabState?.isLoading;
|
||||||
|
const error = currentTabState?.error;
|
||||||
|
|
||||||
|
const tabs = useMemo((): { icon: IconName; title: string }[] => [
|
||||||
|
{ icon: 'language', title: lang('AiMessageEditorTranslate') },
|
||||||
|
{ icon: 'ai-edit', title: lang('AiMessageEditorStyle') },
|
||||||
|
{ icon: 'ai-fix', title: lang('AiMessageEditorFix') },
|
||||||
|
], [lang]);
|
||||||
|
|
||||||
|
const activeTabIndex = TAB_ID_TO_INDEX[activeTab || 'style'] ?? TAB_STYLE;
|
||||||
|
|
||||||
|
const handleTabChange = useLastCallback((index: number) => {
|
||||||
|
const tab = INDEX_TO_TAB_ID[index];
|
||||||
|
setAiMessageEditorTab({ tab });
|
||||||
|
|
||||||
|
if (!text?.text) return;
|
||||||
|
|
||||||
|
switch (tab) {
|
||||||
|
case 'translate':
|
||||||
|
composeWithAiMessageEditor({
|
||||||
|
translateToLang: translateTab?.selectedLanguage,
|
||||||
|
changeTone: translateTab?.selectedTone,
|
||||||
|
isEmojify: translateTab?.shouldEmojify,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'style':
|
||||||
|
if (styleTab?.selectedTone) {
|
||||||
|
composeWithAiMessageEditor({ changeTone: styleTab.selectedTone, isEmojify: styleTab?.shouldEmojify });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'fix':
|
||||||
|
composeWithAiMessageEditor({ shouldProofread: true });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleApply = useLastCallback(() => {
|
||||||
|
applyAiMessageEditorResult();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOpenCocoonModal = useLastCallback(() => {
|
||||||
|
openCocoonModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSendAction = useLastCallback((
|
||||||
|
isSilent?: boolean, scheduledAt?: number, scheduleRepeatPeriod?: number,
|
||||||
|
) => {
|
||||||
|
sendAiMessageEditorResult({ isSilent, scheduledAt, scheduleRepeatPeriod });
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSend = useLastCallback(() => {
|
||||||
|
if (isInScheduledList) {
|
||||||
|
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
||||||
|
sendAiMessageEditorResult({ scheduledAt, scheduleRepeatPeriod });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
handleActionWithPaymentConfirmation(handleSendAction);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSendSilent = useLastCallback(() => {
|
||||||
|
handleActionWithPaymentConfirmation(handleSendAction, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSendSchedule = useLastCallback(() => {
|
||||||
|
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
||||||
|
sendAiMessageEditorResult({ scheduledAt, scheduleRepeatPeriod });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSendWhenOnline = useLastCallback(() => {
|
||||||
|
sendAiMessageEditorResult({ scheduledAt: SCHEDULED_WHEN_ONLINE });
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderTabContent() {
|
||||||
|
switch (activeTabIndex) {
|
||||||
|
case TAB_TRANSLATE:
|
||||||
|
return (
|
||||||
|
<div className={styles.tabContent}>
|
||||||
|
<AiTextTranslateEditor
|
||||||
|
text={text}
|
||||||
|
selectedLanguage={translateTab?.selectedLanguage}
|
||||||
|
selectedTone={translateTab?.selectedTone}
|
||||||
|
shouldEmojify={translateTab?.shouldEmojify}
|
||||||
|
isLoading={translateTab?.isLoading}
|
||||||
|
result={translateTab?.result}
|
||||||
|
error={translateTab?.error}
|
||||||
|
isPremium={isPremium}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case TAB_STYLE:
|
||||||
|
return (
|
||||||
|
<div className={styles.tabContent}>
|
||||||
|
<AiTextStyleEditor
|
||||||
|
text={text}
|
||||||
|
selectedTone={styleTab?.selectedTone}
|
||||||
|
shouldEmojify={styleTab?.shouldEmojify}
|
||||||
|
isLoading={styleTab?.isLoading}
|
||||||
|
result={styleTab?.result}
|
||||||
|
error={styleTab?.error}
|
||||||
|
isPremium={isPremium}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case TAB_FIX:
|
||||||
|
return (
|
||||||
|
<div className={styles.tabContent}>
|
||||||
|
<AiTextFixEditor
|
||||||
|
text={text}
|
||||||
|
isLoading={fixTab?.isLoading}
|
||||||
|
result={fixTab?.result}
|
||||||
|
error={fixTab?.error}
|
||||||
|
isPremium={isPremium}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
title={lang('AiMessageEditor')}
|
||||||
|
hasCloseButton
|
||||||
|
onClose={closeAiMessageEditorModal}
|
||||||
|
className={styles.modal}
|
||||||
|
headerClassName="modal-header-condensed-wide"
|
||||||
|
dialogClassName={styles.modalDialog}
|
||||||
|
contentClassName={styles.modalContent}
|
||||||
|
headerRightToolBar={(
|
||||||
|
<Button
|
||||||
|
className={styles.helpButton}
|
||||||
|
round
|
||||||
|
size="tiny"
|
||||||
|
color="translucent"
|
||||||
|
iconName="help"
|
||||||
|
ariaLabel={lang('ButtonHelp')}
|
||||||
|
onClick={handleOpenCocoonModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
isSlim
|
||||||
|
>
|
||||||
|
<div className={styles.tabListWrapper}>
|
||||||
|
<TabList
|
||||||
|
tabs={tabs}
|
||||||
|
activeTab={activeTabIndex}
|
||||||
|
onSwitchTab={handleTabChange}
|
||||||
|
className={styles.tabList}
|
||||||
|
tabClassName={styles.tab}
|
||||||
|
stretched
|
||||||
|
itemAlignment="vertical"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.transitionWrapper}>
|
||||||
|
<Transition
|
||||||
|
className={styles.transition}
|
||||||
|
name={resolveTransitionName('slideOptimized', animationLevel, undefined, lang.isRtl)}
|
||||||
|
activeKey={activeTabIndex}
|
||||||
|
renderCount={tabs.length}
|
||||||
|
>
|
||||||
|
{renderTabContent()}
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<Button
|
||||||
|
className={styles.applyButton}
|
||||||
|
disabled={isLoading || Boolean(error)}
|
||||||
|
onClick={handleApply}
|
||||||
|
>
|
||||||
|
{lang('AiMessageEditorApply')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
ref={mainButtonRef}
|
||||||
|
className={styles.sendButton}
|
||||||
|
round
|
||||||
|
color="primary"
|
||||||
|
disabled={isLoading || Boolean(error)}
|
||||||
|
ariaLabel={lang('Send')}
|
||||||
|
onClick={handleSend}
|
||||||
|
onContextMenu={!isInScheduledList && !paidMessagesStars ? handleContextMenu : undefined}
|
||||||
|
iconName="new-send"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className={buildClassName(
|
||||||
|
styles.paidStarsBadge,
|
||||||
|
!shouldRenderPaidBadge && styles.hidden,
|
||||||
|
)}
|
||||||
|
nonInteractive
|
||||||
|
size="tiny"
|
||||||
|
color="stars"
|
||||||
|
pill
|
||||||
|
fluid
|
||||||
|
>
|
||||||
|
<div className={styles.paidStarsBadgeText}>
|
||||||
|
<Icon name="star" />
|
||||||
|
<AnimatedCounter text={lang.number(starsForMessage)} />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Button>
|
||||||
|
{isOpen && !isInScheduledList && (
|
||||||
|
<CustomSendMenu
|
||||||
|
isOpen={isCustomSendMenuOpen}
|
||||||
|
canSchedule
|
||||||
|
canScheduleUntilOnline={canScheduleUntilOnline}
|
||||||
|
onSendSilent={!isChatWithSelf ? handleSendSilent : undefined}
|
||||||
|
onSendSchedule={handleSendSchedule}
|
||||||
|
onSendWhenOnline={handleSendWhenOnline}
|
||||||
|
onClose={handleContextMenuClose}
|
||||||
|
onCloseAnimationEnd={handleContextMenuHide}
|
||||||
|
isSavedMessages={isChatWithSelf}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{calendar}
|
||||||
|
<PaymentMessageConfirmDialog
|
||||||
|
isOpen={Boolean(isPaymentMessageConfirmDialogOpen)}
|
||||||
|
onClose={closeConfirmModalPayForMessage}
|
||||||
|
userName={chat ? getPeerTitle(lang, chat) : undefined}
|
||||||
|
messagePriceInStars={paidMessagesStars || 0}
|
||||||
|
messagesCount={1}
|
||||||
|
shouldAutoApprove={shouldPaidMessageAutoApprove}
|
||||||
|
setAutoApprove={setShouldPaidMessageAutoApprove}
|
||||||
|
confirmHandler={confirmModalPayForMessageHandler}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { modal }): Complete<StateProps> => {
|
||||||
|
const chatId = modal?.chatId;
|
||||||
|
const currentMessageList = selectCurrentMessageList(global);
|
||||||
|
const tabState = selectTabState(global);
|
||||||
|
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||||
|
const paidMessagesStars = chatId ? selectPeerPaidMessagesStars(global, chatId) : undefined;
|
||||||
|
const starsBalance = global.stars?.balance.amount || 0;
|
||||||
|
const isStarsBalanceModalOpen = Boolean(tabState.starsBalanceModal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
animationLevel: selectAnimationLevel(global),
|
||||||
|
isPremium: selectIsCurrentUserPremium(global),
|
||||||
|
isChatWithSelf: chatId ? selectIsChatWithSelf(global, chatId) : undefined,
|
||||||
|
canScheduleUntilOnline: currentMessageList?.chatId
|
||||||
|
? selectCanScheduleUntilOnline(global, currentMessageList.chatId)
|
||||||
|
: undefined,
|
||||||
|
isInScheduledList: currentMessageList?.type === 'scheduled',
|
||||||
|
chat,
|
||||||
|
paidMessagesStars,
|
||||||
|
isPaymentMessageConfirmDialogOpen: tabState.isPaymentMessageConfirmDialogOpen,
|
||||||
|
starsBalance,
|
||||||
|
isStarsBalanceModalOpen,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(AiMessageEditorModal));
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
.section {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
import { memo } from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { ApiComposedMessageWithAI, ApiFormattedText } from '../../../../api/types';
|
||||||
|
|
||||||
|
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||||
|
|
||||||
|
import useLang from '../../../../hooks/useLang';
|
||||||
|
|
||||||
|
import ExpandableText from '../../../ui/ExpandableText';
|
||||||
|
import { AiEditorCopyButton, AiEditorErrorMessage, AiEditorResultArea } from './AiEditorShared';
|
||||||
|
|
||||||
|
import sharedStyles from './AiEditorShared.module.scss';
|
||||||
|
import modalStyles from './AiMessageEditorModal.module.scss';
|
||||||
|
import styles from './AiTextFixEditor.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
text?: ApiFormattedText;
|
||||||
|
isLoading?: boolean;
|
||||||
|
result?: ApiComposedMessageWithAI;
|
||||||
|
error?: 'floodPremium' | 'aiError' | 'generic';
|
||||||
|
isPremium?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AiTextFixEditor = ({
|
||||||
|
text,
|
||||||
|
isLoading,
|
||||||
|
result,
|
||||||
|
error,
|
||||||
|
isPremium,
|
||||||
|
}: OwnProps) => {
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const hasError = Boolean(error);
|
||||||
|
const displayResult = result?.diffText || result?.resultText;
|
||||||
|
|
||||||
|
function renderResultText() {
|
||||||
|
if (hasError) {
|
||||||
|
return <AiEditorErrorMessage error={error} isPremium={isPremium} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayResult && renderDiffText(displayResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={modalStyles.editorBlock}>
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={sharedStyles.labelRow}>
|
||||||
|
<span className={sharedStyles.label}>
|
||||||
|
{lang('AiMessageEditorOriginal')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ExpandableText text={text?.text} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={sharedStyles.separator} />
|
||||||
|
|
||||||
|
<div className={sharedStyles.labelRow}>
|
||||||
|
<span className={sharedStyles.label}>{lang('AiMessageEditorResult')}</span>
|
||||||
|
</div>
|
||||||
|
<AiEditorResultArea isLoading={isLoading}>
|
||||||
|
{renderResultText()}
|
||||||
|
</AiEditorResultArea>
|
||||||
|
<AiEditorCopyButton
|
||||||
|
textToCopy={result?.resultText?.text || text?.text}
|
||||||
|
isHidden={isLoading || hasError || !displayResult?.text}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderDiffText(formattedText: ApiFormattedText) {
|
||||||
|
const { text, entities } = formattedText;
|
||||||
|
|
||||||
|
return renderTextWithEntities({
|
||||||
|
text,
|
||||||
|
entities,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(AiTextFixEditor);
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
.styleBlock {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabList {
|
||||||
|
--tab-radius: 1rem;
|
||||||
|
|
||||||
|
margin-top: 0.4375rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-inline: -1rem;
|
||||||
|
padding-inline: 0.9375rem;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabListIndicator {
|
||||||
|
padding-inline: 0.9375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
gap: 0.125rem;
|
||||||
|
padding: 0.125rem 0.625rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLabel,
|
||||||
|
.resultLabel {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewText {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
@ -0,0 +1,164 @@
|
|||||||
|
import { memo, useMemo } from '../../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../../global';
|
||||||
|
|
||||||
|
import type { ApiAiComposeStyle, ApiComposedMessageWithAI, ApiFormattedText } from '../../../../api/types';
|
||||||
|
import type { TabWithProperties } from '../../../ui/TabList';
|
||||||
|
import { ApiMessageEntityTypes } from '../../../../api/types';
|
||||||
|
|
||||||
|
import EMOJI_REGEX from '../../../../lib/twemojiRegex';
|
||||||
|
import buildClassName from '../../../../util/buildClassName';
|
||||||
|
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||||
|
import { getStyleTitle } from './helpers';
|
||||||
|
|
||||||
|
import useLang from '../../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import CheckboxField from '../../../gili/templates/CheckboxField';
|
||||||
|
import TabList from '../../../ui/TabList';
|
||||||
|
import Transition from '../../../ui/Transition';
|
||||||
|
import { AiEditorCopyButton, AiEditorErrorMessage, AiEditorResultArea } from './AiEditorShared';
|
||||||
|
|
||||||
|
import sharedStyles from './AiEditorShared.module.scss';
|
||||||
|
import modalStyles from './AiMessageEditorModal.module.scss';
|
||||||
|
import styles from './AiTextStyleEditor.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
text?: ApiFormattedText;
|
||||||
|
selectedTone?: string;
|
||||||
|
shouldEmojify?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
result?: ApiComposedMessageWithAI;
|
||||||
|
error?: 'floodPremium' | 'aiError' | 'generic';
|
||||||
|
isPremium?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
aiComposeStyles: ApiAiComposeStyle[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const AiTextStyleEditor = ({
|
||||||
|
text,
|
||||||
|
selectedTone,
|
||||||
|
shouldEmojify,
|
||||||
|
isLoading,
|
||||||
|
result,
|
||||||
|
error,
|
||||||
|
isPremium,
|
||||||
|
aiComposeStyles,
|
||||||
|
}: OwnProps & StateProps) => {
|
||||||
|
const {
|
||||||
|
setAiMessageEditorStyleOptions,
|
||||||
|
composeWithAiMessageEditor,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const hasResult = Boolean(result?.resultText);
|
||||||
|
const hasRequest = Boolean(selectedTone) || shouldEmojify;
|
||||||
|
const shouldShowError = Boolean(error) && hasRequest;
|
||||||
|
|
||||||
|
const styleTabs = useMemo((): TabWithProperties[] => aiComposeStyles.map(({ documentId, title, tone }) => {
|
||||||
|
const emojiMatch = title.match(EMOJI_REGEX);
|
||||||
|
const localizedTitle = getStyleTitle(lang, tone, title);
|
||||||
|
|
||||||
|
return {
|
||||||
|
emoticon: documentId ? {
|
||||||
|
type: ApiMessageEntityTypes.CustomEmoji,
|
||||||
|
offset: 0,
|
||||||
|
length: emojiMatch?.[0].length || 2,
|
||||||
|
documentId,
|
||||||
|
} : undefined,
|
||||||
|
title: localizedTitle,
|
||||||
|
};
|
||||||
|
}), [aiComposeStyles, lang]);
|
||||||
|
|
||||||
|
const activeStyleIndex = aiComposeStyles.findIndex(({ tone }) => tone === selectedTone);
|
||||||
|
|
||||||
|
const handleStyleSelect = useLastCallback((index: number) => {
|
||||||
|
const styleId = aiComposeStyles[index].tone;
|
||||||
|
setAiMessageEditorStyleOptions({ selectedTone: styleId });
|
||||||
|
composeWithAiMessageEditor({ changeTone: styleId, isEmojify: shouldEmojify });
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleEmojifyChange = useLastCallback((newEmojify: boolean) => {
|
||||||
|
if (!selectedTone && !newEmojify) {
|
||||||
|
setAiMessageEditorStyleOptions({ shouldEmojify: newEmojify, clearResult: true });
|
||||||
|
} else {
|
||||||
|
setAiMessageEditorStyleOptions({ shouldEmojify: newEmojify });
|
||||||
|
composeWithAiMessageEditor({ changeTone: selectedTone, isEmojify: newEmojify });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayText = hasResult ? result?.resultText : text;
|
||||||
|
const showResultLabel = hasRequest || isLoading;
|
||||||
|
const displayLabel = showResultLabel ? lang('AiMessageEditorResult') : lang('AiMessageEditorOriginal');
|
||||||
|
|
||||||
|
const transitionKey = (activeStyleIndex >= 0 ? activeStyleIndex : 0) + (shouldEmojify ? aiComposeStyles.length : 0);
|
||||||
|
|
||||||
|
function renderPreviewText() {
|
||||||
|
if (shouldShowError) {
|
||||||
|
return <AiEditorErrorMessage error={error} isPremium={isPremium} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.previewText}>
|
||||||
|
{displayText?.text && renderTextWithEntities({
|
||||||
|
text: displayText.text,
|
||||||
|
entities: displayText.entities,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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={sharedStyles.separator} />
|
||||||
|
|
||||||
|
<div className={sharedStyles.optionsRow}>
|
||||||
|
<Transition
|
||||||
|
name="fade"
|
||||||
|
activeKey={showResultLabel ? 1 : 0}
|
||||||
|
className={sharedStyles.labelTransition}
|
||||||
|
slideClassName={sharedStyles.labelSlide}
|
||||||
|
>
|
||||||
|
<span className={showResultLabel ? styles.resultLabel : styles.textLabel}>{displayLabel}</span>
|
||||||
|
</Transition>
|
||||||
|
<CheckboxField
|
||||||
|
className={sharedStyles.emojifyCheckbox}
|
||||||
|
controlClassName={sharedStyles.emojifyCheckboxControl}
|
||||||
|
labelClassName={sharedStyles.emojifyCheckboxLabel}
|
||||||
|
label={lang('AiMessageEditorEmojify')}
|
||||||
|
checked={Boolean(shouldEmojify)}
|
||||||
|
isRound
|
||||||
|
onChange={handleEmojifyChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AiEditorResultArea isLoading={isLoading} transitionKey={transitionKey}>
|
||||||
|
{renderPreviewText()}
|
||||||
|
</AiEditorResultArea>
|
||||||
|
<AiEditorCopyButton
|
||||||
|
textToCopy={displayText?.text}
|
||||||
|
isHidden={isLoading || shouldShowError || !displayText?.text}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): Complete<StateProps> => {
|
||||||
|
return {
|
||||||
|
aiComposeStyles: global.appConfig?.aiComposeStyles ?? [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(AiTextStyleEditor));
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
.section {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sourceLanguage {
|
||||||
|
padding-inline-start: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.languageLink {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 0.125rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-primary);
|
||||||
|
|
||||||
|
transition: background-color 0.15s;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primary-opacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.languageMenu :global(.bubble) {
|
||||||
|
left: -2rem !important;
|
||||||
|
|
||||||
|
overflow: initial;
|
||||||
|
|
||||||
|
min-width: 10rem;
|
||||||
|
padding: 0 !important;
|
||||||
|
|
||||||
|
background: none !important;
|
||||||
|
backdrop-filter: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.languageMenu :global(.TranslationToneSelector) {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 100%;
|
||||||
|
transform: translateX(0.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.languageItems {
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
max-height: 16rem;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
border-radius: var(--border-radius-default);
|
||||||
|
|
||||||
|
background: var(--color-background);
|
||||||
|
box-shadow: 0 0.25rem 0.5rem 0.125rem var(--color-default-shadow);
|
||||||
|
|
||||||
|
body:not(.no-menu-blur) & {
|
||||||
|
background: var(--color-background-compact-menu);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,244 @@
|
|||||||
|
import { memo, useMemo, useRef, useState } from '../../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../../global';
|
||||||
|
|
||||||
|
import type { ApiAiComposeStyle, ApiComposedMessageWithAI, ApiFormattedText } from '../../../../api/types';
|
||||||
|
import type { IAnchorPosition } from '../../../../types';
|
||||||
|
|
||||||
|
import { SUPPORTED_TRANSLATION_LANGUAGES } from '../../../../config';
|
||||||
|
import buildClassName from '../../../../util/buildClassName';
|
||||||
|
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||||
|
|
||||||
|
import useFlag from '../../../../hooks/useFlag';
|
||||||
|
import useLang from '../../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||||
|
import useTextLanguage from '../../../../hooks/useTextLanguage';
|
||||||
|
|
||||||
|
import CheckboxField from '../../../gili/templates/CheckboxField';
|
||||||
|
import ExpandableText from '../../../ui/ExpandableText';
|
||||||
|
import Menu from '../../../ui/Menu';
|
||||||
|
import MenuItem from '../../../ui/MenuItem';
|
||||||
|
import TranslationToneSelector from '../../message/TranslationToneSelector';
|
||||||
|
import { AiEditorCopyButton, AiEditorErrorMessage, AiEditorResultArea } from './AiEditorShared';
|
||||||
|
|
||||||
|
import sharedStyles from './AiEditorShared.module.scss';
|
||||||
|
import modalStyles from './AiMessageEditorModal.module.scss';
|
||||||
|
import styles from './AiTextTranslateEditor.module.scss';
|
||||||
|
|
||||||
|
const EMPTY_AI_COMPOSE_STYLES: ApiAiComposeStyle[] = [];
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
text?: ApiFormattedText;
|
||||||
|
selectedLanguage?: string;
|
||||||
|
selectedTone?: string;
|
||||||
|
shouldEmojify?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
result?: ApiComposedMessageWithAI;
|
||||||
|
error?: 'floodPremium' | 'aiError' | 'generic';
|
||||||
|
isPremium?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
aiComposeStyles: ApiAiComposeStyle[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const AiTextTranslateEditor = ({
|
||||||
|
text,
|
||||||
|
selectedLanguage,
|
||||||
|
selectedTone,
|
||||||
|
shouldEmojify,
|
||||||
|
isLoading,
|
||||||
|
result,
|
||||||
|
error,
|
||||||
|
isPremium,
|
||||||
|
aiComposeStyles,
|
||||||
|
}: OwnProps & StateProps) => {
|
||||||
|
const {
|
||||||
|
setAiMessageEditorTranslateOptions,
|
||||||
|
composeWithAiMessageEditor,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const [isMenuOpen, openMenu, closeMenu] = useFlag(false);
|
||||||
|
const [menuAnchor, setMenuAnchor] = useState<IAnchorPosition | undefined>();
|
||||||
|
|
||||||
|
const triggerRef = useRef<HTMLSpanElement>();
|
||||||
|
|
||||||
|
const detectedLanguage = useTextLanguage(text?.text);
|
||||||
|
const hasError = Boolean(error);
|
||||||
|
|
||||||
|
const currentLanguageCode = lang.code;
|
||||||
|
|
||||||
|
const languages = useMemo(() => SUPPORTED_TRANSLATION_LANGUAGES.map((langCode: string) => {
|
||||||
|
const displayNames = new Intl.DisplayNames([currentLanguageCode], { type: 'language' });
|
||||||
|
const displayName = displayNames.of(langCode) || langCode;
|
||||||
|
|
||||||
|
return {
|
||||||
|
langCode,
|
||||||
|
displayName,
|
||||||
|
};
|
||||||
|
}), [currentLanguageCode]);
|
||||||
|
|
||||||
|
const detectedLanguageName = useMemo(() => {
|
||||||
|
if (!detectedLanguage) return undefined;
|
||||||
|
const displayNames = new Intl.DisplayNames([currentLanguageCode], { type: 'language' });
|
||||||
|
return displayNames.of(detectedLanguage);
|
||||||
|
}, [detectedLanguage, currentLanguageCode]);
|
||||||
|
|
||||||
|
const selectedLanguageName = useMemo(() => {
|
||||||
|
if (!selectedLanguage) return undefined;
|
||||||
|
const displayNames = new Intl.DisplayNames([currentLanguageCode], { type: 'language' });
|
||||||
|
return displayNames.of(selectedLanguage);
|
||||||
|
}, [selectedLanguage, currentLanguageCode]);
|
||||||
|
|
||||||
|
const handleLanguageSelect = useLastCallback((langCode: string) => {
|
||||||
|
setAiMessageEditorTranslateOptions({ selectedLanguage: langCode });
|
||||||
|
composeWithAiMessageEditor({
|
||||||
|
translateToLang: langCode,
|
||||||
|
isEmojify: shouldEmojify,
|
||||||
|
changeTone: selectedTone,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleEmojifyChange = useLastCallback((newEmojify: boolean) => {
|
||||||
|
setAiMessageEditorTranslateOptions({ shouldEmojify: newEmojify });
|
||||||
|
if (selectedLanguage) {
|
||||||
|
composeWithAiMessageEditor({
|
||||||
|
translateToLang: selectedLanguage,
|
||||||
|
isEmojify: newEmojify,
|
||||||
|
changeTone: selectedTone,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTriggerClick = useLastCallback(() => {
|
||||||
|
if (triggerRef.current) {
|
||||||
|
const rect = triggerRef.current.getBoundingClientRect();
|
||||||
|
setMenuAnchor({ x: rect.left, y: rect.bottom });
|
||||||
|
openMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getTriggerElement = useLastCallback(() => triggerRef.current);
|
||||||
|
const getRootElement = useLastCallback(() => document.body);
|
||||||
|
const getMenuElement = useLastCallback(() => document.querySelector('.language-menu .bubble'));
|
||||||
|
const getLayout = useLastCallback(() => ({ withPortal: true }));
|
||||||
|
|
||||||
|
const handleToneSelect = useLastCallback((tone?: string) => {
|
||||||
|
setAiMessageEditorTranslateOptions({ selectedTone: tone });
|
||||||
|
if (selectedLanguage) {
|
||||||
|
composeWithAiMessageEditor({
|
||||||
|
translateToLang: selectedLanguage,
|
||||||
|
isEmojify: shouldEmojify,
|
||||||
|
changeTone: tone,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayResult = result?.resultText;
|
||||||
|
|
||||||
|
const languageIndex = SUPPORTED_TRANSLATION_LANGUAGES.indexOf(selectedLanguage || '');
|
||||||
|
const toneIndex = aiComposeStyles.findIndex(({ tone }) => tone === selectedTone);
|
||||||
|
const totalLanguages = SUPPORTED_TRANSLATION_LANGUAGES.length;
|
||||||
|
const totalTones = aiComposeStyles.length;
|
||||||
|
const transitionKey = languageIndex
|
||||||
|
+ (toneIndex + 1) * totalLanguages
|
||||||
|
+ (shouldEmojify ? totalLanguages * (totalTones + 1) : 0);
|
||||||
|
|
||||||
|
function renderResultText() {
|
||||||
|
if (hasError) {
|
||||||
|
return <AiEditorErrorMessage error={error} isPremium={isPremium} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayResult && renderTextWithEntities({
|
||||||
|
text: displayResult.text,
|
||||||
|
entities: displayResult.entities,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={modalStyles.editorBlock}>
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={sharedStyles.labelRow}>
|
||||||
|
<span className={sharedStyles.label}>
|
||||||
|
{lang('AiMessageEditorFrom')}
|
||||||
|
</span>
|
||||||
|
<span className={styles.sourceLanguage}>
|
||||||
|
{detectedLanguageName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ExpandableText text={text?.text} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={sharedStyles.separator} />
|
||||||
|
|
||||||
|
<div className={sharedStyles.optionsRow}>
|
||||||
|
<div className={sharedStyles.labelRow}>
|
||||||
|
<span className={sharedStyles.label}>
|
||||||
|
{lang('AiMessageEditorTo')}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
ref={triggerRef}
|
||||||
|
className={styles.languageLink}
|
||||||
|
onClick={handleTriggerClick}
|
||||||
|
>
|
||||||
|
{selectedLanguageName}
|
||||||
|
<i className="icon icon-down" />
|
||||||
|
</span>
|
||||||
|
<Menu
|
||||||
|
isOpen={isMenuOpen}
|
||||||
|
anchor={menuAnchor}
|
||||||
|
getTriggerElement={getTriggerElement}
|
||||||
|
getRootElement={getRootElement}
|
||||||
|
getMenuElement={getMenuElement}
|
||||||
|
getLayout={getLayout}
|
||||||
|
className={buildClassName('language-menu', 'with-menu-transitions', styles.languageMenu)}
|
||||||
|
autoClose
|
||||||
|
onClose={closeMenu}
|
||||||
|
withPortal
|
||||||
|
>
|
||||||
|
<TranslationToneSelector
|
||||||
|
selectedTone={selectedTone}
|
||||||
|
onSelectTone={handleToneSelect}
|
||||||
|
/>
|
||||||
|
<div className={buildClassName(styles.languageItems, 'custom-scroll')}>
|
||||||
|
{languages.map(({ langCode, displayName }) => (
|
||||||
|
<MenuItem
|
||||||
|
key={langCode}
|
||||||
|
onClick={() => handleLanguageSelect(langCode)}
|
||||||
|
>
|
||||||
|
{displayName}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
<CheckboxField
|
||||||
|
className={sharedStyles.emojifyCheckbox}
|
||||||
|
controlClassName={sharedStyles.emojifyCheckboxControl}
|
||||||
|
labelClassName={sharedStyles.emojifyCheckboxLabel}
|
||||||
|
label={lang('AiMessageEditorEmojify')}
|
||||||
|
checked={Boolean(shouldEmojify)}
|
||||||
|
isRound
|
||||||
|
onChange={handleEmojifyChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AiEditorResultArea isLoading={isLoading} transitionKey={transitionKey}>
|
||||||
|
{renderResultText()}
|
||||||
|
</AiEditorResultArea>
|
||||||
|
<AiEditorCopyButton
|
||||||
|
textToCopy={result?.resultText?.text || text?.text}
|
||||||
|
isHidden={isLoading || hasError || !displayResult?.text}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): Complete<StateProps> => {
|
||||||
|
return {
|
||||||
|
aiComposeStyles: global.appConfig.aiComposeStyles || EMPTY_AI_COMPOSE_STYLES,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(AiTextTranslateEditor));
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import type { LangFn } from '../../../../util/localization';
|
||||||
|
|
||||||
|
export function getStyleTitle(lang: LangFn, tone: string, fallbackTitle: string) {
|
||||||
|
if (!tone) return fallbackTitle;
|
||||||
|
const capitalizedTone = tone.charAt(0).toUpperCase() + tone.slice(1);
|
||||||
|
const key = `AiMessageEditorStyle${capitalizedTone}`;
|
||||||
|
// @ts-ignore - Dynamic lang key
|
||||||
|
const translated = lang(key);
|
||||||
|
return translated !== key ? translated : fallbackTitle;
|
||||||
|
}
|
||||||
@ -42,7 +42,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.symbol-menu-button, .mobile-symbol-menu-button {
|
.symbol-menu-button, .mobile-symbol-menu-button {
|
||||||
align-self: center;
|
align-self: flex-end;
|
||||||
margin-right: -1.5rem;
|
margin-right: -1.5rem;
|
||||||
margin-left: -0.25rem !important;
|
margin-left: -0.25rem !important;
|
||||||
color: var(--color-composer-button);
|
color: var(--color-composer-button);
|
||||||
@ -146,6 +146,27 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aiButton {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
|
||||||
|
color: var(--color-composer-button);
|
||||||
|
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
transition: opacity 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aiButtonHidden {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.dropTarget {
|
.dropTarget {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -219,6 +240,8 @@
|
|||||||
|
|
||||||
.send-wrapper {
|
.send-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-bottom: 0.1875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.send {
|
.send {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { type FC, memo, useEffect, useMemo, useRef, useState } from '../../../li
|
|||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiAttachment, ApiChatMember, ApiMessage, ApiSticker } from '../../../api/types';
|
import type { ApiAttachment, ApiChatMember, ApiMessage, ApiSticker } from '../../../api/types';
|
||||||
import type { GlobalState } from '../../../global/types';
|
import type { GlobalState, TabState } from '../../../global/types';
|
||||||
import type { MessageListType, ThreadId } from '../../../types';
|
import type { MessageListType, ThreadId } from '../../../types';
|
||||||
import type { Signal } from '../../../util/signals';
|
import type { Signal } from '../../../util/signals';
|
||||||
|
|
||||||
@ -13,17 +13,20 @@ import {
|
|||||||
SUPPORTED_PHOTO_CONTENT_TYPES,
|
SUPPORTED_PHOTO_CONTENT_TYPES,
|
||||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
import { requestMeasure, requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||||
import { getAttachmentMediaType } from '../../../global/helpers';
|
import { getAttachmentMediaType } from '../../../global/helpers';
|
||||||
import { selectChatFullInfo, selectIsChatWithSelf, selectTabState } from '../../../global/selectors';
|
import { selectChatFullInfo, selectIsChatWithSelf, selectTabState } from '../../../global/selectors';
|
||||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||||
import { selectSharedSettings } from '../../../global/selectors/sharedState';
|
import { selectSharedSettings } from '../../../global/selectors/sharedState';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
|
import calcTextLineHeightAndCount from '../../../util/element/calcTextLineHeightAndCount';
|
||||||
import { validateFiles } from '../../../util/files';
|
import { validateFiles } from '../../../util/files';
|
||||||
import { formatStarsAsIcon } from '../../../util/localization/format';
|
import { formatStarsAsIcon } from '../../../util/localization/format';
|
||||||
|
import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
|
||||||
import { removeAllSelections } from '../../../util/selection';
|
import { removeAllSelections } from '../../../util/selection';
|
||||||
import { openSystemFilesDialog } from '../../../util/systemFilesDialog';
|
import { openSystemFilesDialog } from '../../../util/systemFilesDialog';
|
||||||
|
import { getTextWithEntitiesAsHtml } from '../../common/helpers/renderTextWithEntities';
|
||||||
import buildAttachment from './helpers/buildAttachment';
|
import buildAttachment from './helpers/buildAttachment';
|
||||||
import getFilesFromDataTransferItems from './helpers/getFilesFromDataTransferItems';
|
import getFilesFromDataTransferItems from './helpers/getFilesFromDataTransferItems';
|
||||||
import { getHtmlTextLength } from './helpers/getHtmlTextLength';
|
import { getHtmlTextLength } from './helpers/getHtmlTextLength';
|
||||||
@ -81,7 +84,10 @@ export type OwnProps = {
|
|||||||
onAttachmentsUpdate: (attachments: ApiAttachment[]) => void;
|
onAttachmentsUpdate: (attachments: ApiAttachment[]) => void;
|
||||||
onClear: NoneToVoidFunction;
|
onClear: NoneToVoidFunction;
|
||||||
onSendSilent: (sendCompressed: boolean, sendGrouped: boolean, isInvertedMedia?: true) => void;
|
onSendSilent: (sendCompressed: boolean, sendGrouped: boolean, isInvertedMedia?: true) => void;
|
||||||
onSendScheduled: (sendCompressed: boolean, sendGrouped: boolean, isInvertedMedia?: true) => void;
|
onSendScheduled: (
|
||||||
|
sendCompressed: boolean, sendGrouped: boolean, isInvertedMedia?: true,
|
||||||
|
scheduledAt?: number, scheduleRepeatPeriod?: number,
|
||||||
|
) => void;
|
||||||
onCustomEmojiSelect: (emoji: ApiSticker) => void;
|
onCustomEmojiSelect: (emoji: ApiSticker) => void;
|
||||||
onRemoveSymbol: VoidFunction;
|
onRemoveSymbol: VoidFunction;
|
||||||
onEmojiSelect: (emoji: string) => void;
|
onEmojiSelect: (emoji: string) => void;
|
||||||
@ -101,6 +107,7 @@ type StateProps = {
|
|||||||
attachmentSettings: GlobalState['attachmentSettings'];
|
attachmentSettings: GlobalState['attachmentSettings'];
|
||||||
shouldSaveAttachmentsCompression?: boolean;
|
shouldSaveAttachmentsCompression?: boolean;
|
||||||
shouldOpenMessageMediaEditor?: boolean;
|
shouldOpenMessageMediaEditor?: boolean;
|
||||||
|
aiMessageEditorPendingResult?: TabState['aiMessageEditorPendingResult'];
|
||||||
};
|
};
|
||||||
|
|
||||||
const ATTACHMENT_MODAL_INPUT_ID = 'caption-input-text';
|
const ATTACHMENT_MODAL_INPUT_ID = 'caption-input-text';
|
||||||
@ -137,6 +144,7 @@ const AttachmentModal = ({
|
|||||||
canSchedule,
|
canSchedule,
|
||||||
paidMessagesStars,
|
paidMessagesStars,
|
||||||
shouldOpenMessageMediaEditor,
|
shouldOpenMessageMediaEditor,
|
||||||
|
aiMessageEditorPendingResult,
|
||||||
onAttachmentsUpdate,
|
onAttachmentsUpdate,
|
||||||
onCaptionUpdate,
|
onCaptionUpdate,
|
||||||
onSend,
|
onSend,
|
||||||
@ -153,7 +161,7 @@ const AttachmentModal = ({
|
|||||||
const svgRef = useRef<SVGSVGElement>();
|
const svgRef = useRef<SVGSVGElement>();
|
||||||
const {
|
const {
|
||||||
addRecentCustomEmoji, addRecentEmoji, updateAttachmentSettings, resetMessageMediaEditorRequest,
|
addRecentCustomEmoji, addRecentEmoji, updateAttachmentSettings, resetMessageMediaEditorRequest,
|
||||||
updateShouldSaveAttachmentsCompression,
|
updateShouldSaveAttachmentsCompression, openAiMessageEditorModal, clearAiMessageEditorPendingResult,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -174,6 +182,8 @@ const AttachmentModal = ({
|
|||||||
|
|
||||||
const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag();
|
const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag();
|
||||||
const [editingAttachmentIndex, setEditingAttachmentIndex] = useState<number | undefined>(undefined);
|
const [editingAttachmentIndex, setEditingAttachmentIndex] = useState<number | undefined>(undefined);
|
||||||
|
const [shouldShowAiButton, setShouldShowAiButton] = useState(false);
|
||||||
|
const html = useDerivedState(() => getHtml(), [getHtml]);
|
||||||
const editingAttachment = editingAttachmentIndex !== undefined
|
const editingAttachment = editingAttachmentIndex !== undefined
|
||||||
? attachments[editingAttachmentIndex] : undefined;
|
? attachments[editingAttachmentIndex] : undefined;
|
||||||
|
|
||||||
@ -308,22 +318,69 @@ const AttachmentModal = ({
|
|||||||
handleContextMenuHide,
|
handleContextMenuHide,
|
||||||
} = useContextMenuHandlers(mainButtonRef, !canShowCustomSendMenu || !isOpen);
|
} = useContextMenuHandlers(mainButtonRef, !canShowCustomSendMenu || !isOpen);
|
||||||
|
|
||||||
const sendAttachments = useLastCallback((isSilent?: boolean, shouldSendScheduled?: boolean) => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
requestMeasure(() => {
|
||||||
const send = ((shouldSchedule || shouldSendScheduled) && isForMessage && !editingMessage) ? onSendScheduled
|
const input = inputRef.current;
|
||||||
: isSilent ? onSendSilent : onSend;
|
if (!html || !input) {
|
||||||
send(isSendingCompressed, shouldSendGrouped, isInvertedMedia);
|
setShouldShowAiButton(false);
|
||||||
updateAttachmentSettings({
|
return;
|
||||||
...(shouldSaveAttachmentsCompression && {
|
}
|
||||||
defaultAttachmentCompression: attachmentSettings.shouldCompress ? 'compress' : 'original',
|
const { totalLines } = calcTextLineHeightAndCount(input, true);
|
||||||
}),
|
setShouldShowAiButton(totalLines >= 3);
|
||||||
shouldSendGrouped,
|
});
|
||||||
isInvertedMedia,
|
}, [html, isOpen]);
|
||||||
shouldSendInHighQuality,
|
|
||||||
});
|
const handleOpenAiEditor = useLastCallback(() => {
|
||||||
}
|
const { text, entities } = parseHtmlAsFormattedText(getHtml());
|
||||||
|
openAiMessageEditorModal({
|
||||||
|
chatId,
|
||||||
|
text: { text, entities },
|
||||||
|
isFromAttachment: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sendAttachments = useLastCallback((
|
||||||
|
isSilent?: boolean, scheduledAt?: number | true, scheduleRepeatPeriod?: number,
|
||||||
|
) => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
const shouldSendScheduled = (shouldSchedule || scheduledAt) && isForMessage && !editingMessage;
|
||||||
|
if (shouldSendScheduled) {
|
||||||
|
const actualScheduledAt = typeof scheduledAt === 'number' ? scheduledAt : undefined;
|
||||||
|
onSendScheduled(isSendingCompressed, shouldSendGrouped, isInvertedMedia, actualScheduledAt, scheduleRepeatPeriod);
|
||||||
|
} else if (isSilent) {
|
||||||
|
onSendSilent(isSendingCompressed, shouldSendGrouped, isInvertedMedia);
|
||||||
|
} else {
|
||||||
|
onSend(isSendingCompressed, shouldSendGrouped, isInvertedMedia);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAttachmentSettings({
|
||||||
|
...(shouldSaveAttachmentsCompression && {
|
||||||
|
defaultAttachmentCompression: attachmentSettings.shouldCompress ? 'compress' : 'original',
|
||||||
|
}),
|
||||||
|
shouldSendGrouped,
|
||||||
|
isInvertedMedia,
|
||||||
|
shouldSendInHighQuality,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSendWithAiResult = useLastCallback(() => {
|
||||||
|
if (!aiMessageEditorPendingResult?.shouldSendWithAttachments || !isOpen) return;
|
||||||
|
|
||||||
|
const { text, isSilent, scheduledAt, scheduleRepeatPeriod } = aiMessageEditorPendingResult;
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
onCaptionUpdate(getTextWithEntitiesAsHtml(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
sendAttachments(isSilent, scheduledAt, scheduleRepeatPeriod);
|
||||||
|
clearAiMessageEditorPendingResult();
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleSendWithAiResult();
|
||||||
|
}, [aiMessageEditorPendingResult, handleSendWithAiResult]);
|
||||||
|
|
||||||
const handleSendSilent = useLastCallback(() => {
|
const handleSendSilent = useLastCallback(() => {
|
||||||
sendAttachments(true);
|
sendAttachments(true);
|
||||||
});
|
});
|
||||||
@ -749,6 +806,16 @@ const AttachmentModal = ({
|
|||||||
styles.captionWrapper,
|
styles.captionWrapper,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
size="tiny"
|
||||||
|
color="translucent"
|
||||||
|
tabIndex={shouldShowAiButton ? 0 : -1}
|
||||||
|
className={buildClassName(styles.aiButton, !shouldShowAiButton && styles.aiButtonHidden)}
|
||||||
|
onClick={handleOpenAiEditor}
|
||||||
|
ariaLabel={lang('AiMessageEditor')}
|
||||||
|
iconName="ai"
|
||||||
|
/>
|
||||||
<MentionTooltip
|
<MentionTooltip
|
||||||
isOpen={isMentionTooltipOpen}
|
isOpen={isMentionTooltipOpen}
|
||||||
filteredUsers={mentionFilteredUsers}
|
filteredUsers={mentionFilteredUsers}
|
||||||
@ -861,7 +928,11 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
attachmentSettings,
|
attachmentSettings,
|
||||||
} = global;
|
} = global;
|
||||||
|
|
||||||
const { shouldSaveAttachmentsCompression, shouldOpenMessageMediaEditor } = selectTabState(global);
|
const {
|
||||||
|
shouldSaveAttachmentsCompression,
|
||||||
|
shouldOpenMessageMediaEditor,
|
||||||
|
aiMessageEditorPendingResult,
|
||||||
|
} = selectTabState(global);
|
||||||
const chatFullInfo = selectChatFullInfo(global, chatId);
|
const chatFullInfo = selectChatFullInfo(global, chatId);
|
||||||
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
|
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
|
||||||
const { shouldSuggestCustomEmoji } = global.settings.byKey;
|
const { shouldSuggestCustomEmoji } = global.settings.byKey;
|
||||||
@ -882,6 +953,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
attachmentSettings,
|
attachmentSettings,
|
||||||
shouldSaveAttachmentsCompression,
|
shouldSaveAttachmentsCompression,
|
||||||
shouldOpenMessageMediaEditor,
|
shouldOpenMessageMediaEditor,
|
||||||
|
aiMessageEditorPendingResult,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(AttachmentModal));
|
)(AttachmentModal));
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export default function usePaidMessageConfirmation(
|
|||||||
starsForAllMessages: number,
|
starsForAllMessages: number,
|
||||||
isStarsBalanceModeOpen: boolean,
|
isStarsBalanceModeOpen: boolean,
|
||||||
starsBalance: number,
|
starsBalance: number,
|
||||||
|
shouldDelayConfirmHandler?: boolean,
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
shouldPaidMessageAutoApprove,
|
shouldPaidMessageAutoApprove,
|
||||||
@ -45,14 +46,19 @@ export default function usePaidMessageConfirmation(
|
|||||||
const dialogHandler = useLastCallback(() => {
|
const dialogHandler = useLastCallback(() => {
|
||||||
if (starsForAllMessages > starsBalance) {
|
if (starsForAllMessages > starsBalance) {
|
||||||
handleStarsTopup();
|
handleStarsTopup();
|
||||||
|
} else if (shouldDelayConfirmHandler) {
|
||||||
|
setTimeout(() => {
|
||||||
|
confirmPaymentHandlerRef?.current?.();
|
||||||
|
}, 250);
|
||||||
} else {
|
} else {
|
||||||
confirmPaymentHandlerRef?.current?.();
|
confirmPaymentHandlerRef?.current?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
getActions().closePaymentMessageConfirmDialogOpen();
|
getActions().closePaymentMessageConfirmDialogOpen();
|
||||||
if (shouldAutoApprove) getActions().setPaidMessageAutoApprove();
|
if (shouldAutoApprove) getActions().setPaidMessageAutoApprove();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleWithConfirmation = <T extends (...args: any[]) => void>(
|
const handleWithConfirmation = useLastCallback(<T extends (...args: any[]) => void>(
|
||||||
handler: T,
|
handler: T,
|
||||||
...args: Parameters<T>
|
...args: Parameters<T>
|
||||||
) => {
|
) => {
|
||||||
@ -70,7 +76,7 @@ export default function usePaidMessageConfirmation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
handler(...args);
|
handler(...args);
|
||||||
};
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
closeConfirmDialog,
|
closeConfirmDialog,
|
||||||
|
|||||||
@ -76,6 +76,13 @@
|
|||||||
transform: translateY(calc(-100% - 0.5rem));
|
transform: translateY(calc(-100% - 0.5rem));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.TranslationToneSelector {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 100%;
|
||||||
|
transform: translateX(0.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
.no-forwards-notice {
|
.no-forwards-notice {
|
||||||
min-width: 12rem;
|
min-width: 12rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,57 @@
|
|||||||
|
.root {
|
||||||
|
min-width: 3rem;
|
||||||
|
max-width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemsWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0.1875rem 0;
|
||||||
|
border-radius: var(--border-radius-default);
|
||||||
|
|
||||||
|
background: var(--color-background);
|
||||||
|
box-shadow: 0 0.25rem 0.5rem 0.125rem var(--color-default-shadow);
|
||||||
|
|
||||||
|
body:not(.no-menu-blur) & {
|
||||||
|
background: var(--color-background-compact-menu);
|
||||||
|
backdrop-filter: blur(25px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
margin-block: 0.0625rem;
|
||||||
|
margin-inline: 0.25rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-background-compact-menu-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--color-item-active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.neutralEmoji {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
80
src/components/middle/message/TranslationToneSelector.tsx
Normal file
80
src/components/middle/message/TranslationToneSelector.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { memo } from '../../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { ApiAiComposeStyle } from '../../../api/types';
|
||||||
|
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import CustomEmoji from '../../common/CustomEmoji';
|
||||||
|
import { getStyleTitle } from '../composer/AiMessageEditorModal/helpers';
|
||||||
|
|
||||||
|
import styles from './TranslationToneSelector.module.scss';
|
||||||
|
|
||||||
|
const EMOJI_SIZE = 20;
|
||||||
|
const EMPTY_AI_COMPOSE_STYLES: ApiAiComposeStyle[] = [];
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
selectedTone?: string;
|
||||||
|
style?: string;
|
||||||
|
onSelectTone: (tone?: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
aiComposeStyles: ApiAiComposeStyle[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const TranslationToneSelector = ({
|
||||||
|
selectedTone,
|
||||||
|
style,
|
||||||
|
aiComposeStyles,
|
||||||
|
onSelectTone,
|
||||||
|
}: OwnProps & StateProps) => {
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const handleToneClick = useLastCallback((tone?: string) => {
|
||||||
|
onSelectTone(tone);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!aiComposeStyles.length) return undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.root, 'TranslationToneSelector')} style={style}>
|
||||||
|
<div className={styles.itemsWrapper}>
|
||||||
|
<div
|
||||||
|
className={buildClassName(styles.item, !selectedTone && styles.selected)}
|
||||||
|
onClick={() => handleToneClick(undefined)}
|
||||||
|
>
|
||||||
|
<span className={styles.neutralEmoji}>🏳️</span>
|
||||||
|
<span className={styles.title}>{lang('TranslationToneNeutral')}</span>
|
||||||
|
</div>
|
||||||
|
{aiComposeStyles.map(({ tone, documentId, title }) => (
|
||||||
|
<div
|
||||||
|
key={tone}
|
||||||
|
className={buildClassName(styles.item, selectedTone === tone && styles.selected)}
|
||||||
|
onClick={() => handleToneClick(tone)}
|
||||||
|
>
|
||||||
|
{documentId && (
|
||||||
|
<CustomEmoji
|
||||||
|
documentId={documentId}
|
||||||
|
size={EMOJI_SIZE}
|
||||||
|
shouldNotLoop
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className={styles.title}>{getStyleTitle(lang, tone, title)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): Complete<StateProps> => {
|
||||||
|
return {
|
||||||
|
aiComposeStyles: global.appConfig.aiComposeStyles || EMPTY_AI_COMPOSE_STYLES,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(TranslationToneSelector));
|
||||||
@ -1060,6 +1060,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-entity-diff-insert,
|
||||||
|
.text-entity-diff-replace-new {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-entity-diff-replace-new {
|
||||||
|
text-decoration: underline wavy;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-entity-diff-replace-old {
|
||||||
|
color: var(--color-error);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-entity-diff-delete {
|
||||||
|
color: var(--color-error);
|
||||||
|
text-decoration: line-through;
|
||||||
|
text-decoration-color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
// Keep this close to `CodeBlock` style to avoid jumps in height
|
// Keep this close to `CodeBlock` style to avoid jumps in height
|
||||||
.text-entity-pre {
|
.text-entity-pre {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { pick } from '../../util/iteratees';
|
|||||||
|
|
||||||
import VerificationMonetizationModal from '../common/VerificationMonetizationModal.async';
|
import VerificationMonetizationModal from '../common/VerificationMonetizationModal.async';
|
||||||
import WebAppsCloseConfirmationModal from '../main/WebAppsCloseConfirmationModal.async';
|
import WebAppsCloseConfirmationModal from '../main/WebAppsCloseConfirmationModal.async';
|
||||||
|
import AiMessageEditorModal from '../middle/composer/AiMessageEditorModal/AiMessageEditorModal.async';
|
||||||
import AboutAdsModal from './aboutAds/AboutAdsModal.async';
|
import AboutAdsModal from './aboutAds/AboutAdsModal.async';
|
||||||
import AgeVerificationModal from './ageVerification/AgeVerificationModal.async';
|
import AgeVerificationModal from './ageVerification/AgeVerificationModal.async';
|
||||||
import AttachBotInstallModal from './attachBotInstall/AttachBotInstallModal.async';
|
import AttachBotInstallModal from './attachBotInstall/AttachBotInstallModal.async';
|
||||||
@ -80,6 +81,7 @@ import WebAppModal from './webApp/WebAppModal.async';
|
|||||||
|
|
||||||
// `Pick` used only to provide tab completion
|
// `Pick` used only to provide tab completion
|
||||||
type ModalKey = keyof Pick<TabState,
|
type ModalKey = keyof Pick<TabState,
|
||||||
|
'aiMessageEditorModal' |
|
||||||
'giftCodeModal' |
|
'giftCodeModal' |
|
||||||
'boostModal' |
|
'boostModal' |
|
||||||
'chatlistModal' |
|
'chatlistModal' |
|
||||||
@ -166,6 +168,7 @@ type Entries<T> = {
|
|||||||
}[keyof T][];
|
}[keyof T][];
|
||||||
|
|
||||||
const MODALS: ModalRegistry = {
|
const MODALS: ModalRegistry = {
|
||||||
|
aiMessageEditorModal: AiMessageEditorModal,
|
||||||
giftCodeModal: GiftCodeModal,
|
giftCodeModal: GiftCodeModal,
|
||||||
boostModal: BoostModal,
|
boostModal: BoostModal,
|
||||||
chatlistModal: ChatlistModal,
|
chatlistModal: ChatlistModal,
|
||||||
|
|||||||
@ -195,6 +195,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.translucent-primary,
|
||||||
&.translucent {
|
&.translucent {
|
||||||
--ripple-color: var(--color-interactive-element-hover);
|
--ripple-color: var(--color-interactive-element-hover);
|
||||||
|
|
||||||
@ -245,6 +246,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.translucent-primary {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
&.translucent-bordered {
|
&.translucent-bordered {
|
||||||
--ripple-color: rgba(0, 0, 0, 0.08);
|
--ripple-color: rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,8 @@ export type OwnProps = {
|
|||||||
size?: 'default' | 'smaller' | 'tiny';
|
size?: 'default' | 'smaller' | 'tiny';
|
||||||
color?: (
|
color?: (
|
||||||
'primary' | 'secondary' | 'gray' | 'danger' | 'translucent' | 'translucent-white' | 'translucent-black'
|
'primary' | 'secondary' | 'gray' | 'danger' | 'translucent' | 'translucent-white' | 'translucent-black'
|
||||||
| 'translucent-bordered' | 'dark' | 'green' | 'adaptive' | 'stars' | 'bluredStarsBadge' | 'transparentBlured'
|
| 'translucent-bordered' | 'translucent-primary' | 'dark' | 'green' | 'adaptive' | 'stars' | 'bluredStarsBadge'
|
||||||
|
| 'transparentBlured'
|
||||||
);
|
);
|
||||||
backgroundImage?: string;
|
backgroundImage?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|||||||
@ -55,7 +55,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ type OwnProps = {
|
|||||||
positionY?: 'top' | 'bottom';
|
positionY?: 'top' | 'bottom';
|
||||||
footer?: string;
|
footer?: string;
|
||||||
forceOpen?: boolean;
|
forceOpen?: boolean;
|
||||||
|
withPortal?: boolean;
|
||||||
onOpen?: NoneToVoidFunction;
|
onOpen?: NoneToVoidFunction;
|
||||||
onClose?: NoneToVoidFunction;
|
onClose?: NoneToVoidFunction;
|
||||||
onHide?: NoneToVoidFunction;
|
onHide?: NoneToVoidFunction;
|
||||||
@ -39,6 +40,7 @@ const DropdownMenu: FC<OwnProps> = ({
|
|||||||
positionY = 'top',
|
positionY = 'top',
|
||||||
footer,
|
footer,
|
||||||
forceOpen,
|
forceOpen,
|
||||||
|
withPortal,
|
||||||
onOpen,
|
onOpen,
|
||||||
onClose,
|
onClose,
|
||||||
onTransitionEnd,
|
onTransitionEnd,
|
||||||
@ -114,6 +116,7 @@ const DropdownMenu: FC<OwnProps> = ({
|
|||||||
positionY={positionY}
|
positionY={positionY}
|
||||||
footer={footer}
|
footer={footer}
|
||||||
autoClose={autoClose}
|
autoClose={autoClose}
|
||||||
|
withPortal={withPortal}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
onCloseAnimationEnd={onHide}
|
onCloseAnimationEnd={onHide}
|
||||||
onMouseEnterBackdrop={onMouseEnterBackdrop}
|
onMouseEnterBackdrop={onMouseEnterBackdrop}
|
||||||
|
|||||||
54
src/components/ui/ExpandableText.module.scss
Normal file
54
src/components/ui/ExpandableText.module.scss
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
.root {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--color-text);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||||
|
transition: height 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moreLinkWrapper {
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.truncated .moreLinkWrapper {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0.25rem;
|
||||||
|
|
||||||
|
padding-inline-start: 1.5rem;
|
||||||
|
|
||||||
|
background: linear-gradient(to right, transparent, var(--color-background) 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.moreLink {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding-inline: 0.5rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: var(--color-primary);
|
||||||
|
|
||||||
|
transition: background-color 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primary-opacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/components/ui/ExpandableText.tsx
Normal file
79
src/components/ui/ExpandableText.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { memo, useEffect, useRef, useState } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import { REM } from '../common/helpers/mediaDimensions';
|
||||||
|
|
||||||
|
import useFlag from '../../hooks/useFlag';
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
|
import useResizeObserver from '../../hooks/useResizeObserver';
|
||||||
|
|
||||||
|
import styles from './ExpandableText.module.scss';
|
||||||
|
|
||||||
|
const DEFAULT_COLLAPSED_HEIGHT = 4.1875 * REM;
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
text?: string;
|
||||||
|
collapsedHeight?: number;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExpandableText = ({ text, collapsedHeight = DEFAULT_COLLAPSED_HEIGHT, className }: OwnProps) => {
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const [isExpanded, expand, collapse] = useFlag(false);
|
||||||
|
const [expandedHeight, setExpandedHeight] = useState<number | undefined>();
|
||||||
|
const [shouldTruncate, setShouldTruncate] = useState(false);
|
||||||
|
|
||||||
|
const contentRef = useRef<HTMLSpanElement>();
|
||||||
|
|
||||||
|
const displayText = text || '';
|
||||||
|
const canExpand = shouldTruncate;
|
||||||
|
|
||||||
|
const measureTruncation = useLastCallback(() => {
|
||||||
|
requestMeasure(() => {
|
||||||
|
if (!contentRef.current) return;
|
||||||
|
setShouldTruncate(contentRef.current.scrollHeight > collapsedHeight);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
measureTruncation();
|
||||||
|
}, [displayText, collapsedHeight, measureTruncation]);
|
||||||
|
|
||||||
|
useResizeObserver(contentRef, measureTruncation);
|
||||||
|
|
||||||
|
const handleToggleExpand = useLastCallback(() => {
|
||||||
|
if (isExpanded) {
|
||||||
|
collapse();
|
||||||
|
} else {
|
||||||
|
if (contentRef.current) {
|
||||||
|
setExpandedHeight(contentRef.current.scrollHeight);
|
||||||
|
}
|
||||||
|
expand();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.root, !isExpanded && canExpand && styles.truncated, className)}>
|
||||||
|
<div
|
||||||
|
className={styles.inner}
|
||||||
|
style={isExpanded && expandedHeight
|
||||||
|
? `height: ${expandedHeight}px`
|
||||||
|
: `height: ${collapsedHeight / REM}rem`}
|
||||||
|
>
|
||||||
|
<span ref={contentRef} className={styles.content}>{displayText}</span>
|
||||||
|
</div>
|
||||||
|
{canExpand && (
|
||||||
|
<span className={styles.moreLinkWrapper}>
|
||||||
|
<span className={styles.moreLink} onClick={handleToggleExpand}>
|
||||||
|
{lang(isExpanded ? 'TextShowLess' : 'TextShowMore')}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ExpandableText);
|
||||||
@ -2,6 +2,7 @@ import type { ElementRef, TeactNode } from '../../lib/teact/teact';
|
|||||||
import { memo, useEffect, useRef } from '../../lib/teact/teact';
|
import { memo, useEffect, useRef } from '../../lib/teact/teact';
|
||||||
|
|
||||||
import type { ApiMessageEntityCustomEmoji } from '../../api/types';
|
import type { ApiMessageEntityCustomEmoji } from '../../api/types';
|
||||||
|
import type { IconName } from '../../types/icons';
|
||||||
import type { MenuItemContextAction } from './ListItem';
|
import type { MenuItemContextAction } from './ListItem';
|
||||||
|
|
||||||
import animateHorizontalScroll from '../../util/animateHorizontalScroll';
|
import animateHorizontalScroll from '../../util/animateHorizontalScroll';
|
||||||
@ -19,6 +20,7 @@ import './SquareTabList.scss';
|
|||||||
export type TabWithProperties = {
|
export type TabWithProperties = {
|
||||||
id?: number;
|
id?: number;
|
||||||
title: TeactNode;
|
title: TeactNode;
|
||||||
|
icon?: IconName;
|
||||||
badgeCount?: number;
|
badgeCount?: number;
|
||||||
isBlocked?: boolean;
|
isBlocked?: boolean;
|
||||||
isBadgeActive?: boolean;
|
isBadgeActive?: boolean;
|
||||||
|
|||||||
@ -11,6 +11,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
--tab-radius: 1.25rem;
|
||||||
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|
||||||
@ -33,6 +35,16 @@
|
|||||||
&.ready {
|
&.ready {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
--tab-radius: 1.75rem;
|
||||||
|
|
||||||
|
border-radius: 1.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.centered {
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.activeIndicator {
|
.activeIndicator {
|
||||||
@ -54,6 +66,10 @@
|
|||||||
background-color: var(--color-primary-opacity);
|
background-color: var(--color-primary-opacity);
|
||||||
|
|
||||||
transition: clip-path var(--slide-transition);
|
transition: clip-path var(--slide-transition);
|
||||||
|
|
||||||
|
&.stretched {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
@ -65,7 +81,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
padding: 0.375rem 1rem;
|
padding: 0.375rem 1rem;
|
||||||
border-radius: 1.25rem;
|
border-radius: var(--tab-radius);
|
||||||
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
@ -79,6 +95,23 @@
|
|||||||
.activeIndicator & {
|
.activeIndicator & {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stretched {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabEmoji,
|
||||||
|
.tabIcon {
|
||||||
|
--custom-emoji-size: 1.25rem;
|
||||||
|
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lockIcon {
|
.lockIcon {
|
||||||
|
|||||||
@ -10,14 +10,22 @@ import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
|||||||
import useLastCallback from '../../hooks/useLastCallback';
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
import useResizeObserver from '../../hooks/useResizeObserver';
|
import useResizeObserver from '../../hooks/useResizeObserver';
|
||||||
|
|
||||||
|
import CustomEmoji from '../common/CustomEmoji';
|
||||||
import Icon from '../common/icons/Icon';
|
import Icon from '../common/icons/Icon';
|
||||||
|
|
||||||
import styles from './TabList.module.scss';
|
import styles from './TabList.module.scss';
|
||||||
|
|
||||||
|
const EMOJI_SIZE = 20;
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
tabs: readonly TabWithProperties[];
|
tabs: readonly TabWithProperties[];
|
||||||
activeTab: number;
|
activeTab: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
tabClassName?: string;
|
||||||
|
indicatorClassName?: string;
|
||||||
|
centered?: boolean;
|
||||||
|
stretched?: boolean;
|
||||||
|
itemAlignment?: 'vertical' | 'horizontal';
|
||||||
onSwitchTab: (index: number) => void;
|
onSwitchTab: (index: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,6 +33,11 @@ const TabList = ({
|
|||||||
tabs,
|
tabs,
|
||||||
activeTab,
|
activeTab,
|
||||||
className,
|
className,
|
||||||
|
tabClassName,
|
||||||
|
indicatorClassName,
|
||||||
|
centered,
|
||||||
|
stretched,
|
||||||
|
itemAlignment,
|
||||||
onSwitchTab,
|
onSwitchTab,
|
||||||
}: OwnProps) => {
|
}: OwnProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement>();
|
const containerRef = useRef<HTMLDivElement>();
|
||||||
@ -43,7 +56,9 @@ const TabList = ({
|
|||||||
const left = (offsetLeft / containerWidth * 100).toFixed(1);
|
const left = (offsetLeft / containerWidth * 100).toFixed(1);
|
||||||
const right = ((containerWidth - (offsetLeft + offsetWidth)) / containerWidth * 100).toFixed(1);
|
const right = ((containerWidth - (offsetLeft + offsetWidth)) / containerWidth * 100).toFixed(1);
|
||||||
|
|
||||||
setClipPath(`inset(0.25rem ${right}% 0.25rem ${left}% round 1.25rem)`);
|
setClipPath(`inset(0.25rem ${right}% 0.25rem ${left}% round var(--tab-radius))`);
|
||||||
|
} else if (activeTab < 0) {
|
||||||
|
setClipPath('inset(0 100% 0 100%)');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -59,27 +74,56 @@ const TabList = ({
|
|||||||
|
|
||||||
if (!tabs.length) return undefined;
|
if (!tabs.length) return undefined;
|
||||||
|
|
||||||
const renderTab = (tab: TabWithProperties, index: number) => (
|
const renderTab = (tab: TabWithProperties, index: number) => {
|
||||||
<div
|
const stringEmoticon = typeof tab.emoticon === 'string' ? tab.emoticon : undefined;
|
||||||
key={tab.id ?? index}
|
const customEmoji = typeof tab.emoticon === 'object' ? tab.emoticon : undefined;
|
||||||
className={styles.tab}
|
|
||||||
onClick={() => handleTabClick(index)}
|
return (
|
||||||
>
|
<div
|
||||||
{tab.title}
|
key={tab.id ?? index}
|
||||||
{tab.isBlocked && <Icon name="lock-badge" className={styles.lockIcon} />}
|
className={buildClassName(
|
||||||
</div>
|
styles.tab,
|
||||||
);
|
tabClassName,
|
||||||
|
itemAlignment === 'vertical' && styles.vertical,
|
||||||
|
stretched && styles.stretched,
|
||||||
|
)}
|
||||||
|
onClick={() => handleTabClick(index)}
|
||||||
|
>
|
||||||
|
{stringEmoticon && <span className={styles.tabEmoji}>{stringEmoticon}</span>}
|
||||||
|
{customEmoji && (
|
||||||
|
<CustomEmoji
|
||||||
|
documentId={customEmoji.documentId}
|
||||||
|
className={styles.tabEmoji}
|
||||||
|
size={EMOJI_SIZE}
|
||||||
|
shouldNotLoop
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tab.icon && <Icon name={tab.icon} className={styles.tabIcon} />}
|
||||||
|
{tab.title}
|
||||||
|
{tab.isBlocked && <Icon name="lock-badge" className={styles.lockIcon} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={buildClassName(styles.container, className, clipPath && styles.ready)}
|
className={buildClassName(
|
||||||
|
styles.container,
|
||||||
|
centered && styles.centered,
|
||||||
|
itemAlignment === 'vertical' && styles.vertical,
|
||||||
|
className,
|
||||||
|
clipPath && styles.ready,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{tabs.map(renderTab)}
|
{tabs.map(renderTab)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={clipPathContainerRef}
|
ref={clipPathContainerRef}
|
||||||
className={styles.activeIndicator}
|
className={buildClassName(styles.activeIndicator,
|
||||||
|
centered && styles.centered,
|
||||||
|
stretched && styles.stretched,
|
||||||
|
indicatorClassName)}
|
||||||
style={clipPath ? `clip-path: ${clipPath}` : undefined}
|
style={clipPath ? `clip-path: ${clipPath}` : undefined}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
|
|||||||
@ -74,6 +74,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.fast-wave::before {
|
||||||
|
content: "";
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
background: linear-gradient(90deg, transparent 0%, var(--color-skeleton-foreground) 50%, transparent 100%);
|
||||||
|
|
||||||
|
animation: skeleton-fast-wave 1.5s ease-in-out infinite;
|
||||||
|
|
||||||
|
@keyframes skeleton-fast-wave {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:global(body.in-background) &::before {
|
:global(body.in-background) &::before {
|
||||||
animation-play-state: paused;
|
animation-play-state: paused;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,12 +8,13 @@ import './Skeleton.scss';
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
variant?: 'rectangular' | 'rounded-rect' | 'round';
|
variant?: 'rectangular' | 'rounded-rect' | 'round';
|
||||||
animation?: 'wave' | 'pulse' | 'none';
|
animation?: 'wave' | 'fast-wave' | 'pulse' | 'none';
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
forceAspectRatio?: boolean;
|
forceAspectRatio?: boolean;
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
style?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Skeleton: FC<OwnProps> = ({
|
const Skeleton: FC<OwnProps> = ({
|
||||||
@ -24,16 +25,18 @@ const Skeleton: FC<OwnProps> = ({
|
|||||||
forceAspectRatio,
|
forceAspectRatio,
|
||||||
inline,
|
inline,
|
||||||
className,
|
className,
|
||||||
|
style,
|
||||||
}) => {
|
}) => {
|
||||||
const classNames = buildClassName('Skeleton', variant, animation, className, inline && 'inline');
|
const classNames = buildClassName('Skeleton', variant, animation, className, inline && 'inline');
|
||||||
const aspectRatio = (width && height) ? `aspect-ratio: ${width}/${height}` : undefined;
|
const aspectRatio = (width && height) ? `aspect-ratio: ${width}/${height}` : undefined;
|
||||||
const style = buildStyle(
|
const computedStyle = buildStyle(
|
||||||
|
style,
|
||||||
forceAspectRatio && aspectRatio,
|
forceAspectRatio && aspectRatio,
|
||||||
Boolean(width) && `width: ${width}px`,
|
Boolean(width) && `width: ${width}px`,
|
||||||
!forceAspectRatio && Boolean(height) && `height: ${height}px`,
|
!forceAspectRatio && Boolean(height) && `height: ${height}px`,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className={classNames} style={style}>{inline && '\u00A0'}</div>
|
<div className={classNames} style={computedStyle}>{inline && '\u00A0'}</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
height: 0.5rem;
|
||||||
|
border-radius: 0.125rem;
|
||||||
|
}
|
||||||
45
src/components/ui/placeholder/TextLoadingPlaceholder.tsx
Normal file
45
src/components/ui/placeholder/TextLoadingPlaceholder.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { memo, useMemo } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
|
import Skeleton from './Skeleton';
|
||||||
|
|
||||||
|
import styles from './TextLoadingPlaceholder.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
lines?: number;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WIDTH_PATTERNS = [
|
||||||
|
[85, 90, 87, 83, 87, 70],
|
||||||
|
[81, 75, 83, 87, 81, 93],
|
||||||
|
[73, 87, 91, 75, 87, 91],
|
||||||
|
];
|
||||||
|
|
||||||
|
const TextLoadingPlaceholder = ({
|
||||||
|
lines = 6,
|
||||||
|
className,
|
||||||
|
}: OwnProps) => {
|
||||||
|
const lineWidths = useMemo(() => {
|
||||||
|
const patternIndex = Math.floor(Math.random() * WIDTH_PATTERNS.length);
|
||||||
|
const pattern = WIDTH_PATTERNS[patternIndex];
|
||||||
|
return Array.from({ length: lines }, (_, i) => pattern[i % pattern.length]);
|
||||||
|
}, [lines]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.root, className)}>
|
||||||
|
{lineWidths.map((width, i) => (
|
||||||
|
<Skeleton
|
||||||
|
key={i}
|
||||||
|
variant="rounded-rect"
|
||||||
|
animation="fast-wave"
|
||||||
|
className={styles.line}
|
||||||
|
style={`width: ${width}%`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(TextLoadingPlaceholder);
|
||||||
@ -416,6 +416,7 @@ export const PREMIUM_FEATURE_SECTIONS = [
|
|||||||
'last_seen',
|
'last_seen',
|
||||||
'message_privacy',
|
'message_privacy',
|
||||||
'effects',
|
'effects',
|
||||||
|
'ai_compose',
|
||||||
'todo',
|
'todo',
|
||||||
'pm_noforwards',
|
'pm_noforwards',
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import './api/middleSearch';
|
|||||||
import './api/management';
|
import './api/management';
|
||||||
import './api/sync';
|
import './api/sync';
|
||||||
import './api/accounts';
|
import './api/accounts';
|
||||||
|
import './api/ai';
|
||||||
import './api/users';
|
import './api/users';
|
||||||
import './api/bots';
|
import './api/bots';
|
||||||
import './api/settings';
|
import './api/settings';
|
||||||
@ -24,6 +25,7 @@ import './ui/globalSearch';
|
|||||||
import './ui/middleSearch';
|
import './ui/middleSearch';
|
||||||
import './ui/stickerSearch';
|
import './ui/stickerSearch';
|
||||||
import './ui/account';
|
import './ui/account';
|
||||||
|
import './ui/aiMessageEditor';
|
||||||
import './ui/users';
|
import './ui/users';
|
||||||
import './ui/settings';
|
import './ui/settings';
|
||||||
import './ui/misc';
|
import './ui/misc';
|
||||||
|
|||||||
144
src/global/actions/api/ai.ts
Normal file
144
src/global/actions/api/ai.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
|
import { callApi } from '../../../api/gramjs';
|
||||||
|
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||||
|
import { updateTabState } from '../../reducers/tabs';
|
||||||
|
import { selectTabState } from '../../selectors';
|
||||||
|
|
||||||
|
function buildStyleCacheKey(tone: string | undefined, emojify: boolean | undefined) {
|
||||||
|
return `${tone || ''}_${emojify ? '1' : '0'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTranslateCacheKey(lang: string | undefined, tone: string | undefined, emojify: boolean | undefined) {
|
||||||
|
return `${lang || ''}_${tone || ''}_${emojify ? '1' : '0'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
addActionHandler('composeWithAiMessageEditor', async (global, actions, payload): Promise<void> => {
|
||||||
|
const {
|
||||||
|
shouldProofread, isEmojify, translateToLang, changeTone,
|
||||||
|
tabId = getCurrentTabId(),
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
let modal = selectTabState(global, tabId).aiMessageEditorModal;
|
||||||
|
if (!modal) return;
|
||||||
|
|
||||||
|
let cachedResult;
|
||||||
|
let tabKey: 'translateTab' | 'styleTab' | 'fixTab';
|
||||||
|
|
||||||
|
if (shouldProofread) {
|
||||||
|
tabKey = 'fixTab';
|
||||||
|
cachedResult = modal.fixTab?.cache;
|
||||||
|
} else if (translateToLang) {
|
||||||
|
tabKey = 'translateTab';
|
||||||
|
const cacheKey = buildTranslateCacheKey(translateToLang, changeTone, isEmojify);
|
||||||
|
cachedResult = modal.translateTab?.cache?.[cacheKey];
|
||||||
|
} else {
|
||||||
|
tabKey = 'styleTab';
|
||||||
|
const cacheKey = buildStyleCacheKey(changeTone, isEmojify);
|
||||||
|
cachedResult = modal.styleTab?.cache?.[cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedResult) {
|
||||||
|
global = getGlobal();
|
||||||
|
modal = selectTabState(global, tabId).aiMessageEditorModal;
|
||||||
|
if (!modal) return;
|
||||||
|
global = updateTabState(global, {
|
||||||
|
aiMessageEditorModal: {
|
||||||
|
...modal,
|
||||||
|
[tabKey]: { ...modal[tabKey], result: cachedResult, error: undefined, isLoading: false },
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
setGlobal(global);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
modal = selectTabState(global, tabId).aiMessageEditorModal;
|
||||||
|
if (!modal) return;
|
||||||
|
global = updateTabState(global, {
|
||||||
|
aiMessageEditorModal: {
|
||||||
|
...modal,
|
||||||
|
[tabKey]: { ...modal[tabKey], isLoading: true },
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
setGlobal(global);
|
||||||
|
|
||||||
|
const response = await callApi('composeMessageWithAI', {
|
||||||
|
text: modal.text,
|
||||||
|
shouldProofread,
|
||||||
|
isEmojify,
|
||||||
|
translateToLang,
|
||||||
|
changeTone,
|
||||||
|
});
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
modal = selectTabState(global, tabId).aiMessageEditorModal;
|
||||||
|
if (!modal) return;
|
||||||
|
|
||||||
|
if (response?.error) {
|
||||||
|
global = updateTabState(global, {
|
||||||
|
aiMessageEditorModal: {
|
||||||
|
...modal,
|
||||||
|
[tabKey]: { ...modal[tabKey], result: undefined, isLoading: false, error: response.error },
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
setGlobal(global);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = response?.result;
|
||||||
|
const currentTabState = modal[tabKey] || {};
|
||||||
|
|
||||||
|
let isOutdatedResult = false;
|
||||||
|
if (translateToLang) {
|
||||||
|
const { selectedLanguage, selectedTone, shouldEmojify } = modal.translateTab || {};
|
||||||
|
isOutdatedResult = selectedLanguage !== translateToLang
|
||||||
|
|| selectedTone !== changeTone
|
||||||
|
|| Boolean(shouldEmojify) !== Boolean(isEmojify);
|
||||||
|
} else if (!shouldProofread) {
|
||||||
|
const { selectedTone, shouldEmojify } = modal.styleTab || {};
|
||||||
|
isOutdatedResult = selectedTone !== changeTone
|
||||||
|
|| Boolean(shouldEmojify) !== Boolean(isEmojify);
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatedCache;
|
||||||
|
if (result) {
|
||||||
|
if (shouldProofread) {
|
||||||
|
updatedCache = result;
|
||||||
|
} else if (translateToLang) {
|
||||||
|
const cacheKey = buildTranslateCacheKey(translateToLang, changeTone, isEmojify);
|
||||||
|
updatedCache = { ...currentTabState.cache, [cacheKey]: result };
|
||||||
|
} else {
|
||||||
|
const cacheKey = buildStyleCacheKey(changeTone, isEmojify);
|
||||||
|
updatedCache = { ...currentTabState.cache, [cacheKey]: result };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOutdatedResult) {
|
||||||
|
global = updateTabState(global, {
|
||||||
|
aiMessageEditorModal: {
|
||||||
|
...modal,
|
||||||
|
[tabKey]: {
|
||||||
|
...currentTabState,
|
||||||
|
isLoading: false,
|
||||||
|
cache: updatedCache !== undefined ? updatedCache : currentTabState.cache,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
setGlobal(global);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = updateTabState(global, {
|
||||||
|
aiMessageEditorModal: {
|
||||||
|
...modal,
|
||||||
|
[tabKey]: {
|
||||||
|
...currentTabState,
|
||||||
|
isLoading: false,
|
||||||
|
result,
|
||||||
|
error: undefined,
|
||||||
|
cache: updatedCache !== undefined ? updatedCache : currentTabState.cache,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
189
src/global/actions/ui/aiMessageEditor.ts
Normal file
189
src/global/actions/ui/aiMessageEditor.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import type { ActionReturnType } from '../../types';
|
||||||
|
|
||||||
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
|
import { addActionHandler, getActions } from '../../index';
|
||||||
|
import { updateTabState } from '../../reducers/tabs';
|
||||||
|
import { selectTabState } from '../../selectors';
|
||||||
|
import { selectCurrentMessageList } from '../../selectors/messages';
|
||||||
|
import { selectTranslationLanguage } from '../../selectors/settings';
|
||||||
|
|
||||||
|
addActionHandler('openAiMessageEditorModal', (global, actions, payload): ActionReturnType => {
|
||||||
|
const {
|
||||||
|
chatId, text, initialTab = 'style', isFromAttachment,
|
||||||
|
tabId = getCurrentTabId(),
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
const defaultTranslationLanguage = selectTranslationLanguage(global);
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
aiMessageEditorModal: {
|
||||||
|
chatId,
|
||||||
|
text,
|
||||||
|
activeTab: initialTab,
|
||||||
|
isFromAttachment,
|
||||||
|
translateTab: {
|
||||||
|
selectedLanguage: defaultTranslationLanguage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('closeAiMessageEditorModal', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
aiMessageEditorModal: undefined,
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('setAiMessageEditorTab', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tab, tabId = getCurrentTabId() } = payload;
|
||||||
|
|
||||||
|
const aiMessageEditorModal = selectTabState(global, tabId).aiMessageEditorModal;
|
||||||
|
if (!aiMessageEditorModal) return undefined;
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
aiMessageEditorModal: {
|
||||||
|
...aiMessageEditorModal,
|
||||||
|
activeTab: tab,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('setAiMessageEditorTranslateOptions', (global, actions, payload): ActionReturnType => {
|
||||||
|
const {
|
||||||
|
selectedLanguage, shouldEmojify, clearResult,
|
||||||
|
tabId = getCurrentTabId(),
|
||||||
|
} = payload;
|
||||||
|
const hasSelectedTone = 'selectedTone' in payload;
|
||||||
|
|
||||||
|
const aiMessageEditorModal = selectTabState(global, tabId).aiMessageEditorModal;
|
||||||
|
if (!aiMessageEditorModal) return undefined;
|
||||||
|
|
||||||
|
const translateTab = aiMessageEditorModal.translateTab || {};
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
aiMessageEditorModal: {
|
||||||
|
...aiMessageEditorModal,
|
||||||
|
translateTab: {
|
||||||
|
...translateTab,
|
||||||
|
selectedLanguage: selectedLanguage !== undefined ? selectedLanguage : translateTab.selectedLanguage,
|
||||||
|
selectedTone: hasSelectedTone ? payload.selectedTone : translateTab.selectedTone,
|
||||||
|
shouldEmojify: shouldEmojify !== undefined ? shouldEmojify : translateTab.shouldEmojify,
|
||||||
|
result: clearResult ? undefined : translateTab.result,
|
||||||
|
error: clearResult ? undefined : translateTab.error,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('setAiMessageEditorStyleOptions', (global, actions, payload): ActionReturnType => {
|
||||||
|
const {
|
||||||
|
shouldEmojify, clearResult,
|
||||||
|
tabId = getCurrentTabId(),
|
||||||
|
} = payload;
|
||||||
|
const hasSelectedTone = 'selectedTone' in payload;
|
||||||
|
|
||||||
|
const aiMessageEditorModal = selectTabState(global, tabId).aiMessageEditorModal;
|
||||||
|
if (!aiMessageEditorModal) return undefined;
|
||||||
|
|
||||||
|
const styleTab = aiMessageEditorModal.styleTab || {};
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
aiMessageEditorModal: {
|
||||||
|
...aiMessageEditorModal,
|
||||||
|
styleTab: {
|
||||||
|
...styleTab,
|
||||||
|
selectedTone: hasSelectedTone ? payload.selectedTone : styleTab.selectedTone,
|
||||||
|
shouldEmojify: shouldEmojify !== undefined ? shouldEmojify : styleTab.shouldEmojify,
|
||||||
|
result: clearResult ? undefined : styleTab.result,
|
||||||
|
error: clearResult ? undefined : styleTab.error,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('applyAiMessageEditorResult', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
|
||||||
|
const aiMessageEditorModal = selectTabState(global, tabId).aiMessageEditorModal;
|
||||||
|
if (!aiMessageEditorModal) return undefined;
|
||||||
|
|
||||||
|
const { activeTab } = aiMessageEditorModal;
|
||||||
|
const tabState = activeTab === 'translate' ? aiMessageEditorModal.translateTab
|
||||||
|
: activeTab === 'style' ? aiMessageEditorModal.styleTab : aiMessageEditorModal.fixTab;
|
||||||
|
|
||||||
|
const textToApply = tabState?.result?.resultText || aiMessageEditorModal.text;
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
aiMessageEditorModal: undefined,
|
||||||
|
aiMessageEditorPendingResult: {
|
||||||
|
text: textToApply,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('sendAiMessageEditorResult', (global, actions, payload): ActionReturnType => {
|
||||||
|
const {
|
||||||
|
isSilent, scheduledAt, scheduleRepeatPeriod,
|
||||||
|
tabId = getCurrentTabId(),
|
||||||
|
} = payload || {};
|
||||||
|
|
||||||
|
const aiMessageEditorModal = selectTabState(global, tabId).aiMessageEditorModal;
|
||||||
|
if (!aiMessageEditorModal) return undefined;
|
||||||
|
|
||||||
|
const { activeTab, isFromAttachment } = aiMessageEditorModal;
|
||||||
|
const tabState = activeTab === 'translate' ? aiMessageEditorModal.translateTab
|
||||||
|
: activeTab === 'style' ? aiMessageEditorModal.styleTab : aiMessageEditorModal.fixTab;
|
||||||
|
|
||||||
|
const textToSend = tabState?.result?.resultText || aiMessageEditorModal.text;
|
||||||
|
|
||||||
|
if (isFromAttachment) {
|
||||||
|
return updateTabState(global, {
|
||||||
|
aiMessageEditorModal: undefined,
|
||||||
|
aiMessageEditorPendingResult: {
|
||||||
|
text: textToSend,
|
||||||
|
shouldSendWithAttachments: true,
|
||||||
|
isSilent,
|
||||||
|
scheduledAt,
|
||||||
|
scheduleRepeatPeriod,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
||||||
|
if (!currentMessageList) return undefined;
|
||||||
|
|
||||||
|
const { chatId, threadId } = currentMessageList;
|
||||||
|
|
||||||
|
const messageList = scheduledAt
|
||||||
|
? { ...currentMessageList, type: 'scheduled' as const }
|
||||||
|
: currentMessageList;
|
||||||
|
|
||||||
|
getActions().sendMessage({
|
||||||
|
messageList,
|
||||||
|
text: textToSend.text,
|
||||||
|
entities: textToSend.entities,
|
||||||
|
isSilent,
|
||||||
|
scheduledAt,
|
||||||
|
scheduleRepeatPeriod,
|
||||||
|
tabId,
|
||||||
|
});
|
||||||
|
|
||||||
|
getActions().clearDraft({ chatId, threadId, isLocalOnly: true });
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
aiMessageEditorModal: undefined,
|
||||||
|
aiMessageEditorPendingResult: {
|
||||||
|
shouldClear: true,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('clearAiMessageEditorPendingResult', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
aiMessageEditorPendingResult: undefined,
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
@ -2600,6 +2600,41 @@ export interface ActionPayloads {
|
|||||||
} & WithTabId) | undefined;
|
} & WithTabId) | undefined;
|
||||||
closePremiumModal: WithTabId | undefined;
|
closePremiumModal: WithTabId | undefined;
|
||||||
|
|
||||||
|
openAiMessageEditorModal: {
|
||||||
|
chatId: string;
|
||||||
|
text: ApiFormattedText;
|
||||||
|
initialTab?: 'translate' | 'style' | 'fix';
|
||||||
|
isFromAttachment?: boolean;
|
||||||
|
} & WithTabId;
|
||||||
|
closeAiMessageEditorModal: WithTabId | undefined;
|
||||||
|
setAiMessageEditorTab: {
|
||||||
|
tab: 'translate' | 'style' | 'fix';
|
||||||
|
} & WithTabId;
|
||||||
|
setAiMessageEditorTranslateOptions: {
|
||||||
|
selectedLanguage?: string;
|
||||||
|
selectedTone?: string;
|
||||||
|
shouldEmojify?: boolean;
|
||||||
|
clearResult?: boolean;
|
||||||
|
} & WithTabId;
|
||||||
|
setAiMessageEditorStyleOptions: {
|
||||||
|
selectedTone?: string;
|
||||||
|
shouldEmojify?: boolean;
|
||||||
|
clearResult?: boolean;
|
||||||
|
} & WithTabId;
|
||||||
|
composeWithAiMessageEditor: {
|
||||||
|
shouldProofread?: boolean;
|
||||||
|
isEmojify?: boolean;
|
||||||
|
translateToLang?: string;
|
||||||
|
changeTone?: string;
|
||||||
|
} & WithTabId;
|
||||||
|
applyAiMessageEditorResult: WithTabId | undefined;
|
||||||
|
sendAiMessageEditorResult: ({
|
||||||
|
isSilent?: boolean;
|
||||||
|
scheduledAt?: number;
|
||||||
|
scheduleRepeatPeriod?: number;
|
||||||
|
} & WithTabId) | undefined;
|
||||||
|
clearAiMessageEditorPendingResult: WithTabId | undefined;
|
||||||
|
|
||||||
openGiveawayModal: ({
|
openGiveawayModal: ({
|
||||||
chatId: string;
|
chatId: string;
|
||||||
gifts?: number[] | undefined;
|
gifts?: number[] | undefined;
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import type {
|
|||||||
ApiChatType,
|
ApiChatType,
|
||||||
ApiCheckedGiftCode,
|
ApiCheckedGiftCode,
|
||||||
ApiCollectibleInfo,
|
ApiCollectibleInfo,
|
||||||
|
ApiComposedMessageWithAI,
|
||||||
ApiDialog,
|
ApiDialog,
|
||||||
ApiEmojiStatusCollectible,
|
ApiEmojiStatusCollectible,
|
||||||
ApiFormattedText,
|
ApiFormattedText,
|
||||||
@ -107,6 +108,12 @@ export type PollVote = {
|
|||||||
date: number;
|
date: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AiEditorTabBase = {
|
||||||
|
isLoading?: boolean;
|
||||||
|
result?: ApiComposedMessageWithAI;
|
||||||
|
error?: 'floodPremium' | 'aiError' | 'generic';
|
||||||
|
};
|
||||||
|
|
||||||
export type TabState = {
|
export type TabState = {
|
||||||
id: number;
|
id: number;
|
||||||
isBlurred?: boolean;
|
isBlurred?: boolean;
|
||||||
@ -662,6 +669,36 @@ export type TabState = {
|
|||||||
gift?: ApiStarGift;
|
gift?: ApiStarGift;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
aiMessageEditorModal?: {
|
||||||
|
chatId: string;
|
||||||
|
text: ApiFormattedText;
|
||||||
|
activeTab: 'translate' | 'style' | 'fix';
|
||||||
|
isFromAttachment?: boolean;
|
||||||
|
translateTab?: AiEditorTabBase & {
|
||||||
|
selectedLanguage?: string;
|
||||||
|
selectedTone?: string;
|
||||||
|
shouldEmojify?: boolean;
|
||||||
|
cache?: Record<string, ApiComposedMessageWithAI>;
|
||||||
|
};
|
||||||
|
styleTab?: AiEditorTabBase & {
|
||||||
|
selectedTone?: string;
|
||||||
|
shouldEmojify?: boolean;
|
||||||
|
cache?: Record<string, ApiComposedMessageWithAI>;
|
||||||
|
};
|
||||||
|
fixTab?: AiEditorTabBase & {
|
||||||
|
cache?: ApiComposedMessageWithAI;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
aiMessageEditorPendingResult?: {
|
||||||
|
text?: ApiFormattedText;
|
||||||
|
shouldClear?: boolean;
|
||||||
|
shouldSendWithAttachments?: boolean;
|
||||||
|
isSilent?: boolean;
|
||||||
|
scheduledAt?: number;
|
||||||
|
scheduleRepeatPeriod?: number;
|
||||||
|
};
|
||||||
|
|
||||||
giveawayModal?: {
|
giveawayModal?: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
|
|||||||
@ -33,6 +33,7 @@ type Params = {
|
|||||||
orderedFolderIds?: number[];
|
orderedFolderIds?: number[];
|
||||||
chatFoldersById: Record<number, ApiChatFolder>;
|
chatFoldersById: Record<number, ApiChatFolder>;
|
||||||
maxFolders: number;
|
maxFolders: number;
|
||||||
|
noEmoticons?: boolean;
|
||||||
} & ({
|
} & ({
|
||||||
isReadOnly?: false;
|
isReadOnly?: false;
|
||||||
maxChatLists: number;
|
maxChatLists: number;
|
||||||
@ -48,6 +49,7 @@ const useFolderTabs = (params: Params) => {
|
|||||||
orderedFolderIds,
|
orderedFolderIds,
|
||||||
chatFoldersById,
|
chatFoldersById,
|
||||||
maxFolders,
|
maxFolders,
|
||||||
|
noEmoticons,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
@ -236,13 +238,13 @@ const useFolderTabs = (params: Params) => {
|
|||||||
isBadgeActive: Boolean(folderCountersById[id]?.notificationsCount),
|
isBadgeActive: Boolean(folderCountersById[id]?.notificationsCount),
|
||||||
isBlocked,
|
isBlocked,
|
||||||
contextActions: contextActions.length ? contextActions : undefined,
|
contextActions: contextActions.length ? contextActions : undefined,
|
||||||
emoticon: folderIcon,
|
emoticon: noEmoticons ? undefined : folderIcon,
|
||||||
noTitleAnimations: folder.noTitleAnimations,
|
noTitleAnimations: folder.noTitleAnimations,
|
||||||
} satisfies TabWithProperties;
|
} satisfies TabWithProperties;
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
displayedFolders, maxFolders, folderCountersById, lang, chatFoldersById, maxChatLists, folderInvitesById,
|
displayedFolders, maxFolders, folderCountersById, lang, chatFoldersById, maxChatLists, folderInvitesById,
|
||||||
maxFolderInvites, folderUnreadChatsCountersById, isReadOnly, sidebarMode, isMobile,
|
maxFolderInvites, folderUnreadChatsCountersById, isReadOnly, sidebarMode, isMobile, noEmoticons,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1811,6 +1811,7 @@ messages.getFutureChatCreatorAfterLeave#3b7d0ea6 peer:InputPeer = User;
|
|||||||
messages.editChatParticipantRank#a00f32b0 peer:InputPeer participant:InputPeer rank:string = Updates;
|
messages.editChatParticipantRank#a00f32b0 peer:InputPeer participant:InputPeer rank:string = Updates;
|
||||||
messages.declineUrlAuth#35436bbc url:string = Bool;
|
messages.declineUrlAuth#35436bbc url:string = Bool;
|
||||||
messages.checkUrlAuthMatchCode#c9a47b0b url:string match_code:string = Bool;
|
messages.checkUrlAuthMatchCode#c9a47b0b url:string match_code:string = Bool;
|
||||||
|
messages.composeMessageWithAI#fd426afe flags:# proofread:flags.0?true emojify:flags.3?true text:TextWithEntities translate_to_lang:flags.1?string change_tone:flags.2?string = messages.ComposedMessageWithAI;
|
||||||
updates.getState#edd4882a = updates.State;
|
updates.getState#edd4882a = updates.State;
|
||||||
updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference;
|
updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference;
|
||||||
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
|
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
|
||||||
|
|||||||
@ -204,6 +204,7 @@
|
|||||||
"messages.getAvailableEffects",
|
"messages.getAvailableEffects",
|
||||||
"messages.setDefaultReaction",
|
"messages.setDefaultReaction",
|
||||||
"messages.translateText",
|
"messages.translateText",
|
||||||
|
"messages.composeMessageWithAI",
|
||||||
"messages.getAttachMenuBots",
|
"messages.getAttachMenuBots",
|
||||||
"messages.getAttachMenuBot",
|
"messages.getAttachMenuBot",
|
||||||
"messages.toggleBotInAttachMenu",
|
"messages.toggleBotInAttachMenu",
|
||||||
|
|||||||
@ -164,4 +164,5 @@ export const DEFAULT_APP_CONFIG: ApiAppConfig = {
|
|||||||
passkeysMaxCount: 5,
|
passkeysMaxCount: 5,
|
||||||
diceEmojies: [],
|
diceEmojies: [],
|
||||||
diceEmojiesSuccess: {},
|
diceEmojiesSuccess: {},
|
||||||
|
aiComposeStyles: [],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: block;
|
font-display: block;
|
||||||
src: url("./icons.woff2?711696a367cfec0ddfb1ac8f13f32988") format("woff2"),
|
src: url("./icons.woff2?58724e9b0f0ff4defb438785e40e9668") format("woff2"),
|
||||||
url("./icons.woff?711696a367cfec0ddfb1ac8f13f32988") format("woff");
|
url("./icons.woff?58724e9b0f0ff4defb438785e40e9668") format("woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-char::before {
|
.icon-char::before {
|
||||||
@ -906,108 +906,117 @@ url("./icons.woff?711696a367cfec0ddfb1ac8f13f32988") format("woff");
|
|||||||
.icon-allow-share::before {
|
.icon-allow-share::before {
|
||||||
content: "\f227";
|
content: "\f227";
|
||||||
}
|
}
|
||||||
.icon-admin::before {
|
.icon-ai::before {
|
||||||
content: "\f228";
|
content: "\f228";
|
||||||
}
|
}
|
||||||
.icon-add::before {
|
.icon-ai-fix::before {
|
||||||
content: "\f229";
|
content: "\f229";
|
||||||
}
|
}
|
||||||
.icon-add-user::before {
|
.icon-ai-edit::before {
|
||||||
content: "\f22a";
|
content: "\f22a";
|
||||||
}
|
}
|
||||||
.icon-add-user-filled::before {
|
.icon-admin::before {
|
||||||
content: "\f22b";
|
content: "\f22b";
|
||||||
}
|
}
|
||||||
.icon-add-one-badge::before {
|
.icon-add::before {
|
||||||
content: "\f22c";
|
content: "\f22c";
|
||||||
}
|
}
|
||||||
.icon-add-filled::before {
|
.icon-add-user::before {
|
||||||
content: "\f22d";
|
content: "\f22d";
|
||||||
}
|
}
|
||||||
.icon-add-caption::before {
|
.icon-add-user-filled::before {
|
||||||
content: "\f22e";
|
content: "\f22e";
|
||||||
}
|
}
|
||||||
.icon-active-sessions::before {
|
.icon-add-one-badge::before {
|
||||||
content: "\f22f";
|
content: "\f22f";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-negative::before {
|
.icon-add-filled::before {
|
||||||
content: "\f230";
|
content: "\f230";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level90::before {
|
.icon-add-caption::before {
|
||||||
content: "\f231";
|
content: "\f231";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level9::before {
|
.icon-active-sessions::before {
|
||||||
content: "\f232";
|
content: "\f232";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level80::before {
|
.icon-rating-icons-negative::before {
|
||||||
content: "\f233";
|
content: "\f233";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level8::before {
|
.icon-rating-icons-level90::before {
|
||||||
content: "\f234";
|
content: "\f234";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level70::before {
|
.icon-rating-icons-level9::before {
|
||||||
content: "\f235";
|
content: "\f235";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level7::before {
|
.icon-rating-icons-level80::before {
|
||||||
content: "\f236";
|
content: "\f236";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level60::before {
|
.icon-rating-icons-level8::before {
|
||||||
content: "\f237";
|
content: "\f237";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level6::before {
|
.icon-rating-icons-level70::before {
|
||||||
content: "\f238";
|
content: "\f238";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level50::before {
|
.icon-rating-icons-level7::before {
|
||||||
content: "\f239";
|
content: "\f239";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level5::before {
|
.icon-rating-icons-level60::before {
|
||||||
content: "\f23a";
|
content: "\f23a";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level40::before {
|
.icon-rating-icons-level6::before {
|
||||||
content: "\f23b";
|
content: "\f23b";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level4::before {
|
.icon-rating-icons-level50::before {
|
||||||
content: "\f23c";
|
content: "\f23c";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level30::before {
|
.icon-rating-icons-level5::before {
|
||||||
content: "\f23d";
|
content: "\f23d";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level3::before {
|
.icon-rating-icons-level40::before {
|
||||||
content: "\f23e";
|
content: "\f23e";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level20::before {
|
.icon-rating-icons-level4::before {
|
||||||
content: "\f23f";
|
content: "\f23f";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level2::before {
|
.icon-rating-icons-level30::before {
|
||||||
content: "\f240";
|
content: "\f240";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level10::before {
|
.icon-rating-icons-level3::before {
|
||||||
content: "\f241";
|
content: "\f241";
|
||||||
}
|
}
|
||||||
.icon-rating-icons-level1::before {
|
.icon-rating-icons-level20::before {
|
||||||
content: "\f242";
|
content: "\f242";
|
||||||
}
|
}
|
||||||
.icon-folder-tabs-user::before {
|
.icon-rating-icons-level2::before {
|
||||||
content: "\f243";
|
content: "\f243";
|
||||||
}
|
}
|
||||||
.icon-folder-tabs-star::before {
|
.icon-rating-icons-level10::before {
|
||||||
content: "\f244";
|
content: "\f244";
|
||||||
}
|
}
|
||||||
.icon-folder-tabs-group::before {
|
.icon-rating-icons-level1::before {
|
||||||
content: "\f245";
|
content: "\f245";
|
||||||
}
|
}
|
||||||
.icon-folder-tabs-folder::before {
|
.icon-folder-tabs-user::before {
|
||||||
content: "\f246";
|
content: "\f246";
|
||||||
}
|
}
|
||||||
.icon-folder-tabs-chats::before {
|
.icon-folder-tabs-star::before {
|
||||||
content: "\f247";
|
content: "\f247";
|
||||||
}
|
}
|
||||||
.icon-folder-tabs-chat::before {
|
.icon-folder-tabs-group::before {
|
||||||
content: "\f248";
|
content: "\f248";
|
||||||
}
|
}
|
||||||
.icon-folder-tabs-channel::before {
|
.icon-folder-tabs-folder::before {
|
||||||
content: "\f249";
|
content: "\f249";
|
||||||
}
|
}
|
||||||
.icon-folder-tabs-bot::before {
|
.icon-folder-tabs-chats::before {
|
||||||
content: "\f24a";
|
content: "\f24a";
|
||||||
}
|
}
|
||||||
|
.icon-folder-tabs-chat::before {
|
||||||
|
content: "\f24b";
|
||||||
|
}
|
||||||
|
.icon-folder-tabs-channel::before {
|
||||||
|
content: "\f24c";
|
||||||
|
}
|
||||||
|
.icon-folder-tabs-bot::before {
|
||||||
|
content: "\f24d";
|
||||||
|
}
|
||||||
|
|||||||
@ -311,39 +311,42 @@ $icons-map: (
|
|||||||
"animals": "\f225",
|
"animals": "\f225",
|
||||||
"allow-speak": "\f226",
|
"allow-speak": "\f226",
|
||||||
"allow-share": "\f227",
|
"allow-share": "\f227",
|
||||||
"admin": "\f228",
|
"ai": "\f228",
|
||||||
"add": "\f229",
|
"ai-fix": "\f229",
|
||||||
"add-user": "\f22a",
|
"ai-edit": "\f22a",
|
||||||
"add-user-filled": "\f22b",
|
"admin": "\f22b",
|
||||||
"add-one-badge": "\f22c",
|
"add": "\f22c",
|
||||||
"add-filled": "\f22d",
|
"add-user": "\f22d",
|
||||||
"add-caption": "\f22e",
|
"add-user-filled": "\f22e",
|
||||||
"active-sessions": "\f22f",
|
"add-one-badge": "\f22f",
|
||||||
"rating-icons-negative": "\f230",
|
"add-filled": "\f230",
|
||||||
"rating-icons-level90": "\f231",
|
"add-caption": "\f231",
|
||||||
"rating-icons-level9": "\f232",
|
"active-sessions": "\f232",
|
||||||
"rating-icons-level80": "\f233",
|
"rating-icons-negative": "\f233",
|
||||||
"rating-icons-level8": "\f234",
|
"rating-icons-level90": "\f234",
|
||||||
"rating-icons-level70": "\f235",
|
"rating-icons-level9": "\f235",
|
||||||
"rating-icons-level7": "\f236",
|
"rating-icons-level80": "\f236",
|
||||||
"rating-icons-level60": "\f237",
|
"rating-icons-level8": "\f237",
|
||||||
"rating-icons-level6": "\f238",
|
"rating-icons-level70": "\f238",
|
||||||
"rating-icons-level50": "\f239",
|
"rating-icons-level7": "\f239",
|
||||||
"rating-icons-level5": "\f23a",
|
"rating-icons-level60": "\f23a",
|
||||||
"rating-icons-level40": "\f23b",
|
"rating-icons-level6": "\f23b",
|
||||||
"rating-icons-level4": "\f23c",
|
"rating-icons-level50": "\f23c",
|
||||||
"rating-icons-level30": "\f23d",
|
"rating-icons-level5": "\f23d",
|
||||||
"rating-icons-level3": "\f23e",
|
"rating-icons-level40": "\f23e",
|
||||||
"rating-icons-level20": "\f23f",
|
"rating-icons-level4": "\f23f",
|
||||||
"rating-icons-level2": "\f240",
|
"rating-icons-level30": "\f240",
|
||||||
"rating-icons-level10": "\f241",
|
"rating-icons-level3": "\f241",
|
||||||
"rating-icons-level1": "\f242",
|
"rating-icons-level20": "\f242",
|
||||||
"folder-tabs-user": "\f243",
|
"rating-icons-level2": "\f243",
|
||||||
"folder-tabs-star": "\f244",
|
"rating-icons-level10": "\f244",
|
||||||
"folder-tabs-group": "\f245",
|
"rating-icons-level1": "\f245",
|
||||||
"folder-tabs-folder": "\f246",
|
"folder-tabs-user": "\f246",
|
||||||
"folder-tabs-chats": "\f247",
|
"folder-tabs-star": "\f247",
|
||||||
"folder-tabs-chat": "\f248",
|
"folder-tabs-group": "\f248",
|
||||||
"folder-tabs-channel": "\f249",
|
"folder-tabs-folder": "\f249",
|
||||||
"folder-tabs-bot": "\f24a",
|
"folder-tabs-chats": "\f24a",
|
||||||
|
"folder-tabs-chat": "\f24b",
|
||||||
|
"folder-tabs-channel": "\f24c",
|
||||||
|
"folder-tabs-bot": "\f24d",
|
||||||
);
|
);
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -294,6 +294,9 @@ export type FontIconName =
|
|||||||
| 'animals'
|
| 'animals'
|
||||||
| 'allow-speak'
|
| 'allow-speak'
|
||||||
| 'allow-share'
|
| 'allow-share'
|
||||||
|
| 'ai'
|
||||||
|
| 'ai-fix'
|
||||||
|
| 'ai-edit'
|
||||||
| 'admin'
|
| 'admin'
|
||||||
| 'add'
|
| 'add'
|
||||||
| 'add-user'
|
| 'add-user'
|
||||||
|
|||||||
30
src/types/language.d.ts
vendored
30
src/types/language.d.ts
vendored
@ -1772,6 +1772,8 @@ export interface LangPair {
|
|||||||
'ToDoListErrorChooseTitle': undefined;
|
'ToDoListErrorChooseTitle': undefined;
|
||||||
'ToDoListErrorChooseTasks': undefined;
|
'ToDoListErrorChooseTasks': undefined;
|
||||||
'PremiumPreviewTodo': undefined;
|
'PremiumPreviewTodo': undefined;
|
||||||
|
'PremiumPreviewAiTools': undefined;
|
||||||
|
'PremiumPreviewAiToolsDescription': undefined;
|
||||||
'NativeDownloadFailed': undefined;
|
'NativeDownloadFailed': undefined;
|
||||||
'DescriptionAboutTon': undefined;
|
'DescriptionAboutTon': undefined;
|
||||||
'ButtonTopUpViaFragment': undefined;
|
'ButtonTopUpViaFragment': undefined;
|
||||||
@ -2065,6 +2067,31 @@ export interface LangPair {
|
|||||||
'ReminderSetToast': undefined;
|
'ReminderSetToast': undefined;
|
||||||
'NoForwardsRequestReject': undefined;
|
'NoForwardsRequestReject': undefined;
|
||||||
'NoForwardsRequestAccept': undefined;
|
'NoForwardsRequestAccept': undefined;
|
||||||
|
'AiMessageEditor': undefined;
|
||||||
|
'AiMessageEditorTranslate': undefined;
|
||||||
|
'AiMessageEditorStyle': undefined;
|
||||||
|
'AiMessageEditorFix': undefined;
|
||||||
|
'AiMessageEditorSelectStyle': undefined;
|
||||||
|
'AiMessageEditorDailyLimitReachedPremium': undefined;
|
||||||
|
'AiMessageEditorGenericError': undefined;
|
||||||
|
'AiMessageEditorStyleFormal': undefined;
|
||||||
|
'AiMessageEditorStyleShort': undefined;
|
||||||
|
'AiMessageEditorStyleTribal': undefined;
|
||||||
|
'AiMessageEditorStyleCorp': undefined;
|
||||||
|
'AiMessageEditorStyleBiblical': undefined;
|
||||||
|
'AiMessageEditorStyleViking': undefined;
|
||||||
|
'AiMessageEditorStyleZen': undefined;
|
||||||
|
'AiMessageEditorResult': undefined;
|
||||||
|
'AiMessageEditorOriginal': undefined;
|
||||||
|
'AiMessageEditorApply': undefined;
|
||||||
|
'AiMessageEditorEmojify': undefined;
|
||||||
|
'AiMessageEditorTranslation': undefined;
|
||||||
|
'TextShowMore': undefined;
|
||||||
|
'TextShowLess': undefined;
|
||||||
|
'AiMessageEditorFrom': undefined;
|
||||||
|
'AiMessageEditorTo': undefined;
|
||||||
|
'TranslationToneNeutral': undefined;
|
||||||
|
'ButtonHelp': undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LangPairWithVariables<V = LangVariable> {
|
export interface LangPairWithVariables<V = LangVariable> {
|
||||||
@ -3625,6 +3652,9 @@ export interface LangPairWithVariables<V = LangVariable> {
|
|||||||
'RankEditText': {
|
'RankEditText': {
|
||||||
'user': V;
|
'user': V;
|
||||||
};
|
};
|
||||||
|
'AiMessageEditorDailyLimitReached': {
|
||||||
|
'link': V;
|
||||||
|
};
|
||||||
'UnofficialSecurityRisk': {
|
'UnofficialSecurityRisk': {
|
||||||
'peer': V;
|
'peer': V;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,22 @@
|
|||||||
export default function calcTextLineHeightAndCount(textContainer: HTMLElement) {
|
export default function calcTextLineHeightAndCount(
|
||||||
const lineHeight = parseInt(getComputedStyle(textContainer).lineHeight, 10);
|
textContainer: HTMLElement,
|
||||||
|
shouldSubtractPadding?: boolean,
|
||||||
|
) {
|
||||||
|
const DEFAULT_LINE_HEIGHT_RATIO = 1.2; // CSS spec default for line-height: normal
|
||||||
|
|
||||||
const totalLines = textContainer.scrollHeight / lineHeight;
|
const style = getComputedStyle(textContainer);
|
||||||
|
const lineHeight = style.lineHeight === 'normal'
|
||||||
|
? parseFloat(style.fontSize) * DEFAULT_LINE_HEIGHT_RATIO
|
||||||
|
: parseFloat(style.lineHeight);
|
||||||
|
|
||||||
|
let contentHeight = textContainer.scrollHeight;
|
||||||
|
if (shouldSubtractPadding) {
|
||||||
|
const paddingTop = parseFloat(style.paddingTop) || 0;
|
||||||
|
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
||||||
|
contentHeight -= paddingTop + paddingBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalLines = Math.round(contentHeight / lineHeight);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalLines,
|
totalLines,
|
||||||
|
|||||||
@ -259,6 +259,15 @@ function getEntityDataFromNode(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === ApiMessageEntityTypes.DiffInsert
|
||||||
|
|| type === ApiMessageEntityTypes.DiffReplace
|
||||||
|
|| type === ApiMessageEntityTypes.DiffDelete) {
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
entity: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
index,
|
index,
|
||||||
entity: {
|
entity: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user