diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 3c9a9585..b6a9a913 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -252,6 +252,7 @@ export default class ChatInput { public sendAsPeerId: PeerId; private replyInTopicOverlay: HTMLDivElement; + private restoreInputLock: () => void; constructor( private chat: Chat, @@ -368,12 +369,18 @@ export default class ChatInput { this.inputContainer.append(c); } + private createButtonIcon(...args: Parameters) { + const button = ButtonIcon(...args); + button.tabIndex = -1; + return button; + } + public constructPeerHelpers() { this.replyElements.container = document.createElement('div'); this.replyElements.container.classList.add('reply-wrapper'); - this.replyElements.iconBtn = ButtonIcon(''); - this.replyElements.cancelBtn = ButtonIcon('close reply-cancel', {noRipple: true}); + this.replyElements.iconBtn = this.createButtonIcon(''); + this.replyElements.cancelBtn = this.createButtonIcon('close reply-cancel', {noRipple: true}); this.replyElements.container.append(this.replyElements.iconBtn, this.replyElements.cancelBtn); @@ -501,7 +508,7 @@ export default class ChatInput { this.replyInTopicOverlay.classList.add('reply-in-topic-overlay', 'hide'); this.replyInTopicOverlay.append(i18n('Chat.Input.ReplyToAnswer')); - this.btnToggleEmoticons = ButtonIcon('none toggle-emoticons', {noRipple: true}); + this.btnToggleEmoticons = this.createButtonIcon('none toggle-emoticons', {noRipple: true}); this.inputMessageContainer = document.createElement('div'); this.inputMessageContainer.classList.add('input-message-container'); @@ -530,7 +537,7 @@ export default class ChatInput { }); }, {listenerSetter: this.listenerSetter}); - this.btnScheduled = ButtonIcon('scheduled btn-scheduled float hide', {noRipple: true}); + this.btnScheduled = this.createButtonIcon('scheduled btn-scheduled float hide', {noRipple: true}); attachClickEvent(this.btnScheduled, (e) => { this.appImManager.openScheduled(this.chat.peerId); @@ -554,7 +561,7 @@ export default class ChatInput { }); }); - this.btnToggleReplyMarkup = ButtonIcon('botcom toggle-reply-markup float hide', {noRipple: true}); + this.btnToggleReplyMarkup = this.createButtonIcon('botcom toggle-reply-markup float hide', {noRipple: true}); this.replyKeyboard = new ReplyKeyboard({ appendTo: this.rowsWrapper, listenerSetter: this.listenerSetter, @@ -780,7 +787,7 @@ export default class ChatInput { this.inlineHelper = new InlineHelper(this.rowsWrapper, this.autocompleteHelperController, this.chat, this.managers); this.rowsWrapper.append(this.newMessageWrapper); - this.btnCancelRecord = ButtonIcon('binfilled btn-circle btn-record-cancel chat-secondary-button'); + this.btnCancelRecord = this.createButtonIcon('binfilled btn-circle btn-record-cancel chat-secondary-button'); this.btnSendContainer = document.createElement('div'); this.btnSendContainer.classList.add('btn-send-container'); @@ -788,7 +795,7 @@ export default class ChatInput { this.recordRippleEl = document.createElement('div'); this.recordRippleEl.classList.add('record-ripple'); - this.btnSend = ButtonIcon('none btn-circle btn-send animated-button-icon'); + this.btnSend = this.createButtonIcon('none btn-circle btn-send animated-button-icon'); this.btnSend.insertAdjacentHTML('afterbegin', ` @@ -1591,7 +1598,10 @@ export default class ChatInput { return; } + const oldKey = i.key; i.compareAndUpdate({key}); + + return {oldKey, newKey: key}; } private filterAttachMenuButtons() { @@ -1617,15 +1627,23 @@ export default class ChatInput { chatInput.classList.remove('no-transition'); } - this.updateMessageInputPlaceholder(placeholderKey); + const isEditingAndLocked = canSend && !canSendPlain && this.restoreInputLock; - if(!canSend || !canSendPlain) { + !isEditingAndLocked && this.updateMessageInputPlaceholder(placeholderKey); + + if(isEditingAndLocked) { + this.restoreInputLock = () => { + this.updateMessageInputPlaceholder(placeholderKey); + this.messageInput.contentEditable = 'false'; + }; + } else if(!canSend || !canSendPlain) { messageInput.contentEditable = 'false'; if(!canSendPlain) { this.messageInputField.onFakeInput(undefined, true); } } else { + this.restoreInputLock = undefined; messageInput.contentEditable = 'true'; this.setDraft(undefined, false); @@ -1650,6 +1668,7 @@ export default class ChatInput { withLinebreaks: true }); + this.messageInputField.input.tabIndex = -1; 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; @@ -2354,7 +2373,7 @@ export default class ChatInput { foundHelper = this.inlineHelper; if(!this.btnPreloader) { - this.btnPreloader = ButtonIcon('none btn-preloader float show disable-hover', {noRipple: true}); + this.btnPreloader = this.createButtonIcon('none btn-preloader float show disable-hover', {noRipple: true}); putPreloader(this.btnPreloader, true); this.inputMessageContainer.parentElement.insertBefore(this.btnPreloader, this.inputMessageContainer.nextSibling); } else { @@ -2901,12 +2920,27 @@ export default class ChatInput { let input = wrapDraftText(message.message, {entities: message.totalEntities, wrappingForPeerId: this.chat.peerId}); const f = async() => { + let restoreInputLock: () => void; + if(!this.messageInput.isContentEditable) { + const placeholderKey = await this.getPlaceholderKey(true); + const {contentEditable} = this.messageInput; + this.messageInput.contentEditable = 'true'; + const {oldKey} = this.updateMessageInputPlaceholder(placeholderKey); + + restoreInputLock = () => { + this.messageInput.contentEditable = contentEditable; + this.updateMessageInputPlaceholder(oldKey); + }; + } + const replyFragment = await wrapMessageForReply({message, usingMids: [message.mid]}); this.setTopInfo('edit', f, i18n('AccDescrEditing'), replyFragment, input, message); this.editMsgId = mid; this.editMessage = message; input = undefined; + + this.restoreInputLock = restoreInputLock; }; f(); } @@ -3084,6 +3118,11 @@ export default class ChatInput { this.editMsgId = this.editMessage = undefined; this.helperType = this.helperFunc = undefined; + if(this.restoreInputLock) { + this.restoreInputLock?.(); + this.restoreInputLock = undefined; + } + if(this.chat.container.classList.contains('is-helper-active')) { appNavigationController.removeByType('input-helper'); this.chat.container.classList.remove('is-helper-active'); @@ -3139,7 +3178,7 @@ export default class ChatInput { const oldReply = replyParent.lastElementChild.previousElementSibling; const haveReply = oldReply.classList.contains('reply'); - this.replyElements.iconBtn.replaceWith(this.replyElements.iconBtn = ButtonIcon((type === 'webpage' ? 'link' : type) + ' active reply-icon', {noRipple: true})); + this.replyElements.iconBtn.replaceWith(this.replyElements.iconBtn = this.createButtonIcon((type === 'webpage' ? 'link' : type) + ' active reply-icon', {noRipple: true})); const {container} = wrapReply(title, subtitle, this.chat.animationGroup, message); if(haveReply) { oldReply.replaceWith(container); diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index 38351cd0..cad08237 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -209,7 +209,7 @@ export class AppSidebarLeft extends SidebarSlider { }); }, verify: () => App.isMainDomain - }, { + }, /* { icon: 'char w', text: 'ChatList.Menu.SwitchTo.Webogram', onClick: () => { @@ -218,7 +218,7 @@ export class AppSidebarLeft extends SidebarSlider { }); }, verify: () => App.isMainDomain - }, { + }, */ { icon: 'download', text: 'PWA.Install', onClick: () => { diff --git a/src/components/wrappers/sticker.ts b/src/components/wrappers/sticker.ts index a561e7ef..2b8dd07c 100644 --- a/src/components/wrappers/sticker.ts +++ b/src/components/wrappers/sticker.ts @@ -45,6 +45,7 @@ import PopupStickers from '../popups/stickers'; import {hideToast, toastNew} from '../toast'; import wrapStickerAnimation from './stickerAnimation'; import framesCache from '../../helpers/framesCache'; +import {IS_MOBILE} from '../../environment/userAgent'; // https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp#L40 export const STICKER_EFFECT_MULTIPLIER = 1 + 0.245 * 2; @@ -753,6 +754,11 @@ export async function onEmojiStickerClick({event, container, managers, peerId, m return; } + const activeAnimations: Set<{}> = (container as any).activeAnimations ??= new Set(); + if(activeAnimations.size >= (IS_MOBILE ? 3 : 5)) { + return; + } + const doc = await managers.appStickersManager.getAnimatedEmojiSticker(emoji, true); if(!doc) { return; @@ -763,7 +769,7 @@ export async function onEmojiStickerClick({event, container, managers, peerId, m v: 1 }; - const sendInteractionThrottled: () => void = (container as any).sendInteractionThrottled = throttle(() => { + const sendInteractionThrottled: () => void = (container as any).sendInteractionThrottled ??= throttle(() => { const length = data.a.length; if(!length) { return; @@ -789,6 +795,9 @@ export async function onEmojiStickerClick({event, container, managers, peerId, m data.a.length = 0; }, 1000, false); + const o = {}; + activeAnimations.add(o); + const isOut = bubble ? bubble.classList.contains('is-out') : undefined; const {animationDiv} = wrapStickerAnimation({ doc, @@ -797,7 +806,10 @@ export async function onEmojiStickerClick({event, container, managers, peerId, m size: 360, target: container, play: true, - withRandomOffset: true + withRandomOffset: true, + onUnmount: () => { + activeAnimations.delete(o); + } }); if(isOut !== undefined && !isOut) { diff --git a/src/components/wrappers/stickerAnimation.ts b/src/components/wrappers/stickerAnimation.ts index 6b727297..94cb91c5 100644 --- a/src/components/wrappers/stickerAnimation.ts +++ b/src/components/wrappers/stickerAnimation.ts @@ -30,7 +30,8 @@ export default function wrapStickerAnimation({ fullThumb, withRandomOffset, relativeEffect, - loopEffect + loopEffect, + onUnmount }: { size: number, doc: MyDocument, @@ -43,7 +44,8 @@ export default function wrapStickerAnimation({ fullThumb?: PhotoSize | Extract, withRandomOffset?: boolean, relativeEffect?: boolean, - loopEffect?: boolean + loopEffect?: boolean, + onUnmount?: () => void }) { const animationDiv = document.createElement('div'); animationDiv.classList.add('emoji-animation'); @@ -58,6 +60,7 @@ export default function wrapStickerAnimation({ animation?.remove(); animationDiv.remove(); appImManager.chat.bubbles.scrollable.container.removeEventListener('scroll', onScroll); + onUnmount?.(); }; const middlewareHelper = middleware?.create() ?? getMiddleware(); diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index d633cf05..4b34e6c7 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -1061,13 +1061,14 @@ export class AppImManager extends EventListenerBase<{ if( chat?.input?.messageInput && - e.target !== chat.input.messageInput && + target !== chat.input.messageInput && target.tagName !== 'INPUT' && !target.isContentEditable && !IS_TOUCH_SUPPORTED && (!mediaSizes.isMobile || this.tabId === APP_TABS.CHAT) && !chat.selection.isSelecting && - !chat.input.recording + !chat.input.recording && + chat.input.messageInput.isContentEditable ) { chat.input.messageInput.focus(); placeCaretAtEnd(chat.input.messageInput); diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 3286b315..49b45ce9 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -3440,9 +3440,15 @@ export class AppMessagesManager extends AppManager { return true; } - const canEditMessageInPeer = this.appPeersManager.isBroadcast(message.peerId) ? - this.appChatsManager.hasRights(message.peerId.toChatId(), 'edit_messages') : - message.pFlags.out; + const {peerId} = message; + + const canEditMessageInPeer = this.appPeersManager.isBroadcast(peerId) ? + this.appChatsManager.hasRights(peerId.toChatId(), 'edit_messages') : + ( + peerId.isAnyChat() && kind === 'text' ? + this.appChatsManager.hasRights(peerId.toChatId(), 'send_plain') || this.appChatsManager.hasRights(peerId.toChatId(), 'send_media') : + true + ) && message.pFlags.out; if( !canEditMessageInPeer || (