diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 09e14423..96373001 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -43,7 +43,7 @@ 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 { isMobile, isMobileSafari } from '../../helpers/userAgent'; import { i18n } from '../../lib/langPack'; import { generateTail } from './bubbles'; import findUpClassName from '../../helpers/dom/findUpClassName'; @@ -64,6 +64,8 @@ import CommandsHelper from './commandsHelper'; import AutocompleteHelperController from './autocompleteHelperController'; import AutocompleteHelper from './autocompleteHelper'; import MentionsHelper from './mentionsHelper'; +import fixSafariStickyInput from '../../helpers/dom/fixSafariStickyInput'; +import { emojiFromCodePoints } from '../../vendor/emoji'; const RECORD_MIN_TIME = 500; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; @@ -1142,15 +1144,15 @@ export default class ChatInput { this.updateSendBtn(); }; - public insertAtCaret(insertText: string, insertEntity?: MessageEntity) { + public insertAtCaret(insertText: string, insertEntity?: MessageEntity, isHelper = true) { const {value: fullValue, caretPos, entities} = getRichValueWithCaret(this.messageInput); const pos = caretPos >= 0 ? caretPos : fullValue.length; const prefix = fullValue.substr(0, pos); const suffix = fullValue.substr(pos); - const matches = prefix.match(ChatInput.AUTO_COMPLETE_REG_EXP); + const matches = isHelper ? prefix.match(ChatInput.AUTO_COMPLETE_REG_EXP) : null; - const matchIndex = matches.index + (matches[0].length - matches[2].length); + const matchIndex = matches ? matches.index + (matches[0].length - matches[2].length) : prefix.length; const newPrefix = prefix.slice(0, matchIndex); const newValue = newPrefix + insertText + suffix; @@ -1173,7 +1175,7 @@ export default class ChatInput { }); // add offset to entities next to emoji - const diff = insertLength - matches[2].length; + const diff = insertLength - (matches ? matches[2].length : prefix.length); entities.forEach(entity => { if(entity.offset >= matchIndex) { entity.offset += diff; @@ -1423,7 +1425,17 @@ export default class ChatInput { }; public clearInput(canSetDraft = true) { - this.messageInputField.value = ''; + if(document.activeElement === this.messageInput && isMobileSafari) { + const i = document.createElement('input'); + document.body.append(i); + fixSafariStickyInput(i); + this.messageInputField.value = ''; + fixSafariStickyInput(this.messageInput); + i.remove(); + } else { + this.messageInputField.value = ''; + } + if(isTouchSupported) { //this.messageInput.innerText = ''; } else { @@ -1477,6 +1489,14 @@ export default class ChatInput { this.scheduleDate = undefined; this.sendSilent = undefined; + const value = this.messageInputField.value; + const entities = RichTextProcessor.parseEntities(value); + const emojiEntities: MessageEntity.messageEntityEmoji[] = entities.filter(entity => entity._ === 'messageEntityEmoji') as any; + emojiEntities.forEach(entity => { + const emoji = emojiFromCodePoints(entity.unicode); + this.appEmojiManager.pushRecentEmoji(emoji); + }); + if(clearInput) { this.lastUrl = ''; delete this.noWebPage; diff --git a/src/components/emoticonsDropdown/tabs/emoji.ts b/src/components/emoticonsDropdown/tabs/emoji.ts index 10ceeb6a..5532334e 100644 --- a/src/components/emoticonsDropdown/tabs/emoji.ts +++ b/src/components/emoticonsDropdown/tabs/emoji.ts @@ -15,6 +15,7 @@ import Config from "../../../lib/config"; import { i18n, LangPackKey } from "../../../lib/langPack"; import { RichTextProcessor } from "../../../lib/richtextprocessor"; import rootScope from "../../../lib/rootScope"; +import { emojiFromCodePoints } from "../../../vendor/emoji"; import { putPreloader } from "../../misc"; import Scrollable from "../../scrollable"; import StickyIntersector from "../../stickyIntersector"; @@ -53,10 +54,10 @@ export function appendEmoji(emoji: string, container: HTMLElement, prepend = fal if(spanEmoji.firstElementChild && !RichTextProcessor.emojiSupported) { const image = spanEmoji.firstElementChild as HTMLImageElement; - image.setAttribute('loading', 'lazy'); - + const url = image.src; if(!loadedURLs.has(url)) { + image.setAttribute('loading', 'lazy'); const placeholder = document.createElement('span'); placeholder.classList.add('emoji-placeholder'); @@ -89,6 +90,8 @@ export function appendEmoji(emoji: string, container: HTMLElement, prepend = fal } export function getEmojiFromElement(element: HTMLElement) { + if(!findUpClassName(element, 'super-emoji')) return ''; + if(element.nodeType === 3) return element.nodeValue; if(element.tagName === 'SPAN' && !element.classList.contains('emoji') && element.firstElementChild) { element = element.firstElementChild as HTMLElement; @@ -170,7 +173,7 @@ export default class EmojiTab implements EmoticonsTab { console.log('append emoji', emoji, emojiUnicode(emoji)); } */ - let emoji = unified.split('-').reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), ''); + let emoji = emojiFromCodePoints(unified); //if(emoji.includes('🕵')) { //console.log('toCodePoints', toCodePoints(emoji)); //emoji = emoji.replace(/(\u200d[\u2640\u2642\u2695])(?!\ufe0f)/, '\ufe0f$1'); @@ -236,64 +239,75 @@ export default class EmojiTab implements EmoticonsTab { this.content.addEventListener('click', this.onContentClick); this.stickyIntersector = EmoticonsDropdown.menuOnClick(menu, emojiScroll); this.init = null; - } - onContentClick = (e: MouseEvent) => { - cancelEvent(e); - let target = e.target as HTMLElement; - //if(target.tagName !== 'SPAN') return; + rootScope.addEventListener('emoji_recent', (emoji) => { + const children = Array.from(this.recentItemsDiv.children) as HTMLElement[]; + for(let i = 0, length = children.length; i < length; ++i) { + const el = children[i]; + const _emoji = getEmojiFromElement(el); + if(emoji === _emoji) { + if(i === 0) { + return; + } - if(target.tagName === 'SPAN' && !target.classList.contains('emoji')) { - target = findUpClassName(target, 'super-emoji'); - if(!target) { - return; + el.remove(); + } } - target = target.firstChild as HTMLElement; - } else if(target.tagName === 'DIV') return; + appendEmoji(emoji, this.recentItemsDiv, true); + this.recentItemsDiv.parentElement.classList.remove('hide'); + }); + } - // set selection range - const savedRange = isTouchSupported ? undefined : emoticonsDropdown.getSavedRange(); - let sel: Selection; - if(savedRange) { - sel = document.getSelection(); - sel.removeAllRanges(); - sel.addRange(savedRange); + onContentClick = (e: MouseEvent) => { + cancelEvent(e); + + const emoji = getEmojiFromElement(e.target as HTMLElement); + if(!emoji) { + return; } - const html = RichTextProcessor.emojiSupported ? - (target.nodeType === 3 ? target.nodeValue : target.innerHTML) : - target.outerHTML; + const messageInput = appImManager.chat.input.messageInput; + let inputHTML = messageInput.innerHTML; + + const html = RichTextProcessor.wrapEmojiText(emoji); + let inserted = false; + if(window.getSelection) { + const savedRange = isTouchSupported ? undefined : emoticonsDropdown.getSavedRange(); + let sel = window.getSelection(); + if(savedRange) { + sel.removeAllRanges(); + sel.addRange(savedRange); + } - if((document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.hasAttribute('contenteditable'))) || - savedRange) { - document.execCommand('insertHTML', true, html); - } else { - appImManager.chat.input.messageInput.innerHTML += html; + if(sel.getRangeAt && sel.rangeCount) { + var el = document.createElement('div'); + el.innerHTML = html; + var node = el.firstChild; + var range = sel.getRangeAt(0); + range.deleteContents(); + //range.insertNode(document.createTextNode(' ')); + range.insertNode(node); + range.setStart(node, 0); + inserted = true; + + setTimeout(() => { + range = document.createRange(); + range.setStartAfter(node); + range.collapse(true); + sel.removeAllRanges(); + sel.addRange(range); + }, 0); + } } - /* if(sel && isTouchSupported) { - sel.removeRange(savedRange); - blurActiveElement(); - } */ - - // Recent - const emoji = getEmojiFromElement(target); - (Array.from(this.recentItemsDiv.children) as HTMLElement[]).forEach((el, idx) => { - const _emoji = getEmojiFromElement(el); - if(emoji === _emoji) { - el.remove(); - } - }); + if(!inserted || messageInput.innerHTML === inputHTML) { + messageInput.insertAdjacentHTML('beforeend', html); + } - appendEmoji(emoji, this.recentItemsDiv, true); - - appEmojiManager.pushRecentEmoji(emoji); - this.recentItemsDiv.parentElement.classList.remove('hide'); - // Append to input const event = new Event('input', {bubbles: true, cancelable: true}); - appImManager.chat.input.messageInput.dispatchEvent(event); + messageInput.dispatchEvent(event); }; onClose() { diff --git a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts index 0da731a4..992c9235 100644 --- a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts +++ b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts @@ -34,7 +34,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { private authorizations: Authorization.authorization[]; protected async init() { - this.container.classList.add('privacy-container'); + this.container.classList.add('dont-u-dare-block-me'); this.setTitle('PrivacySettings'); const SUBTITLE: LangPackKey = 'Loading'; diff --git a/src/helpers/emojiSupport.ts b/src/helpers/emojiSupport.ts new file mode 100644 index 00000000..dc77b719 --- /dev/null +++ b/src/helpers/emojiSupport.ts @@ -0,0 +1,3 @@ +const IS_EMOJI_SUPPORTED = navigator.userAgent.search(/OS X|iPhone|iPad|iOS/i) !== -1/* && false *//* || true */; + +export default IS_EMOJI_SUPPORTED; \ No newline at end of file diff --git a/src/helpers/userAgent.ts b/src/helpers/userAgent.ts index d32848f1..52bfcafb 100644 --- a/src/helpers/userAgent.ts +++ b/src/helpers/userAgent.ts @@ -9,14 +9,6 @@ export const isApple = navigator.userAgent.search(/OS X|iPhone|iPad|iOS/i) !== - export const isAndroid = navigator.userAgent.toLowerCase().indexOf('android') !== -1; export const isChromium = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); -/** - * Returns true when run in WebKit derived browsers. - * This is used as a workaround for a memory leak in Safari caused by using Transferable objects to - * transfer data between WebWorkers and the main thread. - * https://github.com/mapbox/mapbox-gl-js/issues/8771 - * - * This should be removed once the underlying Safari issue is fixed. - */ export const ctx = typeof(window) !== 'undefined' ? window : self; // https://stackoverflow.com/a/58065241 diff --git a/src/index.ts b/src/index.ts index eaad18cf..8244ea69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,9 +6,11 @@ import App from './config/app'; import blurActiveElement from './helpers/dom/blurActiveElement'; +import { cancelEvent } from './helpers/dom/cancelEvent'; import findUpClassName from './helpers/dom/findUpClassName'; import fixSafariStickyInput from './helpers/dom/fixSafariStickyInput'; import loadFonts from './helpers/dom/loadFonts'; +import IS_EMOJI_SUPPORTED from './helpers/emojiSupport'; import { isMobileSafari } from './helpers/userAgent'; import './materialize.scss'; import './scss/style.scss'; @@ -142,6 +144,16 @@ console.timeEnd('get storage1'); */ toggleResizeMode(); }); + if(userAgent.isFirefox && !IS_EMOJI_SUPPORTED) { + document.addEventListener('dragstart', (e) => { + const target = e.target as HTMLElement; + if(target.tagName === 'IMG' && target.classList.contains('emoji')) { + cancelEvent(e); + return false; + } + }); + } + if(userAgent.isApple) { if(userAgent.isSafari) { document.documentElement.classList.add('is-safari'); diff --git a/src/lib/appManagers/appEmojiManager.ts b/src/lib/appManagers/appEmojiManager.ts index 6e50a6b4..de5a9a6a 100644 --- a/src/lib/appManagers/appEmojiManager.ts +++ b/src/lib/appManagers/appEmojiManager.ts @@ -10,6 +10,7 @@ import { validateInitObject } from "../../helpers/object"; import I18n from "../langPack"; import { isObject } from "../mtproto/bin_utils"; import apiManager from "../mtproto/mtprotoworker"; +import rootScope from "../rootScope"; import SearchIndex from "../searchIndex"; import stateStorage from "../stateStorage"; import appStateManager from "./appStateManager"; @@ -226,6 +227,7 @@ export class AppEmojiManager { } appStateManager.pushToState('recentEmoji', recent); + rootScope.dispatchEvent('emoji_recent', emoji); }); } } diff --git a/src/lib/richtextprocessor.ts b/src/lib/richtextprocessor.ts index 74ac1252..bd00a776 100644 --- a/src/lib/richtextprocessor.ts +++ b/src/lib/richtextprocessor.ts @@ -17,6 +17,7 @@ import { MessageEntity } from '../layer'; import { encodeEntities } from '../helpers/string'; import { isSafari } from '../helpers/userAgent'; import { MOUNT_CLASS_TO } from '../config/debug'; +import IS_EMOJI_SUPPORTED from '../helpers/emojiSupport'; const EmojiHelper = { emojiMap: (code: string) => { return code; }, @@ -114,7 +115,7 @@ for(let i in markdownEntities) { } namespace RichTextProcessor { - export const emojiSupported = navigator.userAgent.search(/OS X|iPhone|iPad|iOS/i) !== -1/* && false *//* || true */; + export const emojiSupported = IS_EMOJI_SUPPORTED; export function getEmojiSpritesheetCoords(emojiCode: string) { let unified = encodeEmoji(emojiCode).replace(/-?fe0f/g, ''); diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index 2ca316e0..66c485e6 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -123,6 +123,8 @@ export type BroadcastEvents = { 'push_init': PushSubscriptionNotify, 'push_subscribe': PushSubscriptionNotify, 'push_unsubscribe': PushSubscriptionNotify, + + 'emoji_recent': string }; export class RootScope extends EventListenerBase<{ diff --git a/src/scss/partials/_avatar.scss b/src/scss/partials/_avatar.scss index c940c305..93bbfd26 100644 --- a/src/scss/partials/_avatar.scss +++ b/src/scss/partials/_avatar.scss @@ -87,6 +87,7 @@ avatar-element { width: var(--size) !important; height: var(--size) !important; border-radius: inherit !important; + display: block; // fix Firefox below empty space &.fade-in { animation: fade-in-opacity .2s ease forwards; diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index 350e75a5..9a15b04c 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -1000,7 +1000,7 @@ } } -.privacy-container { +.dont-u-dare-block-me { .sidebar-left-section.no-delimiter { padding-top: .75rem; } diff --git a/src/scss/style.scss b/src/scss/style.scss index 12a088fc..93b9d6d0 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -1302,6 +1302,7 @@ middle-ellipsis-element { height: 1.75rem; border-radius: 50%; background-color: var(--light-secondary-text-color); + pointer-events: none; @include animation-level(2) { opacity: 0; diff --git a/src/vendor/emoji/index.ts b/src/vendor/emoji/index.ts index 6a681680..bc79efee 100644 --- a/src/vendor/emoji/index.ts +++ b/src/vendor/emoji/index.ts @@ -38,4 +38,8 @@ export function toCodePoints(unicodeSurrogates: string): Array { export function getEmojiToneIndex(input: string) { let match = input.match(/[\uDFFB-\uDFFF]/); return match ? 5 - (57343 - match[0].charCodeAt(0)) : 0; +} + +export function emojiFromCodePoints(codePoints: string) { + return codePoints.split('-').reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), ''); } \ No newline at end of file