diff --git a/.eslintignore b/.eslintignore index ea337da98..d9fde8543 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,7 @@ src/lib/rlottie/rlottie-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/api.d.ts diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 08d4abd8f..5a9561e75 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -61,6 +61,7 @@ type AllEmojis = Record; declare module '*.png'; declare module '*.svg'; declare module '*.tgs'; +declare module '*.wasm'; declare module '*.txt' { const content: string; diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 4cee6f120..1862c267b 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -1616,3 +1616,12 @@ function buildThreadInfo( ...(recentRepliers && { recentReplierIds: recentRepliers.map(getApiChatIdFromMtpPeer) }), }; } + +export function buildApiFormattedText(textWithEntities: GramJs.TextWithEntities): ApiFormattedText { + const { text, entities } = textWithEntities; + + return { + text, + entities: entities.map(buildApiMessageEntity), + }; +} diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 935574203..c7044d8c4 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -22,6 +22,7 @@ import type { ApiRequestInputInvoice, ApiChatReactions, ApiReaction, + ApiFormattedText, } from '../../types'; import { ApiMessageEntityTypes, @@ -598,3 +599,10 @@ export function buildInputEmojiStatus(emojiStatus: ApiSticker, expires?: number) documentId: BigInt(emojiStatus.id), }); } + +export function buildInputTextWithEntities(formatted: ApiFormattedText) { + return new GramJs.TextWithEntities({ + text: formatted.text, + entities: formatted.entities?.map(buildMtpMessageEntity) || [], + }); +} diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 3b9216529..8f47caec9 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -30,7 +30,7 @@ export { fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages, reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs, saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio, - closePoll, fetchExtendedMedia, + closePoll, fetchExtendedMedia, translateText, } from './messages'; export { diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index b3e3eb0fc..bcad3bd86 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -16,6 +16,7 @@ import type { ApiSendMessageAction, ApiContact, ApiPoll, + ApiFormattedText, } from '../../types'; import { MAIN_THREAD_ID, @@ -36,6 +37,7 @@ import { buildLocalMessage, buildWebPage, buildApiSponsoredMessage, + buildApiFormattedText, } from '../apiBuilders/messages'; import { buildApiUser } from '../apiBuilders/users'; import { @@ -51,6 +53,7 @@ import { isServiceMessageWithMedia, buildSendMessageAction, buildInputPollFromExisting, + buildInputTextWithEntities, } from '../gramjsBuilders'; import localDb from '../localDb'; import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/chats'; @@ -69,6 +72,15 @@ import { getServerTimeOffset } from '../../../util/serverTime'; const FAST_SEND_TIMEOUT = 1000; const INPUT_WAVEFORM_LENGTH = 63; +type TranslateTextParams = ({ + text: ApiFormattedText[]; +} | { + chat: ApiChat; + messageIds: number[]; +}) & { + toLanguageCode: string; +}; + let onUpdate: OnApiUpdate; export function init(_onUpdate: OnApiUpdate) { @@ -1560,3 +1572,38 @@ export async function transcribeAudio({ 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; +} diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index ca7738752..6d2cfe20f 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -601,6 +601,14 @@ export type ApiUpdateTopics = { chatId: string; }; +export type ApiUpdateMessageTranslations = { + '@type': 'updateMessageTranslations'; + chatId: string; + messageIds: number[]; + translations: ApiFormattedText[]; + toLanguageCode: string; +}; + export type ApiUpdate = ( ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | @@ -616,7 +624,7 @@ export type ApiUpdate = ( ApiUpdateFavoriteStickers | ApiUpdateStickerSet | ApiUpdateStickerSets | ApiUpdateStickerSetsOrder | ApiUpdateRecentStickers | ApiUpdateSavedGifs | ApiUpdateNewScheduledMessage | ApiUpdateMoveStickerSetToTop | ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | - ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | + ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations | ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent | ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy | ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions | diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index afb940f89..6a88d62e5 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -28,6 +28,7 @@ export { default as MessageSelectToolbar } from '../components/middle/MessageSel export { default as SeenByModal } from '../components/common/SeenByModal'; export { default as ReactorListModal } from '../components/middle/ReactorListModal'; export { default as EmojiInteractionAnimation } from '../components/middle/EmojiInteractionAnimation'; +export { default as MessageLanguageModal } from '../components/middle/MessageLanguageModal'; // eslint-disable-next-line import/no-cycle export { default as LeftSearch } from '../components/left/search/LeftSearch'; diff --git a/src/components/common/MessageText.tsx b/src/components/common/MessageText.tsx index 30478236c..25cb81f8f 100644 --- a/src/components/common/MessageText.tsx +++ b/src/components/common/MessageText.tsx @@ -1,15 +1,17 @@ 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 { ApiMessageEntityTypes } from '../../api/types'; import trimText from '../../util/trimText'; -import { getMessageText } from '../../global/helpers'; +import { getMessageText, stripCustomEmoji } from '../../global/helpers'; import { renderTextWithEntities } from './helpers/renderTextWithEntities'; interface OwnProps { message: ApiMessage; + translatedText?: ApiFormattedText; + isForAnimation?: boolean; emojiSize?: number; highlight?: string; isSimple?: boolean; @@ -25,6 +27,8 @@ const MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS = 3; function MessageText({ message, + translatedText, + isForAnimation, emojiSize, highlight, isSimple, @@ -40,7 +44,12 @@ function MessageText({ // eslint-disable-next-line no-null/no-null const sharedCanvasHqRef = useRef(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 hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler); if (hasSpoilers) { diff --git a/src/components/common/helpers/renderText.tsx b/src/components/common/helpers/renderText.tsx index aff5a6f1f..3208f744f 100644 --- a/src/components/common/helpers/renderText.tsx +++ b/src/components/common/helpers/renderText.tsx @@ -23,7 +23,6 @@ export type TextFilter = ( 'simple_markdown' | 'simple_markdown_html' ); -const RE_LETTER_OR_DIGIT = /^[\d\wа-яё]$/i; const SIMPLE_MARKDOWN_REGEX = /(\*\*|__).+?\1/g; export default function renderText( @@ -186,8 +185,7 @@ function addHighlight(textParts: TextPart[], highlight: string | undefined): Tex const lowerCaseText = part.toLowerCase(); const queryPosition = lowerCaseText.indexOf(highlight.toLowerCase()); - const nextSymbol = lowerCaseText[queryPosition + highlight.length]; - if (queryPosition < 0 || (nextSymbol && nextSymbol.match(RE_LETTER_OR_DIGIT))) { + if (queryPosition < 0) { result.push(part); return result; } @@ -200,7 +198,6 @@ function addHighlight(textParts: TextPart[], highlight: string | undefined): Tex , ); newParts.push(part.substring(queryPosition + highlight.length)); - return [...result, ...newParts]; }, []); } diff --git a/src/components/left/LeftColumn.tsx b/src/components/left/LeftColumn.tsx index b141468d1..0f5197015 100644 --- a/src/components/left/LeftColumn.tsx +++ b/src/components/left/LeftColumn.tsx @@ -298,6 +298,10 @@ const LeftColumn: FC = ({ case SettingsScreens.CustomEmoji: setSettingsScreen(SettingsScreens.Stickers); return; + + case SettingsScreens.DoNotTranslate: + setSettingsScreen(SettingsScreens.Language); + return; default: break; } diff --git a/src/components/left/settings/Settings.scss b/src/components/left/settings/Settings.scss index f0a2ded16..15f540954 100644 --- a/src/components/left/settings/Settings.scss +++ b/src/components/left/settings/Settings.scss @@ -306,6 +306,10 @@ .Radio:last-child { margin-bottom: 0; } + + .Checkbox { + margin-left: 0; + } } .Radio + .Radio, diff --git a/src/components/left/settings/Settings.tsx b/src/components/left/settings/Settings.tsx index 29a9962b9..dfde1348e 100644 --- a/src/components/left/settings/Settings.tsx +++ b/src/components/left/settings/Settings.tsx @@ -29,6 +29,7 @@ import SettingsQuickReaction from './SettingsQuickReaction'; import SettingsPasscode from './passcode/SettingsPasscode'; import SettingsStickers from './SettingsStickers'; import SettingsCustomEmoji from './SettingsCustomEmoji'; +import SettingsDoNotTranslate from './SettingsDoNotTranslate'; import SettingsExperimental from './SettingsExperimental'; import './Settings.scss'; @@ -249,7 +250,15 @@ const Settings: FC = ({ ); case SettingsScreens.Language: return ( - + + ); + case SettingsScreens.DoNotTranslate: + return ( + ); case SettingsScreens.Stickers: return ( diff --git a/src/components/left/settings/SettingsDoNotTranslate.module.scss b/src/components/left/settings/SettingsDoNotTranslate.module.scss new file mode 100644 index 000000000..0e0508c08 --- /dev/null +++ b/src/components/left/settings/SettingsDoNotTranslate.module.scss @@ -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(); +} diff --git a/src/components/left/settings/SettingsDoNotTranslate.tsx b/src/components/left/settings/SettingsDoNotTranslate.tsx new file mode 100644 index 000000000..d83746a8d --- /dev/null +++ b/src/components/left/settings/SettingsDoNotTranslate.tsx @@ -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; + +const SettingsDoNotTranslate: FC = ({ + isActive, + language, + doNotTranslate, + onReset, +}) => { + const { setSettingOption } = getActions(); + + const lang = useLang(); + const [displayedOptions, setDisplayedOptions] = useState([]); + 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) => { + 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) => { + 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 ( +
+
+ +
+ {filteredDisplayedOptions.map((option) => ( + + ))} +
+
+
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const { + language, doNotTranslate, + } = global.settings.byKey; + + return { + language, + doNotTranslate, + }; + }, +)(SettingsDoNotTranslate)); diff --git a/src/components/left/settings/SettingsHeader.tsx b/src/components/left/settings/SettingsHeader.tsx index 4b92bbd5e..bf825ff70 100644 --- a/src/components/left/settings/SettingsHeader.tsx +++ b/src/components/left/settings/SettingsHeader.tsx @@ -97,6 +97,8 @@ const SettingsHeader: FC = ({ return

{lang('PrivacySettings')}

; case SettingsScreens.Language: return

{lang('Language')}

; + case SettingsScreens.DoNotTranslate: + return

{lang('DoNotTranslate')}

; case SettingsScreens.Stickers: return

{lang('StickersName')}

; case SettingsScreens.Experimental: diff --git a/src/components/left/settings/SettingsLanguage.tsx b/src/components/left/settings/SettingsLanguage.tsx index ed6c2ceb3..cda78b0d1 100644 --- a/src/components/left/settings/SettingsLanguage.tsx +++ b/src/components/left/settings/SettingsLanguage.tsx @@ -1,31 +1,44 @@ -import type { FC } from '../../../lib/teact/teact'; import React, { memo, useCallback, useEffect, useMemo, useState, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; +import type { FC } from '../../../lib/teact/teact'; +import { SettingsScreens } from '../../../types'; import type { ISettings, LangCode } from '../../../types'; import type { ApiLanguage } from '../../../api/types'; 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 Loading from '../../ui/Loading'; -import useFlag from '../../../hooks/useFlag'; -import useHistoryBack from '../../../hooks/useHistoryBack'; +import Checkbox from '../../ui/Checkbox'; +import ListItem from '../../ui/ListItem'; type OwnProps = { isActive?: boolean; onReset: () => void; + onScreenSelect: (screen: SettingsScreens) => void; }; -type StateProps = Pick; +type StateProps = { + lastSyncTime?: number; +} & Pick; const SettingsLanguage: FC = ({ isActive, - onReset, languages, language, + canTranslate, + doNotTranslate, + lastSyncTime, + onScreenSelect, + onReset, }) => { const { loadLanguages, @@ -36,10 +49,13 @@ const SettingsLanguage: FC = ({ const [selectedLanguage, setSelectedLanguage] = useState(language); const [isLoading, markIsLoading, unmarkIsLoading] = useFlag(); - // TODO Throttle + const lang = useLang(); + useEffect(() => { - loadLanguages(); - }, [loadLanguages]); + if (lastSyncTime && !languages?.length) { + loadLanguages(); + } + }, [languages, lastSyncTime, loadLanguages]); const handleChange = useCallback((langCode: string) => { setSelectedLanguage(langCode); @@ -58,24 +74,65 @@ const SettingsLanguage: FC = ({ return languages ? buildOptions(languages) : undefined; }, [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({ isActive, onBack: onReset, }); return ( -
- {options ? ( - +
+ - ) : ( - - )} + {canTranslate && ( + + {lang('DoNotTranslate')} + {doNotTranslateText} + + )} +

+ {lang('lng_translate_settings_about')} +

+
+
+ {options ? ( + + ) : ( + + )} +
); }; @@ -95,9 +152,16 @@ function buildOptions(languages: ApiLanguage[]) { export default memo(withGlobal( (global): StateProps => { + const { + language, languages, canTranslate, doNotTranslate, + } = global.settings.byKey; + return { - languages: global.settings.byKey.languages, - language: global.settings.byKey.language, + lastSyncTime: global.lastSyncTime, + languages, + language, + canTranslate, + doNotTranslate, }; }, )(SettingsLanguage)); diff --git a/src/components/middle/MessageLanguageModal.async.tsx b/src/components/middle/MessageLanguageModal.async.tsx new file mode 100644 index 000000000..5e682ecf8 --- /dev/null +++ b/src/components/middle/MessageLanguageModal.async.tsx @@ -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 = (props) => { + const { isOpen } = props; + const MessageLanguageModal = useModuleLoader(Bundles.Extra, 'MessageLanguageModal', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return MessageLanguageModal ? : undefined; +}; + +export default memo(MessageLanguageModalAsync); diff --git a/src/components/middle/MessageLanguageModal.module.scss b/src/components/middle/MessageLanguageModal.module.scss new file mode 100644 index 000000000..c6e9ac038 --- /dev/null +++ b/src/components/middle/MessageLanguageModal.module.scss @@ -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(); +} diff --git a/src/components/middle/MessageLanguageModal.tsx b/src/components/middle/MessageLanguageModal.tsx new file mode 100644 index 000000000..3447bc464 --- /dev/null +++ b/src/components/middle/MessageLanguageModal.tsx @@ -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 = ({ + 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) => { + 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 ( + + +
+ {filteredDisplayedLanguages.map(({ langCode, originalName, translatedName }) => ( + handleSelect(langCode)} + > + + {renderText(originalName, ['highlight'], { highlight: search })} + + + {renderText(translatedName, ['highlight'], { highlight: search })} + + + ))} +
+
+ ); +}; + +export default memo(withGlobal( + (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)); diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index 5b0d8ec0a..64773b081 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -77,6 +77,7 @@ import SeenByModal from '../common/SeenByModal.async'; import EmojiInteractionAnimation from './EmojiInteractionAnimation.async'; import ReactorListModal from './ReactorListModal.async'; import GiftPremiumModal from '../main/premium/GiftPremiumModal.async'; +import MessageLanguageModal from './MessageLanguageModal.async'; import './MiddleColumn.scss'; import styles from './MiddleColumn.module.scss'; @@ -111,6 +112,7 @@ type StateProps = { isSeenByModalOpen: boolean; isReactorListModalOpen: boolean; isGiftPremiumModalOpen?: boolean; + isMessageLanguageModalOpen?: boolean; animationLevel: AnimationLevel; shouldSkipHistoryAnimations?: boolean; currentTransitionKey: number; @@ -156,6 +158,7 @@ const MiddleColumn: FC = ({ isSeenByModalOpen, isReactorListModalOpen, isGiftPremiumModalOpen, + isMessageLanguageModalOpen, animationLevel, shouldSkipHistoryAnimations, currentTransitionKey, @@ -551,6 +554,7 @@ const MiddleColumn: FC = ({ /> + @@ -596,6 +600,7 @@ export default memo(withGlobal( const { messageLists, isLeftColumnShown, activeEmojiInteractions, seenByModal, giftPremiumModal, reactorModal, audioPlayer, shouldSkipHistoryAnimations, + messageLanguageModal, } = selectTabState(global); const currentMessageList = selectCurrentMessageList(global); const { chats: { listIds }, lastSyncTime } = global; @@ -613,6 +618,7 @@ export default memo(withGlobal( isSeenByModalOpen: Boolean(seenByModal), isReactorListModalOpen: Boolean(reactorModal), isGiftPremiumModalOpen: giftPremiumModal?.isOpen, + isMessageLanguageModalOpen: Boolean(messageLanguageModal), animationLevel: global.settings.byKey.animationLevel, currentTransitionKey: Math.max(0, messageLists.length - 1), activeEmojiInteractions, diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index 3c69f9c94..5e5acc8e7 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -20,6 +20,8 @@ import { selectIsMessageProtected, selectIsPremiumPurchaseBlocked, selectMessageCustomEmojiSets, + selectMessageTranslations, + selectRequestedTranslationLanguage, selectStickerSet, } from '../../../global/selectors'; import { @@ -27,6 +29,7 @@ import { isChatGroup, isOwnMessage, areReactionsEmpty, isUserId, isMessageLocal, getMessageVideo, getChatMessageLink, } from '../../../global/helpers'; import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config'; +import { IS_TRANSLATION_SUPPORTED } from '../../../util/environment'; import buildClassName from '../../../util/buildClassName'; import { copyTextToClipboard } from '../../../util/clipboard'; @@ -49,6 +52,7 @@ export type OwnProps = { anchor: IAnchorPosition; messageListType: MessageListType; noReplies?: boolean; + detectedLanguage?: string; onClose: () => void; onCloseAnimationEnd: () => void; repliesThreadInfo?: ApiThreadInfo; @@ -74,6 +78,9 @@ type StateProps = { canFaveSticker?: boolean; canUnfaveSticker?: boolean; canCopy?: boolean; + canTranslate?: boolean; + canShowOriginal?: boolean; + canSelectLanguage?: boolean; isPrivate?: boolean; isCurrentUserPremium?: boolean; hasFullInfo?: boolean; @@ -101,8 +108,6 @@ const ContextMenuContainer: FC = ({ customEmojiSets, album, anchor, - onClose, - onCloseAnimationEnd, noOptions, canSendNow, hasFullInfo, @@ -135,7 +140,12 @@ const ContextMenuContainer: FC = ({ noReplies, canShowSeenBy, canScheduleUntilOnline, + canTranslate, + canShowOriginal, + canSelectLanguage, threadId, + onClose, + onCloseAnimationEnd, }) => { const { openChat, @@ -161,6 +171,9 @@ const ContextMenuContainer: FC = ({ cancelPollVote, closePoll, toggleReaction, + requestMessageTranslation, + showOriginalMessage, + openMessageLanguageModal, } = getActions(); const lang = useLang(); @@ -393,6 +406,30 @@ const ContextMenuContainer: FC = ({ closeMenu(); }, [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]); if (noOptions) { @@ -438,6 +475,9 @@ const ContextMenuContainer: FC = ({ canRevote={canRevote} canClosePoll={canClosePoll} canShowSeenBy={canShowSeenBy} + canTranslate={canTranslate} + canShowOriginal={canShowOriginal} + canSelectLanguage={canSelectLanguage} hasCustomEmoji={hasCustomEmoji} customEmojiSets={customEmojiSets} isDownloading={isDownloading} @@ -467,6 +507,9 @@ const ContextMenuContainer: FC = ({ onShowSeenBy={handleOpenSeenByModal} onToggleReaction={handleToggleReaction} onShowReactors={handleOpenReactorListModal} + onTranslate={handleTranslate} + onShowOriginal={handleShowOriginal} + onSelectLanguage={handleSelectLanguage} /> = ({ }; export default memo(withGlobal( - (global, { message, messageListType }): StateProps => { + (global, { message, messageListType, detectedLanguage }): StateProps => { const { threadId } = selectCurrentMessageList(global) || {}; const activeDownloads = selectActiveDownloadIds(global, message.chatId); const chat = selectChat(global, message.chatId); @@ -524,6 +567,7 @@ export default memo(withGlobal( canClosePoll, } = (threadId && selectAllowedMessageActions(global, message, threadId)) || {}; + const isOwn = isOwnMessage(message); const isPinned = messageListType === 'pinned'; const isScheduled = messageListType === 'scheduled'; const isChannel = chat && isChatChannel(chat); @@ -532,7 +576,7 @@ export default memo(withGlobal( && seenByMaxChatMembers && seenByExpiresAt && isChatGroup(chat) - && isOwnMessage(message) + && isOwn && !isScheduled && chat.membersCount && chat.membersCount <= seenByMaxChatMembers @@ -550,6 +594,18 @@ export default memo(withGlobal( const customEmojiSets = customEmojiSetsNotFiltered?.every(Boolean) ? 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 { availableReactions: global.availableReactions, noOptions, @@ -585,6 +641,9 @@ export default memo(withGlobal( customEmojiSets, canScheduleUntilOnline: selectCanScheduleUntilOnline(global, message.chatId), threadId, + canTranslate, + canShowOriginal: hasTranslation, + canSelectLanguage: hasTranslation, }; }, )(ContextMenuContainer)); diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index c2b6540d5..0a45f3e5c 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -9,7 +9,9 @@ import React, { } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; -import type { ActiveEmojiInteraction, ActiveReaction, MessageListType } from '../../../global/types'; +import type { + ActiveEmojiInteraction, ActiveReaction, ChatTranslatedMessages, MessageListType, +} from '../../../global/types'; import type { ApiMessage, ApiMessageOutgoingStatus, @@ -63,6 +65,8 @@ import { selectIsChatProtected, selectTopicFromMessage, selectTabState, + selectChatTranslations, + selectRequestedTranslationLanguage, } from '../../../global/selectors'; import { getMessageContent, @@ -113,6 +117,9 @@ import useInnerHandlers from './hooks/useInnerHandlers'; import useAppLayout from '../../../hooks/useAppLayout'; import useResizeObserver from '../../../hooks/useResizeObserver'; 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 Avatar from '../../common/Avatar'; @@ -233,6 +240,9 @@ type StateProps = { senderAdminMember?: ApiChatMember; messageTopic?: ApiTopic; hasTopicChip?: boolean; + chatTranslations?: ChatTranslatedMessages; + areTranslationsEnabled?: boolean; + requestedTranslationLanguage?: string; }; type MetaPosition = @@ -329,6 +339,9 @@ const Message: FC = ({ senderAdminMember, messageTopic, hasTopicChip, + chatTranslations, + areTranslationsEnabled, + requestedTranslationLanguage, }) => { const { toggleMessageSelection, @@ -468,6 +481,7 @@ const Message: FC = ({ handleAudioPlay, handleAlbumMediaClick, handleMetaClick, + handleTranslationClick, handleOpenThread, handleReadMedia, handleCancelUpload, @@ -537,6 +551,17 @@ const Message: FC = ({ text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice, location, action, game, } = 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 withCommentButton = repliesThreadInfo && !isInDocumentGroupNotLast && messageListType === 'thread' @@ -663,13 +688,15 @@ const Message: FC = ({ } 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) { noMediaCorners = true; } } } 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) { noMediaCorners = true; } @@ -707,6 +734,22 @@ const Message: FC = ({ ); } + function renderMessageText(isForAnimation?: boolean) { + return ( + + ); + } + function renderReactionsAndMeta() { const meta = ( = ({ signature={signature} withReactionOffset={reactionsPosition === 'inside'} availableReactions={availableReactions} + isTranslated={Boolean(requestedTranslationLanguage ? currentTranslatedText : undefined)} onClick={handleMetaClick} + onTranslationClick={handleTranslationClick} onOpenThread={handleOpenThread} /> ); @@ -749,6 +794,7 @@ const Message: FC = ({ const hasCustomAppendix = isLastInGroup && !hasText && !asForwarded && !hasThread; const textContentClass = buildClassName( 'text-content', + 'clearfix', metaPosition === 'in-text' && 'with-meta', outgoingStatus && 'with-outgoing-icon', ); @@ -954,15 +1000,14 @@ const Message: FC = ({ {!hasAnimatedEmoji && hasText && (
- + {renderMessageText()} + {isTranslationPending && ( +
+
+ {renderMessageText(true)} +
+
+ )} {metaPosition === 'in-text' && renderReactionsAndMeta()}
)} @@ -1208,6 +1253,7 @@ const Message: FC = ({ onCloseAnimationEnd={handleContextMenuHide} repliesThreadInfo={repliesThreadInfo} noReplies={noReplies} + detectedLanguage={detectedLanguage} /> )} @@ -1300,6 +1346,8 @@ export default memo(withGlobal( : undefined; const isLocation = Boolean(getMessageLocation(message)); + const chatTranslations = selectChatTranslations(global, chatId); + const requestedTranslationLanguage = selectRequestedTranslationLanguage(global, chatId, message.id); return { theme: selectTheme(global), @@ -1355,6 +1403,9 @@ export default memo(withGlobal( messageTopic, genericEffects: global.genericEmojiEffects, hasTopicChip, + chatTranslations, + areTranslationsEnabled: global.settings.byKey.canTranslate, + requestedTranslationLanguage, ...((canShowSender || isLocation) && { sender }), ...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }), ...(typeof uploadProgress === 'number' && { uploadProgress }), diff --git a/src/components/middle/message/MessageContextMenu.tsx b/src/components/middle/message/MessageContextMenu.tsx index 3c190ac36..ea4f46faf 100644 --- a/src/components/middle/message/MessageContextMenu.tsx +++ b/src/components/middle/message/MessageContextMenu.tsx @@ -61,6 +61,9 @@ type OwnProps = { canCopy?: boolean; canCopyLink?: boolean; canSelect?: boolean; + canTranslate?: boolean; + canShowOriginal?: boolean; + canSelectLanguage?: boolean; isPrivate?: boolean; isCurrentUserPremium?: boolean; canDownload?: boolean; @@ -99,6 +102,9 @@ type OwnProps = { onShowReactors?: () => void; onAboutAds?: () => void; onSponsoredHide?: () => void; + onTranslate?: () => void; + onShowOriginal?: () => void; + onSelectLanguage?: () => void; onToggleReaction?: (reaction: ApiReaction) => void; }; @@ -135,6 +141,9 @@ const MessageContextMenu: FC = ({ canSaveGif, canRevote, canClosePoll, + canTranslate, + canShowOriginal, + canSelectLanguage, isDownloading, repliesThreadInfo, canShowSeenBy, @@ -170,6 +179,9 @@ const MessageContextMenu: FC = ({ onCopyMessages, onAboutAds, onSponsoredHide, + onTranslate, + onShowOriginal, + onSelectLanguage, }) => { const { showNotification, openStickerSet, openCustomEmojiSets } = getActions(); // eslint-disable-next-line no-null/no-null @@ -319,6 +331,11 @@ const MessageContextMenu: FC = ({ {canUnfaveSticker && ( {lang('Stickers.RemoveFromFavorites')} )} + {canTranslate && {lang('TranslateMessage')}} + {canShowOriginal && {lang('ShowOriginalButton')}} + {canSelectLanguage && ( + {lang('lng_settings_change_lang')} + )} {canCopy && copyOptions.map((option) => ( {lang(option.label)} ))} diff --git a/src/components/middle/message/MessageMeta.scss b/src/components/middle/message/MessageMeta.scss index 7259f3536..8c932a880 100644 --- a/src/components/middle/message/MessageMeta.scss +++ b/src/components/middle/message/MessageMeta.scss @@ -17,7 +17,8 @@ .message-imported, .message-signature, .message-views, - .message-replies { + .message-replies, + .message-translated { font-size: 0.75rem; white-space: nowrap; } @@ -36,6 +37,10 @@ margin-inline-start: 0.1875rem; } + .message-translated { + margin-inline-end: 0.25rem; + } + .message-imported, .message-signature { overflow: hidden; diff --git a/src/components/middle/message/MessageMeta.tsx b/src/components/middle/message/MessageMeta.tsx index c8cc003d3..382a03cb6 100644 --- a/src/components/middle/message/MessageMeta.tsx +++ b/src/components/middle/message/MessageMeta.tsx @@ -27,8 +27,10 @@ type OwnProps = { availableReactions?: ApiAvailableReaction[]; noReplies?: boolean; repliesThreadInfo?: ApiThreadInfo; - onClick: (e: React.MouseEvent) => void; - onOpenThread: () => void; + isTranslated?: boolean; + onClick: (e: React.MouseEvent) => void; + onTranslationClick: (e: React.MouseEvent) => void; + onOpenThread: NoneToVoidFunction; }; const MessageMeta: FC = ({ @@ -38,7 +40,9 @@ const MessageMeta: FC = ({ withReactionOffset, repliesThreadInfo, noReplies, + isTranslated, onClick, + onTranslationClick, onOpenThread, }) => { const { showNotification } = getActions(); @@ -90,6 +94,9 @@ const MessageMeta: FC = ({ onClick={onClick} data-ignore-on-paste > + {isTranslated && ( + + )} {Boolean(message.views) && ( <> diff --git a/src/components/middle/message/_message-content.scss b/src/components/middle/message/_message-content.scss index 9d7346c1a..d059c4215 100644 --- a/src/components/middle/message/_message-content.scss +++ b/src/components/middle/message/_message-content.scss @@ -41,8 +41,10 @@ white-space: pre-wrap; line-height: 1.3125; text-align: initial; - display: flow-root; + display: block; unicode-bidi: plaintext; + border-radius: 0.25rem; + position: relative; } .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 { unicode-bidi: plaintext; } @@ -735,7 +771,6 @@ } .text-content { - margin-bottom: 1.25rem; word-break: normal; line-height: var(--emoji-only-size); font-size: var(--emoji-only-size); @@ -747,7 +782,7 @@ .MessageMeta { text-shadow: none; - bottom: 0; + bottom: -1.25rem; right: 0; line-height: normal; } @@ -903,3 +938,19 @@ font-family: var(--font-family-monospace); 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; + } +} diff --git a/src/components/middle/message/hooks/useInnerHandlers.ts b/src/components/middle/message/hooks/useInnerHandlers.ts index a7f904050..d4642b8a0 100644 --- a/src/components/middle/message/hooks/useInnerHandlers.ts +++ b/src/components/middle/message/hooks/useInnerHandlers.ts @@ -29,6 +29,7 @@ export default function useInnerHandlers( const { openChat, showNotification, focusMessage, openMediaViewer, openAudioPlayer, markMessagesRead, cancelSendingMessage, sendPollVote, openForwardMenu, focusMessageInComments, + openMessageLanguageModal, } = getActions(); const { @@ -154,12 +155,18 @@ export default function useInnerHandlers( focusMessageInComments, replyToTopMessageId, ]); - const selectWithGroupedId = useCallback((e: React.MouseEvent) => { + const selectWithGroupedId = useCallback((e: React.MouseEvent) => { e.stopPropagation(); selectMessage(e, groupedId); }, [selectMessage, groupedId]); + const handleTranslationClick = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + + openMessageLanguageModal({ chatId, id: messageId }); + }, [chatId, messageId, openMessageLanguageModal]); + const handleOpenThread = useCallback(() => { openChat({ id: message.chatId, @@ -185,6 +192,7 @@ export default function useInnerHandlers( handleAudioPlay, handleAlbumMediaClick, handleMetaClick: selectWithGroupedId, + handleTranslationClick, handleOpenThread, handleReadMedia, handleCancelUpload, diff --git a/src/components/middle/message/hooks/useMessageTranslation.ts b/src/components/middle/message/hooks/useMessageTranslation.ts new file mode 100644 index 000000000..61d2b3604 --- /dev/null +++ b/src/components/middle/message/hooks/useMessageTranslation.ts @@ -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, + }; +} diff --git a/src/components/ui/CheckboxGroup.tsx b/src/components/ui/CheckboxGroup.tsx index ffa4fb4c1..d84165826 100644 --- a/src/components/ui/CheckboxGroup.tsx +++ b/src/components/ui/CheckboxGroup.tsx @@ -4,9 +4,10 @@ import React, { useCallback, memo, useState } from '../../lib/teact/teact'; import Checkbox from './Checkbox'; -type IRadioOption = { +export type IRadioOption = { label: string; subLabel?: string; + disabled?: boolean; value: string; }; @@ -52,7 +53,7 @@ const CheckboxGroup: FC = ({ subLabel={option.subLabel} value={option.value} checked={selected.indexOf(option.value) !== -1} - disabled={disabled} + disabled={option.disabled || disabled} round={round} isLoading={loadingOptions ? loadingOptions.indexOf(option.value) !== -1 : undefined} onChange={handleChange} diff --git a/src/components/ui/Modal.scss b/src/components/ui/Modal.scss index c6d6e0213..a23d4c008 100644 --- a/src/components/ui/Modal.scss +++ b/src/components/ui/Modal.scss @@ -28,6 +28,16 @@ } } + &.slim { + .modal-dialog { + max-width: 25rem; + } + + .modal-content { + max-height: min(92vh, 32rem); + } + } + .modal-container { position: fixed; top: 0; diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index a36454a24..04056f1f1 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -26,6 +26,7 @@ type OwnProps = { className?: string; isOpen?: boolean; header?: TeactNode; + isSlim?: boolean; hasCloseButton?: boolean; noBackdrop?: boolean; noBackdropClose?: boolean; @@ -46,6 +47,7 @@ const Modal: FC = ({ title, className, isOpen, + isSlim, header, hasCloseButton, noBackdrop, @@ -136,6 +138,7 @@ const Modal: FC = ({ className, transitionClassNames, noBackdrop && 'transparent-backdrop', + isSlim && 'slim', ); return ( diff --git a/src/config.ts b/src/config.ts index 701141140..63ced2d81 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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 MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB 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 AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500]; 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.'; +// 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 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_-]+)'; diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 000d8561d..742e9bd5d 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -54,6 +54,9 @@ import { updateTopic, updateThreadInfo, replaceTabThreadParam, + updateRequestedMessageTranslation, + removeRequestedMessageTranslation, + updateMessageTranslation, } from '../../reducers'; import { selectChat, @@ -80,7 +83,9 @@ import { selectIsCurrentUserPremium, selectForwardsContainVoiceMessages, selectTabState, - selectThreadIdFromMessage, selectForwardsCanBeSentToChat, + selectThreadIdFromMessage, + selectLanguageCode, + selectForwardsCanBeSentToChat, } from '../../selectors'; import { debounce, onTickEnd, rafPromise, @@ -1411,6 +1416,49 @@ addActionHandler('forwardToSavedMessages', (global, actions, payload): ActionRet 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) { let count = 0; diff --git a/src/global/actions/apiUpdaters/messages.ts b/src/global/actions/apiUpdaters/messages.ts index 28e730e80..af33af711 100644 --- a/src/global/actions/apiUpdaters/messages.ts +++ b/src/global/actions/apiUpdaters/messages.ts @@ -27,6 +27,8 @@ import { updateThreadUnreadFromForwardedMessage, updateTopic, deleteTopic, + updateMessageTranslations, + clearMessageTranslation, } from '../../reducers'; import { selectChatMessage, @@ -211,6 +213,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { 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); break; @@ -639,6 +645,18 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { global = updateChatMessage(global, chatId, localId, { sendingState: 'messageSendingStateFailed' }); 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( isDeleting: true, }); + global = clearMessageTranslation(global, chatId, id); + const newLastMessage = findLastMessage(global, chatId); if (newLastMessage) { global = updateChatLastMessage(global, chatId, newLastMessage, true); diff --git a/src/global/actions/apiUpdaters/users.ts b/src/global/actions/apiUpdaters/users.ts index 7301231d0..3349c58e4 100644 --- a/src/global/actions/apiUpdaters/users.ts +++ b/src/global/actions/apiUpdaters/users.ts @@ -39,8 +39,17 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { case 'updateUser': { Object.values(global.byTabId).forEach(({ id: tabId }) => { - if (update.id === global.currentUserId && update.user.isPremium && !selectIsCurrentUserPremium(global)) { - actions.openPremiumModal({ isSuccess: true, tabId }); + if (update.id === global.currentUserId && update.user.isPremium !== selectIsCurrentUserPremium(global)) { + // 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: {}, + }, + }; } }); diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts index 5b094baff..1c6bb20a8 100644 --- a/src/global/actions/ui/messages.ts +++ b/src/global/actions/ui/messages.ts @@ -41,6 +41,7 @@ import { selectSender, selectChatScheduledMessages, selectTabState, + selectRequestedTranslationLanguage, } from '../../selectors'; import { compact, findLast } from '../../../util/iteratees'; import { getServerTime } from '../../../util/serverTime'; @@ -74,7 +75,7 @@ addActionHandler('setScrollOffset', (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); if (!currentMessageList) { return undefined; @@ -391,9 +392,9 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType => chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId, replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel, tabId = getCurrentTabId(), - } = payload!; + } = payload; - let { messageId } = payload!; + let { messageId } = payload; if (groupedId !== undefined) { const ids = selectForwardedMessageIdsByGroupId(global, groupedChatId!, groupedId); @@ -759,7 +760,7 @@ addActionHandler('createServiceNotification', (global, actions, payload): Action }); addActionHandler('openReactorListModal', (global, actions, payload): ActionReturnType => { - const { chatId, messageId, tabId = getCurrentTabId() } = payload!; + const { chatId, messageId, tabId = getCurrentTabId() } = payload; return updateTabState(global, { reactorModal: { chatId, messageId }, @@ -775,7 +776,7 @@ addActionHandler('closeReactorListModal', (global, actions, payload): ActionRetu }); addActionHandler('openSeenByModal', (global, actions, payload): ActionReturnType => { - const { chatId, messageId, tabId = getCurrentTabId() } = payload!; + const { chatId, messageId, tabId = getCurrentTabId() } = payload; return updateTabState(global, { seenByModal: { chatId, messageId }, @@ -790,6 +791,24 @@ addActionHandler('closeSeenByModal', (global, actions, payload): ActionReturnTyp }, 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 => { const { tabId = getCurrentTabId() } = payload; const tabState = selectTabState(global, tabId); diff --git a/src/global/cache.ts b/src/global/cache.ts index c365d0da5..0cadf0ee3 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -257,10 +257,6 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) { cached.recentCustomEmojis = []; } - if (cached.settings.byKey.shouldSuggestCustomEmoji === undefined) { - cached.settings.byKey.shouldSuggestCustomEmoji = true; - } - if (!cached.stickers.premiumSet) { cached.stickers.premiumSet = { stickers: [], diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 45e2560df..d9d2eaf42 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -165,6 +165,9 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { wasTimeFormatSetManually: false, isConnectionStatusMinimized: true, shouldArchiveAndMuteNewNonContact: false, + canTranslate: false, + canTranslateChats: true, + doNotTranslate: [], }, themes: { light: { @@ -184,6 +187,9 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { trustedBotIds: [], transcriptions: {}, + translations: { + byChatId: {}, + }, byTabId: {}, @@ -267,4 +273,8 @@ export const INITIAL_TAB_STATE: TabState = { pollModal: { isOpen: false, }, + + requestedTranslations: { + byChatId: {}, + }, }; diff --git a/src/global/reducers/index.ts b/src/global/reducers/index.ts index 52e487131..2c395a9aa 100644 --- a/src/global/reducers/index.ts +++ b/src/global/reducers/index.ts @@ -10,3 +10,4 @@ export * from './twoFaSettings'; export * from './passcode'; export * from './payments'; export * from './statistics'; +export * from './translations'; diff --git a/src/global/reducers/messages.ts b/src/global/reducers/messages.ts index b9be302be..cc3095b4c 100644 --- a/src/global/reducers/messages.ts +++ b/src/global/reducers/messages.ts @@ -1,7 +1,9 @@ import type { GlobalState, MessageList, MessageListType, TabArgs, Thread, TabThread, } 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 type { FocusDirection } from '../../types'; diff --git a/src/global/reducers/translations.ts b/src/global/reducers/translations.ts new file mode 100644 index 000000000..035d6e85e --- /dev/null +++ b/src/global/reducers/translations.ts @@ -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( + global: T, chatId: string, messageId: number, toLanguageCode: string, translation: Partial, +) { + 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( + 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>); + + return { + ...global, + translations: { + ...global.translations, + byChatId: { + ...global.translations.byChatId, + [chatId]: { + ...chatTranslations, + byLangCode: newByLangCode, + }, + }, + }, + }; +} + +export function updateMessageTranslations( + 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( + global: T, chatId: string, messageId: number, toLanguageCode: string, ...[tabId = getCurrentTabId()]: TabArgs +) { + 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( + global: T, chatId: string, messageId: number, ...[tabId = getCurrentTabId()]: TabArgs +) { + 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; +} diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index f622453cd..f08597fca 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -1,5 +1,5 @@ import type { - GlobalState, MessageListType, TabArgs, Thread, TabThread, + GlobalState, MessageListType, TabArgs, Thread, TabThread, ChatTranslatedMessages, } from '../types'; import type { ApiChat, @@ -1280,6 +1280,24 @@ export function selectForwardsContainVoiceMessages( }); } +export function selectChatTranslations( + global: T, chatId: string, +): ChatTranslatedMessages { + return global.translations.byChatId[chatId]; +} + +export function selectMessageTranslations( + global: T, chatId: string, toLanguageCode: string, +) { + return selectChatTranslations(global, chatId)?.byLangCode[toLanguageCode] || {}; +} + +export function selectRequestedTranslationLanguage( + global: T, chatId: string, messageId: number, tabId = getCurrentTabId(), +): string | undefined { + return selectTabState(global, tabId).requestedTranslations.byChatId[chatId]?.manualMessages?.[messageId]; +} + export function selectForwardsCanBeSentToChat( global: T, toChatId: string, diff --git a/src/global/selectors/settings.ts b/src/global/selectors/settings.ts index 3cf4c1a50..7dd3fc834 100644 --- a/src/global/selectors/settings.ts +++ b/src/global/selectors/settings.ts @@ -7,3 +7,7 @@ export function selectNotifySettings(global: T) { export function selectNotifyExceptions(global: T) { return global.settings.notifyExceptions; } + +export function selectLanguageCode(global: T) { + return global.settings.byKey.language.replace('-raw', ''); +} diff --git a/src/global/types.ts b/src/global/types.ts index 26d5f7d8f..a5ada802f 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -160,6 +160,20 @@ export type ApiLimitTypeWithModal = Exclude; +export type TranslatedMessage = { + isPending?: boolean; + text?: ApiFormattedText; +}; + +export type ChatTranslatedMessages = { + byLangCode: Record>; +}; + +export type ChatRequestedTranslations = { + toLanguage?: string; + manualMessages?: Record; +}; + export type TabState = { id: number; isMasterTab: boolean; @@ -520,6 +534,15 @@ export type TabState = { topicId: number; isLoading?: boolean; }; + + requestedTranslations: { + byChatId: Record; + }; + messageLanguageModal?: { + chatId: string; + messageId: number; + activeLanguage?: string; + }; }; export type GlobalState = { @@ -786,6 +809,10 @@ export type GlobalState = { isMinimized: boolean; isHidden: boolean; }; + + translations: { + byChatId: Record; + }; }; export type CallSound = ( @@ -1370,6 +1397,12 @@ export interface ActionPayloads { disableContextMenuHint: WithTabId | undefined; focusNextReply: WithTabId | undefined; + openMessageLanguageModal: { + chatId: string; + id: number; + } & WithTabId; + closeMessageLanguageModal: WithTabId | undefined; + // poll result openPollResults: { chatId: string; @@ -1665,6 +1698,23 @@ export interface ActionPayloads { ids: number[]; }; + requestMessageTranslation: { + chatId: string; + id: number; + toLanguageCode?: string; + } & WithTabId; + + showOriginalMessage: { + chatId: string; + id: number; + } & WithTabId; + + translateMessages: { + chatId: string; + messageIds: number[]; + toLanguageCode?: string; + }; + // Reactions loadAvailableReactions: undefined; diff --git a/src/hooks/useTextLanguage.ts b/src/hooks/useTextLanguage.ts new file mode 100644 index 000000000..7609589fe --- /dev/null +++ b/src/hooks/useTextLanguage.ts @@ -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; +} diff --git a/src/lib/fasttextweb/fasttext-wasm.js b/src/lib/fasttextweb/fasttext-wasm.js new file mode 100644 index 000000000..6341d22a0 --- /dev/null +++ b/src/lib/fasttextweb/fasttext-wasm.js @@ -0,0 +1,20 @@ + +var fasttextmodule = (() => { + var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined; + if (typeof __filename !== 'undefined') _scriptDir = _scriptDir || __filename; + return ( +function(fasttextmodule = {}) { + +var Module=typeof fasttextmodule!="undefined"?fasttextmodule:{};var readyPromiseResolve,readyPromiseReject;Module["ready"]=new Promise(function(resolve,reject){readyPromiseResolve=resolve;readyPromiseReject=reject});var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var read_,readAsync,readBinary,setWindowTitle;function logExceptionOnExit(e){if(e instanceof ExitStatus)return;let toLog=e;err("exiting due to exception: "+toLog)}if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");if(ENVIRONMENT_IS_WORKER){scriptDirectory=nodePath.dirname(scriptDirectory)+"/"}else{scriptDirectory=__dirname+"/"}read_=(filename,binary)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);return fs.readFileSync(filename,binary?undefined:"utf8")};readBinary=filename=>{var ret=read_(filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}return ret};readAsync=(filename,onload,onerror)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);fs.readFile(filename,function(err,data){if(err)onerror(err);else onload(data.buffer)})};if(process["argv"].length>1){thisProgram=process["argv"][1].replace(/\\/g,"/")}arguments_=process["argv"].slice(2);process["on"]("uncaughtException",function(ex){if(!(ex instanceof ExitStatus)){throw ex}});var nodeMajor=process.version.match(/^v(\d+)\./)[1];if(nodeMajor<15){process["on"]("unhandledRejection",function(reason){throw reason})}quit_=(status,toThrow)=>{if(keepRuntimeAlive()){process["exitCode"]=status;throw toThrow}logExceptionOnExit(toThrow);process["exit"](status)};Module["inspect"]=function(){return"[Emscripten Module object]"}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptDir){scriptDirectory=_scriptDir}if(scriptDirectory.indexOf("blob:")!==0){scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}else{scriptDirectory=""}{read_=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=(url,onload,onerror)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)}}setWindowTitle=title=>document.title=title}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.warn.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["quit"])quit_=Module["quit"];var wasmBinary;if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];var noExitRuntime=Module["noExitRuntime"]||true;if(typeof WebAssembly!="object"){abort("no native wasm support detected")}var wasmMemory;var ABORT=false;var EXITSTATUS;function assert(condition,text){if(!condition){abort(text)}}var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder("utf8"):undefined;function UTF8ArrayToString(heapOrArray,idx,maxBytesToRead){var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str}function UTF8ToString(ptr,maxBytesToRead){return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):""}function stringToUTF8Array(str,heap,outIdx,maxBytesToWrite){if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx}function stringToUTF8(str,outPtr,maxBytesToWrite){return stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite)}function lengthBytesUTF8(str){var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len}var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var wasmTable;var __ATPRERUN__=[];var __ATINIT__=[];var __ATMAIN__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function keepRuntimeAlive(){return noExitRuntime}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.init.initialized)FS.init();FS.ignorePermissions=false;TTY.init();callRuntimeCallbacks(__ATINIT__)}function preMain(){callRuntimeCallbacks(__ATMAIN__)}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}}function removeRunDependency(id){runDependencies--;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){if(Module["onAbort"]){Module["onAbort"](what)}what="Aborted("+what+")";err(what);ABORT=true;EXITSTATUS=1;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";function isDataURI(filename){return filename.startsWith(dataURIPrefix)}function isFileURI(filename){return filename.startsWith("file://")}var wasmBinaryFile;wasmBinaryFile="fasttext-wasm.wasm";if(!isDataURI(wasmBinaryFile)){wasmBinaryFile=locateFile(wasmBinaryFile)}function getBinary(file){try{if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}catch(err){abort(err)}}function getBinaryPromise(){if(!wasmBinary&&(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER)){if(typeof fetch=="function"&&!isFileURI(wasmBinaryFile)){return fetch(wasmBinaryFile,{credentials:"same-origin"}).then(function(response){if(!response["ok"]){throw"failed to load wasm binary file at '"+wasmBinaryFile+"'"}return response["arrayBuffer"]()}).catch(function(){return getBinary(wasmBinaryFile)})}else{if(readAsync){return new Promise(function(resolve,reject){readAsync(wasmBinaryFile,function(response){resolve(new Uint8Array(response))},reject)})}}}return Promise.resolve().then(function(){return getBinary(wasmBinaryFile)})}function createWasm(){var info={"env":wasmImports,"wasi_snapshot_preview1":wasmImports};function receiveInstance(instance,module){var exports=instance.exports;Module["asm"]=exports;wasmMemory=Module["asm"]["memory"];updateMemoryViews();wasmTable=Module["asm"]["__indirect_function_table"];addOnInit(Module["asm"]["__wasm_call_ctors"]);removeRunDependency("wasm-instantiate")}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}function instantiateArrayBuffer(receiver){return getBinaryPromise().then(function(binary){return WebAssembly.instantiate(binary,info)}).then(function(instance){return instance}).then(receiver,function(reason){err("failed to asynchronously prepare wasm: "+reason);abort(reason)})}function instantiateAsync(){if(!wasmBinary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(wasmBinaryFile)&&!isFileURI(wasmBinaryFile)&&!ENVIRONMENT_IS_NODE&&typeof fetch=="function"){return fetch(wasmBinaryFile,{credentials:"same-origin"}).then(function(response){var result=WebAssembly.instantiateStreaming(response,info);return result.then(receiveInstantiationResult,function(reason){err("wasm streaming compile failed: "+reason);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(receiveInstantiationResult)})})}else{return instantiateArrayBuffer(receiveInstantiationResult)}}if(Module["instantiateWasm"]){try{var exports=Module["instantiateWasm"](info,receiveInstance);return exports}catch(e){err("Module.instantiateWasm callback failed with error: "+e);readyPromiseReject(e)}}instantiateAsync().catch(readyPromiseReject);return{}}var tempDouble;var tempI64;var ASM_CONSTS={963844:()=>{FS.mount(NODEFS,{root:"."},".")}};function ExitStatus(status){this.name="ExitStatus";this.message="Program terminated with exit("+status+")";this.status=status}function callRuntimeCallbacks(callbacks){while(callbacks.length>0){callbacks.shift()(Module)}}function ___assert_fail(condition,filename,line,func){abort("Assertion failed: "+UTF8ToString(condition)+", at: "+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"])}function ExceptionInfo(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24;this.set_type=function(type){HEAPU32[this.ptr+4>>2]=type};this.get_type=function(){return HEAPU32[this.ptr+4>>2]};this.set_destructor=function(destructor){HEAPU32[this.ptr+8>>2]=destructor};this.get_destructor=function(){return HEAPU32[this.ptr+8>>2]};this.set_refcount=function(refcount){HEAP32[this.ptr>>2]=refcount};this.set_caught=function(caught){caught=caught?1:0;HEAP8[this.ptr+12>>0]=caught};this.get_caught=function(){return HEAP8[this.ptr+12>>0]!=0};this.set_rethrown=function(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13>>0]=rethrown};this.get_rethrown=function(){return HEAP8[this.ptr+13>>0]!=0};this.init=function(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor);this.set_refcount(0);this.set_caught(false);this.set_rethrown(false)};this.add_ref=function(){var value=HEAP32[this.ptr>>2];HEAP32[this.ptr>>2]=value+1};this.release_ref=function(){var prev=HEAP32[this.ptr>>2];HEAP32[this.ptr>>2]=prev-1;return prev===1};this.set_adjusted_ptr=function(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr};this.get_adjusted_ptr=function(){return HEAPU32[this.ptr+16>>2]};this.get_exception_ptr=function(){var isPointer=___cxa_is_pointer_type(this.get_type());if(isPointer){return HEAPU32[this.excPtr>>2]}var adjusted=this.get_adjusted_ptr();if(adjusted!==0)return adjusted;return this.excPtr}}var exceptionLast=0;var uncaughtExceptionCount=0;function ___cxa_throw(ptr,type,destructor){var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw ptr}function setErrNo(value){HEAP32[___errno_location()>>2]=value;return value}var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.substr(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir},basename:path=>{if(path==="/")return"/";path=PATH.normalize(path);path=path.replace(/\/$/,"");var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},join:function(){var paths=Array.prototype.slice.call(arguments);return PATH.normalize(paths.join("/"))},join2:(l,r)=>{return PATH.normalize(l+"/"+r)}};function getRandomDevice(){if(typeof crypto=="object"&&typeof crypto["getRandomValues"]=="function"){var randomBuffer=new Uint8Array(1);return()=>{crypto.getRandomValues(randomBuffer);return randomBuffer[0]}}else if(ENVIRONMENT_IS_NODE){try{var crypto_module=require("crypto");return()=>crypto_module["randomBytes"](1)[0]}catch(e){}}return()=>abort("randomDevice")}var PATH_FS={resolve:function(){var resolvedPath="",resolvedAbsolute=false;for(var i=arguments.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?arguments[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).substr(1);to=PATH_FS.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}var TTY={ttys:[],init:function(){},shutdown:function(){},register:function(dev,ops){TTY.ttys[dev]={input:[],output:[],ops:ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open:function(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close:function(stream){stream.tty.ops.fsync(stream.tty)},fsync:function(stream){stream.tty.ops.fsync(stream.tty)},read:function(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){result=buf.slice(0,bytesRead).toString("utf-8")}else{result=null}}else if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else if(typeof readline=="function"){result=readline();if(result!==null){result+="\n"}}if(!result){return null}tty.input=intArrayFromString(result,true)}return tty.input.shift()},put_char:function(tty,val){if(val===null||val===10){out(UTF8ArrayToString(tty.output,0));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync:function(tty){if(tty.output&&tty.output.length>0){out(UTF8ArrayToString(tty.output,0));tty.output=[]}}},default_tty1_ops:{put_char:function(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output,0));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync:function(tty){if(tty.output&&tty.output.length>0){err(UTF8ArrayToString(tty.output,0));tty.output=[]}}}};function mmapAlloc(size){abort()}var MEMFS={ops_table:null,mount:function(mount){return MEMFS.createNode(null,"/",16384|511,0)},createNode:function(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}if(!MEMFS.ops_table){MEMFS.ops_table={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,allocate:MEMFS.stream_ops.allocate,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}}}var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.timestamp=Date.now();if(parent){parent.contents[name]=node;parent.timestamp=node.timestamp}return node},getFileDataAsTypedArray:function(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage:function(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage:function(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr:function(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.timestamp);attr.mtime=new Date(node.timestamp);attr.ctime=new Date(node.timestamp);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr:function(node,attr){if(attr.mode!==undefined){node.mode=attr.mode}if(attr.timestamp!==undefined){node.timestamp=attr.timestamp}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup:function(parent,name){throw FS.genericErrors[44]},mknod:function(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename:function(old_node,new_dir,new_name){if(FS.isDir(old_node.mode)){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}}delete old_node.parent.contents[old_node.name];old_node.parent.timestamp=Date.now();old_node.name=new_name;new_dir.contents[new_name]=old_node;new_dir.timestamp=old_node.parent.timestamp;old_node.parent=new_dir},unlink:function(parent,name){delete parent.contents[name];parent.timestamp=Date.now()},rmdir:function(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.timestamp=Date.now()},readdir:function(node){var entries=[".",".."];for(var key in node.contents){if(!node.contents.hasOwnProperty(key)){continue}entries.push(key)}return entries},symlink:function(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink:function(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read:function(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{assert(arrayBuffer,'Loading data file "'+url+'" failed (no arrayBuffer).');onload(new Uint8Array(arrayBuffer));if(dep)removeRunDependency(dep)},event=>{if(onerror){onerror()}else{throw'Loading data file "'+url+'" failed.'}});if(dep)addRunDependency(dep)}var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,ErrnoError:null,genericErrors:{},filesystems:null,syncFSRequests:0,lookupPath:(path,opts={})=>{path=PATH_FS.resolve(path);if(!path)return{path:"",node:null};var defaults={follow_mount:true,recurse_count:0};opts=Object.assign(defaults,opts);if(opts.recurse_count>8){throw new FS.ErrnoError(32)}var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i40){throw new FS.ErrnoError(32)}}}}return{path:current_path,node:current}},getPath:node=>{var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!=="/"?mount+"/"+path:mount+path}path=path?node.name+"/"+path:node.name;node=node.parent}},hashName:(parentid,name)=>{var hash=0;for(var i=0;i>>0)%FS.nameTable.length},hashAddNode:node=>{var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode:node=>{var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode:(parent,name)=>{var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode,parent)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode:(parent,name,mode,rdev)=>{var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode:node=>{FS.hashRemoveNode(node)},isRoot:node=>{return node===node.parent},isMountpoint:node=>{return!!node.mounted},isFile:mode=>{return(mode&61440)===32768},isDir:mode=>{return(mode&61440)===16384},isLink:mode=>{return(mode&61440)===40960},isChrdev:mode=>{return(mode&61440)===8192},isBlkdev:mode=>{return(mode&61440)===24576},isFIFO:mode=>{return(mode&61440)===4096},isSocket:mode=>{return(mode&49152)===49152},flagModes:{"r":0,"r+":2,"w":577,"w+":578,"a":1089,"a+":1090},modeStringToFlags:str=>{var flags=FS.flagModes[str];if(typeof flags=="undefined"){throw new Error("Unknown file open mode: "+str)}return flags},flagsToPermissionString:flag=>{var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions:(node,perms)=>{if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup:dir=>{var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate:(dir,name)=>{try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete:(dir,name,isdir)=>{var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen:(node,flags)=>{if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&512){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},MAX_OPEN_FDS:4096,nextfd:(fd_start=0,fd_end=FS.MAX_OPEN_FDS)=>{for(var fd=fd_start;fd<=fd_end;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStream:fd=>FS.streams[fd],createStream:(stream,fd_start,fd_end)=>{if(!FS.FSStream){FS.FSStream=function(){this.shared={}};FS.FSStream.prototype={};Object.defineProperties(FS.FSStream.prototype,{object:{get:function(){return this.node},set:function(val){this.node=val}},isRead:{get:function(){return(this.flags&2097155)!==1}},isWrite:{get:function(){return(this.flags&2097155)!==0}},isAppend:{get:function(){return this.flags&1024}},flags:{get:function(){return this.shared.flags},set:function(val){this.shared.flags=val}},position:{get:function(){return this.shared.position},set:function(val){this.shared.position=val}}})}stream=Object.assign(new FS.FSStream,stream);var fd=FS.nextfd(fd_start,fd_end);stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream:fd=>{FS.streams[fd]=null},chrdev_stream_ops:{open:stream=>{var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;if(stream.stream_ops.open){stream.stream_ops.open(stream)}},llseek:()=>{throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice:(dev,ops)=>{FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts:mount=>{var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push.apply(check,m.mounts)}return mounts},syncfs:(populate,callback)=>{if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err("warning: "+FS.syncFSRequests+" FS.syncfs operations in flight at once, probably just doing extra work")}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount:(type,opts,mountpoint)=>{var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type:type,opts:opts,mountpoint:mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount:mountpoint=>{var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup:(parent,name)=>{return parent.node_ops.lookup(parent,name)},mknod:(path,mode,dev)=>{var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name||name==="."||name===".."){throw new FS.ErrnoError(28)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},create:(path,mode)=>{mode=mode!==undefined?mode:438;mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir:(path,mode)=>{mode=mode!==undefined?mode:511;mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree:(path,mode)=>{var dirs=path.split("/");var d="";for(var i=0;i{if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink:(oldpath,newpath)=>{if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename:(old_path,new_path)=>{var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name)}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir:path=>{var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir:path=>{var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;if(!node.node_ops.readdir){throw new FS.ErrnoError(54)}return node.node_ops.readdir(node)},unlink:path=>{var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink:path=>{var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return PATH_FS.resolve(FS.getPath(link.parent),link.node_ops.readlink(link))},stat:(path,dontFollow)=>{var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;if(!node){throw new FS.ErrnoError(44)}if(!node.node_ops.getattr){throw new FS.ErrnoError(63)}return node.node_ops.getattr(node)},lstat:path=>{return FS.stat(path,true)},chmod:(path,mode,dontFollow)=>{var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}if(!node.node_ops.setattr){throw new FS.ErrnoError(63)}node.node_ops.setattr(node,{mode:mode&4095|node.mode&~4095,timestamp:Date.now()})},lchmod:(path,mode)=>{FS.chmod(path,mode,true)},fchmod:(fd,mode)=>{var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}FS.chmod(stream.node,mode)},chown:(path,uid,gid,dontFollow)=>{var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}if(!node.node_ops.setattr){throw new FS.ErrnoError(63)}node.node_ops.setattr(node,{timestamp:Date.now()})},lchown:(path,uid,gid)=>{FS.chown(path,uid,gid,true)},fchown:(fd,uid,gid)=>{var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}FS.chown(stream.node,uid,gid)},truncate:(path,len)=>{if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}if(!node.node_ops.setattr){throw new FS.ErrnoError(63)}if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}node.node_ops.setattr(node,{size:len,timestamp:Date.now()})},ftruncate:(fd,len)=>{var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.truncate(stream.node,len)},utime:(path,atime,mtime)=>{var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;node.node_ops.setattr(node,{timestamp:Math.max(atime,mtime)})},open:(path,flags,mode)=>{if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS.modeStringToFlags(flags):flags;mode=typeof mode=="undefined"?438:mode;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;if(typeof path=="object"){node=path}else{path=PATH.normalize(path);try{var lookup=FS.lookupPath(path,{follow:!(flags&131072)});node=lookup.node}catch(e){}}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else{node=FS.mknod(path,mode,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node:node,path:FS.getPath(node),flags:flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(Module["logReadFiles"]&&!(flags&1)){if(!FS.readFiles)FS.readFiles={};if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close:stream=>{if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed:stream=>{return stream.fd===null},llseek:(stream,offset,whence)=>{if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read:(stream,buffer,offset,length,position)=>{if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write:(stream,buffer,offset,length,position,canOwn)=>{if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},allocate:(stream,offset,length)=>{if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(offset<0||length<=0){throw new FS.ErrnoError(28)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(!FS.isFile(stream.node.mode)&&!FS.isDir(stream.node.mode)){throw new FS.ErrnoError(43)}if(!stream.stream_ops.allocate){throw new FS.ErrnoError(138)}stream.stream_ops.allocate(stream,offset,length)},mmap:(stream,length,position,prot,flags)=>{if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync:(stream,buffer,offset,length,mmapFlags)=>{if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},munmap:stream=>0,ioctl:(stream,cmd,arg)=>{if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile:(path,opts={})=>{opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){throw new Error('Invalid encoding type "'+opts.encoding+'"')}var ret;var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){ret=UTF8ArrayToString(buf,0)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile:(path,data,opts={})=>{opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir:path=>{var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories:()=>{FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices:()=>{FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var random_device=getRandomDevice();FS.createDevice("/dev","random",random_device);FS.createDevice("/dev","urandom",random_device);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories:()=>{FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount:()=>{var node=FS.createNode(proc_self,"fd",16384|511,73);node.node_ops={lookup:(parent,name)=>{var fd=+name;var stream=FS.getStream(fd);if(!stream)throw new FS.ErrnoError(8);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path}};ret.parent=ret;return ret}};return node}},{},"/proc/self/fd")},createStandardStreams:()=>{if(Module["stdin"]){FS.createDevice("/dev","stdin",Module["stdin"])}else{FS.symlink("/dev/tty","/dev/stdin")}if(Module["stdout"]){FS.createDevice("/dev","stdout",null,Module["stdout"])}else{FS.symlink("/dev/tty","/dev/stdout")}if(Module["stderr"]){FS.createDevice("/dev","stderr",null,Module["stderr"])}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},ensureErrnoError:()=>{if(FS.ErrnoError)return;FS.ErrnoError=function ErrnoError(errno,node){this.node=node;this.setErrno=function(errno){this.errno=errno};this.setErrno(errno);this.message="FS error"};FS.ErrnoError.prototype=new Error;FS.ErrnoError.prototype.constructor=FS.ErrnoError;[44].forEach(code=>{FS.genericErrors[code]=new FS.ErrnoError(code);FS.genericErrors[code].stack=""})},staticInit:()=>{FS.ensureErrnoError();FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={"MEMFS":MEMFS}},init:(input,output,error)=>{FS.init.initialized=true;FS.ensureErrnoError();Module["stdin"]=input||Module["stdin"];Module["stdout"]=output||Module["stdout"];Module["stderr"]=error||Module["stderr"];FS.createStandardStreams()},quit:()=>{FS.init.initialized=false;for(var i=0;i{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode},findObject:(path,dontResolveLastLink)=>{var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath:(path,dontResolveLastLink)=>{try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath:(parent,path,canRead,canWrite)=>{parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){}parent=current}return current},createFile:(parent,name,properties,canRead,canWrite)=>{var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS.getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile:(parent,name,data,canRead,canWrite,canOwn)=>{var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS.getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;i{var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS.getMode(!!input,!!output);if(!FS.createDevice.major)FS.createDevice.major=64;var dev=FS.makedev(FS.createDevice.major++,0);FS.registerDevice(dev,{open:stream=>{stream.seekable=false},close:stream=>{if(output&&output.buffer&&output.buffer.length){output(10)}},read:(stream,buffer,offset,length,pos)=>{var bytesRead=0;for(var i=0;i{for(var i=0;i{if(obj.isDevice||obj.isFolder||obj.link||obj.contents)return true;if(typeof XMLHttpRequest!="undefined"){throw new Error("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread.")}else if(read_){try{obj.contents=intArrayFromString(read_(obj.url),true);obj.usedBytes=obj.contents.length}catch(e){throw new FS.ErrnoError(29)}}else{throw new Error("Cannot load without read() or XMLHttpRequest.")}},createLazyFile:(parent,name,url,canRead,canWrite)=>{function LazyUint8Array(){this.lengthKnown=false;this.chunks=[]}LazyUint8Array.prototype.get=function LazyUint8Array_get(idx){if(idx>this.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]};LazyUint8Array.prototype.setDataGetter=function LazyUint8Array_setDataGetter(getter){this.getter=getter};LazyUint8Array.prototype.cacheLength=function LazyUint8Array_cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true};if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;Object.defineProperties(lazyArray,{length:{get:function(){if(!this.lengthKnown){this.cacheLength()}return this._length}},chunkSize:{get:function(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}});var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url:url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=function forceLoadLazyFile(){FS.forceLoadFile(node);return fn.apply(null,arguments)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr:ptr,allocated:true}};node.stream_ops=stream_ops;return node},createPreloadedFile:(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency("cp "+fullname);function processData(byteArray){function finish(byteArray){if(preFinish)preFinish();if(!dontCreateFile){FS.createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}if(onload)onload();removeRunDependency(dep)}if(Browser.handledByPreloadPlugin(byteArray,fullname,finish,()=>{if(onerror)onerror();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url,byteArray=>processData(byteArray),onerror)}else{processData(url)}},indexedDB:()=>{return window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB},DB_NAME:()=>{return"EM_FS_"+window.location.pathname},DB_VERSION:20,DB_STORE_NAME:"FILE_DATA",saveFilesToDB:(paths,onload=(()=>{}),onerror=(()=>{}))=>{var indexedDB=FS.indexedDB();try{var openRequest=indexedDB.open(FS.DB_NAME(),FS.DB_VERSION)}catch(e){return onerror(e)}openRequest.onupgradeneeded=()=>{out("creating db");var db=openRequest.result;db.createObjectStore(FS.DB_STORE_NAME)};openRequest.onsuccess=()=>{var db=openRequest.result;var transaction=db.transaction([FS.DB_STORE_NAME],"readwrite");var files=transaction.objectStore(FS.DB_STORE_NAME);var ok=0,fail=0,total=paths.length;function finish(){if(fail==0)onload();else onerror()}paths.forEach(path=>{var putRequest=files.put(FS.analyzePath(path).object.contents,path);putRequest.onsuccess=()=>{ok++;if(ok+fail==total)finish()};putRequest.onerror=()=>{fail++;if(ok+fail==total)finish()}});transaction.onerror=onerror};openRequest.onerror=onerror},loadFilesFromDB:(paths,onload=(()=>{}),onerror=(()=>{}))=>{var indexedDB=FS.indexedDB();try{var openRequest=indexedDB.open(FS.DB_NAME(),FS.DB_VERSION)}catch(e){return onerror(e)}openRequest.onupgradeneeded=onerror;openRequest.onsuccess=()=>{var db=openRequest.result;try{var transaction=db.transaction([FS.DB_STORE_NAME],"readonly")}catch(e){onerror(e);return}var files=transaction.objectStore(FS.DB_STORE_NAME);var ok=0,fail=0,total=paths.length;function finish(){if(fail==0)onload();else onerror()}paths.forEach(path=>{var getRequest=files.get(path);getRequest.onsuccess=()=>{if(FS.analyzePath(path).exists){FS.unlink(path)}FS.createDataFile(PATH.dirname(path),PATH.basename(path),getRequest.result,true,true,true);ok++;if(ok+fail==total)finish()};getRequest.onerror=()=>{fail++;if(ok+fail==total)finish()}});transaction.onerror=onerror};openRequest.onerror=onerror}};var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt:function(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return PATH.join2(dir,path)},doStat:function(func,path,buf){try{var stat=func(path)}catch(e){if(e&&e.node&&PATH.normalize(path)!==PATH.normalize(FS.getPath(e.node))){return-54}throw e}HEAP32[buf>>2]=stat.dev;HEAP32[buf+8>>2]=stat.ino;HEAP32[buf+12>>2]=stat.mode;HEAPU32[buf+16>>2]=stat.nlink;HEAP32[buf+20>>2]=stat.uid;HEAP32[buf+24>>2]=stat.gid;HEAP32[buf+28>>2]=stat.rdev;tempI64=[stat.size>>>0,(tempDouble=stat.size,+Math.abs(tempDouble)>=1?tempDouble>0?(Math.min(+Math.floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>2]=tempI64[0],HEAP32[buf+44>>2]=tempI64[1];HEAP32[buf+48>>2]=4096;HEAP32[buf+52>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();tempI64=[Math.floor(atime/1e3)>>>0,(tempDouble=Math.floor(atime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?(Math.min(+Math.floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+56>>2]=tempI64[0],HEAP32[buf+60>>2]=tempI64[1];HEAPU32[buf+64>>2]=atime%1e3*1e3;tempI64=[Math.floor(mtime/1e3)>>>0,(tempDouble=Math.floor(mtime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?(Math.min(+Math.floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+72>>2]=tempI64[0],HEAP32[buf+76>>2]=tempI64[1];HEAPU32[buf+80>>2]=mtime%1e3*1e3;tempI64=[Math.floor(ctime/1e3)>>>0,(tempDouble=Math.floor(ctime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?(Math.min(+Math.floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+88>>2]=tempI64[0],HEAP32[buf+92>>2]=tempI64[1];HEAPU32[buf+96>>2]=ctime%1e3*1e3;tempI64=[stat.ino>>>0,(tempDouble=stat.ino,+Math.abs(tempDouble)>=1?tempDouble>0?(Math.min(+Math.floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+104>>2]=tempI64[0],HEAP32[buf+108>>2]=tempI64[1];return 0},doMsync:function(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},varargs:undefined,get:function(){SYSCALLS.varargs+=4;var ret=HEAP32[SYSCALLS.varargs-4>>2];return ret},getStr:function(ptr){var ret=UTF8ToString(ptr);return ret},getStreamFromFD:function(fd){var stream=FS.getStream(fd);if(!stream)throw new FS.ErrnoError(8);return stream}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=SYSCALLS.get();if(arg<0){return-28}var newStream;newStream=FS.createStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=SYSCALLS.get();stream.flags|=arg;return 0}case 5:{var arg=SYSCALLS.get();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 6:case 7:return 0;case 16:case 8:return-28;case 9:setErrNo(28);return-1;default:{return-28}}}catch(e){if(typeof FS=="undefined"||!(e instanceof FS.ErrnoError))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:case 21505:{if(!stream.tty)return-59;return 0}case 21510:case 21511:case 21512:case 21506:case 21507:case 21508:{if(!stream.tty)return-59;return 0}case 21519:{if(!stream.tty)return-59;var argp=SYSCALLS.get();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=SYSCALLS.get();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;return 0}case 21524:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e instanceof FS.ErrnoError))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?SYSCALLS.get():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e instanceof FS.ErrnoError))throw e;return-e.errno}}function __embind_register_bigint(primitiveType,name,size,minRange,maxRange){}function getShiftFromSize(size){switch(size){case 1:return 0;case 2:return 1;case 4:return 2;case 8:return 3;default:throw new TypeError("Unknown type size: "+size)}}function embind_init_charCodes(){var codes=new Array(256);for(var i=0;i<256;++i){codes[i]=String.fromCharCode(i)}embind_charCodes=codes}var embind_charCodes=undefined;function readLatin1String(ptr){var ret="";var c=ptr;while(HEAPU8[c]){ret+=embind_charCodes[HEAPU8[c++]]}return ret}var awaitingDependencies={};var registeredTypes={};var typeDependencies={};var char_0=48;var char_9=57;function makeLegalFunctionName(name){if(undefined===name){return"_unknown"}name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return"_"+name}return name}function createNamedFunction(name,body){name=makeLegalFunctionName(name);return new Function("body","return function "+name+"() {\n"+' "use strict";'+" return body.apply(this, arguments);\n"+"};\n")(body)}function extendError(baseErrorType,errorName){var errorClass=createNamedFunction(errorName,function(message){this.name=errorName;this.message=message;var stack=new Error(message).stack;if(stack!==undefined){this.stack=this.toString()+"\n"+stack.replace(/^Error(:[^\n]*)?\n/,"")}});errorClass.prototype=Object.create(baseErrorType.prototype);errorClass.prototype.constructor=errorClass;errorClass.prototype.toString=function(){if(this.message===undefined){return this.name}else{return this.name+": "+this.message}};return errorClass}var BindingError=undefined;function throwBindingError(message){throw new BindingError(message)}var InternalError=undefined;function throwInternalError(message){throw new InternalError(message)}function whenDependentTypesAreResolved(myTypes,dependentTypes,getTypeConverters){myTypes.forEach(function(type){typeDependencies[type]=dependentTypes});function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i{if(registeredTypes.hasOwnProperty(dt)){typeConverters[i]=registeredTypes[dt]}else{unregisteredTypes.push(dt);if(!awaitingDependencies.hasOwnProperty(dt)){awaitingDependencies[dt]=[]}awaitingDependencies[dt].push(()=>{typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}});if(0===unregisteredTypes.length){onComplete(typeConverters)}}function registerType(rawType,registeredInstance,options={}){if(!("argPackAdvance"in registeredInstance)){throw new TypeError("registerType registeredInstance requires argPackAdvance")}var name=registeredInstance.name;if(!rawType){throwBindingError('type "'+name+'" must have a positive integer typeid pointer')}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError("Cannot register type '"+name+"' twice")}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function __embind_register_bool(rawType,name,size,trueValue,falseValue){var shift=getShiftFromSize(size);name=readLatin1String(name);registerType(rawType,{name:name,"fromWireType":function(wt){return!!wt},"toWireType":function(destructors,o){return o?trueValue:falseValue},"argPackAdvance":8,"readValueFromPointer":function(pointer){var heap;if(size===1){heap=HEAP8}else if(size===2){heap=HEAP16}else if(size===4){heap=HEAP32}else{throw new TypeError("Unknown boolean type size: "+name)}return this["fromWireType"](heap[pointer>>shift])},destructorFunction:null})}var emval_free_list=[];var emval_handle_array=[{},{value:undefined},{value:null},{value:true},{value:false}];function __emval_decref(handle){if(handle>4&&0===--emval_handle_array[handle].refcount){emval_handle_array[handle]=undefined;emval_free_list.push(handle)}}function count_emval_handles(){var count=0;for(var i=5;i{if(!handle){throwBindingError("Cannot use deleted val. handle = "+handle)}return emval_handle_array[handle].value},toHandle:value=>{switch(value){case undefined:return 1;case null:return 2;case true:return 3;case false:return 4;default:{var handle=emval_free_list.length?emval_free_list.pop():emval_handle_array.length;emval_handle_array[handle]={refcount:1,value:value};return handle}}}};function simpleReadValueFromPointer(pointer){return this["fromWireType"](HEAP32[pointer>>2])}function __embind_register_emval(rawType,name){name=readLatin1String(name);registerType(rawType,{name:name,"fromWireType":function(handle){var rv=Emval.toValue(handle);__emval_decref(handle);return rv},"toWireType":function(destructors,value){return Emval.toHandle(value)},"argPackAdvance":8,"readValueFromPointer":simpleReadValueFromPointer,destructorFunction:null})}function floatReadValueFromPointer(name,shift){switch(shift){case 2:return function(pointer){return this["fromWireType"](HEAPF32[pointer>>2])};case 3:return function(pointer){return this["fromWireType"](HEAPF64[pointer>>3])};default:throw new TypeError("Unknown float type: "+name)}}function __embind_register_float(rawType,name,size){var shift=getShiftFromSize(size);name=readLatin1String(name);registerType(rawType,{name:name,"fromWireType":function(value){return value},"toWireType":function(destructors,value){return value},"argPackAdvance":8,"readValueFromPointer":floatReadValueFromPointer(name,shift),destructorFunction:null})}function new_(constructor,argumentList){if(!(constructor instanceof Function)){throw new TypeError("new_ called with constructor type "+typeof constructor+" which is not a function")}var dummy=createNamedFunction(constructor.name||"unknownFunctionName",function(){});dummy.prototype=constructor.prototype;var obj=new dummy;var r=constructor.apply(obj,argumentList);return r instanceof Object?r:obj}function runDestructors(destructors){while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}}function craftInvokerFunction(humanName,argTypes,classType,cppInvokerFunc,cppTargetFunc){var argCount=argTypes.length;if(argCount<2){throwBindingError("argTypes array size mismatch! Must at least get return value and 'this' types!")}var isClassMethodFunc=argTypes[1]!==null&&classType!==null;var needsDestructorStack=false;for(var i=1;i0?", ":"")+argsListWired}invokerFnBody+=(returns?"var rv = ":"")+"invoker(fn"+(argsListWired.length>0?", ":"")+argsListWired+");\n";if(needsDestructorStack){invokerFnBody+="runDestructors(destructors);\n"}else{for(var i=isClassMethodFunc?1:2;i>2])}return array}function replacePublicSymbol(name,value,numArguments){if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistant public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}}function dynCallLegacy(sig,ptr,args){var f=Module["dynCall_"+sig];return args&&args.length?f.apply(null,[ptr].concat(args)):f.call(null,ptr)}var wasmTableMirror=[];function getWasmTableEntry(funcPtr){var func=wasmTableMirror[funcPtr];if(!func){if(funcPtr>=wasmTableMirror.length)wasmTableMirror.length=funcPtr+1;wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func}function dynCall(sig,ptr,args){if(sig.includes("j")){return dynCallLegacy(sig,ptr,args)}var rtn=getWasmTableEntry(ptr).apply(null,args);return rtn}function getDynCaller(sig,ptr){var argCache=[];return function(){argCache.length=0;Object.assign(argCache,arguments);return dynCall(sig,ptr,argCache)}}function embind__requireFunction(signature,rawFunction){signature=readLatin1String(signature);function makeDynCaller(){if(signature.includes("j")){return getDynCaller(signature,rawFunction)}return getWasmTableEntry(rawFunction)}var fp=makeDynCaller();if(typeof fp!="function"){throwBindingError("unknown function pointer with signature "+signature+": "+rawFunction)}return fp}var UnboundTypeError=undefined;function getTypeName(type){var ptr=___getTypeName(type);var rv=readLatin1String(ptr);_free(ptr);return rv}function throwUnboundTypeError(message,types){var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(message+": "+unboundTypes.map(getTypeName).join([", "]))}function __embind_register_function(name,argCount,rawArgTypesAddr,signature,rawInvoker,fn){var argTypes=heap32VectorToArray(argCount,rawArgTypesAddr);name=readLatin1String(name);rawInvoker=embind__requireFunction(signature,rawInvoker);exposePublicSymbol(name,function(){throwUnboundTypeError("Cannot call "+name+" due to unbound types",argTypes)},argCount-1);whenDependentTypesAreResolved([],argTypes,function(argTypes){var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));replacePublicSymbol(name,craftInvokerFunction(name,invokerArgsArray,null,rawInvoker,fn),argCount-1);return[]})}function integerReadValueFromPointer(name,shift,signed){switch(shift){case 0:return signed?function readS8FromPointer(pointer){return HEAP8[pointer]}:function readU8FromPointer(pointer){return HEAPU8[pointer]};case 1:return signed?function readS16FromPointer(pointer){return HEAP16[pointer>>1]}:function readU16FromPointer(pointer){return HEAPU16[pointer>>1]};case 2:return signed?function readS32FromPointer(pointer){return HEAP32[pointer>>2]}:function readU32FromPointer(pointer){return HEAPU32[pointer>>2]};default:throw new TypeError("Unknown integer type: "+name)}}function __embind_register_integer(primitiveType,name,size,minRange,maxRange){name=readLatin1String(name);if(maxRange===-1){maxRange=4294967295}var shift=getShiftFromSize(size);var fromWireType=value=>value;if(minRange===0){var bitshift=32-8*size;fromWireType=value=>value<>>bitshift}var isUnsignedType=name.includes("unsigned");var checkAssertions=(value,toTypeName)=>{};var toWireType;if(isUnsignedType){toWireType=function(destructors,value){checkAssertions(value,this.name);return value>>>0}}else{toWireType=function(destructors,value){checkAssertions(value,this.name);return value}}registerType(primitiveType,{name:name,"fromWireType":fromWireType,"toWireType":toWireType,"argPackAdvance":8,"readValueFromPointer":integerReadValueFromPointer(name,shift,minRange!==0),destructorFunction:null})}function __embind_register_memory_view(rawType,dataTypeIndex,name){var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){handle=handle>>2;var heap=HEAPU32;var size=heap[handle];var data=heap[handle+1];return new TA(heap.buffer,data,size)}name=readLatin1String(name);registerType(rawType,{name:name,"fromWireType":decodeMemoryView,"argPackAdvance":8,"readValueFromPointer":decodeMemoryView},{ignoreDuplicateRegistrations:true})}function __embind_register_std_string(rawType,name){name=readLatin1String(name);var stdStringIsUTF8=name==="std::string";registerType(rawType,{name:name,"fromWireType":function(value){var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){var decodeStartPtr=payload;for(var i=0;i<=length;++i){var currentBytePtr=payload+i;if(i==length||HEAPU8[currentBytePtr]==0){var maxRead=currentBytePtr-decodeStartPtr;var stringSegment=UTF8ToString(decodeStartPtr,maxRead);if(str===undefined){str=stringSegment}else{str+=String.fromCharCode(0);str+=stringSegment}decodeStartPtr=currentBytePtr+1}}}else{var a=new Array(length);for(var i=0;i>2]=length;if(stdStringIsUTF8&&valueIsOfTypeString){stringToUTF8(value,ptr,length+1)}else{if(valueIsOfTypeString){for(var i=0;i255){_free(ptr);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+i]=charCode}}else{for(var i=0;i>1;var maxIdx=idx+maxBytesToRead/2;while(!(idx>=maxIdx)&&HEAPU16[idx])++idx;endPtr=idx<<1;if(endPtr-ptr>32&&UTF16Decoder)return UTF16Decoder.decode(HEAPU8.subarray(ptr,endPtr));var str="";for(var i=0;!(i>=maxBytesToRead/2);++i){var codeUnit=HEAP16[ptr+i*2>>1];if(codeUnit==0)break;str+=String.fromCharCode(codeUnit)}return str}function stringToUTF16(str,outPtr,maxBytesToWrite){if(maxBytesToWrite===undefined){maxBytesToWrite=2147483647}if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr}function lengthBytesUTF16(str){return str.length*2}function UTF32ToString(ptr,maxBytesToRead){var i=0;var str="";while(!(i>=maxBytesToRead/4)){var utf32=HEAP32[ptr+i*4>>2];if(utf32==0)break;++i;if(utf32>=65536){var ch=utf32-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}else{str+=String.fromCharCode(utf32)}}return str}function stringToUTF32(str,outPtr,maxBytesToWrite){if(maxBytesToWrite===undefined){maxBytesToWrite=2147483647}if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i=55296&&codeUnit<=57343){var trailSurrogate=str.charCodeAt(++i);codeUnit=65536+((codeUnit&1023)<<10)|trailSurrogate&1023}HEAP32[outPtr>>2]=codeUnit;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr}function lengthBytesUTF32(str){var len=0;for(var i=0;i=55296&&codeUnit<=57343)++i;len+=4}return len}function __embind_register_std_wstring(rawType,charSize,name){name=readLatin1String(name);var decodeString,encodeString,getHeap,lengthBytesUTF,shift;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16;getHeap=()=>HEAPU16;shift=1}else if(charSize===4){decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32;getHeap=()=>HEAPU32;shift=2}registerType(rawType,{name:name,"fromWireType":function(value){var length=HEAPU32[value>>2];var HEAP=getHeap();var str;var decodeStartPtr=value+4;for(var i=0;i<=length;++i){var currentBytePtr=value+4+i*charSize;if(i==length||HEAP[currentBytePtr>>shift]==0){var maxReadBytes=currentBytePtr-decodeStartPtr;var stringSegment=decodeString(decodeStartPtr,maxReadBytes);if(str===undefined){str=stringSegment}else{str+=String.fromCharCode(0);str+=stringSegment}decodeStartPtr=currentBytePtr+charSize}}_free(value);return str},"toWireType":function(destructors,value){if(!(typeof value=="string")){throwBindingError("Cannot pass non-string to C++ string type "+name)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length>>shift;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},"argPackAdvance":8,"readValueFromPointer":simpleReadValueFromPointer,destructorFunction:function(ptr){_free(ptr)}})}function __embind_register_void(rawType,name){name=readLatin1String(name);registerType(rawType,{isVoid:true,name:name,"argPackAdvance":0,"fromWireType":function(){return undefined},"toWireType":function(destructors,o){return undefined}})}function __emscripten_fs_load_embedded_files(ptr){do{var name_addr=HEAPU32[ptr>>2];ptr+=4;var len=HEAPU32[ptr>>2];ptr+=4;var content=HEAPU32[ptr>>2];ptr+=4;var name=UTF8ToString(name_addr);FS.createPath("/",PATH.dirname(name),true,true);FS.createDataFile(name,null,HEAP8.subarray(content,content+len),true,true,true)}while(HEAPU32[ptr>>2])}function _abort(){abort("")}var readEmAsmArgsArray=[];function readEmAsmArgs(sigPtr,buf){readEmAsmArgsArray.length=0;var ch;buf>>=2;while(ch=HEAPU8[sigPtr++]){buf+=ch!=105&buf;readEmAsmArgsArray.push(ch==105?HEAP32[buf]:HEAPF64[buf++>>1]);++buf}return readEmAsmArgsArray}function runEmAsmFunction(code,sigPtr,argbuf){var args=readEmAsmArgs(sigPtr,argbuf);return ASM_CONSTS[code].apply(null,args)}function _emscripten_asm_const_int(code,sigPtr,argbuf){return runEmAsmFunction(code,sigPtr,argbuf)}function _emscripten_memcpy_big(dest,src,num){HEAPU8.copyWithin(dest,src,src+num)}function abortOnCannotGrowMemory(requestedSize){abort("OOM")}function _emscripten_resize_heap(requestedSize){var oldSize=HEAPU8.length;requestedSize=requestedSize>>>0;abortOnCannotGrowMemory(requestedSize)}var ENV={};function getExecutableName(){return thisProgram||"./this.program"}function getEnvStrings(){if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={"USER":"web_user","LOGNAME":"web_user","PATH":"/","PWD":"/","HOME":"/home/web_user","LANG":lang,"_":getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(x+"="+env[x])}getEnvStrings.strings=strings}return getEnvStrings.strings}function writeAsciiToMemory(str,buffer,dontAddNull){for(var i=0;i>0]=str.charCodeAt(i)}if(!dontAddNull)HEAP8[buffer>>0]=0}function _environ_get(__environ,environ_buf){var bufSize=0;getEnvStrings().forEach(function(string,i){var ptr=environ_buf+bufSize;HEAPU32[__environ+i*4>>2]=ptr;writeAsciiToMemory(string,ptr);bufSize+=string.length+1});return 0}function _environ_sizes_get(penviron_count,penviron_buf_size){var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;strings.forEach(function(string){bufSize+=string.length+1});HEAPU32[penviron_buf_size>>2]=bufSize;return 0}function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e instanceof FS.ErrnoError))throw e;return e.errno}}function doReadv(stream,iov,iovcnt,offset){var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e instanceof FS.ErrnoError))throw e;return e.errno}}function convertI32PairToI53Checked(lo,hi){return hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN}function _fd_seek(fd,offset_low,offset_high,whence,newOffset){try{var offset=convertI32PairToI53Checked(offset_low,offset_high);if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);tempI64=[stream.position>>>0,(tempDouble=stream.position,+Math.abs(tempDouble)>=1?tempDouble>0?(Math.min(+Math.floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[newOffset>>2]=tempI64[0],HEAP32[newOffset+4>>2]=tempI64[1];if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e instanceof FS.ErrnoError))throw e;return e.errno}}function doWritev(stream,iov,iovcnt,offset){var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(typeof offset!=="undefined"){offset+=curr}}return ret}function _fd_write(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doWritev(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e instanceof FS.ErrnoError))throw e;return e.errno}}function __isLeapYear(year){return year%4===0&&(year%100!==0||year%400===0)}function __arraySum(array,index){var sum=0;for(var i=0;i<=index;sum+=array[i++]){}return sum}var __MONTH_DAYS_LEAP=[31,29,31,30,31,30,31,31,30,31,30,31];var __MONTH_DAYS_REGULAR=[31,28,31,30,31,30,31,31,30,31,30,31];function __addDays(date,days){var newDate=new Date(date.getTime());while(days>0){var leap=__isLeapYear(newDate.getFullYear());var currentMonth=newDate.getMonth();var daysInCurrentMonth=(leap?__MONTH_DAYS_LEAP:__MONTH_DAYS_REGULAR)[currentMonth];if(days>daysInCurrentMonth-newDate.getDate()){days-=daysInCurrentMonth-newDate.getDate()+1;newDate.setDate(1);if(currentMonth<11){newDate.setMonth(currentMonth+1)}else{newDate.setMonth(0);newDate.setFullYear(newDate.getFullYear()+1)}}else{newDate.setDate(newDate.getDate()+days);return newDate}}return newDate}function writeArrayToMemory(array,buffer){HEAP8.set(array,buffer)}function _strftime(s,maxsize,format,tm){var tm_zone=HEAP32[tm+40>>2];var date={tm_sec:HEAP32[tm>>2],tm_min:HEAP32[tm+4>>2],tm_hour:HEAP32[tm+8>>2],tm_mday:HEAP32[tm+12>>2],tm_mon:HEAP32[tm+16>>2],tm_year:HEAP32[tm+20>>2],tm_wday:HEAP32[tm+24>>2],tm_yday:HEAP32[tm+28>>2],tm_isdst:HEAP32[tm+32>>2],tm_gmtoff:HEAP32[tm+36>>2],tm_zone:tm_zone?UTF8ToString(tm_zone):""};var pattern=UTF8ToString(format);var EXPANSION_RULES_1={"%c":"%a %b %d %H:%M:%S %Y","%D":"%m/%d/%y","%F":"%Y-%m-%d","%h":"%b","%r":"%I:%M:%S %p","%R":"%H:%M","%T":"%H:%M:%S","%x":"%m/%d/%y","%X":"%H:%M:%S","%Ec":"%c","%EC":"%C","%Ex":"%m/%d/%y","%EX":"%H:%M:%S","%Ey":"%y","%EY":"%Y","%Od":"%d","%Oe":"%e","%OH":"%H","%OI":"%I","%Om":"%m","%OM":"%M","%OS":"%S","%Ou":"%u","%OU":"%U","%OV":"%V","%Ow":"%w","%OW":"%W","%Oy":"%y"};for(var rule in EXPANSION_RULES_1){pattern=pattern.replace(new RegExp(rule,"g"),EXPANSION_RULES_1[rule])}var WEEKDAYS=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];var MONTHS=["January","February","March","April","May","June","July","August","September","October","November","December"];function leadingSomething(value,digits,character){var str=typeof value=="number"?value.toString():value||"";while(str.length0?1:0}var compare;if((compare=sgn(date1.getFullYear()-date2.getFullYear()))===0){if((compare=sgn(date1.getMonth()-date2.getMonth()))===0){compare=sgn(date1.getDate()-date2.getDate())}}return compare}function getFirstWeekStartDate(janFourth){switch(janFourth.getDay()){case 0:return new Date(janFourth.getFullYear()-1,11,29);case 1:return janFourth;case 2:return new Date(janFourth.getFullYear(),0,3);case 3:return new Date(janFourth.getFullYear(),0,2);case 4:return new Date(janFourth.getFullYear(),0,1);case 5:return new Date(janFourth.getFullYear()-1,11,31);case 6:return new Date(janFourth.getFullYear()-1,11,30)}}function getWeekBasedYear(date){var thisDate=__addDays(new Date(date.tm_year+1900,0,1),date.tm_yday);var janFourthThisYear=new Date(thisDate.getFullYear(),0,4);var janFourthNextYear=new Date(thisDate.getFullYear()+1,0,4);var firstWeekStartThisYear=getFirstWeekStartDate(janFourthThisYear);var firstWeekStartNextYear=getFirstWeekStartDate(janFourthNextYear);if(compareByDay(firstWeekStartThisYear,thisDate)<=0){if(compareByDay(firstWeekStartNextYear,thisDate)<=0){return thisDate.getFullYear()+1}return thisDate.getFullYear()}return thisDate.getFullYear()-1}var EXPANSION_RULES_2={"%a":function(date){return WEEKDAYS[date.tm_wday].substring(0,3)},"%A":function(date){return WEEKDAYS[date.tm_wday]},"%b":function(date){return MONTHS[date.tm_mon].substring(0,3)},"%B":function(date){return MONTHS[date.tm_mon]},"%C":function(date){var year=date.tm_year+1900;return leadingNulls(year/100|0,2)},"%d":function(date){return leadingNulls(date.tm_mday,2)},"%e":function(date){return leadingSomething(date.tm_mday,2," ")},"%g":function(date){return getWeekBasedYear(date).toString().substring(2)},"%G":function(date){return getWeekBasedYear(date)},"%H":function(date){return leadingNulls(date.tm_hour,2)},"%I":function(date){var twelveHour=date.tm_hour;if(twelveHour==0)twelveHour=12;else if(twelveHour>12)twelveHour-=12;return leadingNulls(twelveHour,2)},"%j":function(date){return leadingNulls(date.tm_mday+__arraySum(__isLeapYear(date.tm_year+1900)?__MONTH_DAYS_LEAP:__MONTH_DAYS_REGULAR,date.tm_mon-1),3)},"%m":function(date){return leadingNulls(date.tm_mon+1,2)},"%M":function(date){return leadingNulls(date.tm_min,2)},"%n":function(){return"\n"},"%p":function(date){if(date.tm_hour>=0&&date.tm_hour<12){return"AM"}return"PM"},"%S":function(date){return leadingNulls(date.tm_sec,2)},"%t":function(){return"\t"},"%u":function(date){return date.tm_wday||7},"%U":function(date){var days=date.tm_yday+7-date.tm_wday;return leadingNulls(Math.floor(days/7),2)},"%V":function(date){var val=Math.floor((date.tm_yday+7-(date.tm_wday+6)%7)/7);if((date.tm_wday+371-date.tm_yday-2)%7<=2){val++}if(!val){val=52;var dec31=(date.tm_wday+7-date.tm_yday-1)%7;if(dec31==4||dec31==5&&__isLeapYear(date.tm_year%400-1)){val++}}else if(val==53){var jan1=(date.tm_wday+371-date.tm_yday)%7;if(jan1!=4&&(jan1!=3||!__isLeapYear(date.tm_year)))val=1}return leadingNulls(val,2)},"%w":function(date){return date.tm_wday},"%W":function(date){var days=date.tm_yday+7-(date.tm_wday+6)%7;return leadingNulls(Math.floor(days/7),2)},"%y":function(date){return(date.tm_year+1900).toString().substring(2)},"%Y":function(date){return date.tm_year+1900},"%z":function(date){var off=date.tm_gmtoff;var ahead=off>=0;off=Math.abs(off)/60;off=off/60*100+off%60;return(ahead?"+":"-")+String("0000"+off).slice(-4)},"%Z":function(date){return date.tm_zone},"%%":function(){return"%"}};pattern=pattern.replace(/%%/g,"\0\0");for(var rule in EXPANSION_RULES_2){if(pattern.includes(rule)){pattern=pattern.replace(new RegExp(rule,"g"),EXPANSION_RULES_2[rule](date))}}pattern=pattern.replace(/\0\0/g,"%");var bytes=intArrayFromString(pattern,false);if(bytes.length>maxsize){return 0}writeArrayToMemory(bytes,s);return bytes.length-1}function _strftime_l(s,maxsize,format,tm,loc){return _strftime(s,maxsize,format,tm)}function _proc_exit(code){EXITSTATUS=code;if(!keepRuntimeAlive()){if(Module["onExit"])Module["onExit"](code);ABORT=true}quit_(code,new ExitStatus(code))}function exitJS(status,implicit){EXITSTATUS=status;_proc_exit(status)}function handleException(e){if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)}function allocateUTF8OnStack(str){var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8Array(str,HEAP8,ret,size);return ret}var FSNode=function(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev};var readMode=292|73;var writeMode=146;Object.defineProperties(FSNode.prototype,{read:{get:function(){return(this.mode&readMode)===readMode},set:function(val){val?this.mode|=readMode:this.mode&=~readMode}},write:{get:function(){return(this.mode&writeMode)===writeMode},set:function(val){val?this.mode|=writeMode:this.mode&=~writeMode}},isFolder:{get:function(){return FS.isDir(this.mode)}},isDevice:{get:function(){return FS.isChrdev(this.mode)}}});FS.FSNode=FSNode;FS.staticInit();Module["FS_createPath"]=FS.createPath;Module["FS_createDataFile"]=FS.createDataFile;Module["FS_createPreloadedFile"]=FS.createPreloadedFile;Module["FS_unlink"]=FS.unlink;Module["FS_createLazyFile"]=FS.createLazyFile;Module["FS_createDevice"]=FS.createDevice;embind_init_charCodes();BindingError=Module["BindingError"]=extendError(Error,"BindingError");InternalError=Module["InternalError"]=extendError(Error,"InternalError");init_emval();UnboundTypeError=Module["UnboundTypeError"]=extendError(Error,"UnboundTypeError");var decodeBase64=typeof atob=="function"?atob:function(input){var keyStr="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";var output="";var chr1,chr2,chr3;var enc1,enc2,enc3,enc4;var i=0;input=input.replace(/[^A-Za-z0-9\+\/\=]/g,"");do{enc1=keyStr.indexOf(input.charAt(i++));enc2=keyStr.indexOf(input.charAt(i++));enc3=keyStr.indexOf(input.charAt(i++));enc4=keyStr.indexOf(input.charAt(i++));chr1=enc1<<2|enc2>>4;chr2=(enc2&15)<<4|enc3>>2;chr3=(enc3&3)<<6|enc4;output=output+String.fromCharCode(chr1);if(enc3!==64){output=output+String.fromCharCode(chr2)}if(enc4!==64){output=output+String.fromCharCode(chr3)}}while(i>2;args.forEach(arg=>{HEAP32[argv_ptr++]=allocateUTF8OnStack(arg)});HEAP32[argv_ptr]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(args=arguments_){if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve(Module);if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();if(shouldRunNow)callMain(args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(function(){setTimeout(function(){Module["setStatus"]("")},1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}var shouldRunNow=true;if(Module["noInitialRun"])shouldRunNow=false;run(); + + + return fasttextmodule.ready +} +); +})(); +if (typeof exports === 'object' && typeof module === 'object') + module.exports = fasttextmodule; +else if (typeof define === 'function' && define['amd']) + define([], function() { return fasttextmodule; }); +else if (typeof exports === 'object') + exports["fasttextmodule"] = fasttextmodule; diff --git a/src/lib/fasttextweb/fasttext-wasm.wasm b/src/lib/fasttextweb/fasttext-wasm.wasm new file mode 100644 index 000000000..5590b1243 Binary files /dev/null and b/src/lib/fasttextweb/fasttext-wasm.wasm differ diff --git a/src/lib/fasttextweb/fasttext.worker.ts b/src/lib/fasttextweb/fasttext.worker.ts new file mode 100644 index 000000000..e19ec5153 --- /dev/null +++ b/src/lib/fasttextweb/fasttext.worker.ts @@ -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; +}; + +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; diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index df1b3b3eb..7d63f6667 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1262,6 +1262,7 @@ messages.getMessageReactionsList#461b3f48 flags:# peer:InputPeer id:int reaction messages.setChatAvailableReactions#feb16771 peer:InputPeer available_reactions:ChatReactions = Updates; messages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions; messages.setDefaultReaction#4f47a016 reaction:Reaction = Bool; +messages.translateText#63183030 flags:# peer:flags.0?InputPeer id:flags.0?Vector text:flags.1?Vector 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.readReactions#54aa7f8e flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; messages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 3de677585..edc3fce5a 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -243,6 +243,7 @@ "messages.setChatAvailableReactions", "messages.getAvailableReactions", "messages.setDefaultReaction", + "messages.translateText", "help.getAppConfig", "stats.getBroadcastStats", "stats.getMegagroupStats", diff --git a/src/lib/rlottie/RLottie.ts b/src/lib/rlottie/RLottie.ts index 8559603ce..c9d4360a6 100644 --- a/src/lib/rlottie/RLottie.ts +++ b/src/lib/rlottie/RLottie.ts @@ -1,7 +1,9 @@ +import type { RLottieApi } from './rlottie.worker'; + import { DPR, IS_SAFARI, IS_ANDROID, IS_IOS, } from '../../util/environment'; -import WorkerConnector from '../../util/WorkerConnector'; +import { createConnector } from '../../util/PostMessageConnector'; import { animate } from '../../util/animation'; import cycleRestrict from '../../util/cycleRestrict'; import { fastRaf } from '../../util/schedulers'; @@ -30,7 +32,7 @@ const LOW_PRIORITY_CACHE_MODULO = 0; const instancesById = new Map(); const workers = new Array(MAX_WORKERS).fill(undefined).map( - () => new WorkerConnector(new Worker(new URL('./rlottie.worker.ts', import.meta.url))), + () => createConnector(new Worker(new URL('./rlottie.worker.ts', import.meta.url))), ); let lastWorkerIndex = -1; @@ -108,7 +110,7 @@ class RLottie { onLoad: NoneToVoidFunction | undefined, private id: string, private tgsUrl: string, - private params: Params = {}, + private params: Params = { }, private customColor?: [number, number, number], private onEnded?: (isDestroyed?: boolean) => void, private onLoop?: () => void, @@ -360,7 +362,7 @@ class RLottie { this.id, this.tgsUrl, this.imgSize, - this.params.isLowPriority, + this.params.isLowPriority || false, this.customColor, this.onRendererInit.bind(this), ], @@ -395,7 +397,7 @@ class RLottie { args: [ this.id, this.tgsUrl, - this.params.isLowPriority, + this.params.isLowPriority || false, this.onChangeData.bind(this), ], }); diff --git a/src/lib/rlottie/rlottie.worker.ts b/src/lib/rlottie/rlottie.worker.ts index cf36e8dbb..25f0e12e6 100644 --- a/src/lib/rlottie/rlottie.worker.ts +++ b/src/lib/rlottie/rlottie.worker.ts @@ -1,6 +1,6 @@ import { inflate } from 'pako/dist/pako_inflate'; -import createWorkerInterface from '../../util/createWorkerInterface'; -import type { CancellableCallback } from '../../util/WorkerConnector'; +import { createWorkerInterface } from '../../util/createPostMessageInterface'; +import type { CancellableCallback } from '../../util/PostMessageConnector'; import 'script-loader!./rlottie-wasm'; @@ -164,9 +164,13 @@ function destroy(key: string, isRepeated = false) { } } -createWorkerInterface({ +const api = { init, changeData, renderFrames, destroy, -}); +}; + +createWorkerInterface(api); + +export type RLottieApi = typeof api; diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts index dabafc826..136ae3d1a 100644 --- a/src/serviceWorker.ts +++ b/src/serviceWorker.ts @@ -13,7 +13,7 @@ import { pause } from './util/schedulers'; 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 ACTIVATE_TIMEOUT = 3000; @@ -64,7 +64,7 @@ self.addEventListener('fetch', (e: FetchEvent) => { } 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)); return true; } diff --git a/src/styles/index.scss b/src/styles/index.scss index 9d05d5024..ad8456625 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -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. https://snook.ca/archives/html_and_css/hiding-content-for-accessibility diff --git a/src/types/index.ts b/src/types/index.ts index 94a745556..728ed8f7e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -89,6 +89,9 @@ export interface ISettings extends NotifySettings, Record { wasTimeFormatSetManually: boolean; isConnectionStatusMinimized: boolean; shouldArchiveAndMuteNewNonContact?: boolean; + canTranslate: boolean; + canTranslateChats: boolean; + doNotTranslate: string[]; } export interface ApiPrivacySettings { @@ -216,6 +219,7 @@ export enum SettingsScreens { Stickers, QuickReaction, CustomEmoji, + DoNotTranslate, } export type StickerSetOrRecent = Pick; + +type Values = T[keyof T]; +export type RequestTypes = Values<{ + [Name in keyof (T)]: { + name: Name & string; + args: Parameters; + } +}>; + +class ConnectorClass { + private requestStates = new Map(); + + private requestStatesByCallback = new Map(); + + 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) { + 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 = 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( + worker: Worker, + onUpdate?: (update: ApiUpdate) => void, + channel?: string, +) { + const connector = new ConnectorClass(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 = ReturnType>; diff --git a/src/util/WorkerConnector.ts b/src/util/WorkerConnector.ts deleted file mode 100644 index 2268a65d3..000000000 --- a/src/util/WorkerConnector.ts +++ /dev/null @@ -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(); - - private requestStatesByCallback = new Map(); - - 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 = 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); - } - }); - } -} diff --git a/src/util/createPostMessageInterface.ts b/src/util/createPostMessageInterface.ts new file mode 100644 index 000000000..5cd8f4110 --- /dev/null +++ b/src/util/createPostMessageInterface.ts @@ -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(); + +type ApiConfig = + ((name: string, ...args: any[]) => any | [any, ArrayBuffer[]]) + | Record; +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' } }); + }); +} diff --git a/src/util/createWorkerInterface.ts b/src/util/createWorkerInterface.ts deleted file mode 100644 index 486872301..000000000 --- a/src/util/createWorkerInterface.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type { CancellableCallback, OriginMessageEvent, WorkerMessageData } from './WorkerConnector'; -import { DEBUG } from '../config'; - -declare const self: WorkerGlobalScope; - -handleErrors(); - -const callbackState = new Map(); - -export default function createInterface(api: Record) { - 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); - } -} diff --git a/src/util/environment.ts b/src/util/environment.ts index 5ac54d81a..61f96fcfc 100644 --- a/src/util/environment.ts +++ b/src/util/environment.ts @@ -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_INSTALL_PROMPT_SUPPORTED = 'onbeforeinstallprompt' 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` export const MESSAGE_LIST_SENSITIVE_AREA = IS_SCROLL_PATCH_NEEDED ? 300 : 750; diff --git a/src/util/iteratees.ts b/src/util/iteratees.ts index ce0196837..3f9842663 100644 --- a/src/util/iteratees.ts +++ b/src/util/iteratees.ts @@ -117,6 +117,17 @@ export function split(array: T[], chunkSize: number) { return result; } +export function partition( + 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(value: T): T { if (!isObject(value)) { return value; diff --git a/src/util/languageDetection.ts b/src/util/languageDetection.ts new file mode 100644 index 000000000..af68b4f3c --- /dev/null +++ b/src/util/languageDetection.ts @@ -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 | undefined; +const initializationDeferred = new Deferred(); + +setTimeout(initWorker, WORKER_INIT_DELAY); + +function initWorker() { + if (!worker) { + worker = createConnector( + 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; +}