fix: filter unsupported emoji

This commit is contained in:
Nolan Lawson 2020-06-28 16:31:53 -07:00
parent 6833b12be1
commit 53126351cc
8 changed files with 128 additions and 4 deletions

View File

@ -2,10 +2,31 @@ import { store } from '../_store/store'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import * as emojiDatabase from '../_utils/emojiDatabase'
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
import { testEmojiSupported } from '../_utils/testEmojiSupported'
import { mark, stop } from '../_utils/marks'
async function searchEmoji (searchText) {
const results = await emojiDatabase.findBySearchQuery(searchText.substring(1))
return results.slice(0, SEARCH_RESULTS_LIMIT)
let emojis = await emojiDatabase.findBySearchQuery(searchText)
const results = []
if (searchText.startsWith(':') && searchText.endsWith(':')) {
// exact shortcode search
const shortcode = searchText.substring(1, searchText.length - 1).toLowerCase()
emojis = emojis.filter(_ => _.shortcodes.includes(shortcode))
}
mark('testEmojiSupported')
for (const emoji of emojis) {
if (results.length === SEARCH_RESULTS_LIMIT) {
break
}
if (testEmojiSupported(emoji.unicode)) {
results.push(emoji)
}
}
stop('testEmojiSupported')
return results
}
export function doEmojiSearch (searchText) {

View File

@ -51,7 +51,7 @@
</span>
{/if}
<span class="compose-autosuggest-list-display-name compose-autosuggest-list-display-name-single">
{':' + item.shortcodes[0] + ':'}
{item.shortcodes.map(_ => `:${_}:`).join(' ')}
</span>
{/if}
</div>

View File

@ -10,6 +10,7 @@
ref:textarea
bind:value=rawText
on:focus="onFocus()"
on:blur="onBlur()"
on:selectionChange="onSelectionChange(event)"
on:keydown="onKeydown(event)"
></textarea>

View File

@ -0,0 +1,3 @@
// same as the one used for PinaforeEmoji
export const FONT_FAMILY = '"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol",' +
'"Twemoji Mozilla","Noto Color Emoji","EmojiOne Color","Android Emoji",sans-serif'

View File

@ -5,7 +5,7 @@ const MIN_PREFIX_LENGTH = 2
// Technically mastodon accounts allow dots, but it would be weird to do an autosuggest search if it ends with a dot.
// Also this is rare. https://github.com/tootsuite/mastodon/pull/6844
// However for emoji search we allow some extra things (e.g. :+1:, :white_heart:)
const VALID_CHARS = '[\\w\\+_]'
const VALID_CHARS = '[\\w\\+_\\-:]'
const PREFIXES = '(?:@|:|#)'
const REGEX = new RegExp(`(?:\\s|^)(${PREFIXES}${VALID_CHARS}{${MIN_PREFIX_LENGTH},})$`)

View File

@ -0,0 +1,39 @@
// Copied from
// https://github.com/nolanlawson/emoji-picker-element/blob/04f490a/src/picker/utils/testColorEmojiSupported.js
import { FONT_FAMILY } from '../_static/fonts'
const getTextFeature = (text, color) => {
try {
const canvas = document.createElement('canvas')
canvas.width = canvas.height = 1
const ctx = canvas.getContext('2d')
ctx.textBaseline = 'top'
ctx.font = `100px ${FONT_FAMILY}`
ctx.fillStyle = color
ctx.scale(0.01, 0.01)
ctx.fillText(text, 0, 0)
return ctx.getImageData(0, 0, 1, 1).data
} catch (e) { /* ignore, return undefined */ }
}
const compareFeatures = (feature1, feature2) => {
const feature1Str = [...feature1].join(',')
const feature2Str = [...feature2].join(',')
return feature1Str === feature2Str && feature1Str !== '0,0,0,0'
}
export function testColorEmojiSupported (text) {
// Render white and black and then compare them to each other and ensure they're the same
// color, and neither one is black. This shows that the emoji was rendered in color.
const feature1 = getTextFeature(text, '#000')
const feature2 = getTextFeature(text, '#fff')
const supported = feature1 && feature2 && compareFeatures(feature1, feature2)
if (!supported) {
console.log('Filtered unsupported emoji via color test', text)
}
return supported
}

View File

@ -0,0 +1,43 @@
// Return true if the unicode is rendered as a double character, e.g.
// "black cat" or "polar bar" or "person with red hair" or other emoji
// that look like double or triple emoji if the unicode is not rendered properly
const BASELINE_EMOJI = '😀'
let baselineWidth
export function testEmojiRenderedAtCorrectSize (unicode) {
if (!unicode.includes('\u200d')) { // ZWJ
return true // benefit of the doubt
}
let emojiTestDiv = document.getElementById('emoji-test')
if (!emojiTestDiv) {
emojiTestDiv = document.createElement('div')
emojiTestDiv.id = 'emoji-test'
emojiTestDiv.ariaHidden = true
Object.assign(emojiTestDiv.style, {
position: 'absolute',
opacity: '0',
'pointer-events': 'none',
'font-family': 'PinaforeEmoji',
'font-size': '14px',
contain: 'content'
})
document.body.appendChild(emojiTestDiv)
}
emojiTestDiv.textContent = unicode
const { width } = emojiTestDiv.getBoundingClientRect()
if (typeof baselineWidth === 'undefined') {
emojiTestDiv.textContent = BASELINE_EMOJI
baselineWidth = emojiTestDiv.getBoundingClientRect().width
}
// WebKit has some imprecision here, so round it
const emojiSupported = width.toFixed(2) === baselineWidth.toFixed(2)
if (!emojiSupported) {
console.log('Filtered unsupported emoji via size test', unicode, 'width', width, 'baselineWidth', baselineWidth)
}
return emojiSupported
}

View File

@ -0,0 +1,17 @@
import { testColorEmojiSupported } from './testColorEmojiSupported'
import { testEmojiRenderedAtCorrectSize } from './testEmojiRenderedAtCorrectSize'
import { QuickLRU } from '../_thirdparty/quick-lru/quick-lru'
// avoid recomputing emoji support over and over again
const emojiSupportCache = new QuickLRU({
maxSize: 500
})
export function testEmojiSupported (unicode) {
let supported = emojiSupportCache.get(unicode)
if (typeof supported !== 'boolean') {
supported = !!(testColorEmojiSupported(unicode) && testEmojiRenderedAtCorrectSize(unicode))
emojiSupportCache.set(unicode, supported)
}
return supported
}