diff options
66 files changed, 1670 insertions, 1879 deletions
@@ -1,5 +1,5 @@ { "presets": ["@babel/preset-env"], "plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"], - "comments": false + "comments": true } @@ -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", @@ -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 @@ <MobilePostStatusButton /> <UserReportingModal /> <PostStatusModal /> + <EditStatusModal v-if="editingAvailable" /> + <StatusHistoryModal v-if="editingAvailable" /> <SettingsModal /> <UpdateNotification /> <div id="modal" /> diff --git a/src/assets/pleromatan_apology_fox_mask.png b/src/assets/pleromatan_apology_fox_mask.png Binary files differnew file mode 100644 index 00000000..4d1990d5 --- /dev/null +++ b/src/assets/pleromatan_apology_fox_mask.png diff --git a/src/assets/pleromatan_apology_mask.png b/src/assets/pleromatan_apology_mask.png Binary files differnew file mode 100644 index 00000000..18adafff --- /dev/null +++ b/src/assets/pleromatan_apology_mask.png diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 38b5f38e..886d52f2 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -251,6 +251,7 @@ const getNodeInfo = async ({ store }) => { store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') }) store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) + store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') }) store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js index 735dd81c..c23407f9 100644 --- a/src/components/account_actions/account_actions.js +++ b/src/components/account_actions/account_actions.js @@ -36,6 +36,9 @@ const AccountActions = { unblockUser () { this.$store.dispatch('unblockUser', this.user.id) }, + removeUserFromFollowers () { + this.$store.dispatch('removeUserFromFollowers', this.user.id) + }, reportUser () { this.$store.dispatch('openUserReportingModal', { userId: this.user.id }) }, diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index 770740e0..218aa6b3 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -30,6 +30,13 @@ </template> <UserListMenu :user="user" /> <button + v-if="relationship.followed_by" + class="btn button-default btn-block dropdown-item" + @click="removeUserFromFollowers" + > + {{ $t('user_card.remove_follower') }} + </button> + <button v-if="relationship.blocking" class="btn button-default btn-block dropdown-item" @click="unblockUser" diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index d62a4adc..5dc50475 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -129,6 +129,9 @@ const Attachment = { ...mapGetters(['mergedConfig']) }, watch: { + 'attachment.description' (newVal) { + this.localDescription = newVal + }, localDescription (newVal) { this.onEdit(newVal) } diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 712e2a2c..85e6d8ad 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -1,6 +1,8 @@ import { reduce, filter, findIndex, clone, get } from 'lodash' import Status from '../status/status.vue' import ThreadTree from '../thread_tree/thread_tree.vue' +import { WSConnectionStatus } from '../../services/api/api.service.js' +import { mapGetters, mapState } from 'vuex' import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue' import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue' @@ -79,6 +81,9 @@ const conversation = { const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2 return maxDepth >= 1 ? maxDepth : 1 }, + streamingEnabled () { + return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED + }, displayStyle () { return this.$store.getters.mergedConfig.conversationDisplay }, @@ -341,7 +346,11 @@ const conversation = { }, maybeHighlight () { return this.isExpanded ? this.highlight : null - } + }, + ...mapGetters(['mergedConfig']), + ...mapState({ + mastoUserSocketStatus: state => state.api.mastoUserSocketStatus + }) }, components: { Status, @@ -399,6 +408,11 @@ const conversation = { setHighlight (id) { if (!id) return this.highlight = id + + if (!this.streamingEnabled) { + this.$store.dispatch('fetchStatus', id) + } + this.$store.dispatch('fetchFavsAndRepeats', id) this.$store.dispatch('fetchEmojiReactionsBy', id) }, diff --git a/src/components/edit_status_modal/edit_status_modal.js b/src/components/edit_status_modal/edit_status_modal.js new file mode 100644 index 00000000..75adfea7 --- /dev/null +++ b/src/components/edit_status_modal/edit_status_modal.js @@ -0,0 +1,75 @@ +import PostStatusForm from '../post_status_form/post_status_form.vue' +import Modal from '../modal/modal.vue' +import statusPosterService from '../../services/status_poster/status_poster.service.js' +import get from 'lodash/get' + +const EditStatusModal = { + components: { + PostStatusForm, + Modal + }, + data () { + return { + resettingForm: false + } + }, + computed: { + isLoggedIn () { + return !!this.$store.state.users.currentUser + }, + modalActivated () { + return this.$store.state.editStatus.modalActivated + }, + isFormVisible () { + return this.isLoggedIn && !this.resettingForm && this.modalActivated + }, + params () { + return this.$store.state.editStatus.params || {} + } + }, + watch: { + params (newVal, oldVal) { + if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) { + this.resettingForm = true + this.$nextTick(() => { + this.resettingForm = false + }) + } + }, + isFormVisible (val) { + if (val) { + this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus()) + } + } + }, + methods: { + doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) { + const params = { + store: this.$store, + statusId: this.$store.state.editStatus.params.statusId, + status, + spoilerText, + sensitive, + poll, + media, + contentType + } + + return statusPosterService.editStatus(params) + .then((data) => { + return data + }) + .catch((err) => { + console.error('Error editing status', err) + return { + error: err.message + } + }) + }, + closeModal () { + this.$store.dispatch('closeEditStatusModal') + } + } +} + +export default EditStatusModal diff --git a/src/components/edit_status_modal/edit_status_modal.vue b/src/components/edit_status_modal/edit_status_modal.vue new file mode 100644 index 00000000..1dbacaab --- /dev/null +++ b/src/components/edit_status_modal/edit_status_modal.vue @@ -0,0 +1,48 @@ +<template> + <Modal + v-if="isFormVisible" + class="edit-form-modal-view" + @backdropClicked="closeModal" + > + <div class="edit-form-modal-panel panel"> + <div class="panel-heading"> + {{ $t('post_status.edit_status') }} + </div> + <PostStatusForm + class="panel-body" + v-bind="params" + :post-handler="doEditStatus" + :disable-polls="true" + :disable-visibility-selector="true" + @posted="closeModal" + /> + </div> + </Modal> +</template> + +<script src="./edit_status_modal.js"></script> + +<style lang="scss"> +.modal-view.edit-form-modal-view { + align-items: flex-start; +} +.edit-form-modal-panel { + flex-shrink: 0; + margin-top: 25%; + margin-bottom: 2em; + width: 100%; + max-width: 700px; + + @media (orientation: landscape) { + margin-top: 8%; + } + + .form-bottom-left { + max-width: 6.5em; + + .emoji-icon { + justify-content: right; + } + } +} +</style> diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index b664d6b3..ffc0ffac 100644 --- a/src/components/emoji_input/emoji_input.js +++ b/src/components/emoji_input/emoji_input.js @@ -3,7 +3,7 @@ import EmojiPicker from '../emoji_picker/emoji_picker.vue' import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue' import { take } from 'lodash' import { findOffset } from '../../services/offset_finder/offset_finder.service.js' - +import { ensureFinalFallback } from '../../i18n/languages.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faSmileBeam @@ -143,6 +143,51 @@ const EmojiInput = { const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {} return word } + }, + languages () { + return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage) + }, + maybeLocalizedEmojiNamesAndKeywords () { + return emoji => { + const names = [emoji.displayText] + const keywords = [] + + if (emoji.displayTextI18n) { + names.push(this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)) + } + + if (emoji.annotations) { + this.languages.forEach(lang => { + names.push(emoji.annotations[lang]?.name) + + keywords.push(...(emoji.annotations[lang]?.keywords || [])) + }) + } + + return { + names: names.filter(k => k), + keywords: keywords.filter(k => k) + } + } + }, + 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 + } } }, mounted () { @@ -181,7 +226,7 @@ const EmojiInput = { const firstchar = newWord.charAt(0) this.suggestions = [] if (newWord === firstchar) return - const matchedSuggestions = await this.suggest(newWord) + const matchedSuggestions = await this.suggest(newWord, this.maybeLocalizedEmojiNamesAndKeywords) // Async: cancel if textAtCaret has changed during wait if (this.textAtCaret !== newWord) return if (matchedSuggestions.length <= 0) return @@ -207,7 +252,6 @@ const EmojiInput = { }, triggerShowPicker () { this.showPicker = true - this.$refs.picker.startEmojiLoad() this.$nextTick(() => { this.scrollIntoView() this.focusPickerInput() diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue index 81b81913..43581dbf 100644 --- a/src/components/emoji_input/emoji_input.vue +++ b/src/components/emoji_input/emoji_input.vue @@ -19,6 +19,7 @@ v-if="enableEmojiPicker" ref="picker" :class="{ hide: !showPicker }" + :showing="showPicker" :enable-sticker-picker="enableStickerPicker" class="emoji-picker-panel" @emoji="insert" @@ -63,7 +64,7 @@ v-if="!suggestion.user" class="displayText" > - {{ suggestion.displayText }} + {{ maybeLocalizedEmojiName(suggestion) }} </span> <span class="detailText">{{ suggestion.detailText }}</span> </div> 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() - } + toggleStickers () { + this.showingStickers = !this.showingStickers }, - scrolledGroup (target) { - const top = target.scrollTop + 5 + setShowStickers (value) { + this.showingStickers = value + }, + filterByKeyword (list, keyword) { + return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName) + }, + initializeLazyLoad () { + this.destroyLazyLoad() this.$nextTick(() => { - this.emojisView.forEach(group => { - const ref = this.$refs['group-' + group.id] - if (ref.offsetTop <= top) { - this.activeGroup = group.id + 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() }) }, - loadEmoji () { - const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length - - if (allLoaded) { - return - } - - this.customEmojiBufferSlice += LOAD_EMOJI_BY + waitForDomAndInitializeLazyLoad () { + this.$nextTick(() => this.initializeLazyLoad()) }, - 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 + destroyLazyLoad () { + if (this.$lozad) { + if (this.$lozad.observer) { + this.$lozad.observer.disconnect() + } + if (this.$lozad.mutationObserver) { + this.$lozad.mutationObserver.disconnect() + } } - this.customEmojiBufferSlice = LOAD_EMOJI_BY }, - toggleStickers () { - this.showingStickers = !this.showingStickers + 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) + } + }) + } }, - setShowStickers (value) { - this.showingStickers = value + 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 @@ <template> - <div class="emoji-picker panel panel-default panel-body"> + <div + class="emoji-picker panel panel-default panel-body" + > <div class="heading"> - <span class="emoji-tabs"> + <span + ref="header" + class="emoji-tabs" + > <span - v-for="group in emojis" + v-for="group in filteredEmojiGroups" + :ref="setGroupRef('group-header-' + group.id)" :key="group.id" class="emoji-tabs-item" :class="{ - active: activeGroupView === group.id, - disabled: group.emojis.length === 0 + active: activeGroupView === group.id }" :title="group.text" @click.prevent="highlight(group.id)" > + <span + v-if="group.image" + class="emoji-picker-header-image" + > + <still-image + :alt="group.text" + :src="group.image" + /> + </span> <FAIcon + v-else :icon="group.icon" fixed-width /> @@ -36,7 +51,10 @@ </span> </span> </div> - <div class="content"> + <div + v-if="contentLoaded" + class="content" + > <div class="emoji-content" :class="{hidden: showingStickers}" @@ -57,12 +75,12 @@ @scroll="onScroll" > <div - v-for="group in emojisView" + v-for="group in filteredEmojiGroups" :key="group.id" class="emoji-group" > <h6 - :ref="'group-' + group.id" + :ref="setGroupRef('group-' + group.id)" class="emoji-group-title" > {{ group.text }} @@ -70,17 +88,23 @@ <span v-for="emoji in group.emojis" :key="group.id + emoji.displayText" - :title="emoji.displayText" + :title="maybeLocalizedEmojiName(emoji)" class="emoji-item" @click.stop.prevent="onEmoji(emoji)" > - <span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span> - <img + <span + v-if="!emoji.imageUrl" + class="emoji-picker-emoji -unicode" + >{{ emoji.replacement }}</span> + <still-image v-else - :src="emoji.imageUrl" - > + :ref="setEmojiRef(group.id + emoji.displayText)" + class="emoji-picker-emoji -custom" + :data-src="emoji.imageUrl" + :data-emoji-name="group.id + emoji.displayText" + /> </span> - <span :ref="'group-end-' + group.id" /> + <span :ref="setGroupRef('group-end-' + group.id)" /> </div> </div> <div class="keep-open"> diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js index 68fa66ad..2e495423 100644 --- a/src/components/extra_buttons/extra_buttons.js +++ b/src/components/extra_buttons/extra_buttons.js @@ -7,6 +7,7 @@ import { faThumbtack, faShareAlt, faExternalLinkAlt, + faHistory, faPlus, faTimes } from '@fortawesome/free-solid-svg-icons' @@ -24,6 +25,7 @@ library.add( faShareAlt, faExternalLinkAlt, faFlag, + faHistory, faPlus, faTimes ) @@ -86,6 +88,25 @@ const ExtraButtons = { }, reportStatus () { this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] }) + }, + editStatus () { + this.$store.dispatch('fetchStatusSource', { id: this.status.id }) + .then(data => this.$store.dispatch('openEditStatusModal', { + statusId: this.status.id, + subject: data.spoiler_text, + statusText: data.text, + statusIsSensitive: this.status.nsfw, + statusPoll: this.status.poll, + statusFiles: [...this.status.attachments], + visibility: this.status.visibility, + statusContentType: data.content_type + })) + }, + showStatusHistory () { + const originalStatus = { ...this.status } + const stripFieldsList = ['attachments', 'created_at', 'emojis', 'text', 'raw_html', 'nsfw', 'poll', 'summary', 'summary_raw_html'] + stripFieldsList.forEach(p => delete originalStatus[p]) + this.$store.dispatch('openStatusHistoryModal', originalStatus) } }, computed: { @@ -109,7 +130,11 @@ const ExtraButtons = { }, statusLink () { return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}` - } + }, + isEdited () { + return this.status.edited_at !== null + }, + editingAvailable () { return this.$store.state.instance.editingAvailable } } } diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index 011dff9b..b2fad1c9 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -78,6 +78,28 @@ </button> </template> <button + v-if="ownStatus && editingAvailable" + class="button-default dropdown-item dropdown-item-icon" + @click.prevent="editStatus" + @click="close" + > + <FAIcon + fixed-width + icon="pen" + /><span>{{ $t("status.edit") }}</span> + </button> + <button + v-if="isEdited && editingAvailable" + class="button-default dropdown-item dropdown-item-icon" + @click.prevent="showStatusHistory" + @click="close" + > + <FAIcon + fixed-width + icon="history" + /><span>{{ $t("status.status_history") }}</span> + </button> + <button v-if="canDelete" class="button-default dropdown-item dropdown-item-icon" @click.prevent="deleteStatus" diff --git a/src/components/follow_card/follow_card.js b/src/components/follow_card/follow_card.js index 6dcb6d47..b26b27a7 100644 --- a/src/components/follow_card/follow_card.js +++ b/src/components/follow_card/follow_card.js @@ -1,6 +1,7 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue' import RemoteFollow from '../remote_follow/remote_follow.vue' import FollowButton from '../follow_button/follow_button.vue' +import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue' const FollowCard = { props: [ @@ -10,7 +11,8 @@ const FollowCard = { components: { BasicUserCard, RemoteFollow, - FollowButton + FollowButton, + RemoveFollowerButton }, computed: { isMe () { diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue index 895a8fa3..c919b11a 100644 --- a/src/components/follow_card/follow_card.vue +++ b/src/components/follow_card/follow_card.vue @@ -22,6 +22,11 @@ class="follow-card-follow-button" :user="user" /> + <RemoveFollowerButton + v-if="noFollowsYou && relationship.followed_by" + :relationship="relationship" + class="follow-card-button" + /> </template> </div> </basic-user-card> @@ -40,6 +45,12 @@ line-height: 1.5em; } + &-button { + margin-top: 0.5em; + padding: 0 1.5em; + margin-left: 1em; + } + &-follow-button { margin-top: 0.5em; margin-left: auto; diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 2688bcf4..7373ca63 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -121,7 +121,6 @@ border-bottom: 1px solid; border-color: $fallback--border; border-color: var(--border, $fallback--border); - padding: 0; } > li { @@ -150,12 +149,6 @@ font-size: 1.1em; } - .menu-item { - .timelines-chevron { - margin-right: 0; - } - } - .timelines-background { padding: 0 0 0 0.6em; background-color: $fallback--lightBg; diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js index fe3402fc..81cc936a 100644 --- a/src/components/navigation/navigation_entry.js +++ b/src/components/navigation/navigation_entry.js @@ -1,5 +1,6 @@ import { mapState } from 'vuex' import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js' +import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faThumbtack } from '@fortawesome/free-solid-svg-icons' @@ -7,6 +8,9 @@ library.add(faThumbtack) const NavigationEntry = { props: ['item', 'showPin'], + components: { + OptionalRouterLink + }, methods: { isPinned (value) { return this.pinnedItems.has(value) diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue index 824c00a2..f4d53836 100644 --- a/src/components/navigation/navigation_entry.vue +++ b/src/components/navigation/navigation_entry.vue @@ -1,26 +1,37 @@ <template> - <li class="NavigationEntry"> - <component - :is="routeTo ? 'router-link' : 'button'" - class="menu-item button-unstyled" - :to="routeTo" + <OptionalRouterLink + v-slot="{ isActive, href, navigate } = {}" + ass="ass" + :to="routeTo" + > + <li + class="NavigationEntry menu-item" + :class="{ '-active': isActive }" + v-bind="$attrs" > - <span> - <FAIcon - v-if="item.icon" - fixed-width - class="fa-scale-110 menu-icon" - :icon="item.icon" - /> - </span> - <span - v-if="item.iconLetter" - class="icon iconLetter fa-scale-110 menu-icon" - >{{ item.iconLetter }} - </span> - <span class="label"> - {{ item.labelRaw || $t(item.label) }} - </span> + <component + :is="routeTo ? 'a' : 'button'" + class="main-link button-unstyled" + :href="href" + @click="navigate" + > + <span> + <FAIcon + v-if="item.icon" + fixed-width + class="fa-scale-110 menu-icon" + :icon="item.icon" + /> + </span> + <span + v-if="item.iconLetter" + class="icon iconLetter fa-scale-110 menu-icon" + >{{ item.iconLetter }} + </span> + <span class="label"> + {{ item.labelRaw || $t(item.label) }} + </span> + </component> <slot /> <div v-if="item.badgeGetter && getters[item.badgeGetter]" @@ -45,8 +56,8 @@ icon="thumbtack" /> </button> - </component> - </li> + </li> + </OptionalRouterLink> </template> <script src="./navigation_entry.js"></script> @@ -55,7 +66,21 @@ @import '../../_variables.scss'; .NavigationEntry { - .label { + display: flex; + box-sizing: border-box; + align-items: baseline; + height: 3.5em; + line-height: 3.5em; + padding: 0 1em; + width: 100%; + color: $fallback--link; + color: var(--link, $fallback--link); + + .timelines-chevron { + margin-right: 0; + } + + .main-link { flex: 1; } @@ -72,48 +97,36 @@ } } - .menu-item { - display: flex; - box-sizing: border-box; - align-items: baseline; - height: 3.5em; - line-height: 3.5em; - padding: 0 1em; - width: 100%; + &:hover { + background-color: $fallback--lightBg; + background-color: var(--selectedMenu, $fallback--lightBg); color: $fallback--link; - color: var(--link, $fallback--link); - - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuText, $fallback--link); - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); + color: var(--selectedMenuText, $fallback--link); + --faint: var(--selectedMenuFaintText, $fallback--faint); + --faintLink: var(--selectedMenuFaintLink, $fallback--faint); + --lightText: var(--selectedMenuLightText, $fallback--lightText); - .menu-icon { - --icon: var(--text, $fallback--icon); - } + .menu-icon { + --icon: var(--text, $fallback--icon); } + } - &.router-link-active { - font-weight: bolder; - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedMenuText, $fallback--text); - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); + &.-active { + font-weight: bolder; + background-color: $fallback--lightBg; + background-color: var(--selectedMenu, $fallback--lightBg); + color: $fallback--text; + color: var(--selectedMenuText, $fallback--text); + --faint: var(--selectedMenuFaintText, $fallback--faint); + --faintLink: var(--selectedMenuFaintLink, $fallback--faint); + --lightText: var(--selectedMenuLightText, $fallback--lightText); - .menu-icon { - --icon: var(--text, $fallback--icon); - } + .menu-icon { + --icon: var(--text, $fallback--icon); + } - &:hover { - text-decoration: underline; - } + &:hover { + text-decoration: underline; } } } diff --git a/src/components/optional_router_link/optional_router_link.vue b/src/components/optional_router_link/optional_router_link.vue new file mode 100644 index 00000000..d56ad268 --- /dev/null +++ b/src/components/optional_router_link/optional_router_link.vue @@ -0,0 +1,23 @@ +<template> + <!-- eslint-disable vue/no-multiple-template-root --> + <router-link + v-if="to" + v-slot="props" + :to="to" + custom + > + <slot + v-bind="props" + /> + </router-link> + <slot + v-else + v-bind="{}" + /> +</template> + +<script> +export default { + props: ['to'] +} +</script> diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index c0d80b20..5c536b74 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -55,6 +55,14 @@ const pxStringToNumber = (str) => { const PostStatusForm = { props: [ + 'statusId', + 'statusText', + 'statusIsSensitive', + 'statusPoll', + 'statusFiles', + 'statusMediaDescriptions', + 'statusScope', + 'statusContentType', 'replyTo', 'repliedUser', 'attentions', @@ -62,6 +70,7 @@ const PostStatusForm = { 'subject', 'disableSubject', 'disableScopeSelector', + 'disableVisibilitySelector', 'disableNotice', 'disableLockWarning', 'disablePolls', @@ -125,22 +134,38 @@ const PostStatusForm = { const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig + let statusParams = { + spoilerText: this.subject || '', + status: statusText, + nsfw: !!sensitiveByDefault, + files: [], + poll: {}, + mediaDescriptions: {}, + visibility: scope, + contentType + } + + if (this.statusId) { + const statusContentType = this.statusContentType || contentType + statusParams = { + spoilerText: this.subject || '', + status: this.statusText || '', + nsfw: this.statusIsSensitive || !!sensitiveByDefault, + files: this.statusFiles || [], + poll: this.statusPoll || {}, + mediaDescriptions: this.statusMediaDescriptions || {}, + visibility: this.statusScope || scope, + contentType: statusContentType + } + } + return { dropFiles: [], uploadingFiles: false, error: null, posting: false, highlighted: 0, - newStatus: { - spoilerText: this.subject || '', - status: statusText, - nsfw: !!sensitiveByDefault, - files: [], - poll: {}, - mediaDescriptions: {}, - visibility: scope, - contentType - }, + newStatus: statusParams, caret: 0, pollFormVisible: false, showDropIcon: 'hide', @@ -164,7 +189,7 @@ const PostStatusForm = { emojiUserSuggestor () { return suggestor({ emoji: [ - ...this.$store.state.instance.emoji, + ...this.$store.getters.standardEmojiList, ...this.$store.state.instance.customEmoji ], store: this.$store @@ -173,13 +198,13 @@ const PostStatusForm = { emojiSuggestor () { return suggestor({ emoji: [ - ...this.$store.state.instance.emoji, + ...this.$store.getters.standardEmojiList, ...this.$store.state.instance.customEmoji ] }) }, emoji () { - return this.$store.state.instance.emoji || [] + return this.$store.getters.standardEmojiList || [] }, customEmoji () { return this.$store.state.instance.customEmoji || [] @@ -236,6 +261,9 @@ const PostStatusForm = { uploadFileLimitReached () { return this.newStatus.files.length >= this.fileLimit }, + isEdit () { + return typeof this.statusId !== 'undefined' && this.statusId.trim() !== '' + }, ...mapGetters(['mergedConfig']), ...mapState({ mobileLayout: state => state.interface.mobileLayout diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 62613bd1..f65058f4 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -67,6 +67,13 @@ <span v-else>{{ $t('post_status.direct_warning_to_all') }}</span> </p> <div + v-if="isEdit" + class="visibility-notice edit-warning" + > + <p>{{ $t('post_status.edit_remote_warning') }}</p> + <p>{{ $t('post_status.edit_unsupported_warning') }}</p> + </div> + <div v-if="!disablePreview" class="preview-heading faint" > @@ -170,6 +177,7 @@ class="visibility-tray" > <scope-selector + v-if="!disableVisibilitySelector" :show-all="showAllScopes" :user-default="userDefaultScope" :original-scope="copyMessageScope" @@ -410,6 +418,16 @@ align-items: baseline; } + .visibility-notice.edit-warning { + > :first-child { + margin-top: 0; + } + + > :last-child { + margin-bottom: 0; + } + } + .media-upload-icon, .poll-icon, .emoji-icon { font-size: 1.85em; line-height: 1.1; diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js index 5e052e1e..e65bfd93 100644 --- a/src/components/react_button/react_button.js +++ b/src/components/react_button/react_button.js @@ -59,7 +59,7 @@ const ReactButton = { if (this.filterWord !== '') { const filterWordLowercase = trim(this.filterWord.toLowerCase()) const orderedEmojiList = [] - for (const emoji of this.$store.state.instance.emoji) { + for (const emoji of this.$store.getters.standardEmojiList) { if (emoji.replacement === this.filterWord) return [emoji] const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase) @@ -72,7 +72,7 @@ const ReactButton = { } return orderedEmojiList.flat() } - return this.$store.state.instance.emoji || [] + return this.$store.getters.standardEmojiList || [] }, mergedConfig () { return this.$store.getters.mergedConfig diff --git a/src/components/remove_follower_button/remove_follower_button.js b/src/components/remove_follower_button/remove_follower_button.js new file mode 100644 index 00000000..e1a7531b --- /dev/null +++ b/src/components/remove_follower_button/remove_follower_button.js @@ -0,0 +1,25 @@ +export default { + props: ['relationship'], + data () { + return { + inProgress: false + } + }, + computed: { + label () { + if (this.inProgress) { + return this.$t('user_card.follow_progress') + } else { + return this.$t('user_card.remove_follower') + } + } + }, + methods: { + onClick () { + this.inProgress = true + this.$store.dispatch('removeUserFromFollowers', this.relationship.id).then(() => { + this.inProgress = false + }) + } + } +} diff --git a/src/components/remove_follower_button/remove_follower_button.vue b/src/components/remove_follower_button/remove_follower_button.vue new file mode 100644 index 00000000..a3a4c242 --- /dev/null +++ b/src/components/remove_follower_button/remove_follower_button.vue @@ -0,0 +1,13 @@ +<template> + <button + class="btn button-default follow-button" + :class="{ toggled: inProgress }" + :disabled="inProgress" + :title="$t('user_card.remove_follower')" + @click="onClick" + > + {{ label }} + </button> +</template> + +<script src="./remove_follower_button.js"></script> diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index 376248ef..b86faef0 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -64,7 +64,7 @@ const ProfileTab = { emojiUserSuggestor () { return suggestor({ emoji: [ - ...this.$store.state.instance.emoji, + ...this.$store.getters.standardEmojiList, ...this.$store.state.instance.customEmoji ], store: this.$store @@ -73,7 +73,7 @@ const ProfileTab = { emojiSuggestor () { return suggestor({ emoji: [ - ...this.$store.state.instance.emoji, + ...this.$store.getters.standardEmojiList, ...this.$store.state.instance.customEmoji ] }) diff --git a/src/components/status/status.js b/src/components/status/status.js index 19356664..9a9bca7a 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -395,6 +395,12 @@ const Status = { }, visibilityLocalized () { return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility) + }, + isEdited () { + return this.status.edited_at !== null + }, + editingAvailable () { + return this.$store.state.instance.editingAvailable } }, methods: { diff --git a/src/components/status/status.scss b/src/components/status/status.scss index b3ad3818..ada9841e 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -156,7 +156,8 @@ margin-right: 0.2em; } - & .heading-reply-row { + & .heading-reply-row, + & .heading-edited-row { position: relative; align-content: baseline; font-size: 0.85em; diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 91f5fffa..82eb7ac6 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -327,6 +327,24 @@ class="mentions-line" /> </div> + <div + v-if="isEdited && editingAvailable && !isPreview" + class="heading-edited-row" + > + <i18n-t + keypath="status.edited_at" + tag="span" + > + <template #time> + <Timeago + template-key="time.in_past" + :time="status.edited_at" + :auto-update="60" + :long-format="true" + /> + </template> + </i18n-t> + </div> </div> <StatusContent diff --git a/src/components/status_history_modal/status_history_modal.js b/src/components/status_history_modal/status_history_modal.js new file mode 100644 index 00000000..3941a56f --- /dev/null +++ b/src/components/status_history_modal/status_history_modal.js @@ -0,0 +1,60 @@ +import { get } from 'lodash' +import Modal from '../modal/modal.vue' +import Status from '../status/status.vue' + +const StatusHistoryModal = { + components: { + Modal, + Status + }, + data () { + return { + statuses: [] + } + }, + computed: { + modalActivated () { + return this.$store.state.statusHistory.modalActivated + }, + params () { + return this.$store.state.statusHistory.params + }, + statusId () { + return this.params.id + }, + historyCount () { + return this.statuses.length + }, + history () { + return this.statuses + } + }, + watch: { + params (newVal, oldVal) { + const newStatusId = get(newVal, 'id') !== get(oldVal, 'id') + if (newStatusId) { + this.resetHistory() + } + + if (newStatusId || get(newVal, 'edited_at') !== get(oldVal, 'edited_at')) { + this.fetchStatusHistory() + } + } + }, + methods: { + resetHistory () { + this.statuses = [] + }, + fetchStatusHistory () { + this.$store.dispatch('fetchStatusHistory', this.params) + .then(data => { + this.statuses = data + }) + }, + closeModal () { + this.$store.dispatch('closeStatusHistoryModal') + } + } +} + +export default StatusHistoryModal diff --git a/src/components/status_history_modal/status_history_modal.vue b/src/components/status_history_modal/status_history_modal.vue new file mode 100644 index 00000000..990be35b --- /dev/null +++ b/src/components/status_history_modal/status_history_modal.vue @@ -0,0 +1,46 @@ +<template> + <Modal + v-if="modalActivated" + class="status-history-modal-view" + @backdropClicked="closeModal" + > + <div class="status-history-modal-panel panel"> + <div class="panel-heading"> + {{ $t('status.status_history') }} ({{ historyCount }}) + </div> + <div class="panel-body"> + <div + v-if="historyCount > 0" + class="history-body" + > + <status + v-for="status in history" + :key="status.id" + :statusoid="status" + :is-preview="true" + class="conversation-status status-fadein panel-body" + /> + </div> + </div> + </div> + </Modal> +</template> + +<script src="./status_history_modal.js"></script> + +<style lang="scss"> +.modal-view.status-history-modal-view { + align-items: flex-start; +} +.status-history-modal-panel { + flex-shrink: 0; + margin-top: 25%; + margin-bottom: 2em; + width: 100%; + max-width: 700px; + + @media (orientation: landscape) { + margin-top: 8%; + } +} +</style> diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js index d7abbcb5..200ef147 100644 --- a/src/components/still-image/still-image.js +++ b/src/components/still-image/still-image.js @@ -7,16 +7,23 @@ const StillImage = { 'imageLoadHandler', 'alt', 'height', - 'width' + 'width', + 'dataSrc' ], data () { return { + // for lazy loading, see loadLazy() + realSrc: this.src, stopGifs: this.$store.getters.mergedConfig.stopGifs } }, computed: { animated () { - return this.stopGifs && (this.mimetype === 'image/gif' || this.src.endsWith('.gif')) + if (!this.realSrc) { + return false + } + + return this.stopGifs && (this.mimetype === 'image/gif' || this.realSrc.endsWith('.gif')) }, style () { const appendPx = (str) => /\d$/.test(str) ? str + 'px' : str @@ -27,7 +34,15 @@ const StillImage = { } }, methods: { + loadLazy () { + if (this.dataSrc) { + this.realSrc = this.dataSrc + } + }, onLoad () { + if (!this.realSrc) { + return + } const image = this.$refs.src if (!image) return this.imageLoadHandler && this.imageLoadHandler(image) @@ -42,6 +57,14 @@ const StillImage = { onError () { this.imageLoadError && this.imageLoadError() } + }, + watch: { + src () { + this.realSrc = this.src + }, + dataSrc () { + this.$el.removeAttribute('data-loaded') + } } } diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue index ab3080c8..633fb229 100644 --- a/src/components/still-image/still-image.vue +++ b/src/components/still-image/still-image.vue @@ -11,10 +11,11 @@ <!-- NOTE: key is required to force to re-render img tag when src is changed --> <img ref="src" - :key="src" + :key="realSrc" :alt="alt" :title="alt" - :src="src" + :data-src="dataSrc" + :src="realSrc" :referrerpolicy="referrerpolicy" @load="onLoad" @error="onError" diff --git a/src/components/timeago/timeago.vue b/src/components/timeago/timeago.vue index 2b487dfd..b5f49515 100644 --- a/src/components/timeago/timeago.vue +++ b/src/components/timeago/timeago.vue @@ -3,7 +3,7 @@ :datetime="time" :title="localeDateString" > - {{ $tc(relativeTime.key, relativeTime.num, [relativeTime.num]) }} + {{ relativeTimeString }} </time> </template> @@ -13,7 +13,7 @@ import localeService from 'src/services/locale/locale.service.js' export default { name: 'Timeago', - props: ['time', 'autoUpdate', 'longFormat', 'nowThreshold'], + props: ['time', 'autoUpdate', 'longFormat', 'nowThreshold', 'templateKey'], data () { return { relativeTime: { key: 'time.now', num: 0 }, @@ -26,6 +26,23 @@ export default { return typeof this.time === 'string' ? new Date(Date.parse(this.time)).toLocaleString(browserLocale) : this.time.toLocaleString(browserLocale) + }, + relativeTimeString () { + const timeString = this.$i18n.tc(this.relativeTime.key, this.relativeTime.num, [this.relativeTime.num]) + + if (typeof this.templateKey === 'string' && this.relativeTime.key !== 'time.now') { + return this.$i18n.t(this.templateKey, [timeString]) + } + + return timeString + } + }, + watch: { + time (newVal, oldVal) { + if (oldVal !== newVal) { + clearTimeout(this.interval) + this.refreshRelativeTimeObject() + } } }, created () { diff --git a/src/components/update_notification/update_notification.js b/src/components/update_notification/update_notification.js index 609842c4..ddf379f5 100644 --- a/src/components/update_notification/update_notification.js +++ b/src/components/update_notification/update_notification.js @@ -2,6 +2,8 @@ import Modal from 'src/components/modal/modal.vue' import { library } from '@fortawesome/fontawesome-svg-core' import pleromaTan from 'src/assets/pleromatan_apology.png' import pleromaTanFox from 'src/assets/pleromatan_apology_fox.png' +import pleromaTanMask from 'src/assets/pleromatan_apology_mask.png' +import pleromaTanFoxMask from 'src/assets/pleromatan_apology_fox_mask.png' import { faTimes @@ -15,9 +17,9 @@ export const CURRENT_UPDATE_COUNTER = 1 const UpdateNotification = { data () { return { + showingImage: false, pleromaTanVariant: Math.random() > 0.5 ? pleromaTan : pleromaTanFox, - showingMore: false, - contentHeight: 0 + showingMore: false } }, components: { @@ -25,13 +27,9 @@ const UpdateNotification = { }, computed: { pleromaTanStyles () { + const mask = this.pleromaTanVariant === pleromaTan ? pleromaTanMask : pleromaTanFoxMask return { - 'shape-outside': 'url(' + this.pleromaTanVariant + ')' - } - }, - dynamicStyles () { - return { - '--____extraInfoGroupHeight': this.contentHeight + 'px' + 'shape-outside': 'url(' + mask + ')' } }, shouldShow () { @@ -57,9 +55,14 @@ const UpdateNotification = { } }, mounted () { - setTimeout(() => { - this.contentHeight = this.$refs.animatedText.scrollHeight - }, 1000) + this.contentHeightNoImage = this.$refs.animatedText.scrollHeight + + // Workaround to get the text height only after mask loaded. A bit hacky. + const newImg = new Image() + newImg.onload = () => { + setTimeout(() => { this.showingImage = true }, 100) + } + newImg.src = this.pleromaTanVariant === pleromaTan ? pleromaTanMask : pleromaTanFoxMask } } diff --git a/src/components/update_notification/update_notification.scss b/src/components/update_notification/update_notification.scss index 8cad1bc7..ce8129d0 100644 --- a/src/components/update_notification/update_notification.scss +++ b/src/components/update_notification/update_notification.scss @@ -35,6 +35,12 @@ margin-top: calc(-1 * var(--__top-fringe)); margin-bottom: calc(-1 * var(--__bottom-fringe)); margin-right: calc(-1 * var(--__right-fringe)); + + &.-noImage { + .text { + padding-right: var(--__right-fringe); + } + } } .panel-body { @@ -75,9 +81,9 @@ .extra-info-group { transition: max-height, padding, height; - transition-timing-function: ease-in-out; - transition-duration: 500ms; - max-height: calc(var(--____extraInfoGroupHeight) + 1em); // include bottom padding + transition-timing-function: ease-in; + transition-duration: 700ms; + max-height: 70vh; mask: linear-gradient(to top, white, transparent) bottom/100% 2px no-repeat, linear-gradient(to top, white, white); diff --git a/src/components/update_notification/update_notification.vue b/src/components/update_notification/update_notification.vue index 00841af2..78e70a74 100644 --- a/src/components/update_notification/update_notification.vue +++ b/src/components/update_notification/update_notification.vue @@ -7,7 +7,6 @@ <div class="UpdateNotificationModal panel" :class="{ '-peek': !showingMore }" - :style="dynamicStyles" > <div class="panel-heading"> <span class="title"> @@ -15,8 +14,12 @@ </span> </div> <div class="panel-body"> - <div class="content"> + <div + class="content" + :class="{ '-noImage': !showingImage }" + > <img + v-if="showingImage" class="pleroma-tan" :src="pleromaTanVariant" :style="pleromaTanStyles" diff --git a/src/i18n/en.json b/src/i18n/en.json index ffa4c6b0..30b59e82 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -199,8 +199,20 @@ "add_emoji": "Insert emoji", "custom": "Custom emoji", "unicode": "Unicode emoji", + "unicode_groups": { + "activities": "Activities", + "animals-and-nature": "Animals & Nature", + "flags": "Flags", + "food-and-drink": "Food & Drink", + "objects": "Objects", + "people-and-body": "People & Body", + "smileys-and-emotion": "Smileys & Emotion", + "symbols": "Symbols", + "travel-and-places": "Travel & Places" + }, "load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.", - "load_all": "Loading all {emojiAmount} emoji" + "load_all": "Loading all {emojiAmount} emoji", + "regional_indicator": "Regional indicator {letter}" }, "errors": { "storage_unavailable": "Pleroma could not access browser storage. Your login or your local settings won't be saved and you might encounter unexpected issues. Try enabling cookies." @@ -214,6 +226,7 @@ "load_older": "Load older interactions" }, "post_status": { + "edit_status": "Edit status", "new_status": "Post new status", "account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.", "account_not_locked_warning_link": "locked", @@ -229,6 +242,8 @@ "default": "Just landed in L.A.", "direct_warning_to_all": "This post will be visible to all the mentioned users.", "direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.", + "edit_remote_warning": "Other remote instances may not support editing and unable to receive the latest version of your post.", + "edit_unsupported_warning": "Pleroma does not support editing mentions or polls.", "posting": "Posting", "post": "Post", "preview": "Preview", @@ -797,6 +812,8 @@ "favorites": "Favorites", "repeats": "Repeats", "delete": "Delete status", + "edit": "Edit status", + "edited_at": "(last edited {time})", "pin": "Pin on profile", "unpin": "Unpin from profile", "pinned": "Pinned", @@ -844,7 +861,8 @@ "ancestor_follow_with_icon": "{icon} {text}", "show_all_conversation_with_icon": "{icon} {text}", "show_all_conversation": "Show full conversation ({numStatus} other status) | Show full conversation ({numStatus} other statuses)", - "show_only_conversation_under_this": "Only show replies to this status" + "show_only_conversation_under_this": "Only show replies to this status", + "status_history": "Status history" }, "user_card": { "approve": "Approve", @@ -872,6 +890,7 @@ "muted": "Muted", "per_day": "per day", "remote_follow": "Remote follow", + "remove_follower": "Remove follower", "report": "Report", "statuses": "Statuses", "subscribe": "Subscribe", diff --git a/src/i18n/languages.js b/src/i18n/languages.js new file mode 100644 index 00000000..250b3b1a --- /dev/null +++ b/src/i18n/languages.js @@ -0,0 +1,53 @@ + +const languages = [ + 'ar', + 'ca', + 'cs', + 'de', + 'eo', + 'en', + 'es', + 'et', + 'eu', + 'fi', + 'fr', + 'ga', + 'he', + 'hu', + 'it', + 'ja', + 'ja_easy', + 'ko', + 'nb', + 'nl', + 'oc', + 'pl', + 'pt', + 'ro', + 'ru', + 'sk', + 'te', + 'uk', + 'zh', + 'zh_Hant' +] + +const specialJsonName = { + ja: 'ja_pedantic' +} + +const langCodeToJsonName = (code) => specialJsonName[code] || code + +const langCodeToCldrName = (code) => code + +const ensureFinalFallback = codes => { + const codeList = Array.isArray(codes) ? codes : [codes] + return codeList.includes('en') ? codeList : codeList.concat(['en']) +} + +module.exports = { + languages, + langCodeToJsonName, + langCodeToCldrName, + ensureFinalFallback +} diff --git a/src/i18n/messages.js b/src/i18n/messages.js index eae75c80..74a89ca8 100644 --- a/src/i18n/messages.js +++ b/src/i18n/messages.js @@ -7,46 +7,26 @@ // sed -i -e "s/'//gm" -e 's/"/\\"/gm' -re 's/^( +)(.+?): ((.+?))?(,?)(\{?)$/\1"\2": "\4"/gm' -e 's/\"\{\"/{/g' -e 's/,"$/",/g' file.json // There's only problem that apostrophe character ' gets replaced by \\ so you have to fix it manually, sorry. -const loaders = { - ar: () => import('./ar.json'), - ca: () => import('./ca.json'), - cs: () => import('./cs.json'), - de: () => import('./de.json'), - eo: () => import('./eo.json'), - es: () => import('./es.json'), - et: () => import('./et.json'), - eu: () => import('./eu.json'), - fi: () => import('./fi.json'), - fr: () => import('./fr.json'), - ga: () => import('./ga.json'), - he: () => import('./he.json'), - hu: () => import('./hu.json'), - it: () => import('./it.json'), - ja: () => import('./ja_pedantic.json'), - ja_easy: () => import('./ja_easy.json'), - ko: () => import('./ko.json'), - nb: () => import('./nb.json'), - nl: () => import('./nl.json'), - oc: () => import('./oc.json'), - pl: () => import('./pl.json'), - pt: () => import('./pt.json'), - ro: () => import('./ro.json'), - ru: () => import('./ru.json'), - sk: () => import('./sk.json'), - te: () => import('./te.json'), - uk: () => import('./uk.json'), - zh: () => import('./zh.json'), - zh_Hant: () => import('./zh_Hant.json') +import { languages, langCodeToJsonName } from './languages.js' + +const hasLanguageFile = (code) => languages.includes(code) + +const loadLanguageFile = (code) => { + return import( + /* webpackInclude: /\.json$/ */ + /* webpackChunkName: "i18n/[request]" */ + `./${langCodeToJsonName(code)}.json` + ) } const messages = { - languages: ['en', ...Object.keys(loaders)], + languages, default: { en: require('./en.json').default }, setLanguage: async (i18n, language) => { - if (loaders[language]) { - const messages = await loaders[language]() + if (hasLanguageFile(language)) { + const messages = await loadLanguageFile(language) i18n.setLocaleMessage(language, messages.default) } i18n.locale = language diff --git a/src/main.js b/src/main.js index 0a050b04..6aa9cbb7 100644 --- a/src/main.js +++ b/src/main.js @@ -20,6 +20,9 @@ import oauthTokensModule from './modules/oauth_tokens.js' import reportsModule from './modules/reports.js' import pollsModule from './modules/polls.js' import postStatusModule from './modules/postStatus.js' +import editStatusModule from './modules/editStatus.js' +import statusHistoryModule from './modules/statusHistory.js' + import chatsModule from './modules/chats.js' import { createI18n } from 'vue-i18n' @@ -86,6 +89,8 @@ const persistedStateOptions = { reports: reportsModule, polls: pollsModule, postStatus: postStatusModule, + editStatus: editStatusModule, + statusHistory: statusHistoryModule, chats: chatsModule }, plugins, diff --git a/src/modules/api.js b/src/modules/api.js index f783fa4f..0acc03f1 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -16,7 +16,7 @@ const api = { followRequests: [] }, getters: { - followRequestCount: state => state.api.followRequests.length + followRequestCount: state => state.followRequests.length }, mutations: { setBackendInteractor (state, backendInteractor) { @@ -103,6 +103,13 @@ const api = { showImmediately: timelineData.visibleStatuses.length === 0, timeline: 'friends' }) + } else if (message.event === 'status.update') { + dispatch('addNewStatuses', { + statuses: [message.status], + userId: false, + showImmediately: message.status.id in timelineData.visibleStatusesObject, + timeline: 'friends' + }) } else if (message.event === 'delete') { dispatch('deleteStatusById', message.id) } else if (message.event === 'pleroma:chat_update') { diff --git a/src/modules/config.js b/src/modules/config.js index eeaac917..c966602e 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -183,6 +183,7 @@ const config = { break case 'interfaceLanguage': messages.setLanguage(this.getters.i18n, value) + dispatch('loadUnicodeEmojiData', value) Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value)) break case 'thirdColumnMode': diff --git a/src/modules/editStatus.js b/src/modules/editStatus.js new file mode 100644 index 00000000..fd316519 --- /dev/null +++ b/src/modules/editStatus.js @@ -0,0 +1,25 @@ +const editStatus = { + state: { + params: null, + modalActivated: false + }, + mutations: { + openEditStatusModal (state, params) { + state.params = params + state.modalActivated = true + }, + closeEditStatusModal (state) { + state.modalActivated = false + } + }, + actions: { + openEditStatusModal ({ commit }, params) { + commit('openEditStatusModal', params) + }, + closeEditStatusModal ({ commit }) { + commit('closeEditStatusModal') + } + } +} + +export default editStatus diff --git a/src/modules/instance.js b/src/modules/instance.js index bfce6f38..b1bc9779 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -2,6 +2,39 @@ import { getPreset, applyTheme } from '../services/style_setter/style_setter.js' import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' import apiService from '../services/api/api.service.js' import { instanceDefaultProperties } from './config.js' +import { langCodeToCldrName, ensureFinalFallback } from '../i18n/languages.js' + +const SORTED_EMOJI_GROUP_IDS = [ + 'smileys-and-emotion', + 'people-and-body', + 'animals-and-nature', + 'food-and-drink', + 'travel-and-places', + 'activities', + 'objects', + 'symbols', + 'flags' +] + +const REGIONAL_INDICATORS = (() => { + const start = 0x1F1E6 + const end = 0x1F1FF + const A = 'A'.codePointAt(0) + const res = new Array(end - start + 1) + for (let i = start; i <= end; ++i) { + const letter = String.fromCodePoint(A + i - start) + res[i - start] = { + replacement: String.fromCodePoint(i), + imageUrl: false, + displayText: 'regional_indicator_' + letter, + displayTextI18n: { + key: 'emoji.regional_indicator', + args: { letter } + } + } + } + return res +})() const defaultState = { // Stuff from apiConfig @@ -64,8 +97,9 @@ const defaultState = { // Nasty stuff customEmoji: [], customEmojiFetched: false, - emoji: [], + emoji: {}, emojiFetched: false, + unicodeEmojiAnnotations: {}, pleromaBackend: true, postFormats: [], restrictedNicknames: [], @@ -97,6 +131,31 @@ const defaultState = { } } +const loadAnnotations = (lang) => { + return import( + /* webpackChunkName: "emoji-annotations/[request]" */ + `@kazvmoe-infra/unicode-emoji-json/annotations/${langCodeToCldrName(lang)}.json` + ) + .then(k => k.default) +} + +const injectAnnotations = (emoji, annotations) => { + const availableLangs = Object.keys(annotations) + + return { + ...emoji, + annotations: availableLangs.reduce((acc, cur) => { + acc[cur] = annotations[cur][emoji.replacement] + return acc + }, {}) + } +} + +const injectRegionalIndicators = groups => { + groups.symbols.push(...REGIONAL_INDICATORS) + return groups +} + const instance = { state: defaultState, mutations: { @@ -107,6 +166,9 @@ const instance = { }, setKnownDomains (state, domains) { state.knownDomains = domains + }, + setUnicodeEmojiAnnotations (state, { lang, annotations }) { + state.unicodeEmojiAnnotations[lang] = annotations } }, getters: { @@ -115,6 +177,41 @@ const instance = { .map(key => [key, state[key]]) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) }, + groupedCustomEmojis (state) { + const packsOf = emoji => { + return emoji.tags + .filter(k => k.startsWith('pack:')) + .map(k => k.slice(5)) // remove 'pack:' prefix + } + + return state.customEmoji + .reduce((res, emoji) => { + packsOf(emoji).forEach(packName => { + const packId = `custom-${packName}` + if (!res[packId]) { + res[packId] = ({ + id: packId, + text: packName, + image: emoji.imageUrl, + emojis: [] + }) + } + res[packId].emojis.push(emoji) + }) + return res + }, {}) + }, + standardEmojiList (state) { + return SORTED_EMOJI_GROUP_IDS + .map(groupId => (state.emoji[groupId] || []).map(k => injectAnnotations(k, state.unicodeEmojiAnnotations))) + .reduce((a, b) => a.concat(b), []) + }, + standardEmojiGroupList (state) { + return SORTED_EMOJI_GROUP_IDS.map(groupId => ({ + id: groupId, + emojis: (state.emoji[groupId] || []).map(k => injectAnnotations(k, state.unicodeEmojiAnnotations)) + })) + }, instanceDomain (state) { return new URL(state.server).hostname } @@ -138,32 +235,52 @@ const instance = { }, async getStaticEmoji ({ commit }) { try { - const res = await window.fetch('/static/emoji.json') - if (res.ok) { - const values = await res.json() - const emoji = Object.keys(values).map((key) => { - return { - displayText: key, - imageUrl: false, - replacement: values[key] - } - }).sort((a, b) => a.name > b.name ? 1 : -1) - commit('setInstanceOption', { name: 'emoji', value: emoji }) - } else { - throw (res) - } + const values = (await import(/* webpackChunkName: 'emoji' */ '../../static/emoji.json')).default + + const emoji = Object.keys(values).reduce((res, groupId) => { + res[groupId] = values[groupId].map(e => ({ + displayText: e.slug, + imageUrl: false, + replacement: e.emoji + })) + return res + }, {}) + commit('setInstanceOption', { name: 'emoji', value: injectRegionalIndicators(emoji) }) } catch (e) { console.warn("Can't load static emoji") console.warn(e) } }, + loadUnicodeEmojiData ({ commit, state }, language) { + const langList = ensureFinalFallback(language) + + return Promise.all( + langList + .map(async lang => { + if (!state.unicodeEmojiAnnotations[lang]) { + const annotations = await loadAnnotations(lang) + commit('setUnicodeEmojiAnnotations', { lang, annotations }) + } + })) + }, + async getCustomEmoji ({ commit, state }) { try { const res = await window.fetch('/api/pleroma/emoji.json') if (res.ok) { const result = await res.json() const values = Array.isArray(result) ? Object.assign({}, ...result) : result + const caseInsensitiveStrCmp = (a, b) => { + const la = a.toLowerCase() + const lb = b.toLowerCase() + return la > lb ? 1 : (la < lb ? -1 : 0) + } + const byPackThenByName = (a, b) => { + const packOf = emoji => (emoji.tags.filter(k => k.startsWith('pack:'))[0] || '').slice(5) + return caseInsensitiveStrCmp(packOf(a), packOf(b)) || caseInsensitiveStrCmp(a.displayText, b.displayText) + } + const emoji = Object.entries(values).map(([key, value]) => { const imageUrl = value.image_url return { @@ -174,7 +291,7 @@ const instance = { } // Technically could use tags but those are kinda useless right now, // should have been "pack" field, that would be more useful - }).sort((a, b) => a.displayText.toLowerCase() > b.displayText.toLowerCase() ? 1 : -1) + }).sort(byPackThenByName) commit('setInstanceOption', { name: 'customEmoji', value: emoji }) } else { throw (res) diff --git a/src/modules/statusHistory.js b/src/modules/statusHistory.js new file mode 100644 index 00000000..db3d6d4b --- /dev/null +++ b/src/modules/statusHistory.js @@ -0,0 +1,25 @@ +const statusHistory = { + state: { + params: {}, + modalActivated: false + }, + mutations: { + openStatusHistoryModal (state, params) { + state.params = params + state.modalActivated = true + }, + closeStatusHistoryModal (state) { + state.modalActivated = false + } + }, + actions: { + openStatusHistoryModal ({ commit }, params) { + commit('openStatusHistoryModal', params) + }, + closeStatusHistoryModal ({ commit }) { + commit('closeStatusHistoryModal') + } + } +} + +export default statusHistory diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 4aa04683..803d7019 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -249,6 +249,9 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us status: (status) => { addStatus(status, showImmediately) }, + edit: (status) => { + addStatus(status, showImmediately) + }, retweet: (status) => { // RetweetedStatuses are never shown immediately const retweetedStatus = addStatus(status.retweeted_status, false, false) @@ -606,6 +609,12 @@ const statuses = { return rootState.api.backendInteractor.fetchStatus({ id }) .then((status) => dispatch('addNewStatuses', { statuses: [status] })) }, + fetchStatusSource ({ rootState, dispatch }, status) { + return apiService.fetchStatusSource({ id: status.id, credentials: rootState.users.currentUser.credentials }) + }, + fetchStatusHistory ({ rootState, dispatch }, status) { + return apiService.fetchStatusHistory({ status }) + }, deleteStatus ({ rootState, commit }, status) { commit('setDeleted', { status }) apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials }) diff --git a/src/modules/users.js b/src/modules/users.js index de28766a..eef87c2c 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -51,6 +51,11 @@ const unblockUser = (store, id) => { .then((relationship) => store.commit('updateUserRelationship', [relationship])) } +const removeUserFromFollowers = (store, id) => { + return store.rootState.api.backendInteractor.removeUserFromFollowers({ id }) + .then((relationship) => store.commit('updateUserRelationship', [relationship])) +} + const muteUser = (store, id) => { const predictedRelationship = store.state.relationships[id] || { id } predictedRelationship.muting = true @@ -321,6 +326,9 @@ const users = { unblockUser (store, id) { return unblockUser(store, id) }, + removeUserFromFollowers (store, id) { + return removeUserFromFollowers(store, id) + }, blockUsers (store, ids = []) { return Promise.all(ids.map(id => blockUser(store, id))) }, diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index dd85b281..e692338e 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1,5 +1,5 @@ import { each, map, concat, last, get } from 'lodash' -import { parseStatus, parseUser, parseNotification, parseAttachment, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js' +import { parseStatus, parseSource, parseUser, parseNotification, parseAttachment, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js' import { RegistrationError, StatusCodeError } from '../errors/errors' /* eslint-env browser */ @@ -49,6 +49,8 @@ const MASTODON_PUBLIC_TIMELINE = '/api/v1/timelines/public' const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home' const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}` const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context` +const MASTODON_STATUS_SOURCE_URL = id => `/api/v1/statuses/${id}/source` +const MASTODON_STATUS_HISTORY_URL = id => `/api/v1/statuses/${id}/history` const MASTODON_USER_URL = '/api/v1/accounts' const MASTODON_USER_LOOKUP_URL = '/api/v1/accounts/lookup' const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships' @@ -65,6 +67,7 @@ const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block` const MASTODON_UNBLOCK_USER_URL = id => `/api/v1/accounts/${id}/unblock` const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute` const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute` +const MASTODON_REMOVE_USER_FROM_FOLLOWERS = id => `/api/v1/accounts/${id}/remove_from_followers` const MASTODON_SUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/subscribe` const MASTODON_UNSUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/unsubscribe` const MASTODON_BOOKMARK_STATUS_URL = id => `/api/v1/statuses/${id}/bookmark` @@ -305,6 +308,13 @@ const unblockUser = ({ id, credentials }) => { }).then((data) => data.json()) } +const removeUserFromFollowers = ({ id, credentials }) => { + return fetch(MASTODON_REMOVE_USER_FROM_FOLLOWERS(id), { + headers: authHeaders(credentials), + method: 'POST' + }).then((data) => data.json()) +} + const approveUser = ({ id, credentials }) => { const url = MASTODON_APPROVE_USER_URL(id) return fetch(url, { @@ -522,6 +532,31 @@ const fetchStatus = ({ id, credentials }) => { .then((data) => parseStatus(data)) } +const fetchStatusSource = ({ id, credentials }) => { + const url = MASTODON_STATUS_SOURCE_URL(id) + return fetch(url, { headers: authHeaders(credentials) }) + .then((data) => { + if (data.ok) { + return data + } + throw new Error('Error fetching source', data) + }) + .then((data) => data.json()) + .then((data) => parseSource(data)) +} + +const fetchStatusHistory = ({ status, credentials }) => { + const url = MASTODON_STATUS_HISTORY_URL(status.id) + return promisedRequest({ url, credentials }) + .then((data) => { + data.reverse() + return data.map((item) => { + item.originalStatus = status + return parseStatus(item) + }) + }) +} + const tagUser = ({ tag, credentials, user }) => { const screenName = user.screen_name const form = { @@ -825,6 +860,54 @@ const postStatus = ({ .then((data) => data.error ? data : parseStatus(data)) } +const editStatus = ({ + id, + credentials, + status, + spoilerText, + sensitive, + poll, + mediaIds = [], + contentType +}) => { + const form = new FormData() + const pollOptions = poll.options || [] + + form.append('status', status) + if (spoilerText) form.append('spoiler_text', spoilerText) + if (sensitive) form.append('sensitive', sensitive) + if (contentType) form.append('content_type', contentType) + mediaIds.forEach(val => { + form.append('media_ids[]', val) + }) + + if (pollOptions.some(option => option !== '')) { + const normalizedPoll = { + expires_in: poll.expiresIn, + multiple: poll.multiple + } + Object.keys(normalizedPoll).forEach(key => { + form.append(`poll[${key}]`, normalizedPoll[key]) + }) + + pollOptions.forEach(option => { + form.append('poll[options][]', option) + }) + } + + const putHeaders = authHeaders(credentials) + + return fetch(MASTODON_STATUS_URL(id), { + body: form, + method: 'PUT', + headers: putHeaders + }) + .then((response) => { + return response.json() + }) + .then((data) => data.error ? data : parseStatus(data)) +} + const deleteStatus = ({ id, credentials }) => { return fetch(MASTODON_DELETE_URL(id), { headers: authHeaders(credentials), @@ -1291,7 +1374,8 @@ const MASTODON_STREAMING_EVENTS = new Set([ 'update', 'notification', 'delete', - 'filters_changed' + 'filters_changed', + 'status.update' ]) const PLEROMA_STREAMING_EVENTS = new Set([ @@ -1363,6 +1447,8 @@ export const handleMastoWS = (wsEvent) => { const data = payload ? JSON.parse(payload) : null if (event === 'update') { return { event, status: parseStatus(data) } + } else if (event === 'status.update') { + return { event, status: parseStatus(data) } } else if (event === 'notification') { return { event, notification: parseNotification(data) } } else if (event === 'pleroma:chat_update') { @@ -1497,6 +1583,8 @@ const apiService = { fetchPinnedStatuses, fetchConversation, fetchStatus, + fetchStatusSource, + fetchStatusHistory, fetchFriends, exportFriends, fetchFollowers, @@ -1508,6 +1596,7 @@ const apiService = { unmuteConversation, blockUser, unblockUser, + removeUserFromFollowers, fetchUser, fetchUserByName, fetchUserRelationship, @@ -1518,6 +1607,7 @@ const apiService = { bookmarkStatus, unbookmarkStatus, postStatus, + editStatus, deleteStatus, uploadMedia, setMediaDescription, diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 451427da..23061eba 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -251,6 +251,16 @@ export const parseAttachment = (data) => { return output } +export const parseSource = (data) => { + const output = {} + + output.text = data.text + output.spoiler_text = data.spoiler_text + output.content_type = data.content_type + + return output +} + export const parseStatus = (data) => { const output = {} const masto = Object.prototype.hasOwnProperty.call(data, 'account') @@ -272,6 +282,8 @@ export const parseStatus = (data) => { output.tags = data.tags + output.edited_at = data.edited_at + if (data.pleroma) { const { pleroma } = data output.text = pleroma.content ? data.pleroma.content['text/plain'] : data.content @@ -373,6 +385,10 @@ export const parseStatus = (data) => { output.favoritedBy = [] output.rebloggedBy = [] + if (Object.prototype.hasOwnProperty.call(data, 'originalStatus')) { + Object.assign(output, data.originalStatus) + } + return output } diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js index f09196aa..1eb10bb6 100644 --- a/src/services/status_poster/status_poster.service.js +++ b/src/services/status_poster/status_poster.service.js @@ -47,6 +47,47 @@ const postStatus = ({ }) } +const editStatus = ({ + store, + statusId, + status, + spoilerText, + sensitive, + poll, + media = [], + contentType = 'text/plain' +}) => { + const mediaIds = map(media, 'id') + + return apiService.editStatus({ + id: statusId, + credentials: store.state.users.currentUser.credentials, + status, + spoilerText, + sensitive, + poll, + mediaIds, + contentType + }) + .then((data) => { + if (!data.error) { + store.dispatch('addNewStatuses', { + statuses: [data], + timeline: 'friends', + showImmediately: true, + noIdUpdate: true // To prevent missing notices on next pull. + }) + } + return data + }) + .catch((err) => { + console.error('Error editing status', err) + return { + error: err.message + } + }) +} + const uploadMedia = ({ store, formData }) => { const credentials = store.state.users.currentUser.credentials return apiService.uploadMedia({ credentials, formData }) @@ -59,6 +100,7 @@ const setMediaDescription = ({ store, id, description }) => { const statusPosterService = { postStatus, + editStatus, uploadMedia, setMediaDescription } diff --git a/static/emoji.json b/static/emoji.json deleted file mode 100644 index 12b91b3f..00000000 --- a/static/emoji.json +++ /dev/null @@ -1,1431 +0,0 @@ -{ - "100": "๐ฏ", - "1234": "๐ข", - "1st_place_medal": "๐ฅ", - "2nd_place_medal": "๐ฅ", - "3rd_place_medal": "๐ฅ", - "8ball": "๐ฑ", - "a_button_blood_type": "๐
ฐ", - "ab": "๐", - "abacus": "๐งฎ", - "abc": "๐ค", - "abcd": "๐ก", - "accept": "๐", - "adhesive_bandage": "๐ฉน", - "admission_tickets": "๐", - "adult": "๐ง", - "aerial_tramway": "๐ก", - "airplane": "โ", - "airplane_arriving": "๐ฌ", - "airplane_departure": "๐ซ", - "alarm_clock": "โฐ", - "alembic": "โ๏ธ", - "alien": "๐ฝ", - "ambulance": "๐", - "amphora": "๐บ", - "anchor": "โ", - "angel": "๐ผ", - "anger": "๐ข", - "anger_right": "๐ฏ", - "angry": "๐ ", - "anguished": "๐ง", - "ant": "๐", - "apple": "๐", - "aquarius": "โ", - "aries": "โ", - "arrow_backward": "โ๏ธ", - "arrow_double_down": "โฌ", - "arrow_double_up": "โซ", - "arrow_down": "โฌ๏ธ", - "arrow_down_small": "๐ฝ", - "arrow_forward": "โถ๏ธ", - "arrow_heading_down": "โคต๏ธ", - "arrow_heading_up": "โคด๏ธ", - "arrow_left": "โฌ
๏ธ", - "arrow_lower_left": "โ๏ธ", - "arrow_lower_right": "โ๏ธ", - "arrow_right": "โก", - "arrow_right_hook": "โช๏ธ", - "arrow_up": "โฌ๏ธ", - "arrow_up_down": "โ", - "arrow_up_small": "๐ผ", - "arrow_upper_left": "โ", - "arrow_upper_right": "โ๏ธ", - "arrows_clockwise": "๐", - "arrows_counterclockwise": "๐", - "art": "๐จ", - "articulated_lorry": "๐", - "artist_palette": "๐จ", - "asterisk": "*โฃ", - "astonished": "๐ฒ", - "athletic_shoe": "๐", - "atm": "๐ง", - "atom": "โ", - "atom_symbol": "โ๏ธ", - "auto_rickshaw": "๐บ", - "automobile": "๐", - "avocado": "๐ฅ", - "axe": "๐ช", - "b_button_blood_type": "๐
ฑ", - "baby": "๐ถ", - "baby_bottle": "๐ผ", - "baby_chick": "๐ค", - "baby_symbol": "๐ผ", - "back": "๐", - "bacon": "๐ฅ", - "badger": "๐ฆก", - "badminton": "๐ธ", - "bagel": "๐ฅฏ", - "baggage_claim": "๐", - "baguette_bread": "๐ฅ", - "balance_scale": "โ๏ธ", - "bald": "๐ฆฒ", - "ballet_shoes": "๐ฉฐ", - "balloon": "๐", - "ballot_box": "๐ณ", - "ballot_box_with_check": "โ๏ธ", - "bamboo": "๐", - "banana": "๐", - "bangbang": "โผ๏ธ", - "banjo": "๐ช", - "bank": "๐ฆ", - "bar_chart": "๐", - "barber": "๐", - "baseball": "โพ", - "basket": "๐งบ", - "basketball": "๐", - "basketballer": "โน", - "bat": "๐ฆ", - "bath": "๐", - "bathtub": "๐", - "battery": "๐", - "beach_umbrella": "โฑ", - "beach_with_umbrella": "๐", - "bear": "๐ป", - "beard": "๐ง", - "bearded_person": "๐ง", - "bed": "๐", - "bee": "๐", - "beer": "๐บ", - "beers": "๐ป", - "beetle": "๐", - "beginner": "๐ฐ", - "bell": "๐", - "bellhop_bell": "๐", - "bento": "๐ฑ", - "beverage_box": "๐ง", - "bicyclist": "๐ด", - "bike": "๐ฒ", - "bikini": "๐", - "billed_cap": "๐งข", - "biohazard": "โฃ๏ธ", - "bird": "๐ฆ", - "birthday": "๐", - "black_circle": "โซ", - "black_heart": "๐ค", - "black_joker": "๐", - "black_large_square": "โฌ", - "black_medium_small_square": "โพ", - "black_medium_square": "โผ", - "black_nib": "โ๏ธ", - "black_small_square": "โช", - "black_square_button": "๐ฒ", - "blond_haired_person": "๐ฑ", - "blossom": "๐ผ", - "blowfish": "๐ก", - "blue_book": "๐", - "blue_car": "๐", - "blue_circle": "๐ต", - "blue_heart": "๐", - "blue_square": "๐ฆ", - "blush": "๐", - "boar": "๐", - "bomb": "๐ฃ", - "bone": "๐ฆด", - "book": "๐", - "bookmark": "๐", - "bookmark_tabs": "๐", - "books": "๐", - "boom": "๐ฅ", - "boot": "๐ข", - "bouquet": "๐", - "bow": "๐", - "bow_and_arrow": "๐น", - "bowl_with_spoon": "๐ฅฃ", - "bowling": "๐ณ", - "boxing_glove": "๐ฅ", - "boy": "๐ฆ", - "brain": "๐ง ", - "bread": "๐", - "breast_feeding": "๐คฑ", - "breastfeeding": "๐คฑ", - "brick": "๐งฑ", - "bride_with_veil": "๐ฐ", - "bridge_at_night": "๐", - "briefcase": "๐ผ", - "briefs": "๐ฉฒ", - "broccoli": "๐ฅฆ", - "broken_heart": "๐", - "broom": "๐งน", - "brown_circle": "๐ค", - "brown_heart": "๐ค", - "bug": "๐", - "building_construction": "๐", - "bulb": "๐ก", - "bullettrain_front": "๐
", - "bullettrain_side": "๐", - "burrito": "๐ฏ", - "bus": "๐", - "busstop": "๐", - "bust_in_silhouette": "๐ค", - "busts_in_silhouette": "๐ฅ", - "butter": "๐ง", - "butterfly": "๐ฆ", - "cactus": "๐ต", - "cake": "๐ฐ", - "calendar": "๐", - "call_me": "๐ค", - "call_me_hand": "๐ค", - "calling": "๐ฒ", - "camel": "๐ซ", - "camera": "๐ท", - "camera_with_flash": "๐ธ", - "camping": "๐", - "cancer": "โ", - "candle": "๐ฏ", - "candy": "๐ฌ", - "canned_food": "๐ฅซ", - "canoe": "๐ถ", - "capital_abcd": "๐ ", - "capricorn": "โ", - "card_file_box": "๐", - "card_index": "๐", - "card_index_dividers": "๐", - "carousel_horse": "๐ ", - "carrot": "๐ฅ", - "cat": "๐ฑ", - "cat2": "๐", - "cd": "๐ฟ", - "chains": "โ๏ธ", - "chair": "๐ช", - "champagne": "๐พ", - "champagne_glass": "๐ฅ", - "chart": "๐น", - "chart_with_downwards_trend": "๐", - "chart_with_upwards_trend": "๐", - "check_box_with_check": "โ", - "check_mark": "โ", - "checkered_flag": "๐", - "cheese": "๐ง", - "cheese_wedge": "๐ง", - "cherries": "๐", - "cherry_blossom": "๐ธ", - "chess_pawn": "โ", - "chestnut": "๐ฐ", - "chicken": "๐", - "child": "๐ง", - "children_crossing": "๐ธ", - "chipmunk": "๐ฟ", - "chocolate_bar": "๐ซ", - "chopsticks": "๐ฅข", - "christmas_tree": "๐", - "church": "โช", - "cinema": "๐ฆ", - "circled_m": "โ", - "circus_tent": "๐ช", - "city_dusk": "๐", - "city_sunset": "๐", - "cityscape": "๐", - "cityscape_at_dusk": "๐", - "cl": "๐", - "clap": "๐", - "clapper": "๐ฌ", - "classical_building": "๐", - "clinking_glasses": "๐ฅ", - "clipboard": "๐", - "clock1": "๐", - "clock10": "๐", - "clock1030": "๐ฅ", - "clock11": "๐", - "clock1130": "๐ฆ", - "clock12": "๐", - "clock1230": "๐ง", - "clock130": "๐", - "clock2": "๐", - "clock230": "๐", - "clock3": "๐", - "clock330": "๐", - "clock4": "๐", - "clock430": "๐", - "clock5": "๐", - "clock530": "๐ ", - "clock6": "๐", - "clock630": "๐ก", - "clock7": "๐", - "clock730": "๐ข", - "clock8": "๐", - "clock830": "๐ฃ", - "clock9": "๐", - "clock930": "๐ค", - "closed_book": "๐", - "closed_lock_with_key": "๐", - "closed_umbrella": "๐", - "cloud": "โ๏ธ", - "cloud_with_lightning": "๐ฉ", - "cloud_with_lightning_and_rain": "โ๏ธ", - "cloud_with_rain": "๐ง", - "cloud_with_snow": "๐จ", - "clown": "๐คก", - "clown_face": "๐คก", - "club_suit": "โฃ๏ธ", - "clubs": "โฃ", - "coat": "๐งฅ", - "cocktail": "๐ธ", - "coconut": "๐ฅฅ", - "coffee": "โ", - "coffin": "โฐ๏ธ", - "cold_face": "๐ฅถ", - "cold_sweat": "๐ฐ", - "comet": "โ๏ธ", - "compass": "๐งญ", - "compression": "๐", - "computer": "๐ป", - "computer_mouse": "๐ฑ", - "confetti_ball": "๐", - "confounded": "๐", - "confused": "๐", - "congratulations": "ใ", - "construction": "๐ง", - "construction_worker": "๐ท", - "control_knobs": "๐", - "convenience_store": "๐ช", - "cookie": "๐ช", - "cooking": "๐ณ", - "cool": "๐", - "cop": "๐ฎ", - "copyright": "ยฉ", - "corn": "๐ฝ", - "couch_and_lamp": "๐", - "couple": "๐ซ", - "couple_with_heart": "๐", - "couplekiss": "๐", - "cow": "๐ฎ", - "cow2": "๐", - "cowboy": "๐ค ", - "cowboy_hat_face": "๐ค ", - "crab": "๐ฆ", - "crayon": "๐", - "crazy_face": "๐คช", - "credit_card": "๐ณ", - "crescent_moon": "๐", - "cricket": "๐ฆ", - "cricket_game": "๐", - "crocodile": "๐", - "croissant": "๐ฅ", - "cross": "โ๏ธ", - "crossed_fingers": "๐ค", - "crossed_flags": "๐", - "crossed_swords": "โ๏ธ", - "crown": "๐", - "cry": "๐ข", - "crying_cat_face": "๐ฟ", - "crystal_ball": "๐ฎ", - "cucumber": "๐ฅ", - "cup_with_straw": "๐ฅค", - "cupcake": "๐ง", - "cupid": "๐", - "curling_stone": "๐ฅ", - "curly_hair": "๐ฆฑ", - "curly_loop": "โฐ", - "currency_exchange": "๐ฑ", - "curry": "๐", - "custard": "๐ฎ", - "customs": "๐", - "cut_of_meat": "๐ฅฉ", - "cyclone": "๐", - "dagger": "๐ก", - "dancer": "๐", - "dancers": "๐ฏ", - "dango": "๐ก", - "dark_skin_tone": "๐ฟ", - "dark_sunglasses": "๐ถ", - "dart": "๐ฏ", - "dash": "๐จ", - "date": "๐
", - "deaf_person": "๐ง", - "deciduous_tree": "๐ณ", - "deer": "๐ฆ", - "department_store": "๐ฌ", - "derelict_house": "๐", - "desert": "๐", - "desert_island": "๐", - "desktop_computer": "๐ฅ", - "detective": "๐ต", - "diamond_shape_with_a_dot_inside": "๐ ", - "diamond_suit": "โฆ๏ธ", - "diamonds": "โฆ", - "disappointed": "๐", - "disappointed_relieved": "๐ฅ", - "diving_mask": "๐คฟ", - "diya_lamp": "๐ช", - "dizzy": "๐ซ", - "dizzy_face": "๐ต", - "dna": "๐งฌ", - "do_not_litter": "๐ฏ", - "dog": "๐ถ", - "dog2": "๐", - "dollar": "๐ต", - "dolls": "๐", - "dolphin": "๐ฌ", - "door": "๐ช", - "double_exclamation_mark": "โผ", - "doughnut": "๐ฉ", - "dove": "๐", - "down_arrow": "โฌ", - "downleft_arrow": "โ", - "downright_arrow": "โ", - "dragon": "๐", - "dragon_face": "๐ฒ", - "dress": "๐", - "dromedary_camel": "๐ช", - "drooling_face": "๐คค", - "drop_of_blood": "๐ฉธ", - "droplet": "๐ง", - "drum": "๐ฅ", - "duck": "๐ฆ", - "dumpling": "๐ฅ", - "dvd": "๐", - "e-mail": "๐ง", - "eagle": "๐ฆ
", - "ear": "๐", - "ear_of_rice": "๐พ", - "ear_with_hearing_aid": "๐ฆป", - "earth_africa": "๐", - "earth_americas": "๐", - "earth_asia": "๐", - "egg": "๐ฅ", - "eggplant": "๐", - "eight": "8โฃ", - "eight_pointed_black_star": "โด๏ธ", - "eight_spoked_asterisk": "โณ๏ธ", - "eightpointed_star": "โด", - "eightspoked_asterisk": "โณ", - "eject_button": "โ", - "electric_plug": "๐", - "elephant": "๐", - "elf": "๐ง", - "end": "๐", - "envelope": "โ", - "envelope_with_arrow": "๐ฉ", - "euro": "๐ถ", - "european_castle": "๐ฐ", - "european_post_office": "๐ค", - "evergreen_tree": "๐ฒ", - "exclamation": "โ", - "exclamation_question_mark": "โ", - "exploding_head": "๐คฏ", - "expressionless": "๐", - "eye": "๐", - "eyeglasses": "๐", - "eyes": "๐", - "face_vomiting": "๐คฎ", - "face_with_hand_over_mouth": "๐คญ", - "face_with_headbandage": "๐ค", - "face_with_monocle": "๐ง", - "face_with_raised_eyebrow": "๐คจ", - "face_with_symbols_on_mouth": "๐คฌ", - "face_with_symbols_over_mouth": "๐คฌ", - "face_with_thermometer": "๐ค", - "factory": "๐ญ", - "fairy": "๐ง", - "falafel": "๐ง", - "fallen_leaf": "๐", - "family": "๐ช", - "fast_forward": "โฉ", - "fax": "๐ ", - "fearful": "๐จ", - "feet": "๐พ", - "female_sign": "โ", - "ferris_wheel": "๐ก", - "ferry": "โด๏ธ", - "field_hockey": "๐", - "file_cabinet": "๐", - "file_folder": "๐", - "film_frames": "๐", - "film_projector": "๐ฝ", - "fingers_crossed": "๐ค", - "fire": "๐ฅ", - "fire_engine": "๐", - "fire_extinguisher": "๐งฏ", - "firecracker": "๐งจ", - "fireworks": "๐", - "first_place": "๐ฅ", - "first_quarter_moon": "๐", - "first_quarter_moon_with_face": "๐", - "fish": "๐", - "fish_cake": "๐ฅ", - "fishing_pole_and_fish": "๐ฃ", - "fist": "โ", - "five": "5โฃ", - "flag_black": "๐ด", - "flag_white": "๐ณ", - "flags": "๐", - "flamingo": "๐ฆฉ", - "flashlight": "๐ฆ", - "flat_shoe": "๐ฅฟ", - "fleur-de-lis": "โ", - "fleurde-lis": "โ๏ธ", - "floppy_disk": "๐พ", - "flower_playing_cards": "๐ด", - "flushed": "๐ณ", - "flying_disc": "๐ฅ", - "flying_saucer": "๐ธ", - "fog": "๐ซ", - "foggy": "๐", - "foot": "๐ฆถ", - "football": "๐", - "footprints": "๐ฃ", - "fork_and_knife": "๐ด", - "fork_and_knife_with_plate": "๐ฝ", - "fortune_cookie": "๐ฅ ", - "fountain": "โฒ", - "fountain_pen": "๐", - "four": "4โฃ", - "four_leaf_clover": "๐", - "fox": "๐ฆ", - "framed_picture": "๐ผ", - "free": "๐", - "french_bread": "๐ฅ", - "fried_shrimp": "๐ค", - "fries": "๐", - "frog": "๐ธ", - "frowning": "๐ฆ", - "frowning_face": "โน๏ธ", - "fuelpump": "โฝ", - "full_moon": "๐", - "full_moon_with_face": "๐", - "funeral_urn": "โฑ๏ธ", - "game_die": "๐ฒ", - "garlic": "๐ง", - "gear": "โ๏ธ", - "gem": "๐", - "gemini": "โ", - "genie": "๐ง", - "ghost": "๐ป", - "gift": "๐", - "gift_heart": "๐", - "giraffe": "๐ฆ", - "girl": "๐ง", - "glass_of_milk": "๐ฅ", - "globe_with_meridians": "๐", - "gloves": "๐งค", - "goal": "๐ฅ
", - "goal_net": "๐ฅ
", - "goat": "๐", - "goggles": "๐ฅฝ", - "golf": "โณ", - "golfer": "๐", - "gorilla": "๐ฆ", - "grapes": "๐", - "green_apple": "๐", - "green_book": "๐", - "green_circle": "๐ข", - "green_heart": "๐", - "green_salad": "๐ฅ", - "green_square": "๐ฉ", - "grey_exclamation": "โ", - "grey_question": "โ", - "grimacing": "๐ฌ", - "grin": "๐", - "grinning": "๐", - "guard": "๐", - "guardsman": "๐", - "guide_dog": "๐ฆฎ", - "guitar": "๐ธ", - "gun": "๐ซ", - "haircut": "๐", - "hamburger": "๐", - "hammer": "๐จ", - "hammer_and_pick": "โ๏ธ", - "hammer_and_wrench": "๐ ", - "hamster": "๐น", - "hand_with_fingers_splayed": "๐", - "handbag": "๐", - "handshake": "๐ค", - "hash": "#โฃ", - "hatched_chick": "๐ฅ", - "hatching_chick": "๐ฃ", - "head_bandage": "๐ค", - "headphones": "๐ง", - "hear_no_evil": "๐", - "heart": "โค๏ธ", - "heart_decoration": "๐", - "heart_exclamation": "โฃ", - "heart_eyes": "๐", - "heart_eyes_cat": "๐ป", - "heart_suit": "โฅ๏ธ", - "heartbeat": "๐", - "heartpulse": "๐", - "hearts": "โฅ", - "heavy_check_mark": "โ๏ธ", - "heavy_division_sign": "โ", - "heavy_dollar_sign": "๐ฒ", - "heavy_minus_sign": "โ", - "heavy_multiplication_x": "โ๏ธ", - "heavy_plus_sign": "โ", - "hedgehog": "๐ฆ", - "helicopter": "๐", - "herb": "๐ฟ", - "hibiscus": "๐บ", - "high_brightness": "๐", - "high_heel": "๐ ", - "hiking_boot": "๐ฅพ", - "hindu_temple": "๐", - "hippopotamus": "๐ฆ", - "hockey": "๐", - "hole": "๐ณ", - "honey_pot": "๐ฏ", - "horse": "๐ด", - "horse_racing": "๐", - "hospital": "๐ฅ", - "hot_face": "๐ฅต", - "hot_pepper": "๐ถ", - "hot_springs": "โจ", - "hotdog": "๐ญ", - "hotel": "๐จ", - "hotsprings": "โจ๏ธ", - "hourglass": "โ", - "hourglass_flowing_sand": "โณ", - "house": "๐ ", - "house_with_garden": "๐ก", - "houses": "๐", - "hugging": "๐ค", - "hundred_points": "๐ฏ", - "hushed": "๐ฏ", - "ice": "๐ง", - "ice_cream": "๐จ", - "ice_hockey": "๐", - "ice_skate": "โธ๏ธ", - "icecream": "๐ฆ", - "id": "๐", - "ideograph_advantage": "๐", - "imp": "๐ฟ", - "inbox_tray": "๐ฅ", - "incoming_envelope": "๐จ", - "index_pointing_up": "โ", - "infinity": "โพ", - "information": "โน๏ธ", - "information_desk_person": "๐", - "information_source": "โน", - "innocent": "๐", - "input_numbers": "๐ข", - "interrobang": "โ๏ธ", - "iphone": "๐ฑ", - "izakaya_lantern": "๐ฎ", - "jack_o_lantern": "๐", - "japan": "๐พ", - "japanese_castle": "๐ฏ", - "japanese_congratulations_button": "ใ๏ธ", - "japanese_free_of_charge_button": "๐", - "japanese_goblin": "๐บ", - "japanese_ogre": "๐น", - "japanese_reserved_button": "๐ฏ", - "japanese_secret_button": "ใ๏ธ", - "japanese_service_charge_button": "๐", - "jeans": "๐", - "joy": "๐", - "joy_cat": "๐น", - "joystick": "๐น", - "kaaba": "๐", - "kangaroo": "๐ฆ", - "key": "๐", - "keyboard": "โจ๏ธ", - "keycap_ten": "๐", - "kick_scooter": "๐ด", - "kimono": "๐", - "kiss": "๐", - "kissing": "๐", - "kissing_cat": "๐ฝ", - "kissing_closed_eyes": "๐", - "kissing_heart": "๐", - "kissing_smiling_eyes": "๐", - "kitchen_knife": "๐ช", - "kite": "๐ช", - "kiwi": "๐ฅ", - "kiwi_fruit": "๐ฅ", - "knife": "๐ช", - "koala": "๐จ", - "koko": "๐", - "lab_coat": "๐ฅผ", - "label": "๐ท", - "lacrosse": "๐ฅ", - "large_blue_diamond": "๐ท", - "large_orange_diamond": "๐ถ", - "last_quarter_moon": "๐", - "last_quarter_moon_with_face": "๐", - "last_track_button": "โฎ๏ธ", - "latin_cross": "โ", - "laughing": "๐", - "leafy_green": "๐ฅฌ", - "leaves": "๐", - "ledger": "๐", - "left_arrow": "โฌ
", - "left_arrow_curving_right": "โช", - "left_facing_fist": "๐ค", - "left_luggage": "๐
", - "left_right_arrow": "โ", - "leftfacing_fist": "๐ค", - "leftright_arrow": "โ๏ธ", - "leftwards_arrow_with_hook": "โฉ๏ธ", - "leg": "๐ฆต", - "lemon": "๐", - "leo": "โ", - "leopard": "๐", - "level_slider": "๐", - "libra": "โ", - "light_rail": "๐", - "light_skin_tone": "๐ป", - "link": "๐", - "linked_paperclips": "๐", - "lion_face": "๐ฆ", - "lips": "๐", - "lipstick": "๐", - "lizard": "๐ฆ", - "llama": "๐ฆ", - "lobster": "๐ฆ", - "lock": "๐", - "lock_with_ink_pen": "๐", - "lollipop": "๐ญ", - "loop": "โฟ", - "lotion_bottle": "๐งด", - "loud_sound": "๐", - "loudspeaker": "๐ข", - "love_hotel": "๐ฉ", - "love_letter": "๐", - "love_you_gesture": "๐ค", - "loveyou_gesture": "๐ค", - "low_brightness": "๐
", - "luggage": "๐งณ", - "lying_face": "๐คฅ", - "m": "โ๏ธ", - "mag": "๐", - "mag_right": "๐", - "mage": "๐ง", - "magnet": "๐งฒ", - "mahjong": "๐", - "mailbox": "๐ซ", - "mailbox_closed": "๐ช", - "mailbox_with_mail": "๐ฌ", - "mailbox_with_no_mail": "๐ญ", - "male_sign": "โ", - "man": "๐จ", - "man_dancing": "๐บ", - "man_in_suit": "๐ด", - "man_in_tuxedo": "๐คต", - "man_with_chinese_cap": "๐ฒ", - "man_with_gua_pi_mao": "๐ฒ", - "man_with_turban": "๐ณ", - "mango": "๐ฅญ", - "mans_shoe": "๐", - "mantelpiece_clock": "๐ฐ", - "manual_wheelchair": "๐ฆฝ", - "maple_leaf": "๐", - "martial_arts_uniform": "๐ฅ", - "mask": "๐ท", - "massage": "๐", - "mate": "๐ง", - "meat_on_bone": "๐", - "mechanical_arm": "๐ฆพ", - "mechanical_leg": "๐ฆฟ", - "medal": "๐
", - "medical_symbol": "โ", - "medium_skin_tone": "๐ฝ", - "mediumdark_skin_tone": "๐พ", - "mediumlight_skin_tone": "๐ผ", - "mega": "๐ฃ", - "melon": "๐", - "memo": "๐", - "menorah": "๐", - "mens": "๐น", - "merperson": "๐ง", - "metal": "๐ค", - "metro": "๐", - "microbe": "๐ฆ ", - "microphone": "๐ค", - "microscope": "๐ฌ", - "middle_finger": "๐", - "military_medal": "๐", - "milk": "๐ฅ", - "milky_way": "๐", - "minibus": "๐", - "minidisc": "๐ฝ", - "mobile_phone_off": "๐ด", - "money_mouth": "๐ค", - "money_with_wings": "๐ธ", - "moneybag": "๐ฐ", - "moneymouth_face": "๐ค", - "monkey": "๐", - "monkey_face": "๐ต", - "monorail": "๐", - "moon_cake": "๐ฅฎ", - "mortar_board": "๐", - "mosque": "๐", - "mosquito": "๐ฆ", - "motor_boat": "๐ฅ", - "motor_scooter": "๐ต", - "motorcycle": "๐", - "motorized_wheelchair": "๐ฆผ", - "motorway": "๐ฃ", - "mount_fuji": "๐ป", - "mountain": "โฐ๏ธ", - "mountain_bicyclist": "๐ต", - "mountain_cableway": "๐ ", - "mountain_railway": "๐", - "mouse": "๐ญ", - "mouse2": "๐", - "movie_camera": "๐ฅ", - "moyai": "๐ฟ", - "mrs_claus": "๐คถ", - "multiplication_sign": "โ", - "muscle": "๐ช", - "mushroom": "๐", - "musical_keyboard": "๐น", - "musical_note": "๐ต", - "musical_score": "๐ผ", - "mute": "๐", - "nail_care": "๐
", - "name_badge": "๐", - "national_park": "๐", - "nauseated_face": "๐คข", - "nazar_amulet": "๐งฟ", - "necktie": "๐", - "negative_squared_cross_mark": "โ", - "nerd": "๐ค", - "neutral_face": "๐", - "new": "๐", - "new_moon": "๐", - "new_moon_with_face": "๐", - "newspaper": "๐ฐ", - "next_track_button": "โญ๏ธ", - "ng": "๐", - "night_with_stars": "๐", - "nine": "9โฃ", - "no_bell": "๐", - "no_bicycles": "๐ณ", - "no_entry": "โ", - "no_entry_sign": "๐ซ", - "no_good": "๐
", - "no_mobile_phones": "๐ต", - "no_mouth": "๐ถ", - "no_pedestrians": "๐ท", - "no_smoking": "๐ญ", - "non-potable_water": "๐ฑ", - "nose": "๐", - "notebook": "๐", - "notebook_with_decorative_cover": "๐", - "notes": "๐ถ", - "nut_and_bolt": "๐ฉ", - "o": "โญ", - "o_button_blood_type": "๐
พ", - "ocean": "๐", - "octagonal_sign": "๐", - "octopus": "๐", - "oden": "๐ข", - "office": "๐ข", - "oil_drum": "๐ข", - "ok": "๐", - "ok_hand": "๐", - "ok_woman": "๐", - "old_key": "๐", - "older_adult": "๐ง", - "older_man": "๐ด", - "older_person": "๐ง", - "older_woman": "๐ต", - "om_symbol": "๐", - "on": "๐", - "oncoming_automobile": "๐", - "oncoming_bus": "๐", - "oncoming_fist": "๐", - "oncoming_police_car": "๐", - "oncoming_taxi": "๐", - "one": "1โฃ", - "onepiece_swimsuit": "๐ฉฑ", - "onion": "๐ง
", - "open_file_folder": "๐", - "open_hands": "๐", - "open_mouth": "๐ฎ", - "ophiuchus": "โ", - "orange_book": "๐", - "orange_circle": "๐ ", - "orange_heart": "๐งก", - "orange_square": "๐ง", - "orangutan": "๐ฆง", - "orthodox_cross": "โฆ๏ธ", - "otter": "๐ฆฆ", - "outbox_tray": "๐ค", - "owl": "๐ฆ", - "ox": "๐", - "oyster": "๐ฆช", - "p_button": "๐
ฟ", - "package": "๐ฆ", - "page_facing_up": "๐", - "page_with_curl": "๐", - "pager": "๐", - "paintbrush": "๐", - "palm_tree": "๐ด", - "palms_up_together": "๐คฒ", - "pancakes": "๐ฅ", - "panda_face": "๐ผ", - "paperclip": "๐", - "parachute": "๐ช", - "parrot": "๐ฆ", - "part_alternation_mark": "ใฝ", - "partly_sunny": "โ
", - "partying_face": "๐ฅณ", - "passenger_ship": "๐ณ", - "passport_control": "๐", - "pause_button": "โธ๏ธ", - "peace": "โฎ", - "peace_symbol": "โฎ๏ธ", - "peach": "๐", - "peacock": "๐ฆ", - "peanuts": "๐ฅ", - "pear": "๐", - "pen": "๐", - "pencil": "๐", - "pencil2": "โ", - "penguin": "๐ง", - "pensive": "๐", - "people_with_bunny_ears_partying": "๐ฏ", - "people_wrestling": "๐คผ", - "performing_arts": "๐ญ", - "persevere": "๐ฃ", - "person": "๐ง", - "person_biking": "๐ด", - "person_bouncing_ball": "โน๏ธ", - "person_bowing": "๐", - "person_cartwheeling": "๐คธ", - "person_climbing": "๐ง", - "person_doing_cartwheel": "๐คธ", - "person_facepalming": "๐คฆ", - "person_fencing": "๐คบ", - "person_frowning": "๐", - "person_gesturing_no": "๐
", - "person_gesturing_ok": "๐", - "person_getting_haircut": "๐", - "person_getting_massage": "๐", - "person_in_lotus_position": "๐ง", - "person_in_steamy_room": "๐ง", - "person_juggling": "๐คน", - "person_kneeling": "๐ง", - "person_mountain_biking": "๐ต", - "person_playing_handball": "๐คพ", - "person_playing_water_polo": "๐คฝ", - "person_pouting": "๐", - "person_raising_hand": "๐", - "person_rowing_boat": "๐ฃ", - "person_running": "๐", - "person_shrugging": "๐คท", - "person_standing": "๐ง", - "person_surfing": "๐", - "person_swimming": "๐", - "person_tipping_hand": "๐", - "person_walking": "๐ถ", - "person_wearing_turban": "๐ณ", - "person_with_blond_hair": "๐ฑ", - "person_with_pouting_face": "๐", - "petri_dish": "๐งซ", - "pick": "โ๏ธ", - "pie": "๐ฅง", - "pig": "๐ท", - "pig2": "๐", - "pig_nose": "๐ฝ", - "pill": "๐", - "pinching_hand": "๐ค", - "pineapple": "๐", - "ping_pong": "๐", - "pisces": "โ", - "pizza": "๐", - "place_of_worship": "๐", - "play_button": "โถ", - "play_or_pause_button": "โฏ๏ธ", - "play_pause": "โฏ", - "pleading_face": "๐ฅบ", - "point_down": "๐", - "point_left": "๐", - "point_right": "๐", - "point_up": "โ๏ธ", - "point_up_2": "๐", - "police_car": "๐", - "police_officer": "๐ฎ", - "poodle": "๐ฉ", - "poop": "๐ฉ", - "popcorn": "๐ฟ", - "post_office": "๐ฃ", - "postal_horn": "๐ฏ", - "postbox": "๐ฎ", - "potable_water": "๐ฐ", - "potato": "๐ฅ", - "pouch": "๐", - "poultry_leg": "๐", - "pound": "๐ท", - "pouting_cat": "๐พ", - "pray": "๐", - "prayer_beads": "๐ฟ", - "pregnant_woman": "๐คฐ", - "pretzel": "๐ฅจ", - "prince": "๐คด", - "princess": "๐ธ", - "printer": "๐จ", - "probing_cane": "๐ฆฏ", - "punch": "๐", - "purple_circle": "๐ฃ", - "purple_heart": "๐", - "purse": "๐", - "pushpin": "๐", - "put_litter_in_its_place": "๐ฎ", - "puzzle_piece": "๐งฉ", - "question": "โ", - "rabbit": "๐ฐ", - "rabbit2": "๐", - "raccoon": "๐ฆ", - "racehorse": "๐", - "racing_car": "๐", - "radio": "๐ป", - "radio_button": "๐", - "radioactive": "โข๏ธ", - "rage": "๐ก", - "railway_car": "๐", - "railway_track": "๐ค", - "rainbow": "๐", - "raised_back_of_hand": "๐ค", - "raised_hand": "โ", - "raised_hands": "๐", - "raising_hand": "๐", - "ram": "๐", - "ramen": "๐", - "rat": "๐", - "razor": "๐ช", - "receipt": "๐งพ", - "record_button": "โบ๏ธ", - "recycle": "โป", - "recycling_symbol": "โป๏ธ", - "red_car": "๐", - "red_circle": "๐ด", - "red_envelope": "๐งง", - "red_hair": "๐ฆฐ", - "red_heart": "โค", - "red_square": "๐ฅ", - "regional_indicator_a": "๐ฆ", - "regional_indicator_b": "๐ง", - "regional_indicator_c": "๐จ", - "regional_indicator_d": "๐ฉ", - "regional_indicator_e": "๐ช", - "regional_indicator_f": "๐ซ", - "regional_indicator_g": "๐ฌ", - "regional_indicator_h": "๐ญ", - "regional_indicator_i": "๐ฎ", - "regional_indicator_j": "๐ฏ", - "regional_indicator_k": "๐ฐ", - "regional_indicator_l": "๐ฑ", - "regional_indicator_m": "๐ฒ", - "regional_indicator_n": "๐ณ", - "regional_indicator_o": "๐ด", - "regional_indicator_p": "๐ต", - "regional_indicator_q": "๐ถ", - "regional_indicator_r": "๐ท", - "regional_indicator_s": "๐ธ", - "regional_indicator_t": "๐น", - "regional_indicator_u": "๐บ", - "regional_indicator_v": "๐ป", - "regional_indicator_w": "๐ผ", - "regional_indicator_x": "๐ฝ", - "regional_indicator_y": "๐พ", - "regional_indicator_z": "๐ฟ", - "registered": "ยฎ", - "relieved": "๐", - "reminder_ribbon": "๐", - "repeat": "๐", - "repeat_one": "๐", - "rescue_workerโs_helmet": "โ๏ธ", - "restroom": "๐ป", - "reverse_button": "โ", - "revolving_hearts": "๐", - "rewind": "โช", - "rhino": "๐ฆ", - "rhinoceros": "๐ฆ", - "ribbon": "๐", - "rice": "๐", - "rice_ball": "๐", - "rice_cracker": "๐", - "rice_scene": "๐", - "right_arrow": "โก๏ธ", - "right_arrow_curving_down": "โคต", - "right_arrow_curving_left": "โฉ", - "right_arrow_curving_up": "โคด", - "right_facing_fist": "๐ค", - "rightfacing_fist": "๐ค", - "ring": "๐", - "ringed_planet": "๐ช", - "robot": "๐ค", - "rocket": "๐", - "rofl": "๐คฃ", - "roll_of_paper": "๐งป", - "rolledup_newspaper": "๐", - "roller_coaster": "๐ข", - "rolling_eyes": "๐", - "rolling_on_the_floor_laughing": "๐คฃ", - "rooster": "๐", - "rose": "๐น", - "rosette": "๐ต", - "rotating_light": "๐จ", - "round_pushpin": "๐", - "rowboat": "๐ฃ", - "rugby_football": "๐", - "runner": "๐", - "running_shirt_with_sash": "๐ฝ", - "safety_pin": "๐งท", - "safety_vest": "๐ฆบ", - "sagittarius": "โ", - "sailboat": "โต", - "sake": "๐ถ", - "salad": "๐ฅ", - "salt": "๐ง", - "sandal": "๐ก", - "sandwich": "๐ฅช", - "santa": "๐
", - "sari": "๐ฅป", - "satellite": "๐ก", - "sauropod": "๐ฆ", - "saxophone": "๐ท", - "scales": "โ", - "scarf": "๐งฃ", - "school": "๐ซ", - "school_satchel": "๐", - "scissors": "โ", - "scooter": "๐ด", - "scorpion": "๐ฆ", - "scorpius": "โ", - "scream": "๐ฑ", - "scream_cat": "๐", - "scroll": "๐", - "seat": "๐บ", - "second_place": "๐ฅ", - "secret": "ใ", - "see_no_evil": "๐", - "seedling": "๐ฑ", - "selfie": "๐คณ", - "seven": "7โฃ", - "shallow_pan_of_food": "๐ฅ", - "shamrock": "โ๏ธ", - "shark": "๐ฆ", - "shaved_ice": "๐ง", - "sheep": "๐", - "shell": "๐", - "shield": "๐ก", - "shinto_shrine": "โฉ๏ธ", - "ship": "๐ข", - "shirt": "๐", - "shopping_bags": "๐", - "shopping_cart": "๐", - "shorts": "๐ฉณ", - "shower": "๐ฟ", - "shrimp": "๐ฆ", - "shushing_face": "๐คซ", - "sign_of_the_horns": "๐ค", - "signal_strength": "๐ถ", - "six": "6โฃ", - "six_pointed_star": "๐ฏ", - "skateboard": "๐น", - "ski": "๐ฟ", - "skier": "โท๏ธ", - "skull": "๐", - "skull_and_crossbones": "โ ๏ธ", - "skull_crossbones": "โ ", - "skunk": "๐ฆจ", - "sled": "๐ท", - "sleeping": "๐ด", - "sleeping_accommodation": "๐", - "sleepy": "๐ช", - "slight_frown": "๐", - "slight_smile": "๐", - "slightly_frowning_face": "๐", - "slot_machine": "๐ฐ", - "sloth": "๐ฆฅ", - "small_airplane": "๐ฉ", - "small_blue_diamond": "๐น", - "small_orange_diamond": "๐ธ", - "small_red_triangle": "๐บ", - "small_red_triangle_down": "๐ป", - "smile": "๐", - "smile_cat": "๐ธ", - "smiley": "๐", - "smiley_cat": "๐บ", - "smiling": "โบ๏ธ", - "smiling_face": "โบ", - "smiling_face_with_hearts": "๐ฅฐ", - "smiling_imp": "๐", - "smirk": "๐", - "smirk_cat": "๐ผ", - "smoking": "๐ฌ", - "snail": "๐", - "snake": "๐", - "sneezing_face": "๐คง", - "snowboarder": "๐", - "snowcapped_mountain": "๐", - "snowflake": "โ", - "snowman": "โ", - "soap": "๐งผ", - "sob": "๐ญ", - "soccer": "โฝ", - "socks": "๐งฆ", - "softball": "๐ฅ", - "soon": "๐", - "sos": "๐", - "sound": "๐", - "space_invader": "๐พ", - "spade_suit": "โ ๏ธ", - "spades": "โ ", - "spaghetti": "๐", - "sparkle": "โ", - "sparkler": "๐", - "sparkles": "โจ", - "sparkling_heart": "๐", - "speak_no_evil": "๐", - "speaker": "๐", - "speaking_head": "๐ฃ", - "speech_balloon": "๐ฌ", - "speech_left": "๐จ", - "speedboat": "๐ค", - "spider": "๐ท", - "spider_web": "๐ธ", - "spiral_calendar": "๐", - "spiral_notepad": "๐", - "sponge": "๐งฝ", - "spoon": "๐ฅ", - "squid": "๐ฆ", - "stadium": "๐", - "star": "โญ", - "star2": "๐", - "star_and_crescent": "โช๏ธ", - "star_of_david": "โก", - "star_struck": "๐คฉ", - "stars": "๐ ", - "starstruck": "๐คฉ", - "station": "๐", - "statue_of_liberty": "๐ฝ", - "steam_locomotive": "๐", - "stethoscope": "๐ฉบ", - "stew": "๐ฒ", - "stop_button": "โน๏ธ", - "stopwatch": "โฑ๏ธ", - "straight_ruler": "๐", - "strawberry": "๐", - "stuck_out_tongue": "๐", - "stuck_out_tongue_closed_eyes": "๐", - "stuck_out_tongue_winking_eye": "๐", - "studio_microphone": "๐", - "stuffed_flatbread": "๐ฅ", - "sun": "โ", - "sun_behind_large_cloud": "๐ฅ", - "sun_behind_rain_cloud": "๐ฆ", - "sun_behind_small_cloud": "๐ค", - "sun_with_face": "๐", - "sunflower": "๐ป", - "sunglasses": "๐", - "sunny": "โ๏ธ", - "sunrise": "๐
", - "sunrise_over_mountains": "๐", - "superhero": "๐ฆธ", - "supervillain": "๐ฆน", - "surfer": "๐", - "sushi": "๐ฃ", - "suspension_railway": "๐", - "swan": "๐ฆข", - "sweat": "๐", - "sweat_drops": "๐ฆ", - "sweat_smile": "๐
", - "sweet_potato": "๐ ", - "swimmer": "๐", - "symbols": "๐ฃ", - "synagogue": "๐", - "syringe": "๐", - "t_rex": "๐ฆ", - "taco": "๐ฎ", - "tada": "๐", - "takeout_box": "๐ฅก", - "tanabata_tree": "๐", - "tangerine": "๐", - "taurus": "โ", - "taxi": "๐", - "tea": "๐ต", - "teddy_bear": "๐งธ", - "telephone": "โ", - "telephone_receiver": "๐", - "telescope": "๐ญ", - "tennis": "๐พ", - "tent": "โบ", - "test_tube": "๐งช", - "thermometer": "๐ก", - "thermometer_face": "๐ค", - "thinking": "๐ค", - "third_place": "๐ฅ", - "thought_balloon": "๐ญ", - "thread": "๐งต", - "three": "3โฃ", - "thumbsdown": "๐", - "thumbsup": "๐", - "ticket": "๐ซ", - "tiger": "๐ฏ", - "tiger2": "๐
", - "timer_clock": "โฒ๏ธ", - "tired_face": "๐ซ", - "tm": "โข", - "toilet": "๐ฝ", - "tokyo_tower": "๐ผ", - "tomato": "๐
", - "tone1": "๐ป", - "tone2": "๐ผ", - "tone3": "๐ฝ", - "tone4": "๐พ", - "tone5": "๐ฟ", - "tongue": "๐
", - "toolbox": "๐งฐ", - "tooth": "๐ฆท", - "top": "๐", - "tophat": "๐ฉ", - "tornado": "๐ช", - "track_next": "โญ", - "track_previous": "โฎ", - "trackball": "๐ฒ", - "tractor": "๐", - "trade_mark": "โข๏ธ", - "traffic_light": "๐ฅ", - "train": "๐", - "train2": "๐", - "tram": "๐", - "trex": "๐ฆ", - "triangular_flag_on_post": "๐ฉ", - "triangular_ruler": "๐", - "trident": "๐ฑ", - "triumph": "๐ค", - "trolleybus": "๐", - "trophy": "๐", - "tropical_drink": "๐น", - "tropical_fish": "๐ ", - "truck": "๐", - "trumpet": "๐บ", - "tulip": "๐ท", - "tumbler_glass": "๐ฅ", - "turkey": "๐ฆ", - "turtle": "๐ข", - "tv": "๐บ", - "twisted_rightwards_arrows": "๐", - "two": "2โฃ", - "two_hearts": "๐", - "two_men_holding_hands": "๐ฌ", - "two_women_holding_hands": "๐ญ", - "u5272": "๐น", - "u5408": "๐ด", - "u55b6": "๐บ", - "u6307": "๐ฏ", - "u6708": "๐ท", - "u6709": "๐ถ", - "u6e80": "๐ต", - "u7121": "๐", - "u7533": "๐ธ", - "u7981": "๐ฒ", - "u7a7a": "๐ณ", - "umbrella": "โ", - "umbrella_on_ground": "โฑ๏ธ", - "unamused": "๐", - "underage": "๐", - "unicorn": "๐ฆ", - "unlock": "๐", - "up": "๐", - "up_arrow": "โฌ", - "updown_arrow": "โ๏ธ", - "upleft_arrow": "โ๏ธ", - "upright_arrow": "โ", - "upside_down": "๐", - "v": "โ๏ธ", - "vampire": "๐ง", - "vertical_traffic_light": "๐ฆ", - "vhs": "๐ผ", - "vibration_mode": "๐ณ", - "victory_hand": "โ", - "video_camera": "๐น", - "video_game": "๐ฎ", - "violin": "๐ป", - "virgo": "โ", - "volcano": "๐", - "volleyball": "๐", - "vs": "๐", - "vulcan": "๐", - "vulcan_salute": "๐", - "waffle": "๐ง", - "walking": "๐ถ", - "waning_crescent_moon": "๐", - "waning_gibbous_moon": "๐", - "warning": "โ ", - "wastebasket": "๐", - "watch": "โ", - "water_buffalo": "๐", - "watermelon": "๐", - "wave": "๐", - "wavy_dash": "ใฐ๏ธ", - "waxing_crescent_moon": "๐", - "waxing_gibbous_moon": "๐", - "wc": "๐พ", - "weary": "๐ฉ", - "wedding": "๐", - "weightlifter": "๐", - "whale": "๐ณ", - "whale2": "๐", - "wheel_of_dharma": "โธ๏ธ", - "wheelchair": "โฟ", - "white_check_mark": "โ
", - "white_circle": "โช", - "white_flower": "๐ฎ", - "white_hair": "๐ฆณ", - "white_heart": "๐ค", - "white_large_square": "โฌ", - "white_medium_small_square": "โฝ", - "white_medium_square": "โป๏ธ", - "white_small_square": "โซ๏ธ", - "white_square_button": "๐ณ", - "wilted_flower": "๐ฅ", - "wilted_rose": "๐ฅ", - "wind_blowing_face": "๐ฌ", - "wind_chime": "๐", - "wine_glass": "๐ท", - "wink": "๐", - "wolf": "๐บ", - "woman": "๐ฉ", - "woman_with_headscarf": "๐ง", - "womans_clothes": "๐", - "womans_hat": "๐", - "womens": "๐บ", - "woozy_face": "๐ฅด", - "world_map": "๐บ", - "worried": "๐", - "wrench": "๐ง", - "writing_hand": "โ๏ธ", - "x": "โ", - "yarn": "๐งถ", - "yawning_face": "๐ฅฑ", - "yellow_circle": "๐ก", - "yellow_heart": "๐", - "yellow_square": "๐จ", - "yen": "๐ด", - "yin_yang": "โฏ๏ธ", - "yoyo": "๐ช", - "yum": "๐", - "zany_face": "๐คช", - "zap": "โก", - "zebra": "๐ฆ", - "zero": "0โฃ", - "zipper_mouth": "๐ค", - "zombie": "๐ง", - "zzz": "๐ค" -}
\ No newline at end of file @@ -1433,31 +1433,31 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@fortawesome/fontawesome-common-types@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.2.tgz#c1095b1bbabf19f37f9ff0719db38d92a410bcfe" - integrity sha512-wBaAPGz1Awxg05e0PBRkDRuTsy4B3dpBm+zreTTyd9TH4uUM27cAL4xWyWR0rLJCrRwzVsQ4hF3FvM6rqydKPA== +"@fortawesome/fontawesome-common-types@6.2.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.0.tgz#76467a94aa888aeb22aafa43eb6ff889df3a5a7f" + integrity sha512-rBevIsj2nclStJ7AxTdfsa3ovHb1H+qApwrxcTVo+NNdeJiB9V75hsKfrkG5AwNcRUNxrPPiScGYCNmLMoh8pg== -"@fortawesome/fontawesome-svg-core@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.2.tgz#11e2e8583a7dea75d734e4d0e53d91c63fae7511" - integrity sha512-853G/Htp0BOdXnPoeCPTjFrVwyrJHpe8MhjB/DYE9XjwhnNDfuBCd3aKc2YUYbEfHEcBws4UAA0kA9dymZKGjA== +"@fortawesome/fontawesome-svg-core@6.2.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.2.0.tgz#11856eaf4dd1d865c442ddea1eed8ee855186ba2" + integrity sha512-Cf2mAAeMWFMzpLC7Y9H1I4o3wEU+XovVJhTiNG8ZNgSQj53yl7OCJaS80K4YjrABWZzbAHVaoHE1dVJ27AAYXw== dependencies: - "@fortawesome/fontawesome-common-types" "6.1.2" + "@fortawesome/fontawesome-common-types" "6.2.0" -"@fortawesome/free-regular-svg-icons@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.1.2.tgz#9f04009098addcc11d0d185126f058ed042c3099" - integrity sha512-xR4hA+tAwsaTHGfb+25H1gVU/aJ0Rzu+xIUfnyrhaL13yNQ7TWiI2RvzniAaB+VGHDU2a+Pk96Ve+pkN3/+TTQ== +"@fortawesome/free-regular-svg-icons@6.2.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.2.0.tgz#947e1f03be17da3a60bfeb2666b5348b19448ce2" + integrity sha512-M1dG+PAmkYMTL9BSUHFXY5oaHwBYfHCPhbJ8qj8JELsc9XCrUJ6eEHWip4q0tE+h9C0DVyFkwIM9t7QYyCpprQ== dependencies: - "@fortawesome/fontawesome-common-types" "6.1.2" + "@fortawesome/fontawesome-common-types" "6.2.0" -"@fortawesome/free-solid-svg-icons@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.1.2.tgz#491d668b8a6603698d0ce1ac620f66fd22b74c84" - integrity sha512-lTgZz+cMpzjkHmCwOG3E1ilUZrnINYdqMmrkv30EC3XbRsGlbIOL8H9LaNp5SV4g0pNJDfQ4EdTWWaMvdwyLiQ== +"@fortawesome/free-solid-svg-icons@6.2.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.2.0.tgz#8dcde48109354fd7a5ece8ea48d678bb91d4b5f0" + integrity sha512-UjCILHIQ4I8cN46EiQn0CZL/h8AwCGgR//1c4R96Q5viSRwuKVo0NdQEc4bm+69ZwC0dUvjbDqAHF1RR5FA3XA== dependencies: - "@fortawesome/fontawesome-common-types" "6.1.2" + "@fortawesome/fontawesome-common-types" "6.2.0" "@fortawesome/vue-fontawesome@3.0.1": version "3.0.1" @@ -1629,6 +1629,11 @@ dependencies: pointer-tracker "^2.0.3" +"@kazvmoe-infra/unicode-emoji-json@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@kazvmoe-infra/unicode-emoji-json/-/unicode-emoji-json-0.4.0.tgz#555bab2f8d11db74820ef0a2fbe2805b17c22587" + integrity sha512-22OffREdHzD0U6A/W4RaFPV8NR73za6euibtAxNxO/fu5A6TwxRO2lAdbDWKJH9COv/vYs8zqfEiSalXH2nXJA== + "@nightwatch/chai@5.0.2": version "5.0.2" resolved "https://registry.yarnpkg.com/@nightwatch/chai/-/chai-5.0.2.tgz#86b20908fc090dffd5c9567c0392bc6a494cc2e6" @@ -1667,6 +1672,34 @@ resolved "https://registry.yarnpkg.com/@ruffle-rs/ruffle/-/ruffle-0.1.0-nightly.2022.7.12.tgz#c2d77fce7a0e98d51a6535371550e0bff019d0ea" integrity sha512-DFsiT4kdUuSHsYXzHV97e9Ui3FkcsHEg1GyHJipt/lCpCoZ2uRtP41uEz9eNc9ug8jWd7UyXxJmdkkRvs9UHgQ== +"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" + integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@>=5", "@sinonjs/fake-timers@^9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c" + integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sinonjs/samsam@^6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-6.1.1.tgz#627f7f4cbdb56e6419fa2c1a3e4751ce4f6a00b1" + integrity sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA== + dependencies: + "@sinonjs/commons" "^1.6.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918" + integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ== + "@socket.io/base64-arraybuffer@~1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#568d9beae00b0d835f4f8c53fd55714986492e61" @@ -1854,47 +1887,47 @@ html-tags "^3.1.0" svg-tags "^1.0.0" -"@vue/compiler-core@3.2.37": - version "3.2.37" - resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz#b3c42e04c0e0f2c496ff1784e543fbefe91e215a" - integrity sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg== +"@vue/compiler-core@3.2.38": + version "3.2.38" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.38.tgz#0a2a7bffd2280ac19a96baf5301838a2cf1964d7" + integrity sha512-/FsvnSu7Z+lkd/8KXMa4yYNUiqQrI22135gfsQYVGuh5tqEgOB0XqrUdb/KnCLa5+TmQLPwvyUnKMyCpu+SX3Q== dependencies: "@babel/parser" "^7.16.4" - "@vue/shared" "3.2.37" + "@vue/shared" "3.2.38" estree-walker "^2.0.2" source-map "^0.6.1" -"@vue/compiler-dom@3.2.37": - version "3.2.37" - resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz#10d2427a789e7c707c872da9d678c82a0c6582b5" - integrity sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ== +"@vue/compiler-dom@3.2.38": + version "3.2.38" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.38.tgz#53d04ed0c0c62d1ef259bf82f9b28100a880b6fd" + integrity sha512-zqX4FgUbw56kzHlgYuEEJR8mefFiiyR3u96498+zWPsLeh1WKvgIReoNE+U7gG8bCUdvsrJ0JRmev0Ky6n2O0g== dependencies: - "@vue/compiler-core" "3.2.37" - "@vue/shared" "3.2.37" + "@vue/compiler-core" "3.2.38" + "@vue/shared" "3.2.38" -"@vue/compiler-sfc@3.2.37": - version "3.2.37" - resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz#3103af3da2f40286edcd85ea495dcb35bc7f5ff4" - integrity sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg== +"@vue/compiler-sfc@3.2.38": + version "3.2.38" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.38.tgz#9e763019471a535eb1fceeaac9d4d18a83f0940f" + integrity sha512-KZjrW32KloMYtTcHAFuw3CqsyWc5X6seb8KbkANSWt3Cz9p2qA8c1GJpSkksFP9ABb6an0FLCFl46ZFXx3kKpg== dependencies: "@babel/parser" "^7.16.4" - "@vue/compiler-core" "3.2.37" - "@vue/compiler-dom" "3.2.37" - "@vue/compiler-ssr" "3.2.37" - "@vue/reactivity-transform" "3.2.37" - "@vue/shared" "3.2.37" + "@vue/compiler-core" "3.2.38" + "@vue/compiler-dom" "3.2.38" + "@vue/compiler-ssr" "3.2.38" + "@vue/reactivity-transform" "3.2.38" + "@vue/shared" "3.2.38" estree-walker "^2.0.2" magic-string "^0.25.7" postcss "^8.1.10" source-map "^0.6.1" -"@vue/compiler-ssr@3.2.37": - version "3.2.37" - resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz#4899d19f3a5fafd61524a9d1aee8eb0505313cff" - integrity sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw== +"@vue/compiler-ssr@3.2.38": + version "3.2.38" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.38.tgz#933b23bf99e667e5078eefc6ba94cb95fd765dfe" + integrity sha512-bm9jOeyv1H3UskNm4S6IfueKjUNFmi2kRweFIGnqaGkkRePjwEcfCVqyS3roe7HvF4ugsEkhf4+kIvDhip6XzQ== dependencies: - "@vue/compiler-dom" "3.2.37" - "@vue/shared" "3.2.37" + "@vue/compiler-dom" "3.2.38" + "@vue/shared" "3.2.38" "@vue/devtools-api@^6.0.0-beta.11": version "6.1.3" @@ -1906,53 +1939,53 @@ resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.2.1.tgz#6f2948ff002ec46df01420dfeff91de16c5b4092" integrity sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ== -"@vue/reactivity-transform@3.2.37": - version "3.2.37" - resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz#0caa47c4344df4ae59f5a05dde2a8758829f8eca" - integrity sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg== +"@vue/reactivity-transform@3.2.38": + version "3.2.38" + resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.38.tgz#a856c217b2ead99eefb6fddb1d61119b2cb67984" + integrity sha512-3SD3Jmi1yXrDwiNJqQ6fs1x61WsDLqVk4NyKVz78mkaIRh6d3IqtRnptgRfXn+Fzf+m6B1KxBYWq1APj6h4qeA== dependencies: "@babel/parser" "^7.16.4" - "@vue/compiler-core" "3.2.37" - "@vue/shared" "3.2.37" + "@vue/compiler-core" "3.2.38" + "@vue/shared" "3.2.38" estree-walker "^2.0.2" magic-string "^0.25.7" -"@vue/reactivity@3.2.37": - version "3.2.37" - resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.37.tgz#5bc3847ac58828e2b78526e08219e0a1089f8848" - integrity sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A== +"@vue/reactivity@3.2.38": + version "3.2.38" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.38.tgz#d576fdcea98eefb96a1f1ad456e289263e87292e" + integrity sha512-6L4myYcH9HG2M25co7/BSo0skKFHpAN8PhkNPM4xRVkyGl1K5M3Jx4rp5bsYhvYze2K4+l+pioN4e6ZwFLUVtw== dependencies: - "@vue/shared" "3.2.37" + "@vue/shared" "3.2.38" -"@vue/runtime-core@3.2.37": - version "3.2.37" - resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.37.tgz#7ba7c54bb56e5d70edfc2f05766e1ca8519966e3" - integrity sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ== +"@vue/runtime-core@3.2.38": + version "3.2.38" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.38.tgz#d19cf591c210713f80e6a94ffbfef307c27aea06" + integrity sha512-kk0qiSiXUU/IKxZw31824rxmFzrLr3TL6ZcbrxWTKivadoKupdlzbQM4SlGo4MU6Zzrqv4fzyUasTU1jDoEnzg== dependencies: - "@vue/reactivity" "3.2.37" - "@vue/shared" "3.2.37" + "@vue/reactivity" "3.2.38" + "@vue/shared" "3.2.38" -"@vue/runtime-dom@3.2.37": - version "3.2.37" - resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz#002bdc8228fa63949317756fb1e92cdd3f9f4bbd" - integrity sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw== +"@vue/runtime-dom@3.2.38": + version "3.2.38" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.38.tgz#fec711f65c2485991289fd4798780aa506469b48" + integrity sha512-4PKAb/ck2TjxdMSzMsnHViOrrwpudk4/A56uZjhzvusoEU9xqa5dygksbzYepdZeB5NqtRw5fRhWIiQlRVK45A== dependencies: - "@vue/runtime-core" "3.2.37" - "@vue/shared" "3.2.37" + "@vue/runtime-core" "3.2.38" + "@vue/shared" "3.2.38" csstype "^2.6.8" -"@vue/server-renderer@3.2.37": - version "3.2.37" - resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.37.tgz#840a29c8dcc29bddd9b5f5ffa22b95c0e72afdfc" - integrity sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA== +"@vue/server-renderer@3.2.38": + version "3.2.38" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.38.tgz#01a4c0f218e90b8ad1815074208a1974ded109aa" + integrity sha512-pg+JanpbOZ5kEfOZzO2bt02YHd+ELhYP8zPeLU1H0e7lg079NtuuSB8fjLdn58c4Ou8UQ6C1/P+528nXnLPAhA== dependencies: - "@vue/compiler-ssr" "3.2.37" - "@vue/shared" "3.2.37" + "@vue/compiler-ssr" "3.2.38" + "@vue/shared" "3.2.38" -"@vue/shared@3.2.37": - version "3.2.37" - resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.37.tgz#8e6adc3f2759af52f0e85863dfb0b711ecc5c702" - integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw== +"@vue/shared@3.2.38": + version "3.2.38" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.38.tgz#e823f0cb2e85b6bf43430c0d6811b1441c300f3c" + integrity sha512-dTyhTIRmGXBjxJE+skC8tTWCGLCVc4wQgRRLt8+O9p5ewBAjoBwtCAkLPrtToSr1xltoe3st21Pv953aOZ7alg== "@vue/test-utils@2.0.2": version "2.0.2" @@ -1966,12 +1999,12 @@ dependencies: vue-demi "^0.13.4" -"@vuelidate/validators@2.0.0-alpha.31": - version "2.0.0-alpha.31" - resolved "https://registry.yarnpkg.com/@vuelidate/validators/-/validators-2.0.0-alpha.31.tgz#04d63307bc0a12db9f7ad94243350b83aacee998" - integrity sha512-+MFA9nZ7Y9zCpq383/voPDk/hiAmu6KqiJJhLOYB/FmrUPVoyKnuKnI9Bwiq8ok9GZlVkI8BnIrKPKGj9QpwiQ== +"@vuelidate/validators@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@vuelidate/validators/-/validators-2.0.0.tgz#1ddd86c6c81b2cfbb5720961e951cc53ec0a80be" + integrity sha512-fQQcmDWfz7pyH5/JPi0Ng2GEgNK1pUHn/Z/j5rG/Q+HwhgIXvJblTPcZwKOj1ABL7V4UVuGKECvZCDHNGOwdrg== dependencies: - vue-demi "^0.13.4" + vue-demi "^0.13.11" "@webassemblyjs/ast@1.11.1": version "1.11.1" @@ -3373,16 +3406,16 @@ didyoumean@1.2.2: resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== -diff@3.5.0, diff@^3.1.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - diff@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== +diff@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" + integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== + dijkstrajs@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.2.tgz#2e48c0d3b825462afe75ab4ad5e829c8ece36257" @@ -4213,12 +4246,6 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -formatio@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb" - dependencies: - samsam "1.x" - forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -5210,6 +5237,11 @@ jszip@^3.10.0: readable-stream "~2.3.6" setimmediate "^1.0.5" +just-extend@^4.0.2: + version "4.2.1" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" + integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== + karma-coverage@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/karma-coverage/-/karma-coverage-2.2.0.tgz#64f838b66b71327802e7f6f6c39d569b7024e40c" @@ -5530,6 +5562,11 @@ lodash.find@^3.2.1: lodash.isarray "^3.0.0" lodash.keys "^3.0.0" +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -5672,11 +5709,6 @@ log4js@^6.4.1: rfdc "^1.3.0" streamroller "^3.0.6" -lolex@1.6.0, lolex@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6" - integrity sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY= - longest-streak@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" @@ -5696,6 +5728,11 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" +lozad@1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/lozad/-/lozad-1.16.0.tgz#86ce732c64c69926ccdebb81c8c90bb3735948b4" + integrity sha512-JBr9WjvEFeKoyim3svo/gsQPTkgG/mOHJmDctZ/+U9H3ymUuvEkqpn8bdQMFsvTMcyRJrdJkLv0bXqGm0sP72w== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -6064,10 +6101,6 @@ nanoid@^3.3.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== -native-promise-only@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11" - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -6118,6 +6151,17 @@ nightwatch@2.3.3: stacktrace-parser "^0.1.10" strip-ansi "6.0.1" +nise@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.1.tgz#ac4237e0d785ecfcb83e20f389185975da5c31f3" + integrity sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A== + dependencies: + "@sinonjs/commons" "^1.8.3" + "@sinonjs/fake-timers" ">=5" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + path-to-regexp "^1.7.0" + no-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" @@ -7330,10 +7374,6 @@ safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" -samsam@1.x, samsam@^1.1.3: - version "1.3.0" - resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" - sass-loader@13.0.2: version "13.0.2" resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-13.0.2.tgz#e81a909048e06520e9f2ff25113a801065adb3fe" @@ -7342,10 +7382,10 @@ sass-loader@13.0.2: klona "^2.0.4" neo-async "^2.6.2" -sass@1.54.5: - version "1.54.5" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.5.tgz#93708f5560784f6ff2eab8542ade021a4a947b3a" - integrity sha512-p7DTOzxkUPa/63FU0R3KApkRHwcVZYC0PLnLm5iyZACyp15qSi32x7zVUhRdABAATmkALqgGrjCJAcWvobmhHw== +sass@1.55.0: + version "1.55.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.55.0.tgz#0c4d3c293cfe8f8a2e8d3b666e1cf1bff8065d1c" + integrity sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -7522,19 +7562,17 @@ sinon-chai@3.7.0: resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-3.7.0.tgz#cfb7dec1c50990ed18c153f1840721cf13139783" integrity sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g== -sinon@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.4.1.tgz#021fd64b54cb77d9d2fb0d43cdedfae7629c3a36" - integrity sha512-vFTrO9Wt0ECffDYIPSP/E5bBugt0UjcBQOfQUMh66xzkyPEnhl/vM2LRZi2ajuTdkH07sA6DzrM6KvdvGIH8xw== +sinon@14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-14.0.0.tgz#203731c116d3a2d58dc4e3cbe1f443ba9382a031" + integrity sha512-ugA6BFmE+WrJdh0owRZHToLd32Uw3Lxq6E6LtNRU+xTVBefx632h03Q7apXWRsRdZAJ41LB8aUfn2+O4jsDNMw== dependencies: - diff "^3.1.0" - formatio "1.2.0" - lolex "^1.6.0" - native-promise-only "^0.8.1" - path-to-regexp "^1.7.0" - samsam "^1.1.3" - text-encoding "0.6.4" - type-detect "^4.0.0" + "@sinonjs/commons" "^1.8.3" + "@sinonjs/fake-timers" "^9.1.2" + "@sinonjs/samsam" "^6.1.1" + diff "^5.0.0" + nise "^5.1.1" + supports-color "^7.2.0" slash@^3.0.0: version "3.0.0" @@ -7893,6 +7931,13 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -7960,10 +8005,6 @@ terser@^5.10.0, terser@^5.14.1: commander "^2.20.0" source-map-support "~0.5.20" -text-encoding@0.6.4: - version "0.6.4" - resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" - text-table@0.2.0, text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -8023,7 +8064,7 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5: +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -8239,6 +8280,11 @@ void-elements@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" +vue-demi@^0.13.11: + version "0.13.11" + resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.11.tgz#7d90369bdae8974d87b1973564ad390182410d99" + integrity sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A== + vue-demi@^0.13.4: version "0.13.5" resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.5.tgz#d5eddbc9eaefb89ce5995269d1fa6b0486312092" @@ -8299,16 +8345,16 @@ vue-template-compiler@2.7.10: de-indent "^1.0.2" he "^1.2.0" -vue@3.2.37: - version "3.2.37" - resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.37.tgz#da220ccb618d78579d25b06c7c21498ca4e5452e" - integrity sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ== +vue@3.2.38: + version "3.2.38" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.38.tgz#cda3a414631745b194971219318a792dbbccdec0" + integrity sha512-hHrScEFSmDAWL0cwO4B6WO7D3sALZPbfuThDsGBebthrNlDxdJZpGR3WB87VbjpPh96mep1+KzukYEhpHDFa8Q== dependencies: - "@vue/compiler-dom" "3.2.37" - "@vue/compiler-sfc" "3.2.37" - "@vue/runtime-dom" "3.2.37" - "@vue/server-renderer" "3.2.37" - "@vue/shared" "3.2.37" + "@vue/compiler-dom" "3.2.38" + "@vue/compiler-sfc" "3.2.38" + "@vue/runtime-dom" "3.2.38" + "@vue/server-renderer" "3.2.38" + "@vue/shared" "3.2.38" vuex@4.0.2: version "4.0.2" |
