import type { AppNotificationsManager } from '../../lib/appManagers/appNotificationsManager' ;
import type { AppChatsManager } from '../../lib/appManagers/appChatsManager' ;
import type { AppDocsManager , MyDocument } from "../../lib/appManagers/appDocsManager" ;
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager" ;
import type { AppPeersManager } from '../../lib/appManagers/appPeersManager' ;
import type { AppWebPagesManager } from "../../lib/appManagers/appWebPagesManager" ;
import type { AppImManager } from '../../lib/appManagers/appImManager' ;
import type { AppDraftsManager , MyDraftMessage } from '../../lib/appManagers/appDraftsManager' ;
import type { ServerTimeManager } from '../../lib/mtproto/serverTimeManager' ;
import type Chat from './chat' ;
import Recorder from '../../../public/recorder.min' ;
import { isTouchSupported } from "../../helpers/touchSupport" ;
import apiManager from "../../lib/mtproto/mtprotoworker" ;
//import Recorder from '../opus-recorder/dist/recorder.min';
import opusDecodeController from "../../lib/opusDecodeController" ;
import RichTextProcessor from "../../lib/richtextprocessor" ;
import { attachClickEvent , blurActiveElement , cancelEvent , cancelSelection , findUpClassName , getRichValue , isInputEmpty , markdownTags , MarkdownType , placeCaretAtEnd , isSendShortcutPressed } from "../../helpers/dom" ;
import { ButtonMenuItemOptions } from '../buttonMenu' ;
import emoticonsDropdown from "../emoticonsDropdown" ;
import PopupCreatePoll from "../popups/createPoll" ;
import PopupForward from '../popups/forward' ;
import PopupNewMedia from '../popups/newMedia' ;
import { toast } from "../toast" ;
import { wrapReply } from "../wrappers" ;
import InputField from '../inputField' ;
import { MessageEntity , DraftMessage } from '../../layer' ;
import StickersHelper from './stickersHelper' ;
import ButtonIcon from '../buttonIcon' ;
import DivAndCaption from '../divAndCaption' ;
import ButtonMenuToggle from '../buttonMenuToggle' ;
import ListenerSetter from '../../helpers/listenerSetter' ;
import Button from '../button' ;
import PopupSchedule from '../popups/schedule' ;
import SendMenu from './sendContextMenu' ;
import rootScope from '../../lib/rootScope' ;
import PopupPinMessage from '../popups/unpinMessage' ;
import { debounce } from '../../helpers/schedulers' ;
import { tsNow } from '../../helpers/date' ;
import appNavigationController from '../appNavigationController' ;
import { isMobile } from '../../helpers/userAgent' ;
import { i18n } from '../../lib/langPack' ;
const RECORD_MIN_TIME = 500 ;
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.' ;
type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply' ;
export default class ChatInput {
public pageEl = document . getElementById ( 'page-chats' ) as HTMLDivElement ;
public messageInput : HTMLElement ;
public messageInputField : InputField ;
public fileInput : HTMLInputElement ;
public inputMessageContainer : HTMLDivElement ;
public btnSend = document . getElementById ( 'btn-send' ) as HTMLButtonElement ;
public btnCancelRecord : HTMLButtonElement ;
public lastUrl = '' ;
public lastTimeType = 0 ;
public chatInput : HTMLElement ;
public inputContainer : HTMLElement ;
public rowsWrapper : HTMLDivElement ;
private newMessageWrapper : HTMLDivElement ;
private btnToggleEmoticons : HTMLButtonElement ;
public btnSendContainer : HTMLDivElement ;
public attachMenu : HTMLButtonElement ;
private attachMenuButtons : ( ButtonMenuItemOptions & { verify : ( peerId : number ) = > boolean } ) [ ] ;
public sendMenu : SendMenu ;
public replyElements : {
container? : HTMLElement ,
cancelBtn? : HTMLButtonElement ,
titleEl? : HTMLElement ,
subtitleEl? : HTMLElement
} = { } ;
public willSendWebPage : any = null ;
public forwardingMids : number [ ] = [ ] ;
public forwardingFromPeerId : number = 0 ;
public replyToMsgId : number ;
public editMsgId : number ;
public noWebPage : true ;
public scheduleDate : number ;
public sendSilent : true ;
private recorder : any ;
private recording = false ;
private recordCanceled = false ;
private recordTimeEl : HTMLElement ;
private recordRippleEl : HTMLElement ;
private recordStartTime = 0 ;
// private scrollTop = 0;
// private scrollOffsetTop = 0;
// private scrollDiff = 0;
public helperType : Exclude < ChatInputHelperType , ' webpage ' > ;
private helperFunc : ( ) = > void ;
private helperWaitingForward : boolean ;
public willAttachType : 'document' | 'media' ;
private lockRedo = false ;
private canRedoFromHTML = '' ;
readonly undoHistory : string [ ] = [ ] ;
readonly executedHistory : string [ ] = [ ] ;
private canUndoFromHTML = '' ;
public stickersHelper : StickersHelper ;
public listenerSetter : ListenerSetter ;
public pinnedControlBtn : HTMLButtonElement ;
public goDownBtn : HTMLButtonElement ;
public goDownUnreadBadge : HTMLElement ;
public btnScheduled : HTMLButtonElement ;
public saveDraftDebounced : ( ) = > void ;
public fakeRowsWrapper : HTMLDivElement ;
private fakePinnedControlBtn : HTMLElement ;
constructor ( private chat : Chat , private appMessagesManager : AppMessagesManager , private appDocsManager : AppDocsManager , private appChatsManager : AppChatsManager , private appPeersManager : AppPeersManager , private appWebPagesManager : AppWebPagesManager , private appImManager : AppImManager , private appDraftsManager : AppDraftsManager , private serverTimeManager : ServerTimeManager , private appNotificationsManager : AppNotificationsManager ) {
this . listenerSetter = new ListenerSetter ( ) ;
}
public construct() {
this . chatInput = document . createElement ( 'div' ) ;
this . chatInput . classList . add ( 'chat-input' ) ;
this . chatInput . style . display = 'none' ;
this . inputContainer = document . createElement ( 'div' ) ;
this . inputContainer . classList . add ( 'chat-input-container' ) ;
this . rowsWrapper = document . createElement ( 'div' ) ;
this . rowsWrapper . classList . add ( 'rows-wrapper' , 'chat-input-wrapper' ) ;
const fakeRowsWrapper = this . fakeRowsWrapper = document . createElement ( 'div' ) ;
fakeRowsWrapper . classList . add ( 'fake-wrapper' , 'fake-rows-wrapper' ) ;
const fakeSelectionWrapper = document . createElement ( 'div' ) ;
fakeSelectionWrapper . classList . add ( 'fake-wrapper' , 'fake-selection-wrapper' ) ;
this . inputContainer . append ( this . rowsWrapper , fakeRowsWrapper , fakeSelectionWrapper ) ;
this . chatInput . append ( this . inputContainer ) ;
this . goDownBtn = Button ( 'bubbles-go-down btn-corner btn-circle z-depth-1 hide' , { icon : 'arrow_down' } ) ;
this . goDownUnreadBadge = document . createElement ( 'span' ) ;
this . goDownUnreadBadge . classList . add ( 'badge' , 'badge-24' , 'badge-green' ) ;
this . goDownBtn . append ( this . goDownUnreadBadge ) ;
this . inputContainer . append ( this . goDownBtn ) ;
attachClickEvent ( this . goDownBtn , ( e ) = > {
cancelEvent ( e ) ;
this . chat . bubbles . onGoDownClick ( ) ;
} , { listenerSetter : this.listenerSetter } ) ;
// * constructor end
/ * l e t s e t S c r o l l T o p T i m e o u t : n u m b e r ;
// @ts-ignore
let height = window . visualViewport . height ; * /
// @ts-ignore
// this.listenerSetter.add(window.visualViewport, 'resize', () => {
// const scrollable = this.chat.bubbles.scrollable;
// const wasScrolledDown = scrollable.isScrolledDown;
// /* if(wasScrolledDown) {
// this.saveScroll();
// } */
// // @ts-ignore
// let newHeight = window.visualViewport.height;
// const diff = height - newHeight;
// const scrollTop = scrollable.scrollTop;
// const needScrollTop = wasScrolledDown ? scrollable.scrollHeight : scrollTop + diff; // * wasScrolledDown это проверка для десктоп хрома, когда пропадает панель загрузок снизу
// console.log('resize before', scrollable.scrollTop, scrollable.container.clientHeight, scrollable.scrollHeight, wasScrolledDown, scrollable.lastScrollTop, diff, needScrollTop);
// scrollable.scrollTop = needScrollTop;
// if(setScrollTopTimeout) clearTimeout(setScrollTopTimeout);
// setScrollTopTimeout = window.setTimeout(() => {
// const diff = height - newHeight;
// const isScrolledDown = scrollable.scrollHeight - Math.round(scrollable.scrollTop + scrollable.container.offsetHeight + diff) <= 1;
// height = newHeight;
// scrollable.scrollTop = needScrollTop;
// console.log('resize after', scrollable.scrollTop, scrollable.container.clientHeight, scrollable.scrollHeight, scrollable.isScrolledDown, scrollable.lastScrollTop, isScrolledDown);
// /* if(isScrolledDown) {
// scrollable.scrollTop = scrollable.scrollHeight;
// } */
// //scrollable.scrollTop += diff;
// setScrollTopTimeout = 0;
// }, 0);
// });
// ! Can't use it with resizeObserver
/ * t h i s . l i s t e n e r S e t t e r . a d d ( w i n d o w . v i s u a l V i e w p o r t , ' r e s i z e ' , ( ) = > {
const scrollable = this . chat . bubbles . scrollable ;
const wasScrolledDown = scrollable . isScrolledDown ;
// @ts-ignore
let newHeight = window . visualViewport . height ;
const diff = height - newHeight ;
const needScrollTop = wasScrolledDown ? scrollable.scrollHeight : scrollable.scrollTop + diff ; // * wasScrolledDown это проверка для десктоп хрома, когда пропадает панель загрузок снизу
//console.log('resize before', scrollable.scrollTop, scrollable.container.clientHeight, scrollable.scrollHeight, wasScrolledDown, scrollable.lastScrollTop, diff, needScrollTop);
scrollable . scrollTop = needScrollTop ;
height = newHeight ;
if ( setScrollTopTimeout ) clearTimeout ( setScrollTopTimeout ) ;
setScrollTopTimeout = window . setTimeout ( ( ) = > { // * try again for scrolled down Android Chrome
scrollable . scrollTop = needScrollTop ;
//console.log('resize after', scrollable.scrollTop, scrollable.container.clientHeight, scrollable.scrollHeight, scrollable.isScrolledDown, scrollable.lastScrollTop, isScrolledDown);
setScrollTopTimeout = 0 ;
} , 0 ) ;
} ) ; * /
}
public constructPeerHelpers() {
this . replyElements . container = document . createElement ( 'div' ) ;
this . replyElements . container . classList . add ( 'reply-wrapper' ) ;
this . replyElements . cancelBtn = ButtonIcon ( 'close reply-cancel' ) ;
const dac = new DivAndCaption ( 'reply' ) ;
this . replyElements . titleEl = dac . title ;
this . replyElements . subtitleEl = dac . subtitle ;
this . replyElements . container . append ( this . replyElements . cancelBtn , dac . container ) ;
this . newMessageWrapper = document . createElement ( 'div' ) ;
this . newMessageWrapper . classList . add ( 'new-message-wrapper' ) ;
this . btnToggleEmoticons = ButtonIcon ( 'none toggle-emoticons' , { noRipple : true } ) ;
this . inputMessageContainer = document . createElement ( 'div' ) ;
this . inputMessageContainer . classList . add ( 'input-message-container' ) ;
if ( this . chat . type === 'chat' ) {
this . btnScheduled = ButtonIcon ( 'scheduled' , { noRipple : true } ) ;
this . btnScheduled . classList . add ( 'btn-scheduled' , 'hide' ) ;
attachClickEvent ( this . btnScheduled , ( e ) = > {
this . appImManager . openScheduled ( this . chat . peerId ) ;
} , { listenerSetter : this.listenerSetter } ) ;
this . listenerSetter . add ( rootScope , 'scheduled_new' , ( e ) = > {
const peerId = e . peerId ;
if ( this . chat . peerId !== peerId ) {
return ;
}
this . btnScheduled . classList . remove ( 'hide' ) ;
} ) ;
this . listenerSetter . add ( rootScope , 'scheduled_delete' , ( e ) = > {
const peerId = e . peerId ;
if ( this . chat . peerId !== peerId ) {
return ;
}
this . appMessagesManager . getScheduledMessages ( this . chat . peerId ) . then ( value = > {
this . btnScheduled . classList . toggle ( 'hide' , ! value . length ) ;
} ) ;
} ) ;
}
this . attachMenuButtons = [ {
icon : 'photo' ,
text : 'Chat.Input.Attach.PhotoOrVideo' ,
onClick : ( ) = > {
this . fileInput . value = '' ;
this . fileInput . setAttribute ( 'accept' , 'image/*, video/*' ) ;
this . willAttachType = 'media' ;
this . fileInput . click ( ) ;
} ,
verify : ( peerId : number ) = > peerId > 0 || this . appChatsManager . hasRights ( peerId , 'send_media' )
} , {
icon : 'document' ,
text : 'Chat.Input.Attach.Document' ,
onClick : ( ) = > {
this . fileInput . value = '' ;
this . fileInput . removeAttribute ( 'accept' ) ;
this . willAttachType = 'document' ;
this . fileInput . click ( ) ;
} ,
verify : ( peerId : number ) = > peerId > 0 || this . appChatsManager . hasRights ( peerId , 'send_media' )
} , {
icon : 'poll' ,
text : 'Poll' ,
onClick : ( ) = > {
new PopupCreatePoll ( this . chat ) . show ( ) ;
} ,
verify : ( peerId : number ) = > peerId < 0 && this . appChatsManager . hasRights ( peerId , 'send_polls' )
} ] ;
this . attachMenu = ButtonMenuToggle ( { noRipple : true , listenerSetter : this.listenerSetter } , 'top-left' , this . attachMenuButtons ) ;
this . attachMenu . classList . add ( 'attach-file' , 'tgico-attach' ) ;
this . attachMenu . classList . remove ( 'tgico-more' ) ;
//this.inputContainer.append(this.sendMenu);
this . recordTimeEl = document . createElement ( 'div' ) ;
this . recordTimeEl . classList . add ( 'record-time' ) ;
this . fileInput = document . createElement ( 'input' ) ;
this . fileInput . type = 'file' ;
this . fileInput . multiple = true ;
this . fileInput . style . display = 'none' ;
this . newMessageWrapper . append ( . . . [ this . btnToggleEmoticons , this . inputMessageContainer , this . btnScheduled , this . attachMenu , this . recordTimeEl , this . fileInput ] . filter ( Boolean ) ) ;
this . rowsWrapper . append ( this . replyElements . container , this . newMessageWrapper ) ;
this . btnCancelRecord = ButtonIcon ( 'delete danger btn-circle z-depth-1 btn-record-cancel' ) ;
this . btnSendContainer = document . createElement ( 'div' ) ;
this . btnSendContainer . classList . add ( 'btn-send-container' ) ;
this . recordRippleEl = document . createElement ( 'div' ) ;
this . recordRippleEl . classList . add ( 'record-ripple' ) ;
this . btnSend = ButtonIcon ( 'none btn-circle z-depth-1 btn-send' ) ;
this . btnSend . insertAdjacentHTML ( 'afterbegin' , `
< span class = "tgico tgico-send" > < / span >
< span class = "tgico tgico-schedule" > < / span >
< span class = "tgico tgico-check" > < / span >
< span class = "tgico tgico-microphone2" > < / span >
` );
this . btnSendContainer . append ( this . recordRippleEl , this . btnSend ) ;
if ( this . chat . type !== 'scheduled' ) {
this . sendMenu = new SendMenu ( {
onSilentClick : ( ) = > {
this . sendSilent = true ;
this . sendMessage ( ) ;
} ,
onScheduleClick : ( ) = > {
this . scheduleSending ( undefined ) ;
} ,
listenerSetter : this.listenerSetter ,
openSide : 'top-left' ,
onContextElement : this.btnSend ,
onOpen : ( ) = > {
return ! this . isInputEmpty ( ) ;
}
} ) ;
this . btnSendContainer . append ( this . sendMenu . sendMenu ) ;
}
this . inputContainer . append ( this . btnCancelRecord , this . btnSendContainer ) ;
emoticonsDropdown . attachButtonListener ( this . btnToggleEmoticons ) ;
emoticonsDropdown . events . onOpen . push ( this . onEmoticonsOpen ) ;
emoticonsDropdown . events . onClose . push ( this . onEmoticonsClose ) ;
this . attachMessageInputField ( ) ;
/ * t h i s . a t t a c h M e n u . a d d E v e n t L i s t e n e r ( ' m o u s e d o w n ' , ( e ) = > {
const hidden = this . attachMenu . querySelectorAll ( '.hide' ) ;
if ( hidden . length === this . attachMenuButtons . length ) {
toast ( POSTING_MEDIA_NOT_ALLOWED ) ;
cancelEvent ( e ) ;
return false ;
}
} , { passive : false , capture : true } ) ; * /
this . stickersHelper = new StickersHelper ( this . rowsWrapper ) ;
this . listenerSetter . add ( rootScope , 'settings_updated' , ( ) = > {
if ( this . stickersHelper ) {
if ( ! rootScope . settings . stickers . suggest ) {
this . stickersHelper . checkEmoticon ( '' ) ;
} else {
this . onMessageInput ( ) ;
}
}
if ( this . messageInputField ) {
this . messageInputField . onFakeInput ( ) ;
}
} ) ;
this . listenerSetter . add ( rootScope , 'draft_updated' , ( e ) = > {
const { peerId , threadId , draft } = e ;
if ( this . chat . threadId !== threadId || this . chat . peerId !== peerId ) return ;
this . setDraft ( draft ) ;
} ) ;
this . listenerSetter . add ( rootScope , 'peer_changing' , ( chat ) = > {
if ( this . chat === chat ) {
this . saveDraft ( ) ;
}
} ) ;
try {
this . recorder = new Recorder ( {
//encoderBitRate: 32,
//encoderPath: "../dist/encoderWorker.min.js",
encoderSampleRate : 48000 ,
monitorGain : 0 ,
numberOfChannels : 1 ,
recordingGain : 1 ,
reuseWorker : true
} ) ;
} catch ( err ) {
console . error ( 'Recorder constructor error:' , err ) ;
}
this . updateSendBtn ( ) ;
this . listenerSetter . add ( this . fileInput , 'change' , ( e ) = > {
let files = ( e . target as HTMLInputElement & EventTarget ) . files ;
if ( ! files . length ) {
return ;
}
new PopupNewMedia ( this . chat , Array . from ( files ) . slice ( ) , this . willAttachType ) ;
this . fileInput . value = '' ;
} , false ) ;
/ * l e t t i m e = D a t e . n o w ( ) ;
this . btnSend . addEventListener ( 'touchstart' , ( e ) = > {
time = Date . now ( ) ;
} ) ;
let eventName1 = 'touchend' ;
this . btnSend . addEventListener ( eventName1 , ( e : Event ) = > {
//cancelEvent(e);
console . log ( eventName1 + ', time: ' + ( Date . now ( ) - time ) ) ;
} ) ;
let eventName = 'mousedown' ;
this . btnSend . addEventListener ( eventName , ( e : Event ) = > {
cancelEvent ( e ) ;
console . log ( eventName + ', time: ' + ( Date . now ( ) - time ) ) ;
} ) ; * /
attachClickEvent ( this . btnSend , this . onBtnSendClick , { listenerSetter : this.listenerSetter , touchMouseDown : true } ) ;
if ( this . recorder ) {
attachClickEvent ( this . btnCancelRecord , this . onCancelRecordClick , { listenerSetter : this.listenerSetter } ) ;
this . recorder . onstop = ( ) = > {
this . recording = false ;
this . chatInput . classList . remove ( 'is-recording' , 'is-locked' ) ;
this . updateSendBtn ( ) ;
this . recordRippleEl . style . transform = '' ;
} ;
this . recorder . ondataavailable = ( typedArray : Uint8Array ) = > {
if ( this . recordCanceled ) return ;
const duration = ( Date . now ( ) - this . recordStartTime ) / 1000 | 0 ;
const dataBlob = new Blob ( [ typedArray ] , { type : 'audio/ogg' } ) ;
/ * c o n s t f i l e N a m e = n e w D a t e ( ) . t o I S O S t r i n g ( ) + " . o p u s " ;
console . log ( 'Recorder data received' , typedArray , dataBlob ) ; * /
//let perf = performance.now();
opusDecodeController . decode ( typedArray , true ) . then ( result = > {
//console.log('WAVEFORM!:', /* waveform, */performance.now() - perf);
opusDecodeController . setKeepAlive ( false ) ;
let peerId = this . chat . peerId ;
// тут objectURL ставится уже с audio/wav
this . appMessagesManager . sendFile ( peerId , dataBlob , {
isVoiceMessage : true ,
isMedia : true ,
duration ,
waveform : result.waveform ,
objectURL : result.url ,
replyToMsgId : this.replyToMsgId ,
threadId : this.chat.threadId ,
clearDraft : true
} ) ;
this . onMessageSent ( false , true ) ;
} ) ;
} ;
}
attachClickEvent ( this . replyElements . cancelBtn , this . onHelperCancel , { listenerSetter : this.listenerSetter } ) ;
attachClickEvent ( this . replyElements . container , this . onHelperClick , { listenerSetter : this.listenerSetter } ) ;
this . saveDraftDebounced = debounce ( ( ) = > this . saveDraft ( ) , 2500 , false , true ) ;
}
public constructPinnedHelpers() {
const container = document . createElement ( 'div' ) ;
container . classList . add ( 'pinned-container' ) ;
this . pinnedControlBtn = Button ( 'btn-primary btn-transparent text-bold pinned-container-button' , { icon : 'unpin' } ) ;
container . append ( this . pinnedControlBtn ) ;
const fakeContainer = container . cloneNode ( true ) ;
this . fakePinnedControlBtn = fakeContainer . firstChild as HTMLElement ;
this . fakeRowsWrapper . append ( fakeContainer ) ;
this . listenerSetter . add ( this . pinnedControlBtn , 'click' , ( ) = > {
const peerId = this . chat . peerId ;
new PopupPinMessage ( peerId , 0 , true , ( ) = > {
this . chat . appImManager . setPeer ( 0 ) ; // * close tab
// ! костыль, это скроет закреплённые сообщения сразу, вместо того, чтобы ждать пока анимация перехода закончится
const originalChat = this . chat . appImManager . chat ;
if ( originalChat . topbar . pinnedMessage ) {
originalChat . topbar . pinnedMessage . pinnedMessageContainer . toggle ( true ) ;
}
} ) ;
} ) ;
this . rowsWrapper . append ( container ) ;
this . chatInput . classList . add ( 'type-pinned' ) ;
this . rowsWrapper . classList . add ( 'is-centered' ) ;
}
private onCancelRecordClick = ( e? : Event ) = > {
if ( e ) {
cancelEvent ( e ) ;
}
this . recordCanceled = true ;
this . recorder . stop ( ) ;
opusDecodeController . setKeepAlive ( false ) ;
} ;
private onEmoticonsOpen = ( ) = > {
const toggleClass = isTouchSupported ? 'flip-icon' : 'active' ;
this . btnToggleEmoticons . classList . toggle ( toggleClass , true ) ;
} ;
private onEmoticonsClose = ( ) = > {
const toggleClass = isTouchSupported ? 'flip-icon' : 'active' ;
this . btnToggleEmoticons . classList . toggle ( toggleClass , false ) ;
} ;
public scheduleSending = ( callback : ( ) = > void = this . sendMessage . bind ( this , true ) , initDate = new Date ( ) ) = > {
new PopupSchedule ( initDate , ( timestamp ) = > {
const minTimestamp = ( Date . now ( ) / 1000 | 0 ) + 10 ;
if ( timestamp <= minTimestamp ) {
timestamp = undefined ;
}
this . scheduleDate = timestamp ;
callback ( ) ;
if ( this . chat . type !== 'scheduled' && timestamp ) {
this . appImManager . openScheduled ( this . chat . peerId ) ;
}
} ) . show ( ) ;
} ;
public setUnreadCount() {
const dialog = this . appMessagesManager . getDialogByPeerId ( this . chat . peerId ) [ 0 ] ;
const count = dialog ? . unread_count ;
this . goDownUnreadBadge . innerText = '' + ( count || '' ) ;
this . goDownUnreadBadge . classList . toggle ( 'badge-gray' , this . appNotificationsManager . isPeerLocalMuted ( this . chat . peerId , true ) ) ;
}
public saveDraft() {
if ( ! this . chat . peerId || this . editMsgId || this . chat . type === 'scheduled' ) return ;
const entities : MessageEntity [ ] = [ ] ;
const str = getRichValue ( this . messageInputField . input , entities ) ;
let draft : DraftMessage.draftMessage ;
if ( str . length || this . replyToMsgId ) {
draft = {
_ : 'draftMessage' ,
date : tsNow ( true ) + this . serverTimeManager . serverTimeOffset ,
message : str ,
entities : entities.length ? entities : undefined ,
pFlags : {
no_webpage : this.noWebPage
} ,
reply_to_msg_id : this.replyToMsgId
} ;
}
this . appDraftsManager . syncDraft ( this . chat . peerId , this . chat . threadId , draft ) ;
}
public destroy() {
//this.chat.log.error('Input destroying');
emoticonsDropdown . events . onOpen . findAndSplice ( f = > f === this . onEmoticonsOpen ) ;
emoticonsDropdown . events . onClose . findAndSplice ( f = > f === this . onEmoticonsClose ) ;
this . listenerSetter . removeAll ( ) ;
}
public cleanup ( helperToo = true ) {
if ( ! this . chat . peerId ) {
this . chatInput . style . display = 'none' ;
this . goDownBtn . classList . add ( 'hide' ) ;
}
cancelSelection ( ) ;
this . lastTimeType = 0 ;
if ( this . messageInput ) {
this . clearInput ( ) ;
helperToo && this . clearHelper ( ) ;
}
}
public setDraft ( draft? : MyDraftMessage , fromUpdate = true ) {
if ( ! isInputEmpty ( this . messageInput ) || this . chat . type === 'scheduled' ) return false ;
if ( ! draft ) {
draft = this . appDraftsManager . getDraft ( this . chat . peerId , this . chat . threadId ) ;
if ( ! draft ) {
return false ;
}
}
this . noWebPage = draft . pFlags . no_webpage ;
if ( draft . reply_to_msg_id ) {
this . initMessageReply ( draft . reply_to_msg_id ) ;
}
this . setInputValue ( draft . rMessage , fromUpdate , fromUpdate ) ;
return true ;
}
public finishPeerChange() {
const peerId = this . chat . peerId ;
this . chatInput . style . display = '' ;
const isBroadcast = this . appPeersManager . isBroadcast ( peerId ) ;
this . goDownBtn . classList . toggle ( 'is-broadcast' , isBroadcast ) ;
this . goDownBtn . classList . remove ( 'hide' ) ;
if ( this . goDownUnreadBadge ) {
this . setUnreadCount ( ) ;
}
if ( this . chat . type === 'pinned' ) {
this . chatInput . classList . toggle ( 'can-pin' , this . appPeersManager . canPinMessage ( peerId ) ) ;
} / * else if ( this . chat . type === 'chat' ) {
} * /
if ( this . btnScheduled ) {
this . btnScheduled . classList . add ( 'hide' ) ;
const middleware = this . chat . bubbles . getMiddleware ( ) ;
this . appMessagesManager . getScheduledMessages ( peerId ) . then ( mids = > {
if ( ! middleware ( ) ) return ;
this . btnScheduled . classList . toggle ( 'hide' , ! mids . length ) ;
} ) ;
}
if ( this . sendMenu ) {
this . sendMenu . setPeerId ( peerId ) ;
}
if ( this . messageInput ) {
const canWrite = this . appMessagesManager . canWriteToPeer ( peerId ) ;
this . chatInput . classList . add ( 'no-transition' ) ;
this . chatInput . classList . toggle ( 'is-hidden' , ! canWrite ) ;
void this . chatInput . offsetLeft ; // reflow
this . chatInput . classList . remove ( 'no-transition' ) ;
const visible = this . attachMenuButtons . filter ( button = > {
const good = button . verify ( peerId ) ;
button . element . classList . toggle ( 'hide' , ! good ) ;
return good ;
} ) ;
if ( ! canWrite ) {
this . messageInput . removeAttribute ( 'contenteditable' ) ;
} else {
this . messageInput . setAttribute ( 'contenteditable' , 'true' ) ;
this . setDraft ( undefined , false ) ;
if ( ! this . messageInput . innerHTML ) {
this . messageInputField . onFakeInput ( ) ;
}
}
this . attachMenu . toggleAttribute ( 'disabled' , ! visible . length ) ;
this . updateSendBtn ( ) ;
} else if ( this . pinnedControlBtn ) {
if ( this . appPeersManager . canPinMessage ( this . chat . peerId ) ) {
this . pinnedControlBtn . append ( i18n ( 'Chat.Input.UnpinAll' ) ) ;
this . fakePinnedControlBtn . append ( i18n ( 'Chat.Input.UnpinAll' ) ) ;
} else {
this . pinnedControlBtn . append ( i18n ( 'Chat.Pinned.DontShow' ) ) ;
this . fakePinnedControlBtn . append ( i18n ( 'Chat.Pinned.DontShow' ) ) ;
}
}
}
private attachMessageInputField() {
const oldInputField = this . messageInputField ;
this . messageInputField = new InputField ( {
placeholder : 'Message' ,
name : 'message' ,
animate : true
} ) ;
this . messageInputField . input . classList . replace ( 'input-field-input' , 'input-message-input' ) ;
this . messageInputField . inputFake . classList . replace ( 'input-field-input' , 'input-message-input' ) ;
this . messageInput = this . messageInputField . input ;
this . attachMessageInputListeners ( ) ;
if ( oldInputField ) {
oldInputField . input . replaceWith ( this . messageInputField . input ) ;
oldInputField . inputFake . replaceWith ( this . messageInputField . inputFake ) ;
} else {
this . inputMessageContainer . append ( this . messageInputField . input , this . messageInputField . inputFake ) ;
}
}
private attachMessageInputListeners() {
this . listenerSetter . add ( this . messageInput , 'keydown' , ( e : KeyboardEvent ) = > {
if ( isSendShortcutPressed ( e ) ) {
this . sendMessage ( ) ;
} else if ( e . ctrlKey || e . metaKey ) {
this . handleMarkdownShortcut ( e ) ;
}
} ) ;
if ( isTouchSupported ) {
attachClickEvent ( this . messageInput , ( e ) = > {
this . appImManager . selectTab ( 1 ) ; // * set chat tab for album orientation
//this.saveScroll();
emoticonsDropdown . toggle ( false ) ;
} , { listenerSetter : this.listenerSetter } ) ;
/ * t h i s . l i s t e n e r S e t t e r . a d d ( w i n d o w , ' r e s i z e ' , ( ) = > {
this . restoreScroll ( ) ;
} ) ; * /
/ * i f ( i s S a f a r i ) {
this . listenerSetter . add ( this . messageInput , 'mousedown' , ( ) = > {
window . requestAnimationFrame ( ( ) = > {
window . requestAnimationFrame ( ( ) = > {
emoticonsDropdown . toggle ( false ) ;
} ) ;
} ) ;
} ) ;
} * /
}
/ * t h i s . l i s t e n e r S e t t e r . a d d ( t h i s . m e s s a g e I n p u t , ' b e f o r e i n p u t ' , ( e : E v e n t ) = > {
// * validate due to manual formatting through browser's context menu
const inputType = ( e as InputEvent ) . inputType ;
//console.log('message beforeinput event', e);
if ( inputType . indexOf ( 'format' ) === 0 ) {
//console.log('message beforeinput format', e, inputType, this.messageInput.innerHTML);
const markdownType = inputType . split ( 'format' ) [ 1 ] . toLowerCase ( ) as MarkdownType ;
if ( this . applyMarkdown ( markdownType ) ) {
cancelEvent ( e ) ; // * cancel legacy markdown event
}
}
} ) ; * /
this . listenerSetter . add ( this . messageInput , 'input' , this . onMessageInput ) ;
if ( this . chat . type === 'chat' || this . chat . type === 'discussion' ) {
this . listenerSetter . add ( this . messageInput , 'focusin' , ( ) = > {
if ( this . chat . bubbles . scrollable . loadedAll . bottom ) {
this . appMessagesManager . readAllHistory ( this . chat . peerId , this . chat . threadId ) ;
}
} ) ;
}
}
private prepareDocumentExecute = ( ) = > {
this . executedHistory . push ( this . messageInput . innerHTML ) ;
return ( ) = > this . canUndoFromHTML = this . messageInput . innerHTML ;
} ;
private undoRedo = ( e : Event , type : 'undo' | 'redo' , needHTML : string ) = > {
cancelEvent ( e ) ; // cancel legacy event
let html = this . messageInput . innerHTML ;
if ( html && html !== needHTML ) {
this . lockRedo = true ;
let sameHTMLTimes = 0 ;
do {
document . execCommand ( type , false , null ) ;
const currentHTML = this . messageInput . innerHTML ;
if ( html === currentHTML ) {
if ( ++ sameHTMLTimes > 2 ) { // * unlink, removeFormat (а может и нет, случай: заболдить подчёркнутый текст (выделить ровно его), попробовать отменить)
break ;
}
} else {
sameHTMLTimes = 0 ;
}
html = currentHTML ;
} while ( html !== needHTML ) ;
this . lockRedo = false ;
}
} ;
public applyMarkdown ( type : MarkdownType , href? : string ) {
const commandsMap : Partial < { [ key in typeof type ] : string | ( ( ) = > void ) } > = {
bold : 'Bold' ,
italic : 'Italic' ,
underline : 'Underline' ,
strikethrough : 'Strikethrough' ,
monospace : ( ) = > document . execCommand ( 'fontName' , false , 'monospace' ) ,
link : href ? ( ) = > document . execCommand ( 'createLink' , false , href ) : ( ) = > document . execCommand ( 'unlink' , false , null )
} ;
if ( ! commandsMap [ type ] ) {
return false ;
}
const command = commandsMap [ type ] ;
//type = 'monospace';
const saveExecuted = this . prepareDocumentExecute ( ) ;
const executed : any [ ] = [ ] ;
/ * *
* * clear previous formatting , due to Telegram ' s inability to handle several entities
* /
/ * c o n s t c h e c k F o r S i n g l e = ( ) = > {
const nodes = getSelectedNodes ( ) ;
//console.log('Using formatting:', commandsMap[type], nodes, this.executedHistory);
const parents = [ . . . new Set ( nodes . map ( node = > node . parentNode ) ) ] ;
//const differentParents = !!nodes.find(node => node.parentNode !== firstParent);
const differentParents = parents . length > 1 ;
let notSingle = false ;
if ( differentParents ) {
notSingle = true ;
} else {
const node = nodes [ 0 ] ;
if ( node && ( node . parentNode as HTMLElement ) !== this . messageInput && ( node . parentNode . parentNode as HTMLElement ) !== this . messageInput ) {
notSingle = true ;
}
}
if ( notSingle ) {
//if(type === 'monospace') {
executed . push ( document . execCommand ( 'styleWithCSS' , false , 'true' ) ) ;
//}
executed . push ( document . execCommand ( 'unlink' , false , null ) ) ;
executed . push ( document . execCommand ( 'removeFormat' , false , null ) ) ;
executed . push ( typeof ( command ) === 'function' ? command ( ) : document . execCommand ( command , false , null ) ) ;
//if(type === 'monospace') {
executed . push ( document . execCommand ( 'styleWithCSS' , false , 'false' ) ) ;
//}
}
} ; * /
executed . push ( document . execCommand ( 'styleWithCSS' , false , 'true' ) ) ;
if ( type === 'monospace' ) {
let haveThisType = false ;
//executed.push(document.execCommand('styleWithCSS', false, 'true'));
const selection = window . getSelection ( ) ;
if ( ! selection . isCollapsed ) {
const range = selection . getRangeAt ( 0 ) ;
const tag = markdownTags [ type ] ;
const node = range . commonAncestorContainer ;
if ( ( node . parentNode as HTMLElement ) . matches ( tag . match ) || ( node instanceof HTMLElement && node . matches ( tag . match ) ) ) {
haveThisType = true ;
}
}
//executed.push(document.execCommand('removeFormat', false, null));
if ( haveThisType ) {
executed . push ( document . execCommand ( 'fontName' , false , 'Roboto' ) ) ;
} else {
executed . push ( typeof ( command ) === 'function' ? command ( ) : document . execCommand ( command , false , null ) ) ;
}
} else {
executed . push ( typeof ( command ) === 'function' ? command ( ) : document . execCommand ( command , false , null ) ) ;
}
executed . push ( document . execCommand ( 'styleWithCSS' , false , 'false' ) ) ;
//checkForSingle();
saveExecuted ( ) ;
if ( this . appImManager . markupTooltip ) {
this . appImManager . markupTooltip . setActiveMarkupButton ( ) ;
}
return true ;
}
private handleMarkdownShortcut = ( e : KeyboardEvent ) = > {
const formatKeys : { [ key : string ] : MarkdownType } = {
'B' : 'bold' ,
'I' : 'italic' ,
'U' : 'underline' ,
'S' : 'strikethrough' ,
'M' : 'monospace' ,
'K' : 'link'
} ;
const selection = document . getSelection ( ) ;
if ( selection . toString ( ) . trim ( ) . length ) {
for ( const key in formatKeys ) {
const good = e . code === ( 'Key' + key ) ;
if ( good ) {
// * костыльчик
if ( key === 'K' ) {
this . appImManager . markupTooltip . showLinkEditor ( ) ;
cancelEvent ( e ) ;
break ;
}
this . applyMarkdown ( formatKeys [ key ] ) ;
cancelEvent ( e ) ; // cancel legacy event
break ;
}
}
}
//return;
if ( e . code === 'KeyZ' ) {
let html = this . messageInput . innerHTML ;
if ( e . shiftKey ) {
if ( this . undoHistory . length ) {
this . executedHistory . push ( html ) ;
html = this . undoHistory . pop ( ) ;
this . undoRedo ( e , 'redo' , html ) ;
html = this . messageInput . innerHTML ;
this . canRedoFromHTML = this . undoHistory . length ? html : '' ;
this . canUndoFromHTML = html ;
}
} else {
// * подождём, когда пользователь сам восстановит поле до нужного состояния, которое стало сразу после saveExecuted
if ( this . executedHistory . length && ( ! this . canUndoFromHTML || html === this . canUndoFromHTML ) ) {
this . undoHistory . push ( html ) ;
html = this . executedHistory . pop ( ) ;
this . undoRedo ( e , 'undo' , html ) ;
// * поставим новое состояние чтобы снова подождать, если пользователь изменит что-то, и потом попробует откатить до предыдущего состояния
this . canUndoFromHTML = this . canRedoFromHTML = this . messageInput . innerHTML ;
}
}
}
} ;
private onMessageInput = ( e? : Event ) = > {
// * validate due to manual formatting through browser's context menu
/ * c o n s t i n p u t T y p e = ( e a s I n p u t E v e n t ) . i n p u t T y p e ;
console . log ( 'message input event' , e ) ;
if ( inputType === 'formatBold' ) {
console . log ( 'message input format' , this . messageInput . innerHTML ) ;
cancelEvent ( e ) ;
}
if ( ! isSelectionSingle ( ) ) {
alert ( 'not single' ) ;
} * /
//console.log('messageInput input', this.messageInput.innerText);
//const value = this.messageInput.innerText;
const markdownEntities : MessageEntity [ ] = [ ] ;
const richValue = getRichValue ( this . messageInputField . input , markdownEntities ) ;
//const entities = RichTextProcessor.parseEntities(value);
const value = RichTextProcessor . parseMarkdown ( richValue , markdownEntities ) ;
const entities = RichTextProcessor . mergeEntities ( markdownEntities , RichTextProcessor . parseEntities ( value ) ) ;
//this.chat.log('messageInput entities', richValue, value, markdownEntities);
if ( this . stickersHelper &&
rootScope . settings . stickers . suggest &&
( this . chat . peerId > 0 || this . appChatsManager . hasRights ( this . chat . peerId , 'send_stickers' ) ) ) {
let emoticon = '' ;
if ( entities . length && entities [ 0 ] . _ === 'messageEntityEmoji' ) {
const entity = entities [ 0 ] ;
if ( entity . length === richValue . length && ! entity . offset ) {
emoticon = richValue ;
}
}
this . stickersHelper . checkEmoticon ( emoticon ) ;
}
if ( ! richValue . trim ( ) ) {
this . appImManager . markupTooltip . hide ( ) ;
}
const html = this . messageInput . innerHTML ;
if ( this . canRedoFromHTML && html !== this . canRedoFromHTML && ! this . lockRedo ) {
this . canRedoFromHTML = '' ;
this . undoHistory . length = 0 ;
}
const urlEntities : Array < MessageEntity.messageEntityUrl | MessageEntity.messageEntityTextUrl > = entities . filter ( e = > e . _ === 'messageEntityUrl' || e . _ === 'messageEntityTextUrl' ) as any ;
if ( urlEntities . length ) {
for ( const entity of urlEntities ) {
let url : string ;
if ( entity . _ === 'messageEntityTextUrl' ) {
url = entity . url ;
} else {
url = richValue . slice ( entity . offset , entity . offset + entity . length ) ;
if ( ! ( url . includes ( 'http://' ) || url . includes ( 'https://' ) ) ) {
continue ;
}
}
//console.log('messageInput url:', url);
if ( this . lastUrl !== url ) {
this . lastUrl = url ;
this . willSendWebPage = null ;
apiManager . invokeApi ( 'messages.getWebPage' , {
url ,
hash : 0
} ) . then ( ( webpage ) = > {
webpage = this . appWebPagesManager . saveWebPage ( webpage ) ;
if ( webpage . _ === 'webPage' ) {
if ( this . lastUrl !== url ) return ;
//console.log('got webpage: ', webpage);
this . setTopInfo ( 'webpage' , ( ) = > { } , webpage . site_name || webpage . title || 'Webpage' , webpage . description || webpage . url || '' ) ;
delete this . noWebPage ;
this . willSendWebPage = webpage ;
}
} ) ;
}
break ;
}
} else if ( this . lastUrl ) {
this . lastUrl = '' ;
delete this . noWebPage ;
this . willSendWebPage = null ;
if ( this . helperType ) {
this . helperFunc ( ) ;
} else {
this . clearHelper ( ) ;
}
}
if ( this . isInputEmpty ( ) ) {
if ( this . lastTimeType ) {
this . appMessagesManager . setTyping ( this . chat . peerId , 'sendMessageCancelAction' ) ;
}
} else {
const time = Date . now ( ) ;
if ( time - this . lastTimeType >= 6000 ) {
this . lastTimeType = time ;
this . appMessagesManager . setTyping ( this . chat . peerId , 'sendMessageTypingAction' ) ;
}
}
if ( ! this . editMsgId ) {
this . saveDraftDebounced ( ) ;
}
this . updateSendBtn ( ) ;
} ;
private onBtnSendClick = ( e : Event ) = > {
cancelEvent ( e ) ;
if ( ! this . recorder || this . recording || ! this . isInputEmpty ( ) || this . forwardingMids . length || this . editMsgId ) {
if ( this . recording ) {
if ( ( Date . now ( ) - this . recordStartTime ) < RECORD_MIN_TIME ) {
this . onCancelRecordClick ( ) ;
} else {
this . recorder . stop ( ) ;
}
} else {
this . sendMessage ( ) ;
}
} else {
if ( this . chat . peerId < 0 && ! this . appChatsManager . hasRights ( this . chat . peerId , 'send_media' ) ) {
toast ( POSTING_MEDIA_NOT_ALLOWED ) ;
return ;
}
this . chatInput . classList . add ( 'is-locked' ) ;
blurActiveElement ( ) ;
this . recorder . start ( ) . then ( ( ) = > {
this . recordCanceled = false ;
this . chatInput . classList . add ( 'is-recording' ) ;
this . recording = true ;
this . updateSendBtn ( ) ;
opusDecodeController . setKeepAlive ( true ) ;
this . recordStartTime = Date . now ( ) ;
const sourceNode : MediaStreamAudioSourceNode = this . recorder . sourceNode ;
const context = sourceNode . context ;
const analyser = context . createAnalyser ( ) ;
sourceNode . connect ( analyser ) ;
//analyser.connect(context.destination);
analyser . fftSize = 32 ;
const frequencyData = new Uint8Array ( analyser . frequencyBinCount ) ;
const max = frequencyData . length * 255 ;
const min = 54 / 150 ;
let r = ( ) = > {
if ( ! this . recording ) return ;
analyser . getByteFrequencyData ( frequencyData ) ;
let sum = 0 ;
frequencyData . forEach ( value = > {
sum += value ;
} ) ;
let percents = Math . min ( 1 , ( sum / max ) + min ) ;
//console.log('frequencyData', frequencyData, percents);
this . recordRippleEl . style . transform = ` scale( ${ percents } ) ` ;
let diff = Date . now ( ) - this . recordStartTime ;
let ms = diff % 1000 ;
let formatted = ( '' + ( diff / 1000 ) ) . toHHMMSS ( ) + ',' + ( '00' + Math . round ( ms / 10 ) ) . slice ( - 2 ) ;
this . recordTimeEl . innerText = formatted ;
window . requestAnimationFrame ( r ) ;
} ;
r ( ) ;
} ) . catch ( ( e : Error ) = > {
switch ( e . name as string ) {
case 'NotAllowedError' : {
toast ( 'Please allow access to your microphone' ) ;
break ;
}
case 'NotReadableError' : {
toast ( e . message ) ;
break ;
}
default :
console . error ( 'Recorder start error:' , e , e . name , e . message ) ;
toast ( e . message ) ;
break ;
}
this . chatInput . classList . remove ( 'is-recording' , 'is-locked' ) ;
} ) ;
}
} ;
private onHelperCancel = ( e? : Event ) = > {
if ( e ) {
cancelEvent ( e ) ;
}
if ( this . willSendWebPage ) {
const lastUrl = this . lastUrl ;
let needReturn = false ;
if ( this . helperType ) {
//if(this.helperFunc) {
this . helperFunc ( ) ;
//}
needReturn = true ;
}
// * restore values
this . lastUrl = lastUrl ;
this . noWebPage = true ;
this . willSendWebPage = null ;
if ( needReturn ) return ;
}
this . clearHelper ( ) ;
this . updateSendBtn ( ) ;
} ;
private onHelperClick = ( e : Event ) = > {
cancelEvent ( e ) ;
if ( ! findUpClassName ( e . target , 'reply-wrapper' ) ) return ;
if ( this . helperType === 'forward' ) {
if ( this . helperWaitingForward ) return ;
this . helperWaitingForward = true ;
const fromId = this . forwardingFromPeerId ;
const mids = this . forwardingMids . slice ( ) ;
const helperFunc = this . helperFunc ;
this . clearHelper ( ) ;
let selected = false ;
new PopupForward ( fromId , mids , ( ) = > {
selected = true ;
} , ( ) = > {
this . helperWaitingForward = false ;
if ( ! selected ) {
helperFunc ( ) ;
}
} ) ;
} else if ( this . helperType === 'reply' ) {
this . chat . setMessageId ( this . replyToMsgId ) ;
} else if ( this . helperType === 'edit' ) {
this . chat . setMessageId ( this . editMsgId ) ;
}
} ;
public clearInput ( canSetDraft = true ) {
this . messageInputField . value = '' ;
if ( isTouchSupported ) {
//this.messageInput.innerText = '';
} else {
//this.attachMessageInputField();
//this.messageInput.innerText = '';
// clear executions
this . canRedoFromHTML = '' ;
this . undoHistory . length = 0 ;
this . executedHistory . length = 0 ;
this . canUndoFromHTML = '' ;
}
let set = false ;
if ( canSetDraft ) {
set = this . setDraft ( undefined , false ) ;
}
/ * i f ( ! s e t ) {
this . onMessageInput ( ) ;
} * /
}
public isInputEmpty() {
return isInputEmpty ( this . messageInput ) ;
}
public updateSendBtn() {
let icon : 'send' | 'record' | 'edit' | 'schedule' ;
const isInputEmpty = this . isInputEmpty ( ) ;
if ( this . editMsgId ) icon = 'edit' ;
else if ( ! this . recorder || this . recording || ! isInputEmpty || this . forwardingMids . length ) icon = this . chat . type === 'scheduled' ? 'schedule' : 'send' ;
else icon = 'record' ;
[ 'send' , 'record' , 'edit' , 'schedule' ] . forEach ( i = > {
this . btnSend . classList . toggle ( i , icon === i ) ;
} ) ;
if ( this . btnScheduled ) {
this . btnScheduled . classList . toggle ( 'show' , isInputEmpty ) ;
}
}
public onMessageSent ( clearInput = true , clearReply? : boolean ) {
if ( this . chat . type !== 'scheduled' ) {
this . appMessagesManager . readAllHistory ( this . chat . peerId , this . chat . threadId , true ) ;
}
this . scheduleDate = undefined ;
this . sendSilent = undefined ;
if ( clearInput ) {
this . lastUrl = '' ;
delete this . noWebPage ;
this . willSendWebPage = null ;
this . clearInput ( ) ;
}
if ( clearReply || clearInput ) {
this . clearHelper ( ) ;
}
this . updateSendBtn ( ) ;
}
public sendMessage ( force = false ) {
if ( this . chat . type === 'scheduled' && ! force && ! this . editMsgId ) {
this . scheduleSending ( ) ;
return ;
}
const entities : MessageEntity [ ] = [ ] ;
const str = getRichValue ( this . messageInputField . input , entities ) ;
//return;
if ( this . editMsgId ) {
this . appMessagesManager . editMessage ( this . chat . getMessage ( this . editMsgId ) , str , {
entities ,
noWebPage : this.noWebPage
} ) ;
} else {
this . appMessagesManager . sendText ( this . chat . peerId , str , {
entities ,
replyToMsgId : this.replyToMsgId ,
threadId : this.chat.threadId ,
noWebPage : this.noWebPage ,
webPage : this.willSendWebPage ,
scheduleDate : this.scheduleDate ,
silent : this.sendSilent ,
clearDraft : true
} ) ;
}
// * wait for sendText set messageId for invokeAfterMsg
if ( this . forwardingMids . length ) {
const mids = this . forwardingMids . slice ( ) ;
const fromPeerId = this . forwardingFromPeerId ;
const peerId = this . chat . peerId ;
const silent = this . sendSilent ;
const scheduleDate = this . scheduleDate ;
setTimeout ( ( ) = > {
this . appMessagesManager . forwardMessages ( peerId , fromPeerId , mids , {
silent ,
scheduleDate : scheduleDate
} ) ;
} , 0 ) ;
}
this . onMessageSent ( ) ;
}
public sendMessageWithDocument ( document : MyDocument | string , force = false , clearDraft = false ) {
document = this . appDocsManager . getDoc ( document ) ;
const flag = document . type === 'sticker' ? 'send_stickers' : ( document . type === 'gif' ? 'send_gifs' : 'send_media' ) ;
if ( this . chat . peerId < 0 && ! this . appChatsManager . hasRights ( this . chat . peerId , flag ) ) {
toast ( POSTING_MEDIA_NOT_ALLOWED ) ;
return ;
}
if ( this . chat . type === 'scheduled' && ! force ) {
this . scheduleSending ( ( ) = > this . sendMessageWithDocument ( document , true ) ) ;
return false ;
}
if ( document ) {
this . appMessagesManager . sendFile ( this . chat . peerId , document , {
isMedia : true ,
replyToMsgId : this.replyToMsgId ,
threadId : this.chat.threadId ,
silent : this.sendSilent ,
scheduleDate : this.scheduleDate ,
clearDraft : clearDraft || undefined
} ) ;
this . onMessageSent ( clearDraft , true ) ;
if ( document . type === 'sticker' ) {
emoticonsDropdown . stickersTab ? . pushRecentSticker ( document ) ;
}
return true ;
}
return false ;
}
/ * p u b l i c s e n d S o m e t h i n g ( c a l l b a c k : ( ) = > v o i d , f o r c e = f a l s e ) {
if ( this . chat . type === 'scheduled' && ! force ) {
this . scheduleSending ( ( ) = > this . sendSomething ( callback , true ) ) ;
return false ;
}
callback ( ) ;
this . onMessageSent ( false , true ) ;
return true ;
} * /
public initMessageEditing ( mid : number ) {
const message = this . chat . getMessage ( mid ) ;
let input = RichTextProcessor . wrapDraftText ( message . message , { entities : message.totalEntities } ) ;
const f = ( ) = > {
// ! костыль
const replyFragment = this . appMessagesManager . wrapMessageForReply ( message , undefined , [ message . mid ] ) ;
this . setTopInfo ( 'edit' , f , 'Editing' , undefined , input , message ) ;
const subtitleEl = this . replyElements . container . querySelector ( '.reply-subtitle' ) ;
subtitleEl . textContent = '' ;
subtitleEl . append ( replyFragment ) ;
this . editMsgId = mid ;
input = undefined ;
} ;
f ( ) ;
}
public initMessagesForward ( fromPeerId : number , mids : number [ ] ) {
const f = ( ) = > {
//const peerTitles: string[]
const smth : Set < string | number > = new Set ( mids . map ( mid = > {
const message = this . appMessagesManager . getMessageByPeer ( fromPeerId , mid ) ;
if ( message . fwd_from && message . fwd_from . from_name && ! message . fromId && ! message . fwdFromId ) {
return message . fwd_from . from_name ;
} else {
return message . fromId ;
}
} ) ) ;
const onlyFirstName = smth . size > 1 ;
const peerTitles = [ . . . smth ] . map ( smth = > {
return typeof ( smth ) === 'number' ?
this . appPeersManager . getPeerTitle ( smth , true , onlyFirstName ) :
( onlyFirstName ? smth . split ( ' ' ) [ 0 ] : smth ) ;
} ) ;
const title = peerTitles . length < 3 ? peerTitles . join ( ' and ' ) : peerTitles [ 0 ] + ' and ' + ( peerTitles . length - 1 ) + ' others' ;
const firstMessage = this . appMessagesManager . getMessageByPeer ( fromPeerId , mids [ 0 ] ) ;
let usingFullAlbum = true ;
if ( firstMessage . grouped_id ) {
const albumMids = this . appMessagesManager . getMidsByMessage ( firstMessage ) ;
if ( albumMids . length !== mids . length || albumMids . find ( mid = > ! mids . includes ( mid ) ) ) {
usingFullAlbum = false ;
}
}
const replyFragment = this . appMessagesManager . wrapMessageForReply ( firstMessage , undefined , mids ) ;
if ( usingFullAlbum || mids . length === 1 ) {
this . setTopInfo ( 'forward' , f , title ) ;
// ! костыль
const subtitleEl = this . replyElements . container . querySelector ( '.reply-subtitle' ) ;
subtitleEl . textContent = '' ;
subtitleEl . append ( replyFragment ) ;
} else {
this . setTopInfo ( 'forward' , f , title , mids . length + ' ' + ( mids . length > 1 ? 'forwarded messages' : 'forwarded message' ) ) ;
}
this . forwardingMids = mids . slice ( ) ;
this . forwardingFromPeerId = fromPeerId ;
} ;
f ( ) ;
}
public initMessageReply ( mid : number ) {
const message = this . chat . getMessage ( mid ) ;
const f = ( ) = > {
this . setTopInfo ( 'reply' , f , this . appPeersManager . getPeerTitle ( message . fromId , true ) , message . message , undefined , message ) ;
this . replyToMsgId = mid ;
} ;
f ( ) ;
}
public clearHelper ( type ? : ChatInputHelperType ) {
if ( this . helperType === 'edit' && type !== 'edit' ) {
this . clearInput ( ) ;
}
if ( type ) {
this . lastUrl = '' ;
delete this . noWebPage ;
this . willSendWebPage = null ;
}
this . replyToMsgId = undefined ;
this . forwardingMids . length = 0 ;
this . forwardingFromPeerId = 0 ;
this . editMsgId = undefined ;
this . helperType = this . helperFunc = undefined ;
if ( this . chat . container . classList . contains ( 'is-helper-active' ) ) {
appNavigationController . removeByType ( 'input-helper' ) ;
this . chat . container . classList . remove ( 'is-helper-active' ) ;
}
}
public setInputValue ( value : string , clear = true , focus = true ) {
clear && this . clearInput ( ) ;
this . messageInputField . value = value || '' ;
window . requestAnimationFrame ( ( ) = > {
focus && placeCaretAtEnd ( this . messageInput ) ;
this . messageInput . scrollTop = this . messageInput . scrollHeight ;
} ) ;
}
public setTopInfo ( type : ChatInputHelperType , callerFunc : ( ) = > void , title = '' , subtitle = '' , input? : string , message? : any ) {
if ( type !== 'webpage' ) {
this . clearHelper ( type ) ;
this . helperType = type ;
this . helperFunc = callerFunc ;
}
if ( this . replyElements . container . lastElementChild . tagName === 'DIV' ) {
this . replyElements . container . lastElementChild . remove ( ) ;
this . replyElements . container . append ( wrapReply ( title , subtitle , message ) ) ;
}
this . chat . container . classList . add ( 'is-helper-active' ) ;
/ * c o n s t s c r o l l = a p p I m M a n a g e r . s c r o l l a b l e ;
if ( scroll . isScrolledDown && ! scroll . scrollLocked && ! appImManager . messagesQueuePromise && ! appImManager . setPeerPromise ) {
scroll . scrollTo ( scroll . scrollHeight , 'top' , true , true , 200 ) ;
} * /
if ( ! isMobile ) {
appNavigationController . pushItem ( {
type : 'input-helper' ,
onPop : ( ) = > {
this . onHelperCancel ( ) ;
}
} ) ;
}
if ( input !== undefined ) {
this . setInputValue ( input ) ;
}
setTimeout ( ( ) = > {
this . updateSendBtn ( ) ;
} , 0 ) ;
}
// public saveScroll() {
// this.scrollTop = this.chat.bubbles.scrollable.container.scrollTop;
// this.scrollOffsetTop = this.chatInput.offsetTop;
// }
// public restoreScroll() {
// if(this.chatInput.style.display) return;
// //console.log('input resize', offsetTop, this.chatInput.offsetTop);
// let newOffsetTop = this.chatInput.offsetTop;
// let container = this.chat.bubbles.scrollable.container;
// let scrollTop = container.scrollTop;
// let clientHeight = container.clientHeight;
// let maxScrollTop = container.scrollHeight;
// if(newOffsetTop < this.scrollOffsetTop) {
// this.scrollDiff = this.scrollOffsetTop - newOffsetTop;
// container.scrollTop += this.scrollDiff;
// } else if(scrollTop !== this.scrollTop) {
// let endDiff = maxScrollTop - (scrollTop + clientHeight);
// if(endDiff < this.scrollDiff/* && false */) {
// //container.scrollTop -= endDiff;
// } else {
// container.scrollTop -= this.scrollDiff;
// }
// }
// }
}