Browse Source

Emoji 13.0

Emoji autocomplete helper
Fix parsing gender emoji
Fix loading first comment
Fix selecting emoji in ESG
Deactivate parallel tabs
Fix bubble reply containers width
Reset pinned dialogs order
master
morethanwords 4 years ago
parent
commit
7dc7dde962
  1. 10
      src/components/appNavigationController.ts
  2. 66
      src/components/chat/autocompleteHelper.ts
  3. 2
      src/components/chat/contextMenu.ts
  4. 37
      src/components/chat/emojiHelper.ts
  5. 60
      src/components/chat/input.ts
  6. 39
      src/components/chat/stickersHelper.ts
  7. 23
      src/components/emoticonsDropdown/index.ts
  8. 83
      src/components/emoticonsDropdown/tabs/emoji.ts
  9. 4
      src/config/app.ts
  10. 36
      src/emoji_test.js
  11. 90
      src/helpers/blur.ts
  12. 25
      src/helpers/dom/attachListNavigation.ts
  13. 54
      src/helpers/dom/getRichElementValue.ts
  14. 4
      src/helpers/dom/getRichValueWithCaret.ts
  15. 46
      src/helpers/dom/setRichFocus.ts
  16. 6
      src/lang.ts
  17. 9
      src/layer.d.ts
  18. 23
      src/lib/appManagers/appDialogsManager.ts
  19. 60
      src/lib/appManagers/appEmojiManager.ts
  20. 36
      src/lib/appManagers/appImManager.ts
  21. 24
      src/lib/appManagers/appMessagesManager.ts
  22. 1
      src/lib/appManagers/appPhotosManager.ts
  23. 33
      src/lib/appManagers/appStateManager.ts
  24. 6
      src/lib/config.ts
  25. 10
      src/lib/mtproto/apiManager.ts
  26. 8
      src/lib/mtproto/mtproto.service.ts
  27. 7
      src/lib/mtproto/mtproto.worker.ts
  28. 11
      src/lib/mtproto/mtprotoworker.ts
  29. 63
      src/lib/mtproto/networker.ts
  30. 11
      src/lib/mtproto/networkerFactory.ts
  31. 56
      src/lib/mtproto/singleInstance.ts
  32. 11
      src/lib/mtproto/transports/tcpObfuscated.ts
  33. 21
      src/lib/richtextprocessor.ts
  34. 2
      src/lib/rootScope.ts
  35. 10
      src/lib/searchIndex.ts
  36. 4
      src/lib/storage.ts
  37. 4
      src/lib/storages/dialogs.ts
  38. 9
      src/scripts/format_jsons.js
  39. 20820
      src/scripts/in/emoji_pretty.json
  40. 7
      src/scripts/in/schema_additional_params.json
  41. 2
      src/scripts/out/countries.json
  42. 2
      src/scripts/out/emoji.json
  43. 5
      src/scss/partials/_chatBubble.scss
  44. 12
      src/scss/partials/_chatEmojiHelper.scss
  45. 6
      src/scss/partials/_chatlist.scss
  46. 22
      src/scss/partials/popups/_instanceDeactivated.scss
  47. 29
      src/scss/style.scss
  48. 3
      src/types.d.ts
  49. 19
      src/vendor/emoji/regex.ts

10
src/components/appNavigationController.ts

@ -12,10 +12,12 @@ import blurActiveElement from "../helpers/dom/blurActiveElement";
import { cancelEvent } from "../helpers/dom/cancelEvent"; import { cancelEvent } from "../helpers/dom/cancelEvent";
export type NavigationItem = { export type NavigationItem = {
type: 'left' | 'right' | 'im' | 'chat' | 'popup' | 'media' | 'menu' | 'esg' | 'multiselect' | 'input-helper' | 'markup' | 'global-search', type: 'left' | 'right' | 'im' | 'chat' | 'popup' | 'media' | 'menu' |
'esg' | 'multiselect' | 'input-helper' | 'autocomplete-helper' | 'markup' | 'global-search',
onPop: (canAnimate: boolean) => boolean | void, onPop: (canAnimate: boolean) => boolean | void,
onEscape?: () => boolean, onEscape?: () => boolean,
noHistory?: boolean, noHistory?: boolean,
noBlurOnPop?: boolean,
}; };
export class AppNavigationController { export class AppNavigationController {
@ -61,9 +63,9 @@ export class AppNavigationController {
if(!item) return; if(!item) return;
if(e.key === 'Escape' && (item.onEscape ? item.onEscape() : true)) { if(e.key === 'Escape' && (item.onEscape ? item.onEscape() : true)) {
cancelEvent(e); cancelEvent(e);
this.back(); this.back(item.type);
} }
}, {capture: true}); }, {capture: true, passive: false});
if(isMobileSafari) { if(isMobileSafari) {
const options = {passive: true}; const options = {passive: true};
@ -117,7 +119,7 @@ export class AppNavigationController {
this.debug && this.log('popstate, navigation:', item, this.navigations); this.debug && this.log('popstate, navigation:', item, this.navigations);
if(good === false) { if(good === false) {
this.pushItem(item); this.pushItem(item);
} else { } else if(!item.noBlurOnPop) {
blurActiveElement(); // no better place for it blurActiveElement(); // no better place for it
} }

66
src/components/chat/autocompleteHelper.ts

@ -4,29 +4,89 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import attachListNavigation from "../../helpers/dom/attachlistNavigation";
import EventListenerBase from "../../helpers/eventListenerBase"; import EventListenerBase from "../../helpers/eventListenerBase";
import { isMobile } from "../../helpers/userAgent";
import rootScope from "../../lib/rootScope"; import rootScope from "../../lib/rootScope";
import appNavigationController, { NavigationItem } from "../appNavigationController";
import SetTransition from "../singleTransition"; import SetTransition from "../singleTransition";
export default class AutocompleteHelper extends EventListenerBase<{ export default class AutocompleteHelper extends EventListenerBase<{
hidden: () => void, hidden: () => void,
visible: () => void, visible: () => void,
}> { }> {
protected hidden = true;
protected container: HTMLElement; protected container: HTMLElement;
protected list: HTMLElement;
protected resetTarget: () => void;
constructor(appendTo: HTMLElement) { constructor(appendTo: HTMLElement,
private listType: 'xy' | 'x' | 'y',
private onSelect: (target: Element) => boolean | void,
private waitForKey?: string
) {
super(false); super(false);
this.container = document.createElement('div'); this.container = document.createElement('div');
this.container.classList.add('autocomplete-helper', 'z-depth-1'); this.container.classList.add('autocomplete-helper', 'z-depth-1');
appendTo.append(this.container); appendTo.append(this.container);
this.attachNavigation();
}
protected onVisible = () => {
const list = this.list;
const {detach, resetTarget} = attachListNavigation({
list,
type: this.listType,
onSelect: this.onSelect,
once: true,
waitForKey: this.waitForKey
});
this.resetTarget = resetTarget;
let navigationItem: NavigationItem;
if(!isMobile) {
navigationItem = {
type: 'autocomplete-helper',
onPop: () => this.toggle(true),
noBlurOnPop: true
};
appNavigationController.pushItem(navigationItem);
}
this.addEventListener('hidden', () => {
this.resetTarget = undefined;
list.innerHTML = '';
detach();
if(navigationItem) {
appNavigationController.removeItem(navigationItem);
}
}, true);
};
protected attachNavigation() {
this.addEventListener('visible', this.onVisible);
} }
public toggle(hide?: boolean) { public toggle(hide?: boolean) {
hide = hide === undefined ? this.container.classList.contains('is-visible') : hide; hide = hide === undefined ? this.container.classList.contains('is-visible') && !this.container.classList.contains('backwards') : hide;
if(this.hidden === hide) {
if(!hide && this.resetTarget) {
this.resetTarget();
}
return;
}
this.hidden = hide;
!this.hidden && this.dispatchEvent('visible'); // fire it before so target will be set
SetTransition(this.container, 'is-visible', !hide, rootScope.settings.animationsEnabled ? 200 : 0, () => { SetTransition(this.container, 'is-visible', !hide, rootScope.settings.animationsEnabled ? 200 : 0, () => {
this.dispatchEvent(hide ? 'hidden' : 'visible'); this.hidden && this.dispatchEvent('hidden');
}); });
} }
} }

2
src/components/chat/contextMenu.ts

@ -340,7 +340,7 @@ export default class ChatContextMenu {
private onCopyClick = () => { private onCopyClick = () => {
if(isSelectionEmpty()) { if(isSelectionEmpty()) {
const mids = this.chat.selection.isSelecting ? [...this.chat.selection.selectedMids] : [this.mid]; const mids = this.chat.selection.isSelecting ? [...this.chat.selection.selectedMids].sort((a, b) => a - b) : [this.mid];
const str = mids.reduce((acc, mid) => { const str = mids.reduce((acc, mid) => {
const message = this.chat.getMessage(mid); const message = this.chat.getMessage(mid);
return acc + (message?.message ? message.message + '\n' : ''); return acc + (message?.message ? message.message + '\n' : '');

37
src/components/chat/emojiHelper.ts

@ -1,41 +1,24 @@
import type ChatInput from "./input"; import type ChatInput from "./input";
import attachListNavigation from "../../helpers/dom/attachlistNavigation";
import { appendEmoji, getEmojiFromElement } from "../emoticonsDropdown/tabs/emoji"; import { appendEmoji, getEmojiFromElement } from "../emoticonsDropdown/tabs/emoji";
import { ScrollableX } from "../scrollable"; import { ScrollableX } from "../scrollable";
import AutocompleteHelper from "./autocompleteHelper"; import AutocompleteHelper from "./autocompleteHelper";
export default class EmojiHelper extends AutocompleteHelper { export default class EmojiHelper extends AutocompleteHelper {
private emojisContainer: HTMLDivElement;
private scrollable: ScrollableX; private scrollable: ScrollableX;
constructor(appendTo: HTMLElement, private chatInput: ChatInput) { constructor(appendTo: HTMLElement, private chatInput: ChatInput) {
super(appendTo); super(appendTo, 'x', (target) => {
this.chatInput.onEmojiSelected(getEmojiFromElement(target as any), true);
});
this.container.classList.add('emoji-helper'); this.container.classList.add('emoji-helper');
this.addEventListener('visible', () => {
const list = this.emojisContainer;
const {detach} = attachListNavigation({
list,
type: 'x',
onSelect: (target) => {
this.chatInput.onEmojiSelected(getEmojiFromElement(target as any), true);
},
once: true
});
this.addEventListener('hidden', () => {
list.innerHTML = '';
detach();
}, true);
});
} }
private init() { private init() {
this.emojisContainer = document.createElement('div'); this.list = document.createElement('div');
this.emojisContainer.classList.add('emoji-helper-emojis', 'super-emojis'); this.list.classList.add('emoji-helper-emojis', 'super-emojis');
this.container.append(this.emojisContainer); this.container.append(this.list);
this.scrollable = new ScrollableX(this.container); this.scrollable = new ScrollableX(this.container);
} }
@ -47,12 +30,16 @@ export default class EmojiHelper extends AutocompleteHelper {
} }
if(emojis.length) { if(emojis.length) {
this.emojisContainer.innerHTML = ''; this.list.innerHTML = '';
emojis.forEach(emoji => { emojis.forEach(emoji => {
appendEmoji(emoji, this.emojisContainer); appendEmoji(emoji, this.list, false, true);
}); });
} }
if(!this.hidden) {
this.scrollable.container.scrollLeft = 0;
}
this.toggle(!emojis.length); this.toggle(!emojis.length);
} }
} }

60
src/components/chat/input.ts

@ -58,8 +58,9 @@ import isSendShortcutPressed from '../../helpers/dom/isSendShortcutPressed';
import placeCaretAtEnd from '../../helpers/dom/placeCaretAtEnd'; import placeCaretAtEnd from '../../helpers/dom/placeCaretAtEnd';
import { MarkdownType, markdownTags } from '../../helpers/dom/getRichElementValue'; import { MarkdownType, markdownTags } from '../../helpers/dom/getRichElementValue';
import getRichValueWithCaret from '../../helpers/dom/getRichValueWithCaret'; import getRichValueWithCaret from '../../helpers/dom/getRichValueWithCaret';
import cleanSearchText from '../../helpers/cleanSearchText';
import EmojiHelper from './emojiHelper'; import EmojiHelper from './emojiHelper';
import setRichFocus from '../../helpers/dom/setRichFocus';
import { toCodePoints } from '../../vendor/emoji';
const RECORD_MIN_TIME = 500; const RECORD_MIN_TIME = 500;
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
@ -974,6 +975,7 @@ export default class ChatInput {
} }
private handleMarkdownShortcut = (e: KeyboardEvent) => { private handleMarkdownShortcut = (e: KeyboardEvent) => {
// console.log('handleMarkdownShortcut', e);
const formatKeys: {[key: string]: MarkdownType} = { const formatKeys: {[key: string]: MarkdownType} = {
'B': 'bold', 'B': 'bold',
'I': 'italic', 'I': 'italic',
@ -1145,14 +1147,55 @@ export default class ChatInput {
public onEmojiSelected = (emoji: string, autocomplete: boolean) => { public onEmojiSelected = (emoji: string, autocomplete: boolean) => {
if(autocomplete) { if(autocomplete) {
const {value: fullValue, caretPos} = getRichValueWithCaret(this.messageInput); const {value: fullValue, caretPos, entities} = getRichValueWithCaret(this.messageInput);
const pos = caretPos >= 0 ? caretPos : fullValue.length; const pos = caretPos >= 0 ? caretPos : fullValue.length;
const suffix = fullValue.substr(pos);
const prefix = fullValue.substr(0, pos); const prefix = fullValue.substr(0, pos);
const suffix = fullValue.substr(pos);
const matches = prefix.match(ChatInput.AUTO_COMPLETE_REG_EXP); const matches = prefix.match(ChatInput.AUTO_COMPLETE_REG_EXP);
console.log(matches);
const idx = matches.index + matches[1].length; const matchIndex = matches.index + (matches[0].length - matches[2].length);
const newPrefix = prefix.slice(0, matchIndex);
const newValue = newPrefix + emoji + suffix;
// merge emojis
const hadEntities = RichTextProcessor.parseEntities(fullValue);
RichTextProcessor.mergeEntities(entities, hadEntities);
const emojiEntity: MessageEntity.messageEntityEmoji = {
_: 'messageEntityEmoji',
offset: 0,
length: emoji.length,
unicode: toCodePoints(emoji).join('-')
};
const addEntities: MessageEntity[] = [emojiEntity];
emojiEntity.offset = matchIndex;
addEntities.push({
_: 'messageEntityCaret',
length: 0,
offset: emojiEntity.offset + emojiEntity.length
});
// add offset to entities next to emoji
const diff = emojiEntity.length - matches[2].length;
entities.forEach(entity => {
if(entity.offset >= emojiEntity.offset) {
entity.offset += diff;
}
});
RichTextProcessor.mergeEntities(entities, addEntities);
//const saveExecuted = this.prepareDocumentExecute();
this.messageInputField.value = RichTextProcessor.wrapDraftText(newValue, {entities});
const caret = this.messageInput.querySelector('.composer-sel');
setRichFocus(this.messageInput, caret);
caret.remove();
//saveExecuted();
//document.execCommand('insertHTML', true, RichTextProcessor.wrapEmojiText(emoji));
//const str = //const str =
@ -1176,8 +1219,8 @@ export default class ChatInput {
}; };
private checkAutocomplete(value?: string, caretPos?: number) { private checkAutocomplete(value?: string, caretPos?: number) {
return; //return;
if(value === undefined) { if(value === undefined) {
const r = getRichValueWithCaret(this.messageInputField.input, false); const r = getRichValueWithCaret(this.messageInputField.input, false);
value = r.value; value = r.value;
@ -1260,7 +1303,8 @@ export default class ChatInput {
} }
this.appEmojiManager.getBothEmojiKeywords().then(() => { this.appEmojiManager.getBothEmojiKeywords().then(() => {
const emojis = this.appEmojiManager.searchEmojis(matches[2]); const q = matches[2].replace(/^:/, '');
const emojis = this.appEmojiManager.searchEmojis(q);
this.emojiHelper.renderEmojis(emojis); this.emojiHelper.renderEmojis(emojis);
//console.log(emojis); //console.log(emojis);
}); });

39
src/components/chat/stickersHelper.ts

@ -4,7 +4,6 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import attachListNavigation from "../../helpers/dom/attachlistNavigation";
import { MyDocument } from "../../lib/appManagers/appDocsManager"; import { MyDocument } from "../../lib/appManagers/appDocsManager";
import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager";
import appStickersManager from "../../lib/appManagers/appStickersManager"; import appStickersManager from "../../lib/appManagers/appStickersManager";
@ -15,33 +14,17 @@ import Scrollable from "../scrollable";
import AutocompleteHelper from "./autocompleteHelper"; import AutocompleteHelper from "./autocompleteHelper";
export default class StickersHelper extends AutocompleteHelper { export default class StickersHelper extends AutocompleteHelper {
private stickersContainer: HTMLElement;
private scrollable: Scrollable; private scrollable: Scrollable;
private superStickerRenderer: SuperStickerRenderer; private superStickerRenderer: SuperStickerRenderer;
private lazyLoadQueue: LazyLoadQueue; private lazyLoadQueue: LazyLoadQueue;
private lastEmoticon = ''; private lastEmoticon = '';
constructor(appendTo: HTMLElement) { constructor(appendTo: HTMLElement) {
super(appendTo); super(appendTo, 'xy', (target) => {
EmoticonsDropdown.onMediaClick({target}, true);
}, 'ArrowUp');
this.container.classList.add('stickers-helper'); this.container.classList.add('stickers-helper');
this.addEventListener('visible', () => {
const list = this.stickersContainer;
const {detach} = attachListNavigation({
list,
type: 'xy',
onSelect: (target) => {
EmoticonsDropdown.onMediaClick({target}, true);
},
once: true
});
this.addEventListener('hidden', () => {
list.innerHTML = '';
detach();
}, true);
});
} }
public checkEmoticon(emoticon: string) { public checkEmoticon(emoticon: string) {
@ -73,7 +56,7 @@ export default class StickersHelper extends AutocompleteHelper {
this.init = null; this.init = null;
} }
const container = this.stickersContainer.cloneNode() as HTMLElement; const container = this.list.cloneNode() as HTMLElement;
let ready: Promise<void>; let ready: Promise<void>;
@ -92,8 +75,12 @@ export default class StickersHelper extends AutocompleteHelper {
} }
ready.then(() => { ready.then(() => {
this.stickersContainer.replaceWith(container); if(!this.hidden) {
this.stickersContainer = container; this.scrollable.container.scrollTop = 0;
}
this.list.replaceWith(container);
this.list = container;
this.toggle(!stickers.length); this.toggle(!stickers.length);
this.scrollable.scrollTop = 0; this.scrollable.scrollTop = 0;
@ -102,10 +89,10 @@ export default class StickersHelper extends AutocompleteHelper {
} }
private init() { private init() {
this.stickersContainer = document.createElement('div'); this.list = document.createElement('div');
this.stickersContainer.classList.add('stickers-helper-stickers', 'super-stickers'); this.list.classList.add('stickers-helper-stickers', 'super-stickers');
this.container.append(this.stickersContainer); this.container.append(this.list);
this.scrollable = new Scrollable(this.container); this.scrollable = new Scrollable(this.container);
this.lazyLoadQueue = new LazyLoadQueue(); this.lazyLoadQueue = new LazyLoadQueue();

23
src/components/emoticonsDropdown/index.ts

@ -43,9 +43,9 @@ export class EmoticonsDropdown {
public static lazyLoadQueue = new LazyLoadQueue(); public static lazyLoadQueue = new LazyLoadQueue();
private element: HTMLElement; private element: HTMLElement;
public emojiTab: EmojiTab; private emojiTab: EmojiTab;
public stickersTab: StickersTab; public stickersTab: StickersTab;
public gifsTab: GifsTab; private gifsTab: GifsTab;
private container: HTMLElement; private container: HTMLElement;
private tabsEl: HTMLElement; private tabsEl: HTMLElement;
@ -53,8 +53,8 @@ export class EmoticonsDropdown {
private tabs: {[id: number]: EmoticonsTab}; private tabs: {[id: number]: EmoticonsTab};
public searchButton: HTMLElement; private searchButton: HTMLElement;
public deleteBtn: HTMLElement; private deleteBtn: HTMLElement;
private displayTimeout: number; private displayTimeout: number;
@ -73,6 +73,8 @@ export class EmoticonsDropdown {
private selectTab: ReturnType<typeof horizontalMenu>; private selectTab: ReturnType<typeof horizontalMenu>;
private forceClose = false; private forceClose = false;
private savedRange: Range;
constructor() { constructor() {
this.element = document.getElementById('emoji-dropdown') as HTMLDivElement; this.element = document.getElementById('emoji-dropdown') as HTMLDivElement;
} }
@ -206,7 +208,7 @@ export class EmoticonsDropdown {
this.deleteBtn.classList.toggle('hide', this.tabId !== 0); this.deleteBtn.classList.toggle('hide', this.tabId !== 0);
}; };
public checkRights = () => { private checkRights = () => {
const peerId = appImManager.chat.peerId; const peerId = appImManager.chat.peerId;
const children = this.tabsEl.children; const children = this.tabsEl.children;
const tabsElements = Array.from(children) as HTMLElement[]; const tabsElements = Array.from(children) as HTMLElement[];
@ -251,6 +253,11 @@ export class EmoticonsDropdown {
if((this.element.style.display && enable === undefined) || enable) { if((this.element.style.display && enable === undefined) || enable) {
this.events.onOpen.forEach(cb => cb()); this.events.onOpen.forEach(cb => cb());
const sel = document.getSelection();
if(!sel.isCollapsed) {
this.savedRange = sel.getRangeAt(0);
}
EmoticonsDropdown.lazyLoadQueue.lock(); EmoticonsDropdown.lazyLoadQueue.lock();
//EmoticonsDropdown.lazyLoadQueue.unlock(); //EmoticonsDropdown.lazyLoadQueue.unlock();
animationIntersector.lockIntersectionGroup(EMOTICONSSTICKERGROUP); animationIntersector.lockIntersectionGroup(EMOTICONSSTICKERGROUP);
@ -306,6 +313,8 @@ export class EmoticonsDropdown {
this.container.classList.remove('disable-hover'); this.container.classList.remove('disable-hover');
this.events.onCloseAfter.forEach(cb => cb()); this.events.onCloseAfter.forEach(cb => cb());
this.savedRange = undefined;
}, isTouchSupported ? 0 : ANIMATION_DURATION); }, isTouchSupported ? 0 : ANIMATION_DURATION);
/* if(isTouchSupported) { /* if(isTouchSupported) {
@ -435,6 +444,10 @@ export class EmoticonsDropdown {
lazyLoadQueue.unlockAndRefresh(); lazyLoadQueue.unlockAndRefresh();
}); });
} }
public getSavedRange() {
return this.savedRange;
}
} }
const emoticonsDropdown = new EmoticonsDropdown(); const emoticonsDropdown = new EmoticonsDropdown();

83
src/components/emoticonsDropdown/tabs/emoji.ts

@ -4,21 +4,22 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import { EmoticonsDropdown, EmoticonsTab } from ".."; import emoticonsDropdown, { EmoticonsDropdown, EmoticonsTab } from "..";
import findUpClassName from "../../../helpers/dom/findUpClassName"; import findUpClassName from "../../../helpers/dom/findUpClassName";
import { fastRaf } from "../../../helpers/schedulers"; import { fastRaf, pause } from "../../../helpers/schedulers";
import appEmojiManager from "../../../lib/appManagers/appEmojiManager";
import appImManager from "../../../lib/appManagers/appImManager"; import appImManager from "../../../lib/appManagers/appImManager";
import appStateManager from "../../../lib/appManagers/appStateManager";
import Config from "../../../lib/config"; import Config from "../../../lib/config";
import { i18n, LangPackKey } from "../../../lib/langPack"; import { i18n, LangPackKey } from "../../../lib/langPack";
import { RichTextProcessor } from "../../../lib/richtextprocessor"; import { RichTextProcessor } from "../../../lib/richtextprocessor";
import rootScope from "../../../lib/rootScope"; import rootScope from "../../../lib/rootScope";
import { toCodePoints } from "../../../vendor/emoji";
import { putPreloader } from "../../misc"; import { putPreloader } from "../../misc";
import Scrollable from "../../scrollable"; import Scrollable from "../../scrollable";
import StickyIntersector from "../../stickyIntersector"; import StickyIntersector from "../../stickyIntersector";
const loadedURLs: Set<string> = new Set(); const loadedURLs: Set<string> = new Set();
export function appendEmoji(emoji: string, container: HTMLElement, prepend = false/* , unified = false */) { export function appendEmoji(emoji: string, container: HTMLElement, prepend = false, unify = false) {
//const emoji = details.unified; //const emoji = details.unified;
//const emoji = (details.unified as string).split('-') //const emoji = (details.unified as string).split('-')
//.reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), ''); //.reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
@ -27,18 +28,18 @@ export function appendEmoji(emoji: string, container: HTMLElement, prepend = fal
spanEmoji.classList.add('super-emoji'); spanEmoji.classList.add('super-emoji');
let kek: string; let kek: string;
/* if(unified) { if(unify) {
kek = RichTextProcessor.wrapRichText('_', { kek = RichTextProcessor.wrapRichText(emoji, {
entities: [{ entities: [{
_: 'messageEntityEmoji', _: 'messageEntityEmoji',
offset: 0, offset: 0,
length: emoji.split('-').length, length: emoji.length,
unicode: emoji unicode: toCodePoints(emoji).join('-')
}] }]
}); });
} else { */ } else {
kek = RichTextProcessor.wrapEmojiText(emoji); kek = RichTextProcessor.wrapEmojiText(emoji);
//} }
/* if(!kek.includes('emoji')) { /* if(!kek.includes('emoji')) {
console.log(emoji, kek, spanEmoji, emoji.length, new TextEncoder().encode(emoji), emojiUnicode(emoji)); console.log(emoji, kek, spanEmoji, emoji.length, new TextEncoder().encode(emoji), emojiUnicode(emoji));
@ -104,7 +105,6 @@ export function getEmojiFromElement(element: HTMLElement) {
export default class EmojiTab implements EmoticonsTab { export default class EmojiTab implements EmoticonsTab {
private content: HTMLElement; private content: HTMLElement;
private recent: string[] = [];
private recentItemsDiv: HTMLElement; private recentItemsDiv: HTMLElement;
private scroll: Scrollable; private scroll: Scrollable;
@ -170,12 +170,27 @@ export default class EmojiTab implements EmoticonsTab {
div.append(titleDiv, itemsDiv); div.append(titleDiv, itemsDiv);
emojis.forEach(emoji => { emojis.forEach(unified => {
/* if(emojiUnicode(emoji) === '1f481-200d-2642') { /* if(emojiUnicode(emoji) === '1f481-200d-2642') {
console.log('append emoji', emoji, emojiUnicode(emoji)); console.log('append emoji', emoji, emojiUnicode(emoji));
} */ } */
emoji = emoji.split('-').reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), ''); let emoji = unified.split('-').reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
//if(emoji.includes('🕵')) {
//console.log('toCodePoints', toCodePoints(emoji));
//emoji = emoji.replace(/(\u200d[\u2640\u2642\u2695])(?!\ufe0f)/, '\ufe0f$1');
// const zwjIndex = emoji.indexOf('\u200d');
// if(zwjIndex !== -1 && !emoji.includes('\ufe0f')) {
// /* if(zwjIndex !== (emoji.length - 1)) {
// emoji = emoji.replace(/(\u200d)/g, '\ufe0f$1');
// } */
// emoji += '\ufe0f';
// //emoji += '\ufe0f';
// }
//debugger;
//}
appendEmoji(emoji/* .replace(/[\ufe0f\u2640\u2642\u2695]/g, '') */, itemsDiv, false/* , false */); appendEmoji(emoji/* .replace(/[\ufe0f\u2640\u2642\u2695]/g, '') */, itemsDiv, false/* , false */);
@ -197,22 +212,17 @@ export default class EmojiTab implements EmoticonsTab {
const preloader = putPreloader(this.content, true); const preloader = putPreloader(this.content, true);
Promise.all([ Promise.all([
new Promise((resolve) => setTimeout(resolve, 200)), pause(200),
appEmojiManager.getRecentEmojis()
appStateManager.getState().then(state => { ]).then(([_, recent]) => {
if(Array.isArray(state.recentEmoji)) {
this.recent = state.recentEmoji;
}
})
]).then(() => {
preloader.remove(); preloader.remove();
this.recentItemsDiv = divs['Emoji.Recent'].querySelector('.super-emojis'); this.recentItemsDiv = divs['Emoji.Recent'].querySelector('.super-emojis');
for(const emoji of this.recent) { for(const emoji of recent) {
appendEmoji(emoji, this.recentItemsDiv); appendEmoji(emoji, this.recentItemsDiv);
} }
this.recentItemsDiv.parentElement.classList.toggle('hide', !this.recent.length); this.recentItemsDiv.parentElement.classList.toggle('hide', !this.recentItemsDiv.childElementCount);
categories.unshift('Emoji.Recent'); categories.unshift('Emoji.Recent');
categories.map(category => { categories.map(category => {
@ -246,11 +256,21 @@ export default class EmojiTab implements EmoticonsTab {
target = target.firstChild as HTMLElement; target = target.firstChild as HTMLElement;
} else if(target.tagName === 'DIV') return; } else if(target.tagName === 'DIV') return;
//console.log('contentEmoji div', target); // set selection range
appImManager.chat.input.messageInput.innerHTML += RichTextProcessor.emojiSupported ? const savedRange = emoticonsDropdown.getSavedRange();
if(savedRange) {
const sel = document.getSelection();
sel.removeAllRanges();
sel.addRange(savedRange);
}
const html = RichTextProcessor.emojiSupported ?
(target.nodeType === 3 ? target.nodeValue : target.innerHTML) : (target.nodeType === 3 ? target.nodeValue : target.innerHTML) :
target.outerHTML; target.outerHTML;
// insert emoji in input
document.execCommand('insertHTML', true, html);
// Recent // Recent
const emoji = getEmojiFromElement(target); const emoji = getEmojiFromElement(target);
(Array.from(this.recentItemsDiv.children) as HTMLElement[]).forEach((el, idx) => { (Array.from(this.recentItemsDiv.children) as HTMLElement[]).forEach((el, idx) => {
@ -259,18 +279,11 @@ export default class EmojiTab implements EmoticonsTab {
el.remove(); el.remove();
} }
}); });
//const scrollHeight = this.recentItemsDiv.scrollHeight;
appendEmoji(emoji, this.recentItemsDiv, true); appendEmoji(emoji, this.recentItemsDiv, true);
this.recent.findAndSplice(e => e === emoji); appEmojiManager.pushRecentEmoji(emoji);
this.recent.unshift(emoji); this.recentItemsDiv.parentElement.classList.remove('hide');
if(this.recent.length > 36) {
this.recent.length = 36;
}
this.recentItemsDiv.parentElement.classList.toggle('hide', !this.recent.length);
appStateManager.pushToState('recentEmoji', this.recent);
// Append to input // Append to input
const event = new Event('input', {bubbles: true, cancelable: true}); const event = new Event('input', {bubbles: true, cancelable: true});

4
src/config/app.ts

@ -12,8 +12,8 @@
const App = { const App = {
id: 1025907, id: 1025907,
hash: '452b0359b988148995f22ff0f4229750', hash: '452b0359b988148995f22ff0f4229750',
version: '0.5.3', version: '0.5.4',
langPackVersion: '0.1.8', langPackVersion: '0.1.9',
langPack: 'macos', langPack: 'macos',
langPackCode: 'en', langPackCode: 'en',
domains: [] as string[], domains: [] as string[],

36
src/emoji_test.js

@ -0,0 +1,36 @@
function toCodePoints(unicodeSurrogates) {
const points = [];
let char = 0;
let previous = 0;
let i = 0;
while(i < unicodeSurrogates.length) {
char = unicodeSurrogates.charCodeAt(i++);
if(previous) {
points.push((0x10000 + ((previous - 0xd800) << 10) + (char - 0xdc00)).toString(16));
previous = 0;
} else if (char > 0xd800 && char <= 0xdbff) {
previous = char;
} else {
points.push(char.toString(16));
}
}
if(points.length && points[0].length === 2) {
points[0] = '00' + points[0];
}
return points;
}
var eye = "👁🗨";
toCodePoints(eye)
function ccc(str) {
return str.split('').map(c => c.charCodeAt(0).toString(16));
}
ccc("🏳🌈")
ccc("🏋");
"🏌♀";
var regexp = new RegExp("(?:👨🏻🤝👨<EFBFBD>[<EFBFBD>-<EFBFBD>]|👨🏼🤝👨<EFBFBD>[<EFBFBD><EFBFBD>-<EFBFBD>]|👨🏽🤝👨<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|👨🏾🤝👨<EFBFBD>[<EFBFBD>-<EFBFBD><EFBFBD>]|👨🏿🤝👨<EFBFBD>[<EFBFBD>-<EFBFBD>]|👩🏻🤝👨<EFBFBD>[<EFBFBD>-<EFBFBD>]|👩🏻🤝👩<EFBFBD>[<EFBFBD>-<EFBFBD>]|👩🏼🤝👨<EFBFBD>[<EFBFBD><EFBFBD>-<EFBFBD>]|👩🏼🤝👩<EFBFBD>[<EFBFBD><EFBFBD>-<EFBFBD>]|👩🏽🤝👨<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|👩🏽🤝👩<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|👩🏾🤝👨<EFBFBD>[<EFBFBD>-<EFBFBD><EFBFBD>]|👩🏾🤝👩<EFBFBD>[<EFBFBD>-<EFBFBD><EFBFBD>]|👩🏿🤝👨<EFBFBD>[<EFBFBD>-<EFBFBD>]|👩🏿🤝👩<EFBFBD>[<EFBFBD>-<EFBFBD>]|🧑🏻🤝🧑<EFBFBD>[<EFBFBD>-<EFBFBD>]|🧑🏼🤝🧑<EFBFBD>[<EFBFBD>-<EFBFBD>]|🧑🏽🤝🧑<EFBFBD>[<EFBFBD>-<EFBFBD>]|🧑🏾🤝🧑<EFBFBD>[<EFBFBD>-<EFBFBD>]|🧑🏿🤝🧑<EFBFBD>[<EFBFBD>-<EFBFBD>]|🧑🤝🧑|👫<EFBFBD>[<EFBFBD>-<EFBFBD>]|👬<EFBFBD>[<EFBFBD>-<EFBFBD>]|👭<EFBFBD>[<EFBFBD>-<EFBFBD>]|<EFBFBD>[<EFBFBD>-<EFBFBD>])|(?:<EFBFBD>[<EFBFBD><EFBFBD>]|🧑)(?:<EFBFBD>[<EFBFBD>-<EFBFBD>])?(?:⚕|⚖|✈|<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|<EFBFBD>[<EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>])|(?:<EFBFBD>[<EFBFBD><EFBFBD>]|<EFBFBD>[<EFBFBD><EFBFBD>]|⛹)((?:<EFBFBD>[<EFBFBD>-<EFBFBD>])[♀♂])|(?:<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD>]|<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD>]|<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD>])(?:<EFBFBD>[<EFBFBD>-<EFBFBD>])?[♀♂]|(?:👨💋👨|👨👨👦👦|👨👨👧<EFBFBD>[<EFBFBD><EFBFBD>]|👨👩👦👦|👨👩👧<EFBFBD>[<EFBFBD><EFBFBD>]|👩💋<EFBFBD>[<EFBFBD><EFBFBD>]|👩👩👦👦|👩👩👧<EFBFBD>[<EFBFBD><EFBFBD>]|👨👨|👨👦👦|👨👧<EFBFBD>[<EFBFBD><EFBFBD>]|👨👨<EFBFBD>[<EFBFBD><EFBFBD>]|👨👩<EFBFBD>[<EFBFBD><EFBFBD>]|👩<EFBFBD>[<EFBFBD><EFBFBD>]|👩👦👦|👩👧<EFBFBD>[<EFBFBD><EFBFBD>]|👩👩<EFBFBD>[<EFBFBD><EFBFBD>]|🏳⚧|🏳🌈|🏴☠|🐕🦺|🐻❄|👁🗨|👨<EFBFBD>[<EFBFBD><EFBFBD>]|👩<EFBFBD>[<EFBFBD><EFBFBD>]|👯♀|👯♂|🤼♀|🤼♂|🧞♀|🧞♂|🧟♀|🧟♂|🐈⬛)|[#*0-9]?|(?:[©®™♟])|(?:<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|[‼⁉ℹ↔-↙↩↪⌚⌛⌨⏏⏭-⏯⏱⏲⏸-⏺Ⓜ▪▫▶◀◻-◾☀-☄☎☑☔☕☘☠☢☣☦☪☮☯☸-☺♀♂♈-♓♠♣♥♦♨♻♿⚒-⚗⚙⚛⚜⚠⚡⚧⚪⚫⚰⚱⚽⚾⛄⛅⛈⛏⛑⛓⛔⛩⛪⛰-⛵⛸⛺⛽✂✈✉✏✒✔✖✝✡✳✴❄❇❗❣❤➡⤴⤵⬅-⬇⬛⬜⭐⭕〰〽㊗㊙])(?:(?!))|(?:(?:<EFBFBD>[<EFBFBD><EFBFBD>]|<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD>]|[☝⛷⛹✌✍])(?:(?!))|(?:<EFBFBD>[<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>]|<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>]|<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD>]|[✊✋]))(?:<EFBFBD>[<EFBFBD>-<EFBFBD>])?|(?:🏴󠁧󠁢󠁥󠁮󠁧󠁿|🏴󠁧󠁢󠁳󠁣󠁴󠁿|🏴󠁧󠁢󠁷󠁬󠁳󠁿|🇦<EFBFBD>[<EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|🇧<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|🇨<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD>]|🇩<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|🇪<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD>]|🇫<EFBFBD>[<EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|🇬<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>]|🇭<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|🇮<EFBFBD>[<EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD>]|🇯<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|🇰<EFBFBD>[<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|🇱<EFBFBD>[<EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>]|🇲<EFBFBD>[<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD>]|🇳<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|🇴🇲|🇵<EFBFBD>[<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>]|🇶🇦|🇷<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|🇸<EFBFBD>[<EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD>]|🇹<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|🇺<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|🇻<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]|🇼<EFBFBD>[<EFBFBD><EFBFBD>]|🇽🇰|🇾<EFBFBD>[<EFBFBD><EFBFBD>]|🇿<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD>]|<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD>]|<EFBFBD>[<EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD>]|<EFBFBD>[<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD><EFBFBD>-<EFBFBD>]|[⏩-⏬⏰⏳♾⛎✅✨❌❎❓-❕➕-➗➰➿])|")

90
src/helpers/blur.ts

@ -4,50 +4,50 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import _DEBUG from '../config/debug'; import type fastBlur from '../vendor/fastBlur';
import fastBlur from '../vendor/fastBlur';
import addHeavyTask from './heavyQueue'; import addHeavyTask from './heavyQueue';
const RADIUS = 2; const RADIUS = 2;
const ITERATIONS = 2; const ITERATIONS = 2;
const DEBUG = _DEBUG && true; const isFilterAvailable = 'filter' in (document.createElement('canvas').getContext('2d') || {});
let requireBlurPromise: Promise<any>;
let fastBlurFunc: typeof fastBlur;
if(!isFilterAvailable) {
requireBlurPromise = import('../vendor/fastBlur').then(m => {
fastBlurFunc = m.default;
});
} else {
requireBlurPromise = Promise.resolve();
}
function processBlur(dataUri: string, radius: number, iterations: number) { function processBlurNext(img: HTMLImageElement, radius: number, iterations: number) {
return new Promise<string>((resolve) => { return new Promise<string>((resolve) => {
const img = new Image(); const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const perf = performance.now(); const ctx = canvas.getContext('2d', {alpha: false});
if(DEBUG) { if(isFilterAvailable) {
console.log('[blur] start'); ctx.filter = `blur(${radius}px)`;
ctx.drawImage(img, -radius * 2, -radius * 2, canvas.width + radius * 4, canvas.height + radius * 4);
} else {
ctx.drawImage(img, 0, 0);
fastBlurFunc(ctx, 0, 0, canvas.width, canvas.height, radius, iterations);
} }
img.onload = () => { resolve(canvas.toDataURL());
const canvas = document.createElement('canvas'); /* if(DEBUG) {
canvas.width = img.width; console.log(`[blur] end, radius: ${radius}, iterations: ${iterations}, time: ${performance.now() - perf}`);
canvas.height = img.height; } */
const ctx = canvas.getContext('2d')!; /* canvas.toBlob(blob => {
resolve(URL.createObjectURL(blob));
//ctx.filter = 'blur(2px)';
ctx.drawImage(img, 0, 0);
fastBlur(ctx, 0, 0, canvas.width, canvas.height, radius, iterations);
resolve(canvas.toDataURL());
if(DEBUG) { if(DEBUG) {
console.log(`[blur] end, radius: ${radius}, iterations: ${iterations}, time: ${performance.now() - perf}`); console.log(`[blur] end, radius: ${radius}, iterations: ${iterations}, time: ${performance.now() - perf}`);
} }
}); */
/* canvas.toBlob(blob => {
resolve(URL.createObjectURL(blob));
if(DEBUG) {
console.log(`[blur] end, radius: ${radius}, iterations: ${iterations}, time: ${performance.now() - perf}`);
}
}); */
};
img.src = dataUri;
}); });
} }
@ -67,12 +67,30 @@ export default function blur(dataUri: string, radius: number = RADIUS, iteration
if(blurPromises.has(dataUri)) return blurPromises.get(dataUri); if(blurPromises.has(dataUri)) return blurPromises.get(dataUri);
const promise = new Promise<string>((resolve) => { const promise = new Promise<string>((resolve) => {
//return resolve(dataUri); //return resolve(dataUri);
addHeavyTask({ requireBlurPromise.then(() => {
items: [[dataUri, radius, iterations]], const img = new Image();
context: null, img.onload = () => {
process: processBlur if(isFilterAvailable) {
}, 'unshift').then(results => { processBlurNext(img, radius, iterations).then(resolve);
resolve(results[0]); } else {
addHeavyTask({
items: [[img, radius, iterations]],
context: null,
process: processBlurNext
}, 'unshift').then(results => {
resolve(results[0]);
});
}
};
img.src = dataUri;
/* addHeavyTask({
items: [[dataUri, radius, iterations]],
context: null,
process: processBlur
}, 'unshift').then(results => {
resolve(results[0]);
}); */
}); });
}); });

25
src/helpers/dom/attachListNavigation.ts

@ -14,11 +14,12 @@ type ArrowKey = 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight';
const HANDLE_EVENT = 'keydown'; const HANDLE_EVENT = 'keydown';
const ACTIVE_CLASS_NAME = 'active'; const ACTIVE_CLASS_NAME = 'active';
export default function attachListNavigation({list, type, onSelect, once}: { export default function attachListNavigation({list, type, onSelect, once, waitForKey}: {
list: HTMLElement, list: HTMLElement,
type: 'xy' | 'x' | 'y', type: 'xy' | 'x' | 'y',
onSelect: (target: Element) => void | boolean, onSelect: (target: Element) => void | boolean,
once: boolean, once: boolean,
waitForKey?: string
}) { }) {
const keyNames: Set<ArrowKey> = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']); const keyNames: Set<ArrowKey> = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']);
@ -82,7 +83,7 @@ export default function attachListNavigation({list, type, onSelect, once}: {
handleArrowKey = (currentTarget, key) => getNextTargetX(currentTarget, key === 'ArrowRight' || key === 'ArrowDown'); handleArrowKey = (currentTarget, key) => getNextTargetX(currentTarget, key === 'ArrowRight' || key === 'ArrowDown');
} }
const onKeyDown = (e: KeyboardEvent) => { let onKeyDown = (e: KeyboardEvent) => {
if(!keyNames.has(e.key as any)) { if(!keyNames.has(e.key as any)) {
if(e.key === 'Enter') { if(e.key === 'Enter') {
cancelEvent(e); cancelEvent(e);
@ -99,8 +100,6 @@ export default function attachListNavigation({list, type, onSelect, once}: {
currentTarget = handleArrowKey(currentTarget, e.key as any); currentTarget = handleArrowKey(currentTarget, e.key as any);
setCurrentTarget(currentTarget, true); setCurrentTarget(currentTarget, true);
} }
return false;
}; };
const scrollable = findUpClassName(list, 'scrollable'); const scrollable = findUpClassName(list, 'scrollable');
@ -142,10 +141,26 @@ export default function attachListNavigation({list, type, onSelect, once}: {
}; };
const resetTarget = () => { const resetTarget = () => {
if(waitForKey) return;
setCurrentTarget(list.firstElementChild, false); setCurrentTarget(list.firstElementChild, false);
}; };
resetTarget(); if(waitForKey) {
const _onKeyDown = onKeyDown;
onKeyDown = (e) => {
if(e.key === waitForKey) {
cancelEvent(e);
document.removeEventListener(HANDLE_EVENT, onKeyDown, {capture: true});
onKeyDown = _onKeyDown;
document.addEventListener(HANDLE_EVENT, onKeyDown, {capture: true, passive: false});
resetTarget();
}
};
} else {
resetTarget();
}
// const input = document.activeElement as HTMLElement; // const input = document.activeElement as HTMLElement;
// input.addEventListener(HANDLE_EVENT, onKeyDown, {capture: true, passive: false}); // input.addEventListener(HANDLE_EVENT, onKeyDown, {capture: true, passive: false});

54
src/helpers/dom/getRichElementValue.ts

@ -45,43 +45,43 @@ export const markdownTags: {[type in MarkdownType]: MarkdownTag} = {
export default function getRichElementValue(node: HTMLElement, lines: string[], line: string[], selNode?: Node, selOffset?: number, entities?: MessageEntity[], offset = {offset: 0}) { export default function getRichElementValue(node: HTMLElement, lines: string[], line: string[], selNode?: Node, selOffset?: number, entities?: MessageEntity[], offset = {offset: 0}) {
if(node.nodeType === 3) { // TEXT if(node.nodeType === 3) { // TEXT
const nodeValue = node.nodeValue;
if(selNode === node) { if(selNode === node) {
const value = node.nodeValue; line.push(nodeValue.substr(0, selOffset) + '\x01' + nodeValue.substr(selOffset));
line.push(value.substr(0, selOffset) + '\x01' + value.substr(selOffset));
} else { } else {
const nodeValue = node.nodeValue;
line.push(nodeValue); line.push(nodeValue);
}
if(entities && nodeValue.trim()) { if(entities && nodeValue.trim()) {
if(node.parentNode) { if(node.parentNode) {
const parentElement = node.parentElement; const parentElement = node.parentElement;
for(const type in markdownTags) { for(const type in markdownTags) {
const tag = markdownTags[type as MarkdownType]; const tag = markdownTags[type as MarkdownType];
const closest = parentElement.closest(tag.match + ', [contenteditable]'); const closest = parentElement.closest(tag.match + ', [contenteditable]');
if(closest && closest.getAttribute('contenteditable') === null) { if(closest && closest.getAttribute('contenteditable') === null) {
if(tag.entityName === 'messageEntityTextUrl') { if(tag.entityName === 'messageEntityTextUrl') {
entities.push({ entities.push({
_: tag.entityName as any, _: tag.entityName as any,
url: (parentElement as HTMLAnchorElement).href, url: (parentElement as HTMLAnchorElement).href,
offset: offset.offset, offset: offset.offset,
length: nodeValue.length length: nodeValue.length
}); });
} else { } else {
entities.push({ entities.push({
_: tag.entityName as any, _: tag.entityName as any,
offset: offset.offset, offset: offset.offset,
length: nodeValue.length length: nodeValue.length
}); });
}
} }
} }
} }
} }
offset.offset += nodeValue.length;
} }
offset.offset += nodeValue.length;
return; return;
} }

4
src/helpers/dom/getRichValueWithCaret.ts

@ -18,8 +18,8 @@ export default function getRichValueWithCaret(field: HTMLElement, withEntities =
const line: string[] = []; const line: string[] = [];
const sel = window.getSelection(); const sel = window.getSelection();
var selNode let selNode: Node;
var selOffset let selOffset: number;
if(sel && sel.rangeCount) { if(sel && sel.rangeCount) {
const range = sel.getRangeAt(0); const range = sel.getRangeAt(0);
if(range.startContainer && if(range.startContainer &&

46
src/helpers/dom/setRichFocus.ts

@ -0,0 +1,46 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*
* Originally from:
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
export default function setRichFocus(field: HTMLElement, selectNode: Node, noCollapse?: boolean) {
field.focus();
if(selectNode &&
selectNode.parentNode == field &&
!selectNode.nextSibling &&
!noCollapse) {
field.removeChild(selectNode);
selectNode = null;
}
if(window.getSelection && document.createRange) {
const range = document.createRange();
if(selectNode) {
range.selectNode(selectNode);
} else {
range.selectNodeContents(field);
}
if(!noCollapse) {
range.collapse(false);
}
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
/* else if (document.body.createTextRange !== undefined) {
var textRange = document.body.createTextRange()
textRange.moveToElementText(selectNode || field)
if (!noCollapse) {
textRange.collapse(false)
}
textRange.select()
} */
}

6
src/lang.ts

@ -43,6 +43,11 @@ const lang = {
}, },
"Chat.Search.NoMessagesFound": "No messages found", "Chat.Search.NoMessagesFound": "No messages found",
"Chat.Search.PrivateSearch": "Private Search", "Chat.Search.PrivateSearch": "Private Search",
"ConnectionStatus.ReconnectIn": "Reconnect in %ds, %s",
"ConnectionStatus.Reconnect": "reconnect",
"ConnectionStatus.Waiting": "Waiting for network...",
"Deactivated.Title": "Too many tabs...",
"Deactivated.Subtitle": "Telegram supports only one active tab with the app.\nClick anywhere to continue using this tab.",
//"Saved": "Saved", //"Saved": "Saved",
"General.Keyboard": "Keyboard", "General.Keyboard": "Keyboard",
"General.SendShortcut.Enter": "Send by Enter", "General.SendShortcut.Enter": "Send by Enter",
@ -430,6 +435,7 @@ const lang = {
"Recent": "Recent", "Recent": "Recent",
"Of": "%1$d of %2$d", "Of": "%1$d of %2$d",
"NoResult": "No results", "NoResult": "No results",
"Updating": "Updating...",
// * macos // * macos
"AccountSettings.Filters": "Chat Folders", "AccountSettings.Filters": "Chat Folders",

9
src/layer.d.ts vendored

@ -4025,7 +4025,7 @@ export namespace ReplyMarkup {
/** /**
* @link https://core.telegram.org/type/MessageEntity * @link https://core.telegram.org/type/MessageEntity
*/ */
export type MessageEntity = MessageEntity.messageEntityUnknown | MessageEntity.messageEntityMention | MessageEntity.messageEntityHashtag | MessageEntity.messageEntityBotCommand | MessageEntity.messageEntityUrl | MessageEntity.messageEntityEmail | MessageEntity.messageEntityBold | MessageEntity.messageEntityItalic | MessageEntity.messageEntityCode | MessageEntity.messageEntityPre | MessageEntity.messageEntityTextUrl | MessageEntity.messageEntityMentionName | MessageEntity.inputMessageEntityMentionName | MessageEntity.messageEntityPhone | MessageEntity.messageEntityCashtag | MessageEntity.messageEntityUnderline | MessageEntity.messageEntityStrike | MessageEntity.messageEntityBlockquote | MessageEntity.messageEntityBankCard | MessageEntity.messageEntityEmoji | MessageEntity.messageEntityHighlight | MessageEntity.messageEntityLinebreak; export type MessageEntity = MessageEntity.messageEntityUnknown | MessageEntity.messageEntityMention | MessageEntity.messageEntityHashtag | MessageEntity.messageEntityBotCommand | MessageEntity.messageEntityUrl | MessageEntity.messageEntityEmail | MessageEntity.messageEntityBold | MessageEntity.messageEntityItalic | MessageEntity.messageEntityCode | MessageEntity.messageEntityPre | MessageEntity.messageEntityTextUrl | MessageEntity.messageEntityMentionName | MessageEntity.inputMessageEntityMentionName | MessageEntity.messageEntityPhone | MessageEntity.messageEntityCashtag | MessageEntity.messageEntityUnderline | MessageEntity.messageEntityStrike | MessageEntity.messageEntityBlockquote | MessageEntity.messageEntityBankCard | MessageEntity.messageEntityEmoji | MessageEntity.messageEntityHighlight | MessageEntity.messageEntityLinebreak | MessageEntity.messageEntityCaret;
export namespace MessageEntity { export namespace MessageEntity {
export type messageEntityUnknown = { export type messageEntityUnknown = {
@ -4164,6 +4164,12 @@ export namespace MessageEntity {
offset?: number, offset?: number,
length?: number length?: number
}; };
export type messageEntityCaret = {
_: 'messageEntityCaret',
offset?: number,
length?: number
};
} }
/** /**
@ -9649,6 +9655,7 @@ export interface ConstructorDeclMap {
'messageEntityEmoji': MessageEntity.messageEntityEmoji, 'messageEntityEmoji': MessageEntity.messageEntityEmoji,
'messageEntityHighlight': MessageEntity.messageEntityHighlight, 'messageEntityHighlight': MessageEntity.messageEntityHighlight,
'messageEntityLinebreak': MessageEntity.messageEntityLinebreak, 'messageEntityLinebreak': MessageEntity.messageEntityLinebreak,
'messageEntityCaret': MessageEntity.messageEntityCaret,
'messageActionChatLeave': MessageAction.messageActionChatLeave, 'messageActionChatLeave': MessageAction.messageActionChatLeave,
'messageActionChannelDeletePhoto': MessageAction.messageActionChannelDeletePhoto, 'messageActionChannelDeletePhoto': MessageAction.messageActionChannelDeletePhoto,
'messageActionChannelEditTitle': MessageAction.messageActionChannelEditTitle, 'messageActionChannelEditTitle': MessageAction.messageActionChannelEditTitle,

23
src/lib/appManagers/appDialogsManager.ts

@ -32,7 +32,7 @@ import App from "../../config/app";
import DEBUG, { MOUNT_CLASS_TO } from "../../config/debug"; import DEBUG, { MOUNT_CLASS_TO } from "../../config/debug";
import appNotificationsManager from "./appNotificationsManager"; import appNotificationsManager from "./appNotificationsManager";
import PeerTitle from "../../components/peerTitle"; import PeerTitle from "../../components/peerTitle";
import { i18n, _i18n } from "../langPack"; import { i18n, LangPackKey, _i18n } from "../langPack";
import findUpTag from "../../helpers/dom/findUpTag"; import findUpTag from "../../helpers/dom/findUpTag";
import { LazyLoadQueueIntersector } from "../../components/lazyLoadQueue"; import { LazyLoadQueueIntersector } from "../../components/lazyLoadQueue";
import lottieLoader from "../lottieLoader"; import lottieLoader from "../lottieLoader";
@ -67,8 +67,9 @@ class ConnectionStatusComponent {
private statusEl: HTMLElement; private statusEl: HTMLElement;
private statusPreloader: ProgressivePreloader; private statusPreloader: ProgressivePreloader;
private currentText = ''; private currentLangPackKey = '';
private connectingTimeout: number;
private connecting = false; private connecting = false;
private updating = false; private updating = false;
@ -151,23 +152,29 @@ class ConnectionStatusComponent {
} }
this.connecting = !online; this.connecting = !online;
this.connectingTimeout = status && status.timeout;
DEBUG && this.log('connecting', this.connecting); DEBUG && this.log('connecting', this.connecting);
this.setState(); this.setState();
}); });
}; };
private setStatusText = (text: string) => { private setStatusText = (langPackKey: LangPackKey) => {
if(this.currentText === text) return; if(this.currentLangPackKey === langPackKey) return;
this.statusEl.innerText = this.currentText = text; this.currentLangPackKey = langPackKey;
replaceContent(this.statusEl, i18n(langPackKey));
this.statusPreloader.attach(this.statusEl); this.statusPreloader.attach(this.statusEl);
}; };
private setState = () => { private setState = () => {
const timeout = ConnectionStatusComponent.CHANGE_STATE_DELAY; const timeout = ConnectionStatusComponent.CHANGE_STATE_DELAY;
if(this.connecting) { if(this.connecting) {
this.setStatusText('Waiting for network...'); // if(this.connectingTimeout) {
// this.setStatusText('ConnectionStatus.Reconnect');
// } else {
this.setStatusText('ConnectionStatus.Waiting');
// }
} else if(this.updating) { } else if(this.updating) {
this.setStatusText('Updating...'); this.setStatusText('Updating');
} }
DEBUG && this.log('setState', this.connecting || this.updating); DEBUG && this.log('setState', this.connecting || this.updating);
@ -908,7 +915,7 @@ export class AppDialogsManager {
const offsetTop = this.folders.container.offsetTop; const offsetTop = this.folders.container.offsetTop;
const firstY = rectContainer.y + offsetTop; const firstY = rectContainer.y + offsetTop;
const lastY = rectContainer.y - 8; // 8px - .chatlist padding-bottom const lastY = rectContainer.y/* - 8 */; // 8px - .chatlist padding-bottom
const firstElement = findUpTag(document.elementFromPoint(Math.ceil(rectTarget.x), Math.ceil(firstY + 1)), firstElementChild.tagName) as HTMLElement; const firstElement = findUpTag(document.elementFromPoint(Math.ceil(rectTarget.x), Math.ceil(firstY + 1)), firstElementChild.tagName) as HTMLElement;
const lastElement = findUpTag(document.elementFromPoint(Math.ceil(rectTarget.x), Math.floor(lastY + rectContainer.height - 1)), firstElementChild.tagName) as HTMLElement; const lastElement = findUpTag(document.elementFromPoint(Math.ceil(rectTarget.x), Math.floor(lastY + rectContainer.height - 1)), firstElementChild.tagName) as HTMLElement;

60
src/lib/appManagers/appEmojiManager.ts

@ -1,3 +1,9 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import App from "../../config/app"; import App from "../../config/app";
import { MOUNT_CLASS_TO } from "../../config/debug"; import { MOUNT_CLASS_TO } from "../../config/debug";
import { validateInitObject } from "../../helpers/object"; import { validateInitObject } from "../../helpers/object";
@ -6,6 +12,7 @@ import { isObject } from "../mtproto/bin_utils";
import apiManager from "../mtproto/mtprotoworker"; import apiManager from "../mtproto/mtprotoworker";
import SearchIndex from "../searchIndex"; import SearchIndex from "../searchIndex";
import sessionStorage from "../sessionStorage"; import sessionStorage from "../sessionStorage";
import appStateManager from "./appStateManager";
type EmojiLangPack = { type EmojiLangPack = {
keywords: { keywords: {
@ -21,7 +28,10 @@ const EMOJI_LANG_PACK: EmojiLangPack = {
langCode: App.langPackCode langCode: App.langPackCode
}; };
const RECENT_MAX_LENGTH = 36;
export class AppEmojiManager { export class AppEmojiManager {
private static POPULAR_EMOJI = ["😂", "😘", "❤", "😍", "😊", "😁", "👍", "☺", "😔", "😄", "😭", "💋", "😒", "😳", "😜", "🙈", "😉", "😃", "😢", "😝", "😱", "😡", "😏", "😞", "😅", "😚", "🙊", "😌", "😀", "😋", "😆", "👌", "😐", "😕"];
private keywordLangPacks: { private keywordLangPacks: {
[langCode: string]: EmojiLangPack [langCode: string]: EmojiLangPack
} = {}; } = {};
@ -31,6 +41,9 @@ export class AppEmojiManager {
private getKeywordsPromises: {[langCode: string]: Promise<EmojiLangPack>} = {}; private getKeywordsPromises: {[langCode: string]: Promise<EmojiLangPack>} = {};
private recent: string[];
private getRecentEmojisPromise: Promise<AppEmojiManager['recent']>;
/* public getPopularEmoji() { /* public getPopularEmoji() {
return sessionStorage.get('emojis_popular').then(popEmojis => { return sessionStorage.get('emojis_popular').then(popEmojis => {
var result = [] var result = []
@ -134,7 +147,7 @@ export class AppEmojiManager {
} }
public getBothEmojiKeywords() { public getBothEmojiKeywords() {
const promises: ReturnType<AppEmojiManager['getEmojiKeywords']>[] = [ const promises: Promise<any>[] = [
this.getEmojiKeywords() this.getEmojiKeywords()
]; ];
@ -142,12 +155,16 @@ export class AppEmojiManager {
promises.push(this.getEmojiKeywords(I18n.lastRequestedLangCode)); promises.push(this.getEmojiKeywords(I18n.lastRequestedLangCode));
} }
if(!this.recent) {
promises.push(this.getRecentEmojis());
}
return Promise.all(promises); return Promise.all(promises);
} }
public indexEmojis() { public indexEmojis() {
if(!this.index) { if(!this.index) {
this.index = new SearchIndex(); this.index = new SearchIndex(false, false);
} }
for(const langCode in this.keywordLangPacks) { for(const langCode in this.keywordLangPacks) {
@ -169,15 +186,48 @@ export class AppEmojiManager {
public searchEmojis(q: string) { public searchEmojis(q: string) {
this.indexEmojis(); this.indexEmojis();
q = q.toLowerCase().replace(/_/g, ' ');
//const perf = performance.now(); //const perf = performance.now();
const set = this.index.search(q); let emojis: Array<string>;
const flattened = Array.from(set).reduce((acc, v) => acc.concat(v), []); if(q.trim()) {
const emojis = Array.from(new Set(flattened)); const set = this.index.search(q);
emojis = Array.from(set).reduce((acc, v) => acc.concat(v), []);
} else {
emojis = this.recent.concat(AppEmojiManager.POPULAR_EMOJI).slice(0, RECENT_MAX_LENGTH);
}
emojis = Array.from(new Set(emojis));
//console.log('searchEmojis', q, 'time', performance.now() - perf); //console.log('searchEmojis', q, 'time', performance.now() - perf);
/* for(let i = 0, length = emojis.length; i < length; ++i) {
if(emojis[i].includes(zeroWidthJoiner) && !emojis[i].includes('\ufe0f')) {
emojis[i] += '\ufe0f';
}
} */
return emojis; return emojis;
} }
public getRecentEmojis() {
if(this.getRecentEmojisPromise) return this.getRecentEmojisPromise;
return this.getRecentEmojisPromise = appStateManager.getState().then(state => {
return this.recent = Array.isArray(state.recentEmoji) ? state.recentEmoji : [];
});
}
public pushRecentEmoji(emoji: string) {
this.getRecentEmojis().then(recent => {
recent.findAndSplice(e => e === emoji);
recent.unshift(emoji);
if(recent.length > RECENT_MAX_LENGTH) {
recent.length = RECENT_MAX_LENGTH;
}
appStateManager.pushToState('recentEmoji', recent);
});
}
} }
const appEmojiManager = new AppEmojiManager(); const appEmojiManager = new AppEmojiManager();

36
src/lib/appManagers/appImManager.ts

@ -57,6 +57,8 @@ import placeCaretAtEnd from '../../helpers/dom/placeCaretAtEnd';
import replaceContent from '../../helpers/dom/replaceContent'; import replaceContent from '../../helpers/dom/replaceContent';
import whichChild from '../../helpers/dom/whichChild'; import whichChild from '../../helpers/dom/whichChild';
import appEmojiManager from './appEmojiManager'; import appEmojiManager from './appEmojiManager';
import PopupElement from '../../components/popups';
import singleInstance from '../mtproto/singleInstance';
//console.log('appImManager included33!'); //console.log('appImManager included33!');
@ -181,6 +183,38 @@ export class AppImManager {
this.applyCurrentTheme(); this.applyCurrentTheme();
}); });
rootScope.on('instance_deactivated', () => {
const popup = new PopupElement('popup-instance-deactivated', undefined, {overlayClosable: true});
const c = document.createElement('div');
c.classList.add('instance-deactivated-container');
(popup as any).container.replaceWith(c);
const header = document.createElement('div');
header.classList.add('header');
header.append(i18n('Deactivated.Title'));
const subtitle = document.createElement('div');
subtitle.classList.add('subtitle');
subtitle.append(i18n('Deactivated.Subtitle'));
c.append(header, subtitle);
document.body.classList.add('deactivated');
(popup as any).onClose = () => {
document.body.classList.add('deactivated-backwards');
singleInstance.reset();
singleInstance.checkInstance(false);
setTimeout(() => {
document.body.classList.remove('deactivated', 'deactivated-backwards');
}, 333);
};
popup.show();
});
sessionStorage.get('chatPositions').then((c) => { sessionStorage.get('chatPositions').then((c) => {
sessionStorage.setToCache('chatPositions', c || {}); sessionStorage.setToCache('chatPositions', c || {});
}); });
@ -723,7 +757,7 @@ export class AppImManager {
if(!this.myId) return Promise.resolve(); if(!this.myId) return Promise.resolve();
appUsersManager.setUserStatus(this.myId, this.offline); appUsersManager.setUserStatus(this.myId, this.offline);
return apiManager.invokeApi('account.updateStatus', {offline: this.offline}); return apiManager.invokeApiSingle('account.updateStatus', {offline: this.offline});
} }
private createNewChat() { private createNewChat() {

24
src/lib/appManagers/appMessagesManager.ts

@ -1654,6 +1654,11 @@ export class AppMessagesManager {
telegramMeWebService.setAuthorized(true); telegramMeWebService.setAuthorized(true);
} */ } */
// can reset here pinned order
if(!offsetId && !offsetDate && !offsetPeerId) {
this.dialogsStorage.resetPinnedOrder(folderId);
}
appUsersManager.saveApiUsers(dialogsResult.users); appUsersManager.saveApiUsers(dialogsResult.users);
appChatsManager.saveApiChats(dialogsResult.chats); appChatsManager.saveApiChats(dialogsResult.chats);
this.saveMessages(dialogsResult.messages); this.saveMessages(dialogsResult.messages);
@ -1666,6 +1671,12 @@ export class AppMessagesManager {
// ! нужно передавать folderId, так как по папке !== 0 нет свойства folder_id // ! нужно передавать folderId, так как по папке !== 0 нет свойства folder_id
this.dialogsStorage.saveDialog(dialog, dialog.folder_id ?? folderId); this.dialogsStorage.saveDialog(dialog, dialog.folder_id ?? folderId);
if(!maxSeenIdIncremented &&
!appPeersManager.isChannel(appPeersManager.getPeerId(dialog.peer))) {
this.incrementMaxSeenId(dialog.top_message);
maxSeenIdIncremented = true;
}
if(dialog.peerId === undefined) { if(dialog.peerId === undefined) {
return; return;
} }
@ -1690,12 +1701,6 @@ export class AppMessagesManager {
this.log.error('lun bot', folderId); this.log.error('lun bot', folderId);
} */ } */
} }
if(!maxSeenIdIncremented &&
!appPeersManager.isChannel(appPeersManager.getPeerId(dialog.peer))) {
this.incrementMaxSeenId(dialog.top_message);
maxSeenIdIncremented = true;
}
}); });
if(Object.keys(noIdsDialogs).length) { if(Object.keys(noIdsDialogs).length) {
@ -1712,8 +1717,8 @@ export class AppMessagesManager {
const count = (dialogsResult as MessagesDialogs.messagesDialogsSlice).count; const count = (dialogsResult as MessagesDialogs.messagesDialogsSlice).count;
if(!dialogsResult.dialogs.length || if(limit > dialogsResult.dialogs.length ||
!count || !count ||
dialogs.length >= count) { dialogs.length >= count) {
this.dialogsStorage.setDialogsLoaded(folderId, true); this.dialogsStorage.setDialogsLoaded(folderId, true);
} }
@ -4540,7 +4545,8 @@ export class AppMessagesManager {
delete historyStorage.maxId; delete historyStorage.maxId;
slice.unsetEnd(SliceEnd.Bottom); slice.unsetEnd(SliceEnd.Bottom);
let historyResult = this.getHistory(peerId, slice[0], 0, 50, threadId); // if there is no id - then request by first id because cannot request by id 0 with backLimit
let historyResult = this.getHistory(peerId, slice[0] ?? 1, 0, 50, threadId);
if(historyResult instanceof Promise) { if(historyResult instanceof Promise) {
historyResult = await historyResult; historyResult = await historyResult;
} }

1
src/lib/appManagers/appPhotosManager.ts

@ -240,6 +240,7 @@ export class AppPhotosManager {
if(message && if(message &&
(message.message || (message.message ||
message.reply_to_mid ||
message.media.webpage || message.media.webpage ||
(message.replies && message.replies.pFlags.comments && message.replies.channel_id !== 777) (message.replies && message.replies.pFlags.comments && message.replies.channel_id !== 777)
) )

33
src/lib/appManagers/appStateManager.ts

@ -22,6 +22,7 @@ import { Chat } from '../../layer';
import { isMobile } from '../../helpers/userAgent'; import { isMobile } from '../../helpers/userAgent';
const REFRESH_EVERY = 24 * 60 * 60 * 1000; // 1 day const REFRESH_EVERY = 24 * 60 * 60 * 1000; // 1 day
const REFRESH_EVERY_WEEK = 24 * 60 * 60 * 1000 * 7; // 7 days
const STATE_VERSION = App.version; const STATE_VERSION = App.version;
export type Background = { export type Background = {
@ -145,8 +146,10 @@ export const STATE_INIT: State = {
const ALL_KEYS = Object.keys(STATE_INIT) as any as Array<keyof State>; const ALL_KEYS = Object.keys(STATE_INIT) as any as Array<keyof State>;
const REFRESH_KEYS = ['dialogs', 'allDialogsLoaded', 'messages', 'contactsList', 'stateCreatedTime', const REFRESH_KEYS = ['contactsList', 'stateCreatedTime',
'updates', 'maxSeenMsgId', 'filters', 'topPeers', 'pinnedOrders'] as any as Array<keyof State>; 'maxSeenMsgId', 'filters', 'topPeers'] as any as Array<keyof State>;
const REFRESH_KEYS_WEEK = ['dialogs', 'allDialogsLoaded', 'updates', 'pinnedOrders'] as any as Array<keyof State>;
export class AppStateManager extends EventListenerBase<{ export class AppStateManager extends EventListenerBase<{
save: (state: State) => Promise<void>, save: (state: State) => Promise<void>,
@ -265,16 +268,28 @@ export class AppStateManager extends EventListenerBase<{
if(DEBUG) { if(DEBUG) {
this.log('will refresh state', state.stateCreatedTime, time); this.log('will refresh state', state.stateCreatedTime, time);
} }
const r = (keys: typeof REFRESH_KEYS) => {
keys.forEach(key => {
this.pushToState(key, copy(STATE_INIT[key]));
// @ts-ignore
const s = this.storagesResults[key];
if(s && s.length) {
s.length = 0;
}
});
};
REFRESH_KEYS.forEach(key => { r(REFRESH_KEYS);
this.pushToState(key, copy(STATE_INIT[key]));
// @ts-ignore if((state.stateCreatedTime + REFRESH_EVERY_WEEK) < time) {
const s = this.storagesResults[key]; if(DEBUG) {
if(s && s.length) { this.log('will refresh updates');
s.length = 0;
} }
});
r(REFRESH_KEYS_WEEK);
}
} }
//state = this.state = new Proxy(state, getHandler()); //state = this.state = new Proxy(state, getHandler());

6
src/lib/config.ts

File diff suppressed because one or more lines are too long

10
src/lib/mtproto/apiManager.ts

@ -67,7 +67,7 @@ export type ApiError = Partial<{
} */ } */
export class ApiManager { export class ApiManager {
public cachedNetworkers: { private cachedNetworkers: {
[transportType in TransportType]: { [transportType in TransportType]: {
[connectionType in ConnectionType]: { [connectionType in ConnectionType]: {
[dcId: number]: MTPNetworker[] [dcId: number]: MTPNetworker[]
@ -75,9 +75,9 @@ export class ApiManager {
} }
} = {} as any; } = {} as any;
public cachedExportPromise: {[x: number]: Promise<unknown>} = {}; private cachedExportPromise: {[x: number]: Promise<unknown>} = {};
private gettingNetworkers: {[dcIdAndType: string]: Promise<MTPNetworker>} = {}; private gettingNetworkers: {[dcIdAndType: string]: Promise<MTPNetworker>} = {};
public baseDcId = 0; private baseDcId = 0;
//public telegramMeNotified = false; //public telegramMeNotified = false;
@ -288,7 +288,9 @@ export class ApiManager {
const startTime = Date.now(); const startTime = Date.now();
const interval = ctx.setInterval(() => { const interval = ctx.setInterval(() => {
this.log.error('Request is still processing:', method, params, options, 'time:', (Date.now() - startTime) / 1000); if(!cachedNetworker || !cachedNetworker.isStopped()) {
this.log.error('Request is still processing:', method, params, options, 'time:', (Date.now() - startTime) / 1000);
}
//this.cachedUploadNetworkers[2].requestMessageStatus(); //this.cachedUploadNetworkers[2].requestMessageStatus();
}, 5e3); }, 5e3);
} }

8
src/lib/mtproto/mtproto.service.ts

@ -52,7 +52,7 @@ const onFetch = (event: FetchEvent): void => {
try { try {
const [, url, scope, params] = /http[:s]+\/\/.*?(\/(.*?)(?:$|\/(.*)$))/.exec(event.request.url) || []; const [, url, scope, params] = /http[:s]+\/\/.*?(\/(.*?)(?:$|\/(.*)$))/.exec(event.request.url) || [];
log.debug('[fetch]:', event); //log.debug('[fetch]:', event);
switch(scope) { switch(scope) {
case 'stream': { case 'stream': {
@ -71,7 +71,7 @@ const onFetch = (event: FetchEvent): void => {
offset = info.size - (info.size % limitPart); offset = info.size - (info.size % limitPart);
} */ } */
log.debug('[stream]', url, offset, end); //log.debug('[stream]', url, offset, end);
event.respondWith(Promise.race([ event.respondWith(Promise.race([
timeout(45 * 1000), timeout(45 * 1000),
@ -86,7 +86,7 @@ const onFetch = (event: FetchEvent): void => {
const limit = end && end < limitPart ? alignLimit(end - offset + 1) : limitPart; const limit = end && end < limitPart ? alignLimit(end - offset + 1) : limitPart;
const alignedOffset = alignOffset(offset, limit); const alignedOffset = alignOffset(offset, limit);
log.debug('[stream] requestFilePart:', /* info.dcId, info.location, */ alignedOffset, limit); //log.debug('[stream] requestFilePart:', /* info.dcId, info.location, */ alignedOffset, limit);
const task: ServiceWorkerTask = { const task: ServiceWorkerTask = {
type: 'requestFilePart', type: 'requestFilePart',
@ -99,7 +99,7 @@ const onFetch = (event: FetchEvent): void => {
deferred.then(result => { deferred.then(result => {
let ab = result.bytes as Uint8Array; let ab = result.bytes as Uint8Array;
log.debug('[stream] requestFilePart result:', result); //log.debug('[stream] requestFilePart result:', result);
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Accept-Ranges': 'bytes', 'Accept-Ranges': 'bytes',

7
src/lib/mtproto/mtproto.worker.ts

@ -130,6 +130,13 @@ const onMessage = async(e: any) => {
CacheStorageController.toggleStorage(enabled); CacheStorageController.toggleStorage(enabled);
break; break;
} }
case 'startAll':
case 'stopAll': {
// @ts-ignore
networkerFactory[task.task].apply(networkerFactory);
break;
}
default: { default: {
try { try {

11
src/lib/mtproto/mtprotoworker.ts

@ -20,6 +20,7 @@ import type { MTMessage } from './networker';
import DEBUG, { MOUNT_CLASS_TO } from '../../config/debug'; import DEBUG, { MOUNT_CLASS_TO } from '../../config/debug';
import Socket from './transports/websocket'; import Socket from './transports/websocket';
import IDBStorage from '../idb'; import IDBStorage from '../idb';
import singleInstance from './singleInstance';
type Task = { type Task = {
taskId: number, taskId: number,
@ -86,6 +87,8 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
super(); super();
this.log('constructor'); this.log('constructor');
singleInstance.start();
this.registerServiceWorker(); this.registerServiceWorker();
this.addTaskListener('clear', () => { this.addTaskListener('clear', () => {
@ -482,6 +485,14 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
public toggleStorage(enabled: boolean) { public toggleStorage(enabled: boolean) {
return this.performTaskWorker('toggleStorage', enabled); return this.performTaskWorker('toggleStorage', enabled);
} }
public stopAll() {
return this.performTaskWorker('stopAll');
}
public startAll() {
return this.performTaskWorker('startAll');
}
} }
const apiManagerProxy = new ApiManagerProxy(); const apiManagerProxy = new ApiManagerProxy();

63
src/lib/mtproto/networker.ts

@ -188,7 +188,7 @@ export default class MTPNetworker {
} }
} }
public updateSession() { private updateSession() {
this.seqNo = 0; this.seqNo = 0;
this.prevSessionId = this.sessionId; this.prevSessionId = this.sessionId;
this.sessionId = new Uint8Array(8).randomize(); this.sessionId = new Uint8Array(8).randomize();
@ -203,7 +203,7 @@ export default class MTPNetworker {
} }
} */ } */
public updateSentMessage(sentMessageId: string) { private updateSentMessage(sentMessageId: string) {
const sentMessage = this.sentMessages[sentMessageId]; const sentMessage = this.sentMessages[sentMessageId];
if(!sentMessage) { if(!sentMessage) {
return false; return false;
@ -233,7 +233,7 @@ export default class MTPNetworker {
return sentMessage; return sentMessage;
} }
public generateSeqNo(notContentRelated?: boolean) { private generateSeqNo(notContentRelated?: boolean) {
let seqNo = this.seqNo * 2; let seqNo = this.seqNo * 2;
if(!notContentRelated) { if(!notContentRelated) {
@ -471,11 +471,12 @@ export default class MTPNetworker {
// }; // };
/// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD /// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD
public checkLongPoll = () => { private checkLongPoll = () => {
const isClean = this.cleanupSent(); const isClean = this.cleanupSent();
//this.log.error('Check lp', this.longPollPending, this.dcId, isClean, this); //this.log.error('Check lp', this.longPollPending, this.dcId, isClean, this);
if((this.longPollPending && Date.now() < this.longPollPending) || if((this.longPollPending && Date.now() < this.longPollPending) ||
this.offline) { this.offline ||
this.isStopped()) {
//this.log('No lp this time'); //this.log('No lp this time');
return false; return false;
} }
@ -494,7 +495,7 @@ export default class MTPNetworker {
}); });
}; };
public sendLongPoll() { private sendLongPoll() {
const maxWait = 25000; const maxWait = 25000;
this.longPollPending = Date.now() + maxWait; this.longPollPending = Date.now() + maxWait;
@ -515,7 +516,7 @@ export default class MTPNetworker {
}); });
} }
public checkConnection = (event: Event | string) => { private checkConnection = (event: Event | string) => {
/* rootScope.offlineConnecting = true */ /* rootScope.offlineConnecting = true */
this.log('Check connection', event); this.log('Check connection', event);
@ -548,7 +549,7 @@ export default class MTPNetworker {
}); });
}; };
public toggleOffline(enabled: boolean) { private toggleOffline(enabled: boolean) {
// this.log('toggle ', enabled, this.dcId, this.iii) // this.log('toggle ', enabled, this.dcId, this.iii)
if(this.offline !== undefined && this.offline === enabled) { if(this.offline !== undefined && this.offline === enabled) {
return false; return false;
@ -642,7 +643,7 @@ export default class MTPNetworker {
/// #endif /// #endif
// тут можно сделать таймаут и выводить дисконнект // тут можно сделать таймаут и выводить дисконнект
public pushMessage(message: { private pushMessage(message: {
msg_id: string, msg_id: string,
seq_no: number, seq_no: number,
body: Uint8Array | number[], body: Uint8Array | number[],
@ -692,7 +693,7 @@ export default class MTPNetworker {
return promise; return promise;
} }
public setConnectionStatus(online: boolean) { public setConnectionStatus(online: boolean, timeout?: number) {
const willChange = this.isOnline !== online; const willChange = this.isOnline !== online;
this.isOnline = online; this.isOnline = online;
@ -705,10 +706,15 @@ export default class MTPNetworker {
name: this.name, name: this.name,
isFileNetworker: this.isFileNetworker, isFileNetworker: this.isFileNetworker,
isFileDownload: this.isFileDownload, isFileDownload: this.isFileDownload,
isFileUpload: this.isFileUpload isFileUpload: this.isFileUpload,
timeout
}); });
} }
if(this.isOnline) {
this.scheduleRequest();
}
// if((this.transport as TcpObfuscated).networker) { // if((this.transport as TcpObfuscated).networker) {
// this.sendPingDelayDisconnect(); // this.sendPingDelayDisconnect();
// } // }
@ -720,7 +726,7 @@ export default class MTPNetworker {
} */ } */
} }
public pushResend(messageId: string, delay = 100) { private pushResend(messageId: string, delay = 100) {
const value = delay ? Date.now() + delay : 0; const value = delay ? Date.now() + delay : 0;
const sentMessage = this.sentMessages[messageId]; const sentMessage = this.sentMessages[messageId];
if(sentMessage.container) { if(sentMessage.container) {
@ -743,7 +749,7 @@ export default class MTPNetworker {
} }
// * correct, fully checked // * correct, fully checked
public async getMsgKey(dataWithPadding: ArrayBuffer, isOut: boolean) { private async getMsgKey(dataWithPadding: ArrayBuffer, isOut: boolean) {
const x = isOut ? 0 : 8; const x = isOut ? 0 : 8;
const msgKeyLargePlain = bufferConcat(this.authKeyUint8.subarray(88 + x, 88 + x + 32), dataWithPadding); const msgKeyLargePlain = bufferConcat(this.authKeyUint8.subarray(88 + x, 88 + x + 32), dataWithPadding);
@ -753,7 +759,7 @@ export default class MTPNetworker {
}; };
// * correct, fully checked // * correct, fully checked
public getAesKeyIv(msgKey: Uint8Array | number[], isOut: boolean): Promise<[Uint8Array, Uint8Array]> { private getAesKeyIv(msgKey: Uint8Array | number[], isOut: boolean): Promise<[Uint8Array, Uint8Array]> {
const x = isOut ? 0 : 8; const x = isOut ? 0 : 8;
const sha2aText = new Uint8Array(52); const sha2aText = new Uint8Array(52);
const sha2bText = new Uint8Array(52); const sha2bText = new Uint8Array(52);
@ -785,9 +791,17 @@ export default class MTPNetworker {
}); });
} }
public isStopped() {
return NetworkerFactory.akStopped && !this.isFileNetworker;
}
private performScheduledRequest() { private performScheduledRequest() {
// this.log('scheduled', this.dcId, this.iii) // this.log('scheduled', this.dcId, this.iii)
if(this.isStopped()) {
return false;
}
if(this.pendingAcks.length) { if(this.pendingAcks.length) {
const ackMsgIds: Array<string> = this.pendingAcks.slice(); const ackMsgIds: Array<string> = this.pendingAcks.slice();
@ -936,7 +950,7 @@ export default class MTPNetworker {
if(lengthOverflow) { if(lengthOverflow) {
this.scheduleRequest(); this.scheduleRequest();
} }
}; }
private generateContainerMessage(messagesByteLen: number, messages: MTMessage[]) { private generateContainerMessage(messagesByteLen: number, messages: MTMessage[]) {
const container = new TLSerialization({ const container = new TLSerialization({
@ -973,7 +987,7 @@ export default class MTPNetworker {
}; };
} }
public async getEncryptedMessage(dataWithPadding: ArrayBuffer) { private async getEncryptedMessage(dataWithPadding: ArrayBuffer) {
const msgKey = await this.getMsgKey(dataWithPadding, true); const msgKey = await this.getMsgKey(dataWithPadding, true);
const keyIv = await this.getAesKeyIv(msgKey, true); const keyIv = await this.getAesKeyIv(msgKey, true);
// this.log('after msg key iv') // this.log('after msg key iv')
@ -987,7 +1001,7 @@ export default class MTPNetworker {
}; };
} }
public getDecryptedMessage(msgKey: Uint8Array, encryptedData: Uint8Array): Promise<ArrayBuffer> { private getDecryptedMessage(msgKey: Uint8Array, encryptedData: Uint8Array): Promise<ArrayBuffer> {
// this.log('get decrypted start') // this.log('get decrypted start')
return this.getAesKeyIv(msgKey, false).then((keyIv) => { return this.getAesKeyIv(msgKey, false).then((keyIv) => {
// this.log('after msg key iv') // this.log('after msg key iv')
@ -995,7 +1009,7 @@ export default class MTPNetworker {
}); });
} }
public getEncryptedOutput(message: MTMessage) { private getEncryptedOutput(message: MTMessage) {
/* if(DEBUG) { /* if(DEBUG) {
this.log.debug('Send encrypted', message, this.authKeyId); this.log.debug('Send encrypted', message, this.authKeyId);
} */ } */
@ -1088,7 +1102,7 @@ export default class MTPNetworker {
}); });
} }
public sendEncryptedRequest(message: MTMessage) { private sendEncryptedRequest(message: MTMessage) {
return this.getEncryptedOutput(message).then(requestData => { return this.getEncryptedOutput(message).then(requestData => {
this.debug && this.log.debug('sendEncryptedRequest: launching message into space:', message, [message.msg_id].concat(message.inner || [])); this.debug && this.log.debug('sendEncryptedRequest: launching message into space:', message, [message.msg_id].concat(message.inner || []));
@ -1242,7 +1256,7 @@ export default class MTPNetworker {
}); });
} }
public applyServerSalt(newServerSalt: string) { private applyServerSalt(newServerSalt: string) {
const serverSalt = longToBytes(newServerSalt); const serverSalt = longToBytes(newServerSalt);
sessionStorage.set({ sessionStorage.set({
@ -1313,7 +1327,7 @@ export default class MTPNetworker {
} }
} }
public ackMessage(msgId: string) { private ackMessage(msgId: string) {
// this.log('ack message', msgID) // this.log('ack message', msgID)
this.pendingAcks.push(msgId); this.pendingAcks.push(msgId);
@ -1324,7 +1338,7 @@ export default class MTPNetworker {
/// #endif /// #endif
} }
public reqResendMessage(msgId: string) { private reqResendMessage(msgId: string) {
if(this.debug) { if(this.debug) {
this.log.debug('Req resend', msgId); this.log.debug('Req resend', msgId);
} }
@ -1361,7 +1375,7 @@ export default class MTPNetworker {
return !notEmpty; return !notEmpty;
} }
public processMessageAck(messageId: string) { private processMessageAck(messageId: string) {
const sentMessage = this.sentMessages[messageId]; const sentMessage = this.sentMessages[messageId];
if(sentMessage && !sentMessage.acked) { if(sentMessage && !sentMessage.acked) {
//delete sentMessage.body; //delete sentMessage.body;
@ -1369,7 +1383,7 @@ export default class MTPNetworker {
} }
} }
public processError(rawError: {error_message: string, error_code: number}) { private processError(rawError: {error_message: string, error_code: number}) {
const matches = (rawError.error_message || '').match(/^([A-Z_0-9]+\b)(: (.+))?/) || []; const matches = (rawError.error_message || '').match(/^([A-Z_0-9]+\b)(: (.+))?/) || [];
rawError.error_code = rawError.error_code; rawError.error_code = rawError.error_code;
@ -1518,7 +1532,6 @@ export default class MTPNetworker {
break; break;
} }
case 'new_session_created': { case 'new_session_created': {
this.ackMessage(messageId); this.ackMessage(messageId);

11
src/lib/mtproto/networkerFactory.ts

@ -14,6 +14,7 @@ import { ConnectionStatusChange, InvokeApiOptions } from "../../types";
import MTTransport from "./transports/transport"; import MTTransport from "./transports/transport";
export class NetworkerFactory { export class NetworkerFactory {
private networkers: MTPNetworker[] = [];
public updatesProcessor: (obj: any) => void = null; public updatesProcessor: (obj: any) => void = null;
public onConnectionStatusChange: (info: ConnectionStatusChange) => void = null; public onConnectionStatusChange: (info: ConnectionStatusChange) => void = null;
public akStopped = false; public akStopped = false;
@ -24,13 +25,21 @@ export class NetworkerFactory {
public getNetworker(dcId: number, authKey: number[], authKeyID: Uint8Array, serverSalt: number[], transport: MTTransport, options: InvokeApiOptions) { public getNetworker(dcId: number, authKey: number[], authKeyID: Uint8Array, serverSalt: number[], transport: MTTransport, options: InvokeApiOptions) {
//console.log('NetworkerFactory: creating new instance of MTPNetworker:', dcId, options); //console.log('NetworkerFactory: creating new instance of MTPNetworker:', dcId, options);
return new MTPNetworker(dcId, authKey, authKeyID, serverSalt, transport, options); const networker = new MTPNetworker(dcId, authKey, authKeyID, serverSalt, transport, options);
this.networkers.push(networker);
return networker;
} }
public startAll() { public startAll() {
if(this.akStopped) { if(this.akStopped) {
const stoppedNetworkers = this.networkers.filter(networker => networker.isStopped());
this.akStopped = false; this.akStopped = false;
this.updatesProcessor && this.updatesProcessor({_: 'new_session_created'}); this.updatesProcessor && this.updatesProcessor({_: 'new_session_created'});
for(const networker of stoppedNetworkers) {
networker.scheduleRequest();
}
} }
} }

56
src/lib/mtproto/singleInstance.ts

@ -3,6 +3,7 @@ import { nextRandomInt } from "../../helpers/random";
import { logger } from "../logger"; import { logger } from "../logger";
import rootScope from "../rootScope"; import rootScope from "../rootScope";
import sessionStorage from "../sessionStorage"; import sessionStorage from "../sessionStorage";
import apiManager from "./mtprotoworker";
export type AppInstance = { export type AppInstance = {
id: number, id: number,
@ -10,23 +11,28 @@ export type AppInstance = {
time: number time: number
}; };
const CHECK_INSTANCE_INTERVAL = 5000;
const DEACTIVATE_TIMEOUT = 30000;
const MULTIPLE_TABS_THRESHOLD = 20000;
export class SingleInstance { export class SingleInstance {
private instanceID = nextRandomInt(0xFFFFFFFF); private instanceID: number;
private started = false; private started: boolean;
private masterInstance = false; private masterInstance: boolean;
private deactivateTimeout: number = 0; private deactivateTimeout: number;
private deactivated = false; private deactivated: boolean;
private initial = false; private initial: boolean;
private log = logger('SI'); private log = logger('INSTANCE');
public start() { public start() {
if(!this.started/* && !Config.Navigator.mobile && !Config.Modes.packed */) { if(!this.started/* && !Config.Navigator.mobile && !Config.Modes.packed */) {
this.started = true this.started = true;
this.reset();
//IdleManager.start(); //IdleManager.start();
rootScope.addEventListener('idle', this.checkInstance); rootScope.addEventListener('idle', this.checkInstance);
setInterval(this.checkInstance, 5000); setInterval(this.checkInstance, CHECK_INSTANCE_INTERVAL);
this.checkInstance(); this.checkInstance();
try { try {
@ -35,12 +41,21 @@ export class SingleInstance {
} }
} }
public clearInstance() { public reset() {
this.instanceID = nextRandomInt(0xFFFFFFFF);
this.masterInstance = false;
if(this.deactivateTimeout) clearTimeout(this.deactivateTimeout);
this.deactivateTimeout = 0;
this.deactivated = false;
this.initial = false;
}
public clearInstance = () => {
if(this.masterInstance && !this.deactivated) { if(this.masterInstance && !this.deactivated) {
this.log.warn('clear master instance'); this.log.warn('clear master instance');
sessionStorage.delete('xt_instance'); sessionStorage.delete('xt_instance');
} }
} };
public deactivateInstance = () => { public deactivateInstance = () => {
if(this.masterInstance || this.deactivated) { if(this.masterInstance || this.deactivated) {
@ -56,30 +71,31 @@ export class SingleInstance {
//document.title = _('inactive_tab_title_raw') //document.title = _('inactive_tab_title_raw')
rootScope.idle.deactivated = true; rootScope.idle.deactivated = true;
rootScope.dispatchEvent('instance_deactivated');
}; };
public checkInstance = () => { public checkInstance = (idle = rootScope.idle && rootScope.idle.isIDLE) => {
if(this.deactivated) { if(this.deactivated) {
return false; return false;
} }
const time = Date.now(); const time = Date.now();
const idle = rootScope.idle && rootScope.idle.isIDLE;
const newInstance: AppInstance = { const newInstance: AppInstance = {
id: this.instanceID, id: this.instanceID,
idle, idle,
time time
}; };
sessionStorage.get('xt_instance').then((curInstance: AppInstance) => { sessionStorage.get('xt_instance', false).then((curInstance: AppInstance) => {
// console.log(dT(), 'check instance', newInstance, curInstance) // this.log('check instance', newInstance, curInstance)
if(!idle || if(!idle ||
!curInstance || !curInstance ||
curInstance.id == this.instanceID || curInstance.id === this.instanceID ||
curInstance.time < time - 20000) { curInstance.time < (time - MULTIPLE_TABS_THRESHOLD)) {
sessionStorage.set({xt_instance: newInstance}); sessionStorage.set({xt_instance: newInstance});
if(!this.masterInstance) { if(!this.masterInstance) {
//MtpNetworkerFactory.startAll(); apiManager.startAll();
if(!this.initial) { if(!this.initial) {
this.initial = true; this.initial = true;
} else { } else {
@ -95,10 +111,10 @@ export class SingleInstance {
} }
} else { } else {
if(this.masterInstance) { if(this.masterInstance) {
//MtpNetworkerFactory.stopAll(); apiManager.stopAll();
this.log.warn('now idle instance', newInstance); this.log.warn('now idle instance', newInstance);
if(!this.deactivateTimeout) { if(!this.deactivateTimeout) {
this.deactivateTimeout = window.setTimeout(this.deactivateInstance, 30000); this.deactivateTimeout = window.setTimeout(this.deactivateInstance, DEACTIVATE_TIMEOUT);
} }
this.masterInstance = false; this.masterInstance = false;

11
src/lib/mtproto/transports/tcpObfuscated.ts

@ -28,11 +28,16 @@ export default class TcpObfuscated implements MTTransport {
private log: ReturnType<typeof logger>; private log: ReturnType<typeof logger>;
public connected = false; public connected = false;
private lastCloseTime: number; private lastCloseTime: number;
public connection: MTConnection; private connection: MTConnection;
//private debugPayloads: MTPNetworker['debugRequests'] = []; //private debugPayloads: MTPNetworker['debugRequests'] = [];
constructor(private Connection: MTConnectionConstructable, private dcId: number, private url: string, private logSuffix: string, public retryTimeout: number) { constructor(private Connection: MTConnectionConstructable,
private dcId: number,
private url: string,
private logSuffix: string,
private retryTimeout: number
) {
let logTypes = LogTypes.Error | LogTypes.Log; let logTypes = LogTypes.Error | LogTypes.Log;
if(this.debug) logTypes |= LogTypes.Debug; if(this.debug) logTypes |= LogTypes.Debug;
this.log = logger(`TCP-${dcId}` + logSuffix, logTypes); this.log = logger(`TCP-${dcId}` + logSuffix, logTypes);
@ -115,7 +120,7 @@ export default class TcpObfuscated implements MTTransport {
const needTimeout = !isNaN(diff) && diff < this.retryTimeout ? this.retryTimeout - diff : 0; const needTimeout = !isNaN(diff) && diff < this.retryTimeout ? this.retryTimeout - diff : 0;
if(this.networker) { if(this.networker) {
this.networker.setConnectionStatus(false); this.networker.setConnectionStatus(false, needTimeout);
this.pending.length = 0; this.pending.length = 0;
} }

21
src/lib/richtextprocessor.ts

@ -106,7 +106,8 @@ const markdownEntities: {[markdown: string]: MessageEntity['_']} = {
const passConflictingEntities: Set<MessageEntity['_']> = new Set([ const passConflictingEntities: Set<MessageEntity['_']> = new Set([
'messageEntityEmoji', 'messageEntityEmoji',
'messageEntityLinebreak' 'messageEntityLinebreak',
'messageEntityCaret'
]); ]);
for(let i in markdownEntities) { for(let i in markdownEntities) {
passConflictingEntities.add(markdownEntities[i]); passConflictingEntities.add(markdownEntities[i]);
@ -116,19 +117,20 @@ namespace RichTextProcessor {
export const emojiSupported = navigator.userAgent.search(/OS X|iPhone|iPad|iOS/i) !== -1/* && false *//* || true */; export const emojiSupported = navigator.userAgent.search(/OS X|iPhone|iPad|iOS/i) !== -1/* && false *//* || true */;
export function getEmojiSpritesheetCoords(emojiCode: string) { export function getEmojiSpritesheetCoords(emojiCode: string) {
let unified = encodeEmoji(emojiCode)/* .replace(/(-fe0f|fe0f)/g, '') */; let unified = encodeEmoji(emojiCode);
if(unified === '1f441-200d-1f5e8') { if(unified === '1f441-200d-1f5e8') {
unified = '1f441-fe0f-200d-1f5e8-fe0f'; //unified = '1f441-fe0f-200d-1f5e8-fe0f';
unified = '1f441-fe0f-200d-1f5e8';
} }
if(!emojiData.hasOwnProperty(unified)/* && !emojiData.hasOwnProperty(unified.replace(/(-fe0f|fe0f)/g, '')) */) { if(!emojiData.hasOwnProperty(unified) && !emojiData.hasOwnProperty(unified.replace(/-?fe0f$/, ''))/* && !emojiData.hasOwnProperty(unified.replace(/(-fe0f|fe0f)/g, '')) */) {
//if(!emojiData.hasOwnProperty(emojiCode) && !emojiData.hasOwnProperty(emojiCode.replace(/[\ufe0f\u200d]/g, ''))) { //if(!emojiData.hasOwnProperty(emojiCode) && !emojiData.hasOwnProperty(emojiCode.replace(/[\ufe0f\u200d]/g, ''))) {
//console.error('lol', unified); //console.error('lol', unified);
return null; return null;
} }
return unified.replace(/(-fe0f|fe0f)/g, ''); return unified.replace(/-?fe0f/g, '');
} }
export function parseEntities(text: string) { export function parseEntities(text: string) {
@ -138,6 +140,7 @@ namespace RichTextProcessor {
let matchIndex; let matchIndex;
let rawOffset = 0; let rawOffset = 0;
// var start = tsNow() // var start = tsNow()
fullRegExp.lastIndex = 0;
while((match = raw.match(fullRegExp))) { while((match = raw.match(fullRegExp))) {
matchIndex = rawOffset + match.index; matchIndex = rawOffset + match.index;
@ -540,6 +543,11 @@ namespace RichTextProcessor {
break; break;
} }
case 'messageEntityCaret': {
insertPart(entity, '<span class="composer-sel"></span>');
break;
}
/* case 'messageEntityLinebreak': { /* case 'messageEntityLinebreak': {
if(options.noLinebreaks) { if(options.noLinebreaks) {
@ -587,7 +595,7 @@ namespace RichTextProcessor {
const target = (currentContext || typeof electronHelpers !== 'undefined') const target = (currentContext || typeof electronHelpers !== 'undefined')
? '' : ' target="_blank" rel="noopener noreferrer"'; ? '' : ' target="_blank" rel="noopener noreferrer"';
insertPart(entity, `<a class="anchor-url" href="${href}"${target}${masked ? 'onclick="showMaskedAlert(this)"' : ''}>`, '</a>'); insertPart(entity, `<a class="anchor-url" href="${href}"${target}${masked && !currentContext ? 'onclick="showMaskedAlert(this)"' : ''}>`, '</a>');
} }
break; break;
@ -714,6 +722,7 @@ namespace RichTextProcessor {
var raw = text; var raw = text;
var text: any = [], var text: any = [],
emojiTitle; emojiTitle;
fullRegExp.lastIndex = 0;
while((match = raw.match(fullRegExp))) { while((match = raw.match(fullRegExp))) {
text.push(raw.substr(0, match.index)) text.push(raw.substr(0, match.index))
if(match[8]) { if(match[8]) {

2
src/lib/rootScope.ts

@ -111,6 +111,8 @@ export type BroadcastEvents = {
'language_change': void, 'language_change': void,
'theme_change': void, 'theme_change': void,
'instance_deactivated': void
}; };
export class RootScope extends EventListenerBase<{ export class RootScope extends EventListenerBase<{

10
src/lib/searchIndex.ts

@ -14,12 +14,16 @@ import cleanSearchText from '../helpers/cleanSearchText';
export default class SearchIndex<SearchWhat> { export default class SearchIndex<SearchWhat> {
private fullTexts: Map<SearchWhat, string> = new Map(); private fullTexts: Map<SearchWhat, string> = new Map();
constructor(private cleanText = true, private latinize = true) {
}
public indexObject(id: SearchWhat, searchText: string) { public indexObject(id: SearchWhat, searchText: string) {
/* if(searchIndex.fullTexts.hasOwnProperty(id)) { /* if(searchIndex.fullTexts.hasOwnProperty(id)) {
return false; return false;
} */ } */
if(searchText.trim()) { if(searchText.trim() && this.cleanText) {
searchText = cleanSearchText(searchText); searchText = cleanSearchText(searchText);
} }
@ -49,7 +53,9 @@ export default class SearchIndex<SearchWhat> {
const fullTexts = this.fullTexts; const fullTexts = this.fullTexts;
//const shortIndexes = searchIndex.shortIndexes; //const shortIndexes = searchIndex.shortIndexes;
query = cleanSearchText(query); if(this.cleanText) {
query = cleanSearchText(query, this.latinize);
}
const newFoundObjs: Array<{fullText: string, what: SearchWhat}> = []; const newFoundObjs: Array<{fullText: string, what: SearchWhat}> = [];
const queryWords = query.split(' '); const queryWords = query.split(' ');

4
src/lib/storage.ts

@ -143,8 +143,8 @@ export default class AppStorage<Storage extends Record<string, any>/* Storage ex
return this.cache[key] = value; return this.cache[key] = value;
} }
public async get(key: keyof Storage): Promise<Storage[typeof key]> { public async get(key: keyof Storage, useCache = true): Promise<Storage[typeof key]> {
if(this.cache.hasOwnProperty(key)) { if(this.cache.hasOwnProperty(key) && useCache) {
return this.getFromCache(key); return this.getFromCache(key);
} else if(this.useStorage) { } else if(this.useStorage) {
const r = this.getPromises.get(key); const r = this.getPromises.get(key);

4
src/lib/storages/dialogs.ts

@ -133,6 +133,10 @@ export default class DialogsStorage {
this.dialogsNum = 0; this.dialogsNum = 0;
} }
public resetPinnedOrder(folderId: number) {
this.pinnedOrders[folderId] = [];
}
public getOffsetDate(folderId: number) { public getOffsetDate(folderId: number) {
return this.dialogsOffsetDate[folderId] || 0; return this.dialogsOffsetDate[folderId] || 0;
} }

9
src/scripts/format_jsons.js

@ -12,6 +12,10 @@ let countries = require('fs').readFileSync('./in/countries.dat').toString();
//console.log(emoji, countries); //console.log(emoji, countries);
const path = process.argv[2];
const writePathTo = (/* path || */__dirname + '/out/');
console.log('Writing to:', writePathTo);
let formatted = emoji.filter(e => e.has_img_apple); let formatted = emoji.filter(e => e.has_img_apple);
function encodeEmoji(emojiText) { function encodeEmoji(emojiText) {
@ -150,6 +154,7 @@ if(false) {
emoji = encodeEmoji(emoji); emoji = encodeEmoji(emoji);
//emoji = emoji.replace(/(-fe0f|fe0f)/g, ''); //emoji = emoji.replace(/(-fe0f|fe0f)/g, '');
emoji = emoji.replace(/-?fe0f$/, '');
let c = categories[category] === undefined ? 9 : categories[category]; let c = categories[category] === undefined ? 9 : categories[category];
//obj[emoji] = '' + c + sort_order; //obj[emoji] = '' + c + sort_order;
@ -159,7 +164,7 @@ if(false) {
console.log(obj); console.log(obj);
require('fs').writeFileSync('./out/emoji.json', JSON.stringify(obj)); require('fs').writeFileSync(writePathTo + 'emoji.json', JSON.stringify(obj));
} }
/* { /* {
@ -209,5 +214,5 @@ if(false) {
//console.log(item); //console.log(item);
}); });
require('fs').writeFileSync('./out/countries.json', JSON.stringify(arr)); require('fs').writeFileSync(writePathTo + 'countries.json', JSON.stringify(arr));
} }

20820
src/scripts/in/emoji_pretty.json

File diff suppressed because it is too large Load Diff

7
src/scripts/in/schema_additional_params.json

@ -94,6 +94,13 @@
{"name": "length", "type": "number"} {"name": "length", "type": "number"}
], ],
"type": "MessageEntity" "type": "MessageEntity"
}, {
"predicate": "messageEntityCaret",
"params": [
{"name": "offset", "type": "number"},
{"name": "length", "type": "number"}
],
"type": "MessageEntity"
}, { }, {
"predicate": "user", "predicate": "user",
"params": [ "params": [

2
src/scripts/out/countries.json

File diff suppressed because one or more lines are too long

2
src/scripts/out/emoji.json

File diff suppressed because one or more lines are too long

5
src/scss/partials/_chatBubble.scss

@ -824,9 +824,12 @@ $bubble-margin: .25rem;
margin: 0 4px 6px 4px; margin: 0 4px 6px 4px;
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 4px;
min-width: 10rem;
&-content { &-content {
max-width: 300px; //max-width: 300px;
position: absolute;
max-width: calc(100% - 24px);
} }
} }

12
src/scss/partials/_chatEmojiHelper.scss

@ -12,8 +12,12 @@
} }
.super-emoji:not(.active) { .super-emoji:not(.active) {
@include hover() { @include hover() {
background: none; background: none;
} }
} }
.super-emoji.active {
background-color: var(--primary-color) !important;
}
} }

6
src/scss/partials/_chatlist.scss

@ -76,11 +76,11 @@
} }
ul.chatlist { ul.chatlist {
padding: 0 .5rem .5rem; padding: 0 .5rem/* .5rem */;
@include respond-to(handhelds) { /* @include respond-to(handhelds) {
padding: 0 0 .5rem; padding: 0 0 .5rem;
} } */
} }
.chatlist { .chatlist {

22
src/scss/partials/popups/_instanceDeactivated.scss

@ -0,0 +1,22 @@
.popup-instance-deactivated {
background-color: rgba(0, 0, 0, .6);
.instance-deactivated-container {
margin: auto;
text-align: center;
pointer-events: none;
}
.header {
font-size: 2rem;
color: #fff;
//line-height: var(--line-height);
}
.subtitle {
color: #fff;
opacity: .6;
font-size: 1.5rem;
line-height: var(--line-height);
}
}

29
src/scss/style.scss

@ -270,6 +270,7 @@ html.night {
@import "partials/popups/datePicker"; @import "partials/popups/datePicker";
@import "partials/popups/createPoll"; @import "partials/popups/createPoll";
@import "partials/popups/forward"; @import "partials/popups/forward";
@import "partials/popups/instanceDeactivated";
@import "partials/pages/pages"; @import "partials/pages/pages";
@import "partials/pages/authCode"; @import "partials/pages/authCode";
@ -419,6 +420,34 @@ body {
color: var(--primary-text-color); color: var(--primary-text-color);
} }
body.deactivated {
animation: grayscale-in var(--transition-standard-in) forwards;
}
body.deactivated-backwards {
animation: grayscale-out var(--transition-standard-out) forwards;
}
@keyframes grayscale-in {
0% {
filter: grayscale(0);
}
100% {
filter: grayscale(1);
}
}
@keyframes grayscale-out {
0% {
filter: grayscale(1);
}
100% {
filter: grayscale(0);
}
}
/* body { /* body {
position: absolute; position: absolute;
top: 0; top: 0;

3
src/types.d.ts vendored

@ -81,5 +81,6 @@ export type ConnectionStatusChange = {
name: string, name: string,
isFileNetworker: boolean, isFileNetworker: boolean,
isFileDownload: boolean, isFileDownload: boolean,
isFileUpload: boolean isFileUpload: boolean,
timeout?: number
}; };

19
src/vendor/emoji/regex.ts vendored

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save