Message Context Menu: Introduce Translate feature
This commit is contained in:
parent
da70c3893d
commit
515394d35e
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
src/lib/rlottie/rlottie-wasm.js
|
src/lib/rlottie/rlottie-wasm.js
|
||||||
src/lib/webp/webp_wasm.js
|
src/lib/webp/webp_wasm.js
|
||||||
|
src/lib/fasttextweb/fasttext-wasm.js
|
||||||
|
|
||||||
src/lib/gramjs/tl/types-generator/template.js
|
src/lib/gramjs/tl/types-generator/template.js
|
||||||
src/lib/gramjs/tl/api.d.ts
|
src/lib/gramjs/tl/api.d.ts
|
||||||
|
|||||||
1
src/@types/global.d.ts
vendored
1
src/@types/global.d.ts
vendored
@ -61,6 +61,7 @@ type AllEmojis = Record<string, Emoji | EmojiWithSkins>;
|
|||||||
declare module '*.png';
|
declare module '*.png';
|
||||||
declare module '*.svg';
|
declare module '*.svg';
|
||||||
declare module '*.tgs';
|
declare module '*.tgs';
|
||||||
|
declare module '*.wasm';
|
||||||
|
|
||||||
declare module '*.txt' {
|
declare module '*.txt' {
|
||||||
const content: string;
|
const content: string;
|
||||||
|
|||||||
@ -1616,3 +1616,12 @@ function buildThreadInfo(
|
|||||||
...(recentRepliers && { recentReplierIds: recentRepliers.map(getApiChatIdFromMtpPeer) }),
|
...(recentRepliers && { recentReplierIds: recentRepliers.map(getApiChatIdFromMtpPeer) }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildApiFormattedText(textWithEntities: GramJs.TextWithEntities): ApiFormattedText {
|
||||||
|
const { text, entities } = textWithEntities;
|
||||||
|
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
entities: entities.map(buildApiMessageEntity),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import type {
|
|||||||
ApiRequestInputInvoice,
|
ApiRequestInputInvoice,
|
||||||
ApiChatReactions,
|
ApiChatReactions,
|
||||||
ApiReaction,
|
ApiReaction,
|
||||||
|
ApiFormattedText,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import {
|
import {
|
||||||
ApiMessageEntityTypes,
|
ApiMessageEntityTypes,
|
||||||
@ -598,3 +599,10 @@ export function buildInputEmojiStatus(emojiStatus: ApiSticker, expires?: number)
|
|||||||
documentId: BigInt(emojiStatus.id),
|
documentId: BigInt(emojiStatus.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildInputTextWithEntities(formatted: ApiFormattedText) {
|
||||||
|
return new GramJs.TextWithEntities({
|
||||||
|
text: formatted.text,
|
||||||
|
entities: formatted.entities?.map(buildMtpMessageEntity) || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export {
|
|||||||
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
||||||
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
|
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
|
||||||
saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio,
|
saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio,
|
||||||
closePoll, fetchExtendedMedia,
|
closePoll, fetchExtendedMedia, translateText,
|
||||||
} from './messages';
|
} from './messages';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import type {
|
|||||||
ApiSendMessageAction,
|
ApiSendMessageAction,
|
||||||
ApiContact,
|
ApiContact,
|
||||||
ApiPoll,
|
ApiPoll,
|
||||||
|
ApiFormattedText,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import {
|
import {
|
||||||
MAIN_THREAD_ID,
|
MAIN_THREAD_ID,
|
||||||
@ -36,6 +37,7 @@ import {
|
|||||||
buildLocalMessage,
|
buildLocalMessage,
|
||||||
buildWebPage,
|
buildWebPage,
|
||||||
buildApiSponsoredMessage,
|
buildApiSponsoredMessage,
|
||||||
|
buildApiFormattedText,
|
||||||
} from '../apiBuilders/messages';
|
} from '../apiBuilders/messages';
|
||||||
import { buildApiUser } from '../apiBuilders/users';
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
import {
|
import {
|
||||||
@ -51,6 +53,7 @@ import {
|
|||||||
isServiceMessageWithMedia,
|
isServiceMessageWithMedia,
|
||||||
buildSendMessageAction,
|
buildSendMessageAction,
|
||||||
buildInputPollFromExisting,
|
buildInputPollFromExisting,
|
||||||
|
buildInputTextWithEntities,
|
||||||
} from '../gramjsBuilders';
|
} from '../gramjsBuilders';
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/chats';
|
import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/chats';
|
||||||
@ -69,6 +72,15 @@ import { getServerTimeOffset } from '../../../util/serverTime';
|
|||||||
const FAST_SEND_TIMEOUT = 1000;
|
const FAST_SEND_TIMEOUT = 1000;
|
||||||
const INPUT_WAVEFORM_LENGTH = 63;
|
const INPUT_WAVEFORM_LENGTH = 63;
|
||||||
|
|
||||||
|
type TranslateTextParams = ({
|
||||||
|
text: ApiFormattedText[];
|
||||||
|
} | {
|
||||||
|
chat: ApiChat;
|
||||||
|
messageIds: number[];
|
||||||
|
}) & {
|
||||||
|
toLanguageCode: string;
|
||||||
|
};
|
||||||
|
|
||||||
let onUpdate: OnApiUpdate;
|
let onUpdate: OnApiUpdate;
|
||||||
|
|
||||||
export function init(_onUpdate: OnApiUpdate) {
|
export function init(_onUpdate: OnApiUpdate) {
|
||||||
@ -1560,3 +1572,38 @@ export async function transcribeAudio({
|
|||||||
|
|
||||||
return result.transcriptionId.toString();
|
return result.transcriptionId.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function translateText(params: TranslateTextParams) {
|
||||||
|
let result;
|
||||||
|
const isMessageTranslation = 'chat' in params;
|
||||||
|
if (isMessageTranslation) {
|
||||||
|
const { chat, messageIds, toLanguageCode } = params;
|
||||||
|
result = await invokeRequest(new GramJs.messages.TranslateText({
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
id: messageIds,
|
||||||
|
toLang: toLanguageCode,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
const { text, toLanguageCode } = params;
|
||||||
|
result = await invokeRequest(new GramJs.messages.TranslateText({
|
||||||
|
text: text.map((t) => buildInputTextWithEntities(t)),
|
||||||
|
toLang: toLanguageCode,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) return undefined;
|
||||||
|
|
||||||
|
const formattedText = result.result.map((r) => buildApiFormattedText(r));
|
||||||
|
|
||||||
|
if (isMessageTranslation) {
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updateMessageTranslations',
|
||||||
|
chatId: params.chat.id,
|
||||||
|
messageIds: params.messageIds,
|
||||||
|
translations: formattedText,
|
||||||
|
toLanguageCode: params.toLanguageCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedText;
|
||||||
|
}
|
||||||
|
|||||||
@ -601,6 +601,14 @@ export type ApiUpdateTopics = {
|
|||||||
chatId: string;
|
chatId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiUpdateMessageTranslations = {
|
||||||
|
'@type': 'updateMessageTranslations';
|
||||||
|
chatId: string;
|
||||||
|
messageIds: number[];
|
||||||
|
translations: ApiFormattedText[];
|
||||||
|
toLanguageCode: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiUpdate = (
|
export type ApiUpdate = (
|
||||||
ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate |
|
ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate |
|
||||||
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
|
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
|
||||||
@ -616,7 +624,7 @@ export type ApiUpdate = (
|
|||||||
ApiUpdateFavoriteStickers | ApiUpdateStickerSet | ApiUpdateStickerSets | ApiUpdateStickerSetsOrder |
|
ApiUpdateFavoriteStickers | ApiUpdateStickerSet | ApiUpdateStickerSets | ApiUpdateStickerSetsOrder |
|
||||||
ApiUpdateRecentStickers | ApiUpdateSavedGifs | ApiUpdateNewScheduledMessage | ApiUpdateMoveStickerSetToTop |
|
ApiUpdateRecentStickers | ApiUpdateSavedGifs | ApiUpdateNewScheduledMessage | ApiUpdateMoveStickerSetToTop |
|
||||||
ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage |
|
ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage |
|
||||||
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages |
|
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations |
|
||||||
ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent |
|
ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent |
|
||||||
ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy |
|
ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy |
|
||||||
ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions |
|
ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions |
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export { default as MessageSelectToolbar } from '../components/middle/MessageSel
|
|||||||
export { default as SeenByModal } from '../components/common/SeenByModal';
|
export { default as SeenByModal } from '../components/common/SeenByModal';
|
||||||
export { default as ReactorListModal } from '../components/middle/ReactorListModal';
|
export { default as ReactorListModal } from '../components/middle/ReactorListModal';
|
||||||
export { default as EmojiInteractionAnimation } from '../components/middle/EmojiInteractionAnimation';
|
export { default as EmojiInteractionAnimation } from '../components/middle/EmojiInteractionAnimation';
|
||||||
|
export { default as MessageLanguageModal } from '../components/middle/MessageLanguageModal';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
export { default as LeftSearch } from '../components/left/search/LeftSearch';
|
export { default as LeftSearch } from '../components/left/search/LeftSearch';
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import React, { memo, useMemo, useRef } from '../../lib/teact/teact';
|
import React, { memo, useMemo, useRef } from '../../lib/teact/teact';
|
||||||
|
|
||||||
import type { ApiMessage } from '../../api/types';
|
import type { ApiFormattedText, ApiMessage } from '../../api/types';
|
||||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
import { ApiMessageEntityTypes } from '../../api/types';
|
import { ApiMessageEntityTypes } from '../../api/types';
|
||||||
import trimText from '../../util/trimText';
|
import trimText from '../../util/trimText';
|
||||||
import { getMessageText } from '../../global/helpers';
|
import { getMessageText, stripCustomEmoji } from '../../global/helpers';
|
||||||
import { renderTextWithEntities } from './helpers/renderTextWithEntities';
|
import { renderTextWithEntities } from './helpers/renderTextWithEntities';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
|
translatedText?: ApiFormattedText;
|
||||||
|
isForAnimation?: boolean;
|
||||||
emojiSize?: number;
|
emojiSize?: number;
|
||||||
highlight?: string;
|
highlight?: string;
|
||||||
isSimple?: boolean;
|
isSimple?: boolean;
|
||||||
@ -25,6 +27,8 @@ const MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS = 3;
|
|||||||
|
|
||||||
function MessageText({
|
function MessageText({
|
||||||
message,
|
message,
|
||||||
|
translatedText,
|
||||||
|
isForAnimation,
|
||||||
emojiSize,
|
emojiSize,
|
||||||
highlight,
|
highlight,
|
||||||
isSimple,
|
isSimple,
|
||||||
@ -40,7 +44,12 @@ function MessageText({
|
|||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
|
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
const { text, entities } = message.content.text || {};
|
const formattedText = translatedText || message.content.text || undefined;
|
||||||
|
|
||||||
|
const adaptedFormattedText = isForAnimation && formattedText ? stripCustomEmoji(formattedText) : formattedText;
|
||||||
|
|
||||||
|
const { text, entities } = adaptedFormattedText || {};
|
||||||
|
|
||||||
const withSharedCanvas = useMemo(() => {
|
const withSharedCanvas = useMemo(() => {
|
||||||
const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
|
const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
|
||||||
if (hasSpoilers) {
|
if (hasSpoilers) {
|
||||||
|
|||||||
@ -23,7 +23,6 @@ export type TextFilter = (
|
|||||||
'simple_markdown' | 'simple_markdown_html'
|
'simple_markdown' | 'simple_markdown_html'
|
||||||
);
|
);
|
||||||
|
|
||||||
const RE_LETTER_OR_DIGIT = /^[\d\wа-яё]$/i;
|
|
||||||
const SIMPLE_MARKDOWN_REGEX = /(\*\*|__).+?\1/g;
|
const SIMPLE_MARKDOWN_REGEX = /(\*\*|__).+?\1/g;
|
||||||
|
|
||||||
export default function renderText(
|
export default function renderText(
|
||||||
@ -186,8 +185,7 @@ function addHighlight(textParts: TextPart[], highlight: string | undefined): Tex
|
|||||||
|
|
||||||
const lowerCaseText = part.toLowerCase();
|
const lowerCaseText = part.toLowerCase();
|
||||||
const queryPosition = lowerCaseText.indexOf(highlight.toLowerCase());
|
const queryPosition = lowerCaseText.indexOf(highlight.toLowerCase());
|
||||||
const nextSymbol = lowerCaseText[queryPosition + highlight.length];
|
if (queryPosition < 0) {
|
||||||
if (queryPosition < 0 || (nextSymbol && nextSymbol.match(RE_LETTER_OR_DIGIT))) {
|
|
||||||
result.push(part);
|
result.push(part);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -200,7 +198,6 @@ function addHighlight(textParts: TextPart[], highlight: string | undefined): Tex
|
|||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
newParts.push(part.substring(queryPosition + highlight.length));
|
newParts.push(part.substring(queryPosition + highlight.length));
|
||||||
|
|
||||||
return [...result, ...newParts];
|
return [...result, ...newParts];
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -298,6 +298,10 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
case SettingsScreens.CustomEmoji:
|
case SettingsScreens.CustomEmoji:
|
||||||
setSettingsScreen(SettingsScreens.Stickers);
|
setSettingsScreen(SettingsScreens.Stickers);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case SettingsScreens.DoNotTranslate:
|
||||||
|
setSettingsScreen(SettingsScreens.Language);
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -306,6 +306,10 @@
|
|||||||
.Radio:last-child {
|
.Radio:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Checkbox {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Radio + .Radio,
|
.Radio + .Radio,
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import SettingsQuickReaction from './SettingsQuickReaction';
|
|||||||
import SettingsPasscode from './passcode/SettingsPasscode';
|
import SettingsPasscode from './passcode/SettingsPasscode';
|
||||||
import SettingsStickers from './SettingsStickers';
|
import SettingsStickers from './SettingsStickers';
|
||||||
import SettingsCustomEmoji from './SettingsCustomEmoji';
|
import SettingsCustomEmoji from './SettingsCustomEmoji';
|
||||||
|
import SettingsDoNotTranslate from './SettingsDoNotTranslate';
|
||||||
import SettingsExperimental from './SettingsExperimental';
|
import SettingsExperimental from './SettingsExperimental';
|
||||||
|
|
||||||
import './Settings.scss';
|
import './Settings.scss';
|
||||||
@ -249,7 +250,15 @@ const Settings: FC<OwnProps> = ({
|
|||||||
);
|
);
|
||||||
case SettingsScreens.Language:
|
case SettingsScreens.Language:
|
||||||
return (
|
return (
|
||||||
<SettingsLanguage isActive={isScreenActive} onReset={handleReset} />
|
<SettingsLanguage
|
||||||
|
isActive={isScreenActive || screen === SettingsScreens.DoNotTranslate}
|
||||||
|
onReset={handleReset}
|
||||||
|
onScreenSelect={onScreenSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case SettingsScreens.DoNotTranslate:
|
||||||
|
return (
|
||||||
|
<SettingsDoNotTranslate isActive={isScreenActive} onReset={handleReset} />
|
||||||
);
|
);
|
||||||
case SettingsScreens.Stickers:
|
case SettingsScreens.Stickers:
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
@import "../../../styles/mixins";
|
||||||
|
|
||||||
|
.root, .item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.languages {
|
||||||
|
overflow-y: auto;
|
||||||
|
@include overflow-y-overlay();
|
||||||
|
}
|
||||||
183
src/components/left/settings/SettingsDoNotTranslate.tsx
Normal file
183
src/components/left/settings/SettingsDoNotTranslate.tsx
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import React, {
|
||||||
|
memo, useCallback, useEffect, useMemo, useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import type { ISettings } from '../../../types';
|
||||||
|
import type { IRadioOption } from '../../ui/CheckboxGroup';
|
||||||
|
|
||||||
|
import { SUPPORTED_TRANSLATION_LANGUAGES } from '../../../config';
|
||||||
|
import { partition, unique } from '../../../util/iteratees';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
|
import Checkbox from '../../ui/Checkbox';
|
||||||
|
import InputText from '../../ui/InputText';
|
||||||
|
|
||||||
|
import styles from './SettingsDoNotTranslate.module.scss';
|
||||||
|
|
||||||
|
// https://fasttext.cc/docs/en/language-identification.html
|
||||||
|
const LOCAL_SUPPORTED_DETECTION_LANGUAGES = [
|
||||||
|
'af', 'als', 'am', 'an', 'ar', 'arz', 'as', 'ast', 'av', 'az',
|
||||||
|
'azb', 'ba', 'bar', 'bcl', 'be', 'bg', 'bh', 'bn', 'bo', 'bpy',
|
||||||
|
'br', 'bs', 'bxr', 'ca', 'cbk', 'ce', 'ceb', 'ckb', 'co', 'cs',
|
||||||
|
'cv', 'cy', 'da', 'de', 'diq', 'dsb', 'dty', 'dv', 'el', 'eml',
|
||||||
|
'en', 'eo', 'es', 'et', 'eu', 'fa', 'fi', 'fr', 'frr', 'fy',
|
||||||
|
'ga', 'gd', 'gl', 'gn', 'gom', 'gu', 'gv', 'he', 'hi', 'hif',
|
||||||
|
'hr', 'hsb', 'ht', 'hu', 'hy', 'ia', 'id', 'ie', 'ilo', 'io',
|
||||||
|
'is', 'it', 'ja', 'jbo', 'jv', 'ka', 'kk', 'km', 'kn', 'ko',
|
||||||
|
'krc', 'ku', 'kv', 'kw', 'ky', 'la', 'lb', 'lez', 'li', 'lmo',
|
||||||
|
'lo', 'lrc', 'lt', 'lv', 'mai', 'mg', 'mhr', 'min', 'mk', 'ml',
|
||||||
|
'mn', 'mr', 'mrj', 'ms', 'mt', 'mwl', 'my', 'myv', 'mzn', 'nah',
|
||||||
|
'nap', 'nds', 'ne', 'new', 'nl', 'nn', 'no', 'oc', 'or', 'os',
|
||||||
|
'pa', 'pam', 'pfl', 'pl', 'pms', 'pnb', 'ps', 'pt', 'qu', 'rm',
|
||||||
|
'ro', 'ru', 'rue', 'sa', 'sah', 'sc', 'scn', 'sco', 'sd', 'sh',
|
||||||
|
'si', 'sk', 'sl', 'so', 'sq', 'sr', 'su', 'sv', 'sw', 'ta', 'te',
|
||||||
|
'tg', 'th', 'tk', 'tl', 'tr', 'tt', 'tyv', 'ug', 'uk', 'ur', 'uz',
|
||||||
|
'vec', 'vep', 'vi', 'vls', 'vo', 'wa', 'war', 'wuu', 'xal', 'xmf',
|
||||||
|
'yi', 'yo', 'yue', 'zh',
|
||||||
|
];
|
||||||
|
|
||||||
|
const SUPPORTED_LANGUAGES = SUPPORTED_TRANSLATION_LANGUAGES.filter((lang: string) => (
|
||||||
|
LOCAL_SUPPORTED_DETECTION_LANGUAGES.includes(lang)
|
||||||
|
));
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
onReset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = Pick<ISettings, 'language' | 'doNotTranslate'>;
|
||||||
|
|
||||||
|
const SettingsDoNotTranslate: FC<OwnProps & StateProps> = ({
|
||||||
|
isActive,
|
||||||
|
language,
|
||||||
|
doNotTranslate,
|
||||||
|
onReset,
|
||||||
|
}) => {
|
||||||
|
const { setSettingOption } = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
const [displayedOptions, setDisplayedOptions] = useState<IRadioOption[]>([]);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const options: IRadioOption[] = useMemo(() => {
|
||||||
|
return SUPPORTED_LANGUAGES.map((langCode: string) => {
|
||||||
|
const translatedNames = new Intl.DisplayNames([language], { type: 'language' });
|
||||||
|
const translatedName = translatedNames.of(langCode)!;
|
||||||
|
|
||||||
|
const originalNames = new Intl.DisplayNames([langCode], { type: 'language' });
|
||||||
|
const originalName = originalNames.of(langCode)!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
langCode,
|
||||||
|
translatedName,
|
||||||
|
originalName,
|
||||||
|
};
|
||||||
|
}).map(({ langCode, translatedName, originalName }) => ({
|
||||||
|
label: translatedName,
|
||||||
|
subLabel: originalName,
|
||||||
|
value: langCode,
|
||||||
|
disabled: langCode === language,
|
||||||
|
}));
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
|
const allSelected = useMemo(() => {
|
||||||
|
return unique([...doNotTranslate, language]);
|
||||||
|
}, [doNotTranslate, language]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive) setSearch('');
|
||||||
|
}, [isActive]);
|
||||||
|
|
||||||
|
useEffectWithPrevDeps(([prevIsActive]) => {
|
||||||
|
if (prevIsActive === isActive) return;
|
||||||
|
if (isActive && displayedOptions.length) return;
|
||||||
|
|
||||||
|
const [selected, unselected] = partition(options, (option) => allSelected.includes(option.value));
|
||||||
|
const current = selected.find((option) => option.value === language);
|
||||||
|
const selectedFiltered = selected.filter((option) => option.value !== language);
|
||||||
|
|
||||||
|
setDisplayedOptions([current!, ...selectedFiltered, ...unselected]);
|
||||||
|
}, [isActive, allSelected, displayedOptions.length, language, options]);
|
||||||
|
|
||||||
|
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { value, checked } = event.currentTarget;
|
||||||
|
let newDoNotTranslate: string[];
|
||||||
|
if (checked) {
|
||||||
|
newDoNotTranslate = unique([...doNotTranslate, value]);
|
||||||
|
} else {
|
||||||
|
newDoNotTranslate = doNotTranslate.filter((v) => v !== value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSettingOption({
|
||||||
|
doNotTranslate: newDoNotTranslate,
|
||||||
|
});
|
||||||
|
}, [doNotTranslate, setSettingOption]);
|
||||||
|
|
||||||
|
const handleSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearch(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filteredDisplayedOptions = useMemo(() => {
|
||||||
|
if (!search.trim()) {
|
||||||
|
return displayedOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayedOptions.filter((option) => (
|
||||||
|
option.label.toLowerCase().includes(search.toLowerCase())
|
||||||
|
|| option.subLabel?.toLowerCase().includes(search.toLowerCase())
|
||||||
|
|| option.value.toLowerCase().includes(search.toLowerCase())
|
||||||
|
));
|
||||||
|
}, [displayedOptions, search]);
|
||||||
|
|
||||||
|
useHistoryBack({
|
||||||
|
isActive,
|
||||||
|
onBack: onReset,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.root, 'settings-content custom-scroll')}>
|
||||||
|
<div className={buildClassName(styles.item, 'settings-item')}>
|
||||||
|
<InputText
|
||||||
|
key="search"
|
||||||
|
value={search}
|
||||||
|
onChange={handleSearch}
|
||||||
|
placeholder={lang('Search')}
|
||||||
|
teactExperimentControlled
|
||||||
|
/>
|
||||||
|
<div className={buildClassName(styles.languages, 'radio-group custom-scroll')}>
|
||||||
|
{filteredDisplayedOptions.map((option) => (
|
||||||
|
<Checkbox
|
||||||
|
className={styles.checkbox}
|
||||||
|
label={option.label}
|
||||||
|
subLabel={option.subLabel}
|
||||||
|
checked={allSelected.includes(option.value)}
|
||||||
|
value={option.value}
|
||||||
|
disabled={option.disabled}
|
||||||
|
key={option.value}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): StateProps => {
|
||||||
|
const {
|
||||||
|
language, doNotTranslate,
|
||||||
|
} = global.settings.byKey;
|
||||||
|
|
||||||
|
return {
|
||||||
|
language,
|
||||||
|
doNotTranslate,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(SettingsDoNotTranslate));
|
||||||
@ -97,6 +97,8 @@ const SettingsHeader: FC<OwnProps> = ({
|
|||||||
return <h3>{lang('PrivacySettings')}</h3>;
|
return <h3>{lang('PrivacySettings')}</h3>;
|
||||||
case SettingsScreens.Language:
|
case SettingsScreens.Language:
|
||||||
return <h3>{lang('Language')}</h3>;
|
return <h3>{lang('Language')}</h3>;
|
||||||
|
case SettingsScreens.DoNotTranslate:
|
||||||
|
return <h3>{lang('DoNotTranslate')}</h3>;
|
||||||
case SettingsScreens.Stickers:
|
case SettingsScreens.Stickers:
|
||||||
return <h3>{lang('StickersName')}</h3>;
|
return <h3>{lang('StickersName')}</h3>;
|
||||||
case SettingsScreens.Experimental:
|
case SettingsScreens.Experimental:
|
||||||
|
|||||||
@ -1,31 +1,44 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useMemo, useState,
|
memo, useCallback, useEffect, useMemo, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import { SettingsScreens } from '../../../types';
|
||||||
import type { ISettings, LangCode } from '../../../types';
|
import type { ISettings, LangCode } from '../../../types';
|
||||||
import type { ApiLanguage } from '../../../api/types';
|
import type { ApiLanguage } from '../../../api/types';
|
||||||
|
|
||||||
import { setLanguage } from '../../../util/langProvider';
|
import { setLanguage } from '../../../util/langProvider';
|
||||||
|
import { unique } from '../../../util/iteratees';
|
||||||
|
|
||||||
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
import RadioGroup from '../../ui/RadioGroup';
|
import RadioGroup from '../../ui/RadioGroup';
|
||||||
import Loading from '../../ui/Loading';
|
import Loading from '../../ui/Loading';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import Checkbox from '../../ui/Checkbox';
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import ListItem from '../../ui/ListItem';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
|
onScreenSelect: (screen: SettingsScreens) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = Pick<ISettings, 'languages' | 'language'>;
|
type StateProps = {
|
||||||
|
lastSyncTime?: number;
|
||||||
|
} & Pick<ISettings, 'languages' | 'language' | 'canTranslate' | 'doNotTranslate'>;
|
||||||
|
|
||||||
const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||||
isActive,
|
isActive,
|
||||||
onReset,
|
|
||||||
languages,
|
languages,
|
||||||
language,
|
language,
|
||||||
|
canTranslate,
|
||||||
|
doNotTranslate,
|
||||||
|
lastSyncTime,
|
||||||
|
onScreenSelect,
|
||||||
|
onReset,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
loadLanguages,
|
loadLanguages,
|
||||||
@ -36,10 +49,13 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
|||||||
const [selectedLanguage, setSelectedLanguage] = useState<string>(language);
|
const [selectedLanguage, setSelectedLanguage] = useState<string>(language);
|
||||||
const [isLoading, markIsLoading, unmarkIsLoading] = useFlag();
|
const [isLoading, markIsLoading, unmarkIsLoading] = useFlag();
|
||||||
|
|
||||||
// TODO Throttle
|
const lang = useLang();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadLanguages();
|
if (lastSyncTime && !languages?.length) {
|
||||||
}, [loadLanguages]);
|
loadLanguages();
|
||||||
|
}
|
||||||
|
}, [languages, lastSyncTime, loadLanguages]);
|
||||||
|
|
||||||
const handleChange = useCallback((langCode: string) => {
|
const handleChange = useCallback((langCode: string) => {
|
||||||
setSelectedLanguage(langCode);
|
setSelectedLanguage(langCode);
|
||||||
@ -58,24 +74,65 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
|||||||
return languages ? buildOptions(languages) : undefined;
|
return languages ? buildOptions(languages) : undefined;
|
||||||
}, [languages]);
|
}, [languages]);
|
||||||
|
|
||||||
|
const handleShouldTranslateChange = useCallback((newValue: boolean) => {
|
||||||
|
setSettingOption({ canTranslate: newValue });
|
||||||
|
}, [setSettingOption]);
|
||||||
|
|
||||||
|
const doNotTranslateText = useMemo(() => {
|
||||||
|
const allDoNotTranslateLanguages = unique([...doNotTranslate, language]);
|
||||||
|
// Do not translate current language
|
||||||
|
if (allDoNotTranslateLanguages.length === 1) {
|
||||||
|
if (!languages) {
|
||||||
|
return lang('Loading');
|
||||||
|
}
|
||||||
|
return languages.find(({ langCode }) => langCode === language)?.nativeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lang('Languages', allDoNotTranslateLanguages.length);
|
||||||
|
}, [doNotTranslate, lang, language, languages]);
|
||||||
|
|
||||||
|
const handleDoNotSelectOpen = useCallback(() => {
|
||||||
|
onScreenSelect(SettingsScreens.DoNotTranslate);
|
||||||
|
}, [onScreenSelect]);
|
||||||
|
|
||||||
useHistoryBack({
|
useHistoryBack({
|
||||||
isActive,
|
isActive,
|
||||||
onBack: onReset,
|
onBack: onReset,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings-content settings-item settings-language custom-scroll settings-item--first">
|
<div className="settings-content settings-language custom-scroll">
|
||||||
{options ? (
|
<div className="settings-item">
|
||||||
<RadioGroup
|
<Checkbox
|
||||||
name="keyboard-send-settings"
|
label={lang('ShowTranslateButton')}
|
||||||
options={options}
|
checked={canTranslate}
|
||||||
selected={selectedLanguage}
|
onCheck={handleShouldTranslateChange}
|
||||||
loadingOption={isLoading ? selectedLanguage : undefined}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
{canTranslate && (
|
||||||
<Loading />
|
<ListItem
|
||||||
)}
|
onClick={handleDoNotSelectOpen}
|
||||||
|
>
|
||||||
|
{lang('DoNotTranslate')}
|
||||||
|
<span className="settings-item__current-value">{doNotTranslateText}</span>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
<p className="settings-item-description mb-0 mt-1">
|
||||||
|
{lang('lng_translate_settings_about')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="settings-item">
|
||||||
|
{options ? (
|
||||||
|
<RadioGroup
|
||||||
|
name="language-settings"
|
||||||
|
options={options}
|
||||||
|
selected={selectedLanguage}
|
||||||
|
loadingOption={isLoading ? selectedLanguage : undefined}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Loading />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -95,9 +152,16 @@ function buildOptions(languages: ApiLanguage[]) {
|
|||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
|
const {
|
||||||
|
language, languages, canTranslate, doNotTranslate,
|
||||||
|
} = global.settings.byKey;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
languages: global.settings.byKey.languages,
|
lastSyncTime: global.lastSyncTime,
|
||||||
language: global.settings.byKey.language,
|
languages,
|
||||||
|
language,
|
||||||
|
canTranslate,
|
||||||
|
doNotTranslate,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(SettingsLanguage));
|
)(SettingsLanguage));
|
||||||
|
|||||||
17
src/components/middle/MessageLanguageModal.async.tsx
Normal file
17
src/components/middle/MessageLanguageModal.async.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React, { memo } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import type { OwnProps } from './MessageLanguageModal';
|
||||||
|
|
||||||
|
import { Bundles } from '../../util/moduleLoader';
|
||||||
|
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const MessageLanguageModalAsync: FC<OwnProps> = (props) => {
|
||||||
|
const { isOpen } = props;
|
||||||
|
const MessageLanguageModal = useModuleLoader(Bundles.Extra, 'MessageLanguageModal', !isOpen);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return MessageLanguageModal ? <MessageLanguageModal {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(MessageLanguageModalAsync);
|
||||||
30
src/components/middle/MessageLanguageModal.module.scss
Normal file
30
src/components/middle/MessageLanguageModal.module.scss
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
@import "../../styles/mixins";
|
||||||
|
|
||||||
|
.root {
|
||||||
|
:global(.modal-content) {
|
||||||
|
min-height: min(75vh, 32rem);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.matching-text-highlight) {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&[dir="rtl"] {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title, .subtitle {
|
||||||
|
text-align: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.languages {
|
||||||
|
overflow-y: auto;
|
||||||
|
@include overflow-y-overlay();
|
||||||
|
}
|
||||||
147
src/components/middle/MessageLanguageModal.tsx
Normal file
147
src/components/middle/MessageLanguageModal.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import React, {
|
||||||
|
memo, useCallback, useEffect, useMemo, useState,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { selectLanguageCode, selectRequestedTranslationLanguage, selectTabState } from '../../global/selectors';
|
||||||
|
import { SUPPORTED_TRANSLATION_LANGUAGES } from '../../config';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import renderText from '../common/helpers/renderText';
|
||||||
|
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
|
import Modal from '../ui/Modal';
|
||||||
|
import ListItem from '../ui/ListItem';
|
||||||
|
import InputText from '../ui/InputText';
|
||||||
|
|
||||||
|
import styles from './MessageLanguageModal.module.scss';
|
||||||
|
|
||||||
|
type LanguageItem = {
|
||||||
|
langCode: string;
|
||||||
|
translatedName: string;
|
||||||
|
originalName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
chatId?: string;
|
||||||
|
messageId?: number;
|
||||||
|
activeTranslationLanguage?: string;
|
||||||
|
currentLanguageCode: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MessageLanguageModal: FC<OwnProps & StateProps> = ({
|
||||||
|
isOpen,
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
activeTranslationLanguage,
|
||||||
|
currentLanguageCode,
|
||||||
|
}) => {
|
||||||
|
const { requestMessageTranslation, closeMessageLanguageModal } = getActions();
|
||||||
|
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const handleSelect = useCallback((toLanguageCode: string) => {
|
||||||
|
if (!chatId || !messageId) return;
|
||||||
|
|
||||||
|
requestMessageTranslation({ chatId, id: messageId, toLanguageCode });
|
||||||
|
closeMessageLanguageModal();
|
||||||
|
}, [chatId, closeMessageLanguageModal, messageId, requestMessageTranslation]);
|
||||||
|
|
||||||
|
const handleSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearch(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const translateLanguages = useMemo(() => SUPPORTED_TRANSLATION_LANGUAGES.map((langCode: string) => {
|
||||||
|
const translatedNames = new Intl.DisplayNames([currentLanguageCode], { type: 'language' });
|
||||||
|
const translatedName = translatedNames.of(langCode)!;
|
||||||
|
|
||||||
|
const originalNames = new Intl.DisplayNames([langCode], { type: 'language' });
|
||||||
|
const originalName = originalNames.of(langCode)!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
langCode,
|
||||||
|
translatedName,
|
||||||
|
originalName,
|
||||||
|
} satisfies LanguageItem;
|
||||||
|
}), [currentLanguageCode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) setSearch('');
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const filteredDisplayedLanguages = useMemo(() => {
|
||||||
|
if (!search.trim()) {
|
||||||
|
return translateLanguages;
|
||||||
|
}
|
||||||
|
|
||||||
|
return translateLanguages.filter(({ langCode, translatedName, originalName }) => (
|
||||||
|
translatedName.toLowerCase().includes(search.toLowerCase())
|
||||||
|
|| originalName.toLowerCase().includes(search.toLowerCase())
|
||||||
|
|| langCode.toLowerCase().includes(search.toLowerCase())
|
||||||
|
));
|
||||||
|
}, [translateLanguages, search]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className={styles.root}
|
||||||
|
isSlim
|
||||||
|
isOpen={isOpen}
|
||||||
|
hasCloseButton
|
||||||
|
title={lang('Language')}
|
||||||
|
onClose={closeMessageLanguageModal}
|
||||||
|
>
|
||||||
|
<InputText
|
||||||
|
key="search"
|
||||||
|
value={search}
|
||||||
|
onChange={handleSearch}
|
||||||
|
placeholder={lang('Search')}
|
||||||
|
teactExperimentControlled
|
||||||
|
/>
|
||||||
|
<div className={buildClassName(styles.languages, 'custom-scroll')}>
|
||||||
|
{filteredDisplayedLanguages.map(({ langCode, originalName, translatedName }) => (
|
||||||
|
<ListItem
|
||||||
|
key={langCode}
|
||||||
|
className={styles.listItem}
|
||||||
|
secondaryIcon={activeTranslationLanguage === langCode ? 'check' : undefined}
|
||||||
|
disabled={activeTranslationLanguage === langCode}
|
||||||
|
multiline
|
||||||
|
narrow
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => handleSelect(langCode)}
|
||||||
|
>
|
||||||
|
<span className={buildClassName('title', styles.title)}>
|
||||||
|
{renderText(originalName, ['highlight'], { highlight: search })}
|
||||||
|
</span>
|
||||||
|
<span className={buildClassName('subtitle', styles.subtitle)}>
|
||||||
|
{renderText(translatedName, ['highlight'], { highlight: search })}
|
||||||
|
</span>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): StateProps => {
|
||||||
|
const { chatId, messageId } = selectTabState(global).messageLanguageModal || {};
|
||||||
|
|
||||||
|
const currentLanguageCode = selectLanguageCode(global);
|
||||||
|
const activeTranslationLanguage = chatId && messageId
|
||||||
|
? selectRequestedTranslationLanguage(global, chatId, messageId) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
activeTranslationLanguage,
|
||||||
|
currentLanguageCode,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(MessageLanguageModal));
|
||||||
@ -77,6 +77,7 @@ import SeenByModal from '../common/SeenByModal.async';
|
|||||||
import EmojiInteractionAnimation from './EmojiInteractionAnimation.async';
|
import EmojiInteractionAnimation from './EmojiInteractionAnimation.async';
|
||||||
import ReactorListModal from './ReactorListModal.async';
|
import ReactorListModal from './ReactorListModal.async';
|
||||||
import GiftPremiumModal from '../main/premium/GiftPremiumModal.async';
|
import GiftPremiumModal from '../main/premium/GiftPremiumModal.async';
|
||||||
|
import MessageLanguageModal from './MessageLanguageModal.async';
|
||||||
|
|
||||||
import './MiddleColumn.scss';
|
import './MiddleColumn.scss';
|
||||||
import styles from './MiddleColumn.module.scss';
|
import styles from './MiddleColumn.module.scss';
|
||||||
@ -111,6 +112,7 @@ type StateProps = {
|
|||||||
isSeenByModalOpen: boolean;
|
isSeenByModalOpen: boolean;
|
||||||
isReactorListModalOpen: boolean;
|
isReactorListModalOpen: boolean;
|
||||||
isGiftPremiumModalOpen?: boolean;
|
isGiftPremiumModalOpen?: boolean;
|
||||||
|
isMessageLanguageModalOpen?: boolean;
|
||||||
animationLevel: AnimationLevel;
|
animationLevel: AnimationLevel;
|
||||||
shouldSkipHistoryAnimations?: boolean;
|
shouldSkipHistoryAnimations?: boolean;
|
||||||
currentTransitionKey: number;
|
currentTransitionKey: number;
|
||||||
@ -156,6 +158,7 @@ const MiddleColumn: FC<OwnProps & StateProps> = ({
|
|||||||
isSeenByModalOpen,
|
isSeenByModalOpen,
|
||||||
isReactorListModalOpen,
|
isReactorListModalOpen,
|
||||||
isGiftPremiumModalOpen,
|
isGiftPremiumModalOpen,
|
||||||
|
isMessageLanguageModalOpen,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
currentTransitionKey,
|
currentTransitionKey,
|
||||||
@ -551,6 +554,7 @@ const MiddleColumn: FC<OwnProps & StateProps> = ({
|
|||||||
/>
|
/>
|
||||||
<SeenByModal isOpen={isSeenByModalOpen} />
|
<SeenByModal isOpen={isSeenByModalOpen} />
|
||||||
<ReactorListModal isOpen={isReactorListModalOpen} />
|
<ReactorListModal isOpen={isReactorListModalOpen} />
|
||||||
|
<MessageLanguageModal isOpen={isMessageLanguageModalOpen} />
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
@ -596,6 +600,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const {
|
const {
|
||||||
messageLists, isLeftColumnShown, activeEmojiInteractions,
|
messageLists, isLeftColumnShown, activeEmojiInteractions,
|
||||||
seenByModal, giftPremiumModal, reactorModal, audioPlayer, shouldSkipHistoryAnimations,
|
seenByModal, giftPremiumModal, reactorModal, audioPlayer, shouldSkipHistoryAnimations,
|
||||||
|
messageLanguageModal,
|
||||||
} = selectTabState(global);
|
} = selectTabState(global);
|
||||||
const currentMessageList = selectCurrentMessageList(global);
|
const currentMessageList = selectCurrentMessageList(global);
|
||||||
const { chats: { listIds }, lastSyncTime } = global;
|
const { chats: { listIds }, lastSyncTime } = global;
|
||||||
@ -613,6 +618,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isSeenByModalOpen: Boolean(seenByModal),
|
isSeenByModalOpen: Boolean(seenByModal),
|
||||||
isReactorListModalOpen: Boolean(reactorModal),
|
isReactorListModalOpen: Boolean(reactorModal),
|
||||||
isGiftPremiumModalOpen: giftPremiumModal?.isOpen,
|
isGiftPremiumModalOpen: giftPremiumModal?.isOpen,
|
||||||
|
isMessageLanguageModalOpen: Boolean(messageLanguageModal),
|
||||||
animationLevel: global.settings.byKey.animationLevel,
|
animationLevel: global.settings.byKey.animationLevel,
|
||||||
currentTransitionKey: Math.max(0, messageLists.length - 1),
|
currentTransitionKey: Math.max(0, messageLists.length - 1),
|
||||||
activeEmojiInteractions,
|
activeEmojiInteractions,
|
||||||
|
|||||||
@ -20,6 +20,8 @@ import {
|
|||||||
selectIsMessageProtected,
|
selectIsMessageProtected,
|
||||||
selectIsPremiumPurchaseBlocked,
|
selectIsPremiumPurchaseBlocked,
|
||||||
selectMessageCustomEmojiSets,
|
selectMessageCustomEmojiSets,
|
||||||
|
selectMessageTranslations,
|
||||||
|
selectRequestedTranslationLanguage,
|
||||||
selectStickerSet,
|
selectStickerSet,
|
||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
import {
|
import {
|
||||||
@ -27,6 +29,7 @@ import {
|
|||||||
isChatGroup, isOwnMessage, areReactionsEmpty, isUserId, isMessageLocal, getMessageVideo, getChatMessageLink,
|
isChatGroup, isOwnMessage, areReactionsEmpty, isUserId, isMessageLocal, getMessageVideo, getChatMessageLink,
|
||||||
} from '../../../global/helpers';
|
} from '../../../global/helpers';
|
||||||
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
|
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
|
||||||
|
import { IS_TRANSLATION_SUPPORTED } from '../../../util/environment';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||||
|
|
||||||
@ -49,6 +52,7 @@ export type OwnProps = {
|
|||||||
anchor: IAnchorPosition;
|
anchor: IAnchorPosition;
|
||||||
messageListType: MessageListType;
|
messageListType: MessageListType;
|
||||||
noReplies?: boolean;
|
noReplies?: boolean;
|
||||||
|
detectedLanguage?: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onCloseAnimationEnd: () => void;
|
onCloseAnimationEnd: () => void;
|
||||||
repliesThreadInfo?: ApiThreadInfo;
|
repliesThreadInfo?: ApiThreadInfo;
|
||||||
@ -74,6 +78,9 @@ type StateProps = {
|
|||||||
canFaveSticker?: boolean;
|
canFaveSticker?: boolean;
|
||||||
canUnfaveSticker?: boolean;
|
canUnfaveSticker?: boolean;
|
||||||
canCopy?: boolean;
|
canCopy?: boolean;
|
||||||
|
canTranslate?: boolean;
|
||||||
|
canShowOriginal?: boolean;
|
||||||
|
canSelectLanguage?: boolean;
|
||||||
isPrivate?: boolean;
|
isPrivate?: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
hasFullInfo?: boolean;
|
hasFullInfo?: boolean;
|
||||||
@ -101,8 +108,6 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
customEmojiSets,
|
customEmojiSets,
|
||||||
album,
|
album,
|
||||||
anchor,
|
anchor,
|
||||||
onClose,
|
|
||||||
onCloseAnimationEnd,
|
|
||||||
noOptions,
|
noOptions,
|
||||||
canSendNow,
|
canSendNow,
|
||||||
hasFullInfo,
|
hasFullInfo,
|
||||||
@ -135,7 +140,12 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
noReplies,
|
noReplies,
|
||||||
canShowSeenBy,
|
canShowSeenBy,
|
||||||
canScheduleUntilOnline,
|
canScheduleUntilOnline,
|
||||||
|
canTranslate,
|
||||||
|
canShowOriginal,
|
||||||
|
canSelectLanguage,
|
||||||
threadId,
|
threadId,
|
||||||
|
onClose,
|
||||||
|
onCloseAnimationEnd,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
openChat,
|
openChat,
|
||||||
@ -161,6 +171,9 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
cancelPollVote,
|
cancelPollVote,
|
||||||
closePoll,
|
closePoll,
|
||||||
toggleReaction,
|
toggleReaction,
|
||||||
|
requestMessageTranslation,
|
||||||
|
showOriginalMessage,
|
||||||
|
openMessageLanguageModal,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -393,6 +406,30 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
closeMenu();
|
closeMenu();
|
||||||
}, [closeMenu, message, toggleReaction]);
|
}, [closeMenu, message, toggleReaction]);
|
||||||
|
|
||||||
|
const handleTranslate = useCallback(() => {
|
||||||
|
requestMessageTranslation({
|
||||||
|
chatId: message.chatId,
|
||||||
|
id: message.id,
|
||||||
|
});
|
||||||
|
closeMenu();
|
||||||
|
}, [closeMenu, message, requestMessageTranslation]);
|
||||||
|
|
||||||
|
const handleShowOriginal = useCallback(() => {
|
||||||
|
showOriginalMessage({
|
||||||
|
chatId: message.chatId,
|
||||||
|
id: message.id,
|
||||||
|
});
|
||||||
|
closeMenu();
|
||||||
|
}, [closeMenu, message, showOriginalMessage]);
|
||||||
|
|
||||||
|
const handleSelectLanguage = useCallback(() => {
|
||||||
|
openMessageLanguageModal({
|
||||||
|
chatId: message.chatId,
|
||||||
|
id: message.id,
|
||||||
|
});
|
||||||
|
closeMenu();
|
||||||
|
}, [closeMenu, message.chatId, message.id, openMessageLanguageModal]);
|
||||||
|
|
||||||
const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]);
|
const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]);
|
||||||
|
|
||||||
if (noOptions) {
|
if (noOptions) {
|
||||||
@ -438,6 +475,9 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
canRevote={canRevote}
|
canRevote={canRevote}
|
||||||
canClosePoll={canClosePoll}
|
canClosePoll={canClosePoll}
|
||||||
canShowSeenBy={canShowSeenBy}
|
canShowSeenBy={canShowSeenBy}
|
||||||
|
canTranslate={canTranslate}
|
||||||
|
canShowOriginal={canShowOriginal}
|
||||||
|
canSelectLanguage={canSelectLanguage}
|
||||||
hasCustomEmoji={hasCustomEmoji}
|
hasCustomEmoji={hasCustomEmoji}
|
||||||
customEmojiSets={customEmojiSets}
|
customEmojiSets={customEmojiSets}
|
||||||
isDownloading={isDownloading}
|
isDownloading={isDownloading}
|
||||||
@ -467,6 +507,9 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
onShowSeenBy={handleOpenSeenByModal}
|
onShowSeenBy={handleOpenSeenByModal}
|
||||||
onToggleReaction={handleToggleReaction}
|
onToggleReaction={handleToggleReaction}
|
||||||
onShowReactors={handleOpenReactorListModal}
|
onShowReactors={handleOpenReactorListModal}
|
||||||
|
onTranslate={handleTranslate}
|
||||||
|
onShowOriginal={handleShowOriginal}
|
||||||
|
onSelectLanguage={handleSelectLanguage}
|
||||||
/>
|
/>
|
||||||
<DeleteMessageModal
|
<DeleteMessageModal
|
||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
@ -499,7 +542,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { message, messageListType }): StateProps => {
|
(global, { message, messageListType, detectedLanguage }): StateProps => {
|
||||||
const { threadId } = selectCurrentMessageList(global) || {};
|
const { threadId } = selectCurrentMessageList(global) || {};
|
||||||
const activeDownloads = selectActiveDownloadIds(global, message.chatId);
|
const activeDownloads = selectActiveDownloadIds(global, message.chatId);
|
||||||
const chat = selectChat(global, message.chatId);
|
const chat = selectChat(global, message.chatId);
|
||||||
@ -524,6 +567,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canClosePoll,
|
canClosePoll,
|
||||||
} = (threadId && selectAllowedMessageActions(global, message, threadId)) || {};
|
} = (threadId && selectAllowedMessageActions(global, message, threadId)) || {};
|
||||||
|
|
||||||
|
const isOwn = isOwnMessage(message);
|
||||||
const isPinned = messageListType === 'pinned';
|
const isPinned = messageListType === 'pinned';
|
||||||
const isScheduled = messageListType === 'scheduled';
|
const isScheduled = messageListType === 'scheduled';
|
||||||
const isChannel = chat && isChatChannel(chat);
|
const isChannel = chat && isChatChannel(chat);
|
||||||
@ -532,7 +576,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
&& seenByMaxChatMembers
|
&& seenByMaxChatMembers
|
||||||
&& seenByExpiresAt
|
&& seenByExpiresAt
|
||||||
&& isChatGroup(chat)
|
&& isChatGroup(chat)
|
||||||
&& isOwnMessage(message)
|
&& isOwn
|
||||||
&& !isScheduled
|
&& !isScheduled
|
||||||
&& chat.membersCount
|
&& chat.membersCount
|
||||||
&& chat.membersCount <= seenByMaxChatMembers
|
&& chat.membersCount <= seenByMaxChatMembers
|
||||||
@ -550,6 +594,18 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const customEmojiSets = customEmojiSetsNotFiltered?.every<ApiStickerSet>(Boolean)
|
const customEmojiSets = customEmojiSetsNotFiltered?.every<ApiStickerSet>(Boolean)
|
||||||
? customEmojiSetsNotFiltered : undefined;
|
? customEmojiSetsNotFiltered : undefined;
|
||||||
|
|
||||||
|
const translationRequestLanguage = selectRequestedTranslationLanguage(global, message.chatId, message.id);
|
||||||
|
const hasTranslation = translationRequestLanguage
|
||||||
|
? Boolean(selectMessageTranslations(global, message.chatId, translationRequestLanguage)[message.id]?.text)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const { canTranslate: isTranslationEnabled, language, doNotTranslate } = global.settings.byKey;
|
||||||
|
|
||||||
|
const canTranslateLanguage = !detectedLanguage
|
||||||
|
|| (!doNotTranslate.includes(detectedLanguage) && language !== detectedLanguage);
|
||||||
|
const canTranslate = IS_TRANSLATION_SUPPORTED && isTranslationEnabled && message.content.text
|
||||||
|
&& canTranslateLanguage && !isLocal && !isScheduled && !isAction && !hasTranslation && !message.emojiOnlyCount;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
availableReactions: global.availableReactions,
|
availableReactions: global.availableReactions,
|
||||||
noOptions,
|
noOptions,
|
||||||
@ -585,6 +641,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
customEmojiSets,
|
customEmojiSets,
|
||||||
canScheduleUntilOnline: selectCanScheduleUntilOnline(global, message.chatId),
|
canScheduleUntilOnline: selectCanScheduleUntilOnline(global, message.chatId),
|
||||||
threadId,
|
threadId,
|
||||||
|
canTranslate,
|
||||||
|
canShowOriginal: hasTranslation,
|
||||||
|
canSelectLanguage: hasTranslation,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(ContextMenuContainer));
|
)(ContextMenuContainer));
|
||||||
|
|||||||
@ -9,7 +9,9 @@ import React, {
|
|||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ActiveEmojiInteraction, ActiveReaction, MessageListType } from '../../../global/types';
|
import type {
|
||||||
|
ActiveEmojiInteraction, ActiveReaction, ChatTranslatedMessages, MessageListType,
|
||||||
|
} from '../../../global/types';
|
||||||
import type {
|
import type {
|
||||||
ApiMessage,
|
ApiMessage,
|
||||||
ApiMessageOutgoingStatus,
|
ApiMessageOutgoingStatus,
|
||||||
@ -63,6 +65,8 @@ import {
|
|||||||
selectIsChatProtected,
|
selectIsChatProtected,
|
||||||
selectTopicFromMessage,
|
selectTopicFromMessage,
|
||||||
selectTabState,
|
selectTabState,
|
||||||
|
selectChatTranslations,
|
||||||
|
selectRequestedTranslationLanguage,
|
||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
import {
|
import {
|
||||||
getMessageContent,
|
getMessageContent,
|
||||||
@ -113,6 +117,9 @@ import useInnerHandlers from './hooks/useInnerHandlers';
|
|||||||
import useAppLayout from '../../../hooks/useAppLayout';
|
import useAppLayout from '../../../hooks/useAppLayout';
|
||||||
import useResizeObserver from '../../../hooks/useResizeObserver';
|
import useResizeObserver from '../../../hooks/useResizeObserver';
|
||||||
import useThrottledCallback from '../../../hooks/useThrottledCallback';
|
import useThrottledCallback from '../../../hooks/useThrottledCallback';
|
||||||
|
import useMessageTranslation from './hooks/useMessageTranslation';
|
||||||
|
import usePrevious from '../../../hooks/usePrevious';
|
||||||
|
import useTextLanguage from '../../../hooks/useTextLanguage';
|
||||||
|
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
@ -233,6 +240,9 @@ type StateProps = {
|
|||||||
senderAdminMember?: ApiChatMember;
|
senderAdminMember?: ApiChatMember;
|
||||||
messageTopic?: ApiTopic;
|
messageTopic?: ApiTopic;
|
||||||
hasTopicChip?: boolean;
|
hasTopicChip?: boolean;
|
||||||
|
chatTranslations?: ChatTranslatedMessages;
|
||||||
|
areTranslationsEnabled?: boolean;
|
||||||
|
requestedTranslationLanguage?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MetaPosition =
|
type MetaPosition =
|
||||||
@ -329,6 +339,9 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
senderAdminMember,
|
senderAdminMember,
|
||||||
messageTopic,
|
messageTopic,
|
||||||
hasTopicChip,
|
hasTopicChip,
|
||||||
|
chatTranslations,
|
||||||
|
areTranslationsEnabled,
|
||||||
|
requestedTranslationLanguage,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
toggleMessageSelection,
|
toggleMessageSelection,
|
||||||
@ -468,6 +481,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
handleAudioPlay,
|
handleAudioPlay,
|
||||||
handleAlbumMediaClick,
|
handleAlbumMediaClick,
|
||||||
handleMetaClick,
|
handleMetaClick,
|
||||||
|
handleTranslationClick,
|
||||||
handleOpenThread,
|
handleOpenThread,
|
||||||
handleReadMedia,
|
handleReadMedia,
|
||||||
handleCancelUpload,
|
handleCancelUpload,
|
||||||
@ -537,6 +551,17 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice, location, action, game,
|
text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice, location, action, game,
|
||||||
} = getMessageContent(message);
|
} = getMessageContent(message);
|
||||||
|
|
||||||
|
const { result: detectedLanguage } = useTextLanguage(areTranslationsEnabled ? text?.text : undefined);
|
||||||
|
|
||||||
|
const { isPending: isTranslationPending, translatedText } = useMessageTranslation(
|
||||||
|
chatTranslations, chatId, messageId, requestedTranslationLanguage,
|
||||||
|
);
|
||||||
|
// Used to display previous result while new one is loading
|
||||||
|
const previousTranslatedText = usePrevious(translatedText, true);
|
||||||
|
|
||||||
|
const currentText = isTranslationPending ? (previousTranslatedText || text) : translatedText;
|
||||||
|
const currentTranslatedText = translatedText || previousTranslatedText;
|
||||||
|
|
||||||
const { phoneCall } = action || {};
|
const { phoneCall } = action || {};
|
||||||
|
|
||||||
const withCommentButton = repliesThreadInfo && !isInDocumentGroupNotLast && messageListType === 'thread'
|
const withCommentButton = repliesThreadInfo && !isInDocumentGroupNotLast && messageListType === 'thread'
|
||||||
@ -663,13 +688,15 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (width) {
|
if (width) {
|
||||||
calculatedWidth = Math.max(getMinMediaWidth(Boolean(text), withCommentButton), width);
|
calculatedWidth = Math.max(getMinMediaWidth(Boolean(currentText), withCommentButton), width);
|
||||||
if (invoice?.extendedMedia && calculatedWidth - width > NO_MEDIA_CORNERS_THRESHOLD) {
|
if (invoice?.extendedMedia && calculatedWidth - width > NO_MEDIA_CORNERS_THRESHOLD) {
|
||||||
noMediaCorners = true;
|
noMediaCorners = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (albumLayout) {
|
} else if (albumLayout) {
|
||||||
calculatedWidth = Math.max(getMinMediaWidth(Boolean(text), withCommentButton), albumLayout.containerStyle.width);
|
calculatedWidth = Math.max(
|
||||||
|
getMinMediaWidth(Boolean(currentText), withCommentButton), albumLayout.containerStyle.width,
|
||||||
|
);
|
||||||
if (calculatedWidth - albumLayout.containerStyle.width > NO_MEDIA_CORNERS_THRESHOLD) {
|
if (calculatedWidth - albumLayout.containerStyle.width > NO_MEDIA_CORNERS_THRESHOLD) {
|
||||||
noMediaCorners = true;
|
noMediaCorners = true;
|
||||||
}
|
}
|
||||||
@ -707,6 +734,22 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMessageText(isForAnimation?: boolean) {
|
||||||
|
return (
|
||||||
|
<MessageText
|
||||||
|
message={message}
|
||||||
|
translatedText={requestedTranslationLanguage ? currentTranslatedText : undefined}
|
||||||
|
isForAnimation={isForAnimation}
|
||||||
|
emojiSize={emojiSize}
|
||||||
|
highlight={highlight}
|
||||||
|
isProtected={isProtected}
|
||||||
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||||
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||||
|
withTranslucentThumbs={isCustomShape}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function renderReactionsAndMeta() {
|
function renderReactionsAndMeta() {
|
||||||
const meta = (
|
const meta = (
|
||||||
<MessageMeta
|
<MessageMeta
|
||||||
@ -717,7 +760,9 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
signature={signature}
|
signature={signature}
|
||||||
withReactionOffset={reactionsPosition === 'inside'}
|
withReactionOffset={reactionsPosition === 'inside'}
|
||||||
availableReactions={availableReactions}
|
availableReactions={availableReactions}
|
||||||
|
isTranslated={Boolean(requestedTranslationLanguage ? currentTranslatedText : undefined)}
|
||||||
onClick={handleMetaClick}
|
onClick={handleMetaClick}
|
||||||
|
onTranslationClick={handleTranslationClick}
|
||||||
onOpenThread={handleOpenThread}
|
onOpenThread={handleOpenThread}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -749,6 +794,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
const hasCustomAppendix = isLastInGroup && !hasText && !asForwarded && !hasThread;
|
const hasCustomAppendix = isLastInGroup && !hasText && !asForwarded && !hasThread;
|
||||||
const textContentClass = buildClassName(
|
const textContentClass = buildClassName(
|
||||||
'text-content',
|
'text-content',
|
||||||
|
'clearfix',
|
||||||
metaPosition === 'in-text' && 'with-meta',
|
metaPosition === 'in-text' && 'with-meta',
|
||||||
outgoingStatus && 'with-outgoing-icon',
|
outgoingStatus && 'with-outgoing-icon',
|
||||||
);
|
);
|
||||||
@ -954,15 +1000,14 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
{!hasAnimatedEmoji && hasText && (
|
{!hasAnimatedEmoji && hasText && (
|
||||||
<div className={textContentClass} dir="auto">
|
<div className={textContentClass} dir="auto">
|
||||||
<MessageText
|
{renderMessageText()}
|
||||||
message={message}
|
{isTranslationPending && (
|
||||||
emojiSize={emojiSize}
|
<div className="translation-animation">
|
||||||
highlight={highlight}
|
<div className="text-loading">
|
||||||
isProtected={isProtected}
|
{renderMessageText(true)}
|
||||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
</div>
|
||||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
</div>
|
||||||
withTranslucentThumbs={isCustomShape}
|
)}
|
||||||
/>
|
|
||||||
{metaPosition === 'in-text' && renderReactionsAndMeta()}
|
{metaPosition === 'in-text' && renderReactionsAndMeta()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -1208,6 +1253,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
onCloseAnimationEnd={handleContextMenuHide}
|
onCloseAnimationEnd={handleContextMenuHide}
|
||||||
repliesThreadInfo={repliesThreadInfo}
|
repliesThreadInfo={repliesThreadInfo}
|
||||||
noReplies={noReplies}
|
noReplies={noReplies}
|
||||||
|
detectedLanguage={detectedLanguage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -1300,6 +1346,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const isLocation = Boolean(getMessageLocation(message));
|
const isLocation = Boolean(getMessageLocation(message));
|
||||||
|
const chatTranslations = selectChatTranslations(global, chatId);
|
||||||
|
const requestedTranslationLanguage = selectRequestedTranslationLanguage(global, chatId, message.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme: selectTheme(global),
|
theme: selectTheme(global),
|
||||||
@ -1355,6 +1403,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
messageTopic,
|
messageTopic,
|
||||||
genericEffects: global.genericEmojiEffects,
|
genericEffects: global.genericEmojiEffects,
|
||||||
hasTopicChip,
|
hasTopicChip,
|
||||||
|
chatTranslations,
|
||||||
|
areTranslationsEnabled: global.settings.byKey.canTranslate,
|
||||||
|
requestedTranslationLanguage,
|
||||||
...((canShowSender || isLocation) && { sender }),
|
...((canShowSender || isLocation) && { sender }),
|
||||||
...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }),
|
...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }),
|
||||||
...(typeof uploadProgress === 'number' && { uploadProgress }),
|
...(typeof uploadProgress === 'number' && { uploadProgress }),
|
||||||
|
|||||||
@ -61,6 +61,9 @@ type OwnProps = {
|
|||||||
canCopy?: boolean;
|
canCopy?: boolean;
|
||||||
canCopyLink?: boolean;
|
canCopyLink?: boolean;
|
||||||
canSelect?: boolean;
|
canSelect?: boolean;
|
||||||
|
canTranslate?: boolean;
|
||||||
|
canShowOriginal?: boolean;
|
||||||
|
canSelectLanguage?: boolean;
|
||||||
isPrivate?: boolean;
|
isPrivate?: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
canDownload?: boolean;
|
canDownload?: boolean;
|
||||||
@ -99,6 +102,9 @@ type OwnProps = {
|
|||||||
onShowReactors?: () => void;
|
onShowReactors?: () => void;
|
||||||
onAboutAds?: () => void;
|
onAboutAds?: () => void;
|
||||||
onSponsoredHide?: () => void;
|
onSponsoredHide?: () => void;
|
||||||
|
onTranslate?: () => void;
|
||||||
|
onShowOriginal?: () => void;
|
||||||
|
onSelectLanguage?: () => void;
|
||||||
onToggleReaction?: (reaction: ApiReaction) => void;
|
onToggleReaction?: (reaction: ApiReaction) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -135,6 +141,9 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
canSaveGif,
|
canSaveGif,
|
||||||
canRevote,
|
canRevote,
|
||||||
canClosePoll,
|
canClosePoll,
|
||||||
|
canTranslate,
|
||||||
|
canShowOriginal,
|
||||||
|
canSelectLanguage,
|
||||||
isDownloading,
|
isDownloading,
|
||||||
repliesThreadInfo,
|
repliesThreadInfo,
|
||||||
canShowSeenBy,
|
canShowSeenBy,
|
||||||
@ -170,6 +179,9 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
onCopyMessages,
|
onCopyMessages,
|
||||||
onAboutAds,
|
onAboutAds,
|
||||||
onSponsoredHide,
|
onSponsoredHide,
|
||||||
|
onTranslate,
|
||||||
|
onShowOriginal,
|
||||||
|
onSelectLanguage,
|
||||||
}) => {
|
}) => {
|
||||||
const { showNotification, openStickerSet, openCustomEmojiSets } = getActions();
|
const { showNotification, openStickerSet, openCustomEmojiSets } = getActions();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -319,6 +331,11 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
{canUnfaveSticker && (
|
{canUnfaveSticker && (
|
||||||
<MenuItem icon="favorite" onClick={onUnfaveSticker}>{lang('Stickers.RemoveFromFavorites')}</MenuItem>
|
<MenuItem icon="favorite" onClick={onUnfaveSticker}>{lang('Stickers.RemoveFromFavorites')}</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
{canTranslate && <MenuItem icon="language" onClick={onTranslate}>{lang('TranslateMessage')}</MenuItem>}
|
||||||
|
{canShowOriginal && <MenuItem icon="language" onClick={onShowOriginal}>{lang('ShowOriginalButton')}</MenuItem>}
|
||||||
|
{canSelectLanguage && (
|
||||||
|
<MenuItem icon="web" onClick={onSelectLanguage}>{lang('lng_settings_change_lang')}</MenuItem>
|
||||||
|
)}
|
||||||
{canCopy && copyOptions.map((option) => (
|
{canCopy && copyOptions.map((option) => (
|
||||||
<MenuItem key={option.label} icon={option.icon} onClick={option.handler}>{lang(option.label)}</MenuItem>
|
<MenuItem key={option.label} icon={option.icon} onClick={option.handler}>{lang(option.label)}</MenuItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -17,7 +17,8 @@
|
|||||||
.message-imported,
|
.message-imported,
|
||||||
.message-signature,
|
.message-signature,
|
||||||
.message-views,
|
.message-views,
|
||||||
.message-replies {
|
.message-replies,
|
||||||
|
.message-translated {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@ -36,6 +37,10 @@
|
|||||||
margin-inline-start: 0.1875rem;
|
margin-inline-start: 0.1875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-translated {
|
||||||
|
margin-inline-end: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.message-imported,
|
.message-imported,
|
||||||
.message-signature {
|
.message-signature {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@ -27,8 +27,10 @@ type OwnProps = {
|
|||||||
availableReactions?: ApiAvailableReaction[];
|
availableReactions?: ApiAvailableReaction[];
|
||||||
noReplies?: boolean;
|
noReplies?: boolean;
|
||||||
repliesThreadInfo?: ApiThreadInfo;
|
repliesThreadInfo?: ApiThreadInfo;
|
||||||
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
isTranslated?: boolean;
|
||||||
onOpenThread: () => void;
|
onClick: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
|
onTranslationClick: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
|
onOpenThread: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageMeta: FC<OwnProps> = ({
|
const MessageMeta: FC<OwnProps> = ({
|
||||||
@ -38,7 +40,9 @@ const MessageMeta: FC<OwnProps> = ({
|
|||||||
withReactionOffset,
|
withReactionOffset,
|
||||||
repliesThreadInfo,
|
repliesThreadInfo,
|
||||||
noReplies,
|
noReplies,
|
||||||
|
isTranslated,
|
||||||
onClick,
|
onClick,
|
||||||
|
onTranslationClick,
|
||||||
onOpenThread,
|
onOpenThread,
|
||||||
}) => {
|
}) => {
|
||||||
const { showNotification } = getActions();
|
const { showNotification } = getActions();
|
||||||
@ -90,6 +94,9 @@ const MessageMeta: FC<OwnProps> = ({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
data-ignore-on-paste
|
data-ignore-on-paste
|
||||||
>
|
>
|
||||||
|
{isTranslated && (
|
||||||
|
<i className="icon-language message-translated" onClick={onTranslationClick} />
|
||||||
|
)}
|
||||||
{Boolean(message.views) && (
|
{Boolean(message.views) && (
|
||||||
<>
|
<>
|
||||||
<span className="message-views">
|
<span className="message-views">
|
||||||
|
|||||||
@ -41,8 +41,10 @@
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
line-height: 1.3125;
|
line-height: 1.3125;
|
||||||
text-align: initial;
|
text-align: initial;
|
||||||
display: flow-root;
|
display: block;
|
||||||
unicode-bidi: plaintext;
|
unicode-bidi: plaintext;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transcription {
|
.transcription {
|
||||||
@ -68,6 +70,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.translation-animation {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-loading {
|
||||||
|
--background-gradient-size: 20rem;
|
||||||
|
--animation-color: var(--color-skeleton-background);
|
||||||
|
|
||||||
|
background-image: linear-gradient(to right, transparent 10%, var(--animation-color) 50%, transparent 90%);
|
||||||
|
background-size: var(--background-gradient-size);
|
||||||
|
|
||||||
|
display: inline;
|
||||||
|
box-decoration-break: clone;
|
||||||
|
color: transparent;
|
||||||
|
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
|
||||||
|
animation: text-loading 1.5s linear infinite;
|
||||||
|
|
||||||
|
.theme-dark & {
|
||||||
|
--animation-color: var(--color-skeleton-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.text-entity-link {
|
.text-entity-link {
|
||||||
unicode-bidi: plaintext;
|
unicode-bidi: plaintext;
|
||||||
}
|
}
|
||||||
@ -735,7 +771,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.text-content {
|
.text-content {
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
line-height: var(--emoji-only-size);
|
line-height: var(--emoji-only-size);
|
||||||
font-size: var(--emoji-only-size);
|
font-size: var(--emoji-only-size);
|
||||||
@ -747,7 +782,7 @@
|
|||||||
|
|
||||||
.MessageMeta {
|
.MessageMeta {
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
bottom: 0;
|
bottom: -1.25rem;
|
||||||
right: 0;
|
right: 0;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
}
|
}
|
||||||
@ -903,3 +938,19 @@
|
|||||||
font-family: var(--font-family-monospace);
|
font-family: var(--font-family-monospace);
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes text-loading {
|
||||||
|
0% {
|
||||||
|
background-position-x: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position-x: var(--background-gradient-size);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export default function useInnerHandlers(
|
|||||||
const {
|
const {
|
||||||
openChat, showNotification, focusMessage, openMediaViewer, openAudioPlayer,
|
openChat, showNotification, focusMessage, openMediaViewer, openAudioPlayer,
|
||||||
markMessagesRead, cancelSendingMessage, sendPollVote, openForwardMenu, focusMessageInComments,
|
markMessagesRead, cancelSendingMessage, sendPollVote, openForwardMenu, focusMessageInComments,
|
||||||
|
openMessageLanguageModal,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -154,12 +155,18 @@ export default function useInnerHandlers(
|
|||||||
focusMessageInComments, replyToTopMessageId,
|
focusMessageInComments, replyToTopMessageId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const selectWithGroupedId = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
const selectWithGroupedId = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
selectMessage(e, groupedId);
|
selectMessage(e, groupedId);
|
||||||
}, [selectMessage, groupedId]);
|
}, [selectMessage, groupedId]);
|
||||||
|
|
||||||
|
const handleTranslationClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
openMessageLanguageModal({ chatId, id: messageId });
|
||||||
|
}, [chatId, messageId, openMessageLanguageModal]);
|
||||||
|
|
||||||
const handleOpenThread = useCallback(() => {
|
const handleOpenThread = useCallback(() => {
|
||||||
openChat({
|
openChat({
|
||||||
id: message.chatId,
|
id: message.chatId,
|
||||||
@ -185,6 +192,7 @@ export default function useInnerHandlers(
|
|||||||
handleAudioPlay,
|
handleAudioPlay,
|
||||||
handleAlbumMediaClick,
|
handleAlbumMediaClick,
|
||||||
handleMetaClick: selectWithGroupedId,
|
handleMetaClick: selectWithGroupedId,
|
||||||
|
handleTranslationClick,
|
||||||
handleOpenThread,
|
handleOpenThread,
|
||||||
handleReadMedia,
|
handleReadMedia,
|
||||||
handleCancelUpload,
|
handleCancelUpload,
|
||||||
|
|||||||
27
src/components/middle/message/hooks/useMessageTranslation.ts
Normal file
27
src/components/middle/message/hooks/useMessageTranslation.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useEffect } from '../../../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../../../global';
|
||||||
|
import type { ChatTranslatedMessages } from '../../../../global/types';
|
||||||
|
|
||||||
|
export default function useMessageTranslation(
|
||||||
|
chatTranslations: ChatTranslatedMessages | undefined,
|
||||||
|
chatId: string,
|
||||||
|
messageId: number,
|
||||||
|
requestedLanguageCode?: string,
|
||||||
|
) {
|
||||||
|
const { translateMessages } = getActions();
|
||||||
|
const messageTranslation = requestedLanguageCode
|
||||||
|
? chatTranslations?.byLangCode[requestedLanguageCode]?.[messageId] : undefined;
|
||||||
|
|
||||||
|
const { isPending, text } = messageTranslation || {};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!text && !isPending && requestedLanguageCode) {
|
||||||
|
translateMessages({ chatId, messageIds: [messageId], toLanguageCode: requestedLanguageCode });
|
||||||
|
}
|
||||||
|
}, [chatId, text, isPending, messageId, requestedLanguageCode, translateMessages]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPending,
|
||||||
|
translatedText: text,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -4,9 +4,10 @@ import React, { useCallback, memo, useState } from '../../lib/teact/teact';
|
|||||||
|
|
||||||
import Checkbox from './Checkbox';
|
import Checkbox from './Checkbox';
|
||||||
|
|
||||||
type IRadioOption = {
|
export type IRadioOption = {
|
||||||
label: string;
|
label: string;
|
||||||
subLabel?: string;
|
subLabel?: string;
|
||||||
|
disabled?: boolean;
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ const CheckboxGroup: FC<OwnProps> = ({
|
|||||||
subLabel={option.subLabel}
|
subLabel={option.subLabel}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
checked={selected.indexOf(option.value) !== -1}
|
checked={selected.indexOf(option.value) !== -1}
|
||||||
disabled={disabled}
|
disabled={option.disabled || disabled}
|
||||||
round={round}
|
round={round}
|
||||||
isLoading={loadingOptions ? loadingOptions.indexOf(option.value) !== -1 : undefined}
|
isLoading={loadingOptions ? loadingOptions.indexOf(option.value) !== -1 : undefined}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
|||||||
@ -28,6 +28,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.slim {
|
||||||
|
.modal-dialog {
|
||||||
|
max-width: 25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
max-height: min(92vh, 32rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.modal-container {
|
.modal-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@ -26,6 +26,7 @@ type OwnProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
header?: TeactNode;
|
header?: TeactNode;
|
||||||
|
isSlim?: boolean;
|
||||||
hasCloseButton?: boolean;
|
hasCloseButton?: boolean;
|
||||||
noBackdrop?: boolean;
|
noBackdrop?: boolean;
|
||||||
noBackdropClose?: boolean;
|
noBackdropClose?: boolean;
|
||||||
@ -46,6 +47,7 @@ const Modal: FC<OwnProps & StateProps> = ({
|
|||||||
title,
|
title,
|
||||||
className,
|
className,
|
||||||
isOpen,
|
isOpen,
|
||||||
|
isSlim,
|
||||||
header,
|
header,
|
||||||
hasCloseButton,
|
hasCloseButton,
|
||||||
noBackdrop,
|
noBackdrop,
|
||||||
@ -136,6 +138,7 @@ const Modal: FC<OwnProps & StateProps> = ({
|
|||||||
className,
|
className,
|
||||||
transitionClassNames,
|
transitionClassNames,
|
||||||
noBackdrop && 'transparent-backdrop',
|
noBackdrop && 'transparent-backdrop',
|
||||||
|
isSlim && 'slim',
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export const CUSTOM_EMOJI_PREVIEW_CACHE_DISABLED = false;
|
|||||||
export const CUSTOM_EMOJI_PREVIEW_CACHE_NAME = 'tt-custom-emoji-preview';
|
export const CUSTOM_EMOJI_PREVIEW_CACHE_NAME = 'tt-custom-emoji-preview';
|
||||||
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
|
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
|
||||||
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
|
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
|
||||||
export const LANG_CACHE_NAME = 'tt-lang-packs-v16';
|
export const LANG_CACHE_NAME = 'tt-lang-packs-v17';
|
||||||
export const ASSET_CACHE_NAME = 'tt-assets';
|
export const ASSET_CACHE_NAME = 'tt-assets';
|
||||||
export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
|
export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
|
||||||
export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global';
|
export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global';
|
||||||
@ -218,6 +218,25 @@ export const CONTENT_TYPES_WITH_PREVIEW = new Set([
|
|||||||
|
|
||||||
export const CONTENT_NOT_SUPPORTED = 'The message is not supported on this version of Telegram.';
|
export const CONTENT_NOT_SUPPORTED = 'The message is not supported on this version of Telegram.';
|
||||||
|
|
||||||
|
// Taken from https://github.com/telegramdesktop/tdesktop/blob/41d9a9fcbd0c809c60ddbd9350791b1436aff7d9/Telegram/SourceFiles/ui/boxes/choose_language_box.cpp#L28
|
||||||
|
export const SUPPORTED_TRANSLATION_LANGUAGES = [
|
||||||
|
// Official
|
||||||
|
'en', 'ar', 'be', 'ca', 'zh', 'nl', 'fr', 'de', 'id',
|
||||||
|
'it', 'ja', 'ko', 'pl', 'pt', 'ru', 'es', 'uk',
|
||||||
|
// Unnofficial
|
||||||
|
'af', 'sq', 'am', 'hy', 'az', 'eu', 'bn', 'bs', 'bg',
|
||||||
|
'ceb', 'zh-CN', 'zh-TW', 'co', 'hr', 'cs', 'da', 'eo',
|
||||||
|
'et', 'fi', 'fy', 'gl', 'ka', 'el', 'gu', 'ht', 'ha',
|
||||||
|
'haw', 'he', 'iw', 'hi', 'hmn', 'hu', 'is', 'ig', 'ga',
|
||||||
|
'jv', 'kn', 'kk', 'km', 'rw', 'ku', 'ky', 'lo', 'la',
|
||||||
|
'lv', 'lt', 'lb', 'mk', 'mg', 'ms', 'ml', 'mt', 'mi',
|
||||||
|
'mr', 'mn', 'my', 'ne', 'no', 'ny', 'or', 'ps', 'fa',
|
||||||
|
'pa', 'ro', 'sm', 'gd', 'sr', 'st', 'sn', 'sd', 'si',
|
||||||
|
'sk', 'sl', 'so', 'su', 'sw', 'sv', 'tl', 'tg', 'ta',
|
||||||
|
'tt', 'te', 'th', 'tr', 'tk', 'ur', 'ug', 'uz', 'vi',
|
||||||
|
'cy', 'xh', 'yi', 'yo', 'zu',
|
||||||
|
];
|
||||||
|
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
export const RE_LINK_TEMPLATE = '((ftp|https?):\\/\\/)?((www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,63})\\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)';
|
export const RE_LINK_TEMPLATE = '((ftp|https?):\\/\\/)?((www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,63})\\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)';
|
||||||
export const RE_MENTION_TEMPLATE = '(@[\\w\\d_-]+)';
|
export const RE_MENTION_TEMPLATE = '(@[\\w\\d_-]+)';
|
||||||
|
|||||||
@ -54,6 +54,9 @@ import {
|
|||||||
updateTopic,
|
updateTopic,
|
||||||
updateThreadInfo,
|
updateThreadInfo,
|
||||||
replaceTabThreadParam,
|
replaceTabThreadParam,
|
||||||
|
updateRequestedMessageTranslation,
|
||||||
|
removeRequestedMessageTranslation,
|
||||||
|
updateMessageTranslation,
|
||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
import {
|
import {
|
||||||
selectChat,
|
selectChat,
|
||||||
@ -80,7 +83,9 @@ import {
|
|||||||
selectIsCurrentUserPremium,
|
selectIsCurrentUserPremium,
|
||||||
selectForwardsContainVoiceMessages,
|
selectForwardsContainVoiceMessages,
|
||||||
selectTabState,
|
selectTabState,
|
||||||
selectThreadIdFromMessage, selectForwardsCanBeSentToChat,
|
selectThreadIdFromMessage,
|
||||||
|
selectLanguageCode,
|
||||||
|
selectForwardsCanBeSentToChat,
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import {
|
import {
|
||||||
debounce, onTickEnd, rafPromise,
|
debounce, onTickEnd, rafPromise,
|
||||||
@ -1411,6 +1416,49 @@ addActionHandler('forwardToSavedMessages', (global, actions, payload): ActionRet
|
|||||||
actions.forwardMessages({ isSilent: true, tabId });
|
actions.forwardMessages({ isSilent: true, tabId });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('requestMessageTranslation', (global, actions, payload): ActionReturnType => {
|
||||||
|
const {
|
||||||
|
chatId, id, toLanguageCode = selectLanguageCode(global), tabId = getCurrentTabId(),
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
global = updateRequestedMessageTranslation(global, chatId, id, toLanguageCode, tabId);
|
||||||
|
|
||||||
|
return global;
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('showOriginalMessage', (global, actions, payload): ActionReturnType => {
|
||||||
|
const {
|
||||||
|
chatId, id, tabId = getCurrentTabId(),
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
global = removeRequestedMessageTranslation(global, chatId, id, tabId);
|
||||||
|
|
||||||
|
return global;
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('translateMessages', (global, actions, payload): ActionReturnType => {
|
||||||
|
const {
|
||||||
|
chatId, messageIds, toLanguageCode = selectLanguageCode(global),
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
if (!chat) return undefined;
|
||||||
|
|
||||||
|
messageIds.forEach((id) => {
|
||||||
|
global = updateMessageTranslation(global, chatId, id, toLanguageCode, {
|
||||||
|
isPending: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
callApi('translateText', {
|
||||||
|
chat,
|
||||||
|
messageIds,
|
||||||
|
toLanguageCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
return global;
|
||||||
|
});
|
||||||
|
|
||||||
function countSortedIds(ids: number[], from: number, to: number) {
|
function countSortedIds(ids: number[], from: number, to: number) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,8 @@ import {
|
|||||||
updateThreadUnreadFromForwardedMessage,
|
updateThreadUnreadFromForwardedMessage,
|
||||||
updateTopic,
|
updateTopic,
|
||||||
deleteTopic,
|
deleteTopic,
|
||||||
|
updateMessageTranslations,
|
||||||
|
clearMessageTranslation,
|
||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
import {
|
import {
|
||||||
selectChatMessage,
|
selectChatMessage,
|
||||||
@ -211,6 +213,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
global = updateReactions(global, chatId, id, message.reactions, chat, newMessage.isOutgoing, currentMessage);
|
global = updateReactions(global, chatId, id, message.reactions, chat, newMessage.isOutgoing, currentMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.content?.text?.text !== currentMessage?.content?.text?.text) {
|
||||||
|
global = clearMessageTranslation(global, chatId, id);
|
||||||
|
}
|
||||||
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -639,6 +645,18 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
|
|
||||||
global = updateChatMessage(global, chatId, localId, { sendingState: 'messageSendingStateFailed' });
|
global = updateChatMessage(global, chatId, localId, { sendingState: 'messageSendingStateFailed' });
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'updateMessageTranslations': {
|
||||||
|
const {
|
||||||
|
chatId, messageIds, toLanguageCode, translations,
|
||||||
|
} = update;
|
||||||
|
|
||||||
|
global = updateMessageTranslations(global, chatId, messageIds, toLanguageCode, translations);
|
||||||
|
|
||||||
|
setGlobal(global);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -878,6 +896,8 @@ function deleteMessages<T extends GlobalState>(
|
|||||||
isDeleting: true,
|
isDeleting: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
global = clearMessageTranslation(global, chatId, id);
|
||||||
|
|
||||||
const newLastMessage = findLastMessage(global, chatId);
|
const newLastMessage = findLastMessage(global, chatId);
|
||||||
if (newLastMessage) {
|
if (newLastMessage) {
|
||||||
global = updateChatLastMessage(global, chatId, newLastMessage, true);
|
global = updateChatLastMessage(global, chatId, newLastMessage, true);
|
||||||
|
|||||||
@ -39,8 +39,17 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
|
|
||||||
case 'updateUser': {
|
case 'updateUser': {
|
||||||
Object.values(global.byTabId).forEach(({ id: tabId }) => {
|
Object.values(global.byTabId).forEach(({ id: tabId }) => {
|
||||||
if (update.id === global.currentUserId && update.user.isPremium && !selectIsCurrentUserPremium(global)) {
|
if (update.id === global.currentUserId && update.user.isPremium !== selectIsCurrentUserPremium(global)) {
|
||||||
actions.openPremiumModal({ isSuccess: true, tabId });
|
// TODO Do not display modal if premium is bought from another device
|
||||||
|
if (update.user.isPremium) actions.openPremiumModal({ isSuccess: true, tabId });
|
||||||
|
|
||||||
|
// Reset translation cache cause premium provides additional formatting
|
||||||
|
global = {
|
||||||
|
...global,
|
||||||
|
translations: {
|
||||||
|
byChatId: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,7 @@ import {
|
|||||||
selectSender,
|
selectSender,
|
||||||
selectChatScheduledMessages,
|
selectChatScheduledMessages,
|
||||||
selectTabState,
|
selectTabState,
|
||||||
|
selectRequestedTranslationLanguage,
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import { compact, findLast } from '../../../util/iteratees';
|
import { compact, findLast } from '../../../util/iteratees';
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
@ -74,7 +75,7 @@ addActionHandler('setScrollOffset', (global, actions, payload): ActionReturnType
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('setReplyingToId', (global, actions, payload): ActionReturnType => {
|
addActionHandler('setReplyingToId', (global, actions, payload): ActionReturnType => {
|
||||||
const { messageId, tabId = getCurrentTabId() } = payload!;
|
const { messageId, tabId = getCurrentTabId() } = payload;
|
||||||
const currentMessageList = selectCurrentMessageList(global, tabId);
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
||||||
if (!currentMessageList) {
|
if (!currentMessageList) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -391,9 +392,9 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType =>
|
|||||||
chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId,
|
chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId,
|
||||||
replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel,
|
replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel,
|
||||||
tabId = getCurrentTabId(),
|
tabId = getCurrentTabId(),
|
||||||
} = payload!;
|
} = payload;
|
||||||
|
|
||||||
let { messageId } = payload!;
|
let { messageId } = payload;
|
||||||
|
|
||||||
if (groupedId !== undefined) {
|
if (groupedId !== undefined) {
|
||||||
const ids = selectForwardedMessageIdsByGroupId(global, groupedChatId!, groupedId);
|
const ids = selectForwardedMessageIdsByGroupId(global, groupedChatId!, groupedId);
|
||||||
@ -759,7 +760,7 @@ addActionHandler('createServiceNotification', (global, actions, payload): Action
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('openReactorListModal', (global, actions, payload): ActionReturnType => {
|
addActionHandler('openReactorListModal', (global, actions, payload): ActionReturnType => {
|
||||||
const { chatId, messageId, tabId = getCurrentTabId() } = payload!;
|
const { chatId, messageId, tabId = getCurrentTabId() } = payload;
|
||||||
|
|
||||||
return updateTabState(global, {
|
return updateTabState(global, {
|
||||||
reactorModal: { chatId, messageId },
|
reactorModal: { chatId, messageId },
|
||||||
@ -775,7 +776,7 @@ addActionHandler('closeReactorListModal', (global, actions, payload): ActionRetu
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('openSeenByModal', (global, actions, payload): ActionReturnType => {
|
addActionHandler('openSeenByModal', (global, actions, payload): ActionReturnType => {
|
||||||
const { chatId, messageId, tabId = getCurrentTabId() } = payload!;
|
const { chatId, messageId, tabId = getCurrentTabId() } = payload;
|
||||||
|
|
||||||
return updateTabState(global, {
|
return updateTabState(global, {
|
||||||
seenByModal: { chatId, messageId },
|
seenByModal: { chatId, messageId },
|
||||||
@ -790,6 +791,24 @@ addActionHandler('closeSeenByModal', (global, actions, payload): ActionReturnTyp
|
|||||||
}, tabId);
|
}, tabId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('openMessageLanguageModal', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { chatId, id, tabId = getCurrentTabId() } = payload;
|
||||||
|
|
||||||
|
const activeLanguage = selectRequestedTranslationLanguage(global, chatId, id, tabId);
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
messageLanguageModal: { chatId, messageId: id, activeLanguage },
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('closeMessageLanguageModal', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
messageLanguageModal: undefined,
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('copySelectedMessages', (global, actions, payload): ActionReturnType => {
|
addActionHandler('copySelectedMessages', (global, actions, payload): ActionReturnType => {
|
||||||
const { tabId = getCurrentTabId() } = payload;
|
const { tabId = getCurrentTabId() } = payload;
|
||||||
const tabState = selectTabState(global, tabId);
|
const tabState = selectTabState(global, tabId);
|
||||||
|
|||||||
@ -257,10 +257,6 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
|||||||
cached.recentCustomEmojis = [];
|
cached.recentCustomEmojis = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cached.settings.byKey.shouldSuggestCustomEmoji === undefined) {
|
|
||||||
cached.settings.byKey.shouldSuggestCustomEmoji = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cached.stickers.premiumSet) {
|
if (!cached.stickers.premiumSet) {
|
||||||
cached.stickers.premiumSet = {
|
cached.stickers.premiumSet = {
|
||||||
stickers: [],
|
stickers: [],
|
||||||
|
|||||||
@ -165,6 +165,9 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
|||||||
wasTimeFormatSetManually: false,
|
wasTimeFormatSetManually: false,
|
||||||
isConnectionStatusMinimized: true,
|
isConnectionStatusMinimized: true,
|
||||||
shouldArchiveAndMuteNewNonContact: false,
|
shouldArchiveAndMuteNewNonContact: false,
|
||||||
|
canTranslate: false,
|
||||||
|
canTranslateChats: true,
|
||||||
|
doNotTranslate: [],
|
||||||
},
|
},
|
||||||
themes: {
|
themes: {
|
||||||
light: {
|
light: {
|
||||||
@ -184,6 +187,9 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
|||||||
trustedBotIds: [],
|
trustedBotIds: [],
|
||||||
|
|
||||||
transcriptions: {},
|
transcriptions: {},
|
||||||
|
translations: {
|
||||||
|
byChatId: {},
|
||||||
|
},
|
||||||
|
|
||||||
byTabId: {},
|
byTabId: {},
|
||||||
|
|
||||||
@ -267,4 +273,8 @@ export const INITIAL_TAB_STATE: TabState = {
|
|||||||
pollModal: {
|
pollModal: {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
requestedTranslations: {
|
||||||
|
byChatId: {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,3 +10,4 @@ export * from './twoFaSettings';
|
|||||||
export * from './passcode';
|
export * from './passcode';
|
||||||
export * from './payments';
|
export * from './payments';
|
||||||
export * from './statistics';
|
export * from './statistics';
|
||||||
|
export * from './translations';
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import type {
|
import type {
|
||||||
GlobalState, MessageList, MessageListType, TabArgs, Thread, TabThread,
|
GlobalState, MessageList, MessageListType, TabArgs, Thread, TabThread,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import type { ApiMessage, ApiSponsoredMessage, ApiThreadInfo } from '../../api/types';
|
import type {
|
||||||
|
ApiMessage, ApiSponsoredMessage, ApiThreadInfo,
|
||||||
|
} from '../../api/types';
|
||||||
import { MAIN_THREAD_ID } from '../../api/types';
|
import { MAIN_THREAD_ID } from '../../api/types';
|
||||||
import type { FocusDirection } from '../../types';
|
import type { FocusDirection } from '../../types';
|
||||||
|
|
||||||
|
|||||||
129
src/global/reducers/translations.ts
Normal file
129
src/global/reducers/translations.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import type { GlobalState, TabArgs, TranslatedMessage } from '../types';
|
||||||
|
import type { ApiFormattedText } from '../../api/types';
|
||||||
|
|
||||||
|
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||||
|
import { omit } from '../../util/iteratees';
|
||||||
|
import { selectMessageTranslations, selectTabState } from '../selectors';
|
||||||
|
import { updateTabState } from './tabs';
|
||||||
|
|
||||||
|
export function updateMessageTranslation<T extends GlobalState>(
|
||||||
|
global: T, chatId: string, messageId: number, toLanguageCode: string, translation: Partial<TranslatedMessage>,
|
||||||
|
) {
|
||||||
|
const translatedMessages = selectMessageTranslations(global, chatId, toLanguageCode);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
translations: {
|
||||||
|
...global.translations,
|
||||||
|
byChatId: {
|
||||||
|
...global.translations.byChatId,
|
||||||
|
[chatId]: {
|
||||||
|
...global.translations.byChatId[chatId],
|
||||||
|
byLangCode: {
|
||||||
|
...global.translations.byChatId[chatId]?.byLangCode,
|
||||||
|
[toLanguageCode]: {
|
||||||
|
...translatedMessages,
|
||||||
|
[messageId]: {
|
||||||
|
...translatedMessages[messageId],
|
||||||
|
...translation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearMessageTranslation<T extends GlobalState>(
|
||||||
|
global: T, chatId: string, messageId: number,
|
||||||
|
) {
|
||||||
|
const chatTranslations = global.translations.byChatId[chatId];
|
||||||
|
if (!chatTranslations) return global;
|
||||||
|
|
||||||
|
const { byLangCode } = chatTranslations;
|
||||||
|
const newByLangCode = Object.keys(byLangCode).reduce((acc, langCode) => {
|
||||||
|
const newTranslatedMessages = omit(byLangCode[langCode], [messageId]);
|
||||||
|
if (Object.keys(newTranslatedMessages).length) {
|
||||||
|
acc[langCode] = newTranslatedMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, Record<number, TranslatedMessage>>);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
translations: {
|
||||||
|
...global.translations,
|
||||||
|
byChatId: {
|
||||||
|
...global.translations.byChatId,
|
||||||
|
[chatId]: {
|
||||||
|
...chatTranslations,
|
||||||
|
byLangCode: newByLangCode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateMessageTranslations<T extends GlobalState>(
|
||||||
|
global: T, chatId: string, messageIds: number[], toLanguageCode: string, translations: ApiFormattedText[],
|
||||||
|
) {
|
||||||
|
messageIds.forEach((messageId, index) => {
|
||||||
|
global = updateMessageTranslation(global, chatId, messageId, toLanguageCode, {
|
||||||
|
text: translations[index],
|
||||||
|
isPending: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return global;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateRequestedMessageTranslation<T extends GlobalState>(
|
||||||
|
global: T, chatId: string, messageId: number, toLanguageCode: string, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
|
) {
|
||||||
|
const tabState = selectTabState(global, tabId);
|
||||||
|
global = updateTabState(global, {
|
||||||
|
requestedTranslations: {
|
||||||
|
...tabState.requestedTranslations,
|
||||||
|
byChatId: {
|
||||||
|
...tabState.requestedTranslations.byChatId,
|
||||||
|
[chatId]: {
|
||||||
|
...tabState.requestedTranslations.byChatId[chatId],
|
||||||
|
manualMessages: {
|
||||||
|
...tabState.requestedTranslations.byChatId[chatId]?.manualMessages,
|
||||||
|
[messageId]: toLanguageCode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
|
||||||
|
return global;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeRequestedMessageTranslation<T extends GlobalState>(
|
||||||
|
global: T, chatId: string, messageId: number, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
|
) {
|
||||||
|
const tabState = selectTabState(global, tabId);
|
||||||
|
|
||||||
|
const manualMessages = tabState.requestedTranslations.byChatId[chatId]?.manualMessages;
|
||||||
|
if (!manualMessages) return global;
|
||||||
|
|
||||||
|
const newManualMessages = omit(manualMessages, [messageId]);
|
||||||
|
|
||||||
|
global = updateTabState(global, {
|
||||||
|
requestedTranslations: {
|
||||||
|
...tabState.requestedTranslations,
|
||||||
|
byChatId: {
|
||||||
|
...tabState.requestedTranslations.byChatId,
|
||||||
|
[chatId]: {
|
||||||
|
...tabState.requestedTranslations.byChatId[chatId],
|
||||||
|
manualMessages: newManualMessages,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
|
||||||
|
return global;
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
GlobalState, MessageListType, TabArgs, Thread, TabThread,
|
GlobalState, MessageListType, TabArgs, Thread, TabThread, ChatTranslatedMessages,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import type {
|
import type {
|
||||||
ApiChat,
|
ApiChat,
|
||||||
@ -1280,6 +1280,24 @@ export function selectForwardsContainVoiceMessages<T extends GlobalState>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectChatTranslations<T extends GlobalState>(
|
||||||
|
global: T, chatId: string,
|
||||||
|
): ChatTranslatedMessages {
|
||||||
|
return global.translations.byChatId[chatId];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectMessageTranslations<T extends GlobalState>(
|
||||||
|
global: T, chatId: string, toLanguageCode: string,
|
||||||
|
) {
|
||||||
|
return selectChatTranslations(global, chatId)?.byLangCode[toLanguageCode] || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectRequestedTranslationLanguage<T extends GlobalState>(
|
||||||
|
global: T, chatId: string, messageId: number, tabId = getCurrentTabId(),
|
||||||
|
): string | undefined {
|
||||||
|
return selectTabState(global, tabId).requestedTranslations.byChatId[chatId]?.manualMessages?.[messageId];
|
||||||
|
}
|
||||||
|
|
||||||
export function selectForwardsCanBeSentToChat<T extends GlobalState>(
|
export function selectForwardsCanBeSentToChat<T extends GlobalState>(
|
||||||
global: T,
|
global: T,
|
||||||
toChatId: string,
|
toChatId: string,
|
||||||
|
|||||||
@ -7,3 +7,7 @@ export function selectNotifySettings<T extends GlobalState>(global: T) {
|
|||||||
export function selectNotifyExceptions<T extends GlobalState>(global: T) {
|
export function selectNotifyExceptions<T extends GlobalState>(global: T) {
|
||||||
return global.settings.notifyExceptions;
|
return global.settings.notifyExceptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectLanguageCode<T extends GlobalState>(global: T) {
|
||||||
|
return global.settings.byKey.language.replace('-raw', '');
|
||||||
|
}
|
||||||
|
|||||||
@ -160,6 +160,20 @@ export type ApiLimitTypeWithModal = Exclude<ApiLimitType, (
|
|||||||
'captionLength' | 'aboutLength' | 'stickersFaved' | 'savedGifs'
|
'captionLength' | 'aboutLength' | 'stickersFaved' | 'savedGifs'
|
||||||
)>;
|
)>;
|
||||||
|
|
||||||
|
export type TranslatedMessage = {
|
||||||
|
isPending?: boolean;
|
||||||
|
text?: ApiFormattedText;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChatTranslatedMessages = {
|
||||||
|
byLangCode: Record<string, Record<number, TranslatedMessage>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChatRequestedTranslations = {
|
||||||
|
toLanguage?: string;
|
||||||
|
manualMessages?: Record<number, string>;
|
||||||
|
};
|
||||||
|
|
||||||
export type TabState = {
|
export type TabState = {
|
||||||
id: number;
|
id: number;
|
||||||
isMasterTab: boolean;
|
isMasterTab: boolean;
|
||||||
@ -520,6 +534,15 @@ export type TabState = {
|
|||||||
topicId: number;
|
topicId: number;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
requestedTranslations: {
|
||||||
|
byChatId: Record<string, ChatRequestedTranslations>;
|
||||||
|
};
|
||||||
|
messageLanguageModal?: {
|
||||||
|
chatId: string;
|
||||||
|
messageId: number;
|
||||||
|
activeLanguage?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GlobalState = {
|
export type GlobalState = {
|
||||||
@ -786,6 +809,10 @@ export type GlobalState = {
|
|||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
isHidden: boolean;
|
isHidden: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
translations: {
|
||||||
|
byChatId: Record<string, ChatTranslatedMessages>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CallSound = (
|
export type CallSound = (
|
||||||
@ -1370,6 +1397,12 @@ export interface ActionPayloads {
|
|||||||
disableContextMenuHint: WithTabId | undefined;
|
disableContextMenuHint: WithTabId | undefined;
|
||||||
focusNextReply: WithTabId | undefined;
|
focusNextReply: WithTabId | undefined;
|
||||||
|
|
||||||
|
openMessageLanguageModal: {
|
||||||
|
chatId: string;
|
||||||
|
id: number;
|
||||||
|
} & WithTabId;
|
||||||
|
closeMessageLanguageModal: WithTabId | undefined;
|
||||||
|
|
||||||
// poll result
|
// poll result
|
||||||
openPollResults: {
|
openPollResults: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@ -1665,6 +1698,23 @@ export interface ActionPayloads {
|
|||||||
ids: number[];
|
ids: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
requestMessageTranslation: {
|
||||||
|
chatId: string;
|
||||||
|
id: number;
|
||||||
|
toLanguageCode?: string;
|
||||||
|
} & WithTabId;
|
||||||
|
|
||||||
|
showOriginalMessage: {
|
||||||
|
chatId: string;
|
||||||
|
id: number;
|
||||||
|
} & WithTabId;
|
||||||
|
|
||||||
|
translateMessages: {
|
||||||
|
chatId: string;
|
||||||
|
messageIds: number[];
|
||||||
|
toLanguageCode?: string;
|
||||||
|
};
|
||||||
|
|
||||||
// Reactions
|
// Reactions
|
||||||
loadAvailableReactions: undefined;
|
loadAvailableReactions: undefined;
|
||||||
|
|
||||||
|
|||||||
7
src/hooks/useTextLanguage.ts
Normal file
7
src/hooks/useTextLanguage.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { detectLanguage } from '../util/languageDetection';
|
||||||
|
import useAsync from './useAsync';
|
||||||
|
|
||||||
|
export default function useTextLanguage(text?: string) {
|
||||||
|
const language = useAsync(() => (text ? detectLanguage(text) : Promise.resolve(undefined)), [text], undefined);
|
||||||
|
return language;
|
||||||
|
}
|
||||||
20
src/lib/fasttextweb/fasttext-wasm.js
Normal file
20
src/lib/fasttextweb/fasttext-wasm.js
Normal file
File diff suppressed because one or more lines are too long
BIN
src/lib/fasttextweb/fasttext-wasm.wasm
Normal file
BIN
src/lib/fasttextweb/fasttext-wasm.wasm
Normal file
Binary file not shown.
68
src/lib/fasttextweb/fasttext.worker.ts
Normal file
68
src/lib/fasttextweb/fasttext.worker.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { createWorkerInterface } from '../../util/createPostMessageInterface';
|
||||||
|
import fasttextInitializer from './fasttext-wasm';
|
||||||
|
import fasttextWasmPath from './fasttext-wasm.wasm';
|
||||||
|
|
||||||
|
type FastTextMethods = {
|
||||||
|
makePrediction: (type: 'predict' | 'predict-prob', text: string, k: string, threshold: string) => Promise<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LABEL_PREFIX = '__label__';
|
||||||
|
|
||||||
|
// Since webpack will change the name and potentially the path of the
|
||||||
|
// `.wasm` file, we have to provide a `locateFile()` hook to redirect
|
||||||
|
// to the appropriate URL.
|
||||||
|
// More details: https://kripken.github.io/emscripten-site/docs/api_reference/module.html
|
||||||
|
let fastTextInstance: FastTextMethods;
|
||||||
|
const fastTextPromise = fasttextInitializer({
|
||||||
|
locateFile: (path: string, prefix: string) => {
|
||||||
|
if (path.endsWith('.wasm')) {
|
||||||
|
return fasttextWasmPath;
|
||||||
|
}
|
||||||
|
return prefix + path;
|
||||||
|
},
|
||||||
|
}).then((fastText: FastTextMethods) => {
|
||||||
|
fastTextInstance = fastText;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('[FASTTEXT] Worker ready');
|
||||||
|
});
|
||||||
|
|
||||||
|
function parseLabel(label: string) {
|
||||||
|
return label.split('\n')[0].replace(LABEL_PREFIX, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLabelsWithProbabilities(labels: string) {
|
||||||
|
return labels.trim()
|
||||||
|
.split('\n')
|
||||||
|
.map((labelWithProb: string) => {
|
||||||
|
const [label, prob] = labelWithProb.split(' ');
|
||||||
|
return {
|
||||||
|
label: parseLabel(label),
|
||||||
|
prob: parseFloat(prob),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function detectLanguage(text: string, threshold: number) {
|
||||||
|
if (!fastTextInstance) await fastTextPromise;
|
||||||
|
const label = await fastTextInstance.makePrediction('predict', text, '1', threshold.toString());
|
||||||
|
if (!label.length) return undefined;
|
||||||
|
return parseLabel(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function detectLanguageProbability(text: string, labelsCount: number, threshold: number) {
|
||||||
|
if (!fastTextInstance) await fastTextPromise;
|
||||||
|
const labels = await fastTextInstance.makePrediction(
|
||||||
|
'predict-prob', text, labelsCount.toString(), threshold.toString(),
|
||||||
|
);
|
||||||
|
if (!labels.length) return undefined;
|
||||||
|
return parseLabelsWithProbabilities(labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
detectLanguage,
|
||||||
|
detectLanguageProbability,
|
||||||
|
};
|
||||||
|
|
||||||
|
createWorkerInterface(api);
|
||||||
|
|
||||||
|
export type FastTextApi = typeof api;
|
||||||
@ -1262,6 +1262,7 @@ messages.getMessageReactionsList#461b3f48 flags:# peer:InputPeer id:int reaction
|
|||||||
messages.setChatAvailableReactions#feb16771 peer:InputPeer available_reactions:ChatReactions = Updates;
|
messages.setChatAvailableReactions#feb16771 peer:InputPeer available_reactions:ChatReactions = Updates;
|
||||||
messages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions;
|
messages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions;
|
||||||
messages.setDefaultReaction#4f47a016 reaction:Reaction = Bool;
|
messages.setDefaultReaction#4f47a016 reaction:Reaction = Bool;
|
||||||
|
messages.translateText#63183030 flags:# peer:flags.0?InputPeer id:flags.0?Vector<int> text:flags.1?Vector<TextWithEntities> to_lang:string = messages.TranslatedText;
|
||||||
messages.getUnreadReactions#3223495b flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
|
messages.getUnreadReactions#3223495b flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
|
||||||
messages.readReactions#54aa7f8e flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory;
|
messages.readReactions#54aa7f8e flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory;
|
||||||
messages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots;
|
messages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots;
|
||||||
|
|||||||
@ -243,6 +243,7 @@
|
|||||||
"messages.setChatAvailableReactions",
|
"messages.setChatAvailableReactions",
|
||||||
"messages.getAvailableReactions",
|
"messages.getAvailableReactions",
|
||||||
"messages.setDefaultReaction",
|
"messages.setDefaultReaction",
|
||||||
|
"messages.translateText",
|
||||||
"help.getAppConfig",
|
"help.getAppConfig",
|
||||||
"stats.getBroadcastStats",
|
"stats.getBroadcastStats",
|
||||||
"stats.getMegagroupStats",
|
"stats.getMegagroupStats",
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
|
import type { RLottieApi } from './rlottie.worker';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DPR, IS_SAFARI, IS_ANDROID, IS_IOS,
|
DPR, IS_SAFARI, IS_ANDROID, IS_IOS,
|
||||||
} from '../../util/environment';
|
} from '../../util/environment';
|
||||||
import WorkerConnector from '../../util/WorkerConnector';
|
import { createConnector } from '../../util/PostMessageConnector';
|
||||||
import { animate } from '../../util/animation';
|
import { animate } from '../../util/animation';
|
||||||
import cycleRestrict from '../../util/cycleRestrict';
|
import cycleRestrict from '../../util/cycleRestrict';
|
||||||
import { fastRaf } from '../../util/schedulers';
|
import { fastRaf } from '../../util/schedulers';
|
||||||
@ -30,7 +32,7 @@ const LOW_PRIORITY_CACHE_MODULO = 0;
|
|||||||
const instancesById = new Map<string, RLottie>();
|
const instancesById = new Map<string, RLottie>();
|
||||||
|
|
||||||
const workers = new Array(MAX_WORKERS).fill(undefined).map(
|
const workers = new Array(MAX_WORKERS).fill(undefined).map(
|
||||||
() => new WorkerConnector(new Worker(new URL('./rlottie.worker.ts', import.meta.url))),
|
() => createConnector<RLottieApi>(new Worker(new URL('./rlottie.worker.ts', import.meta.url))),
|
||||||
);
|
);
|
||||||
let lastWorkerIndex = -1;
|
let lastWorkerIndex = -1;
|
||||||
|
|
||||||
@ -108,7 +110,7 @@ class RLottie {
|
|||||||
onLoad: NoneToVoidFunction | undefined,
|
onLoad: NoneToVoidFunction | undefined,
|
||||||
private id: string,
|
private id: string,
|
||||||
private tgsUrl: string,
|
private tgsUrl: string,
|
||||||
private params: Params = {},
|
private params: Params = { },
|
||||||
private customColor?: [number, number, number],
|
private customColor?: [number, number, number],
|
||||||
private onEnded?: (isDestroyed?: boolean) => void,
|
private onEnded?: (isDestroyed?: boolean) => void,
|
||||||
private onLoop?: () => void,
|
private onLoop?: () => void,
|
||||||
@ -360,7 +362,7 @@ class RLottie {
|
|||||||
this.id,
|
this.id,
|
||||||
this.tgsUrl,
|
this.tgsUrl,
|
||||||
this.imgSize,
|
this.imgSize,
|
||||||
this.params.isLowPriority,
|
this.params.isLowPriority || false,
|
||||||
this.customColor,
|
this.customColor,
|
||||||
this.onRendererInit.bind(this),
|
this.onRendererInit.bind(this),
|
||||||
],
|
],
|
||||||
@ -395,7 +397,7 @@ class RLottie {
|
|||||||
args: [
|
args: [
|
||||||
this.id,
|
this.id,
|
||||||
this.tgsUrl,
|
this.tgsUrl,
|
||||||
this.params.isLowPriority,
|
this.params.isLowPriority || false,
|
||||||
this.onChangeData.bind(this),
|
this.onChangeData.bind(this),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { inflate } from 'pako/dist/pako_inflate';
|
import { inflate } from 'pako/dist/pako_inflate';
|
||||||
import createWorkerInterface from '../../util/createWorkerInterface';
|
import { createWorkerInterface } from '../../util/createPostMessageInterface';
|
||||||
import type { CancellableCallback } from '../../util/WorkerConnector';
|
import type { CancellableCallback } from '../../util/PostMessageConnector';
|
||||||
|
|
||||||
import 'script-loader!./rlottie-wasm';
|
import 'script-loader!./rlottie-wasm';
|
||||||
|
|
||||||
@ -164,9 +164,13 @@ function destroy(key: string, isRepeated = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createWorkerInterface({
|
const api = {
|
||||||
init,
|
init,
|
||||||
changeData,
|
changeData,
|
||||||
renderFrames,
|
renderFrames,
|
||||||
destroy,
|
destroy,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
createWorkerInterface(api);
|
||||||
|
|
||||||
|
export type RLottieApi = typeof api;
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { pause } from './util/schedulers';
|
|||||||
|
|
||||||
declare const self: ServiceWorkerGlobalScope;
|
declare const self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
const NETWORK_FIRST_ASSETS = new Set(['/', '/rlottie-wasm.wasm', '/webp_wasm.wasm']);
|
const RE_NETWORK_FIRST_ASSETS = /\.(wasm|html)$/;
|
||||||
const RE_CACHE_FIRST_ASSETS = /[\da-f]{20}.*\.(js|css|woff2?|svg|png|jpg|jpeg|tgs|json|wasm)$/;
|
const RE_CACHE_FIRST_ASSETS = /[\da-f]{20}.*\.(js|css|woff2?|svg|png|jpg|jpeg|tgs|json|wasm)$/;
|
||||||
const ACTIVATE_TIMEOUT = 3000;
|
const ACTIVATE_TIMEOUT = 3000;
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ self.addEventListener('fetch', (e: FetchEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (url.startsWith('http')) {
|
if (url.startsWith('http')) {
|
||||||
if (NETWORK_FIRST_ASSETS.has(new URL(url).pathname)) {
|
if (new URL(url).pathname === '/' || url.match(RE_NETWORK_FIRST_ASSETS)) {
|
||||||
e.respondWith(respondWithCacheNetworkFirst(e));
|
e.respondWith(respondWithCacheNetworkFirst(e));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,6 +125,12 @@ body.cursor-ew-resize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clearfix::after {
|
||||||
|
content: "";
|
||||||
|
clear: both;
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
See the article for more information on this visually-hidden pattern.
|
See the article for more information on this visually-hidden pattern.
|
||||||
https://snook.ca/archives/html_and_css/hiding-content-for-accessibility
|
https://snook.ca/archives/html_and_css/hiding-content-for-accessibility
|
||||||
|
|||||||
@ -89,6 +89,9 @@ export interface ISettings extends NotifySettings, Record<string, any> {
|
|||||||
wasTimeFormatSetManually: boolean;
|
wasTimeFormatSetManually: boolean;
|
||||||
isConnectionStatusMinimized: boolean;
|
isConnectionStatusMinimized: boolean;
|
||||||
shouldArchiveAndMuteNewNonContact?: boolean;
|
shouldArchiveAndMuteNewNonContact?: boolean;
|
||||||
|
canTranslate: boolean;
|
||||||
|
canTranslateChats: boolean;
|
||||||
|
doNotTranslate: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiPrivacySettings {
|
export interface ApiPrivacySettings {
|
||||||
@ -216,6 +219,7 @@ export enum SettingsScreens {
|
|||||||
Stickers,
|
Stickers,
|
||||||
QuickReaction,
|
QuickReaction,
|
||||||
CustomEmoji,
|
CustomEmoji,
|
||||||
|
DoNotTranslate,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StickerSetOrRecent = Pick<ApiStickerSet, (
|
export type StickerSetOrRecent = Pick<ApiStickerSet, (
|
||||||
|
|||||||
216
src/util/PostMessageConnector.ts
Normal file
216
src/util/PostMessageConnector.ts
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
import generateIdFor from './generateIdFor';
|
||||||
|
|
||||||
|
export interface CancellableCallback {
|
||||||
|
(
|
||||||
|
...args: any[]
|
||||||
|
): void;
|
||||||
|
|
||||||
|
isCanceled?: boolean;
|
||||||
|
acceptsBuffer?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type InitData = {
|
||||||
|
channel?: string;
|
||||||
|
type: 'init';
|
||||||
|
messageId?: string;
|
||||||
|
name: 'init';
|
||||||
|
args: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CallMethodData = {
|
||||||
|
channel?: string;
|
||||||
|
type: 'callMethod';
|
||||||
|
messageId?: string;
|
||||||
|
name: string;
|
||||||
|
args: any;
|
||||||
|
withCallback?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OriginMessageData = InitData | CallMethodData | {
|
||||||
|
channel?: string;
|
||||||
|
type: 'cancelProgress';
|
||||||
|
messageId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface OriginMessageEvent {
|
||||||
|
data: OriginMessageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiUpdate =
|
||||||
|
{ type: string }
|
||||||
|
& any;
|
||||||
|
|
||||||
|
export type WorkerMessageData = {
|
||||||
|
channel?: string;
|
||||||
|
type: 'update';
|
||||||
|
update: ApiUpdate;
|
||||||
|
} | {
|
||||||
|
channel?: string;
|
||||||
|
type: 'methodResponse';
|
||||||
|
messageId: string;
|
||||||
|
response?: any;
|
||||||
|
error?: { message: string };
|
||||||
|
} | {
|
||||||
|
channel?: string;
|
||||||
|
type: 'methodCallback';
|
||||||
|
messageId: string;
|
||||||
|
callbackArgs: any[];
|
||||||
|
} | {
|
||||||
|
channel?: string;
|
||||||
|
type: 'unhandledError';
|
||||||
|
error?: { message: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface WorkerMessageEvent {
|
||||||
|
data: WorkerMessageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestStates {
|
||||||
|
messageId: string;
|
||||||
|
resolve: Function;
|
||||||
|
reject: Function;
|
||||||
|
callback: AnyToVoidFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
type InputRequestTypes = Record<string, AnyFunction>;
|
||||||
|
|
||||||
|
type Values<T> = T[keyof T];
|
||||||
|
export type RequestTypes<T extends InputRequestTypes> = Values<{
|
||||||
|
[Name in keyof (T)]: {
|
||||||
|
name: Name & string;
|
||||||
|
args: Parameters<T[Name]>;
|
||||||
|
}
|
||||||
|
}>;
|
||||||
|
|
||||||
|
class ConnectorClass<T extends InputRequestTypes> {
|
||||||
|
private requestStates = new Map<string, RequestStates>();
|
||||||
|
|
||||||
|
private requestStatesByCallback = new Map<AnyToVoidFunction, RequestStates>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public target: Worker,
|
||||||
|
private onUpdate?: (update: ApiUpdate) => void,
|
||||||
|
private channel?: string,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
public destroy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
init(...args: any[]) {
|
||||||
|
this.postMessage({
|
||||||
|
type: 'init',
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
request(messageData: RequestTypes<T>) {
|
||||||
|
const { requestStates, requestStatesByCallback } = this;
|
||||||
|
|
||||||
|
const messageId = generateIdFor(requestStates);
|
||||||
|
const payload: CallMethodData = {
|
||||||
|
type: 'callMethod',
|
||||||
|
messageId,
|
||||||
|
...messageData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestState = { messageId } as RequestStates;
|
||||||
|
|
||||||
|
// Re-wrap type because of `postMessage`
|
||||||
|
const promise: Promise<any> = new Promise((resolve, reject) => {
|
||||||
|
Object.assign(requestState, { resolve, reject });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof payload.args[payload.args.length - 1] === 'function') {
|
||||||
|
payload.withCallback = true;
|
||||||
|
|
||||||
|
const callback = payload.args.pop() as AnyToVoidFunction;
|
||||||
|
requestState.callback = callback;
|
||||||
|
requestStatesByCallback.set(callback, requestState);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestStates.set(messageId, requestState);
|
||||||
|
promise
|
||||||
|
.catch(() => undefined)
|
||||||
|
.finally(() => {
|
||||||
|
requestStates.delete(messageId);
|
||||||
|
|
||||||
|
if (requestState.callback) {
|
||||||
|
requestStatesByCallback.delete(requestState.callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.postMessage(payload);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelCallback(progressCallback: CancellableCallback) {
|
||||||
|
progressCallback.isCanceled = true;
|
||||||
|
|
||||||
|
const { messageId } = this.requestStatesByCallback.get(progressCallback) || {};
|
||||||
|
if (!messageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.postMessage({
|
||||||
|
type: 'cancelProgress',
|
||||||
|
messageId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(data: WorkerMessageData) {
|
||||||
|
const { requestStates, channel } = this;
|
||||||
|
if (data.channel !== channel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'update' && this.onUpdate) {
|
||||||
|
this.onUpdate(data.update);
|
||||||
|
}
|
||||||
|
if (data.type === 'methodResponse') {
|
||||||
|
const requestState = requestStates.get(data.messageId);
|
||||||
|
if (requestState) {
|
||||||
|
if (data.error) {
|
||||||
|
requestState.reject(data.error);
|
||||||
|
} else {
|
||||||
|
requestState.resolve(data.response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (data.type === 'methodCallback') {
|
||||||
|
const requestState = requestStates.get(data.messageId);
|
||||||
|
requestState?.callback?.(...data.callbackArgs);
|
||||||
|
} else if (data.type === 'unhandledError') {
|
||||||
|
throw new Error(data.error?.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private postMessage(data: AnyLiteral) {
|
||||||
|
data.channel = this.channel;
|
||||||
|
|
||||||
|
this.target.postMessage(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createConnector<T extends InputRequestTypes>(
|
||||||
|
worker: Worker,
|
||||||
|
onUpdate?: (update: ApiUpdate) => void,
|
||||||
|
channel?: string,
|
||||||
|
) {
|
||||||
|
const connector = new ConnectorClass<T>(worker, onUpdate, channel);
|
||||||
|
|
||||||
|
function handleMessage({ data }: WorkerMessageEvent) {
|
||||||
|
connector.onMessage(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.addEventListener('message', handleMessage);
|
||||||
|
|
||||||
|
connector.destroy = () => {
|
||||||
|
worker.removeEventListener('message', handleMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
return connector;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Connector<T extends InputRequestTypes> = ReturnType<typeof createConnector<T>>;
|
||||||
@ -1,140 +0,0 @@
|
|||||||
import generateIdFor from './generateIdFor';
|
|
||||||
|
|
||||||
export interface CancellableCallback {
|
|
||||||
(
|
|
||||||
...args: any[]
|
|
||||||
): void;
|
|
||||||
|
|
||||||
isCanceled?: boolean;
|
|
||||||
acceptsBuffer?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
type CallMethodData = {
|
|
||||||
type: 'callMethod';
|
|
||||||
messageId?: string;
|
|
||||||
name: string;
|
|
||||||
args: any;
|
|
||||||
withCallback?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type OriginMessageData = CallMethodData | {
|
|
||||||
type: 'cancelProgress';
|
|
||||||
messageId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface OriginMessageEvent {
|
|
||||||
data: OriginMessageData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WorkerMessageData = {
|
|
||||||
type: 'methodResponse';
|
|
||||||
messageId: string;
|
|
||||||
response?: any;
|
|
||||||
error?: { message: string };
|
|
||||||
} | {
|
|
||||||
type: 'methodCallback';
|
|
||||||
messageId: string;
|
|
||||||
callbackArgs: any[];
|
|
||||||
} | {
|
|
||||||
type: 'unhandledError';
|
|
||||||
error?: { message: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface WorkerMessageEvent {
|
|
||||||
data: WorkerMessageData;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RequestStates {
|
|
||||||
messageId: string;
|
|
||||||
resolve: Function;
|
|
||||||
reject: Function;
|
|
||||||
callback: AnyToVoidFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Replace `any` with proper generics
|
|
||||||
export default class WorkerConnector {
|
|
||||||
private requestStates = new Map<string, RequestStates>();
|
|
||||||
|
|
||||||
private requestStatesByCallback = new Map<AnyToVoidFunction, RequestStates>();
|
|
||||||
|
|
||||||
constructor(private worker: Worker) {
|
|
||||||
this.subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
request(messageData: { name: string; args: any }) {
|
|
||||||
const { worker, requestStates, requestStatesByCallback } = this;
|
|
||||||
|
|
||||||
const messageId = generateIdFor(requestStates);
|
|
||||||
const payload: CallMethodData = {
|
|
||||||
type: 'callMethod',
|
|
||||||
messageId,
|
|
||||||
...messageData,
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestState = { messageId } as RequestStates;
|
|
||||||
|
|
||||||
// Re-wrap type because of `postMessage`
|
|
||||||
const promise: Promise<any> = new Promise((resolve, reject) => {
|
|
||||||
Object.assign(requestState, { resolve, reject });
|
|
||||||
});
|
|
||||||
|
|
||||||
if (typeof payload.args[payload.args.length - 1] === 'function') {
|
|
||||||
payload.withCallback = true;
|
|
||||||
|
|
||||||
const callback = payload.args.pop() as AnyToVoidFunction;
|
|
||||||
requestState.callback = callback;
|
|
||||||
requestStatesByCallback.set(callback, requestState);
|
|
||||||
}
|
|
||||||
|
|
||||||
requestStates.set(messageId, requestState);
|
|
||||||
promise
|
|
||||||
.catch(() => undefined)
|
|
||||||
.finally(() => {
|
|
||||||
requestStates.delete(messageId);
|
|
||||||
|
|
||||||
if (requestState.callback) {
|
|
||||||
requestStatesByCallback.delete(requestState.callback);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
worker.postMessage(payload);
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelCallback(progressCallback: CancellableCallback) {
|
|
||||||
progressCallback.isCanceled = true;
|
|
||||||
|
|
||||||
const { messageId } = this.requestStatesByCallback.get(progressCallback) || {};
|
|
||||||
if (!messageId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.worker.postMessage({
|
|
||||||
type: 'cancelProgress',
|
|
||||||
messageId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private subscribe() {
|
|
||||||
const { worker, requestStates } = this;
|
|
||||||
|
|
||||||
worker.addEventListener('message', ({ data }: WorkerMessageEvent) => {
|
|
||||||
if (data.type === 'methodResponse') {
|
|
||||||
const requestState = requestStates.get(data.messageId);
|
|
||||||
if (requestState) {
|
|
||||||
if (data.error) {
|
|
||||||
requestState.reject(data.error);
|
|
||||||
} else {
|
|
||||||
requestState.resolve(data.response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (data.type === 'methodCallback') {
|
|
||||||
const requestState = requestStates.get(data.messageId);
|
|
||||||
requestState?.callback?.(...data.callbackArgs);
|
|
||||||
} else if (data.type === 'unhandledError') {
|
|
||||||
throw new Error(data.error?.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
144
src/util/createPostMessageInterface.ts
Normal file
144
src/util/createPostMessageInterface.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import type {
|
||||||
|
CancellableCallback, OriginMessageEvent, OriginMessageData, WorkerMessageData, ApiUpdate,
|
||||||
|
} from './PostMessageConnector';
|
||||||
|
|
||||||
|
import { DEBUG } from '../config';
|
||||||
|
|
||||||
|
declare const self: WorkerGlobalScope;
|
||||||
|
|
||||||
|
const callbackState = new Map<string, CancellableCallback>();
|
||||||
|
|
||||||
|
type ApiConfig =
|
||||||
|
((name: string, ...args: any[]) => any | [any, ArrayBuffer[]])
|
||||||
|
| Record<string, Function>;
|
||||||
|
type SendToOrigin = (data: WorkerMessageData, transferables?: Transferable[]) => void;
|
||||||
|
|
||||||
|
export function createWorkerInterface(api: ApiConfig, channel?: string) {
|
||||||
|
function sendToOrigin(data: WorkerMessageData, transferables?: Transferable[]) {
|
||||||
|
data.channel = channel;
|
||||||
|
|
||||||
|
if (transferables) {
|
||||||
|
postMessage(data, transferables);
|
||||||
|
} else {
|
||||||
|
postMessage(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleErrors(sendToOrigin);
|
||||||
|
|
||||||
|
onmessage = (message: OriginMessageEvent) => {
|
||||||
|
if (message.data?.channel === channel) {
|
||||||
|
onMessage(api, message.data, sendToOrigin);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onMessage(
|
||||||
|
api: ApiConfig,
|
||||||
|
data: OriginMessageData,
|
||||||
|
sendToOrigin: SendToOrigin,
|
||||||
|
onUpdate?: (update: ApiUpdate) => void,
|
||||||
|
) {
|
||||||
|
if (!onUpdate) {
|
||||||
|
onUpdate = (update: ApiUpdate) => {
|
||||||
|
sendToOrigin({
|
||||||
|
type: 'update',
|
||||||
|
update,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (data.type) {
|
||||||
|
case 'init': {
|
||||||
|
const { args } = data;
|
||||||
|
const promise = typeof api === 'function'
|
||||||
|
? api('init', onUpdate, ...args)
|
||||||
|
: api.init?.(onUpdate, ...args);
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'callMethod': {
|
||||||
|
const {
|
||||||
|
messageId, name, args, withCallback,
|
||||||
|
} = data;
|
||||||
|
try {
|
||||||
|
if (messageId && withCallback) {
|
||||||
|
const callback = (...callbackArgs: any[]) => {
|
||||||
|
const lastArg = callbackArgs[callbackArgs.length - 1];
|
||||||
|
|
||||||
|
sendToOrigin({
|
||||||
|
type: 'methodCallback',
|
||||||
|
messageId,
|
||||||
|
callbackArgs,
|
||||||
|
}, isTransferable(lastArg) ? [lastArg] : undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
callbackState.set(messageId, callback);
|
||||||
|
|
||||||
|
args.push(callback as never);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = typeof api === 'function'
|
||||||
|
? await api(name, ...args)
|
||||||
|
: await api[name](...args);
|
||||||
|
const { arrayBuffer } = (typeof response === 'object' && 'arrayBuffer' in response && response) || {};
|
||||||
|
if (messageId) {
|
||||||
|
sendToOrigin(
|
||||||
|
{
|
||||||
|
type: 'methodResponse',
|
||||||
|
messageId,
|
||||||
|
response,
|
||||||
|
},
|
||||||
|
arrayBuffer ? [arrayBuffer] : undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (DEBUG) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageId) {
|
||||||
|
sendToOrigin({
|
||||||
|
type: 'methodResponse',
|
||||||
|
messageId,
|
||||||
|
error: { message: error.message },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageId) {
|
||||||
|
callbackState.delete(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'cancelProgress': {
|
||||||
|
const callback = callbackState.get(data.messageId);
|
||||||
|
if (callback) {
|
||||||
|
callback.isCanceled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTransferable(obj: any) {
|
||||||
|
return obj instanceof ArrayBuffer || obj instanceof ImageBitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleErrors(sendToOrigin: SendToOrigin) {
|
||||||
|
self.onerror = (e) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(e);
|
||||||
|
sendToOrigin({ type: 'unhandledError', error: { message: e.error.message || 'Uncaught exception in worker' } });
|
||||||
|
};
|
||||||
|
|
||||||
|
self.addEventListener('unhandledrejection', (e) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(e);
|
||||||
|
sendToOrigin({ type: 'unhandledError', error: { message: e.reason.message || 'Uncaught rejection in worker' } });
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,105 +0,0 @@
|
|||||||
import type { CancellableCallback, OriginMessageEvent, WorkerMessageData } from './WorkerConnector';
|
|
||||||
import { DEBUG } from '../config';
|
|
||||||
|
|
||||||
declare const self: WorkerGlobalScope;
|
|
||||||
|
|
||||||
handleErrors();
|
|
||||||
|
|
||||||
const callbackState = new Map<string, CancellableCallback>();
|
|
||||||
|
|
||||||
export default function createInterface(api: Record<string, Function>) {
|
|
||||||
onmessage = async (message: OriginMessageEvent) => {
|
|
||||||
const { data } = message;
|
|
||||||
|
|
||||||
switch (data.type) {
|
|
||||||
case 'callMethod': {
|
|
||||||
const {
|
|
||||||
messageId, name, args, withCallback,
|
|
||||||
} = data;
|
|
||||||
try {
|
|
||||||
if (messageId && withCallback) {
|
|
||||||
const callback = (...callbackArgs: any[]) => {
|
|
||||||
const lastArg = callbackArgs[callbackArgs.length - 1];
|
|
||||||
|
|
||||||
sendToOrigin({
|
|
||||||
type: 'methodCallback',
|
|
||||||
messageId,
|
|
||||||
callbackArgs,
|
|
||||||
}, isTransferable(lastArg) ? [lastArg] : undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
callbackState.set(messageId, callback);
|
|
||||||
|
|
||||||
args.push(callback as never);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [response, arrayBuffers] = (await api[name](...args)) || [];
|
|
||||||
|
|
||||||
if (messageId) {
|
|
||||||
sendToOrigin(
|
|
||||||
{
|
|
||||||
type: 'methodResponse',
|
|
||||||
messageId,
|
|
||||||
response,
|
|
||||||
},
|
|
||||||
arrayBuffers,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
if (DEBUG) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messageId) {
|
|
||||||
sendToOrigin({
|
|
||||||
type: 'methodResponse',
|
|
||||||
messageId,
|
|
||||||
error: { message: error.message },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messageId) {
|
|
||||||
callbackState.delete(messageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'cancelProgress': {
|
|
||||||
const callback = callbackState.get(data.messageId);
|
|
||||||
if (callback) {
|
|
||||||
callback.isCanceled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTransferable(obj: any) {
|
|
||||||
return obj instanceof ArrayBuffer || obj instanceof ImageBitmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleErrors() {
|
|
||||||
self.onerror = (e) => {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(e);
|
|
||||||
sendToOrigin({ type: 'unhandledError', error: { message: e.error.message || 'Uncaught exception in worker' } });
|
|
||||||
};
|
|
||||||
|
|
||||||
self.addEventListener('unhandledrejection', (e) => {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(e);
|
|
||||||
sendToOrigin({ type: 'unhandledError', error: { message: e.reason.message || 'Uncaught rejection in worker' } });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendToOrigin(data: WorkerMessageData, transferables?: Transferable[]) {
|
|
||||||
if (transferables) {
|
|
||||||
postMessage(data, transferables);
|
|
||||||
} else {
|
|
||||||
postMessage(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -113,6 +113,7 @@ export const IS_COMPACT_MENU = !IS_TOUCH_ENV;
|
|||||||
export const IS_SCROLL_PATCH_NEEDED = !IS_MAC_OS && !IS_IOS && !IS_ANDROID;
|
export const IS_SCROLL_PATCH_NEEDED = !IS_MAC_OS && !IS_IOS && !IS_ANDROID;
|
||||||
export const IS_INSTALL_PROMPT_SUPPORTED = 'onbeforeinstallprompt' in window;
|
export const IS_INSTALL_PROMPT_SUPPORTED = 'onbeforeinstallprompt' in window;
|
||||||
export const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in window;
|
export const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in window;
|
||||||
|
export const IS_TRANSLATION_SUPPORTED = Boolean(Intl.DisplayNames);
|
||||||
|
|
||||||
// Smaller area reduces scroll jumps caused by `patchChromiumScroll`
|
// Smaller area reduces scroll jumps caused by `patchChromiumScroll`
|
||||||
export const MESSAGE_LIST_SENSITIVE_AREA = IS_SCROLL_PATCH_NEEDED ? 300 : 750;
|
export const MESSAGE_LIST_SENSITIVE_AREA = IS_SCROLL_PATCH_NEEDED ? 300 : 750;
|
||||||
|
|||||||
@ -117,6 +117,17 @@ export function split<T extends any>(array: T[], chunkSize: number) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function partition<T extends unknown>(
|
||||||
|
array: T[], filter: (value: T, index: number, array: T[]) => boolean,
|
||||||
|
): [T[], T[]] {
|
||||||
|
const pass: T[] = [];
|
||||||
|
const fail: T[] = [];
|
||||||
|
|
||||||
|
array.forEach((e, idx, arr) => (filter(e, idx, arr) ? pass : fail).push(e));
|
||||||
|
|
||||||
|
return [pass, fail];
|
||||||
|
}
|
||||||
|
|
||||||
export function cloneDeep<T>(value: T): T {
|
export function cloneDeep<T>(value: T): T {
|
||||||
if (!isObject(value)) {
|
if (!isObject(value)) {
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
38
src/util/languageDetection.ts
Normal file
38
src/util/languageDetection.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import type { FastTextApi } from '../lib/fasttextweb/fasttext.worker';
|
||||||
|
import type { Connector } from './PostMessageConnector';
|
||||||
|
|
||||||
|
import { createConnector } from './PostMessageConnector';
|
||||||
|
import Deferred from './Deferred';
|
||||||
|
|
||||||
|
const WORKER_INIT_DELAY = 4000;
|
||||||
|
|
||||||
|
const DEFAULT_THRESHOLD = 0.2;
|
||||||
|
const DEFAULT_LABELS_COUNT = 5;
|
||||||
|
|
||||||
|
let worker: Connector<FastTextApi> | undefined;
|
||||||
|
const initializationDeferred = new Deferred();
|
||||||
|
|
||||||
|
setTimeout(initWorker, WORKER_INIT_DELAY);
|
||||||
|
|
||||||
|
function initWorker() {
|
||||||
|
if (!worker) {
|
||||||
|
worker = createConnector<FastTextApi>(
|
||||||
|
new Worker(new URL('../lib/fasttextweb/fasttext.worker.ts', import.meta.url)),
|
||||||
|
);
|
||||||
|
initializationDeferred.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function detectLanguage(text: string, threshold = DEFAULT_THRESHOLD) {
|
||||||
|
if (!worker) await initializationDeferred.promise;
|
||||||
|
const result = await worker!.request({ name: 'detectLanguage', args: [text, threshold] });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function detectLanguageProbability(
|
||||||
|
text: string, labelsCount = DEFAULT_LABELS_COUNT, threshold = DEFAULT_THRESHOLD,
|
||||||
|
) {
|
||||||
|
if (!worker) await initializationDeferred.promise;
|
||||||
|
const result = await worker!.request({ name: 'detectLanguageProbability', args: [text, labelsCount, threshold] });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user