Browse Source

Emoji parsing fixes

master
morethanwords 4 years ago
parent
commit
d9fb248fc3
  1. 7
      src/components/emoticonsDropdown/index.ts
  2. 22
      src/components/emoticonsDropdown/tabs/emoji.ts
  3. 6
      src/lib/appManagers/appMessagesManager.ts
  4. 4
      src/lib/appManagers/appStickersManager.ts
  5. 2
      src/lib/config.ts
  6. 52
      src/lib/richtextprocessor.ts
  7. 156
      src/scripts/emoji_compile_regex.js
  8. 27
      src/scripts/format_emoji_regex.js
  9. 4
      src/scripts/format_jsons.js
  10. 4879
      src/scripts/in/emoji_test.txt
  11. 2
      src/scripts/out/emoji.json
  12. 1
      src/scripts/out/emoji_regex.txt
  13. 20
      src/vendor/emoji/regex.ts

7
src/components/emoticonsDropdown/index.ts

@ -27,6 +27,7 @@ import ListenerSetter from "../../helpers/listenerSetter"; @@ -27,6 +27,7 @@ import ListenerSetter from "../../helpers/listenerSetter";
import blurActiveElement from "../../helpers/dom/blurActiveElement";
import { attachClickEvent } from "../../helpers/dom/clickEvent";
import whichChild from "../../helpers/dom/whichChild";
import { cancelEvent } from "../../helpers/dom/cancelEvent";
export const EMOTICONSSTICKERGROUP = 'emoticons-dropdown';
@ -158,7 +159,7 @@ export class EmoticonsDropdown { @@ -158,7 +159,7 @@ export class EmoticonsDropdown {
});
this.deleteBtn = this.element.querySelector('.emoji-tabs-delete');
this.deleteBtn.addEventListener('click', () => {
this.deleteBtn.addEventListener('click', (e) => {
const input = appImManager.chat.input.messageInput;
if((input.lastChild as any)?.tagName) {
input.lastElementChild.remove();
@ -173,6 +174,8 @@ export class EmoticonsDropdown { @@ -173,6 +174,8 @@ export class EmoticonsDropdown {
const event = new Event('input', {bubbles: true, cancelable: true});
appImManager.chat.input.messageInput.dispatchEvent(event);
//appSidebarRight.stickersTab.init();
cancelEvent(e);
});
(this.tabsEl.children[1] as HTMLLIElement).click(); // set emoji tab
@ -254,7 +257,7 @@ export class EmoticonsDropdown { @@ -254,7 +257,7 @@ export class EmoticonsDropdown {
this.events.onOpen.forEach(cb => cb());
const sel = document.getSelection();
if(!sel.isCollapsed) {
if(sel.rangeCount) {
this.savedRange = sel.getRangeAt(0);
}

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

@ -5,8 +5,10 @@ @@ -5,8 +5,10 @@
*/
import emoticonsDropdown, { EmoticonsDropdown, EmoticonsTab } from "..";
import { cancelEvent } from "../../../helpers/dom/cancelEvent";
import findUpClassName from "../../../helpers/dom/findUpClassName";
import { fastRaf, pause } from "../../../helpers/schedulers";
import { isTouchSupported } from "../../../helpers/touchSupport";
import appEmojiManager from "../../../lib/appManagers/appEmojiManager";
import appImManager from "../../../lib/appManagers/appImManager";
import Config from "../../../lib/config";
@ -30,6 +32,7 @@ export function appendEmoji(emoji: string, container: HTMLElement, prepend = fal @@ -30,6 +32,7 @@ export function appendEmoji(emoji: string, container: HTMLElement, prepend = fal
if(unify) {
kek = RichTextProcessor.wrapSingleEmoji(emoji);
} else {
emoji = RichTextProcessor.fixEmoji(emoji);
kek = RichTextProcessor.wrapEmojiText(emoji);
}
@ -87,7 +90,7 @@ export function appendEmoji(emoji: string, container: HTMLElement, prepend = fal @@ -87,7 +90,7 @@ export function appendEmoji(emoji: string, container: HTMLElement, prepend = fal
export function getEmojiFromElement(element: HTMLElement) {
if(element.nodeType === 3) return element.nodeValue;
if(element.tagName === 'SPAN' && !element.classList.contains('emoji')) {
if(element.tagName === 'SPAN' && !element.classList.contains('emoji') && element.firstElementChild) {
element = element.firstElementChild as HTMLElement;
}
@ -236,6 +239,7 @@ export default class EmojiTab implements EmoticonsTab { @@ -236,6 +239,7 @@ export default class EmojiTab implements EmoticonsTab {
}
onContentClick = (e: MouseEvent) => {
cancelEvent(e);
let target = e.target as HTMLElement;
//if(target.tagName !== 'SPAN') return;
@ -249,9 +253,10 @@ export default class EmojiTab implements EmoticonsTab { @@ -249,9 +253,10 @@ export default class EmojiTab implements EmoticonsTab {
} else if(target.tagName === 'DIV') return;
// set selection range
const savedRange = emoticonsDropdown.getSavedRange();
const savedRange = isTouchSupported ? undefined : emoticonsDropdown.getSavedRange();
let sel: Selection;
if(savedRange) {
const sel = document.getSelection();
sel = document.getSelection();
sel.removeAllRanges();
sel.addRange(savedRange);
}
@ -260,8 +265,17 @@ export default class EmojiTab implements EmoticonsTab { @@ -260,8 +265,17 @@ export default class EmojiTab implements EmoticonsTab {
(target.nodeType === 3 ? target.nodeValue : target.innerHTML) :
target.outerHTML;
// insert emoji in input
if((document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.hasAttribute('contenteditable'))) ||
savedRange) {
document.execCommand('insertHTML', true, html);
} else {
appImManager.chat.input.messageInput.innerHTML += html;
}
/* if(sel && isTouchSupported) {
sel.removeRange(savedRange);
blurActiveElement();
} */
// Recent
const emoji = getEmojiFromElement(target);

6
src/lib/appManagers/appMessagesManager.ts

@ -2403,9 +2403,11 @@ export class AppMessagesManager { @@ -2403,9 +2403,11 @@ export class AppMessagesManager {
} */
if(message.message && message.message.length && !message.totalEntities) {
const apiEntities = message.entities ? message.entities.slice() : [];
message.message = RichTextProcessor.fixEmoji(message.message, apiEntities);
const myEntities = RichTextProcessor.parseEntities(message.message);
const apiEntities = message.entities || [];
message.totalEntities = RichTextProcessor.mergeEntities(apiEntities.slice(), myEntities); // ! only in this order, otherwise bold and emoji formatting won't work
message.totalEntities = RichTextProcessor.mergeEntities(apiEntities, myEntities); // ! only in this order, otherwise bold and emoji formatting won't work
}
storage[mid] = message;

4
src/lib/appManagers/appStickersManager.ts

@ -24,14 +24,14 @@ export class AppStickersManager { @@ -24,14 +24,14 @@ export class AppStickersManager {
private getStickersByEmoticonsPromises: {[emoticon: string]: Promise<Document[]>} = {};
constructor() {
this.getStickerSet({id: 'emoji', access_hash: ''}, {overwrite: true});
this.getStickerSet({id: 'emoji', access_hash: ''});
rootScope.addMultipleEventsListeners({
updateNewStickerSet: (update) => {
this.saveStickerSet(update.stickerset, update.stickerset.set.id);
rootScope.broadcast('stickers_installed', update.stickerset.set);
}
})
});
}
public saveStickers(docs: Document[]) {

2
src/lib/config.ts

File diff suppressed because one or more lines are too long

52
src/lib/richtextprocessor.ts

@ -117,20 +117,21 @@ namespace RichTextProcessor { @@ -117,20 +117,21 @@ namespace RichTextProcessor {
export const emojiSupported = navigator.userAgent.search(/OS X|iPhone|iPad|iOS/i) !== -1/* && false *//* || true */;
export function getEmojiSpritesheetCoords(emojiCode: string) {
let unified = encodeEmoji(emojiCode);
let unified = encodeEmoji(emojiCode).replace(/-?fe0f/g, '');
if(unified === '1f441-200d-1f5e8') {
/* if(unified === '1f441-200d-1f5e8') {
//unified = '1f441-fe0f-200d-1f5e8-fe0f';
unified = '1f441-fe0f-200d-1f5e8';
}
} */
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(unified)
// && !emojiData.hasOwnProperty(unified.replace(/-?fe0f$/, ''))
) {
//console.error('lol', unified);
return null;
}
return unified.replace(/-?fe0f/g, '');
return unified;
}
export function parseEntities(text: string) {
@ -530,12 +531,13 @@ namespace RichTextProcessor { @@ -530,12 +531,13 @@ namespace RichTextProcessor {
}
case 'messageEntityEmoji': {
if(!(options.wrappingDraft && emojiSupported)) { // * fix safari emoji
if(emojiSupported) { // ! contenteditable="false" нужен для поля ввода, иначе там будет меняться шрифт в Safari, или же рендерить смайлик напрямую, без контейнера
insertPart(entity, '<span class="emoji">', '</span>');
} else {
//if(!(options.wrappingDraft && emojiSupported)) { // * fix safari emoji
if(!emojiSupported) { // no wrapping needed
// if(emojiSupported) { // ! contenteditable="false" нужен для поля ввода, иначе там будет меняться шрифт в Safari, или же рендерить смайлик напрямую, без контейнера
// insertPart(entity, '<span class="emoji">', '</span>');
// } else {
insertPart(entity, `<img src="assets/img/emoji/${entity.unicode}.png" alt="`, `" class="emoji">`);
}
// }
}
/* if(!emojiSupported) {
insertPart(entity, `<img src="assets/img/emoji/${entity.unicode}.png" alt="`, `" class="emoji">`);
@ -665,6 +667,34 @@ namespace RichTextProcessor { @@ -665,6 +667,34 @@ namespace RichTextProcessor {
return out;
}
export function fixEmoji(text: string, entities?: MessageEntity[]) {
/* if(!emojiSupported) {
return text;
} */
// '$`\ufe0f'
text = text.replace(/[\u2640\u2642\u2764](?!\ufe0f)/g, (match, offset, string) => {
if(entities) {
const length = match.length;
offset += length;
entities.forEach(entity => {
const end = entity.offset + entity.length;
if(end === offset) { // current entity
entity.length += length;
} else if(end > offset) {
entity.offset += length;
}
});
}
// console.log([match, offset, string]);
return match + '\ufe0f';
});
return text;
}
export function wrapDraftText(text: string, options: Partial<{
entities: MessageEntity[]
}> = {}) {

156
src/scripts/emoji_compile_regex.js

@ -0,0 +1,156 @@ @@ -0,0 +1,156 @@
// @ts-check
const fs = require('fs');
const data = fs.readFileSync(__dirname + '/in/emoji_test.txt').toString();
/** @type {number[][]} */
const codepoints = [];
/** @type {Map<number, number[][]>} */
const codepointsByLength = new Map();
data.split('\n').forEach(line => {
if(!line || /^#/.test(line)) {
return;
}
const splitted = line.split(';');
if(splitted.length < 2 || !splitted[1].includes('fully-qualified')) {
return;
}
const a = String.fromCodePoint(...splitted[0].trim().split(' ').map((hex) => parseInt(hex, 16))).split('').map(str => str.charCodeAt(0));
codepoints.push(a);
let byLength = codepointsByLength.get(a.length);
if(!byLength) {
byLength = [];
codepointsByLength.set(a.length, byLength);
}
byLength.push(a);
});
/** @type {(codepoints: number[][]) => void} */
const sort = (codepoints) => {
codepoints.sort((a, b) => {
const length = Math.min(a.length, b.length);
for(let i = 0; i < length; ++i) {
const diff = a[i] - b[i];
if(diff) {
return diff;
}
}
return a.length - b.length;
});
};
sort(codepoints);
/** @type {(arr1: number[], arr2: number[]) => boolean} */
const isEqualArray = (arr1, arr2) => {
if(arr1.length !== arr2.length) {
return false;
}
for(let i = 0; i < arr1.length; ++i) {
if(arr1[i] !== arr2[i]) {
return false;
}
}
return true;
};
/** @type {(num: number) => string} */
const ttt = (num) => {
return '\\u' + num.toString(16);
};
/** @type {(arr: number[][], j: number) => string} */
const makeGroup = (arr, j) => {
let str = '';
if(arr.length > 1) str += '[';
str += arr.map(e => e.slice(0, j).map(ttt)).join('');
if(arr.length > 1) str += ']';
return str;
};
let str = '(?:';
let groups = [];
/* codepointsByLength.forEach((value) => {
sort(value);
value.forEach(s => {
str += s.reduce((acc, v) => acc + '\\u' + v.toString(16), '');
});
}); */
// for(let j = 1; j < 5; ++j) {
// for(let i = 0; i < codepoints.length; ++i) {
// const a = codepoints[i];
// /** @type {number[][]} */
// const set = [];
// //for(let j = 1; j < a.length; ++j) {
// const ending = a.slice(j);
// for(let k = i + 1; k < codepoints.length; ++k) {
// const b = codepoints[k];
// const e = b.slice(j);
// if(isEqualArray(ending, e)) {
// codepoints.splice(k, 1);
// set.push(b);
// }
// }
// //}
// if(set.length) {
// set.unshift(a);
// codepoints.splice(i, 1);
// console.log(set.length);
// } else if(j !== (5 - 1)) {
// continue;
// } else {
// set.push(a);
// }
// let group = makeGroup(set, j);
// group += ending.map(ttt).join('');
// groups.push(group);
// str += group;
// }
// }
/* codepointsByLength.forEach((codepoints) => {
for(let i = 0; i < codepoints.length; ++i) {
const a = codepoints[i];
}
}); */
for(let i = 0; i < codepoints.length; ++i) {
const a = codepoints[i];
for(let j = i + 1; j < codepoints.length; ++j) {
}
}
str += ')';
//console.log(codepointsByLength.get(1));
console.log(str);
/* let i = 0;
let s = [codepoints[i++][0]];
for(; i < codepoints.length; ++i) {
const c = codepoints[i];
if((c[0] - s[s.length - 1]) > 1) {
if(s.length > 1) {
console.log('start from', s);
}
s = [c[0]];
} else {
s.push(c[0]);
}
} */
//console.log(codepoints);

27
src/scripts/format_emoji_regex.js

File diff suppressed because one or more lines are too long

4
src/scripts/format_jsons.js

@ -153,8 +153,8 @@ if(false) { @@ -153,8 +153,8 @@ if(false) {
.reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
emoji = encodeEmoji(emoji);
//emoji = emoji.replace(/(-fe0f|fe0f)/g, '');
emoji = emoji.replace(/-?fe0f$/, '');
emoji = emoji.replace(/-?fe0f/g, '');
//emoji = emoji.replace(/-?fe0f$/, '');
let c = categories[category] === undefined ? 9 : categories[category];
//obj[emoji] = '' + c + sort_order;

4879
src/scripts/in/emoji_test.txt

File diff suppressed because it is too large Load Diff

2
src/scripts/out/emoji.json

File diff suppressed because one or more lines are too long

1
src/scripts/out/emoji_regex.txt

File diff suppressed because one or more lines are too long

20
src/vendor/emoji/regex.ts vendored

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