commit: 5fa533fbb7b3d507c0136044fd51db033c25db05
parent 0331e42ee8576893dab2c6319ee5c0d62895b8d8
Author: Henry Jameson <me@hjkos.com>
Date: Sun, 9 Oct 2022 18:51:42 +0300
Merge remote-tracking branch 'origin/develop' into scrolltotop
* origin/develop: (89 commits)
Update dependency @vuelidate/validators to v2.0.0
Remove lolex package
Remove diff package
Pin dependencies
Update dependency sass to v1.55.0
Make suggestor suggest according to cldr annotations
Make chunks named
Use import() for emoji.json
Add regional indicators
Support filtering by keywords from cldr
Display localized unicode emoji names
Load unicode emoji annotations
Extract language list to its own file
using the half-shit approach since proper approach is full-shit
Make unicode emoji phrases match with _
Use console.info
Fix non-square emojis being truncated
Fix emoji picker lint
Fix emoji picker lint
Tweak efficiency when changing filter keywords in emoji picker
...
Diffstat:
66 files changed, 1670 insertions(+), 1880 deletions(-)
diff --git a/.babelrc b/.babelrc
@@ -1,5 +1,5 @@
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
- "comments": false
+ "comments": true
}
diff --git a/.gitignore b/.gitignore
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -18,22 +18,23 @@
"dependencies": {
"@babel/runtime": "7.18.9",
"@chenfengyuan/vue-qrcode": "2.0.0",
- "@fortawesome/fontawesome-svg-core": "6.1.2",
- "@fortawesome/free-regular-svg-icons": "6.1.2",
- "@fortawesome/free-solid-svg-icons": "6.1.2",
+ "@fortawesome/fontawesome-svg-core": "6.2.0",
+ "@fortawesome/free-regular-svg-icons": "6.2.0",
+ "@fortawesome/free-solid-svg-icons": "6.2.0",
"@fortawesome/vue-fontawesome": "3.0.1",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
+ "@kazvmoe-infra/unicode-emoji-json": "0.4.0",
"@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12",
"@vuelidate/core": "2.0.0-alpha.44",
- "@vuelidate/validators": "2.0.0-alpha.31",
+ "@vuelidate/validators": "2.0.0",
"body-scroll-lock": "3.1.5",
"chromatism": "3.0.0",
"click-outside-vue3": "4.0.1",
"cropperjs": "1.5.12",
- "diff": "3.5.0",
"escape-html": "1.0.3",
"js-cookie": "3.0.1",
"localforage": "1.10.0",
+ "lozad": "1.16.0",
"parse-link-header": "2.0.0",
"phoenix": "1.6.2",
"punycode.js": "2.1.0",
@@ -41,7 +42,7 @@
"querystring-es3": "0.2.1",
"url": "0.11.0",
"utf8": "3.0.0",
- "vue": "3.2.37",
+ "vue": "3.2.38",
"vue-i18n": "9.2.2",
"vue-router": "4.1.5",
"vue-template-compiler": "2.7.10",
@@ -57,7 +58,7 @@
"@ungap/event-target": "0.2.3",
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
"@vue/babel-plugin-jsx": "1.1.1",
- "@vue/compiler-sfc": "3.2.37",
+ "@vue/compiler-sfc": "3.2.38",
"@vue/test-utils": "2.0.2",
"autoprefixer": "10.4.8",
"babel-loader": "8.2.5",
@@ -96,7 +97,6 @@
"karma-spec-reporter": "0.0.34",
"karma-webpack": "5.0.0",
"lodash": "4.17.21",
- "lolex": "1.6.0",
"mini-css-extract-plugin": "2.6.1",
"mocha": "10.0.0",
"nightwatch": "2.3.3",
@@ -104,13 +104,13 @@
"ora": "0.4.1",
"postcss": "8.4.16",
"postcss-loader": "7.0.1",
- "sass": "1.54.5",
+ "sass": "1.55.0",
"sass-loader": "13.0.2",
"selenium-server": "2.53.1",
"semver": "7.3.7",
"serviceworker-webpack5-plugin": "2.0.0",
"shelljs": "0.8.5",
- "sinon": "2.4.1",
+ "sinon": "14.0.0",
"sinon-chai": "3.7.0",
"stylelint": "13.13.1",
"stylelint-config-standard": "20.0.0",
diff --git a/src/App.js b/src/App.js
@@ -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
@@ -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 differ.
diff --git a/src/assets/pleromatan_apology_mask.png b/src/assets/pleromatan_apology_mask.png
Binary files differ.
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
diff --git a/yarn.lock b/yarn.lock
@@ -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"