diff --git a/.babelrc b/.babelrc index 373d2c59..4ec10416 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { "presets": ["@babel/preset-env"], "plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"], - "comments": false + "comments": true } diff --git a/.gitignore b/.gitignore index 479d57c4..4df5ec83 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ test/e2e/reports selenium-debug.log .idea/ config/local.json +static/emoji.json diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f666a4ef..bfc41ac4 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -10,3 +10,5 @@ Contributors of this project. - shpuld (shpuld@shitposter.club): CSS and styling - Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images. - hj (hj@shigusegubu.club): Code +- Sean King (seanking@freespeechextremist.com): Code +- Tusooa Zhu (tusooa@kazv.moe): Code diff --git a/build/build.js b/build/build.js index b3c9aad4..35969eb6 100644 --- a/build/build.js +++ b/build/build.js @@ -18,6 +18,9 @@ console.log( var spinner = ora('building for production...') spinner.start() +var updateEmoji = require('./update-emoji').updateEmoji +updateEmoji() + var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory) rm('-rf', assetsPath) mkdir('-p', assetsPath) diff --git a/build/dev-server.js b/build/dev-server.js index 854efa0b..e51ba948 100644 --- a/build/dev-server.js +++ b/build/dev-server.js @@ -10,6 +10,9 @@ var webpackConfig = process.env.NODE_ENV === 'testing' ? require('./webpack.prod.conf') : require('./webpack.dev.conf') +var updateEmoji = require('./update-emoji').updateEmoji +updateEmoji() + // default port where dev server listens for incoming traffic var port = process.env.PORT || config.dev.port // Define HTTP proxies to your custom API backend diff --git a/build/update-emoji.js b/build/update-emoji.js new file mode 100644 index 00000000..9f4b4e67 --- /dev/null +++ b/build/update-emoji.js @@ -0,0 +1,27 @@ + +module.exports = { + updateEmoji () { + const emojis = require('@kazvmoe-infra/unicode-emoji-json/data-by-group') + const fs = require('fs') + + Object.keys(emojis) + .map(k => { + emojis[k].map(e => { + delete e.unicode_version + delete e.emoji_version + delete e.skin_tone_support_unicode_version + }) + }) + + const res = {} + Object.keys(emojis) + .map(k => { + const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase() + res[groupId] = emojis[k] + }) + + console.info('Updating emojis...') + fs.writeFileSync('static/emoji.json', JSON.stringify(res)) + console.info('Done.') + } +} diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index 78b75e3f..bf946922 100644 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -24,7 +24,8 @@ module.exports = { output: { path: config.build.assetsRoot, publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath, - filename: '[name].js' + filename: '[name].js', + chunkFilename: '[name].js' }, optimization: { splitChunks: { diff --git a/package.json b/package.json index f63eb7d7..313654a9 100644 --- a/package.json +++ b/package.json @@ -18,22 +18,23 @@ "dependencies": { "@babel/runtime": "7.18.9", "@chenfengyuan/vue-qrcode": "2.0.0", - "@fortawesome/fontawesome-svg-core": "6.1.2", - "@fortawesome/free-regular-svg-icons": "6.1.2", - "@fortawesome/free-solid-svg-icons": "6.1.2", + "@fortawesome/fontawesome-svg-core": "6.2.0", + "@fortawesome/free-regular-svg-icons": "6.2.0", + "@fortawesome/free-solid-svg-icons": "6.2.0", "@fortawesome/vue-fontawesome": "3.0.1", "@kazvmoe-infra/pinch-zoom-element": "1.2.0", + "@kazvmoe-infra/unicode-emoji-json": "0.4.0", "@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12", "@vuelidate/core": "2.0.0-alpha.44", - "@vuelidate/validators": "2.0.0-alpha.31", + "@vuelidate/validators": "2.0.0", "body-scroll-lock": "3.1.5", "chromatism": "3.0.0", "click-outside-vue3": "4.0.1", "cropperjs": "1.5.12", - "diff": "3.5.0", "escape-html": "1.0.3", "js-cookie": "3.0.1", "localforage": "1.10.0", + "lozad": "1.16.0", "parse-link-header": "2.0.0", "phoenix": "1.6.2", "punycode.js": "2.1.0", @@ -41,7 +42,7 @@ "querystring-es3": "0.2.1", "url": "0.11.0", "utf8": "3.0.0", - "vue": "3.2.37", + "vue": "3.2.38", "vue-i18n": "9.2.2", "vue-router": "4.1.5", "vue-template-compiler": "2.7.10", @@ -57,7 +58,7 @@ "@ungap/event-target": "0.2.3", "@vue/babel-helper-vue-jsx-merge-props": "1.4.0", "@vue/babel-plugin-jsx": "1.1.1", - "@vue/compiler-sfc": "3.2.37", + "@vue/compiler-sfc": "3.2.38", "@vue/test-utils": "2.0.2", "autoprefixer": "10.4.8", "babel-loader": "8.2.5", @@ -96,7 +97,6 @@ "karma-spec-reporter": "0.0.34", "karma-webpack": "5.0.0", "lodash": "4.17.21", - "lolex": "1.6.0", "mini-css-extract-plugin": "2.6.1", "mocha": "10.0.0", "nightwatch": "2.3.3", @@ -104,13 +104,13 @@ "ora": "0.4.1", "postcss": "8.4.16", "postcss-loader": "7.0.1", - "sass": "1.54.5", + "sass": "1.55.0", "sass-loader": "13.0.2", "selenium-server": "2.53.1", "semver": "7.3.7", "serviceworker-webpack5-plugin": "2.0.0", "shelljs": "0.8.5", - "sinon": "2.4.1", + "sinon": "14.0.0", "sinon-chai": "3.7.0", "stylelint": "13.13.1", "stylelint-config-standard": "20.0.0", diff --git a/src/App.js b/src/App.js index 18dd61f2..b7eb2f72 100644 --- a/src/App.js +++ b/src/App.js @@ -10,7 +10,9 @@ import MobilePostStatusButton from './components/mobile_post_status_button/mobil import MobileNav from './components/mobile_nav/mobile_nav.vue' import DesktopNav from './components/desktop_nav/desktop_nav.vue' import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue' +import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue' import PostStatusModal from './components/post_status_modal/post_status_modal.vue' +import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue' import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' import { windowWidth, windowHeight } from './services/window_utils/window_utils' import { mapGetters } from 'vuex' @@ -35,6 +37,8 @@ export default { UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')), UserReportingModal, PostStatusModal, + EditStatusModal, + StatusHistoryModal, GlobalNoticeList }, data: () => ({ @@ -101,6 +105,7 @@ export default { return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile' }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, + editingAvailable () { return this.$store.state.instance.editingAvailable }, shoutboxPosition () { return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false }, diff --git a/src/App.vue b/src/App.vue index a993c238..e0d709f7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -67,6 +67,8 @@ + + diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js index 0ddb4d68..adaa879e 100644 --- a/src/components/emoji_input/suggestor.js +++ b/src/components/emoji_input/suggestor.js @@ -2,7 +2,7 @@ * suggest - generates a suggestor function to be used by emoji-input * data: object providing source information for specific types of suggestions: * data.emoji - optional, an array of all emoji available i.e. - * (state.instance.emoji + state.instance.customEmoji) + * (getters.standardEmojiList + state.instance.customEmoji) * data.users - optional, an array of all known users * updateUsersList - optional, a function to search and append to users * @@ -13,10 +13,10 @@ export default data => { const emojiCurry = suggestEmoji(data.emoji) const usersCurry = data.store && suggestUsers(data.store) - return input => { + return (input, nameKeywordLocalizer) => { const firstChar = input[0] if (firstChar === ':' && data.emoji) { - return emojiCurry(input) + return emojiCurry(input, nameKeywordLocalizer) } if (firstChar === '@' && usersCurry) { return usersCurry(input) @@ -25,34 +25,34 @@ export default data => { } } -export const suggestEmoji = emojis => input => { +export const suggestEmoji = emojis => (input, nameKeywordLocalizer) => { const noPrefix = input.toLowerCase().substr(1) return emojis - .filter(({ displayText }) => displayText.toLowerCase().match(noPrefix)) - .sort((a, b) => { - let aScore = 0 - let bScore = 0 + .map(emoji => ({ ...emoji, ...nameKeywordLocalizer(emoji) })) + .filter((emoji) => (emoji.names.concat(emoji.keywords)).filter(kw => kw.toLowerCase().match(noPrefix)).length) + .map(k => { + let score = 0 // An exact match always wins - aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0 - bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0 + score += Math.max(...k.names.map(name => name.toLowerCase() === noPrefix ? 200 : 0), 0) // Prioritize custom emoji a lot - aScore += a.imageUrl ? 100 : 0 - bScore += b.imageUrl ? 100 : 0 + score += k.imageUrl ? 100 : 0 // Prioritize prefix matches somewhat - aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0 - bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0 + score += Math.max(...k.names.map(kw => kw.toLowerCase().startsWith(noPrefix) ? 10 : 0), 0) // Sort by length - aScore -= a.displayText.length - bScore -= b.displayText.length + score -= k.displayText.length + k.score = score + return k + }) + .sort((a, b) => { // Break ties alphabetically const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5 - return bScore - aScore + alphabetically + return b.score - a.score + alphabetically }) } diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js index f6920208..fafc2af1 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -1,33 +1,76 @@ import { defineAsyncComponent } from 'vue' import Checkbox from '../checkbox/checkbox.vue' +import StillImage from '../still-image/still-image.vue' +import { ensureFinalFallback } from '../../i18n/languages.js' +import lozad from 'lozad' import { library } from '@fortawesome/fontawesome-svg-core' import { faBoxOpen, faStickyNote, - faSmileBeam + faSmileBeam, + faSmile, + faUser, + faPaw, + faIceCream, + faBus, + faBasketballBall, + faLightbulb, + faCode, + faFlag } from '@fortawesome/free-solid-svg-icons' -import { trim } from 'lodash' +import { debounce, trim } from 'lodash' library.add( faBoxOpen, faStickyNote, - faSmileBeam + faSmileBeam, + faSmile, + faUser, + faPaw, + faIceCream, + faBus, + faBasketballBall, + faLightbulb, + faCode, + faFlag ) -// At widest, approximately 20 emoji are visible in a row, -// loading 3 rows, could be overkill for narrow picker -const LOAD_EMOJI_BY = 60 +const UNICODE_EMOJI_GROUP_ICON = { + 'smileys-and-emotion': 'smile', + 'people-and-body': 'user', + 'animals-and-nature': 'paw', + 'food-and-drink': 'ice-cream', + 'travel-and-places': 'bus', + activities: 'basketball-ball', + objects: 'lightbulb', + symbols: 'code', + flags: 'flag' +} -// When to start loading new batch emoji, in pixels -const LOAD_EMOJI_MARGIN = 64 +const maybeLocalizedKeywords = (emoji, languages, nameLocalizer) => { + const res = [emoji.displayText, nameLocalizer(emoji)] + if (emoji.annotations) { + languages.forEach(lang => { + const keywords = emoji.annotations[lang]?.keywords || [] + const name = emoji.annotations[lang]?.name + res.push(...(keywords.concat([name]).filter(k => k))) + }) + } + return res +} -const filterByKeyword = (list, keyword = '') => { +const filterByKeyword = (list, keyword = '', languages, nameLocalizer) => { if (keyword === '') return list const keywordLowercase = keyword.toLowerCase() const orderedEmojiList = [] for (const emoji of list) { - const indexOfKeyword = emoji.displayText.toLowerCase().indexOf(keywordLowercase) + const indices = maybeLocalizedKeywords(emoji, languages, nameLocalizer) + .map(k => k.toLowerCase().indexOf(keywordLowercase)) + .filter(k => k > -1) + + const indexOfKeyword = indices.length ? Math.min(...indices) : -1 + if (indexOfKeyword > -1) { if (!Array.isArray(orderedEmojiList[indexOfKeyword])) { orderedEmojiList[indexOfKeyword] = [] @@ -44,6 +87,10 @@ const EmojiPicker = { required: false, type: Boolean, default: false + }, + showing: { + required: true, + type: Boolean } }, data () { @@ -53,16 +100,26 @@ const EmojiPicker = { showingStickers: false, groupsScrolledClass: 'scrolled-top', keepOpen: false, - customEmojiBufferSlice: LOAD_EMOJI_BY, customEmojiTimeout: null, - customEmojiLoadAllConfirmed: false + // Lazy-load only after the first time `showing` becomes true. + contentLoaded: false, + groupRefs: {}, + emojiRefs: {}, + filteredEmojiGroups: [] } }, components: { StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')), - Checkbox + Checkbox, + StillImage }, methods: { + setGroupRef (name) { + return el => { this.groupRefs[name] = el } + }, + setEmojiRef (name) { + return el => { this.emojiRefs[name] = el } + }, onStickerUploaded (e) { this.$emit('sticker-uploaded', e) }, @@ -77,10 +134,38 @@ const EmojiPicker = { const target = (e && e.target) || this.$refs['emoji-groups'] this.updateScrolledClass(target) this.scrolledGroup(target) - this.triggerLoadMore(target) + }, + scrolledGroup (target) { + const top = target.scrollTop + 5 + this.$nextTick(() => { + this.allEmojiGroups.forEach(group => { + const ref = this.groupRefs['group-' + group.id] + if (ref && ref.offsetTop <= top) { + this.activeGroup = group.id + } + }) + this.scrollHeader() + }) + }, + scrollHeader () { + // Scroll the active tab's header into view + const headerRef = this.groupRefs['group-header-' + this.activeGroup] + const left = headerRef.offsetLeft + const right = left + headerRef.offsetWidth + const headerCont = this.$refs.header + const currentScroll = headerCont.scrollLeft + const currentScrollRight = currentScroll + headerCont.clientWidth + const setScroll = s => { headerCont.scrollLeft = s } + + const margin = 7 // .emoji-tabs-item: padding + if (left - margin < currentScroll) { + setScroll(left - margin) + } else if (right + margin > currentScrollRight) { + setScroll(right + margin - headerCont.clientWidth) + } }, highlight (key) { - const ref = this.$refs['group-' + key] + const ref = this.groupRefs['group-' + key] const top = ref.offsetTop this.setShowStickers(false) this.activeGroup = key @@ -97,73 +182,90 @@ const EmojiPicker = { this.groupsScrolledClass = 'scrolled-middle' } }, - triggerLoadMore (target) { - const ref = this.$refs['group-end-custom'] - if (!ref) return - const bottom = ref.offsetTop + ref.offsetHeight - - const scrollerBottom = target.scrollTop + target.clientHeight - const scrollerTop = target.scrollTop - const scrollerMax = target.scrollHeight - - // Loads more emoji when they come into view - const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN - // Always load when at the very top in case there's no scroll space yet - const atTop = scrollerTop < 5 - // Don't load when looking at unicode category or at the very bottom - const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax - if (!bottomAboveViewport && (approachingBottom || atTop)) { - this.loadEmoji() - } - }, - scrolledGroup (target) { - const top = target.scrollTop + 5 - this.$nextTick(() => { - this.emojisView.forEach(group => { - const ref = this.$refs['group-' + group.id] - if (ref.offsetTop <= top) { - this.activeGroup = group.id - } - }) - }) - }, - loadEmoji () { - const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length - - if (allLoaded) { - return - } - - this.customEmojiBufferSlice += LOAD_EMOJI_BY - }, - startEmojiLoad (forceUpdate = false) { - if (!forceUpdate) { - this.keyword = '' - } - this.$nextTick(() => { - this.$refs['emoji-groups'].scrollTop = 0 - }) - const bufferSize = this.customEmojiBuffer.length - const bufferPrefilledAll = bufferSize === this.filteredEmoji.length - if (bufferPrefilledAll && !forceUpdate) { - return - } - this.customEmojiBufferSlice = LOAD_EMOJI_BY - }, toggleStickers () { this.showingStickers = !this.showingStickers }, setShowStickers (value) { this.showingStickers = value + }, + filterByKeyword (list, keyword) { + return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName) + }, + initializeLazyLoad () { + this.destroyLazyLoad() + this.$nextTick(() => { + this.$lozad = lozad('.still-image.emoji-picker-emoji', { + load: el => { + const name = el.getAttribute('data-emoji-name') + const vn = this.emojiRefs[name] + if (!vn) { + return + } + + vn.loadLazy() + } + }) + this.$lozad.observe() + }) + }, + waitForDomAndInitializeLazyLoad () { + this.$nextTick(() => this.initializeLazyLoad()) + }, + destroyLazyLoad () { + if (this.$lozad) { + if (this.$lozad.observer) { + this.$lozad.observer.disconnect() + } + if (this.$lozad.mutationObserver) { + this.$lozad.mutationObserver.disconnect() + } + } + }, + onShowing () { + const oldContentLoaded = this.contentLoaded + this.contentLoaded = true + this.waitForDomAndInitializeLazyLoad() + this.filteredEmojiGroups = this.getFilteredEmojiGroups() + if (!oldContentLoaded) { + this.$nextTick(() => { + if (this.defaultGroup) { + this.highlight(this.defaultGroup) + } + }) + } + }, + getFilteredEmojiGroups () { + return this.allEmojiGroups + .map(group => ({ + ...group, + emojis: this.filterByKeyword(group.emojis, trim(this.keyword)) + })) + .filter(group => group.emojis.length > 0) } }, watch: { keyword () { - this.customEmojiLoadAllConfirmed = false this.onScroll() - this.startEmojiLoad(true) + this.debouncedHandleKeywordChange() + }, + allCustomGroups () { + this.waitForDomAndInitializeLazyLoad() + this.filteredEmojiGroups = this.getFilteredEmojiGroups() + }, + showing (val) { + if (val) { + this.onShowing() + } } }, + mounted () { + if (this.showing) { + this.onShowing() + } + }, + destroyed () { + this.destroyLazyLoad() + }, computed: { activeGroupView () { return this.showingStickers ? '' : this.activeGroup @@ -174,39 +276,55 @@ const EmojiPicker = { } return 0 }, - filteredEmoji () { - return filterByKeyword( - this.$store.state.instance.customEmoji || [], - trim(this.keyword) - ) + allCustomGroups () { + return this.$store.getters.groupedCustomEmojis }, - customEmojiBuffer () { - return this.filteredEmoji.slice(0, this.customEmojiBufferSlice) + defaultGroup () { + return Object.keys(this.allCustomGroups)[0] }, - emojis () { - const standardEmojis = this.$store.state.instance.emoji || [] - const customEmojis = this.customEmojiBuffer - - return [ - { - id: 'custom', - text: this.$t('emoji.custom'), - icon: 'smile-beam', - emojis: customEmojis - }, - { - id: 'standard', - text: this.$t('emoji.unicode'), - icon: 'box-open', - emojis: filterByKeyword(standardEmojis, trim(this.keyword)) - } - ] + unicodeEmojiGroups () { + return this.$store.getters.standardEmojiGroupList.map(group => ({ + id: `standard-${group.id}`, + text: this.$t(`emoji.unicode_groups.${group.id}`), + icon: UNICODE_EMOJI_GROUP_ICON[group.id], + emojis: group.emojis + })) }, - emojisView () { - return this.emojis.filter(value => value.emojis.length > 0) + allEmojiGroups () { + return Object.entries(this.allCustomGroups) + .map(([_, v]) => v) + .concat(this.unicodeEmojiGroups) }, stickerPickerEnabled () { return (this.$store.state.instance.stickers || []).length !== 0 + }, + debouncedHandleKeywordChange () { + return debounce(() => { + this.waitForDomAndInitializeLazyLoad() + this.filteredEmojiGroups = this.getFilteredEmojiGroups() + }, 500) + }, + languages () { + return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage) + }, + maybeLocalizedEmojiName () { + return emoji => { + if (!emoji.annotations) { + return emoji.displayText + } + + if (emoji.displayTextI18n) { + return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args) + } + + for (const lang of this.languages) { + if (emoji.annotations[lang]?.name) { + return emoji.annotations[lang].name + } + } + + return emoji.displayText + } } } } diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss index a2f17c51..016c46d7 100644 --- a/src/components/emoji_picker/emoji_picker.scss +++ b/src/components/emoji_picker/emoji_picker.scss @@ -1,5 +1,10 @@ @import '../../_variables.scss'; +$emoji-picker-header-height: 36px; +$emoji-picker-header-picture-width: 32px; +$emoji-picker-header-picture-height: 32px; +$emoji-picker-emoji-size: 32px; + .emoji-picker { display: flex; flex-direction: column; @@ -19,6 +24,23 @@ --lightText: var(--popoverLightText, $fallback--lightText); --icon: var(--popoverIcon, $fallback--icon); + &-header-image { + display: inline-flex; + justify-content: center; + align-items: center; + width: $emoji-picker-header-picture-width; + max-width: $emoji-picker-header-picture-width; + height: $emoji-picker-header-picture-height; + max-height: $emoji-picker-header-picture-height; + .still-image { + max-width: 100%; + max-height: 100%; + height: 100%; + width: 100%; + object-fit: contain; + } + } + .keep-open, .too-many-emoji { padding: 7px; @@ -37,7 +59,6 @@ .heading { display: flex; - height: 32px; padding: 10px 7px 5px; } @@ -50,6 +71,10 @@ .emoji-tabs { flex-grow: 1; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + overflow-x: auto; } .emoji-groups { @@ -57,6 +82,8 @@ } .additional-tabs { + display: flex; + flex: 1; border-left: 1px solid; border-left-color: $fallback--icon; border-left-color: var(--icon, $fallback--icon); @@ -66,15 +93,20 @@ .additional-tabs, .emoji-tabs { - display: block; - min-width: 0; flex-basis: auto; - flex-shrink: 1; + display: flex; + align-content: center; &-item { padding: 0 7px; cursor: pointer; font-size: 1.85em; + width: $emoji-picker-header-picture-width; + max-width: $emoji-picker-header-picture-width; + height: $emoji-picker-header-picture-height; + max-height: $emoji-picker-header-picture-height; + display: flex; + align-items: center; &.disabled { opacity: 0.5; @@ -164,22 +196,26 @@ } &-item { - width: 32px; - height: 32px; + width: $emoji-picker-emoji-size; + height: $emoji-picker-emoji-size; box-sizing: border-box; display: flex; - font-size: 32px; + line-height: $emoji-picker-emoji-size; align-items: center; justify-content: center; margin: 4px; cursor: pointer; - img { + .emoji-picker-emoji.-custom { object-fit: contain; max-width: 100%; max-height: 100%; } + .emoji-picker-emoji.-unicode { + font-size: 24px; + overflow: hidden; + } } } diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue index a7269120..57bb0037 100644 --- a/src/components/emoji_picker/emoji_picker.vue +++ b/src/components/emoji_picker/emoji_picker.vue @@ -1,19 +1,34 @@ + +