input.ts 141 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237
  1. /*
  2. * https://github.com/morethanwords/tweb
  3. * Copyright (C) 2019-2021 Eduard Kuzmenko
  4. * https://github.com/morethanwords/tweb/blob/master/LICENSE
  5. */
  6. import type {MyDocument} from '../../lib/appManagers/appDocsManager';
  7. import type {MyDraftMessage} from '../../lib/appManagers/appDraftsManager';
  8. import type {AppMessagesManager, MessageSendingParams} from '../../lib/appManagers/appMessagesManager';
  9. import type Chat from './chat';
  10. import {AppImManager, APP_TABS} from '../../lib/appManagers/appImManager';
  11. import '../../../public/recorder.min';
  12. import IS_TOUCH_SUPPORTED from '../../environment/touchSupport';
  13. import opusDecodeController from '../../lib/opusDecodeController';
  14. import {ButtonMenuItemOptions, ButtonMenuItemOptionsVerifiable, ButtonMenuSync} from '../buttonMenu';
  15. import emoticonsDropdown, {EmoticonsDropdown} from '../emoticonsDropdown';
  16. import PopupCreatePoll from '../popups/createPoll';
  17. import PopupForward from '../popups/forward';
  18. import PopupNewMedia, {getCurrentNewMediaPopup} from '../popups/newMedia';
  19. import {toast, toastNew} from '../toast';
  20. import {MessageEntity, DraftMessage, WebPage, Message, UserFull, AttachMenuPeerType, BotMenuButton, MessageMedia, InputReplyTo, Chat as MTChat, User, ChatFull} from '../../layer';
  21. import StickersHelper from './stickersHelper';
  22. import ButtonIcon from '../buttonIcon';
  23. import ButtonMenuToggle from '../buttonMenuToggle';
  24. import ListenerSetter, {Listener} from '../../helpers/listenerSetter';
  25. import Button, {replaceButtonIcon} from '../button';
  26. import PopupSchedule from '../popups/schedule';
  27. import SendMenu from './sendContextMenu';
  28. import rootScope from '../../lib/rootScope';
  29. import PopupPinMessage from '../popups/unpinMessage';
  30. import tsNow from '../../helpers/tsNow';
  31. import appNavigationController, {NavigationItem} from '../appNavigationController';
  32. import {IS_MOBILE, IS_MOBILE_SAFARI} from '../../environment/userAgent';
  33. import I18n, {FormatterArguments, i18n, join, LangPackKey} from '../../lib/langPack';
  34. import {chatHistoryService} from '../../lib/api/chatHistoryService';
  35. import {chatRecordsService} from '../../lib/api/chatRecordsService';
  36. import {messagesService} from '../../lib/api/messagesService';
  37. import {generateTail} from './bubbles';
  38. import findUpClassName from '../../helpers/dom/findUpClassName';
  39. import ButtonCorner from '../buttonCorner';
  40. import blurActiveElement from '../../helpers/dom/blurActiveElement';
  41. import cancelEvent from '../../helpers/dom/cancelEvent';
  42. import cancelSelection from '../../helpers/dom/cancelSelection';
  43. import {attachClickEvent, simulateClickEvent} from '../../helpers/dom/clickEvent';
  44. import isInputEmpty from '../../helpers/dom/isInputEmpty';
  45. import isSendShortcutPressed from '../../helpers/dom/isSendShortcutPressed';
  46. import placeCaretAtEnd from '../../helpers/dom/placeCaretAtEnd';
  47. import getRichValueWithCaret from '../../helpers/dom/getRichValueWithCaret';
  48. import EmojiHelper from './emojiHelper';
  49. import CommandsHelper from './commandsHelper';
  50. import AutocompleteHelperController from './autocompleteHelperController';
  51. import AutocompleteHelper from './autocompleteHelper';
  52. import MentionsHelper from './mentionsHelper';
  53. import fixSafariStickyInput from '../../helpers/dom/fixSafariStickyInput';
  54. import ReplyKeyboard from './replyKeyboard';
  55. import InlineHelper from './inlineHelper';
  56. import debounce from '../../helpers/schedulers/debounce';
  57. import {putPreloader} from '../putPreloader';
  58. import SetTransition from '../singleTransition';
  59. import PeerTitle from '../peerTitle';
  60. import {fastRaf} from '../../helpers/schedulers';
  61. import PopupDeleteMessages from '../popups/deleteMessages';
  62. import fixSafariStickyInputFocusing, {IS_STICKY_INPUT_BUGGED} from '../../helpers/dom/fixSafariStickyInputFocusing';
  63. import PopupPeer from '../popups/peer';
  64. import appMediaPlaybackController from '../appMediaPlaybackController';
  65. import {BOT_START_PARAM, GENERAL_TOPIC_ID, NULL_PEER_ID, SEND_PAID_WITH_STARS_DELAY, SEND_WHEN_ONLINE_TIMESTAMP} from '../../lib/mtproto/mtproto_config';
  66. import setCaretAt from '../../helpers/dom/setCaretAt';
  67. import DropdownHover from '../../helpers/dropdownHover';
  68. import findUpTag from '../../helpers/dom/findUpTag';
  69. import toggleDisability from '../../helpers/dom/toggleDisability';
  70. import callbackify from '../../helpers/callbackify';
  71. import ChatBotCommands from './botCommands';
  72. import copy from '../../helpers/object/copy';
  73. import toHHMMSS from '../../helpers/string/toHHMMSS';
  74. import documentFragmentToHTML from '../../helpers/dom/documentFragmentToHTML';
  75. import PopupElement from '../popups';
  76. import getEmojiEntityFromEmoji from '../../lib/richTextProcessor/getEmojiEntityFromEmoji';
  77. import mergeEntities from '../../lib/richTextProcessor/mergeEntities';
  78. import parseEntities from '../../lib/richTextProcessor/parseEntities';
  79. import parseMarkdown from '../../lib/richTextProcessor/parseMarkdown';
  80. import wrapDraftText from '../../lib/richTextProcessor/wrapDraftText';
  81. import wrapDraft from '../wrappers/draft';
  82. import wrapMessageForReply from '../wrappers/messageForReply';
  83. import getServerMessageId from '../../lib/appManagers/utils/messageId/getServerMessageId';
  84. import {AppManagers} from '../../lib/appManagers/managers';
  85. import contextMenuController from '../../helpers/contextMenuController';
  86. import {emojiFromCodePoints} from '../../vendor/emoji';
  87. import {modifyAckedPromise} from '../../helpers/modifyAckedResult';
  88. import ChatSendAs from './sendAs';
  89. import filterAsync from '../../helpers/array/filterAsync';
  90. import InputFieldAnimated from '../inputFieldAnimated';
  91. import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb';
  92. import PopupStickers from '../popups/stickers';
  93. import wrapPeerTitle from '../wrappers/peerTitle';
  94. import wrapReply from '../wrappers/reply';
  95. import {getEmojiFromElement} from '../emoticonsDropdown/tabs/emoji';
  96. import RichInputHandler from '../../helpers/dom/richInputHandler';
  97. import {insertRichTextAsHTML} from '../inputField';
  98. import draftsAreEqual from '../../lib/appManagers/utils/drafts/draftsAreEqual';
  99. import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText';
  100. import getAttachMenuBotIcon from '../../lib/appManagers/utils/attachMenuBots/getAttachMenuBotIcon';
  101. import forEachReverse from '../../helpers/array/forEachReverse';
  102. import {MARKDOWN_ENTITIES} from '../../lib/richTextProcessor';
  103. import IMAGE_MIME_TYPES_SUPPORTED from '../../environment/imageMimeTypesSupport';
  104. import VIDEO_MIME_TYPES_SUPPORTED from '../../environment/videoMimeTypesSupport';
  105. import {ChatRights} from '../../lib/appManagers/appChatsManager';
  106. import getPeerActiveUsernames from '../../lib/appManagers/utils/peers/getPeerActiveUsernames';
  107. import replaceContent from '../../helpers/dom/replaceContent';
  108. import getTextWidth from '../../helpers/canvas/getTextWidth';
  109. import {FontFull} from '../../config/font';
  110. import {ChatType} from './chat';
  111. import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise';
  112. import idleController from '../../helpers/idleController';
  113. import Icon from '../icon';
  114. import setBadgeContent from '../../helpers/setBadgeContent';
  115. import createBadge from '../../helpers/createBadge';
  116. import deepEqual from '../../helpers/object/deepEqual';
  117. import {clearMarkdownExecutions, createMarkdownCache, handleMarkdownShortcut, maybeClearUndoHistory, processCurrentFormatting} from '../../helpers/dom/markdown';
  118. import MarkupTooltip from './markupTooltip';
  119. import PopupPremium from '../popups/premium';
  120. import PopupPickUser from '../popups/pickUser';
  121. import getPeerId from '../../lib/appManagers/utils/peers/getPeerId';
  122. import {isSavedDialog} from '../../lib/appManagers/utils/dialogs/isDialog';
  123. import getFwdFromName from '../../lib/appManagers/utils/messages/getFwdFromName';
  124. import apiManagerProxy from '../../lib/mtproto/mtprotoworker';
  125. import eachSecond from '../../helpers/eachSecond';
  126. import {wrapSlowModeLeftDuration} from '../wrappers/wrapDuration';
  127. import showTooltip from '../tooltip';
  128. import createContextMenu from '../../helpers/dom/createContextMenu';
  129. import {Accessor, createEffect, createMemo, createRoot, createSignal, Setter} from 'solid-js';
  130. import {createStore} from 'solid-js/store';
  131. import SelectedEffect from './selectedEffect';
  132. import windowSize from '../../helpers/windowSize';
  133. import {numberThousandSplitterForStars} from '../../helpers/number/numberThousandSplitter';
  134. import accumulate from '../../helpers/array/accumulate';
  135. import splitStringByLength from '../../helpers/string/splitStringByLength';
  136. import PaidMessagesInterceptor, {PAYMENT_REJECTED} from './paidMessagesInterceptor';
  137. import asyncThrottle from '../../helpers/schedulers/asyncThrottle';
  138. import focusInput from '../../helpers/dom/focusInput';
  139. import {PopupChecklist} from '../popups/checklist';
  140. // console.log('Recorder', Recorder);
  141. const RECORD_MIN_TIME = 500;
  142. const REPLY_IN_TOPIC = false;
  143. export const POSTING_NOT_ALLOWED_MAP: {[action in ChatRights]?: LangPackKey} = {
  144. send_voices: 'GlobalAttachVoiceRestricted',
  145. send_stickers: 'GlobalAttachStickersRestricted',
  146. send_gifs: 'GlobalAttachGifRestricted',
  147. send_media: 'GlobalAttachMediaRestricted',
  148. send_plain: 'GlobalSendMessageRestricted',
  149. send_polls: 'ErrorSendRestrictedPollsAll',
  150. send_inline: 'GlobalAttachInlineRestricted'
  151. };
  152. type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply';
  153. type ChatSendBtnIcon = 'send' | 'record' | 'edit' | 'schedule' | 'forward';
  154. export type ChatInputReplyTo = Pick<MessageSendingParams, 'replyToMsgId' | 'replyToQuote' | 'replyToStoryId' | 'replyToPeerId'>;
  155. const CLASS_NAME = 'chat-input';
  156. const PEER_EXCEPTIONS = new Set<ChatType>([ChatType.Scheduled, ChatType.Stories, ChatType.Saved]);
  157. export default class ChatInput {
  158. // private static AUTO_COMPLETE_REG_EXP = /(\s|^)((?::|.)(?!.*[:@]).*|(?:[@\/]\S*))$/;
  159. private static AUTO_COMPLETE_REG_EXP = /(\s|^)((?:(?:@|^\/)\S*)|(?::|^[^:@\/])(?!.*[:@\/]).*)$/;
  160. public messageInput: HTMLElement;
  161. public messageInputField: InputFieldAnimated;
  162. private fileInput: HTMLInputElement;
  163. private inputMessageContainer: HTMLDivElement;
  164. private btnSend: HTMLButtonElement;
  165. public btnCancelRecord: HTMLButtonElement;
  166. public btnReaction: HTMLButtonElement;
  167. public lastUrl = '';
  168. private lastTimeType = 0;
  169. public noRipple: boolean;
  170. public chatInput: HTMLElement;
  171. public inputContainer: HTMLElement;
  172. public rowsWrapper: HTMLDivElement;
  173. private newMessageWrapper: HTMLDivElement;
  174. private btnToggleEmoticons: HTMLButtonElement;
  175. private btnToggleReplyMarkup: HTMLButtonElement;
  176. public btnSendContainer: HTMLDivElement;
  177. private replyKeyboard: ReplyKeyboard;
  178. public attachMenu: HTMLElement;
  179. private attachMenuButtons: ButtonMenuItemOptionsVerifiable[];
  180. private sendMenu: SendMenu;
  181. private replyElements: {
  182. container: HTMLElement,
  183. cancelBtn: HTMLButtonElement,
  184. iconBtn: HTMLButtonElement,
  185. menuContainer: HTMLElement,
  186. replyInAnother: ButtonMenuItemOptions,
  187. doNotReply: ButtonMenuItemOptions,
  188. doNotQuote: ButtonMenuItemOptions
  189. } = {} as any;
  190. private forwardElements: {
  191. changePeer: ButtonMenuItemOptions,
  192. showSender: ButtonMenuItemOptions,
  193. hideSender: ButtonMenuItemOptions,
  194. showCaption: ButtonMenuItemOptions,
  195. hideCaption: ButtonMenuItemOptions,
  196. container: HTMLElement,
  197. modifyArgs?: ButtonMenuItemOptions[]
  198. };
  199. private webPageElements: {
  200. above: ButtonMenuItemOptions,
  201. below: ButtonMenuItemOptions,
  202. larger: ButtonMenuItemOptions,
  203. smaller: ButtonMenuItemOptions,
  204. container: HTMLElement
  205. };
  206. private forwardHover: DropdownHover;
  207. private webPageHover: DropdownHover;
  208. private replyHover: DropdownHover;
  209. private currentHover: DropdownHover;
  210. private forwardWasDroppingAuthor: boolean;
  211. private getWebPagePromise: Promise<void>;
  212. public willSendWebPage: WebPage = null;
  213. public webPageOptions: Parameters<AppMessagesManager['sendText']>[0]['webPageOptions'] = {};
  214. private forwarding: {[fromPeerId: PeerId]: number[]};
  215. public replyToMsgId: MessageSendingParams['replyToMsgId'];
  216. public replyToStoryId: MessageSendingParams['replyToStoryId'];
  217. public replyToQuote: MessageSendingParams['replyToQuote'];
  218. public replyToPeerId: MessageSendingParams['replyToPeerId'];
  219. public editMsgId: number;
  220. public editMessage: Message.message;
  221. private noWebPage: true;
  222. public scheduleDate: number;
  223. public sendSilent: true;
  224. public startParam: string;
  225. public invertMedia: boolean;
  226. public effect: Accessor<DocId>;
  227. public setEffect: Setter<DocId>;
  228. private recorder: any;
  229. public recording = false;
  230. private recordCanceled = false;
  231. private recordTimeEl: HTMLElement;
  232. private recordRippleEl: HTMLElement;
  233. private recordStartTime = 0;
  234. private recordingOverlayListener: Listener;
  235. private recordingNavigationItem: NavigationItem;
  236. // private scrollTop = 0;
  237. // private scrollOffsetTop = 0;
  238. // private scrollDiff = 0;
  239. public helperType: Exclude<ChatInputHelperType, 'webpage'>;
  240. private helperFunc: () => void | Promise<void>;
  241. private helperWaitingForward: boolean;
  242. private helperWaitingReply: boolean;
  243. public willAttachType: 'document' | 'media';
  244. private autocompleteHelperController: AutocompleteHelperController;
  245. private stickersHelper: StickersHelper;
  246. private emojiHelper: EmojiHelper;
  247. private commandsHelper: CommandsHelper;
  248. private mentionsHelper: MentionsHelper;
  249. private inlineHelper: InlineHelper;
  250. private listenerSetter: ListenerSetter;
  251. private hoverListenerSetter: ListenerSetter;
  252. private pinnedControlBtn: HTMLButtonElement;
  253. private openChatBtn: HTMLButtonElement;
  254. private goDownBtn: HTMLButtonElement;
  255. private goDownUnreadBadge: HTMLElement;
  256. private goMentionBtn: HTMLButtonElement;
  257. private goMentionUnreadBadge: HTMLSpanElement;
  258. private goReactionBtn: HTMLButtonElement;
  259. private goReactionUnreadBadge: HTMLElement;
  260. private btnScheduled: HTMLButtonElement;
  261. private btnPreloader: HTMLButtonElement;
  262. private saveDraftDebounced: () => void;
  263. private fakeRowsWrapper: HTMLDivElement;
  264. private previousQuery: string;
  265. private releaseMediaPlayback: () => void;
  266. private botStartBtn: HTMLButtonElement;
  267. private unblockBtn: HTMLButtonElement;
  268. private onlyPremiumBtn: HTMLButtonElement;
  269. private onlyPremiumBtnText: I18n.IntlElement;
  270. private joinBtn: HTMLButtonElement;
  271. private rowsWrapperWrapper: HTMLDivElement;
  272. private controlContainer: HTMLElement;
  273. private fakeSelectionWrapper: HTMLDivElement;
  274. private starsBadge: HTMLElement;
  275. private starsBadgeStars: HTMLElement;
  276. private fakeWrapperTo: HTMLElement;
  277. private toggleControlButtonDisability: () => void;
  278. private botCommandsToggle: HTMLElement;
  279. private botCommands: ChatBotCommands;
  280. private botCommandsIcon: HTMLElement;
  281. private botCommandsView: HTMLElement;
  282. private botMenuButton: BotMenuButton.botMenuButton;
  283. private hasBotCommands: boolean;
  284. // private activeContainer: HTMLElement;
  285. private sendAs: ChatSendAs;
  286. public sendAsPeerId: PeerId;
  287. private replyInTopicOverlay: HTMLDivElement;
  288. private restoreInputLock: () => void;
  289. private isFocused: boolean;
  290. private freezedFocused: boolean;
  291. public onFocusChange: (isFocused: boolean) => void;
  292. public onMenuToggle: (isOpen: boolean) => void;
  293. public onRecording: (isRecording: boolean) => void;
  294. public onUpdateSendBtn: (icon: ChatSendBtnIcon) => void;
  295. public onMessageSent2: () => void;
  296. public forwardStoryCallback: (e: MouseEvent) => void;
  297. public emoticonsDropdown: EmoticonsDropdown;
  298. public excludeParts: Partial<{
  299. scheduled: boolean,
  300. replyMarkup: boolean,
  301. downButton: boolean,
  302. reply: boolean,
  303. forwardOptions: boolean,
  304. mentionButton: boolean,
  305. botCommands: boolean,
  306. attachMenu: boolean,
  307. commandsHelper: boolean,
  308. emoticons: boolean
  309. }>;
  310. public globalMentions: boolean;
  311. public onFileSelection: (promise: Promise<File[]>) => void;
  312. private hasOffset: {type: 'commands' | 'as', forwards: boolean};
  313. private canForwardStory: boolean;
  314. private processingDraftMessage: DraftMessage.draftMessage;
  315. private fileSelectionPromise: CancellablePromise<File[]>;
  316. public paidMessageInterceptor: PaidMessagesInterceptor;
  317. private starsState: ReturnType<ChatInput['constructStarsState']>;
  318. constructor(
  319. public chat: Chat,
  320. private appImManager: AppImManager,
  321. private managers: AppManagers,
  322. private className: string
  323. ) {
  324. this.listenerSetter = new ListenerSetter();
  325. this.hoverListenerSetter = new ListenerSetter();
  326. this.excludeParts = {};
  327. this.isFocused = false;
  328. this.emoticonsDropdown = emoticonsDropdown;
  329. }
  330. public construct() {
  331. const className2 = this.className;
  332. this.chatInput = document.createElement('div');
  333. this.chatInput.classList.add(CLASS_NAME, className2, 'hide');
  334. this.inputContainer = document.createElement('div');
  335. this.inputContainer.classList.add(`${CLASS_NAME}-container`, `${className2}-container`);
  336. this.rowsWrapperWrapper = document.createElement('div');
  337. this.rowsWrapperWrapper.classList.add('rows-wrapper-wrapper');
  338. this.rowsWrapper = document.createElement('div');
  339. this.rowsWrapper.classList.add(...[
  340. 'rows-wrapper',
  341. `${CLASS_NAME}-wrapper`,
  342. `${className2}-wrapper`,
  343. this.chat.type !== ChatType.Stories && 'chat-rows-wrapper'
  344. ].filter(Boolean));
  345. this.rowsWrapperWrapper.append(this.rowsWrapper);
  346. const tail = generateTail(!this.chat.isMainChat);
  347. this.rowsWrapper.append(tail);
  348. const fakeRowsWrapper = this.fakeRowsWrapper = document.createElement('div');
  349. fakeRowsWrapper.classList.add('fake-wrapper', 'fake-rows-wrapper');
  350. const fakeSelectionWrapper = this.fakeSelectionWrapper = document.createElement('div');
  351. fakeSelectionWrapper.classList.add('fake-wrapper', 'fake-selection-wrapper');
  352. this.inputContainer.append(this.rowsWrapperWrapper, fakeRowsWrapper, fakeSelectionWrapper);
  353. this.chatInput.append(this.inputContainer);
  354. if(!this.excludeParts.downButton) {
  355. this.constructGoDownButton();
  356. }
  357. // * constructor end
  358. /* let setScrollTopTimeout: number;
  359. // @ts-ignore
  360. let height = window.visualViewport.height; */
  361. // @ts-ignore
  362. // this.listenerSetter.add(window.visualViewport)('resize', () => {
  363. // const scrollable = this.chat.bubbles.scrollable;
  364. // const wasScrolledDown = scrollable.isScrolledDown;
  365. // /* if(wasScrolledDown) {
  366. // this.saveScroll();
  367. // } */
  368. // // @ts-ignore
  369. // let newHeight = window.visualViewport.height;
  370. // const diff = height - newHeight;
  371. // const scrollTop = scrollable.scrollTop;
  372. // const needScrollTop = wasScrolledDown ? scrollable.scrollHeight : scrollTop + diff; // * wasScrolledDown это проверка для десктоп хрома, когда пропадает панель загрузок снизу
  373. // console.log('resize before', scrollable.scrollTop, scrollable.container.clientHeight, scrollable.scrollHeight, wasScrolledDown, scrollable.lastScrollTop, diff, needScrollTop);
  374. // scrollable.scrollTop = needScrollTop;
  375. // if(setScrollTopTimeout) clearTimeout(setScrollTopTimeout);
  376. // setScrollTopTimeout = window.setTimeout(() => {
  377. // const diff = height - newHeight;
  378. // const isScrolledDown = scrollable.scrollHeight - Math.round(scrollable.scrollTop + scrollable.container.offsetHeight + diff) <= 1;
  379. // height = newHeight;
  380. // scrollable.scrollTop = needScrollTop;
  381. // console.log('resize after', scrollable.scrollTop, scrollable.container.clientHeight, scrollable.scrollHeight, scrollable.isScrolledDown, scrollable.lastScrollTop, isScrolledDown);
  382. // /* if(isScrolledDown) {
  383. // scrollable.scrollTop = scrollable.scrollHeight;
  384. // } */
  385. // //scrollable.scrollTop += diff;
  386. // setScrollTopTimeout = 0;
  387. // }, 0);
  388. // });
  389. // ! Can't use it with resizeObserver
  390. /* this.listenerSetter.add(window.visualViewport)('resize', () => {
  391. const scrollable = this.chat.bubbles.scrollable;
  392. const wasScrolledDown = scrollable.isScrolledDown;
  393. // @ts-ignore
  394. let newHeight = window.visualViewport.height;
  395. const diff = height - newHeight;
  396. const needScrollTop = wasScrolledDown ? scrollable.scrollHeight : scrollable.scrollTop + diff; // * wasScrolledDown это проверка для десктоп хрома, когда пропадает панель загрузок снизу
  397. //console.log('resize before', scrollable.scrollTop, scrollable.container.clientHeight, scrollable.scrollHeight, wasScrolledDown, scrollable.lastScrollTop, diff, needScrollTop);
  398. scrollable.scrollTop = needScrollTop;
  399. height = newHeight;
  400. if(setScrollTopTimeout) clearTimeout(setScrollTopTimeout);
  401. setScrollTopTimeout = window.setTimeout(() => { // * try again for scrolled down Android Chrome
  402. scrollable.scrollTop = needScrollTop;
  403. //console.log('resize after', scrollable.scrollTop, scrollable.container.clientHeight, scrollable.scrollHeight, scrollable.isScrolledDown, scrollable.lastScrollTop, isScrolledDown);
  404. setScrollTopTimeout = 0;
  405. }, 0);
  406. }); */
  407. const c = this.controlContainer = document.createElement('div');
  408. c.classList.add('chat-input-control', 'chat-input-wrapper');
  409. this.inputContainer.append(c);
  410. this.paidMessageInterceptor = new PaidMessagesInterceptor(this.chat, this.managers);
  411. this.getMiddleware().onDestroy(() => {
  412. this.paidMessageInterceptor.dispose();
  413. });
  414. this.starsState = this.constructStarsState();
  415. }
  416. public freezeFocused(focused: boolean) {
  417. if(this.freezedFocused === focused) {
  418. return;
  419. }
  420. this.freezedFocused = focused;
  421. this.updateSendBtn();
  422. }
  423. public createButtonIcon(...args: Parameters<typeof ButtonIcon>) {
  424. if(this.noRipple) {
  425. args[1] ??= {};
  426. args[1].noRipple = true;
  427. }
  428. const button = ButtonIcon(...args);
  429. button.tabIndex = -1;
  430. return button;
  431. }
  432. private constructGoDownButton() {
  433. this.goDownBtn = ButtonCorner({icon: 'arrow_down', className: 'bubbles-corner-button chat-secondary-button bubbles-go-down hide'});
  434. this.inputContainer.append(this.goDownBtn);
  435. attachClickEvent(this.goDownBtn, (e) => {
  436. cancelEvent(e);
  437. this.chat.bubbles.onGoDownClick();
  438. }, {listenerSetter: this.listenerSetter});
  439. }
  440. private constructReplyElements() {
  441. this.replyElements.container = document.createElement('div');
  442. this.replyElements.container.classList.add('reply-wrapper', 'rows-wrapper-row');
  443. this.replyElements.iconBtn = this.createButtonIcon('');
  444. this.replyElements.cancelBtn = this.createButtonIcon('close reply-cancel', {noRipple: true});
  445. this.replyElements.container.append(this.replyElements.iconBtn, this.replyElements.cancelBtn);
  446. attachClickEvent(this.replyElements.cancelBtn, this.onHelperCancel, {listenerSetter: this.listenerSetter});
  447. attachClickEvent(this.replyElements.container, this.onHelperClick, {listenerSetter: this.listenerSetter});
  448. const buttons: ButtonMenuItemOptions[] = [{
  449. icon: 'message_jump',
  450. text: 'ShowMessage',
  451. onClick: () => {
  452. this.onHelperClick();
  453. this.replyHover.toggle(false);
  454. }
  455. }, this.replyElements.replyInAnother = {
  456. icon: 'replace',
  457. text: 'ReplyToAnotherChat',
  458. onClick: () => this.changeReplyRecipient()
  459. }, this.replyElements.doNotReply = {
  460. icon: 'delete',
  461. text: 'DoNotReply',
  462. onClick: this.onHelperCancel,
  463. danger: true/* ,
  464. separator: true */
  465. }, this.replyElements.doNotQuote = {
  466. icon: 'delete',
  467. text: 'DoNotQuote',
  468. onClick: this.onHelperCancel,
  469. danger: true
  470. }];
  471. const btnMenu = this.replyElements.menuContainer = ButtonMenuSync({
  472. buttons,
  473. listenerSetter: this.listenerSetter
  474. });
  475. if(!IS_TOUCH_SUPPORTED) {
  476. this.replyHover = new DropdownHover({element: btnMenu});
  477. }
  478. this.replyElements.container.append(btnMenu);
  479. }
  480. private constructForwardElements() {
  481. const onHideAuthorClick = () => {
  482. isChangingAuthor = true;
  483. };
  484. const onHideCaptionClick = () => {
  485. isChangingAuthor = false;
  486. };
  487. const forwardElements: ChatInput['forwardElements'] = this.forwardElements = {} as any;
  488. let isChangingAuthor = false;
  489. const forwardButtons: ButtonMenuItemOptions[] = [
  490. forwardElements.showSender = {
  491. text: 'Chat.Alert.Forward.Action.Show1',
  492. onClick: onHideAuthorClick,
  493. checkForClose: () => this.canToggleHideAuthor(),
  494. radioGroup: 'author'
  495. },
  496. forwardElements.hideSender = {
  497. text: 'Chat.Alert.Forward.Action.Hide1',
  498. onClick: onHideAuthorClick,
  499. checkForClose: () => this.canToggleHideAuthor(),
  500. radioGroup: 'author'
  501. },
  502. forwardElements.showCaption = {
  503. text: 'Chat.Alert.Forward.Action.ShowCaption',
  504. onClick: onHideCaptionClick,
  505. radioGroup: 'caption'
  506. },
  507. forwardElements.hideCaption = {
  508. text: 'Chat.Alert.Forward.Action.HideCaption',
  509. onClick: onHideCaptionClick,
  510. radioGroup: 'caption'
  511. },
  512. forwardElements.changePeer = {
  513. text: 'Chat.Alert.Forward.Action.Another',
  514. onClick: () => {
  515. this.changeForwardRecipient();
  516. },
  517. icon: 'replace'
  518. },
  519. {
  520. icon: 'delete',
  521. text: 'DoNotForward',
  522. onClick: this.onHelperCancel,
  523. danger: true
  524. }
  525. ];
  526. const forwardBtnMenu = forwardElements.container = ButtonMenuSync({
  527. buttons: forwardButtons,
  528. radioGroups: [{
  529. name: 'author',
  530. onChange: (value) => {
  531. const checked = !!+value;
  532. if(isChangingAuthor) {
  533. this.forwardWasDroppingAuthor = !checked;
  534. }
  535. const replyTitle = this.replyElements.container.querySelector('.reply-title');
  536. if(replyTitle) {
  537. const el = replyTitle.firstElementChild as HTMLElement;
  538. const i = I18n.weakMap.get(el) as I18n.IntlElement;
  539. const langPackKey: LangPackKey = forwardElements.showSender.checkboxField.checked ? 'Chat.Accessory.Forward' : 'Chat.Accessory.Hidden';
  540. i.key = langPackKey;
  541. i.update();
  542. }
  543. },
  544. checked: 0
  545. }, {
  546. name: 'caption',
  547. onChange: (value) => {
  548. const checked = !!+value;
  549. let b: ButtonMenuItemOptions;
  550. if(checked && this.forwardWasDroppingAuthor !== undefined) {
  551. b = this.forwardWasDroppingAuthor ? forwardElements.hideSender : forwardElements.showSender;
  552. } else {
  553. b = checked ? forwardElements.showSender : forwardElements.hideSender;
  554. }
  555. b.checkboxField.checked = true;
  556. },
  557. checked: 0
  558. }],
  559. listenerSetter: this.listenerSetter
  560. });
  561. if(!IS_TOUCH_SUPPORTED) {
  562. this.forwardHover = new DropdownHover({element: forwardBtnMenu});
  563. }
  564. forwardElements.modifyArgs = forwardButtons.slice(0, -2);
  565. this.replyElements.container.append(forwardBtnMenu);
  566. }
  567. private constructWebPageElements() {
  568. this.webPageElements = {} as any;
  569. const buttons: ButtonMenuItemOptions[] = [this.webPageElements.above = {
  570. text: 'AboveMessage',
  571. onClick: () => {},
  572. radioGroup: 'position'
  573. }, this.webPageElements.below = {
  574. text: 'BelowMessage',
  575. onClick: () => {},
  576. radioGroup: 'position'
  577. }, this.webPageElements.larger = {
  578. text: 'LargerMedia',
  579. onClick: () => {},
  580. radioGroup: 'size'
  581. }, this.webPageElements.smaller = {
  582. text: 'SmallerMedia',
  583. onClick: () => {},
  584. radioGroup: 'size'
  585. }, {
  586. text: 'WebPage.RemovePreview',
  587. onClick: () => {
  588. this.onHelperCancel();
  589. },
  590. icon: 'delete',
  591. danger: true
  592. }];
  593. const btnMenu = this.webPageElements.container = ButtonMenuSync({
  594. buttons,
  595. radioGroups: [{
  596. name: 'position',
  597. onChange: (value) => {
  598. this.invertMedia = !!+value;
  599. this.saveDraftDebounced?.();
  600. },
  601. checked: 0
  602. }, {
  603. name: 'size',
  604. onChange: (value) => {
  605. this.webPageOptions.largeMedia = !!+value;
  606. this.webPageOptions.smallMedia = !+value;
  607. this.saveDraftDebounced?.();
  608. },
  609. checked: 0
  610. }],
  611. listenerSetter: this.listenerSetter
  612. });
  613. if(!IS_TOUCH_SUPPORTED) {
  614. this.webPageHover = new DropdownHover({element: btnMenu});
  615. }
  616. this.replyElements.container.append(btnMenu);
  617. }
  618. private constructMentionButton(isReaction?: boolean) {
  619. const btn = ButtonCorner({icon: isReaction ? 'reactions' : 'mention', className: 'bubbles-corner-button chat-secondary-button bubbles-go-mention bubbles-go-reaction'});
  620. const badge = createBadge('span', 24, 'primary');
  621. btn.append(badge);
  622. this.inputContainer.append(btn);
  623. attachClickEvent(btn, (e) => {
  624. cancelEvent(e);
  625. const middleware = this.getMiddleware();
  626. this.managers.appMessagesManager.goToNextMention({peerId: this.chat.peerId, threadId: this.chat.threadId, isReaction}).then((mid) => {
  627. if(!middleware()) {
  628. return;
  629. }
  630. if(mid) {
  631. this.chat.setMessageId({lastMsgId: mid});
  632. }
  633. });
  634. }, {listenerSetter: this.listenerSetter});
  635. createContextMenu({
  636. buttons: [{
  637. icon: 'readchats',
  638. text: isReaction ? 'ReadAllReactions' : 'ReadAllMentions',
  639. onClick: () => {
  640. this.managers.appMessagesManager.readMentions(this.chat.peerId, this.chat.threadId, isReaction);
  641. }
  642. }],
  643. listenTo: btn,
  644. listenerSetter: this.listenerSetter
  645. });
  646. if(isReaction) {
  647. this.goReactionUnreadBadge = badge;
  648. this.goReactionBtn = btn;
  649. } else {
  650. this.goMentionUnreadBadge = badge;
  651. this.goMentionBtn = btn;
  652. }
  653. }
  654. private constructScheduledButton() {
  655. this.btnScheduled = this.createButtonIcon('scheduled btn-scheduled float hide', {noRipple: true});
  656. attachClickEvent(this.btnScheduled, (e) => {
  657. this.appImManager.openScheduled(this.chat.peerId);
  658. }, {listenerSetter: this.listenerSetter});
  659. this.listenerSetter.add(rootScope)('scheduled_new', ({peerId}) => {
  660. if(this.chat.peerId !== peerId) {
  661. return;
  662. }
  663. this.btnScheduled.classList.remove('hide');
  664. });
  665. this.listenerSetter.add(rootScope)('scheduled_delete', ({peerId}) => {
  666. if(this.chat.peerId !== peerId) {
  667. return;
  668. }
  669. this.managers.appMessagesManager.getScheduledMessages(this.chat.peerId).then((value) => {
  670. this.btnScheduled.classList.toggle('hide', !value.length);
  671. });
  672. });
  673. }
  674. private constructReplyMarkup() {
  675. this.btnToggleReplyMarkup = this.createButtonIcon('botcom toggle-reply-markup float hide', {noRipple: true});
  676. this.replyKeyboard = new ReplyKeyboard({
  677. appendTo: this.rowsWrapper,
  678. listenerSetter: this.listenerSetter,
  679. managers: this.managers,
  680. btnHover: this.btnToggleReplyMarkup,
  681. chatInput: this
  682. });
  683. this.listenerSetter.add(this.replyKeyboard)('open', () => this.btnToggleReplyMarkup.classList.add('active'));
  684. this.listenerSetter.add(this.replyKeyboard)('close', () => this.btnToggleReplyMarkup.classList.remove('active'));
  685. }
  686. private constructBotCommands() {
  687. this.botCommands = new ChatBotCommands(this.rowsWrapper, this, this.managers);
  688. this.botCommandsToggle = document.createElement('div');
  689. this.botCommandsToggle.classList.add('new-message-bot-commands');
  690. this.botCommandsToggle.append(Icon('webview', 'new-message-bot-commands-view-icon'));
  691. const scaler = document.createElement('div');
  692. scaler.classList.add('new-message-bot-commands-icon-scale');
  693. const icon = this.botCommandsIcon = document.createElement('div');
  694. icon.classList.add('animated-menu-icon', 'animated-menu-close-icon');
  695. scaler.append(icon);
  696. this.botCommandsView = document.createElement('div');
  697. this.botCommandsView.classList.add('new-message-bot-commands-view');
  698. this.botCommandsToggle.append(scaler, this.botCommandsView);
  699. let webViewTempId = 0, waitingForWebView = false;
  700. attachClickEvent(this.botCommandsToggle, (e) => {
  701. cancelEvent(e);
  702. const botId = this.chat.peerId.toUserId();
  703. const {botMenuButton} = this;
  704. if(botMenuButton) {
  705. if(waitingForWebView) {
  706. return;
  707. }
  708. const tempId = ++webViewTempId;
  709. waitingForWebView = true;
  710. Promise.resolve().then(() => {
  711. if(webViewTempId !== tempId) {
  712. return;
  713. }
  714. return this.chat.openWebApp({
  715. botId,
  716. url: botMenuButton.url,
  717. buttonText: botMenuButton.text,
  718. fromBotMenu: true
  719. });
  720. }).finally(() => {
  721. if(webViewTempId === tempId) {
  722. waitingForWebView = false;
  723. }
  724. });
  725. return;
  726. }
  727. const middleware = this.getMiddleware();
  728. const isShown = icon.classList.contains('state-back');
  729. if(isShown) {
  730. this.botCommands.toggle(true);
  731. // icon.classList.remove('state-back');
  732. } else {
  733. this.botCommands.setUserId(botId, middleware);
  734. // icon.classList.add('state-back');
  735. }
  736. }, {listenerSetter: this.listenerSetter});
  737. this.botCommands.addEventListener('visible', () => {
  738. icon.classList.add('state-back');
  739. });
  740. this.botCommands.addEventListener('hiding', () => {
  741. icon.classList.remove('state-back');
  742. });
  743. }
  744. private constructRecorder() {
  745. const Recorder = (window as any).Recorder;
  746. if(Recorder) try {
  747. this.recorder = new Recorder({
  748. // encoderBitRate: 32,
  749. // encoderPath: "../dist/encoderWorker.min.js",
  750. encoderSampleRate: 48000,
  751. monitorGain: 0,
  752. numberOfChannels: 1,
  753. recordingGain: 1,
  754. reuseWorker: true
  755. });
  756. } catch(err) {
  757. console.error('Recorder constructor error:', err);
  758. }
  759. if(!this.recorder) {
  760. return;
  761. }
  762. attachClickEvent(this.btnCancelRecord, this.onCancelRecordClick, {listenerSetter: this.listenerSetter});
  763. this.recorder.onstop = () => {
  764. this.setRecording(false);
  765. this.chatInput.classList.remove('is-locked');
  766. this.recordRippleEl.style.transform = '';
  767. };
  768. this.recorder.ondataavailable = async(typedArray: Uint8Array) => {
  769. if(this.releaseMediaPlayback) {
  770. this.releaseMediaPlayback();
  771. this.releaseMediaPlayback = undefined;
  772. }
  773. if(this.recordingOverlayListener) {
  774. this.listenerSetter.remove(this.recordingOverlayListener);
  775. this.recordingOverlayListener = undefined;
  776. }
  777. if(this.recordingNavigationItem) {
  778. appNavigationController.removeItem(this.recordingNavigationItem);
  779. this.recordingNavigationItem = undefined;
  780. }
  781. if(this.recordCanceled) {
  782. return;
  783. }
  784. const sendingParams = this.chat.getMessageSendingParams();
  785. const preparedPaymentResult = await this.paidMessageInterceptor.prepareStarsForPayment(1);
  786. if(preparedPaymentResult === PAYMENT_REJECTED) return;
  787. sendingParams.confirmedPaymentResult = preparedPaymentResult;
  788. const duration = (Date.now() - this.recordStartTime) / 1000 | 0;
  789. const dataBlob = new Blob([typedArray as unknown as ArrayBuffer], {type: 'audio/ogg'});
  790. opusDecodeController.decode(typedArray, true).then((result) => {
  791. opusDecodeController.setKeepAlive(false);
  792. // тут objectURL ставится уже с audio/wav
  793. this.managers.appMessagesManager.sendFile({
  794. ...sendingParams,
  795. file: dataBlob,
  796. isVoiceMessage: true,
  797. isMedia: true,
  798. duration,
  799. waveform: result.waveform,
  800. objectURL: result.url,
  801. clearDraft: true
  802. });
  803. this.onMessageSent(false, true);
  804. });
  805. };
  806. }
  807. public constructPeerHelpers() {
  808. if(!this.excludeParts.reply) {
  809. this.constructReplyElements();
  810. if(!this.excludeParts.forwardOptions) {
  811. this.constructForwardElements();
  812. this.constructWebPageElements();
  813. }
  814. }
  815. this.newMessageWrapper = document.createElement('div');
  816. this.newMessageWrapper.classList.add('new-message-wrapper', 'rows-wrapper-row');
  817. if(REPLY_IN_TOPIC) {
  818. this.replyInTopicOverlay = document.createElement('div');
  819. this.replyInTopicOverlay.classList.add('reply-in-topic-overlay', 'hide');
  820. this.replyInTopicOverlay.append(i18n('Chat.Input.ReplyToAnswer'));
  821. }
  822. if(!this.excludeParts.emoticons) this.btnToggleEmoticons = this.createButtonIcon('smile toggle-emoticons', {noRipple: true});
  823. this.inputMessageContainer = document.createElement('div');
  824. this.inputMessageContainer.classList.add('input-message-container');
  825. if(this.goDownBtn) {
  826. this.goDownUnreadBadge = createBadge('span', 24, 'primary');
  827. this.goDownBtn.append(this.goDownUnreadBadge);
  828. }
  829. if(!this.excludeParts.mentionButton) {
  830. this.constructMentionButton();
  831. this.constructMentionButton(true);
  832. }
  833. if(!this.excludeParts.scheduled) {
  834. this.constructScheduledButton();
  835. }
  836. if(!this.excludeParts.replyMarkup) {
  837. this.constructReplyMarkup();
  838. }
  839. if(!this.excludeParts.botCommands) {
  840. this.constructBotCommands();
  841. }
  842. // const getSendMediaRights = () => Promise.all([this.chat.canSend('send_photos'), this.chat.canSend('send_videos')]).then(([photos, videos]) => ({photos, videos}));
  843. this.attachMenuButtons = [{
  844. icon: 'image',
  845. text: 'Chat.Input.Attach.PhotoOrVideo',
  846. onClick: () => this.onAttachClick(false, true, true)
  847. // verify: () => getSendMediaRights().then(({photos, videos}) => photos && videos)
  848. }, /* {
  849. icon: 'image',
  850. text: 'AttachPhoto',
  851. onClick: () => onAttachMediaClick(true, false),
  852. verify: () => getSendMediaRights().then(({photos, videos}) => photos && !videos)
  853. }, {
  854. icon: 'image',
  855. text: 'AttachVideo',
  856. onClick: () => onAttachMediaClick(false, true),
  857. verify: () => getSendMediaRights().then(({photos, videos}) => !photos && videos)
  858. }, */ {
  859. icon: 'document',
  860. text: 'Chat.Input.Attach.Document',
  861. onClick: () => this.onAttachClick(true)
  862. // verify: () => this.chat.canSend('send_docs')
  863. }, {
  864. icon: 'gift',
  865. text: 'GiftPremium',
  866. onClick: () => this.chat.appImManager.giftPremium(this.chat.peerId),
  867. verify: () => {
  868. return this.chat && Promise.all([
  869. this.chat.canGiftPremium(),
  870. this.managers.apiManager.getAppConfig()
  871. ]).then(([canGift, {premium_gift_attach_menu_icon}]) => canGift && premium_gift_attach_menu_icon);
  872. }
  873. }, {
  874. icon: 'poll',
  875. text: 'Poll',
  876. onClick: async() => {
  877. const action: ChatRights = 'send_polls';
  878. if(!(await this.chat.canSend(action))) {
  879. toastNew({langPackKey: POSTING_NOT_ALLOWED_MAP[action]});
  880. return;
  881. }
  882. PopupElement.createPopup(PopupCreatePoll, this.chat).show();
  883. },
  884. verify: () => this.chat.peerId.isAnyChat() || this.chat.isBot
  885. }, {
  886. icon: 'poll',
  887. text: 'Checklist',
  888. onClick: async() => {
  889. if(this.chat.peerId.isAnyChat()) {
  890. const action: ChatRights = 'send_polls';
  891. if(!(await this.chat.canSend(action))) {
  892. toastNew({langPackKey: POSTING_NOT_ALLOWED_MAP[action]});
  893. return;
  894. }
  895. }
  896. if(!rootScope.premium) {
  897. PopupPremium.show();
  898. return;
  899. }
  900. PopupElement.createPopup(PopupChecklist, {chat: this.chat}).show();
  901. }
  902. }];
  903. const attachMenuButtons = this.attachMenuButtons.slice();
  904. this.attachMenu = ButtonMenuToggle({
  905. buttonOptions: {noRipple: true},
  906. listenerSetter: this.listenerSetter,
  907. direction: 'top-left',
  908. buttons: this.attachMenuButtons,
  909. onOpenBefore: this.excludeParts.attachMenu ? undefined : async() => {
  910. const attachMenuBots = await this.managers.appAttachMenuBotsManager.getAttachMenuBots();
  911. const buttons = attachMenuButtons.slice();
  912. const attachMenuBotsButtons = attachMenuBots.filter((attachMenuBot) => {
  913. return attachMenuBot.pFlags.show_in_attach_menu;
  914. }).map((attachMenuBot) => {
  915. const icon = getAttachMenuBotIcon(attachMenuBot);
  916. const button: typeof buttons[0] = {
  917. regularText: wrapEmojiText(attachMenuBot.short_name),
  918. onClick: () => {
  919. this.chat.openWebApp({attachMenuBot, fromAttachMenu: true});
  920. },
  921. iconDoc: icon?.icon as MyDocument,
  922. verify: async() => {
  923. let found = false;
  924. const verifyMap: {
  925. [type in AttachMenuPeerType['_']]: () => boolean | Promise<boolean>
  926. } = {
  927. attachMenuPeerTypeSameBotPM: () => this.chat.peerId.toUserId() === attachMenuBot.bot_id,
  928. attachMenuPeerTypeBotPM: () => this.chat.isBot,
  929. attachMenuPeerTypePM: () => this.chat.peerId.isUser(),
  930. attachMenuPeerTypeChat: () => this.chat.isAnyGroup,
  931. attachMenuPeerTypeBroadcast: () => this.chat.isBroadcast
  932. };
  933. for(const peerType of attachMenuBot.peer_types) {
  934. const verify = verifyMap[peerType._];
  935. found = await verify();
  936. if(found) {
  937. break;
  938. }
  939. }
  940. return found;
  941. }
  942. };
  943. return button;
  944. });
  945. buttons.splice(buttons.length, 0, ...attachMenuBotsButtons);
  946. this.attachMenuButtons.splice(0, this.attachMenuButtons.length, ...buttons);
  947. },
  948. onOpen: () => {
  949. this.emoticonsDropdown?.toggle(false);
  950. this.onMenuToggle?.(true);
  951. },
  952. onClose: () => {
  953. this.onMenuToggle?.(false);
  954. }
  955. });
  956. this.attachMenu.classList.add('attach-file');
  957. this.attachMenu.firstElementChild.replaceWith(Icon('attach'));
  958. // this.inputContainer.append(this.sendMenu);
  959. this.recordTimeEl = document.createElement('div');
  960. this.recordTimeEl.classList.add('record-time');
  961. this.fileInput = document.createElement('input');
  962. this.fileInput.type = 'file';
  963. this.fileInput.multiple = true;
  964. this.fileInput.style.display = 'none';
  965. this.newMessageWrapper.append(...[
  966. this.botCommandsToggle,
  967. this.btnToggleEmoticons,
  968. this.inputMessageContainer,
  969. this.btnScheduled,
  970. this.btnToggleReplyMarkup,
  971. this.attachMenu,
  972. this.recordTimeEl,
  973. this.fileInput
  974. ].filter(Boolean));
  975. if(this.replyElements?.container) this.rowsWrapper.append(this.replyElements.container);
  976. this.autocompleteHelperController = new AutocompleteHelperController();
  977. this.stickersHelper = new StickersHelper(this.rowsWrapper, this.autocompleteHelperController, this.chat, this.managers);
  978. this.emojiHelper = new EmojiHelper(this.rowsWrapper, this.autocompleteHelperController, this, this.managers);
  979. if(!this.excludeParts.commandsHelper) this.commandsHelper = new CommandsHelper(this.rowsWrapper, this.autocompleteHelperController, this, this.managers);
  980. this.mentionsHelper = new MentionsHelper(this.rowsWrapper, this.autocompleteHelperController, this, this.managers);
  981. this.inlineHelper = new InlineHelper(this.rowsWrapper, this.autocompleteHelperController, this.chat, this.managers);
  982. this.rowsWrapper.append(this.newMessageWrapper);
  983. this.btnCancelRecord = this.createButtonIcon('binfilled btn-circle btn-record-cancel chat-input-secondary-button chat-secondary-button');
  984. this.btnSendContainer = document.createElement('div');
  985. this.btnSendContainer.classList.add('btn-send-container');
  986. this.recordRippleEl = document.createElement('div');
  987. this.recordRippleEl.classList.add('record-ripple');
  988. this.btnSend = this.createButtonIcon();
  989. this.btnSend.classList.add('btn-circle', 'btn-send', 'animated-button-icon');
  990. const icons: [Icon, string][] = [
  991. ['send', 'send'],
  992. ['schedule', 'schedule'],
  993. ['check', 'edit'],
  994. ['microphone_filled', 'record'],
  995. ['forward_filled', 'forward']
  996. ];
  997. this.btnSend.append(...icons.map(([name, type]) => Icon(name, 'animated-button-icon-icon', 'btn-send-icon-' + type)));
  998. this.addStarsBadge();
  999. this.btnSendContainer.append(this.recordRippleEl, this.btnSend);
  1000. createRoot((dispose) => {
  1001. this.chat.destroyMiddlewareHelper.onDestroy(dispose);
  1002. const [effect, setEffect] = createSignal<DocId>();
  1003. this.effect = effect;
  1004. this.setEffect = setEffect;
  1005. this.btnSendContainer.append(SelectedEffect({effect: this.effect}) as HTMLElement);
  1006. });
  1007. this.sendMenu = new SendMenu({
  1008. onSilentClick: () => {
  1009. this.sendSilent = true;
  1010. this.sendMessage();
  1011. },
  1012. onScheduleClick: () => {
  1013. this.scheduleSending(undefined);
  1014. },
  1015. onSendWhenOnlineClick: () => {
  1016. this.setScheduleTimestamp(SEND_WHEN_ONLINE_TIMESTAMP, this.sendMessage.bind(this, true));
  1017. },
  1018. middleware: this.chat.destroyMiddlewareHelper.get(),
  1019. openSide: 'top-left',
  1020. onContextElement: this.btnSend,
  1021. onOpen: () => {
  1022. const good = this.chat.type !== ChatType.Scheduled && (!this.isInputEmpty() || !!Object.keys(this.forwarding).length) && !this.editMsgId;
  1023. if(good) {
  1024. this.emoticonsDropdown?.toggle(false);
  1025. }
  1026. return good;
  1027. },
  1028. canSendWhenOnline: this.canSendWhenOnline,
  1029. onRef: (element) => {
  1030. this.btnSendContainer.append(element);
  1031. },
  1032. withEffects: () => this.chat.peerId.isUser() && this.chat.peerId !== rootScope.myId,
  1033. effect: this.effect,
  1034. onEffect: this.setEffect
  1035. });
  1036. this.inputContainer.append(...[this.btnReaction, this.btnCancelRecord, this.btnSendContainer].filter(Boolean));
  1037. if(this.btnToggleEmoticons) {
  1038. this.emoticonsDropdown.attachButtonListener(this.btnToggleEmoticons, this.listenerSetter);
  1039. this.listenerSetter.add(this.emoticonsDropdown)('open', this.onEmoticonsOpen);
  1040. this.listenerSetter.add(this.emoticonsDropdown)('close', this.onEmoticonsClose);
  1041. if(emoticonsDropdown === this.emoticonsDropdown) {
  1042. createRoot((dispose) => {
  1043. this.chat.destroyMiddlewareHelper.onDestroy(dispose);
  1044. createEffect(() => {
  1045. const shouldBeTop = windowSize.height >= 570 && windowSize.width > 600;
  1046. this.emoticonsDropdown.getElement().classList.toggle('is-under', !shouldBeTop);
  1047. });
  1048. });
  1049. }
  1050. }
  1051. this.attachMessageInputField();
  1052. /* this.attachMenu.addEventListener('mousedown', (e) => {
  1053. const hidden = this.attachMenu.querySelectorAll('.hide');
  1054. if(hidden.length === this.attachMenuButtons.length) {
  1055. toast(POSTING_MEDIA_NOT_ALLOWED);
  1056. cancelEvent(e);
  1057. return false;
  1058. }
  1059. }, {passive: false, capture: true}); */
  1060. this.listenerSetter.add(rootScope)('settings_updated', () => {
  1061. if(this.stickersHelper || this.emojiHelper) {
  1062. // this.previousQuery = undefined;
  1063. this.previousQuery = '';
  1064. this.checkAutocomplete();
  1065. /* if(!rootScope.settings.stickers.suggest) {
  1066. this.stickersHelper.checkEmoticon('');
  1067. } else {
  1068. this.onMessageInput();
  1069. } */
  1070. }
  1071. this.messageInputField?.onFakeInput();
  1072. });
  1073. if(this.chat) {
  1074. this.setChatListeners();
  1075. }
  1076. this.constructRecorder();
  1077. this.updateSendBtn();
  1078. this.listenerSetter.add(this.fileInput)('change', (e) => {
  1079. const fileList = (e.target as HTMLInputElement & EventTarget).files;
  1080. const files = Array.from(fileList).slice();
  1081. this.fileSelectionPromise.resolve(files);
  1082. if(!files.length) {
  1083. return;
  1084. }
  1085. const newMediaPopup = getCurrentNewMediaPopup();
  1086. if(newMediaPopup) {
  1087. newMediaPopup.addFiles(files);
  1088. } else {
  1089. PopupElement.createPopup(PopupNewMedia, this.chat, files, this.willAttachType);
  1090. }
  1091. this.fileInput.value = '';
  1092. }, false);
  1093. attachClickEvent(this.btnSend, this.onBtnSendClick, {listenerSetter: this.listenerSetter, touchMouseDown: true});
  1094. this.saveDraftDebounced = debounce(() => this.saveDraft(), 2500, false, true);
  1095. const makeControlButton = (langKey: LangPackKey | HTMLElement) => {
  1096. const button = Button('btn-primary btn-transparent text-bold chat-input-control-button');
  1097. button.append(langKey instanceof HTMLElement ? langKey : i18n(langKey));
  1098. return button;
  1099. };
  1100. this.botStartBtn = makeControlButton('BotStart');
  1101. this.unblockBtn = makeControlButton('Unblock');
  1102. this.joinBtn = this.chat.topbar && makeControlButton('ChannelJoin');
  1103. this.onlyPremiumBtnText = new I18n.IntlElement({key: 'Chat.Input.PremiumRequiredButton', args: [0, document.createElement('a')]});
  1104. this.onlyPremiumBtn = makeControlButton(this.onlyPremiumBtnText.element);
  1105. attachClickEvent(this.botStartBtn, this.startBot, {listenerSetter: this.listenerSetter});
  1106. attachClickEvent(this.unblockBtn, this.unblockUser, {listenerSetter: this.listenerSetter});
  1107. attachClickEvent(this.onlyPremiumBtn, () => {
  1108. PopupPremium.show();
  1109. }, {listenerSetter: this.listenerSetter});
  1110. this.joinBtn && attachClickEvent(this.joinBtn, this.chat.topbar.onJoinClick.bind(this.chat.topbar, this.joinBtn), {listenerSetter: this.listenerSetter});
  1111. // * pinned part start
  1112. this.pinnedControlBtn = Button('btn-primary btn-transparent text-bold chat-input-control-button', {icon: 'unpin'});
  1113. this.listenerSetter.add(this.pinnedControlBtn)('click', () => {
  1114. const peerId = this.chat.peerId;
  1115. PopupElement.createPopup(PopupPinMessage, peerId, 0, true, () => {
  1116. this.chat.appImManager.setPeer({isDeleting: true}); // * close tab
  1117. // ! костыль, это скроет закреплённые сообщения сразу, вместо того, чтобы ждать пока анимация перехода закончится
  1118. const originalChat = this.chat.appImManager.chat;
  1119. if(originalChat.topbar.pinnedMessage) {
  1120. originalChat.topbar.pinnedMessage.pinnedMessageContainer.toggle(true);
  1121. }
  1122. });
  1123. });
  1124. // * pinned part end
  1125. this.openChatBtn = makeControlButton('OpenChat');
  1126. attachClickEvent(this.openChatBtn, () => {
  1127. this.chat.appImManager.setInnerPeer({
  1128. peerId: this.chat.threadId
  1129. });
  1130. }, {listenerSetter: this.listenerSetter});
  1131. this.controlContainer.append(...[
  1132. this.botStartBtn,
  1133. this.unblockBtn,
  1134. this.joinBtn,
  1135. this.onlyPremiumBtn,
  1136. this.replyInTopicOverlay,
  1137. this.pinnedControlBtn,
  1138. this.openChatBtn
  1139. ].filter(Boolean));
  1140. }
  1141. private setChatListeners() {
  1142. this.listenerSetter.add(rootScope)('draft_updated', ({peerId, threadId, draft, force}) => {
  1143. if(this.chat.threadId !== threadId || this.chat.peerId !== peerId || PEER_EXCEPTIONS.has(this.chat.type)) return;
  1144. this.setDraft(draft, true, force);
  1145. });
  1146. this.listenerSetter.add(this.appImManager)('peer_changing', (chat) => {
  1147. if(this.chat === chat && (this.chat.type === ChatType.Chat || this.chat.type === ChatType.Discussion)) {
  1148. this.saveDraft();
  1149. }
  1150. });
  1151. this.listenerSetter.add(this.appImManager)('chat_changing', ({from, to}) => {
  1152. if(this.chat === from) {
  1153. this.autocompleteHelperController.toggleListNavigation(false);
  1154. } else if(this.chat === to) {
  1155. this.autocompleteHelperController.toggleListNavigation(true);
  1156. }
  1157. });
  1158. this.listenerSetter.add(rootScope)('scheduled_delete', ({peerId, mids}) => {
  1159. if(this.chat.type === ChatType.Scheduled && this.chat.peerId === peerId && mids.includes(this.editMsgId)) {
  1160. this.onMessageSent();
  1161. }
  1162. });
  1163. this.listenerSetter.add(rootScope)('history_delete', ({peerId, msgs}) => {
  1164. if(this.chat.peerId === peerId && !PEER_EXCEPTIONS.has(this.chat.type)) {
  1165. if(msgs.has(this.editMsgId)) {
  1166. this.onMessageSent();
  1167. }
  1168. if(this.replyToMsgId && msgs.has(this.replyToMsgId)) {
  1169. this.clearHelper('reply');
  1170. }
  1171. /* if(this.chat.isStartButtonNeeded()) {
  1172. this.setStartParam(BOT_START_PARAM);
  1173. } */
  1174. }
  1175. });
  1176. this.listenerSetter.add(rootScope)('dialogs_multiupdate', (dialogs) => {
  1177. if(dialogs.has(this.chat.peerId) && (this.chat.type === ChatType.Chat || this.chat.type === ChatType.Discussion)) {
  1178. if(this.startParam === BOT_START_PARAM) {
  1179. this.setStartParam();
  1180. } else { // updateNewMessage comes earlier than dialog appers
  1181. this.center(true);
  1182. }
  1183. }
  1184. });
  1185. }
  1186. public onAttachClick = async(documents?: boolean, photos?: boolean, videos?: boolean) => {
  1187. if(await this.showSlowModeTooltipIfNeeded({
  1188. element: this.attachMenu
  1189. })) {
  1190. return;
  1191. }
  1192. const promise = this.fileSelectionPromise = deferredPromise();
  1193. this.fileInput.value = '';
  1194. promise.finally(() => {
  1195. idleController.removeEventListener('change', onIdleChange);
  1196. if(promise !== this.fileSelectionPromise) {
  1197. return;
  1198. }
  1199. });
  1200. const onIdleChange = (idle: boolean) => {
  1201. if(promise !== this.fileSelectionPromise) {
  1202. promise.reject();
  1203. return;
  1204. }
  1205. if(!idle) {
  1206. setTimeout(() => {
  1207. promise.reject();
  1208. }, 1000);
  1209. }
  1210. };
  1211. idleController.addEventListener('change', onIdleChange);
  1212. if(documents) {
  1213. this.fileInput.removeAttribute('accept');
  1214. this.willAttachType = 'document';
  1215. } else {
  1216. const accept = [
  1217. ...(photos ? IMAGE_MIME_TYPES_SUPPORTED : []),
  1218. ...(videos ? VIDEO_MIME_TYPES_SUPPORTED : [])
  1219. ].join(', ');
  1220. this.fileInput.setAttribute('accept', accept || '*/*');
  1221. this.willAttachType = 'media';
  1222. }
  1223. this.fileInput.click();
  1224. this.onFileSelection?.(this.fileSelectionPromise);
  1225. };
  1226. public _center(neededFakeContainer: HTMLElement, animate?: boolean) {
  1227. if(!neededFakeContainer && !this.inputContainer.classList.contains('is-centering')) {
  1228. return;
  1229. }
  1230. if(neededFakeContainer === this.fakeWrapperTo) {
  1231. return;
  1232. }
  1233. /* if(neededFakeContainer === this.botStartContainer && this.fakeWrapperTo === this.fakeSelectionWrapper) {
  1234. this.inputContainer.classList.remove('is-centering');
  1235. void this.rowsWrapper.offsetLeft; // reflow
  1236. // this.inputContainer.classList.add('is-centering');
  1237. // void this.rowsWrapper.offsetLeft; // reflow
  1238. } */
  1239. const fakeSelectionWrapper = neededFakeContainer || this.fakeWrapperTo;
  1240. const forwards = !!neededFakeContainer;
  1241. const oldFakeWrapperTo = this.fakeWrapperTo;
  1242. let transform = '', borderRadius = '', needTranslateX: number;
  1243. // if(forwards) {]
  1244. const fakeSelectionRect = fakeSelectionWrapper.getBoundingClientRect();
  1245. const fakeRowsRect = this.fakeRowsWrapper.getBoundingClientRect();
  1246. const widthFrom = fakeRowsRect.width;
  1247. const widthTo = fakeSelectionRect.width;
  1248. if(widthFrom !== widthTo) {
  1249. const scale = (widthTo/* - 8 */) / widthFrom;
  1250. const initTranslateX = (widthFrom - widthTo) / 2;
  1251. needTranslateX = fakeSelectionRect.left - fakeRowsRect.left - initTranslateX;
  1252. if(forwards) {
  1253. transform = `translateX(${needTranslateX}px) scaleX(${scale})`;
  1254. // transform = `translateX(0px) scaleX(${scale})`;
  1255. if(scale < 1) {
  1256. const br = 16;
  1257. borderRadius = '' + (br + br * (1 - scale)) + 'px';
  1258. }
  1259. }
  1260. // scale = widthTo / widthFrom;
  1261. }
  1262. // }
  1263. this.fakeWrapperTo = neededFakeContainer;
  1264. const duration = animate ? 200 : 0;
  1265. SetTransition({
  1266. element: this.inputContainer,
  1267. className: 'is-centering',
  1268. forwards,
  1269. duration
  1270. });
  1271. SetTransition({
  1272. element: this.rowsWrapperWrapper,
  1273. className: 'is-centering-to-control',
  1274. forwards: !!(forwards && neededFakeContainer && neededFakeContainer.classList.contains('chat-input-control')),
  1275. duration
  1276. });
  1277. this.rowsWrapper.style.transform = transform;
  1278. this.rowsWrapper.style.borderRadius = borderRadius;
  1279. return {
  1280. transform,
  1281. borderRadius,
  1282. needTranslateX: oldFakeWrapperTo && (
  1283. (
  1284. neededFakeContainer &&
  1285. neededFakeContainer.classList.contains('chat-input-control') &&
  1286. oldFakeWrapperTo === this.fakeSelectionWrapper
  1287. ) || oldFakeWrapperTo.classList.contains('chat-input-control')
  1288. ) ? needTranslateX * -.5 : needTranslateX,
  1289. widthFrom,
  1290. widthTo
  1291. };
  1292. }
  1293. public async center(animate = false) {
  1294. return this._center(await this.getNeededFakeContainer(), animate);
  1295. }
  1296. public setStartParam(startParam?: string) {
  1297. if(this.startParam === startParam) {
  1298. return;
  1299. }
  1300. this.startParam = startParam;
  1301. this.center(true);
  1302. }
  1303. public unblockUser = () => {
  1304. const toggle = this.toggleControlButtonDisability = toggleDisability([this.unblockBtn], true);
  1305. const peerId = this.chat.peerId;
  1306. const middleware = this.getMiddleware(() => {
  1307. return this.chat.peerId === peerId && this.toggleControlButtonDisability === toggle;
  1308. });
  1309. this.managers.appUsersManager.toggleBlock(peerId, false).then(() => {
  1310. if(middleware()) {
  1311. toggle();
  1312. this.toggleControlButtonDisability = undefined;
  1313. }
  1314. });
  1315. };
  1316. public startBot = () => {
  1317. const {startParam} = this;
  1318. const toggle = this.toggleControlButtonDisability = toggleDisability([this.botStartBtn], true);
  1319. const peerId = this.chat.peerId;
  1320. const middleware = this.getMiddleware(() => {
  1321. return this.chat.peerId === peerId &&
  1322. this.startParam === startParam &&
  1323. this.toggleControlButtonDisability === toggle;
  1324. });
  1325. this.managers.appMessagesManager.startBot(peerId.toUserId(), undefined, startParam).then(() => {
  1326. if(middleware()) {
  1327. toggle();
  1328. this.toggleControlButtonDisability = undefined;
  1329. this.setStartParam();
  1330. }
  1331. });
  1332. };
  1333. public isReplyInTopicOverlayNeeded() {
  1334. return REPLY_IN_TOPIC &&
  1335. this.chat.isForum &&
  1336. !this.chat.isForumTopic &&
  1337. !this.replyToMsgId &&
  1338. this.chat.type === ChatType.Chat;
  1339. }
  1340. public getJoinButtonType() {
  1341. const {peerId, threadId} = this.chat;
  1342. if(peerId.isUser()) {
  1343. return;
  1344. }
  1345. const chat = apiManagerProxy.getChat(peerId.toChatId());
  1346. if(!chat || !(chat as MTChat.channel).pFlags.left || (chat as MTChat.channel).pFlags.broadcast) {
  1347. return;
  1348. }
  1349. if((chat as MTChat.channel).pFlags.join_request) {
  1350. return 'request';
  1351. }
  1352. if((chat as MTChat.channel).pFlags.join_to_send || !threadId) {
  1353. return 'join';
  1354. }
  1355. }
  1356. public async getNeededFakeContainer(startParam = this.startParam) {
  1357. if(this.chat.selection?.isSelecting) {
  1358. return this.fakeSelectionWrapper;
  1359. } else if(
  1360. // startParam !== undefined || // * startParam isn't always should force control container, so it's commented
  1361. // !(await this.chat.canSend()) || // ! WARNING, TEMPORARILY COMMENTED
  1362. this.chat.type === ChatType.Pinned ||
  1363. (this.chat.type === ChatType.Saved && this.chat.threadId !== this.chat.peerId) ||
  1364. await this.chat.isStartButtonNeeded() ||
  1365. this.isReplyInTopicOverlayNeeded() ||
  1366. (this.chat.peerId.isUser() && (this.chat.isUserBlocked || this.chat.isPremiumRequired)) ||
  1367. this.getJoinButtonType()
  1368. ) {
  1369. return this.controlContainer;
  1370. }
  1371. }
  1372. // public getActiveContainer() {
  1373. // if(this.chat.selection.isSelecting) {
  1374. // return this.chat
  1375. // }
  1376. // return this.startParam !== undefined ? this.botStartContainer : this.rowsWrapper;
  1377. // }
  1378. // public setActiveContainer() {
  1379. // const container = this.activeContainer;
  1380. // const newContainer = this.getActiveContainer();
  1381. // if(newContainer === container) {
  1382. // return;
  1383. // }
  1384. // }
  1385. private onCancelRecordClick = (e?: Event) => {
  1386. if(e) {
  1387. cancelEvent(e);
  1388. }
  1389. this.recordCanceled = true;
  1390. this.recorder.stop();
  1391. opusDecodeController.setKeepAlive(false);
  1392. };
  1393. private onEmoticonsToggle = (open: boolean) => {
  1394. if(!this.btnToggleEmoticons) {
  1395. return;
  1396. }
  1397. if(!IS_TOUCH_SUPPORTED) {
  1398. this.btnToggleEmoticons.classList.toggle('active', open);
  1399. } else {
  1400. replaceButtonIcon(this.btnToggleEmoticons, open ? 'keyboard' : 'smile');
  1401. }
  1402. };
  1403. private onEmoticonsOpen = () => {
  1404. this.onEmoticonsToggle(true);
  1405. };
  1406. private onEmoticonsClose = () => {
  1407. this.onEmoticonsToggle(false);
  1408. };
  1409. public getReadyToSend(callback: () => void) {
  1410. return this.chat.type === ChatType.Scheduled ? (this.scheduleSending(callback), true) : (callback(), false);
  1411. }
  1412. public canSendWhenOnline = async() => {
  1413. const peerId = this.chat.peerId;
  1414. if(rootScope.myId === peerId || !peerId.isUser()) {
  1415. return false;
  1416. }
  1417. if(!(await this.managers.appUsersManager.isUserOnlineVisible(peerId))) {
  1418. return false;
  1419. }
  1420. const user = await this.managers.appUsersManager.getUser(peerId);
  1421. return user.status?._ !== 'userStatusOnline';
  1422. };
  1423. public setScheduleTimestamp(timestamp: number, callback: () => void) {
  1424. const middleware = this.getMiddleware();
  1425. const minTimestamp = (Date.now() / 1000 | 0) + 10;
  1426. if(timestamp <= minTimestamp) {
  1427. timestamp = undefined;
  1428. }
  1429. this.scheduleDate = timestamp;
  1430. callback();
  1431. if(this.chat.type !== ChatType.Scheduled && this.chat.type !== ChatType.Stories && timestamp) {
  1432. setTimeout(() => { // ! need timeout here because .forwardMessages will be called after timeout
  1433. if(!middleware()) {
  1434. return;
  1435. }
  1436. const popups = PopupElement.getPopups(PopupStickers);
  1437. popups.forEach((popup) => popup.hide());
  1438. this.appImManager.openScheduled(this.chat.peerId);
  1439. }, 0);
  1440. }
  1441. }
  1442. public getMiddleware(...args: Parameters<Chat['bubbles']['getMiddleware']>) {
  1443. return this.chat.bubbles.getMiddleware(...args);
  1444. }
  1445. public scheduleSending = async(
  1446. callback: () => void = this.sendMessage.bind(this, true),
  1447. initDate = new Date()
  1448. ) => {
  1449. const middleware = this.getMiddleware();
  1450. const canSendWhenOnline = await this.canSendWhenOnline();
  1451. if(!middleware()) {
  1452. return;
  1453. }
  1454. PopupElement.createPopup(PopupSchedule, {
  1455. initDate,
  1456. onPick: (timestamp) => {
  1457. if(!middleware()) {
  1458. return;
  1459. }
  1460. this.setScheduleTimestamp(timestamp, callback);
  1461. },
  1462. canSendWhenOnline
  1463. }).show();
  1464. };
  1465. public async setUnreadCount() {
  1466. if(!this.goDownUnreadBadge) {
  1467. return;
  1468. }
  1469. const dialog = await this.managers.dialogsStorage.getAnyDialog(
  1470. this.chat.peerId,
  1471. this.chat.type === ChatType.Discussion ? undefined : this.chat.threadId
  1472. );
  1473. if(isSavedDialog(dialog)) {
  1474. return;
  1475. }
  1476. const count = dialog?.unread_count;
  1477. setBadgeContent(this.goDownUnreadBadge, '' + (count || ''));
  1478. const isPeerLocalMuted = await this.managers.appNotificationsManager.isPeerLocalMuted({
  1479. peerId: this.chat.peerId,
  1480. respectType: true,
  1481. threadId: this.chat.threadId
  1482. });
  1483. this.goDownUnreadBadge.classList.toggle('badge-gray', isPeerLocalMuted);
  1484. if(this.goMentionUnreadBadge && this.chat.type === ChatType.Chat) {
  1485. const hasMentions = !!(dialog?.unread_mentions_count && dialog.unread_count);
  1486. setBadgeContent(this.goMentionUnreadBadge, hasMentions ? '' + (dialog.unread_mentions_count) : '');
  1487. this.goMentionBtn.classList.toggle('is-visible', hasMentions);
  1488. }
  1489. if(this.goReactionUnreadBadge && this.chat.type === ChatType.Chat) {
  1490. const hasReactions = !!dialog?.unread_reactions_count;
  1491. setBadgeContent(this.goReactionUnreadBadge, hasReactions ? '' + (dialog.unread_reactions_count) : '');
  1492. this.goReactionBtn.classList.toggle('is-visible', hasReactions);
  1493. }
  1494. }
  1495. public getCurrentInputAsDraft(ignoreEmptyValue?: boolean) {
  1496. const {value, entities} = getRichValueWithCaret(this.messageInputField.input, true, false);
  1497. let draft: DraftMessage.draftMessage;
  1498. if((value.length || ignoreEmptyValue) || this.replyToMsgId || this.willSendWebPage) {
  1499. const webPage = this.willSendWebPage as WebPage.webPage;
  1500. const webPageOptions = this.webPageOptions;
  1501. const hasLargeMedia = !!webPage?.pFlags?.has_large_media;
  1502. const replyTo = this.getReplyTo();
  1503. draft = {
  1504. _: 'draftMessage',
  1505. date: tsNow(true),
  1506. message: value.trim(),
  1507. entities: entities.length ? entities : undefined,
  1508. pFlags: {
  1509. no_webpage: this.noWebPage,
  1510. invert_media: this.invertMedia || undefined
  1511. },
  1512. reply_to: replyTo ? {
  1513. _: 'inputReplyToMessage',
  1514. reply_to_msg_id: replyTo.replyToMsgId,
  1515. top_msg_id: this.chat.threadId,
  1516. reply_to_peer_id: replyTo.replyToPeerId,
  1517. ...(replyTo.replyToQuote && {
  1518. quote_text: replyTo.replyToQuote.text,
  1519. quote_entities: replyTo.replyToQuote.entities,
  1520. quote_offset: replyTo.replyToQuote.offset
  1521. })
  1522. } : undefined,
  1523. media: webPage ? {
  1524. _: 'inputMediaWebPage',
  1525. pFlags: {
  1526. force_large_media: hasLargeMedia && webPageOptions?.largeMedia || undefined,
  1527. force_small_media: hasLargeMedia && webPageOptions?.smallMedia || undefined,
  1528. optional: true
  1529. },
  1530. url: webPage.url
  1531. } : undefined,
  1532. effect: this.effect()
  1533. };
  1534. }
  1535. return draft;
  1536. }
  1537. public saveDraft() {
  1538. if(
  1539. !this.chat.peerId ||
  1540. this.editMsgId ||
  1541. PEER_EXCEPTIONS.has(this.chat.type)
  1542. ) {
  1543. return;
  1544. }
  1545. const draft = this.getCurrentInputAsDraft();
  1546. this.managers.appDraftsManager.syncDraft(this.chat.peerId, this.chat.threadId, draft);
  1547. }
  1548. public mentionUser(peerId: PeerId, isHelper?: boolean) {
  1549. Promise.resolve(this.managers.appPeersManager.getPeer(peerId)).then((peer) => {
  1550. let str = '', entity: MessageEntity;
  1551. const usernames = getPeerActiveUsernames(peer);
  1552. if(usernames[0]) {
  1553. str = '@' + usernames[0];
  1554. } else {
  1555. if(peerId.isUser()) {
  1556. str = (peer as User.user).first_name || (peer as User.user).last_name;
  1557. } else {
  1558. str = (peer as MTChat.channel).title;
  1559. }
  1560. entity = {
  1561. _: 'messageEntityMentionName',
  1562. length: str.length,
  1563. offset: 0,
  1564. user_id: peer.id
  1565. };
  1566. }
  1567. str += ' ';
  1568. this.insertAtCaret(str, entity, isHelper);
  1569. });
  1570. }
  1571. public destroy() {
  1572. // this.chat.log.error('Input destroying');
  1573. this.listenerSetter.removeAll();
  1574. this.setCurrentHover();
  1575. }
  1576. public cleanup(helperToo = true) {
  1577. if(this.chat && !this.chat.peerId) {
  1578. this.chatInput.classList.add('hide');
  1579. this.goDownBtn.classList.add('hide');
  1580. }
  1581. cancelSelection();
  1582. this.lastTimeType = 0;
  1583. this.startParam = undefined;
  1584. if(this.toggleControlButtonDisability) {
  1585. this.toggleControlButtonDisability();
  1586. this.toggleControlButtonDisability = undefined;
  1587. }
  1588. if(this.messageInput) {
  1589. this.clearInput();
  1590. helperToo && this.clearHelper();
  1591. }
  1592. }
  1593. public async setDraft(draft?: MyDraftMessage, fromUpdate = true, force = false) {
  1594. if(
  1595. (!force && !isInputEmpty(this.messageInput)) ||
  1596. PEER_EXCEPTIONS.has(this.chat.type)
  1597. ) {
  1598. return false;
  1599. }
  1600. if(!draft) {
  1601. draft = await this.managers.appDraftsManager.getDraft(this.chat.peerId, this.chat.threadId);
  1602. if(!draft) {
  1603. if(force) { // this situation can only happen when sending message with clearDraft
  1604. /* const height = this.chatInput.getBoundingClientRect().height;
  1605. const willChangeHeight = 78 - height;
  1606. this.willChangeHeight = willChangeHeight; */
  1607. if(this.chat.container.classList.contains('is-helper-active')) {
  1608. this.t();
  1609. }
  1610. this.messageInputField.inputFake.textContent = '';
  1611. this.messageInputField.onFakeInput(false);
  1612. ((this.chat.bubbles.messagesQueuePromise || Promise.resolve()) as Promise<any>).then(() => {
  1613. fastRaf(() => {
  1614. this.onMessageSent();
  1615. });
  1616. });
  1617. }
  1618. return false;
  1619. }
  1620. }
  1621. const wrappedDraft = wrapDraft(draft, {wrappingForPeerId: this.chat.peerId});
  1622. const currentDraft = this.getCurrentInputAsDraft();
  1623. const replyTo = draft.reply_to as InputReplyTo.inputReplyToMessage;
  1624. const draftReplyToMsgId = replyTo?.reply_to_msg_id;
  1625. if(draftsAreEqual(draft, currentDraft)) {
  1626. return false;
  1627. }
  1628. if(fromUpdate) {
  1629. this.clearHelper();
  1630. }
  1631. this.noWebPage = draft.pFlags.no_webpage;
  1632. if(draftReplyToMsgId) {
  1633. this.initMessageReply({
  1634. replyToMsgId: draftReplyToMsgId,
  1635. replyToPeerId: replyTo.reply_to_peer_id && getPeerId(replyTo.reply_to_peer_id),
  1636. replyToQuote: replyTo.quote_text && {
  1637. text: replyTo.quote_text,
  1638. entities: replyTo.quote_entities,
  1639. offset: replyTo.quote_offset
  1640. }
  1641. });
  1642. }
  1643. this.setInputValue(wrappedDraft, fromUpdate, fromUpdate, draft);
  1644. return true;
  1645. }
  1646. private createSendAs() {
  1647. this.sendAsPeerId = undefined;
  1648. if(this.chat && (this.chat.type === ChatType.Chat || this.chat.type === ChatType.Discussion)) {
  1649. let firstChange = true;
  1650. this.sendAs = new ChatSendAs({
  1651. managers: this.managers,
  1652. onReady: (container, skipAnimation) => {
  1653. let useRafs = 0;
  1654. if(!container.parentElement) {
  1655. this.newMessageWrapper.prepend(container);
  1656. useRafs = 2;
  1657. }
  1658. this.updateOffset('as', true, skipAnimation, useRafs);
  1659. },
  1660. onChange: (sendAsPeerId) => {
  1661. this.sendAsPeerId = sendAsPeerId;
  1662. // do not change placeholder earlier than finishPeerChange does
  1663. if(firstChange) {
  1664. firstChange = false;
  1665. return;
  1666. }
  1667. this.getPlaceholderParams().then((params) => {
  1668. this.updateMessageInputPlaceholder(params);
  1669. });
  1670. }
  1671. });
  1672. } else {
  1673. this.sendAs = undefined;
  1674. }
  1675. return this.sendAs;
  1676. }
  1677. public async finishPeerChange(options: Parameters<Chat['finishPeerChange']>[0]) {
  1678. const {peerId, startParam, middleware} = options;
  1679. const {
  1680. forwardElements,
  1681. btnScheduled,
  1682. replyKeyboard,
  1683. sendMenu,
  1684. goDownBtn,
  1685. chatInput,
  1686. botCommandsToggle,
  1687. attachMenu
  1688. } = this;
  1689. const previousSendAs = this.sendAs;
  1690. const sendAs = this.createSendAs();
  1691. const filteredAttachMenuButtons = this.filterAttachMenuButtons();
  1692. const [
  1693. isBroadcast,
  1694. canPinMessage,
  1695. isBot,
  1696. canSend,
  1697. canSendPlain,
  1698. neededFakeContainer,
  1699. ackedPeerFull,
  1700. ackedScheduledMids,
  1701. setSendAsCallback,
  1702. peerTitleShort,
  1703. isPremiumRequired
  1704. ] = await Promise.all([
  1705. this.managers.appPeersManager.isBroadcast(peerId),
  1706. this.managers.appPeersManager.canPinMessage(peerId),
  1707. this.managers.appPeersManager.isBot(peerId),
  1708. this.chat?.canSend('send_messages') || true,
  1709. this.chat?.canSend('send_plain') || true,
  1710. this.getNeededFakeContainer(startParam),
  1711. modifyAckedPromise(this.managers.acknowledged.appProfileManager.getProfileByPeerId(peerId)),
  1712. btnScheduled ? modifyAckedPromise(this.managers.acknowledged.appMessagesManager.getScheduledMessages(peerId)) : undefined,
  1713. sendAs ? (sendAs.setPeerId(peerId), sendAs.updateManual(true)) : undefined,
  1714. wrapPeerTitle({peerId, onlyFirstName: true}),
  1715. this.chat.isPremiumRequiredToContact()
  1716. ]);
  1717. const placeholderParams = this.messageInput ? await this.getPlaceholderParams(canSendPlain) : undefined;
  1718. return () => {
  1719. // console.warn('[input] finishpeerchange start');
  1720. chatInput.classList.remove('hide');
  1721. if(goDownBtn) {
  1722. goDownBtn.classList.toggle('is-broadcast', isBroadcast);
  1723. goDownBtn.classList.remove('hide');
  1724. }
  1725. if(this.goDownUnreadBadge) {
  1726. this.setUnreadCount();
  1727. }
  1728. if(this.chat?.type === ChatType.Pinned) {
  1729. chatInput.classList.toggle('can-pin', canPinMessage);
  1730. }/* else if(this.chat.type === 'chat') {
  1731. } */
  1732. if(forwardElements) {
  1733. this.forwardWasDroppingAuthor = false;
  1734. forwardElements.showCaption.checkboxField.setValueSilently(true);
  1735. forwardElements.showSender.checkboxField.setValueSilently(true);
  1736. }
  1737. if(btnScheduled && ackedScheduledMids) {
  1738. btnScheduled.classList.add('hide');
  1739. callbackify(ackedScheduledMids.result, (mids) => {
  1740. if(!middleware() || !mids) return;
  1741. btnScheduled.classList.toggle('hide', !mids.length);
  1742. });
  1743. }
  1744. if(this.newMessageWrapper) {
  1745. this.updateOffset(null, false, true);
  1746. }
  1747. if(botCommandsToggle) {
  1748. this.hasBotCommands = undefined;
  1749. this.botMenuButton = undefined;
  1750. this.botCommands.toggle(true, undefined, true);
  1751. this.updateBotCommandsToggle(true);
  1752. botCommandsToggle.remove();
  1753. if(isBot) {
  1754. const result = ackedPeerFull.result;
  1755. callbackify(result, (userFull) => {
  1756. if(!middleware()) return;
  1757. this.updateBotCommands(userFull as UserFull.userFull, !(result instanceof Promise));
  1758. });
  1759. }
  1760. }
  1761. previousSendAs?.destroy();
  1762. setSendAsCallback?.();
  1763. replyKeyboard?.setPeer(peerId);
  1764. sendMenu?.setPeerParams({peerId, isPaid: !!this.chat.starsAmount});
  1765. let haveSomethingInControl = false;
  1766. if(this.chat && this.joinBtn) {
  1767. const type = this.getJoinButtonType();
  1768. const good = !haveSomethingInControl && !!type;
  1769. haveSomethingInControl ||= good;
  1770. this.joinBtn.classList.toggle('hide', !good);
  1771. this.joinBtn.replaceChildren(i18n(type === 'request' ? 'ChannelJoinRequest' : 'ChannelJoin'));
  1772. }
  1773. if(this.chat && this.pinnedControlBtn) {
  1774. const good = !haveSomethingInControl && this.chat.type === ChatType.Pinned;
  1775. haveSomethingInControl ||= good;
  1776. this.pinnedControlBtn.classList.toggle('hide', !good);
  1777. this.pinnedControlBtn.replaceChildren(i18n(canPinMessage ? 'Chat.Input.UnpinAll' : 'Chat.Pinned.DontShow'));
  1778. }
  1779. if(this.chat && this.openChatBtn) {
  1780. const good = !haveSomethingInControl && this.chat.type === ChatType.Saved;
  1781. haveSomethingInControl ||= good;
  1782. if(good) {
  1783. const savedPeerId = this.chat.threadId;
  1784. const peer = apiManagerProxy.getPeer(savedPeerId);
  1785. const key: LangPackKey = (peer as MTChat.channel).pFlags.broadcast ? 'OpenChannel2' : (savedPeerId.isUser() ? ((peer as User.user).pFlags.bot ? 'BotWebViewOpenBot' : 'OpenChat') : 'OpenGroup2');
  1786. const span = i18n(key);
  1787. this.openChatBtn.querySelector('.i18n').replaceWith(span);
  1788. }
  1789. this.openChatBtn.classList.toggle('hide', !good);
  1790. }
  1791. if(REPLY_IN_TOPIC && this.chat) {
  1792. const good = !haveSomethingInControl && this.chat.isForum && !this.chat.isForumTopic && this.chat.type === ChatType.Chat;
  1793. haveSomethingInControl ||= good;
  1794. this.replyInTopicOverlay.classList.toggle('hide', !good);
  1795. }
  1796. if(this.chat && this.onlyPremiumBtn) {
  1797. const good = !haveSomethingInControl && !isBot && peerId.isUser() && isPremiumRequired;
  1798. haveSomethingInControl ||= good;
  1799. this.onlyPremiumBtnText.compareAndUpdate({
  1800. args: [peerTitleShort, this.onlyPremiumBtnText.args[1]]
  1801. });
  1802. this.onlyPremiumBtn.classList.toggle('hide', !good);
  1803. }
  1804. if(this.chat) {
  1805. const good = !haveSomethingInControl && !isBot && peerId.isUser();
  1806. haveSomethingInControl ||= good;
  1807. this.unblockBtn.classList.toggle('hide', !good);
  1808. }
  1809. this.botStartBtn.classList.toggle('hide', haveSomethingInControl);
  1810. if(this.messageInput) {
  1811. this.updateMessageInput(
  1812. canSend || haveSomethingInControl,
  1813. canSendPlain,
  1814. placeholderParams,
  1815. peerId.isUser() ? options.text : undefined,
  1816. peerId.isUser() ? options.entities : undefined
  1817. );
  1818. this.messageInput.dataset.peerId = '' + peerId;
  1819. if(filteredAttachMenuButtons && attachMenu) {
  1820. filteredAttachMenuButtons.then((visible) => {
  1821. if(!middleware()) {
  1822. return;
  1823. }
  1824. attachMenu.toggleAttribute('disabled', !visible.length);
  1825. attachMenu.classList.toggle('btn-disabled', !visible.length);
  1826. });
  1827. }
  1828. }
  1829. this.messageInputField?.onFakeInput(undefined, true);
  1830. // * testing
  1831. // this.startParam = this.appPeersManager.isBot(peerId) ? '123' : undefined;
  1832. this.startParam = startParam;
  1833. this._center(neededFakeContainer, false);
  1834. this.setStarsAmount(this.chat.starsAmount); // should reset when undefined
  1835. // console.warn('[input] finishpeerchange ends');
  1836. };
  1837. }
  1838. private updateOffset(
  1839. type: ChatInput['hasOffset']['type'],
  1840. forwards: boolean,
  1841. skipAnimation?: boolean,
  1842. useRafs?: number,
  1843. applySameType?: boolean // ! WARNING
  1844. ) {
  1845. const prevOffset = this.hasOffset;
  1846. const newOffset: ChatInput['hasOffset'] = {type, forwards};
  1847. if(deepEqual(prevOffset, newOffset) && !applySameType) {
  1848. return;
  1849. }
  1850. this.hasOffset = newOffset;
  1851. if(type) {
  1852. this.newMessageWrapper.dataset.offset = type;
  1853. } else {
  1854. delete this.newMessageWrapper.dataset.offset;
  1855. }
  1856. if(prevOffset?.forwards === newOffset.forwards && !applySameType) {
  1857. return;
  1858. }
  1859. SetTransition({
  1860. element: this.newMessageWrapper,
  1861. className: 'has-offset',
  1862. forwards,
  1863. duration: skipAnimation ? 0 : 300,
  1864. useRafs
  1865. });
  1866. }
  1867. private updateBotCommands(userFull: UserFull.userFull, skipAnimation?: boolean) {
  1868. const botInfo = userFull.bot_info;
  1869. const menuButton = botInfo?.menu_button;
  1870. this.hasBotCommands = !!botInfo?.commands?.length;
  1871. this.botMenuButton = menuButton?._ === 'botMenuButton' ? menuButton : undefined;
  1872. replaceContent(this.botCommandsView, this.botMenuButton ? wrapEmojiText(this.botMenuButton.text) : '');
  1873. this.botCommandsIcon.classList.toggle('hide', !!this.botMenuButton);
  1874. this.botCommandsView.classList.toggle('hide', !this.botMenuButton);
  1875. this.botCommandsToggle.classList.toggle('is-view', !!this.botMenuButton);
  1876. this.updateBotCommandsToggle(skipAnimation);
  1877. }
  1878. private updateBotCommandsToggle(skipAnimation?: boolean) {
  1879. const {botCommandsToggle, hasBotCommands, botMenuButton} = this;
  1880. const isNeeded = !!(hasBotCommands || botMenuButton);
  1881. const isInputEmpty = this.isInputEmpty();
  1882. const show = isNeeded && (isInputEmpty || !!botMenuButton);
  1883. if(!isNeeded) {
  1884. if(!botCommandsToggle.parentElement) {
  1885. return;
  1886. }
  1887. botCommandsToggle.remove();
  1888. }
  1889. const forwards = show;
  1890. const useRafs = botCommandsToggle.parentElement ? 0 : 2;
  1891. if(botMenuButton && isInputEmpty) {
  1892. // padding + icon size + icon margin
  1893. const width = getTextWidth(botMenuButton.text, FontFull) + 22 + 20 + 6;
  1894. this.newMessageWrapper.style.setProperty('--commands-size', `${Math.ceil(width)}px`);
  1895. } else {
  1896. // this.newMessageWrapper.style.setProperty('--commands-size', `38px`);
  1897. this.newMessageWrapper.style.removeProperty('--commands-size');
  1898. }
  1899. if(!botCommandsToggle.parentElement) {
  1900. this.newMessageWrapper.prepend(botCommandsToggle);
  1901. }
  1902. this.updateOffset('commands', forwards, skipAnimation, useRafs, true);
  1903. }
  1904. private async getPlaceholderParams(canSend?: boolean): Promise<Parameters<ChatInput['updateMessageInputPlaceholder']>[0]> {
  1905. canSend ??= await this.chat.canSend('send_plain');
  1906. const {peerId, threadId, isForum, type} = this.chat;
  1907. let key: LangPackKey, args: FormatterArguments, inputStarsCountEl: HTMLElement;
  1908. if(!canSend) {
  1909. key = 'Channel.Persmission.MessageBlock';
  1910. } else if(threadId && !isForum && !peerId.isUser()) {
  1911. key = 'Comment';
  1912. } else if(await this.managers.appPeersManager.isBroadcast(peerId)) {
  1913. key = 'ChannelBroadcast';
  1914. } else if(
  1915. (this.sendAsPeerId !== undefined && this.sendAsPeerId !== rootScope.myId) ||
  1916. await this.managers.appMessagesManager.isAnonymousSending(peerId)
  1917. ) {
  1918. key = 'SendAnonymously';
  1919. } else if(type === ChatType.Stories) {
  1920. key = 'Story.ReplyPlaceholder';
  1921. } else if(isForum && type === ChatType.Chat && !threadId) {
  1922. const topic = await this.managers.dialogsStorage.getForumTopic(peerId, GENERAL_TOPIC_ID);
  1923. if(topic) {
  1924. key = 'TypeMessageIn';
  1925. args = [wrapEmojiText(topic.title)];
  1926. } else {
  1927. key = 'Message';
  1928. }
  1929. } else if(this.chat.starsAmount) {
  1930. key = 'PaidMessages.MessageForStars';
  1931. const starsElement = document.createElement('span');
  1932. const span = inputStarsCountEl = document.createElement('span');
  1933. starsElement.append(Icon('star', 'input-message-placeholder-stars'), span);
  1934. args = [starsElement];
  1935. } else {
  1936. key = 'Message';
  1937. }
  1938. return {key, args, inputStarsCountEl};
  1939. }
  1940. private updateMessageInputPlaceholder({key, args = [], inputStarsCountEl}: {key: LangPackKey, args?: FormatterArguments, inputStarsCountEl?: HTMLElement}) {
  1941. // console.warn('[input] update placeholder');
  1942. // const i = I18n.weakMap.get(this.messageInput) as I18n.IntlElement;
  1943. const i = I18n.weakMap.get(this.messageInputField.placeholder) as I18n.IntlElement;
  1944. if(!i) {
  1945. return;
  1946. }
  1947. const oldKey = i.key;
  1948. const oldArgs = i.args;
  1949. i.compareAndUpdateBool({key, args}) &&
  1950. this.starsState.set({inputStarsCountEl});
  1951. return {oldKey, oldArgs};
  1952. }
  1953. private filterAttachMenuButtons() {
  1954. if(!this.attachMenuButtons) return;
  1955. return filterAsync(this.attachMenuButtons, (button) => {
  1956. return button.verify ? button.verify() : true;
  1957. });
  1958. }
  1959. public updateMessageInput(
  1960. canSend: boolean,
  1961. canSendPlain: boolean,
  1962. placeholderParams: Parameters<ChatInput['updateMessageInputPlaceholder']>[0],
  1963. text?: string,
  1964. entities?: MessageEntity[]
  1965. ) {
  1966. const {chatInput, messageInput} = this;
  1967. const isHidden = chatInput.classList.contains('is-hidden');
  1968. const willBeHidden = !canSend;
  1969. if(isHidden !== willBeHidden) {
  1970. chatInput.classList.add('no-transition');
  1971. chatInput.classList.toggle('is-hidden', !canSend);
  1972. void chatInput.offsetLeft; // reflow
  1973. chatInput.classList.remove('no-transition');
  1974. }
  1975. const isEditingAndLocked = canSend && !canSendPlain && this.restoreInputLock;
  1976. !isEditingAndLocked && this.updateMessageInputPlaceholder(placeholderParams);
  1977. if(isEditingAndLocked) {
  1978. this.restoreInputLock = () => {
  1979. this.updateMessageInputPlaceholder(placeholderParams);
  1980. this.messageInput.contentEditable = 'false';
  1981. };
  1982. } else if(!canSend || !canSendPlain) {
  1983. messageInput.contentEditable = 'false';
  1984. if(!canSendPlain) {
  1985. this.messageInputField.onFakeInput(undefined, true);
  1986. }
  1987. } else {
  1988. this.restoreInputLock = undefined;
  1989. messageInput.contentEditable = 'true';
  1990. if(text) {
  1991. this.managers.appDraftsManager.setDraft(this.chat.peerId, undefined, text, entities);
  1992. }
  1993. this.setDraft(undefined, false);
  1994. if(!messageInput.innerHTML) {
  1995. this.messageInputField.onFakeInput(undefined, true);
  1996. }
  1997. }
  1998. this.updateSendBtn();
  1999. }
  2000. private attachMessageInputField() {
  2001. const oldInputField = this.messageInputField;
  2002. this.messageInputField = new InputFieldAnimated({
  2003. placeholder: 'Message',
  2004. // placeholderAsElement: true,
  2005. name: 'message',
  2006. withLinebreaks: true
  2007. });
  2008. this.messageInputField.input.tabIndex = -1;
  2009. this.messageInputField.input.classList.replace('input-field-input', 'input-message-input');
  2010. this.messageInputField.inputFake.classList.replace('input-field-input', 'input-message-input');
  2011. this.messageInput = this.messageInputField.input;
  2012. this.attachMessageInputListeners();
  2013. createMarkdownCache(this.messageInput);
  2014. if(IS_STICKY_INPUT_BUGGED) {
  2015. fixSafariStickyInputFocusing(this.messageInput);
  2016. }
  2017. if(oldInputField) {
  2018. oldInputField.input.replaceWith(this.messageInputField.input);
  2019. oldInputField.placeholder.replaceWith(this.messageInputField.placeholder);
  2020. oldInputField.inputFake.replaceWith(this.messageInputField.inputFake);
  2021. } else {
  2022. this.inputMessageContainer.append(this.messageInputField.input, this.messageInputField.placeholder, this.messageInputField.inputFake);
  2023. }
  2024. }
  2025. public passEventToInput(e: KeyboardEvent): void {
  2026. if(!isSendShortcutPressed(e)) return void focusInput(this.messageInput, e);
  2027. this.sendMessage();
  2028. document.addEventListener('keyup', () => {
  2029. focusInput(this.messageInput);
  2030. }, {once: true});
  2031. }
  2032. private attachMessageInputListeners() {
  2033. this.listenerSetter.add(this.messageInput)('keydown', (e) => {
  2034. const key = e.key;
  2035. if(isSendShortcutPressed(e)) {
  2036. cancelEvent(e);
  2037. this.sendMessage();
  2038. } else if(e.ctrlKey || e.metaKey) {
  2039. handleMarkdownShortcut(this.messageInput, e);
  2040. } else if((key === 'PageUp' || key === 'PageDown') && !e.shiftKey) { // * fix pushing page to left (Chrome Windows)
  2041. e.preventDefault();
  2042. if(key === 'PageUp') {
  2043. const range = document.createRange();
  2044. const sel = window.getSelection();
  2045. range.setStart(this.messageInput.childNodes[0] || this.messageInput, 0);
  2046. range.collapse(true);
  2047. sel.removeAllRanges();
  2048. sel.addRange(range);
  2049. } else {
  2050. placeCaretAtEnd(this.messageInput);
  2051. }
  2052. }
  2053. });
  2054. attachClickEvent(this.messageInput, (e) => {
  2055. if(!this.canSendPlain()) {
  2056. toastNew({
  2057. langPackKey: POSTING_NOT_ALLOWED_MAP['send_plain']
  2058. });
  2059. return;
  2060. }
  2061. // const checkPseudoElementClick = (e: MouseEvent, tag: 'after' | 'before') => {
  2062. // const target = (e.currentTarget || e.target) as HTMLElement;
  2063. // const pseudo = getComputedStyle(target, `:${tag}`);
  2064. // if(!pseudo) {
  2065. // return false;
  2066. // }
  2067. // const [atop, aheight, aleft, awidth] = ['top', 'height', 'left', 'width'].map((k) => pseudo.getPropertyValue(k).slice(0, -2));
  2068. // const ex = (e as any).layerX;
  2069. // const ey = (e as any).layerY;
  2070. // if(ex > aleft && ex < (aleft + awidth) && ey > atop && ey < (atop + aheight)) {
  2071. // return true;
  2072. // }
  2073. // return false;
  2074. // };
  2075. const checkIconClick = (e: MouseEvent, quote: HTMLElement) => {
  2076. const rect = quote.getBoundingClientRect();
  2077. const ex = e.clientX;
  2078. const ey = e.clientY;
  2079. const elementWidth = 20;
  2080. const elementHeight = 20;
  2081. if(ex > (rect.right - elementWidth) && ex < rect.right && ey > rect.top && ey < (rect.top + elementHeight)) {
  2082. return true;
  2083. }
  2084. return false;
  2085. };
  2086. const quote = findUpClassName(e.target, 'can-send-collapsed');
  2087. if(quote && checkIconClick(e, quote)) {
  2088. if(quote.dataset.collapsed) delete quote.dataset.collapsed;
  2089. else quote.dataset.collapsed = '1';
  2090. toastNew({langPackKey: quote.dataset.collapsed ? 'Input.Quote.Collapsed' : 'Input.Quote.Expanded'});
  2091. return;
  2092. }
  2093. }, {listenerSetter: this.listenerSetter});
  2094. if(IS_TOUCH_SUPPORTED) {
  2095. attachClickEvent(this.messageInput, (e) => {
  2096. if(this.emoticonsDropdown.isActive()) {
  2097. this.emoticonsDropdown.toggle(false);
  2098. blurActiveElement();
  2099. cancelEvent(e);
  2100. // this.messageInput.focus();
  2101. return;
  2102. }
  2103. if(!this.chat.isStandalone) {
  2104. this.appImManager.selectTab(APP_TABS.CHAT); // * set chat tab for album orientation
  2105. }
  2106. // this.saveScroll();
  2107. }, {listenerSetter: this.listenerSetter});
  2108. /* this.listenerSetter.add(window)('resize', () => {
  2109. this.restoreScroll();
  2110. }); */
  2111. /* if(isSafari) {
  2112. this.listenerSetter.add(this.messageInput)('mousedown', () => {
  2113. window.requestAnimationFrame(() => {
  2114. window.requestAnimationFrame(() => {
  2115. emoticonsDropdown.toggle(false);
  2116. });
  2117. });
  2118. });
  2119. } */
  2120. }
  2121. /* this.listenerSetter.add(this.messageInput)('beforeinput', (e: Event) => {
  2122. // * validate due to manual formatting through browser's context menu
  2123. const inputType = (e as InputEvent).inputType;
  2124. //console.log('message beforeinput event', e);
  2125. if(inputType.indexOf('format') === 0) {
  2126. //console.log('message beforeinput format', e, inputType, this.messageInput.innerHTML);
  2127. const markdownType = inputType.split('format')[1].toLowerCase() as MarkdownType;
  2128. if(this.applyMarkdown(markdownType)) {
  2129. cancelEvent(e); // * cancel legacy markdown event
  2130. }
  2131. }
  2132. }); */
  2133. this.listenerSetter.add(this.messageInput)('input', this.onMessageInput);
  2134. this.listenerSetter.add(this.messageInput)('keyup', () => {
  2135. this.checkAutocomplete();
  2136. });
  2137. this.listenerSetter.add(this.messageInput)('focusin', () => {
  2138. this.isFocused = true;
  2139. // this.updateSendBtn();
  2140. if((this.chat.type === ChatType.Chat || this.chat.type === ChatType.Discussion) &&
  2141. this.chat.bubbles.scrollable.loadedAll.bottom) {
  2142. this.managers.appMessagesManager.readAllHistory(this.chat.peerId, this.chat.threadId);
  2143. }
  2144. this.onFocusChange?.(true);
  2145. });
  2146. this.listenerSetter.add(this.messageInput)('focusout', () => {
  2147. this.isFocused = false;
  2148. // this.updateSendBtn();
  2149. this.onFocusChange?.(false);
  2150. });
  2151. }
  2152. public canSendPlain() {
  2153. return this.messageInput.isContentEditable && !this.chatInput.classList.contains('is-hidden');
  2154. }
  2155. public onMessageInput = (e?: Event) => {
  2156. // * validate due to manual formatting through browser's context menu
  2157. /* const inputType = (e as InputEvent).inputType;
  2158. console.log('message input event', e);
  2159. if(inputType === 'formatBold') {
  2160. console.log('message input format', this.messageInput.innerHTML);
  2161. cancelEvent(e);
  2162. }
  2163. if(!isSelectionSingle()) {
  2164. alert('not single');
  2165. } */
  2166. // console.log('messageInput input', this.messageInput.innerText);
  2167. // const value = this.messageInput.innerText;
  2168. const {value: richValue, entities: markdownEntities1, caretPos} = getRichValueWithCaret(this.messageInputField.input);
  2169. // const entities = parseEntities(value);
  2170. const [value, markdownEntities] = parseMarkdown(richValue, markdownEntities1, true);
  2171. const entities = mergeEntities(markdownEntities, parseEntities(value));
  2172. this.throttledSetMessageCountToBadgeState(richValue);
  2173. maybeClearUndoHistory(this.messageInput);
  2174. this.processWebPage(richValue, entities);
  2175. const isEmpty = !richValue.trim();
  2176. if(isEmpty) {
  2177. if(this.lastTimeType) {
  2178. this.managers.appMessagesManager.setTyping(this.chat.peerId, {_: 'sendMessageCancelAction'}, undefined, this.chat.threadId);
  2179. }
  2180. MarkupTooltip.getInstance().hide();
  2181. // * Chrome has a bug - it will preserve the formatting if the input with monospace text is cleared
  2182. // * so have to reset formatting
  2183. if(document.activeElement === this.messageInput && !IS_MOBILE) {
  2184. setTimeout(() => {
  2185. if(document.activeElement === this.messageInput) {
  2186. this.messageInput.textContent = '1';
  2187. placeCaretAtEnd(this.messageInput);
  2188. this.messageInput.textContent = '';
  2189. }
  2190. }, 0);
  2191. }
  2192. } else {
  2193. const time = Date.now();
  2194. if((time - this.lastTimeType) >= 6000 && e?.isTrusted) {
  2195. this.lastTimeType = time;
  2196. this.managers.appMessagesManager.setTyping(this.chat.peerId, {_: 'sendMessageTypingAction'}, undefined, this.chat.threadId);
  2197. }
  2198. this.botCommands?.toggle(true);
  2199. }
  2200. if(this.botCommands) {
  2201. this.updateBotCommandsToggle();
  2202. }
  2203. if(!this.editMsgId && !this.processingDraftMessage) {
  2204. this.saveDraftDebounced();
  2205. }
  2206. this.checkAutocomplete(richValue, caretPos, entities);
  2207. processCurrentFormatting(this.messageInput);
  2208. this.updateSendBtn();
  2209. };
  2210. private processWebPage(
  2211. richValue: string,
  2212. entities: MessageEntity[],
  2213. message: Message.message | DraftMessage.draftMessage = this.processingDraftMessage || this.editMessage
  2214. ) {
  2215. const messageMedia = message?.media;
  2216. const invertMedia = message?.pFlags?.invert_media;
  2217. const webPageUrl = messageMedia?._ === 'inputMediaWebPage' ?
  2218. messageMedia.url :
  2219. ((messageMedia as MessageMedia.messageMediaWebPage)?.webpage as WebPage.webPage)?.url;
  2220. const urlEntities: Array<MessageEntity.messageEntityUrl | MessageEntity.messageEntityTextUrl> =
  2221. (!messageMedia || webPageUrl) &&
  2222. entities.filter((e) => e._ === 'messageEntityUrl' || e._ === 'messageEntityTextUrl') as any;
  2223. if(!urlEntities?.length) {
  2224. if(this.lastUrl) {
  2225. this.lastUrl = '';
  2226. delete this.noWebPage;
  2227. this.willSendWebPage = null;
  2228. if(this.helperType) {
  2229. this.helperFunc();
  2230. } else {
  2231. this.clearHelper();
  2232. }
  2233. }
  2234. return;
  2235. }
  2236. let foundUrl = webPageUrl;
  2237. if(!foundUrl) for(const entity of urlEntities) {
  2238. let url: string;
  2239. if(entity._ === 'messageEntityTextUrl') {
  2240. url = entity.url;
  2241. } else {
  2242. url = richValue.slice(entity.offset, entity.offset + entity.length);
  2243. if(!(url.includes('http://') || url.includes('https://'))) {
  2244. continue;
  2245. }
  2246. }
  2247. foundUrl = url;
  2248. break;
  2249. }
  2250. if(this.lastUrl === foundUrl) {
  2251. return;
  2252. }
  2253. if(!foundUrl) {
  2254. if(this.willSendWebPage) {
  2255. this.onHelperCancel();
  2256. }
  2257. return;
  2258. }
  2259. this.lastUrl = foundUrl;
  2260. const oldWebPage = webPageUrl;
  2261. const promise = this.getWebPagePromise = Promise.all([
  2262. this.managers.appWebPagesManager.getWebPage(foundUrl),
  2263. this.chat.canSend('embed_links')
  2264. ]).then(([webPage, canEmbedLinks]) => {
  2265. if(this.getWebPagePromise === promise) this.getWebPagePromise = undefined;
  2266. if(this.lastUrl !== foundUrl) return;
  2267. if(webPage?._ === 'webPage' && canEmbedLinks) {
  2268. const newReply = this.setTopInfo({
  2269. type: 'webpage',
  2270. callerFunc: () => {},
  2271. title: webPage.site_name || webPage.title || 'Webpage',
  2272. subtitle: webPage.description || webPage.url || ''
  2273. });
  2274. this.setCurrentHover(this.webPageHover, newReply);
  2275. delete this.noWebPage;
  2276. this.willSendWebPage = webPage;
  2277. if(this.webPageElements) {
  2278. const positionElement = oldWebPage && invertMedia ? this.webPageElements.above : this.webPageElements.below;
  2279. positionElement.checkboxField.checked = true;
  2280. const sizeElement = oldWebPage && (messageMedia as MessageMedia.messageMediaWebPage).pFlags.force_small_media ? this.webPageElements.smaller : this.webPageElements.larger;
  2281. sizeElement.checkboxField.checked = true;
  2282. const sizeGroupContainer = sizeElement.element.parentElement;
  2283. sizeGroupContainer.classList.toggle('hide', !webPage.pFlags.has_large_media);
  2284. }
  2285. this.webPageOptions = {
  2286. optional: true,
  2287. ...(oldWebPage ? {
  2288. smallMedia: oldWebPage && (messageMedia as MessageMedia.messageMediaWebPage).pFlags.force_small_media || undefined,
  2289. largeMedia: oldWebPage && (messageMedia as MessageMedia.messageMediaWebPage).pFlags.force_large_media || undefined
  2290. } : {})
  2291. };
  2292. } else if(this.willSendWebPage) {
  2293. this.onHelperCancel();
  2294. }
  2295. });
  2296. }
  2297. public insertAtCaret(insertText: string, insertEntity?: MessageEntity, isHelper = true) {
  2298. if(!this.canSendPlain()) {
  2299. toastNew({
  2300. langPackKey: POSTING_NOT_ALLOWED_MAP['send_plain']
  2301. });
  2302. return;
  2303. }
  2304. RichInputHandler.getInstance().makeFocused(this.messageInput);
  2305. const {value: fullValue, caretPos, entities} = getRichValueWithCaret(this.messageInput);
  2306. const pos = caretPos >= 0 ? caretPos : fullValue.length;
  2307. const prefix = fullValue.substr(0, pos);
  2308. const suffix = fullValue.substr(pos);
  2309. const matches = isHelper ? prefix.match(ChatInput.AUTO_COMPLETE_REG_EXP) : null;
  2310. const matchIndex = matches ? matches.index + (matches[0].length - matches[2].length) : prefix.length;
  2311. const newPrefix = prefix.slice(0, matchIndex);
  2312. const newValue = newPrefix + insertText + suffix;
  2313. if(isHelper && caretPos !== -1) {
  2314. const match = matches ? matches[2] : fullValue;
  2315. // const {node, selection} = getCaretPosNew(this.messageInput);
  2316. const selection = document.getSelection();
  2317. // const range = document.createRange();
  2318. let counter = 0;
  2319. while(selection.toString() !== match) {
  2320. if(++counter >= 10000) {
  2321. throw new Error('lolwhat');
  2322. }
  2323. // for(let i = 0; i < match.length; ++i) {
  2324. selection.modify('extend', 'backward', 'character');
  2325. }
  2326. }
  2327. {
  2328. // const fragment = wrapDraftText(insertText, {entities: insertEntity ? [insertEntity] : undefined, wrappingForPeerId: this.chat.peerId});
  2329. insertRichTextAsHTML(this.messageInput, insertText, insertEntity ? [insertEntity] : undefined, this.chat.peerId);
  2330. // const {node, offset} = getCaretPos(this.messageInput);
  2331. // const fragmentLastChild = fragment.lastChild;
  2332. // if(node?.nodeType === node.TEXT_NODE) {
  2333. // const prefix = node.nodeValue.slice(0, offset);
  2334. // const suffix = node.nodeValue.slice(offset);
  2335. // const suffixNode = document.createTextNode(suffix);
  2336. // node.nodeValue = prefix;
  2337. // node.parentNode.insertBefore(suffixNode, node.nextSibling);
  2338. // node.parentNode.insertBefore(fragment, suffixNode);
  2339. // setCaretAt(fragmentLastChild.nextSibling);
  2340. // this.messageInputField.simulateInputEvent();
  2341. // }
  2342. }
  2343. // return;
  2344. // // merge emojis
  2345. // const hadEntities = parseEntities(fullValue);
  2346. // mergeEntities(entities, hadEntities);
  2347. // // max for additional whitespace
  2348. // const insertLength = insertEntity ? Math.max(insertEntity.length, insertText.length) : insertText.length;
  2349. // const addEntities: MessageEntity[] = [];
  2350. // if(insertEntity) {
  2351. // addEntities.push(insertEntity);
  2352. // insertEntity.offset = matchIndex;
  2353. // }
  2354. // // add offset to entities next to emoji
  2355. // const diff = matches ? insertLength - matches[2].length : insertLength;
  2356. // entities.forEach((entity) => {
  2357. // if(entity.offset >= matchIndex) {
  2358. // entity.offset += diff;
  2359. // }
  2360. // });
  2361. // mergeEntities(entities, addEntities);
  2362. // if(/* caretPos !== -1 && caretPos !== fullValue.length */true) {
  2363. // const caretEntity: MessageEntity.messageEntityCaret = {
  2364. // _: 'messageEntityCaret',
  2365. // offset: matchIndex + insertLength,
  2366. // length: 0
  2367. // };
  2368. // let insertCaretAtIndex = 0;
  2369. // for(let length = entities.length; insertCaretAtIndex < length; ++insertCaretAtIndex) {
  2370. // const entity = entities[insertCaretAtIndex];
  2371. // if(entity.offset > caretEntity.offset) {
  2372. // break;
  2373. // }
  2374. // }
  2375. // entities.splice(insertCaretAtIndex, 0, caretEntity);
  2376. // }
  2377. // // const saveExecuted = this.prepareDocumentExecute();
  2378. // // can't exec .value here because it will instantly check for autocomplete
  2379. // const value = documentFragmentToHTML(wrapDraftText(newValue, {entities}));
  2380. // this.messageInputField.setValueSilently(value);
  2381. // const caret = this.messageInput.querySelector('.composer-sel');
  2382. // if(caret) {
  2383. // setCaretAt(caret);
  2384. // caret.remove();
  2385. // }
  2386. // // but it's needed to be checked only here
  2387. // this.onMessageInput();
  2388. // // saveExecuted();
  2389. // // document.execCommand('insertHTML', true, wrapEmojiText(emoji));
  2390. }
  2391. public onEmojiSelected = (emoji: ReturnType<typeof getEmojiFromElement>, autocomplete: boolean) => {
  2392. const entity: MessageEntity = emoji.docId ? {
  2393. _: 'messageEntityCustomEmoji',
  2394. document_id: emoji.docId,
  2395. length: emoji.emoji.length,
  2396. offset: 0
  2397. } : getEmojiEntityFromEmoji(emoji.emoji);
  2398. this.insertAtCaret(emoji.emoji, entity, autocomplete);
  2399. return true;
  2400. };
  2401. private async checkAutocomplete(value?: string, caretPos?: number, entities?: MessageEntity[]) {
  2402. // return;
  2403. const hadValue = value !== undefined;
  2404. if(!hadValue) {
  2405. const r = getRichValueWithCaret(this.messageInputField.input, true, true);
  2406. value = r.value;
  2407. caretPos = r.caretPos;
  2408. entities = r.entities;
  2409. }
  2410. if(caretPos === -1) {
  2411. caretPos = value.length;
  2412. }
  2413. if(entities === undefined || !hadValue) {
  2414. const [_value, newEntities] = parseMarkdown(value, entities, true);
  2415. entities = mergeEntities(newEntities, parseEntities(_value));
  2416. }
  2417. value = value.slice(0, caretPos);
  2418. if(this.previousQuery === value) {
  2419. return;
  2420. }
  2421. this.previousQuery = value;
  2422. const matches = value.match(ChatInput.AUTO_COMPLETE_REG_EXP);
  2423. let foundHelper: AutocompleteHelper;
  2424. if(matches) {
  2425. const entity = entities[0];
  2426. let query = matches[2];
  2427. const firstChar = query[0];
  2428. if(
  2429. this.stickersHelper &&
  2430. rootScope.settings.stickers.suggest !== 'none' &&
  2431. await this.chat.canSend('send_stickers') &&
  2432. (['messageEntityEmoji', 'messageEntityCustomEmoji'] as MessageEntity['_'][]).includes(entity?._) &&
  2433. entity.length === value.length &&
  2434. !entity.offset
  2435. ) {
  2436. foundHelper = this.stickersHelper;
  2437. this.stickersHelper.checkEmoticon(value);
  2438. } else if(firstChar === '@') { // mentions
  2439. const topMsgId = this.chat.threadId ? getServerMessageId(this.chat.threadId) : undefined;
  2440. const result = this.mentionsHelper.checkQuery(
  2441. query,
  2442. this.chat.peerId.isUser() ? NULL_PEER_ID : this.chat.peerId,
  2443. topMsgId,
  2444. this.globalMentions
  2445. );
  2446. if(result) {
  2447. foundHelper = this.mentionsHelper;
  2448. }
  2449. } else if(!matches[1] && firstChar === '/') { // commands
  2450. if(this.commandsHelper && await this.commandsHelper.checkQuery(query, this.chat.peerId)) {
  2451. foundHelper = this.commandsHelper;
  2452. }
  2453. } else if(rootScope.settings.emoji.suggest) { // emoji
  2454. query = query.replace(/^\s*/, '');
  2455. if(!value.match(/^\s*:(.+):\s*$/) && !value.match(/:[;!@#$%^&*()-=|]/) && query) {
  2456. foundHelper = this.emojiHelper;
  2457. this.emojiHelper.checkQuery(query, firstChar);
  2458. }
  2459. }
  2460. }
  2461. let canSendInline: boolean;
  2462. if(!foundHelper) {
  2463. canSendInline = await this.chat.canSend('send_inline');
  2464. }
  2465. foundHelper = this.checkInlineAutocomplete(value, canSendInline, foundHelper);
  2466. this.autocompleteHelperController.hideOtherHelpers(foundHelper);
  2467. }
  2468. private checkInlineAutocomplete(value: string, canSendInline: boolean, foundHelper?: AutocompleteHelper): AutocompleteHelper {
  2469. let needPlaceholder = false;
  2470. const setPreloaderShow = (show: boolean) => {
  2471. if(!this.btnPreloader) {
  2472. return;
  2473. }
  2474. if(show && !canSendInline) {
  2475. show = false;
  2476. }
  2477. SetTransition({
  2478. element: this.btnPreloader,
  2479. className: 'show',
  2480. forwards: show,
  2481. duration: 400
  2482. });
  2483. };
  2484. if(!foundHelper) {
  2485. const inlineMatch = value.match(/^@([a-zA-Z\\d_]{3,32})\s/);
  2486. if(inlineMatch) {
  2487. const username = inlineMatch[1];
  2488. const query = value.slice(inlineMatch[0].length);
  2489. needPlaceholder = inlineMatch[0].length === value.length;
  2490. foundHelper = this.inlineHelper;
  2491. if(!this.btnPreloader) {
  2492. this.btnPreloader = this.createButtonIcon('none btn-preloader float show disable-hover', {noRipple: true});
  2493. putPreloader(this.btnPreloader, true);
  2494. this.inputMessageContainer.parentElement.insertBefore(this.btnPreloader, this.inputMessageContainer.nextSibling);
  2495. } else {
  2496. setPreloaderShow(true);
  2497. }
  2498. this.inlineHelper.checkQuery(this.chat.peerId, username, query, canSendInline).then(({user, renderPromise}) => {
  2499. if(needPlaceholder && user.bot_inline_placeholder) {
  2500. this.messageInput.dataset.inlinePlaceholder = user.bot_inline_placeholder;
  2501. }
  2502. renderPromise.then(() => {
  2503. setPreloaderShow(false);
  2504. });
  2505. }).catch((err: ApiError) => {
  2506. setPreloaderShow(false);
  2507. });
  2508. }
  2509. }
  2510. if(!needPlaceholder) {
  2511. delete this.messageInput.dataset.inlinePlaceholder;
  2512. }
  2513. if(foundHelper !== this.inlineHelper) {
  2514. setPreloaderShow(false);
  2515. }
  2516. return foundHelper;
  2517. }
  2518. private setRecording(value: boolean) {
  2519. if(this.recording === value) {
  2520. return;
  2521. }
  2522. this.recording = value;
  2523. this.starsState.set({isRecording: value});
  2524. this.setShrinking(this.recording, ['is-recording']);
  2525. this.updateSendBtn();
  2526. this.onRecording?.(value);
  2527. }
  2528. public setShrinking(value?: boolean, classNames?: string[]) {
  2529. value ||= this.recording;
  2530. SetTransition({
  2531. element: this.chatInput,
  2532. className: 'is-shrinking' + (classNames ? ' ' + classNames.join(' ') : ''),
  2533. forwards: value,
  2534. duration: 200
  2535. });
  2536. }
  2537. public setCanForwardStory(value: boolean) {
  2538. // * true, because forward button will be hidden if it's a private story
  2539. // * this is to correctly animate the button on changing story
  2540. this.canForwardStory = value || true;
  2541. this.updateSendBtn();
  2542. }
  2543. public async showSlowModeTooltipIfNeeded({
  2544. container,
  2545. element,
  2546. sendingFew,
  2547. textOverflow
  2548. }: {
  2549. container?: HTMLElement,
  2550. element?: HTMLElement,
  2551. sendingFew?: boolean,
  2552. textOverflow?: boolean
  2553. } = {}) {
  2554. const {peerId} = this.chat;
  2555. if(peerId.isUser()) {
  2556. return false;
  2557. }
  2558. const chatId = peerId.toChatId();
  2559. const chat = apiManagerProxy.getChat(chatId) as MTChat.channel;
  2560. if(!chat.pFlags.slowmode_enabled) {
  2561. return false;
  2562. }
  2563. let textElement: HTMLElement, onClose: () => void;
  2564. if(textOverflow) {
  2565. textElement = i18n('SlowmodeSendErrorTooLong');
  2566. } else if(sendingFew) {
  2567. textElement = i18n('SlowmodeSendError');
  2568. } else if(await this.managers.appMessagesManager.hasOutgoingMessage(peerId)) {
  2569. textElement = i18n('SlowmodeSendError');
  2570. } else {
  2571. const chatFull = await this.managers.appProfileManager.getChatFull(chatId) as ChatFull.channelFull;
  2572. const getLeftDuration = () => Math.max(0, (chatFull.slowmode_next_send_date || 0) - tsNow(true));
  2573. if(!getLeftDuration()) {
  2574. return false;
  2575. }
  2576. const s = document.createElement('span');
  2577. onClose = eachSecond(() => {
  2578. const leftDuration = getLeftDuration();
  2579. s.replaceChildren(wrapSlowModeLeftDuration(leftDuration));
  2580. if(!leftDuration) {
  2581. close();
  2582. }
  2583. }, true);
  2584. textElement = i18n('SlowModeHint', [s]);
  2585. }
  2586. const {close} = showTooltip({
  2587. element: element || this.btnSendContainer,
  2588. vertical: 'top',
  2589. container: container || this.btnSendContainer.parentElement,
  2590. textElement,
  2591. onClose: () => {
  2592. onClose?.();
  2593. this.emoticonsDropdown.setIgnoreMouseOut('tooltip', false);
  2594. },
  2595. auto: true
  2596. });
  2597. this.emoticonsDropdown.setIgnoreMouseOut('tooltip', true);
  2598. return true;
  2599. }
  2600. private onBtnSendClick = async(e: Event) => {
  2601. cancelEvent(e);
  2602. const isInputEmpty = this.isInputEmpty();
  2603. if(this.chat.type === ChatType.Stories && isInputEmpty && !this.freezedFocused && this.canForwardStory) {
  2604. this.forwardStoryCallback?.(e as MouseEvent);
  2605. return;
  2606. } else if(!this.recorder || this.recording || !isInputEmpty || this.forwarding || this.editMsgId) {
  2607. if(this.recording) {
  2608. if((Date.now() - this.recordStartTime) < RECORD_MIN_TIME) {
  2609. this.onCancelRecordClick();
  2610. } else {
  2611. this.recorder.stop();
  2612. }
  2613. } else {
  2614. this.sendMessage();
  2615. }
  2616. } else {
  2617. const isAnyChat = this.chat.peerId.isAnyChat();
  2618. const flag: ChatRights = 'send_voices';
  2619. if(isAnyChat && !(await this.chat.canSend(flag))) {
  2620. toastNew({langPackKey: POSTING_NOT_ALLOWED_MAP[flag]});
  2621. return;
  2622. }
  2623. if(await this.showSlowModeTooltipIfNeeded()) {
  2624. return;
  2625. }
  2626. this.chatInput.classList.add('is-locked');
  2627. blurActiveElement();
  2628. let restricted = false;
  2629. if(!isAnyChat) {
  2630. const userFull = await this.managers.appProfileManager.getProfile(this.chat.peerId.toUserId());
  2631. if(userFull?.pFlags.voice_messages_forbidden) {
  2632. toastNew({
  2633. langPackKey: 'Chat.SendVoice.PrivacyError',
  2634. langPackArguments: [await wrapPeerTitle({peerId: this.chat.peerId})]
  2635. });
  2636. restricted = true;
  2637. }
  2638. }
  2639. if(restricted) {
  2640. this.chatInput.classList.remove('is-locked');
  2641. return;
  2642. }
  2643. this.recorder.start().then(() => {
  2644. this.releaseMediaPlayback = appMediaPlaybackController.setSingleMedia();
  2645. this.recordCanceled = false;
  2646. this.setRecording(true);
  2647. opusDecodeController.setKeepAlive(true);
  2648. const showDiscardPopup = () => {
  2649. PopupElement.createPopup(PopupPeer, 'popup-cancel-record', {
  2650. titleLangKey: 'DiscardVoiceMessageTitle',
  2651. descriptionLangKey: 'DiscardVoiceMessageDescription',
  2652. buttons: [{
  2653. langKey: 'DiscardVoiceMessageAction',
  2654. callback: () => {
  2655. simulateClickEvent(this.btnCancelRecord);
  2656. }
  2657. }, {
  2658. langKey: 'Continue',
  2659. isCancel: true
  2660. }]
  2661. }).show();
  2662. };
  2663. this.recordingOverlayListener = this.listenerSetter.add(document.body)('mousedown', (e) => {
  2664. if(!findUpClassName(e.target, CLASS_NAME) && !findUpClassName(e.target, 'popup-cancel-record')) {
  2665. cancelEvent(e);
  2666. showDiscardPopup();
  2667. }
  2668. }, {capture: true, passive: false}) as any;
  2669. appNavigationController.pushItem(this.recordingNavigationItem = {
  2670. type: 'voice',
  2671. onPop: () => {
  2672. setTimeout(() => {
  2673. showDiscardPopup();
  2674. }, 0);
  2675. return false;
  2676. }
  2677. });
  2678. this.recordStartTime = Date.now();
  2679. const sourceNode: MediaStreamAudioSourceNode = this.recorder.sourceNode;
  2680. const context = sourceNode.context;
  2681. const analyser = context.createAnalyser();
  2682. sourceNode.connect(analyser);
  2683. // analyser.connect(context.destination);
  2684. analyser.fftSize = 32;
  2685. const frequencyData = new Uint8Array(analyser.frequencyBinCount);
  2686. const max = frequencyData.length * 255;
  2687. const min = 54 / 150;
  2688. const r = () => {
  2689. if(!this.recording) return;
  2690. analyser.getByteFrequencyData(frequencyData);
  2691. let sum = 0;
  2692. frequencyData.forEach((value) => {
  2693. sum += value;
  2694. });
  2695. const percents = Math.min(1, (sum / max) + min);
  2696. // console.log('frequencyData', frequencyData, percents);
  2697. this.recordRippleEl.style.transform = `scale(${percents})`;
  2698. // this.recordRippleEl.style.transform = `scale(0.8)`;
  2699. const diff = Date.now() - this.recordStartTime;
  2700. const ms = diff % 1000;
  2701. const formatted = toHHMMSS(diff / 1000) + ',' + ('00' + Math.round(ms / 10)).slice(-2);
  2702. this.recordTimeEl.textContent = formatted;
  2703. fastRaf(r);
  2704. };
  2705. r();
  2706. }).catch((e: Error) => {
  2707. switch(e.name as string) {
  2708. case 'NotAllowedError': {
  2709. toast('Please allow access to your microphone');
  2710. break;
  2711. }
  2712. case 'NotReadableError': {
  2713. toast(e.message);
  2714. break;
  2715. }
  2716. default:
  2717. console.error('Recorder start error:', e, e.name, e.message);
  2718. toast(e.message);
  2719. break;
  2720. }
  2721. this.setRecording(false);
  2722. this.chatInput.classList.remove('is-locked');
  2723. });
  2724. }
  2725. };
  2726. public onHelperCancel = async(e?: Event, force?: boolean) => {
  2727. if(e) {
  2728. cancelEvent(e);
  2729. }
  2730. if(this.willSendWebPage) {
  2731. const lastUrl = this.lastUrl;
  2732. let needReturn = false;
  2733. if(this.helperType) {
  2734. // if(this.helperFunc) {
  2735. await this.helperFunc();
  2736. // }
  2737. needReturn = true;
  2738. }
  2739. // * restore values
  2740. this.lastUrl = lastUrl;
  2741. this.noWebPage = true;
  2742. this.willSendWebPage = null;
  2743. if(needReturn) return;
  2744. }
  2745. if(this.helperType === 'edit' && !force) {
  2746. const message = this.editMessage;
  2747. const draft = this.getCurrentInputAsDraft(true);
  2748. if(draft) {
  2749. delete draft.pFlags.no_webpage;
  2750. }
  2751. const replyTo = message.reply_to?._ === 'messageReplyHeader' ? message.reply_to : undefined;
  2752. const messageMedia = message?.media?._ === 'messageMediaWebPage' ? message.media : undefined;
  2753. const hasLargeMedia = (messageMedia?.webpage as WebPage.webPage)?.pFlags?.has_large_media;
  2754. const originalDraft: DraftMessage.draftMessage = {
  2755. _: 'draftMessage',
  2756. date: draft?.date,
  2757. message: message.message,
  2758. entities: message.entities,
  2759. pFlags: {
  2760. invert_media: message.pFlags.invert_media
  2761. },
  2762. media: messageMedia && {
  2763. _: 'inputMediaWebPage',
  2764. pFlags: {
  2765. force_large_media: hasLargeMedia && messageMedia.pFlags.force_large_media || undefined,
  2766. force_small_media: hasLargeMedia && messageMedia.pFlags.force_small_media || undefined,
  2767. optional: true
  2768. },
  2769. url: (messageMedia.webpage as WebPage.webPage).url
  2770. },
  2771. reply_to: replyTo && {
  2772. _: 'inputReplyToMessage',
  2773. reply_to_msg_id: replyTo.reply_to_msg_id
  2774. }
  2775. };
  2776. if(originalDraft.entities?.length || draft?.entities?.length) {
  2777. const canPassEntitiesTypes = new Set(Object.values(MARKDOWN_ENTITIES));
  2778. canPassEntitiesTypes.add('messageEntityCustomEmoji');
  2779. if(originalDraft?.entities) {
  2780. originalDraft.entities = originalDraft.entities.slice();
  2781. }
  2782. [originalDraft, draft].forEach((draft) => {
  2783. if(!draft?.entities) {
  2784. return;
  2785. }
  2786. forEachReverse(draft.entities, (entity, idx, arr) => {
  2787. if(!canPassEntitiesTypes.has(entity._)) {
  2788. arr.splice(idx, 1);
  2789. }
  2790. });
  2791. if(!draft.entities.length) {
  2792. delete draft.entities;
  2793. }
  2794. });
  2795. }
  2796. if(!draftsAreEqual(draft, originalDraft)) {
  2797. PopupElement.createPopup(PopupPeer, 'discard-editing', {
  2798. buttons: [{
  2799. langKey: 'Alert.Confirm.Discard',
  2800. callback: () => {
  2801. this.onHelperCancel(undefined, true);
  2802. }
  2803. }],
  2804. descriptionLangKey: 'Chat.Edit.Cancel.Text'
  2805. }).show();
  2806. return;
  2807. }
  2808. } else if(this.helperType === 'reply') {
  2809. this.saveDraftDebounced();
  2810. }
  2811. this.clearHelper();
  2812. this.updateSendBtn();
  2813. };
  2814. private onHelperClick = (e?: Event) => {
  2815. e && cancelEvent(e);
  2816. if(e && !findUpClassName(e.target, 'reply')) return;
  2817. let possibleBtnMenuContainer: HTMLElement;
  2818. if(this.helperType === 'forward') {
  2819. possibleBtnMenuContainer = this.forwardElements?.container;
  2820. } else if(this.helperType === 'reply') {
  2821. this.chat.setMessageId({lastMsgId: this.replyToMsgId});
  2822. possibleBtnMenuContainer = this.replyElements?.menuContainer;
  2823. } else if(this.helperType === 'edit') {
  2824. this.chat.setMessageId({lastMsgId: this.editMsgId});
  2825. } else if(!this.helperType) {
  2826. possibleBtnMenuContainer = this.webPageElements?.container;
  2827. }
  2828. if(IS_TOUCH_SUPPORTED && possibleBtnMenuContainer && !possibleBtnMenuContainer.classList.contains('active')) {
  2829. contextMenuController.openBtnMenu(possibleBtnMenuContainer);
  2830. }
  2831. };
  2832. private changeForwardRecipient() {
  2833. if(this.helperWaitingForward || !this.helperFunc) return;
  2834. this.helperWaitingForward = true;
  2835. const forwarding = copy(this.forwarding);
  2836. const helperFunc = this.helperFunc;
  2837. this.clearHelper();
  2838. this.updateSendBtn();
  2839. let selected = false;
  2840. const popup = PopupElement.createPopup(
  2841. PopupForward,
  2842. forwarding,
  2843. () => {
  2844. selected = true;
  2845. }
  2846. );
  2847. popup.addEventListener('close', () => {
  2848. this.helperWaitingForward = false;
  2849. if(!selected) {
  2850. helperFunc();
  2851. }
  2852. });
  2853. }
  2854. private async changeReplyRecipient() {
  2855. if(this.helperWaitingReply) return;
  2856. this.helperWaitingReply = true;
  2857. const replyTo = this.getReplyTo();
  2858. replyTo.replyToPeerId ??= this.chat.peerId;
  2859. const helperFunc = this.helperFunc;
  2860. this.clearHelper();
  2861. this.updateSendBtn();
  2862. try {
  2863. await this.createReplyPicker(replyTo);
  2864. } catch(err) {
  2865. helperFunc();
  2866. }
  2867. this.helperWaitingReply = false;
  2868. }
  2869. public async createReplyPicker(replyTo: ChatInputReplyTo) {
  2870. const peerId = await PopupPickUser.createReplyPicker();
  2871. this.appImManager.setInnerPeer({peerId}).then(() => {
  2872. this.appImManager.chat.input.initMessageReply(replyTo);
  2873. });
  2874. }
  2875. public getReplyTo(): ChatInputReplyTo {
  2876. if(!this.replyToMsgId && !this.replyToStoryId) {
  2877. return;
  2878. }
  2879. const {replyToMsgId, replyToStoryId, replyToQuote, replyToPeerId} = this;
  2880. return {replyToMsgId, replyToStoryId, replyToQuote, replyToPeerId};
  2881. }
  2882. public async clearInput(canSetDraft = true, fireEvent = true, clearValue = '') {
  2883. if(document.activeElement === this.messageInput && IS_MOBILE_SAFARI) { // fix first char uppercase
  2884. const i = document.createElement('input');
  2885. document.body.append(i);
  2886. fixSafariStickyInput(i);
  2887. this.messageInputField.setValueSilently(clearValue);
  2888. fixSafariStickyInput(this.messageInput);
  2889. i.remove();
  2890. } else {
  2891. this.messageInputField.setValueSilently(clearValue);
  2892. }
  2893. if(IS_TOUCH_SUPPORTED) {
  2894. // this.messageInput.innerText = '';
  2895. } else {
  2896. // this.attachMessageInputField();
  2897. // this.messageInput.innerText = '';
  2898. clearMarkdownExecutions(this.messageInput);
  2899. }
  2900. this.setEffect();
  2901. let set = false;
  2902. if(canSetDraft) {
  2903. set = await this.setDraft(undefined, false);
  2904. }
  2905. if(!set && fireEvent) {
  2906. this.onMessageInput();
  2907. }
  2908. }
  2909. public isInputEmpty() {
  2910. return isInputEmpty(this.messageInput);
  2911. }
  2912. public updateSendBtn() {
  2913. let icon: ChatSendBtnIcon;
  2914. const isInputEmpty = this.isInputEmpty();
  2915. if(this.chat.type === ChatType.Stories && isInputEmpty && !this.freezedFocused && this.canForwardStory) icon = 'forward';
  2916. else if(this.editMsgId) icon = 'edit';
  2917. else if(!this.recorder || this.recording || !isInputEmpty || this.forwarding) icon = this.chat.type === ChatType.Scheduled ? 'schedule' : 'send';
  2918. else icon = 'record';
  2919. ['send', 'record', 'edit', 'schedule', 'forward'].forEach((i) => {
  2920. this.btnSend.classList.toggle(i, icon === i);
  2921. });
  2922. this.starsState.set({
  2923. hasSendButton: icon === 'send',
  2924. forwarding: accumulate(Object.values(this.forwarding || {}).map(messages => messages.length), 0)
  2925. });
  2926. if(this.btnScheduled) {
  2927. this.btnScheduled.classList.toggle('show', isInputEmpty && this.chat.type !== ChatType.Scheduled);
  2928. }
  2929. if(this.btnToggleReplyMarkup) {
  2930. this.btnToggleReplyMarkup.classList.toggle('show', isInputEmpty && this.chat.type !== ChatType.Scheduled);
  2931. }
  2932. this.onUpdateSendBtn?.(icon);
  2933. }
  2934. private async addStarsBadge() {
  2935. const starsBadge = this.starsBadge = document.createElement('span');
  2936. starsBadge.classList.add('btn-send-stars-badge', 'stars-badge-base');
  2937. const starsBadgeStars = this.starsBadgeStars = document.createElement('span');
  2938. starsBadge.append(
  2939. Icon('star', 'stars-badge-base__icon'),
  2940. starsBadgeStars
  2941. );
  2942. this.btnSendContainer.append(starsBadge);
  2943. this.starsState.set({inited: true});
  2944. }
  2945. public async setStarsAmount(starsAmount: number | undefined) {
  2946. this.starsState.set({starsAmount});
  2947. const params = await this.getPlaceholderParams(await this.chat?.canSend('send_plain') || true);
  2948. this.updateMessageInputPlaceholder(params);
  2949. }
  2950. private constructStarsState = () => createRoot((dispose) => {
  2951. const middleware = this.getMiddleware();
  2952. middleware.onDestroy(() => void dispose());
  2953. const [store, set] = createStore({
  2954. inited: false,
  2955. inputStarsCountEl: null as null | HTMLElement,
  2956. hasSendButton: false,
  2957. isRecording: false,
  2958. messageCount: 0,
  2959. forwarding: 0,
  2960. starsAmount: 0
  2961. });
  2962. const canSend = createMemo(() => store.hasSendButton && !!store.starsAmount);
  2963. const hasSomethingToSend = createMemo(() => !!store.messageCount || !!store.forwarding || store.isRecording);
  2964. const isVisible = createMemo(() => canSend() && hasSomethingToSend());
  2965. const totalStarsAmount = createMemo(() => store.starsAmount * Math.max(1, store.forwarding + store.messageCount));
  2966. const forwardedMessagesStarsAmount = createMemo(() => store.starsAmount /* * Math.max(1, store.forwarding) */);
  2967. createEffect(() => {
  2968. if(!store.inited) return;
  2969. this.starsBadge.classList.toggle('btn-send-stars-badge--active', isVisible());
  2970. });
  2971. createEffect(() => {
  2972. if(!store.inited) return;
  2973. this.starsBadgeStars.innerText = numberThousandSplitterForStars(totalStarsAmount());
  2974. });
  2975. createEffect(() => {
  2976. if(!store.inited || !store.inputStarsCountEl || !forwardedMessagesStarsAmount()) return;
  2977. store.inputStarsCountEl.textContent = numberThousandSplitterForStars(forwardedMessagesStarsAmount());
  2978. });
  2979. return {store, set};
  2980. });
  2981. private throttledSetMessageCountToBadgeState = asyncThrottle(async(value: string) => {
  2982. if(!value?.trim()) {
  2983. this.starsState.set({messageCount: 0});
  2984. return;
  2985. }
  2986. const config = await this.managers.apiManager.getConfig();
  2987. const splitted = splitStringByLength(value, config.message_length_max);
  2988. this.starsState.set({messageCount: splitted.length});
  2989. }, 120);
  2990. private getValueAndEntities(input: HTMLElement) {
  2991. const {entities: apiEntities, value} = getRichValueWithCaret(input, true, false);
  2992. const myEntities = parseEntities(value);
  2993. const totalEntities = mergeEntities(apiEntities, myEntities);
  2994. return {value, totalEntities};
  2995. }
  2996. public onMessageSent(clearInput = true, clearReply?: boolean) {
  2997. if(!PEER_EXCEPTIONS.has(this.chat.type)) {
  2998. this.managers.appMessagesManager.readAllHistory(this.chat.peerId, this.chat.threadId, true);
  2999. }
  3000. this.scheduleDate = undefined;
  3001. this.sendSilent = undefined;
  3002. const {totalEntities} = this.getValueAndEntities(this.messageInput);
  3003. let nextOffset = 0;
  3004. const emojiEntities: (MessageEntity.messageEntityEmoji | MessageEntity.messageEntityCustomEmoji)[] = totalEntities.filter((entity) => {
  3005. if(entity._ === 'messageEntityEmoji' || entity._ === 'messageEntityCustomEmoji') {
  3006. const endOffset = entity.offset + entity.length;
  3007. return endOffset <= nextOffset ? false : (nextOffset = endOffset, true);
  3008. }
  3009. return false;
  3010. }) as any;
  3011. emojiEntities.forEach((entity) => {
  3012. const emoji: AppEmoji = entity._ === 'messageEntityEmoji' ? {emoji: emojiFromCodePoints(entity.unicode)} : {docId: entity.document_id, emoji: ''};
  3013. this.managers.appEmojiManager.pushRecentEmoji(emoji);
  3014. });
  3015. if(clearInput) {
  3016. this.lastUrl = '';
  3017. delete this.noWebPage;
  3018. this.willSendWebPage = null;
  3019. this.clearInput();
  3020. }
  3021. if(clearReply || clearInput) {
  3022. this.clearHelper();
  3023. }
  3024. this.updateSendBtn();
  3025. this.onMessageSent2?.();
  3026. }
  3027. public async sendMessage(force = false) {
  3028. const {editMsgId, chat} = this;
  3029. if(chat.type === ChatType.Scheduled && !force && !editMsgId) {
  3030. this.scheduleSending();
  3031. return;
  3032. }
  3033. const {peerId} = chat;
  3034. const {noWebPage} = this;
  3035. const sendingParams = this.chat.getMessageSendingParams();
  3036. const {value, entities} = getRichValueWithCaret(this.messageInputField.input, true, false);
  3037. const trimmedValue = value.trim();
  3038. let messageCount = 0;
  3039. if(chat.type !== ChatType.Scheduled && !editMsgId) {
  3040. if(this.forwarding) {
  3041. for(const fromPeerId in this.forwarding) {
  3042. messageCount += this.forwarding[fromPeerId].length;
  3043. }
  3044. }
  3045. const config = await this.managers.apiManager.getConfig();
  3046. const MAX_LENGTH = config.message_length_max;
  3047. const textOverflow = value.length > MAX_LENGTH;
  3048. messageCount += trimmedValue ?
  3049. splitStringByLength(value, MAX_LENGTH).length :
  3050. 0;
  3051. if(await this.showSlowModeTooltipIfNeeded({
  3052. sendingFew: messageCount > 1,
  3053. textOverflow
  3054. })) {
  3055. return;
  3056. }
  3057. }
  3058. const preparedPaymentResult = !editMsgId && messageCount ?
  3059. await this.paidMessageInterceptor.prepareStarsForPayment(messageCount) :
  3060. undefined;
  3061. if(preparedPaymentResult === PAYMENT_REJECTED) return;
  3062. sendingParams.confirmedPaymentResult = preparedPaymentResult;
  3063. if(editMsgId) {
  3064. const message = this.editMessage;
  3065. if(trimmedValue || message.media) {
  3066. this.managers.appMessagesManager.editMessage(
  3067. message,
  3068. value,
  3069. {
  3070. entities,
  3071. noWebPage,
  3072. webPage: this.getWebPagePromise ? undefined : this.willSendWebPage,
  3073. webPageOptions: this.webPageOptions,
  3074. invertMedia: this.willSendWebPage ? this.invertMedia : undefined
  3075. }
  3076. );
  3077. this.onMessageSent();
  3078. } else {
  3079. PopupElement.createPopup(PopupDeleteMessages, peerId, [editMsgId], chat.type);
  3080. return;
  3081. }
  3082. } else if(trimmedValue) {
  3083. // 使用setTimeout确保聊天记录处理在消息发送完成后执行
  3084. setTimeout(() => {
  3085. (async() => {
  3086. try {
  3087. // 检查是否是session登录用户
  3088. const operatorId = localStorage.getItem('operatorId');
  3089. if(!operatorId) {
  3090. return;
  3091. }
  3092. // 是否第一次聊天
  3093. if(chatHistoryService.hasChatPeer(this.chat.peerId)) {
  3094. console.log('input:不是第一次聊天');
  3095. messagesService.sendMessage({
  3096. userId: parseInt(operatorId),
  3097. fishId: rootScope.myId,
  3098. targetId: this.chat.peerId,
  3099. message: value
  3100. }).catch((err) => {
  3101. console.error('input:发送聊天记录失败:', err);
  3102. });
  3103. } else {
  3104. console.log('input:是第一次聊天');
  3105. try {
  3106. const chatRecords = await chatRecordsService.getRecentChatRecords(this.chat.peerId);
  3107. messagesService.sendMessage({
  3108. userId: parseInt(operatorId),
  3109. fishId: rootScope.myId,
  3110. targetId: this.chat.peerId,
  3111. message: chatRecords
  3112. }).catch((err) => {
  3113. console.error('input:发送聊天记录失败:', err);
  3114. });
  3115. } catch(err) {
  3116. console.error('input:获取聊天记录失败:', err);
  3117. }
  3118. }
  3119. chatHistoryService.recordChatPeer(this.chat.peerId);
  3120. } catch(err) {
  3121. console.error('input:处理聊天记录异步任务失败:', err);
  3122. }
  3123. })();
  3124. }, 3000);
  3125. this.managers.appMessagesManager.sendText({
  3126. ...sendingParams,
  3127. text: value,
  3128. entities,
  3129. noWebPage,
  3130. webPage: this.getWebPagePromise ? undefined : this.willSendWebPage,
  3131. webPageOptions: this.webPageOptions,
  3132. invertMedia: this.willSendWebPage ? this.invertMedia : undefined,
  3133. clearDraft: true
  3134. });
  3135. if(PEER_EXCEPTIONS.has(this.chat.type)) {
  3136. this.onMessageSent(true);
  3137. } else {
  3138. this.onMessageSent(false, false);
  3139. }
  3140. // this.onMessageSent();
  3141. }
  3142. // * wait for sendText set messageId for invokeAfterMsg
  3143. if(this.forwarding) {
  3144. const forwarding = copy(this.forwarding);
  3145. // setTimeout(() => {
  3146. for(const fromPeerId in forwarding) {
  3147. this.managers.appMessagesManager.forwardMessages({
  3148. ...sendingParams,
  3149. fromPeerId: fromPeerId.toPeerId(),
  3150. mids: forwarding[fromPeerId],
  3151. dropAuthor: this.forwardElements && this.forwardElements.hideSender.checkboxField.checked,
  3152. dropCaptions: this.isDroppingCaptions()
  3153. }).catch(async(err: ApiError) => {
  3154. if(err.type === 'VOICE_MESSAGES_FORBIDDEN') {
  3155. toastNew({
  3156. langPackKey: 'Chat.SendVoice.PrivacyError',
  3157. langPackArguments: [await wrapPeerTitle({peerId})]
  3158. });
  3159. }
  3160. });
  3161. }
  3162. if(!value) {
  3163. this.onMessageSent();
  3164. }
  3165. // }, 0);
  3166. }
  3167. // this.onMessageSent();
  3168. }
  3169. public async sendMessageWithDocument({
  3170. document,
  3171. force = false,
  3172. clearDraft = false,
  3173. silent = false,
  3174. target,
  3175. ignoreNoPremium
  3176. }: {
  3177. document: MyDocument | DocId,
  3178. force?: boolean,
  3179. clearDraft?: boolean,
  3180. silent?: boolean,
  3181. target?: HTMLElement,
  3182. ignoreNoPremium?: boolean
  3183. }) {
  3184. document = await this.managers.appDocsManager.getDoc(document);
  3185. const flag = document.type === 'sticker' ? 'send_stickers' : (document.type === 'gif' ? 'send_gifs' : 'send_media');
  3186. if(this.chat.peerId.isAnyChat() && !(await this.chat.canSend(flag))) {
  3187. toastNew({langPackKey: POSTING_NOT_ALLOWED_MAP[flag]});
  3188. return false;
  3189. }
  3190. // 记录聊天对象ID
  3191. if(this.chat.type !== ChatType.Scheduled) { // 只记录非定时消息
  3192. chatHistoryService.recordChatPeer(this.chat.peerId);
  3193. }
  3194. if(this.chat.type === ChatType.Scheduled && !force) {
  3195. this.scheduleSending(() => this.sendMessageWithDocument({document, force: true, clearDraft, silent, target}));
  3196. return false;
  3197. }
  3198. if(!document) {
  3199. return false;
  3200. }
  3201. if(document.sticker && getStickerEffectThumb(document) && !rootScope.premium && !ignoreNoPremium) {
  3202. PopupPremium.show({feature: 'premium_stickers'});
  3203. return false;
  3204. }
  3205. if(await this.showSlowModeTooltipIfNeeded({element: target})) {
  3206. return false;
  3207. }
  3208. const sendingParams = this.chat.getMessageSendingParams();
  3209. const preparedPaymentResult = await this.paidMessageInterceptor.prepareStarsForPayment(1);
  3210. if(preparedPaymentResult === PAYMENT_REJECTED) return;
  3211. sendingParams.confirmedPaymentResult = preparedPaymentResult;
  3212. this.managers.appMessagesManager.sendFile({
  3213. ...sendingParams,
  3214. file: document,
  3215. isMedia: true,
  3216. clearDraft,
  3217. silent
  3218. });
  3219. this.onMessageSent(clearDraft, true);
  3220. if(document.type === 'sticker') {
  3221. this.managers.appStickersManager.saveRecentSticker(document.id);
  3222. }
  3223. return true;
  3224. }
  3225. private canToggleHideAuthor() {
  3226. const {forwardElements} = this;
  3227. if(!forwardElements) return false;
  3228. const hideCaptionCheckboxField = forwardElements.hideCaption.checkboxField;
  3229. return !hideCaptionCheckboxField.checked ||
  3230. findUpTag(hideCaptionCheckboxField.label, 'FORM').classList.contains('hide');
  3231. }
  3232. private isDroppingCaptions() {
  3233. return !this.canToggleHideAuthor();
  3234. }
  3235. /* public sendSomething(callback: () => void, force = false) {
  3236. if(this.chat.type === 'scheduled' && !force) {
  3237. this.scheduleSending(() => this.sendSomething(callback, true));
  3238. return false;
  3239. }
  3240. callback();
  3241. this.onMessageSent(false, true);
  3242. return true;
  3243. } */
  3244. public initMessageEditing(mid: number) {
  3245. const message = this.chat.getMessage(mid) as Message.message;
  3246. let input = wrapDraftText(message.message, {entities: message.totalEntities, wrappingForPeerId: this.chat.peerId});
  3247. const f = async() => {
  3248. let restoreInputLock: () => void;
  3249. if(!this.messageInput.isContentEditable) {
  3250. const placeholderParams = await this.getPlaceholderParams(true);
  3251. const {contentEditable} = this.messageInput;
  3252. this.messageInput.contentEditable = 'true';
  3253. const {oldKey, oldArgs} = this.updateMessageInputPlaceholder(placeholderParams);
  3254. restoreInputLock = () => {
  3255. this.messageInput.contentEditable = contentEditable;
  3256. this.updateMessageInputPlaceholder({key: oldKey, args: oldArgs});
  3257. };
  3258. }
  3259. const replyFragment = await wrapMessageForReply({message, usingMids: [message.mid]});
  3260. this.setTopInfo({
  3261. type: 'edit',
  3262. callerFunc: f,
  3263. title: i18n('AccDescrEditing'),
  3264. subtitle: replyFragment,
  3265. input,
  3266. message
  3267. });
  3268. this.editMsgId = mid;
  3269. this.editMessage = message;
  3270. input = undefined;
  3271. this.restoreInputLock = restoreInputLock;
  3272. };
  3273. f();
  3274. }
  3275. public initMessagesForward(fromPeerIdsMids: {[fromPeerId: PeerId]: number[]}) {
  3276. const f = async() => {
  3277. // const peerTitles: string[]
  3278. const fromPeerIds = Object.keys(fromPeerIdsMids).map((fromPeerId) => fromPeerId.toPeerId());
  3279. const smth: Set<string> = new Set();
  3280. let length = 0, messagesWithCaptionsLength = 0;
  3281. const p = fromPeerIds.map(async(fromPeerId) => {
  3282. const mids = fromPeerIdsMids[fromPeerId];
  3283. const promises = mids.map(async(mid) => {
  3284. const message = (await this.managers.appMessagesManager.getMessageByPeer(fromPeerId, mid)) as Message.message;
  3285. if(getFwdFromName(message.fwd_from) && !message.fromId && !message.fwdFromId) {
  3286. smth.add('N' + getFwdFromName(message.fwd_from));
  3287. } else {
  3288. smth.add('P' + message.fromId);
  3289. }
  3290. if(
  3291. message.media &&
  3292. !(['messageMediaWebPage'] as MessageMedia['_'][]).includes(message.media._) &&
  3293. message.message
  3294. ) {
  3295. ++messagesWithCaptionsLength;
  3296. }
  3297. });
  3298. await Promise.all(promises);
  3299. length += mids.length;
  3300. });
  3301. await Promise.all(p);
  3302. const onlyFirstName = smth.size > 2;
  3303. const peerTitles = [...smth].map((smth) => {
  3304. const type = smth[0];
  3305. smth = smth.slice(1);
  3306. if(type === 'P') {
  3307. const peerId = smth.toPeerId();
  3308. return peerId === rootScope.myId ? i18n('Chat.Accessory.Forward.You') : new PeerTitle({peerId, dialog: false, onlyFirstName}).element;
  3309. } else {
  3310. return onlyFirstName ? smth.split(' ')[0] : smth;
  3311. }
  3312. });
  3313. const {forwardElements} = this;
  3314. const form = findUpTag(forwardElements.showCaption.checkboxField.label, 'FORM');
  3315. form.classList.toggle('hide', !messagesWithCaptionsLength);
  3316. const hideCaption = forwardElements.hideCaption.checkboxField.checked;
  3317. if(messagesWithCaptionsLength && hideCaption) {
  3318. forwardElements.hideSender.checkboxField.setValueSilently(true);
  3319. } else if(this.forwardWasDroppingAuthor !== undefined) {
  3320. (this.forwardWasDroppingAuthor ? forwardElements.hideSender : forwardElements.showSender).checkboxField.setValueSilently(true);
  3321. }
  3322. const titleKey: LangPackKey = forwardElements.showSender.checkboxField.checked ? 'Chat.Accessory.Forward' : 'Chat.Accessory.Hidden';
  3323. const title = i18n(titleKey, [length]);
  3324. const senderTitles = document.createDocumentFragment();
  3325. if(peerTitles.length < 3) {
  3326. senderTitles.append(...join(peerTitles, false));
  3327. } else {
  3328. senderTitles.append(peerTitles[0], i18n('AndOther', [peerTitles.length - 1]));
  3329. }
  3330. let firstMessage: Message.message, usingFullGrouped: boolean;
  3331. if(fromPeerIds.length === 1) {
  3332. const fromPeerId = fromPeerIds[0];
  3333. const mids = fromPeerIdsMids[fromPeerId];
  3334. firstMessage = (await this.managers.appMessagesManager.getMessageByPeer(fromPeerId, mids[0])) as Message.message;
  3335. usingFullGrouped = !!firstMessage.grouped_id;
  3336. if(usingFullGrouped) {
  3337. const groupedMids = await this.managers.appMessagesManager.getMidsByMessage(firstMessage);
  3338. if(groupedMids.length !== length || groupedMids.find((mid) => !mids.includes(mid))) {
  3339. usingFullGrouped = false;
  3340. }
  3341. }
  3342. }
  3343. const subtitleFragment = document.createDocumentFragment();
  3344. const delimiter = ': ';
  3345. if(usingFullGrouped || length === 1) {
  3346. const mids = fromPeerIdsMids[fromPeerIds[0]];
  3347. const replyFragment = await wrapMessageForReply({message: firstMessage, usingMids: mids});
  3348. subtitleFragment.append(
  3349. senderTitles,
  3350. delimiter,
  3351. replyFragment
  3352. );
  3353. } else {
  3354. subtitleFragment.append(
  3355. i18n('Chat.Accessory.Forward.From'),
  3356. delimiter,
  3357. senderTitles
  3358. );
  3359. }
  3360. const newReply = this.setTopInfo({
  3361. type: 'forward',
  3362. callerFunc: f,
  3363. title,
  3364. subtitle: subtitleFragment
  3365. });
  3366. forwardElements.modifyArgs.forEach((b, idx) => {
  3367. const text = b.textElement;
  3368. const intl: I18n.IntlElement = I18n.weakMap.get(text) as any;
  3369. intl.args = [idx < 2 ? fromPeerIds.length : messagesWithCaptionsLength];
  3370. intl.update();
  3371. });
  3372. this.setCurrentHover(this.forwardHover, newReply);
  3373. this.forwarding = fromPeerIdsMids;
  3374. };
  3375. f();
  3376. }
  3377. public async initMessageReply(replyTo: ReturnType<ChatInput['getReplyTo']>) {
  3378. if(deepEqual(this.getReplyTo(), replyTo)) {
  3379. return;
  3380. }
  3381. let {replyToMsgId, replyToQuote, replyToPeerId} = replyTo;
  3382. replyToPeerId ??= this.chat.peerId;
  3383. let message = await (
  3384. replyToPeerId ?
  3385. this.managers.appMessagesManager.getMessageByPeer(replyToPeerId, replyToMsgId) :
  3386. this.chat.getMessage(replyToMsgId)
  3387. );
  3388. const f = () => {
  3389. let title: HTMLElement, subtitle: string | HTMLElement;
  3390. if(!message) { // load missing replying message
  3391. title = i18n('Loading');
  3392. this.managers.appMessagesManager.reloadMessages(replyToPeerId, replyToMsgId).then((_message) => {
  3393. if(!deepEqual(this.getReplyTo(), replyTo)) {
  3394. return;
  3395. }
  3396. message = _message;
  3397. if(!message) {
  3398. this.clearHelper('reply');
  3399. } else {
  3400. f();
  3401. }
  3402. });
  3403. } else {
  3404. const peerId = message.fromId;
  3405. title = new PeerTitle({
  3406. peerId: message.fromId,
  3407. dialog: false,
  3408. fromName: !peerId ? getFwdFromName((message as Message.message).fwd_from) : undefined
  3409. }).element;
  3410. title = i18n(replyToQuote ? 'ReplyToQuote' : 'ReplyTo', [title]);
  3411. }
  3412. const newReply = this.setTopInfo({
  3413. type: 'reply',
  3414. callerFunc: f,
  3415. title,
  3416. subtitle,
  3417. message,
  3418. setColorPeerId: message?.fromId,
  3419. quote: message ? replyToQuote : undefined
  3420. });
  3421. this.setReplyTo(replyTo);
  3422. this.replyElements.replyInAnother.element.classList.toggle('hide', !this.chat.bubbles.canForward(message as Message.message));
  3423. this.replyElements.doNotReply.element.classList.toggle('hide', !!replyToQuote);
  3424. this.replyElements.doNotQuote.element.classList.toggle('hide', !replyToQuote);
  3425. this.setCurrentHover(this.replyHover, newReply);
  3426. };
  3427. f();
  3428. }
  3429. private setCurrentHover(dropdownHover?: DropdownHover, newReply?: HTMLElement) {
  3430. if(this.currentHover) {
  3431. this.currentHover.toggle(false);
  3432. }
  3433. this.hoverListenerSetter.removeAll();
  3434. this.currentHover = dropdownHover;
  3435. dropdownHover?.attachButtonListener(newReply, this.listenerSetter);
  3436. }
  3437. public setReplyTo(replyTo: ChatInputReplyTo) {
  3438. const {replyToMsgId, replyToQuote, replyToPeerId, replyToStoryId} = replyTo || {};
  3439. this.replyToMsgId = replyToMsgId;
  3440. this.replyToStoryId = replyToStoryId;
  3441. this.replyToQuote = replyToQuote;
  3442. this.replyToPeerId = replyToPeerId;
  3443. this.center(true);
  3444. }
  3445. public clearHelper(type?: ChatInputHelperType) {
  3446. if(this.helperType === 'edit' && type !== 'edit') {
  3447. this.clearInput();
  3448. }
  3449. if(type) {
  3450. this.lastUrl = '';
  3451. delete this.noWebPage;
  3452. this.willSendWebPage = null;
  3453. }
  3454. if(type !== 'reply') {
  3455. this.setReplyTo(undefined);
  3456. this.forwarding = undefined;
  3457. }
  3458. this.editMsgId = this.editMessage = undefined;
  3459. this.helperType = this.helperFunc = undefined;
  3460. this.setCurrentHover();
  3461. this.saveDraftDebounced();
  3462. if(this.restoreInputLock) {
  3463. this.restoreInputLock();
  3464. this.restoreInputLock = undefined;
  3465. }
  3466. if(this.chat.container && this.chat.container.classList.contains('is-helper-active')) {
  3467. appNavigationController.removeByType('input-helper');
  3468. this.chat.container.classList.remove('is-helper-active');
  3469. this.t();
  3470. }
  3471. }
  3472. private t() {
  3473. const className = 'is-toggling-helper';
  3474. SetTransition({
  3475. element: this.chat.container,
  3476. className,
  3477. forwards: true,
  3478. duration: 150,
  3479. onTransitionEnd: () => {
  3480. this.chat.container.classList.remove(className);
  3481. }
  3482. });
  3483. }
  3484. public setInputValue(
  3485. value: Parameters<InputFieldAnimated['setValueSilently']>[0],
  3486. clear = true,
  3487. focus = true,
  3488. draftMessage?: DraftMessage.draftMessage
  3489. ) {
  3490. value ||= '';
  3491. if(clear) this.clearInput(false, false, value as string);
  3492. else this.messageInputField.setValueSilently(value);
  3493. fastRaf(() => {
  3494. focus && placeCaretAtEnd(this.messageInput);
  3495. this.processingDraftMessage = draftMessage;
  3496. if(draftMessage) this.setEffect(draftMessage.effect);
  3497. this.onMessageInput();
  3498. this.processingDraftMessage = undefined;
  3499. this.messageInput.scrollTop = this.messageInput.scrollHeight;
  3500. });
  3501. }
  3502. public setTopInfo({
  3503. type,
  3504. callerFunc,
  3505. title,
  3506. subtitle,
  3507. setColorPeerId,
  3508. input,
  3509. message,
  3510. quote
  3511. }: {
  3512. type: ChatInputHelperType,
  3513. callerFunc: () => void,
  3514. input?: Parameters<InputFieldAnimated['setValueSilently']>[0],
  3515. message?: any
  3516. } & Pick<Parameters<typeof wrapReply>[0], 'title' | 'subtitle' | 'setColorPeerId' | 'quote'>) {
  3517. if(this.willSendWebPage && type === 'reply') {
  3518. return;
  3519. }
  3520. if(type !== 'webpage') {
  3521. this.clearHelper(type);
  3522. this.helperType = type;
  3523. this.helperFunc = callerFunc;
  3524. }
  3525. const replyParent = this.replyElements.container;
  3526. const oldReply = replyParent.lastElementChild.previousElementSibling;
  3527. const haveReply = oldReply.classList.contains('reply');
  3528. this.replyElements.iconBtn.replaceWith(this.replyElements.iconBtn = this.createButtonIcon((type === 'webpage' ? 'link' : type) + ' reply-icon', {noRipple: true}));
  3529. const {container} = wrapReply({
  3530. title,
  3531. subtitle,
  3532. setColorPeerId,
  3533. animationGroup: this.chat.animationGroup,
  3534. message,
  3535. textColor: 'secondary-text-color',
  3536. quote
  3537. });
  3538. this.appImManager.setPeerColorToElement({peerId: setColorPeerId, element: replyParent});
  3539. if(haveReply) {
  3540. oldReply.replaceWith(container);
  3541. } else {
  3542. replyParent.lastElementChild.before(container);
  3543. }
  3544. if(!this.chat.container.classList.contains('is-helper-active')) {
  3545. this.chat.container.classList.add('is-helper-active');
  3546. this.t();
  3547. }
  3548. if(!IS_MOBILE) {
  3549. appNavigationController.pushItem({
  3550. type: 'input-helper',
  3551. onPop: () => {
  3552. this.onHelperCancel();
  3553. }
  3554. });
  3555. }
  3556. if(input !== undefined) {
  3557. this.setInputValue(input);
  3558. }
  3559. setTimeout(() => {
  3560. this.updateSendBtn();
  3561. }, 0);
  3562. return container;
  3563. }
  3564. }