aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.babelrc2
-rw-r--r--.gitignore1
-rw-r--r--CONTRIBUTORS.md2
-rw-r--r--build/build.js3
-rw-r--r--build/dev-server.js3
-rw-r--r--build/update-emoji.js27
-rw-r--r--build/webpack.base.conf.js3
-rw-r--r--package.json20
-rw-r--r--src/App.js5
-rw-r--r--src/App.vue2
-rw-r--r--src/assets/pleromatan_apology_fox_mask.pngbin0 -> 2827 bytes
-rw-r--r--src/assets/pleromatan_apology_mask.pngbin0 -> 2366 bytes
-rw-r--r--src/boot/after_store.js1
-rw-r--r--src/components/account_actions/account_actions.js3
-rw-r--r--src/components/account_actions/account_actions.vue7
-rw-r--r--src/components/attachment/attachment.js3
-rw-r--r--src/components/conversation/conversation.js16
-rw-r--r--src/components/edit_status_modal/edit_status_modal.js75
-rw-r--r--src/components/edit_status_modal/edit_status_modal.vue48
-rw-r--r--src/components/emoji_input/emoji_input.js50
-rw-r--r--src/components/emoji_input/emoji_input.vue3
-rw-r--r--src/components/emoji_input/suggestor.js34
-rw-r--r--src/components/emoji_picker/emoji_picker.js302
-rw-r--r--src/components/emoji_picker/emoji_picker.scss52
-rw-r--r--src/components/emoji_picker/emoji_picker.vue52
-rw-r--r--src/components/extra_buttons/extra_buttons.js27
-rw-r--r--src/components/extra_buttons/extra_buttons.vue22
-rw-r--r--src/components/follow_card/follow_card.js4
-rw-r--r--src/components/follow_card/follow_card.vue11
-rw-r--r--src/components/nav_panel/nav_panel.vue7
-rw-r--r--src/components/navigation/navigation_entry.js4
-rw-r--r--src/components/navigation/navigation_entry.vue133
-rw-r--r--src/components/optional_router_link/optional_router_link.vue23
-rw-r--r--src/components/post_status_form/post_status_form.js54
-rw-r--r--src/components/post_status_form/post_status_form.vue18
-rw-r--r--src/components/react_button/react_button.js4
-rw-r--r--src/components/remove_follower_button/remove_follower_button.js25
-rw-r--r--src/components/remove_follower_button/remove_follower_button.vue13
-rw-r--r--src/components/settings_modal/tabs/profile_tab.js4
-rw-r--r--src/components/status/status.js6
-rw-r--r--src/components/status/status.scss3
-rw-r--r--src/components/status/status.vue18
-rw-r--r--src/components/status_history_modal/status_history_modal.js60
-rw-r--r--src/components/status_history_modal/status_history_modal.vue46
-rw-r--r--src/components/still-image/still-image.js27
-rw-r--r--src/components/still-image/still-image.vue5
-rw-r--r--src/components/timeago/timeago.vue21
-rw-r--r--src/components/update_notification/update_notification.js25
-rw-r--r--src/components/update_notification/update_notification.scss12
-rw-r--r--src/components/update_notification/update_notification.vue7
-rw-r--r--src/i18n/en.json23
-rw-r--r--src/i18n/languages.js53
-rw-r--r--src/i18n/messages.js46
-rw-r--r--src/main.js5
-rw-r--r--src/modules/api.js9
-rw-r--r--src/modules/config.js1
-rw-r--r--src/modules/editStatus.js25
-rw-r--r--src/modules/instance.js149
-rw-r--r--src/modules/statusHistory.js25
-rw-r--r--src/modules/statuses.js9
-rw-r--r--src/modules/users.js8
-rw-r--r--src/services/api/api.service.js94
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js16
-rw-r--r--src/services/status_poster/status_poster.service.js42
-rw-r--r--static/emoji.json1431
-rw-r--r--yarn.lock320
66 files changed, 1670 insertions, 1879 deletions
diff --git a/.babelrc b/.babelrc
index 373d2c59..4ec10416 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,5 +1,5 @@
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
- "comments": false
+ "comments": true
}
diff --git a/.gitignore b/.gitignore
index 479d57c4..4df5ec83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ test/e2e/reports
selenium-debug.log
.idea/
config/local.json
+static/emoji.json
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index f666a4ef..bfc41ac4 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -10,3 +10,5 @@ Contributors of this project.
- shpuld (shpuld@shitposter.club): CSS and styling
- Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images.
- hj (hj@shigusegubu.club): Code
+- Sean King (seanking@freespeechextremist.com): Code
+- Tusooa Zhu (tusooa@kazv.moe): Code
diff --git a/build/build.js b/build/build.js
index b3c9aad4..35969eb6 100644
--- a/build/build.js
+++ b/build/build.js
@@ -18,6 +18,9 @@ console.log(
var spinner = ora('building for production...')
spinner.start()
+var updateEmoji = require('./update-emoji').updateEmoji
+updateEmoji()
+
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
rm('-rf', assetsPath)
mkdir('-p', assetsPath)
diff --git a/build/dev-server.js b/build/dev-server.js
index 854efa0b..e51ba948 100644
--- a/build/dev-server.js
+++ b/build/dev-server.js
@@ -10,6 +10,9 @@ var webpackConfig = process.env.NODE_ENV === 'testing'
? require('./webpack.prod.conf')
: require('./webpack.dev.conf')
+var updateEmoji = require('./update-emoji').updateEmoji
+updateEmoji()
+
// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
// Define HTTP proxies to your custom API backend
diff --git a/build/update-emoji.js b/build/update-emoji.js
new file mode 100644
index 00000000..9f4b4e67
--- /dev/null
+++ b/build/update-emoji.js
@@ -0,0 +1,27 @@
+
+module.exports = {
+ updateEmoji () {
+ const emojis = require('@kazvmoe-infra/unicode-emoji-json/data-by-group')
+ const fs = require('fs')
+
+ Object.keys(emojis)
+ .map(k => {
+ emojis[k].map(e => {
+ delete e.unicode_version
+ delete e.emoji_version
+ delete e.skin_tone_support_unicode_version
+ })
+ })
+
+ const res = {}
+ Object.keys(emojis)
+ .map(k => {
+ const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
+ res[groupId] = emojis[k]
+ })
+
+ console.info('Updating emojis...')
+ fs.writeFileSync('static/emoji.json', JSON.stringify(res))
+ console.info('Done.')
+ }
+}
diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js
index 78b75e3f..bf946922 100644
--- a/build/webpack.base.conf.js
+++ b/build/webpack.base.conf.js
@@ -24,7 +24,8 @@ module.exports = {
output: {
path: config.build.assetsRoot,
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
- filename: '[name].js'
+ filename: '[name].js',
+ chunkFilename: '[name].js'
},
optimization: {
splitChunks: {
diff --git a/package.json b/package.json
index f63eb7d7..313654a9 100644
--- a/package.json
+++ b/package.json
@@ -18,22 +18,23 @@
"dependencies": {
"@babel/runtime": "7.18.9",
"@chenfengyuan/vue-qrcode": "2.0.0",
- "@fortawesome/fontawesome-svg-core": "6.1.2",
- "@fortawesome/free-regular-svg-icons": "6.1.2",
- "@fortawesome/free-solid-svg-icons": "6.1.2",
+ "@fortawesome/fontawesome-svg-core": "6.2.0",
+ "@fortawesome/free-regular-svg-icons": "6.2.0",
+ "@fortawesome/free-solid-svg-icons": "6.2.0",
"@fortawesome/vue-fontawesome": "3.0.1",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
+ "@kazvmoe-infra/unicode-emoji-json": "0.4.0",
"@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12",
"@vuelidate/core": "2.0.0-alpha.44",
- "@vuelidate/validators": "2.0.0-alpha.31",
+ "@vuelidate/validators": "2.0.0",
"body-scroll-lock": "3.1.5",
"chromatism": "3.0.0",
"click-outside-vue3": "4.0.1",
"cropperjs": "1.5.12",
- "diff": "3.5.0",
"escape-html": "1.0.3",
"js-cookie": "3.0.1",
"localforage": "1.10.0",
+ "lozad": "1.16.0",
"parse-link-header": "2.0.0",
"phoenix": "1.6.2",
"punycode.js": "2.1.0",
@@ -41,7 +42,7 @@
"querystring-es3": "0.2.1",
"url": "0.11.0",
"utf8": "3.0.0",
- "vue": "3.2.37",
+ "vue": "3.2.38",
"vue-i18n": "9.2.2",
"vue-router": "4.1.5",
"vue-template-compiler": "2.7.10",
@@ -57,7 +58,7 @@
"@ungap/event-target": "0.2.3",
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
"@vue/babel-plugin-jsx": "1.1.1",
- "@vue/compiler-sfc": "3.2.37",
+ "@vue/compiler-sfc": "3.2.38",
"@vue/test-utils": "2.0.2",
"autoprefixer": "10.4.8",
"babel-loader": "8.2.5",
@@ -96,7 +97,6 @@
"karma-spec-reporter": "0.0.34",
"karma-webpack": "5.0.0",
"lodash": "4.17.21",
- "lolex": "1.6.0",
"mini-css-extract-plugin": "2.6.1",
"mocha": "10.0.0",
"nightwatch": "2.3.3",
@@ -104,13 +104,13 @@
"ora": "0.4.1",
"postcss": "8.4.16",
"postcss-loader": "7.0.1",
- "sass": "1.54.5",
+ "sass": "1.55.0",
"sass-loader": "13.0.2",
"selenium-server": "2.53.1",
"semver": "7.3.7",
"serviceworker-webpack5-plugin": "2.0.0",
"shelljs": "0.8.5",
- "sinon": "2.4.1",
+ "sinon": "14.0.0",
"sinon-chai": "3.7.0",
"stylelint": "13.13.1",
"stylelint-config-standard": "20.0.0",
diff --git a/src/App.js b/src/App.js
index 18dd61f2..b7eb2f72 100644
--- a/src/App.js
+++ b/src/App.js
@@ -10,7 +10,9 @@ import MobilePostStatusButton from './components/mobile_post_status_button/mobil
import MobileNav from './components/mobile_nav/mobile_nav.vue'
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
+import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
+import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
import { mapGetters } from 'vuex'
@@ -35,6 +37,8 @@ export default {
UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')),
UserReportingModal,
PostStatusModal,
+ EditStatusModal,
+ StatusHistoryModal,
GlobalNoticeList
},
data: () => ({
@@ -101,6 +105,7 @@ export default {
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
},
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
+ editingAvailable () { return this.$store.state.instance.editingAvailable },
shoutboxPosition () {
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
},
diff --git a/src/App.vue b/src/App.vue
index a993c238..e0d709f7 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -67,6 +67,8 @@
<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
new file mode 100644
index 00000000..4d1990d5
--- /dev/null
+++ 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
new file mode 100644
index 00000000..18adafff
--- /dev/null
+++ b/src/assets/pleromatan_apology_mask.png
Binary files differ
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 38b5f38e..886d52f2 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -251,6 +251,7 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
+ store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js
index 735dd81c..c23407f9 100644
--- a/src/components/account_actions/account_actions.js
+++ b/src/components/account_actions/account_actions.js
@@ -36,6 +36,9 @@ const AccountActions = {
unblockUser () {
this.$store.dispatch('unblockUser', this.user.id)
},
+ removeUserFromFollowers () {
+ this.$store.dispatch('removeUserFromFollowers', this.user.id)
+ },
reportUser () {
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
},
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index 770740e0..218aa6b3 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -30,6 +30,13 @@
</template>
<UserListMenu :user="user" />
<button
+ v-if="relationship.followed_by"
+ class="btn button-default btn-block dropdown-item"
+ @click="removeUserFromFollowers"
+ >
+ {{ $t('user_card.remove_follower') }}
+ </button>
+ <button
v-if="relationship.blocking"
class="btn button-default btn-block dropdown-item"
@click="unblockUser"
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index d62a4adc..5dc50475 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -129,6 +129,9 @@ const Attachment = {
...mapGetters(['mergedConfig'])
},
watch: {
+ 'attachment.description' (newVal) {
+ this.localDescription = newVal
+ },
localDescription (newVal) {
this.onEdit(newVal)
}
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 712e2a2c..85e6d8ad 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -1,6 +1,8 @@
import { reduce, filter, findIndex, clone, get } from 'lodash'
import Status from '../status/status.vue'
import ThreadTree from '../thread_tree/thread_tree.vue'
+import { WSConnectionStatus } from '../../services/api/api.service.js'
+import { mapGetters, mapState } from 'vuex'
import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue'
import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue'
@@ -79,6 +81,9 @@ const conversation = {
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
return maxDepth >= 1 ? maxDepth : 1
},
+ streamingEnabled () {
+ return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
+ },
displayStyle () {
return this.$store.getters.mergedConfig.conversationDisplay
},
@@ -341,7 +346,11 @@ const conversation = {
},
maybeHighlight () {
return this.isExpanded ? this.highlight : null
- }
+ },
+ ...mapGetters(['mergedConfig']),
+ ...mapState({
+ mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
+ })
},
components: {
Status,
@@ -399,6 +408,11 @@ const conversation = {
setHighlight (id) {
if (!id) return
this.highlight = id
+
+ if (!this.streamingEnabled) {
+ this.$store.dispatch('fetchStatus', id)
+ }
+
this.$store.dispatch('fetchFavsAndRepeats', id)
this.$store.dispatch('fetchEmojiReactionsBy', id)
},
diff --git a/src/components/edit_status_modal/edit_status_modal.js b/src/components/edit_status_modal/edit_status_modal.js
new file mode 100644
index 00000000..75adfea7
--- /dev/null
+++ b/src/components/edit_status_modal/edit_status_modal.js
@@ -0,0 +1,75 @@
+import PostStatusForm from '../post_status_form/post_status_form.vue'
+import Modal from '../modal/modal.vue'
+import statusPosterService from '../../services/status_poster/status_poster.service.js'
+import get from 'lodash/get'
+
+const EditStatusModal = {
+ components: {
+ PostStatusForm,
+ Modal
+ },
+ data () {
+ return {
+ resettingForm: false
+ }
+ },
+ computed: {
+ isLoggedIn () {
+ return !!this.$store.state.users.currentUser
+ },
+ modalActivated () {
+ return this.$store.state.editStatus.modalActivated
+ },
+ isFormVisible () {
+ return this.isLoggedIn && !this.resettingForm && this.modalActivated
+ },
+ params () {
+ return this.$store.state.editStatus.params || {}
+ }
+ },
+ watch: {
+ params (newVal, oldVal) {
+ if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
+ this.resettingForm = true
+ this.$nextTick(() => {
+ this.resettingForm = false
+ })
+ }
+ },
+ isFormVisible (val) {
+ if (val) {
+ this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
+ }
+ }
+ },
+ methods: {
+ doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
+ const params = {
+ store: this.$store,
+ statusId: this.$store.state.editStatus.params.statusId,
+ status,
+ spoilerText,
+ sensitive,
+ poll,
+ media,
+ contentType
+ }
+
+ return statusPosterService.editStatus(params)
+ .then((data) => {
+ return data
+ })
+ .catch((err) => {
+ console.error('Error editing status', err)
+ return {
+ error: err.message
+ }
+ })
+ },
+ closeModal () {
+ this.$store.dispatch('closeEditStatusModal')
+ }
+ }
+}
+
+export default EditStatusModal
diff --git a/src/components/edit_status_modal/edit_status_modal.vue b/src/components/edit_status_modal/edit_status_modal.vue
new file mode 100644
index 00000000..1dbacaab
--- /dev/null
+++ b/src/components/edit_status_modal/edit_status_modal.vue
@@ -0,0 +1,48 @@
+<template>
+ <Modal
+ v-if="isFormVisible"
+ class="edit-form-modal-view"
+ @backdropClicked="closeModal"
+ >
+ <div class="edit-form-modal-panel panel">
+ <div class="panel-heading">
+ {{ $t('post_status.edit_status') }}
+ </div>
+ <PostStatusForm
+ class="panel-body"
+ v-bind="params"
+ :post-handler="doEditStatus"
+ :disable-polls="true"
+ :disable-visibility-selector="true"
+ @posted="closeModal"
+ />
+ </div>
+ </Modal>
+</template>
+
+<script src="./edit_status_modal.js"></script>
+
+<style lang="scss">
+.modal-view.edit-form-modal-view {
+ align-items: flex-start;
+}
+.edit-form-modal-panel {
+ flex-shrink: 0;
+ margin-top: 25%;
+ margin-bottom: 2em;
+ width: 100%;
+ max-width: 700px;
+
+ @media (orientation: landscape) {
+ margin-top: 8%;
+ }
+
+ .form-bottom-left {
+ max-width: 6.5em;
+
+ .emoji-icon {
+ justify-content: right;
+ }
+ }
+}
+</style>
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index b664d6b3..ffc0ffac 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -3,7 +3,7 @@ import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
-
+import { ensureFinalFallback } from '../../i18n/languages.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faSmileBeam
@@ -143,6 +143,51 @@ const EmojiInput = {
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
return word
}
+ },
+ languages () {
+ return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
+ },
+ maybeLocalizedEmojiNamesAndKeywords () {
+ return emoji => {
+ const names = [emoji.displayText]
+ const keywords = []
+
+ if (emoji.displayTextI18n) {
+ names.push(this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args))
+ }
+
+ if (emoji.annotations) {
+ this.languages.forEach(lang => {
+ names.push(emoji.annotations[lang]?.name)
+
+ keywords.push(...(emoji.annotations[lang]?.keywords || []))
+ })
+ }
+
+ return {
+ names: names.filter(k => k),
+ keywords: keywords.filter(k => k)
+ }
+ }
+ },
+ maybeLocalizedEmojiName () {
+ return emoji => {
+ if (!emoji.annotations) {
+ return emoji.displayText
+ }
+
+ if (emoji.displayTextI18n) {
+ return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
+ }
+
+ for (const lang of this.languages) {
+ if (emoji.annotations[lang]?.name) {
+ return emoji.annotations[lang].name
+ }
+ }
+
+ return emoji.displayText
+ }
}
},
mounted () {
@@ -181,7 +226,7 @@ const EmojiInput = {
const firstchar = newWord.charAt(0)
this.suggestions = []
if (newWord === firstchar) return
- const matchedSuggestions = await this.suggest(newWord)
+ const matchedSuggestions = await this.suggest(newWord, this.maybeLocalizedEmojiNamesAndKeywords)
// Async: cancel if textAtCaret has changed during wait
if (this.textAtCaret !== newWord) return
if (matchedSuggestions.length <= 0) return
@@ -207,7 +252,6 @@ const EmojiInput = {
},
triggerShowPicker () {
this.showPicker = true
- this.$refs.picker.startEmojiLoad()
this.$nextTick(() => {
this.scrollIntoView()
this.focusPickerInput()
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index 81b81913..43581dbf 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -19,6 +19,7 @@
v-if="enableEmojiPicker"
ref="picker"
:class="{ hide: !showPicker }"
+ :showing="showPicker"
:enable-sticker-picker="enableStickerPicker"
class="emoji-picker-panel"
@emoji="insert"
@@ -63,7 +64,7 @@
v-if="!suggestion.user"
class="displayText"
>
- {{ suggestion.displayText }}
+ {{ maybeLocalizedEmojiName(suggestion) }}
</span>
<span class="detailText">{{ suggestion.detailText }}</span>
</div>
diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js
index 0ddb4d68..adaa879e 100644
--- a/src/components/emoji_input/suggestor.js
+++ b/src/components/emoji_input/suggestor.js
@@ -2,7 +2,7 @@
* suggest - generates a suggestor function to be used by emoji-input
* data: object providing source information for specific types of suggestions:
* data.emoji - optional, an array of all emoji available i.e.
- * (state.instance.emoji + state.instance.customEmoji)
+ * (getters.standardEmojiList + state.instance.customEmoji)
* data.users - optional, an array of all known users
* updateUsersList - optional, a function to search and append to users
*
@@ -13,10 +13,10 @@
export default data => {
const emojiCurry = suggestEmoji(data.emoji)
const usersCurry = data.store && suggestUsers(data.store)
- return input => {
+ return (input, nameKeywordLocalizer) => {
const firstChar = input[0]
if (firstChar === ':' && data.emoji) {
- return emojiCurry(input)
+ return emojiCurry(input, nameKeywordLocalizer)
}
if (firstChar === '@' && usersCurry) {
return usersCurry(input)
@@ -25,34 +25,34 @@ export default data => {
}
}
-export const suggestEmoji = emojis => input => {
+export const suggestEmoji = emojis => (input, nameKeywordLocalizer) => {
const noPrefix = input.toLowerCase().substr(1)
return emojis
- .filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
- .sort((a, b) => {
- let aScore = 0
- let bScore = 0
+ .map(emoji => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
+ .filter((emoji) => (emoji.names.concat(emoji.keywords)).filter(kw => kw.toLowerCase().match(noPrefix)).length)
+ .map(k => {
+ let score = 0
// An exact match always wins
- aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0
- bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0
+ score += Math.max(...k.names.map(name => name.toLowerCase() === noPrefix ? 200 : 0), 0)
// Prioritize custom emoji a lot
- aScore += a.imageUrl ? 100 : 0
- bScore += b.imageUrl ? 100 : 0
+ score += k.imageUrl ? 100 : 0
// Prioritize prefix matches somewhat
- aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
- bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
+ score += Math.max(...k.names.map(kw => kw.toLowerCase().startsWith(noPrefix) ? 10 : 0), 0)
// Sort by length
- aScore -= a.displayText.length
- bScore -= b.displayText.length
+ score -= k.displayText.length
+ k.score = score
+ return k
+ })
+ .sort((a, b) => {
// Break ties alphabetically
const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5
- return bScore - aScore + alphabetically
+ return b.score - a.score + alphabetically
})
}
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index f6920208..fafc2af1 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,33 +1,76 @@
import { defineAsyncComponent } from 'vue'
import Checkbox from '../checkbox/checkbox.vue'
+import StillImage from '../still-image/still-image.vue'
+import { ensureFinalFallback } from '../../i18n/languages.js'
+import lozad from 'lozad'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faBoxOpen,
faStickyNote,
- faSmileBeam
+ faSmileBeam,
+ faSmile,
+ faUser,
+ faPaw,
+ faIceCream,
+ faBus,
+ faBasketballBall,
+ faLightbulb,
+ faCode,
+ faFlag
} from '@fortawesome/free-solid-svg-icons'
-import { trim } from 'lodash'
+import { debounce, trim } from 'lodash'
library.add(
faBoxOpen,
faStickyNote,
- faSmileBeam
+ faSmileBeam,
+ faSmile,
+ faUser,
+ faPaw,
+ faIceCream,
+ faBus,
+ faBasketballBall,
+ faLightbulb,
+ faCode,
+ faFlag
)
-// At widest, approximately 20 emoji are visible in a row,
-// loading 3 rows, could be overkill for narrow picker
-const LOAD_EMOJI_BY = 60
+const UNICODE_EMOJI_GROUP_ICON = {
+ 'smileys-and-emotion': 'smile',
+ 'people-and-body': 'user',
+ 'animals-and-nature': 'paw',
+ 'food-and-drink': 'ice-cream',
+ 'travel-and-places': 'bus',
+ activities: 'basketball-ball',
+ objects: 'lightbulb',
+ symbols: 'code',
+ flags: 'flag'
+}
-// When to start loading new batch emoji, in pixels
-const LOAD_EMOJI_MARGIN = 64
+const maybeLocalizedKeywords = (emoji, languages, nameLocalizer) => {
+ const res = [emoji.displayText, nameLocalizer(emoji)]
+ if (emoji.annotations) {
+ languages.forEach(lang => {
+ const keywords = emoji.annotations[lang]?.keywords || []
+ const name = emoji.annotations[lang]?.name
+ res.push(...(keywords.concat([name]).filter(k => k)))
+ })
+ }
+ return res
+}
-const filterByKeyword = (list, keyword = '') => {
+const filterByKeyword = (list, keyword = '', languages, nameLocalizer) => {
if (keyword === '') return list
const keywordLowercase = keyword.toLowerCase()
const orderedEmojiList = []
for (const emoji of list) {
- const indexOfKeyword = emoji.displayText.toLowerCase().indexOf(keywordLowercase)
+ const indices = maybeLocalizedKeywords(emoji, languages, nameLocalizer)
+ .map(k => k.toLowerCase().indexOf(keywordLowercase))
+ .filter(k => k > -1)
+
+ const indexOfKeyword = indices.length ? Math.min(...indices) : -1
+
if (indexOfKeyword > -1) {
if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
orderedEmojiList[indexOfKeyword] = []
@@ -44,6 +87,10 @@ const EmojiPicker = {
required: false,
type: Boolean,
default: false
+ },
+ showing: {
+ required: true,
+ type: Boolean
}
},
data () {
@@ -53,16 +100,26 @@ const EmojiPicker = {
showingStickers: false,
groupsScrolledClass: 'scrolled-top',
keepOpen: false,
- customEmojiBufferSlice: LOAD_EMOJI_BY,
customEmojiTimeout: null,
- customEmojiLoadAllConfirmed: false
+ // Lazy-load only after the first time `showing` becomes true.
+ contentLoaded: false,
+ groupRefs: {},
+ emojiRefs: {},
+ filteredEmojiGroups: []
}
},
components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
- Checkbox
+ Checkbox,
+ StillImage
},
methods: {
+ setGroupRef (name) {
+ return el => { this.groupRefs[name] = el }
+ },
+ setEmojiRef (name) {
+ return el => { this.emojiRefs[name] = el }
+ },
onStickerUploaded (e) {
this.$emit('sticker-uploaded', e)
},
@@ -77,10 +134,38 @@ const EmojiPicker = {
const target = (e && e.target) || this.$refs['emoji-groups']
this.updateScrolledClass(target)
this.scrolledGroup(target)
- this.triggerLoadMore(target)
+ },
+ scrolledGroup (target) {
+ const top = target.scrollTop + 5
+ this.$nextTick(() => {
+ this.allEmojiGroups.forEach(group => {
+ const ref = this.groupRefs['group-' + group.id]
+ if (ref && ref.offsetTop <= top) {
+ this.activeGroup = group.id
+ }
+ })
+ this.scrollHeader()
+ })
+ },
+ scrollHeader () {
+ // Scroll the active tab's header into view
+ const headerRef = this.groupRefs['group-header-' + this.activeGroup]
+ const left = headerRef.offsetLeft
+ const right = left + headerRef.offsetWidth
+ const headerCont = this.$refs.header
+ const currentScroll = headerCont.scrollLeft
+ const currentScrollRight = currentScroll + headerCont.clientWidth
+ const setScroll = s => { headerCont.scrollLeft = s }
+
+ const margin = 7 // .emoji-tabs-item: padding
+ if (left - margin < currentScroll) {
+ setScroll(left - margin)
+ } else if (right + margin > currentScrollRight) {
+ setScroll(right + margin - headerCont.clientWidth)
+ }
},
highlight (key) {
- const ref = this.$refs['group-' + key]
+ const ref = this.groupRefs['group-' + key]
const top = ref.offsetTop
this.setShowStickers(false)
this.activeGroup = key
@@ -97,73 +182,90 @@ const EmojiPicker = {
this.groupsScrolledClass = 'scrolled-middle'
}
},
- triggerLoadMore (target) {
- const ref = this.$refs['group-end-custom']
- if (!ref) return
- const bottom = ref.offsetTop + ref.offsetHeight
-
- const scrollerBottom = target.scrollTop + target.clientHeight
- const scrollerTop = target.scrollTop
- const scrollerMax = target.scrollHeight
-
- // Loads more emoji when they come into view
- const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
- // Always load when at the very top in case there's no scroll space yet
- const atTop = scrollerTop < 5
- // Don't load when looking at unicode category or at the very bottom
- const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
- if (!bottomAboveViewport && (approachingBottom || atTop)) {
- this.loadEmoji()
- }
+ toggleStickers () {
+ this.showingStickers = !this.showingStickers
},
- scrolledGroup (target) {
- const top = target.scrollTop + 5
+ setShowStickers (value) {
+ this.showingStickers = value
+ },
+ filterByKeyword (list, keyword) {
+ return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName)
+ },
+ initializeLazyLoad () {
+ this.destroyLazyLoad()
this.$nextTick(() => {
- this.emojisView.forEach(group => {
- const ref = this.$refs['group-' + group.id]
- if (ref.offsetTop <= top) {
- this.activeGroup = group.id
+ this.$lozad = lozad('.still-image.emoji-picker-emoji', {
+ load: el => {
+ const name = el.getAttribute('data-emoji-name')
+ const vn = this.emojiRefs[name]
+ if (!vn) {
+ return
+ }
+
+ vn.loadLazy()
}
})
+ this.$lozad.observe()
})
},
- loadEmoji () {
- const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
-
- if (allLoaded) {
- return
- }
-
- this.customEmojiBufferSlice += LOAD_EMOJI_BY
+ waitForDomAndInitializeLazyLoad () {
+ this.$nextTick(() => this.initializeLazyLoad())
},
- startEmojiLoad (forceUpdate = false) {
- if (!forceUpdate) {
- this.keyword = ''
- }
- this.$nextTick(() => {
- this.$refs['emoji-groups'].scrollTop = 0
- })
- const bufferSize = this.customEmojiBuffer.length
- const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
- if (bufferPrefilledAll && !forceUpdate) {
- return
+ destroyLazyLoad () {
+ if (this.$lozad) {
+ if (this.$lozad.observer) {
+ this.$lozad.observer.disconnect()
+ }
+ if (this.$lozad.mutationObserver) {
+ this.$lozad.mutationObserver.disconnect()
+ }
}
- this.customEmojiBufferSlice = LOAD_EMOJI_BY
},
- toggleStickers () {
- this.showingStickers = !this.showingStickers
+ onShowing () {
+ const oldContentLoaded = this.contentLoaded
+ this.contentLoaded = true
+ this.waitForDomAndInitializeLazyLoad()
+ this.filteredEmojiGroups = this.getFilteredEmojiGroups()
+ if (!oldContentLoaded) {
+ this.$nextTick(() => {
+ if (this.defaultGroup) {
+ this.highlight(this.defaultGroup)
+ }
+ })
+ }
},
- setShowStickers (value) {
- this.showingStickers = value
+ getFilteredEmojiGroups () {
+ return this.allEmojiGroups
+ .map(group => ({
+ ...group,
+ emojis: this.filterByKeyword(group.emojis, trim(this.keyword))
+ }))
+ .filter(group => group.emojis.length > 0)
}
},
watch: {
keyword () {
- this.customEmojiLoadAllConfirmed = false
this.onScroll()
- this.startEmojiLoad(true)
+ this.debouncedHandleKeywordChange()
+ },
+ allCustomGroups () {
+ this.waitForDomAndInitializeLazyLoad()
+ this.filteredEmojiGroups = this.getFilteredEmojiGroups()
+ },
+ showing (val) {
+ if (val) {
+ this.onShowing()
+ }
}
},
+ mounted () {
+ if (this.showing) {
+ this.onShowing()
+ }
+ },
+ destroyed () {
+ this.destroyLazyLoad()
+ },
computed: {
activeGroupView () {
return this.showingStickers ? '' : this.activeGroup
@@ -174,39 +276,55 @@ const EmojiPicker = {
}
return 0
},
- filteredEmoji () {
- return filterByKeyword(
- this.$store.state.instance.customEmoji || [],
- trim(this.keyword)
- )
+ allCustomGroups () {
+ return this.$store.getters.groupedCustomEmojis
},
- customEmojiBuffer () {
- return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
+ defaultGroup () {
+ return Object.keys(this.allCustomGroups)[0]
},
- emojis () {
- const standardEmojis = this.$store.state.instance.emoji || []
- const customEmojis = this.customEmojiBuffer
-
- return [
- {
- id: 'custom',
- text: this.$t('emoji.custom'),
- icon: 'smile-beam',
- emojis: customEmojis
- },
- {
- id: 'standard',
- text: this.$t('emoji.unicode'),
- icon: 'box-open',
- emojis: filterByKeyword(standardEmojis, trim(this.keyword))
- }
- ]
+ unicodeEmojiGroups () {
+ return this.$store.getters.standardEmojiGroupList.map(group => ({
+ id: `standard-${group.id}`,
+ text: this.$t(`emoji.unicode_groups.${group.id}`),
+ icon: UNICODE_EMOJI_GROUP_ICON[group.id],
+ emojis: group.emojis
+ }))
},
- emojisView () {
- return this.emojis.filter(value => value.emojis.length > 0)
+ allEmojiGroups () {
+ return Object.entries(this.allCustomGroups)
+ .map(([_, v]) => v)
+ .concat(this.unicodeEmojiGroups)
},
stickerPickerEnabled () {
return (this.$store.state.instance.stickers || []).length !== 0
+ },
+ debouncedHandleKeywordChange () {
+ return debounce(() => {
+ this.waitForDomAndInitializeLazyLoad()
+ this.filteredEmojiGroups = this.getFilteredEmojiGroups()
+ }, 500)
+ },
+ languages () {
+ return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
+ },
+ maybeLocalizedEmojiName () {
+ return emoji => {
+ if (!emoji.annotations) {
+ return emoji.displayText
+ }
+
+ if (emoji.displayTextI18n) {
+ return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
+ }
+
+ for (const lang of this.languages) {
+ if (emoji.annotations[lang]?.name) {
+ return emoji.annotations[lang].name
+ }
+ }
+
+ return emoji.displayText
+ }
}
}
}
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index a2f17c51..016c46d7 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -1,5 +1,10 @@
@import '../../_variables.scss';
+$emoji-picker-header-height: 36px;
+$emoji-picker-header-picture-width: 32px;
+$emoji-picker-header-picture-height: 32px;
+$emoji-picker-emoji-size: 32px;
+
.emoji-picker {
display: flex;
flex-direction: column;
@@ -19,6 +24,23 @@
--lightText: var(--popoverLightText, $fallback--lightText);
--icon: var(--popoverIcon, $fallback--icon);
+ &-header-image {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ width: $emoji-picker-header-picture-width;
+ max-width: $emoji-picker-header-picture-width;
+ height: $emoji-picker-header-picture-height;
+ max-height: $emoji-picker-header-picture-height;
+ .still-image {
+ max-width: 100%;
+ max-height: 100%;
+ height: 100%;
+ width: 100%;
+ object-fit: contain;
+ }
+ }
+
.keep-open,
.too-many-emoji {
padding: 7px;
@@ -37,7 +59,6 @@
.heading {
display: flex;
- height: 32px;
padding: 10px 7px 5px;
}
@@ -50,6 +71,10 @@
.emoji-tabs {
flex-grow: 1;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ overflow-x: auto;
}
.emoji-groups {
@@ -57,6 +82,8 @@
}
.additional-tabs {
+ display: flex;
+ flex: 1;
border-left: 1px solid;
border-left-color: $fallback--icon;
border-left-color: var(--icon, $fallback--icon);
@@ -66,15 +93,20 @@
.additional-tabs,
.emoji-tabs {
- display: block;
- min-width: 0;
flex-basis: auto;
- flex-shrink: 1;
+ display: flex;
+ align-content: center;
&-item {
padding: 0 7px;
cursor: pointer;
font-size: 1.85em;
+ width: $emoji-picker-header-picture-width;
+ max-width: $emoji-picker-header-picture-width;
+ height: $emoji-picker-header-picture-height;
+ max-height: $emoji-picker-header-picture-height;
+ display: flex;
+ align-items: center;
&.disabled {
opacity: 0.5;
@@ -164,22 +196,26 @@
}
&-item {
- width: 32px;
- height: 32px;
+ width: $emoji-picker-emoji-size;
+ height: $emoji-picker-emoji-size;
box-sizing: border-box;
display: flex;
- font-size: 32px;
+ line-height: $emoji-picker-emoji-size;
align-items: center;
justify-content: center;
margin: 4px;
cursor: pointer;
- img {
+ .emoji-picker-emoji.-custom {
object-fit: contain;
max-width: 100%;
max-height: 100%;
}
+ .emoji-picker-emoji.-unicode {
+ font-size: 24px;
+ overflow: hidden;
+ }
}
}
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index a7269120..57bb0037 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -1,19 +1,34 @@
<template>
- <div class="emoji-picker panel panel-default panel-body">
+ <div
+ class="emoji-picker panel panel-default panel-body"
+ >
<div class="heading">
- <span class="emoji-tabs">
+ <span
+ ref="header"
+ class="emoji-tabs"
+ >
<span
- v-for="group in emojis"
+ v-for="group in filteredEmojiGroups"
+ :ref="setGroupRef('group-header-' + group.id)"
:key="group.id"
class="emoji-tabs-item"
:class="{
- active: activeGroupView === group.id,
- disabled: group.emojis.length === 0
+ active: activeGroupView === group.id
}"
:title="group.text"
@click.prevent="highlight(group.id)"
>
+ <span
+ v-if="group.image"
+ class="emoji-picker-header-image"
+ >
+ <still-image
+ :alt="group.text"
+ :src="group.image"
+ />
+ </span>
<FAIcon
+ v-else
:icon="group.icon"
fixed-width
/>
@@ -36,7 +51,10 @@
</span>
</span>
</div>
- <div class="content">
+ <div
+ v-if="contentLoaded"
+ class="content"
+ >
<div
class="emoji-content"
:class="{hidden: showingStickers}"
@@ -57,12 +75,12 @@
@scroll="onScroll"
>
<div
- v-for="group in emojisView"
+ v-for="group in filteredEmojiGroups"
:key="group.id"
class="emoji-group"
>
<h6
- :ref="'group-' + group.id"
+ :ref="setGroupRef('group-' + group.id)"
class="emoji-group-title"
>
{{ group.text }}
@@ -70,17 +88,23 @@
<span
v-for="emoji in group.emojis"
:key="group.id + emoji.displayText"
- :title="emoji.displayText"
+ :title="maybeLocalizedEmojiName(emoji)"
class="emoji-item"
@click.stop.prevent="onEmoji(emoji)"
>
- <span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
- <img
+ <span
+ v-if="!emoji.imageUrl"
+ class="emoji-picker-emoji -unicode"
+ >{{ emoji.replacement }}</span>
+ <still-image
v-else
- :src="emoji.imageUrl"
- >
+ :ref="setEmojiRef(group.id + emoji.displayText)"
+ class="emoji-picker-emoji -custom"
+ :data-src="emoji.imageUrl"
+ :data-emoji-name="group.id + emoji.displayText"
+ />
</span>
- <span :ref="'group-end-' + group.id" />
+ <span :ref="setGroupRef('group-end-' + group.id)" />
</div>
</div>
<div class="keep-open">
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
index 68fa66ad..2e495423 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -7,6 +7,7 @@ import {
faThumbtack,
faShareAlt,
faExternalLinkAlt,
+ faHistory,
faPlus,
faTimes
} from '@fortawesome/free-solid-svg-icons'
@@ -24,6 +25,7 @@ library.add(
faShareAlt,
faExternalLinkAlt,
faFlag,
+ faHistory,
faPlus,
faTimes
)
@@ -86,6 +88,25 @@ const ExtraButtons = {
},
reportStatus () {
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
+ },
+ editStatus () {
+ this.$store.dispatch('fetchStatusSource', { id: this.status.id })
+ .then(data => this.$store.dispatch('openEditStatusModal', {
+ statusId: this.status.id,
+ subject: data.spoiler_text,
+ statusText: data.text,
+ statusIsSensitive: this.status.nsfw,
+ statusPoll: this.status.poll,
+ statusFiles: [...this.status.attachments],
+ visibility: this.status.visibility,
+ statusContentType: data.content_type
+ }))
+ },
+ showStatusHistory () {
+ const originalStatus = { ...this.status }
+ const stripFieldsList = ['attachments', 'created_at', 'emojis', 'text', 'raw_html', 'nsfw', 'poll', 'summary', 'summary_raw_html']
+ stripFieldsList.forEach(p => delete originalStatus[p])
+ this.$store.dispatch('openStatusHistoryModal', originalStatus)
}
},
computed: {
@@ -109,7 +130,11 @@ const ExtraButtons = {
},
statusLink () {
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
- }
+ },
+ isEdited () {
+ return this.status.edited_at !== null
+ },
+ editingAvailable () { return this.$store.state.instance.editingAvailable }
}
}
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index 011dff9b..b2fad1c9 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -78,6 +78,28 @@
</button>
</template>
<button
+ v-if="ownStatus && editingAvailable"
+ class="button-default dropdown-item dropdown-item-icon"
+ @click.prevent="editStatus"
+ @click="close"
+ >
+ <FAIcon
+ fixed-width
+ icon="pen"
+ /><span>{{ $t("status.edit") }}</span>
+ </button>
+ <button
+ v-if="isEdited && editingAvailable"
+ class="button-default dropdown-item dropdown-item-icon"
+ @click.prevent="showStatusHistory"
+ @click="close"
+ >
+ <FAIcon
+ fixed-width
+ icon="history"
+ /><span>{{ $t("status.status_history") }}</span>
+ </button>
+ <button
v-if="canDelete"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="deleteStatus"
diff --git a/src/components/follow_card/follow_card.js b/src/components/follow_card/follow_card.js
index 6dcb6d47..b26b27a7 100644
--- a/src/components/follow_card/follow_card.js
+++ b/src/components/follow_card/follow_card.js
@@ -1,6 +1,7 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue'
import FollowButton from '../follow_button/follow_button.vue'
+import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue'
const FollowCard = {
props: [
@@ -10,7 +11,8 @@ const FollowCard = {
components: {
BasicUserCard,
RemoteFollow,
- FollowButton
+ FollowButton,
+ RemoveFollowerButton
},
computed: {
isMe () {
diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue
index 895a8fa3..c919b11a 100644
--- a/src/components/follow_card/follow_card.vue
+++ b/src/components/follow_card/follow_card.vue
@@ -22,6 +22,11 @@
class="follow-card-follow-button"
:user="user"
/>
+ <RemoveFollowerButton
+ v-if="noFollowsYou && relationship.followed_by"
+ :relationship="relationship"
+ class="follow-card-button"
+ />
</template>
</div>
</basic-user-card>
@@ -40,6 +45,12 @@
line-height: 1.5em;
}
+ &-button {
+ margin-top: 0.5em;
+ padding: 0 1.5em;
+ margin-left: 1em;
+ }
+
&-follow-button {
margin-top: 0.5em;
margin-left: auto;
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index 2688bcf4..7373ca63 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -121,7 +121,6 @@
border-bottom: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
- padding: 0;
}
> li {
@@ -150,12 +149,6 @@
font-size: 1.1em;
}
- .menu-item {
- .timelines-chevron {
- margin-right: 0;
- }
- }
-
.timelines-background {
padding: 0 0 0 0.6em;
background-color: $fallback--lightBg;
diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js
index fe3402fc..81cc936a 100644
--- a/src/components/navigation/navigation_entry.js
+++ b/src/components/navigation/navigation_entry.js
@@ -1,5 +1,6 @@
import { mapState } from 'vuex'
import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js'
+import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faThumbtack } from '@fortawesome/free-solid-svg-icons'
@@ -7,6 +8,9 @@ library.add(faThumbtack)
const NavigationEntry = {
props: ['item', 'showPin'],
+ components: {
+ OptionalRouterLink
+ },
methods: {
isPinned (value) {
return this.pinnedItems.has(value)
diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue
index 824c00a2..f4d53836 100644
--- a/src/components/navigation/navigation_entry.vue
+++ b/src/components/navigation/navigation_entry.vue
@@ -1,26 +1,37 @@
<template>
- <li class="NavigationEntry">
- <component
- :is="routeTo ? 'router-link' : 'button'"
- class="menu-item button-unstyled"
- :to="routeTo"
+ <OptionalRouterLink
+ v-slot="{ isActive, href, navigate } = {}"
+ ass="ass"
+ :to="routeTo"
+ >
+ <li
+ class="NavigationEntry menu-item"
+ :class="{ '-active': isActive }"
+ v-bind="$attrs"
>
- <span>
- <FAIcon
- v-if="item.icon"
- fixed-width
- class="fa-scale-110 menu-icon"
- :icon="item.icon"
- />
- </span>
- <span
- v-if="item.iconLetter"
- class="icon iconLetter fa-scale-110 menu-icon"
- >{{ item.iconLetter }}
- </span>
- <span class="label">
- {{ item.labelRaw || $t(item.label) }}
- </span>
+ <component
+ :is="routeTo ? 'a' : 'button'"
+ class="main-link button-unstyled"
+ :href="href"
+ @click="navigate"
+ >
+ <span>
+ <FAIcon
+ v-if="item.icon"
+ fixed-width
+ class="fa-scale-110 menu-icon"
+ :icon="item.icon"
+ />
+ </span>
+ <span
+ v-if="item.iconLetter"
+ class="icon iconLetter fa-scale-110 menu-icon"
+ >{{ item.iconLetter }}
+ </span>
+ <span class="label">
+ {{ item.labelRaw || $t(item.label) }}
+ </span>
+ </component>
<slot />
<div
v-if="item.badgeGetter && getters[item.badgeGetter]"
@@ -45,8 +56,8 @@
icon="thumbtack"
/>
</button>
- </component>
- </li>
+ </li>
+ </OptionalRouterLink>
</template>
<script src="./navigation_entry.js"></script>
@@ -55,7 +66,21 @@
@import '../../_variables.scss';
.NavigationEntry {
- .label {
+ display: flex;
+ box-sizing: border-box;
+ align-items: baseline;
+ height: 3.5em;
+ line-height: 3.5em;
+ padding: 0 1em;
+ width: 100%;
+ color: $fallback--link;
+ color: var(--link, $fallback--link);
+
+ .timelines-chevron {
+ margin-right: 0;
+ }
+
+ .main-link {
flex: 1;
}
@@ -72,48 +97,36 @@
}
}
- .menu-item {
- display: flex;
- box-sizing: border-box;
- align-items: baseline;
- height: 3.5em;
- line-height: 3.5em;
- padding: 0 1em;
- width: 100%;
+ &:hover {
+ background-color: $fallback--lightBg;
+ background-color: var(--selectedMenu, $fallback--lightBg);
color: $fallback--link;
- color: var(--link, $fallback--link);
-
- &:hover {
- background-color: $fallback--lightBg;
- background-color: var(--selectedMenu, $fallback--lightBg);
- color: $fallback--link;
- color: var(--selectedMenuText, $fallback--link);
- --faint: var(--selectedMenuFaintText, $fallback--faint);
- --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
- --lightText: var(--selectedMenuLightText, $fallback--lightText);
+ color: var(--selectedMenuText, $fallback--link);
+ --faint: var(--selectedMenuFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuLightText, $fallback--lightText);
- .menu-icon {
- --icon: var(--text, $fallback--icon);
- }
+ .menu-icon {
+ --icon: var(--text, $fallback--icon);
}
+ }
- &.router-link-active {
- font-weight: bolder;
- background-color: $fallback--lightBg;
- background-color: var(--selectedMenu, $fallback--lightBg);
- color: $fallback--text;
- color: var(--selectedMenuText, $fallback--text);
- --faint: var(--selectedMenuFaintText, $fallback--faint);
- --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
- --lightText: var(--selectedMenuLightText, $fallback--lightText);
+ &.-active {
+ font-weight: bolder;
+ background-color: $fallback--lightBg;
+ background-color: var(--selectedMenu, $fallback--lightBg);
+ color: $fallback--text;
+ color: var(--selectedMenuText, $fallback--text);
+ --faint: var(--selectedMenuFaintText, $fallback--faint);
+ --faintLink: var(--selectedMenuFaintLink, $fallback--faint);
+ --lightText: var(--selectedMenuLightText, $fallback--lightText);
- .menu-icon {
- --icon: var(--text, $fallback--icon);
- }
+ .menu-icon {
+ --icon: var(--text, $fallback--icon);
+ }
- &:hover {
- text-decoration: underline;
- }
+ &:hover {
+ text-decoration: underline;
}
}
}
diff --git a/src/components/optional_router_link/optional_router_link.vue b/src/components/optional_router_link/optional_router_link.vue
new file mode 100644
index 00000000..d56ad268
--- /dev/null
+++ b/src/components/optional_router_link/optional_router_link.vue
@@ -0,0 +1,23 @@
+<template>
+ <!-- eslint-disable vue/no-multiple-template-root -->
+ <router-link
+ v-if="to"
+ v-slot="props"
+ :to="to"
+ custom
+ >
+ <slot
+ v-bind="props"
+ />
+ </router-link>
+ <slot
+ v-else
+ v-bind="{}"
+ />
+</template>
+
+<script>
+export default {
+ props: ['to']
+}
+</script>
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index c0d80b20..5c536b74 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -55,6 +55,14 @@ const pxStringToNumber = (str) => {
const PostStatusForm = {
props: [
+ 'statusId',
+ 'statusText',
+ 'statusIsSensitive',
+ 'statusPoll',
+ 'statusFiles',
+ 'statusMediaDescriptions',
+ 'statusScope',
+ 'statusContentType',
'replyTo',
'repliedUser',
'attentions',
@@ -62,6 +70,7 @@ const PostStatusForm = {
'subject',
'disableSubject',
'disableScopeSelector',
+ 'disableVisibilitySelector',
'disableNotice',
'disableLockWarning',
'disablePolls',
@@ -125,22 +134,38 @@ const PostStatusForm = {
const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig
+ let statusParams = {
+ spoilerText: this.subject || '',
+ status: statusText,
+ nsfw: !!sensitiveByDefault,
+ files: [],
+ poll: {},
+ mediaDescriptions: {},
+ visibility: scope,
+ contentType
+ }
+
+ if (this.statusId) {
+ const statusContentType = this.statusContentType || contentType
+ statusParams = {
+ spoilerText: this.subject || '',
+ status: this.statusText || '',
+ nsfw: this.statusIsSensitive || !!sensitiveByDefault,
+ files: this.statusFiles || [],
+ poll: this.statusPoll || {},
+ mediaDescriptions: this.statusMediaDescriptions || {},
+ visibility: this.statusScope || scope,
+ contentType: statusContentType
+ }
+ }
+
return {
dropFiles: [],
uploadingFiles: false,
error: null,
posting: false,
highlighted: 0,
- newStatus: {
- spoilerText: this.subject || '',
- status: statusText,
- nsfw: !!sensitiveByDefault,
- files: [],
- poll: {},
- mediaDescriptions: {},
- visibility: scope,
- contentType
- },
+ newStatus: statusParams,
caret: 0,
pollFormVisible: false,
showDropIcon: 'hide',
@@ -164,7 +189,7 @@ const PostStatusForm = {
emojiUserSuggestor () {
return suggestor({
emoji: [
- ...this.$store.state.instance.emoji,
+ ...this.$store.getters.standardEmojiList,
...this.$store.state.instance.customEmoji
],
store: this.$store
@@ -173,13 +198,13 @@ const PostStatusForm = {
emojiSuggestor () {
return suggestor({
emoji: [
- ...this.$store.state.instance.emoji,
+ ...this.$store.getters.standardEmojiList,
...this.$store.state.instance.customEmoji
]
})
},
emoji () {
- return this.$store.state.instance.emoji || []
+ return this.$store.getters.standardEmojiList || []
},
customEmoji () {
return this.$store.state.instance.customEmoji || []
@@ -236,6 +261,9 @@ const PostStatusForm = {
uploadFileLimitReached () {
return this.newStatus.files.length >= this.fileLimit
},
+ isEdit () {
+ return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
+ },
...mapGetters(['mergedConfig']),
...mapState({
mobileLayout: state => state.interface.mobileLayout
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 62613bd1..f65058f4 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -67,6 +67,13 @@
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
</p>
<div
+ v-if="isEdit"
+ class="visibility-notice edit-warning"
+ >
+ <p>{{ $t('post_status.edit_remote_warning') }}</p>
+ <p>{{ $t('post_status.edit_unsupported_warning') }}</p>
+ </div>
+ <div
v-if="!disablePreview"
class="preview-heading faint"
>
@@ -170,6 +177,7 @@
class="visibility-tray"
>
<scope-selector
+ v-if="!disableVisibilitySelector"
:show-all="showAllScopes"
:user-default="userDefaultScope"
:original-scope="copyMessageScope"
@@ -410,6 +418,16 @@
align-items: baseline;
}
+ .visibility-notice.edit-warning {
+ > :first-child {
+ margin-top: 0;
+ }
+
+ > :last-child {
+ margin-bottom: 0;
+ }
+ }
+
.media-upload-icon, .poll-icon, .emoji-icon {
font-size: 1.85em;
line-height: 1.1;
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
index 5e052e1e..e65bfd93 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -59,7 +59,7 @@ const ReactButton = {
if (this.filterWord !== '') {
const filterWordLowercase = trim(this.filterWord.toLowerCase())
const orderedEmojiList = []
- for (const emoji of this.$store.state.instance.emoji) {
+ for (const emoji of this.$store.getters.standardEmojiList) {
if (emoji.replacement === this.filterWord) return [emoji]
const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase)
@@ -72,7 +72,7 @@ const ReactButton = {
}
return orderedEmojiList.flat()
}
- return this.$store.state.instance.emoji || []
+ return this.$store.getters.standardEmojiList || []
},
mergedConfig () {
return this.$store.getters.mergedConfig
diff --git a/src/components/remove_follower_button/remove_follower_button.js b/src/components/remove_follower_button/remove_follower_button.js
new file mode 100644
index 00000000..e1a7531b
--- /dev/null
+++ b/src/components/remove_follower_button/remove_follower_button.js
@@ -0,0 +1,25 @@
+export default {
+ props: ['relationship'],
+ data () {
+ return {
+ inProgress: false
+ }
+ },
+ computed: {
+ label () {
+ if (this.inProgress) {
+ return this.$t('user_card.follow_progress')
+ } else {
+ return this.$t('user_card.remove_follower')
+ }
+ }
+ },
+ methods: {
+ onClick () {
+ this.inProgress = true
+ this.$store.dispatch('removeUserFromFollowers', this.relationship.id).then(() => {
+ this.inProgress = false
+ })
+ }
+ }
+}
diff --git a/src/components/remove_follower_button/remove_follower_button.vue b/src/components/remove_follower_button/remove_follower_button.vue
new file mode 100644
index 00000000..a3a4c242
--- /dev/null
+++ b/src/components/remove_follower_button/remove_follower_button.vue
@@ -0,0 +1,13 @@
+<template>
+ <button
+ class="btn button-default follow-button"
+ :class="{ toggled: inProgress }"
+ :disabled="inProgress"
+ :title="$t('user_card.remove_follower')"
+ @click="onClick"
+ >
+ {{ label }}
+ </button>
+</template>
+
+<script src="./remove_follower_button.js"></script>
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index 376248ef..b86faef0 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -64,7 +64,7 @@ const ProfileTab = {
emojiUserSuggestor () {
return suggestor({
emoji: [
- ...this.$store.state.instance.emoji,
+ ...this.$store.getters.standardEmojiList,
...this.$store.state.instance.customEmoji
],
store: this.$store
@@ -73,7 +73,7 @@ const ProfileTab = {
emojiSuggestor () {
return suggestor({
emoji: [
- ...this.$store.state.instance.emoji,
+ ...this.$store.getters.standardEmojiList,
...this.$store.state.instance.customEmoji
]
})
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 19356664..9a9bca7a 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -395,6 +395,12 @@ const Status = {
},
visibilityLocalized () {
return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
+ },
+ isEdited () {
+ return this.status.edited_at !== null
+ },
+ editingAvailable () {
+ return this.$store.state.instance.editingAvailable
}
},
methods: {
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index b3ad3818..ada9841e 100644
--- a/src/components/status/status.scss
+++ b/src/components/status/status.scss
@@ -156,7 +156,8 @@
margin-right: 0.2em;
}
- & .heading-reply-row {
+ & .heading-reply-row,
+ & .heading-edited-row {
position: relative;
align-content: baseline;
font-size: 0.85em;
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 91f5fffa..82eb7ac6 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -327,6 +327,24 @@
class="mentions-line"
/>
</div>
+ <div
+ v-if="isEdited && editingAvailable && !isPreview"
+ class="heading-edited-row"
+ >
+ <i18n-t
+ keypath="status.edited_at"
+ tag="span"
+ >
+ <template #time>
+ <Timeago
+ template-key="time.in_past"
+ :time="status.edited_at"
+ :auto-update="60"
+ :long-format="true"
+ />
+ </template>
+ </i18n-t>
+ </div>
</div>
<StatusContent
diff --git a/src/components/status_history_modal/status_history_modal.js b/src/components/status_history_modal/status_history_modal.js
new file mode 100644
index 00000000..3941a56f
--- /dev/null
+++ b/src/components/status_history_modal/status_history_modal.js
@@ -0,0 +1,60 @@
+import { get } from 'lodash'
+import Modal from '../modal/modal.vue'
+import Status from '../status/status.vue'
+
+const StatusHistoryModal = {
+ components: {
+ Modal,
+ Status
+ },
+ data () {
+ return {
+ statuses: []
+ }
+ },
+ computed: {
+ modalActivated () {
+ return this.$store.state.statusHistory.modalActivated
+ },
+ params () {
+ return this.$store.state.statusHistory.params
+ },
+ statusId () {
+ return this.params.id
+ },
+ historyCount () {
+ return this.statuses.length
+ },
+ history () {
+ return this.statuses
+ }
+ },
+ watch: {
+ params (newVal, oldVal) {
+ const newStatusId = get(newVal, 'id') !== get(oldVal, 'id')
+ if (newStatusId) {
+ this.resetHistory()
+ }
+
+ if (newStatusId || get(newVal, 'edited_at') !== get(oldVal, 'edited_at')) {
+ this.fetchStatusHistory()
+ }
+ }
+ },
+ methods: {
+ resetHistory () {
+ this.statuses = []
+ },
+ fetchStatusHistory () {
+ this.$store.dispatch('fetchStatusHistory', this.params)
+ .then(data => {
+ this.statuses = data
+ })
+ },
+ closeModal () {
+ this.$store.dispatch('closeStatusHistoryModal')
+ }
+ }
+}
+
+export default StatusHistoryModal
diff --git a/src/components/status_history_modal/status_history_modal.vue b/src/components/status_history_modal/status_history_modal.vue
new file mode 100644
index 00000000..990be35b
--- /dev/null
+++ b/src/components/status_history_modal/status_history_modal.vue
@@ -0,0 +1,46 @@
+<template>
+ <Modal
+ v-if="modalActivated"
+ class="status-history-modal-view"
+ @backdropClicked="closeModal"
+ >
+ <div class="status-history-modal-panel panel">
+ <div class="panel-heading">
+ {{ $t('status.status_history') }} ({{ historyCount }})
+ </div>
+ <div class="panel-body">
+ <div
+ v-if="historyCount > 0"
+ class="history-body"
+ >
+ <status
+ v-for="status in history"
+ :key="status.id"
+ :statusoid="status"
+ :is-preview="true"
+ class="conversation-status status-fadein panel-body"
+ />
+ </div>
+ </div>
+ </div>
+ </Modal>
+</template>
+
+<script src="./status_history_modal.js"></script>
+
+<style lang="scss">
+.modal-view.status-history-modal-view {
+ align-items: flex-start;
+}
+.status-history-modal-panel {
+ flex-shrink: 0;
+ margin-top: 25%;
+ margin-bottom: 2em;
+ width: 100%;
+ max-width: 700px;
+
+ @media (orientation: landscape) {
+ margin-top: 8%;
+ }
+}
+</style>
diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js
index d7abbcb5..200ef147 100644
--- a/src/components/still-image/still-image.js
+++ b/src/components/still-image/still-image.js
@@ -7,16 +7,23 @@ const StillImage = {
'imageLoadHandler',
'alt',
'height',
- 'width'
+ 'width',
+ 'dataSrc'
],
data () {
return {
+ // for lazy loading, see loadLazy()
+ realSrc: this.src,
stopGifs: this.$store.getters.mergedConfig.stopGifs
}
},
computed: {
animated () {
- return this.stopGifs && (this.mimetype === 'image/gif' || this.src.endsWith('.gif'))
+ if (!this.realSrc) {
+ return false
+ }
+
+ return this.stopGifs && (this.mimetype === 'image/gif' || this.realSrc.endsWith('.gif'))
},
style () {
const appendPx = (str) => /\d$/.test(str) ? str + 'px' : str
@@ -27,7 +34,15 @@ const StillImage = {
}
},
methods: {
+ loadLazy () {
+ if (this.dataSrc) {
+ this.realSrc = this.dataSrc
+ }
+ },
onLoad () {
+ if (!this.realSrc) {
+ return
+ }
const image = this.$refs.src
if (!image) return
this.imageLoadHandler && this.imageLoadHandler(image)
@@ -42,6 +57,14 @@ const StillImage = {
onError () {
this.imageLoadError && this.imageLoadError()
}
+ },
+ watch: {
+ src () {
+ this.realSrc = this.src
+ },
+ dataSrc () {
+ this.$el.removeAttribute('data-loaded')
+ }
}
}
diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue
index ab3080c8..633fb229 100644
--- a/src/components/still-image/still-image.vue
+++ b/src/components/still-image/still-image.vue
@@ -11,10 +11,11 @@
<!-- NOTE: key is required to force to re-render img tag when src is changed -->
<img
ref="src"
- :key="src"
+ :key="realSrc"
:alt="alt"
:title="alt"
- :src="src"
+ :data-src="dataSrc"
+ :src="realSrc"
:referrerpolicy="referrerpolicy"
@load="onLoad"
@error="onError"
diff --git a/src/components/timeago/timeago.vue b/src/components/timeago/timeago.vue
index 2b487dfd..b5f49515 100644
--- a/src/components/timeago/timeago.vue
+++ b/src/components/timeago/timeago.vue
@@ -3,7 +3,7 @@
:datetime="time"
:title="localeDateString"
>
- {{ $tc(relativeTime.key, relativeTime.num, [relativeTime.num]) }}
+ {{ relativeTimeString }}
</time>
</template>
@@ -13,7 +13,7 @@ import localeService from 'src/services/locale/locale.service.js'
export default {
name: 'Timeago',
- props: ['time', 'autoUpdate', 'longFormat', 'nowThreshold'],
+ props: ['time', 'autoUpdate', 'longFormat', 'nowThreshold', 'templateKey'],
data () {
return {
relativeTime: { key: 'time.now', num: 0 },
@@ -26,6 +26,23 @@ export default {
return typeof this.time === 'string'
? new Date(Date.parse(this.time)).toLocaleString(browserLocale)
: this.time.toLocaleString(browserLocale)
+ },
+ relativeTimeString () {
+ const timeString = this.$i18n.tc(this.relativeTime.key, this.relativeTime.num, [this.relativeTime.num])
+
+ if (typeof this.templateKey === 'string' && this.relativeTime.key !== 'time.now') {
+ return this.$i18n.t(this.templateKey, [timeString])
+ }
+
+ return timeString
+ }
+ },
+ watch: {
+ time (newVal, oldVal) {
+ if (oldVal !== newVal) {
+ clearTimeout(this.interval)
+ this.refreshRelativeTimeObject()
+ }
}
},
created () {
diff --git a/src/components/update_notification/update_notification.js b/src/components/update_notification/update_notification.js
index 609842c4..ddf379f5 100644
--- a/src/components/update_notification/update_notification.js
+++ b/src/components/update_notification/update_notification.js
@@ -2,6 +2,8 @@ import Modal from 'src/components/modal/modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import pleromaTan from 'src/assets/pleromatan_apology.png'
import pleromaTanFox from 'src/assets/pleromatan_apology_fox.png'
+import pleromaTanMask from 'src/assets/pleromatan_apology_mask.png'
+import pleromaTanFoxMask from 'src/assets/pleromatan_apology_fox_mask.png'
import {
faTimes
@@ -15,9 +17,9 @@ export const CURRENT_UPDATE_COUNTER = 1
const UpdateNotification = {
data () {
return {
+ showingImage: false,
pleromaTanVariant: Math.random() > 0.5 ? pleromaTan : pleromaTanFox,
- showingMore: false,
- contentHeight: 0
+ showingMore: false
}
},
components: {
@@ -25,13 +27,9 @@ const UpdateNotification = {
},
computed: {
pleromaTanStyles () {
+ const mask = this.pleromaTanVariant === pleromaTan ? pleromaTanMask : pleromaTanFoxMask
return {
- 'shape-outside': 'url(' + this.pleromaTanVariant + ')'
- }
- },
- dynamicStyles () {
- return {
- '--____extraInfoGroupHeight': this.contentHeight + 'px'
+ 'shape-outside': 'url(' + mask + ')'
}
},
shouldShow () {
@@ -57,9 +55,14 @@ const UpdateNotification = {
}
},
mounted () {
- setTimeout(() => {
- this.contentHeight = this.$refs.animatedText.scrollHeight
- }, 1000)
+ this.contentHeightNoImage = this.$refs.animatedText.scrollHeight
+
+ // Workaround to get the text height only after mask loaded. A bit hacky.
+ const newImg = new Image()
+ newImg.onload = () => {
+ setTimeout(() => { this.showingImage = true }, 100)
+ }
+ newImg.src = this.pleromaTanVariant === pleromaTan ? pleromaTanMask : pleromaTanFoxMask
}
}
diff --git a/src/components/update_notification/update_notification.scss b/src/components/update_notification/update_notification.scss
index 8cad1bc7..ce8129d0 100644
--- a/src/components/update_notification/update_notification.scss
+++ b/src/components/update_notification/update_notification.scss
@@ -35,6 +35,12 @@
margin-top: calc(-1 * var(--__top-fringe));
margin-bottom: calc(-1 * var(--__bottom-fringe));
margin-right: calc(-1 * var(--__right-fringe));
+
+ &.-noImage {
+ .text {
+ padding-right: var(--__right-fringe);
+ }
+ }
}
.panel-body {
@@ -75,9 +81,9 @@
.extra-info-group {
transition: max-height, padding, height;
- transition-timing-function: ease-in-out;
- transition-duration: 500ms;
- max-height: calc(var(--____extraInfoGroupHeight) + 1em); // include bottom padding
+ transition-timing-function: ease-in;
+ transition-duration: 700ms;
+ max-height: 70vh;
mask:
linear-gradient(to top, white, transparent) bottom/100% 2px no-repeat,
linear-gradient(to top, white, white);
diff --git a/src/components/update_notification/update_notification.vue b/src/components/update_notification/update_notification.vue
index 00841af2..78e70a74 100644
--- a/src/components/update_notification/update_notification.vue
+++ b/src/components/update_notification/update_notification.vue
@@ -7,7 +7,6 @@
<div
class="UpdateNotificationModal panel"
:class="{ '-peek': !showingMore }"
- :style="dynamicStyles"
>
<div class="panel-heading">
<span class="title">
@@ -15,8 +14,12 @@
</span>
</div>
<div class="panel-body">
- <div class="content">
+ <div
+ class="content"
+ :class="{ '-noImage': !showingImage }"
+ >
<img
+ v-if="showingImage"
class="pleroma-tan"
:src="pleromaTanVariant"
:style="pleromaTanStyles"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index ffa4c6b0..30b59e82 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -199,8 +199,20 @@
"add_emoji": "Insert emoji",
"custom": "Custom emoji",
"unicode": "Unicode emoji",
+ "unicode_groups": {
+ "activities": "Activities",
+ "animals-and-nature": "Animals & Nature",
+ "flags": "Flags",
+ "food-and-drink": "Food & Drink",
+ "objects": "Objects",
+ "people-and-body": "People & Body",
+ "smileys-and-emotion": "Smileys & Emotion",
+ "symbols": "Symbols",
+ "travel-and-places": "Travel & Places"
+ },
"load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.",
- "load_all": "Loading all {emojiAmount} emoji"
+ "load_all": "Loading all {emojiAmount} emoji",
+ "regional_indicator": "Regional indicator {letter}"
},
"errors": {
"storage_unavailable": "Pleroma could not access browser storage. Your login or your local settings won't be saved and you might encounter unexpected issues. Try enabling cookies."
@@ -214,6 +226,7 @@
"load_older": "Load older interactions"
},
"post_status": {
+ "edit_status": "Edit status",
"new_status": "Post new status",
"account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.",
"account_not_locked_warning_link": "locked",
@@ -229,6 +242,8 @@
"default": "Just landed in L.A.",
"direct_warning_to_all": "This post will be visible to all the mentioned users.",
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
+ "edit_remote_warning": "Other remote instances may not support editing and unable to receive the latest version of your post.",
+ "edit_unsupported_warning": "Pleroma does not support editing mentions or polls.",
"posting": "Posting",
"post": "Post",
"preview": "Preview",
@@ -797,6 +812,8 @@
"favorites": "Favorites",
"repeats": "Repeats",
"delete": "Delete status",
+ "edit": "Edit status",
+ "edited_at": "(last edited {time})",
"pin": "Pin on profile",
"unpin": "Unpin from profile",
"pinned": "Pinned",
@@ -844,7 +861,8 @@
"ancestor_follow_with_icon": "{icon} {text}",
"show_all_conversation_with_icon": "{icon} {text}",
"show_all_conversation": "Show full conversation ({numStatus} other status) | Show full conversation ({numStatus} other statuses)",
- "show_only_conversation_under_this": "Only show replies to this status"
+ "show_only_conversation_under_this": "Only show replies to this status",
+ "status_history": "Status history"
},
"user_card": {
"approve": "Approve",
@@ -872,6 +890,7 @@
"muted": "Muted",
"per_day": "per day",
"remote_follow": "Remote follow",
+ "remove_follower": "Remove follower",
"report": "Report",
"statuses": "Statuses",
"subscribe": "Subscribe",
diff --git a/src/i18n/languages.js b/src/i18n/languages.js
new file mode 100644
index 00000000..250b3b1a
--- /dev/null
+++ b/src/i18n/languages.js
@@ -0,0 +1,53 @@
+
+const languages = [
+ 'ar',
+ 'ca',
+ 'cs',
+ 'de',
+ 'eo',
+ 'en',
+ 'es',
+ 'et',
+ 'eu',
+ 'fi',
+ 'fr',
+ 'ga',
+ 'he',
+ 'hu',
+ 'it',
+ 'ja',
+ 'ja_easy',
+ 'ko',
+ 'nb',
+ 'nl',
+ 'oc',
+ 'pl',
+ 'pt',
+ 'ro',
+ 'ru',
+ 'sk',
+ 'te',
+ 'uk',
+ 'zh',
+ 'zh_Hant'
+]
+
+const specialJsonName = {
+ ja: 'ja_pedantic'
+}
+
+const langCodeToJsonName = (code) => specialJsonName[code] || code
+
+const langCodeToCldrName = (code) => code
+
+const ensureFinalFallback = codes => {
+ const codeList = Array.isArray(codes) ? codes : [codes]
+ return codeList.includes('en') ? codeList : codeList.concat(['en'])
+}
+
+module.exports = {
+ languages,
+ langCodeToJsonName,
+ langCodeToCldrName,
+ ensureFinalFallback
+}
diff --git a/src/i18n/messages.js b/src/i18n/messages.js
index eae75c80..74a89ca8 100644
--- a/src/i18n/messages.js
+++ b/src/i18n/messages.js
@@ -7,46 +7,26 @@
// sed -i -e "s/'//gm" -e 's/"/\\"/gm' -re 's/^( +)(.+?): ((.+?))?(,?)(\{?)$/\1"\2": "\4"/gm' -e 's/\"\{\"/{/g' -e 's/,"$/",/g' file.json
// There's only problem that apostrophe character ' gets replaced by \\ so you have to fix it manually, sorry.
-const loaders = {
- ar: () => import('./ar.json'),
- ca: () => import('./ca.json'),
- cs: () => import('./cs.json'),
- de: () => import('./de.json'),
- eo: () => import('./eo.json'),
- es: () => import('./es.json'),
- et: () => import('./et.json'),
- eu: () => import('./eu.json'),
- fi: () => import('./fi.json'),
- fr: () => import('./fr.json'),
- ga: () => import('./ga.json'),
- he: () => import('./he.json'),
- hu: () => import('./hu.json'),
- it: () => import('./it.json'),
- ja: () => import('./ja_pedantic.json'),
- ja_easy: () => import('./ja_easy.json'),
- ko: () => import('./ko.json'),
- nb: () => import('./nb.json'),
- nl: () => import('./nl.json'),
- oc: () => import('./oc.json'),
- pl: () => import('./pl.json'),
- pt: () => import('./pt.json'),
- ro: () => import('./ro.json'),
- ru: () => import('./ru.json'),
- sk: () => import('./sk.json'),
- te: () => import('./te.json'),
- uk: () => import('./uk.json'),
- zh: () => import('./zh.json'),
- zh_Hant: () => import('./zh_Hant.json')
+import { languages, langCodeToJsonName } from './languages.js'
+
+const hasLanguageFile = (code) => languages.includes(code)
+
+const loadLanguageFile = (code) => {
+ return import(
+ /* webpackInclude: /\.json$/ */
+ /* webpackChunkName: "i18n/[request]" */
+ `./${langCodeToJsonName(code)}.json`
+ )
}
const messages = {
- languages: ['en', ...Object.keys(loaders)],
+ languages,
default: {
en: require('./en.json').default
},
setLanguage: async (i18n, language) => {
- if (loaders[language]) {
- const messages = await loaders[language]()
+ if (hasLanguageFile(language)) {
+ const messages = await loadLanguageFile(language)
i18n.setLocaleMessage(language, messages.default)
}
i18n.locale = language
diff --git a/src/main.js b/src/main.js
index 0a050b04..6aa9cbb7 100644
--- a/src/main.js
+++ b/src/main.js
@@ -20,6 +20,9 @@ import oauthTokensModule from './modules/oauth_tokens.js'
import reportsModule from './modules/reports.js'
import pollsModule from './modules/polls.js'
import postStatusModule from './modules/postStatus.js'
+import editStatusModule from './modules/editStatus.js'
+import statusHistoryModule from './modules/statusHistory.js'
+
import chatsModule from './modules/chats.js'
import { createI18n } from 'vue-i18n'
@@ -86,6 +89,8 @@ const persistedStateOptions = {
reports: reportsModule,
polls: pollsModule,
postStatus: postStatusModule,
+ editStatus: editStatusModule,
+ statusHistory: statusHistoryModule,
chats: chatsModule
},
plugins,
diff --git a/src/modules/api.js b/src/modules/api.js
index f783fa4f..0acc03f1 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -16,7 +16,7 @@ const api = {
followRequests: []
},
getters: {
- followRequestCount: state => state.api.followRequests.length
+ followRequestCount: state => state.followRequests.length
},
mutations: {
setBackendInteractor (state, backendInteractor) {
@@ -103,6 +103,13 @@ const api = {
showImmediately: timelineData.visibleStatuses.length === 0,
timeline: 'friends'
})
+ } else if (message.event === 'status.update') {
+ dispatch('addNewStatuses', {
+ statuses: [message.status],
+ userId: false,
+ showImmediately: message.status.id in timelineData.visibleStatusesObject,
+ timeline: 'friends'
+ })
} else if (message.event === 'delete') {
dispatch('deleteStatusById', message.id)
} else if (message.event === 'pleroma:chat_update') {
diff --git a/src/modules/config.js b/src/modules/config.js
index eeaac917..c966602e 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -183,6 +183,7 @@ const config = {
break
case 'interfaceLanguage':
messages.setLanguage(this.getters.i18n, value)
+ dispatch('loadUnicodeEmojiData', value)
Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value))
break
case 'thirdColumnMode':
diff --git a/src/modules/editStatus.js b/src/modules/editStatus.js
new file mode 100644
index 00000000..fd316519
--- /dev/null
+++ b/src/modules/editStatus.js
@@ -0,0 +1,25 @@
+const editStatus = {
+ state: {
+ params: null,
+ modalActivated: false
+ },
+ mutations: {
+ openEditStatusModal (state, params) {
+ state.params = params
+ state.modalActivated = true
+ },
+ closeEditStatusModal (state) {
+ state.modalActivated = false
+ }
+ },
+ actions: {
+ openEditStatusModal ({ commit }, params) {
+ commit('openEditStatusModal', params)
+ },
+ closeEditStatusModal ({ commit }) {
+ commit('closeEditStatusModal')
+ }
+ }
+}
+
+export default editStatus
diff --git a/src/modules/instance.js b/src/modules/instance.js
index bfce6f38..b1bc9779 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -2,6 +2,39 @@ import { getPreset, applyTheme } from '../services/style_setter/style_setter.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import apiService from '../services/api/api.service.js'
import { instanceDefaultProperties } from './config.js'
+import { langCodeToCldrName, ensureFinalFallback } from '../i18n/languages.js'
+
+const SORTED_EMOJI_GROUP_IDS = [
+ 'smileys-and-emotion',
+ 'people-and-body',
+ 'animals-and-nature',
+ 'food-and-drink',
+ 'travel-and-places',
+ 'activities',
+ 'objects',
+ 'symbols',
+ 'flags'
+]
+
+const REGIONAL_INDICATORS = (() => {
+ const start = 0x1F1E6
+ const end = 0x1F1FF
+ const A = 'A'.codePointAt(0)
+ const res = new Array(end - start + 1)
+ for (let i = start; i <= end; ++i) {
+ const letter = String.fromCodePoint(A + i - start)
+ res[i - start] = {
+ replacement: String.fromCodePoint(i),
+ imageUrl: false,
+ displayText: 'regional_indicator_' + letter,
+ displayTextI18n: {
+ key: 'emoji.regional_indicator',
+ args: { letter }
+ }
+ }
+ }
+ return res
+})()
const defaultState = {
// Stuff from apiConfig
@@ -64,8 +97,9 @@ const defaultState = {
// Nasty stuff
customEmoji: [],
customEmojiFetched: false,
- emoji: [],
+ emoji: {},
emojiFetched: false,
+ unicodeEmojiAnnotations: {},
pleromaBackend: true,
postFormats: [],
restrictedNicknames: [],
@@ -97,6 +131,31 @@ const defaultState = {
}
}
+const loadAnnotations = (lang) => {
+ return import(
+ /* webpackChunkName: "emoji-annotations/[request]" */
+ `@kazvmoe-infra/unicode-emoji-json/annotations/${langCodeToCldrName(lang)}.json`
+ )
+ .then(k => k.default)
+}
+
+const injectAnnotations = (emoji, annotations) => {
+ const availableLangs = Object.keys(annotations)
+
+ return {
+ ...emoji,
+ annotations: availableLangs.reduce((acc, cur) => {
+ acc[cur] = annotations[cur][emoji.replacement]
+ return acc
+ }, {})
+ }
+}
+
+const injectRegionalIndicators = groups => {
+ groups.symbols.push(...REGIONAL_INDICATORS)
+ return groups
+}
+
const instance = {
state: defaultState,
mutations: {
@@ -107,6 +166,9 @@ const instance = {
},
setKnownDomains (state, domains) {
state.knownDomains = domains
+ },
+ setUnicodeEmojiAnnotations (state, { lang, annotations }) {
+ state.unicodeEmojiAnnotations[lang] = annotations
}
},
getters: {
@@ -115,6 +177,41 @@ const instance = {
.map(key => [key, state[key]])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
},
+ groupedCustomEmojis (state) {
+ const packsOf = emoji => {
+ return emoji.tags
+ .filter(k => k.startsWith('pack:'))
+ .map(k => k.slice(5)) // remove 'pack:' prefix
+ }
+
+ return state.customEmoji
+ .reduce((res, emoji) => {
+ packsOf(emoji).forEach(packName => {
+ const packId = `custom-${packName}`
+ if (!res[packId]) {
+ res[packId] = ({
+ id: packId,
+ text: packName,
+ image: emoji.imageUrl,
+ emojis: []
+ })
+ }
+ res[packId].emojis.push(emoji)
+ })
+ return res
+ }, {})
+ },
+ standardEmojiList (state) {
+ return SORTED_EMOJI_GROUP_IDS
+ .map(groupId => (state.emoji[groupId] || []).map(k => injectAnnotations(k, state.unicodeEmojiAnnotations)))
+ .reduce((a, b) => a.concat(b), [])
+ },
+ standardEmojiGroupList (state) {
+ return SORTED_EMOJI_GROUP_IDS.map(groupId => ({
+ id: groupId,
+ emojis: (state.emoji[groupId] || []).map(k => injectAnnotations(k, state.unicodeEmojiAnnotations))
+ }))
+ },
instanceDomain (state) {
return new URL(state.server).hostname
}
@@ -138,32 +235,52 @@ const instance = {
},
async getStaticEmoji ({ commit }) {
try {
- const res = await window.fetch('/static/emoji.json')
- if (res.ok) {
- const values = await res.json()
- const emoji = Object.keys(values).map((key) => {
- return {
- displayText: key,
- imageUrl: false,
- replacement: values[key]
- }
- }).sort((a, b) => a.name > b.name ? 1 : -1)
- commit('setInstanceOption', { name: 'emoji', value: emoji })
- } else {
- throw (res)
- }
+ const values = (await import(/* webpackChunkName: 'emoji' */ '../../static/emoji.json')).default
+
+ const emoji = Object.keys(values).reduce((res, groupId) => {
+ res[groupId] = values[groupId].map(e => ({
+ displayText: e.slug,
+ imageUrl: false,
+ replacement: e.emoji
+ }))
+ return res
+ }, {})
+ commit('setInstanceOption', { name: 'emoji', value: injectRegionalIndicators(emoji) })
} catch (e) {
console.warn("Can't load static emoji")
console.warn(e)
}
},
+ loadUnicodeEmojiData ({ commit, state }, language) {
+ const langList = ensureFinalFallback(language)
+
+ return Promise.all(
+ langList
+ .map(async lang => {
+ if (!state.unicodeEmojiAnnotations[lang]) {
+ const annotations = await loadAnnotations(lang)
+ commit('setUnicodeEmojiAnnotations', { lang, annotations })
+ }
+ }))
+ },
+
async getCustomEmoji ({ commit, state }) {
try {
const res = await window.fetch('/api/pleroma/emoji.json')
if (res.ok) {
const result = await res.json()
const values = Array.isArray(result) ? Object.assign({}, ...result) : result
+ const caseInsensitiveStrCmp = (a, b) => {
+ const la = a.toLowerCase()
+ const lb = b.toLowerCase()
+ return la > lb ? 1 : (la < lb ? -1 : 0)
+ }
+ const byPackThenByName = (a, b) => {
+ const packOf = emoji => (emoji.tags.filter(k => k.startsWith('pack:'))[0] || '').slice(5)
+ return caseInsensitiveStrCmp(packOf(a), packOf(b)) || caseInsensitiveStrCmp(a.displayText, b.displayText)
+ }
+
const emoji = Object.entries(values).map(([key, value]) => {
const imageUrl = value.image_url
return {
@@ -174,7 +291,7 @@ const instance = {
}
// Technically could use tags but those are kinda useless right now,
// should have been "pack" field, that would be more useful
- }).sort((a, b) => a.displayText.toLowerCase() > b.displayText.toLowerCase() ? 1 : -1)
+ }).sort(byPackThenByName)
commit('setInstanceOption', { name: 'customEmoji', value: emoji })
} else {
throw (res)
diff --git a/src/modules/statusHistory.js b/src/modules/statusHistory.js
new file mode 100644
index 00000000..db3d6d4b
--- /dev/null
+++ b/src/modules/statusHistory.js
@@ -0,0 +1,25 @@
+const statusHistory = {
+ state: {
+ params: {},
+ modalActivated: false
+ },
+ mutations: {
+ openStatusHistoryModal (state, params) {
+ state.params = params
+ state.modalActivated = true
+ },
+ closeStatusHistoryModal (state) {
+ state.modalActivated = false
+ }
+ },
+ actions: {
+ openStatusHistoryModal ({ commit }, params) {
+ commit('openStatusHistoryModal', params)
+ },
+ closeStatusHistoryModal ({ commit }) {
+ commit('closeStatusHistoryModal')
+ }
+ }
+}
+
+export default statusHistory
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 4aa04683..803d7019 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -249,6 +249,9 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
status: (status) => {
addStatus(status, showImmediately)
},
+ edit: (status) => {
+ addStatus(status, showImmediately)
+ },
retweet: (status) => {
// RetweetedStatuses are never shown immediately
const retweetedStatus = addStatus(status.retweeted_status, false, false)
@@ -606,6 +609,12 @@ const statuses = {
return rootState.api.backendInteractor.fetchStatus({ id })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
+ fetchStatusSource ({ rootState, dispatch }, status) {
+ return apiService.fetchStatusSource({ id: status.id, credentials: rootState.users.currentUser.credentials })
+ },
+ fetchStatusHistory ({ rootState, dispatch }, status) {
+ return apiService.fetchStatusHistory({ status })
+ },
deleteStatus ({ rootState, commit }, status) {
commit('setDeleted', { status })
apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
diff --git a/src/modules/users.js b/src/modules/users.js
index de28766a..eef87c2c 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -51,6 +51,11 @@ const unblockUser = (store, id) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
+const removeUserFromFollowers = (store, id) => {
+ return store.rootState.api.backendInteractor.removeUserFromFollowers({ id })
+ .then((relationship) => store.commit('updateUserRelationship', [relationship]))
+}
+
const muteUser = (store, id) => {
const predictedRelationship = store.state.relationships[id] || { id }
predictedRelationship.muting = true
@@ -321,6 +326,9 @@ const users = {
unblockUser (store, id) {
return unblockUser(store, id)
},
+ removeUserFromFollowers (store, id) {
+ return removeUserFromFollowers(store, id)
+ },
blockUsers (store, ids = []) {
return Promise.all(ids.map(id => blockUser(store, id)))
},
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index dd85b281..e692338e 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,5 +1,5 @@
import { each, map, concat, last, get } from 'lodash'
-import { parseStatus, parseUser, parseNotification, parseAttachment, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
+import { parseStatus, parseSource, parseUser, parseNotification, parseAttachment, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
import { RegistrationError, StatusCodeError } from '../errors/errors'
/* eslint-env browser */
@@ -49,6 +49,8 @@ const MASTODON_PUBLIC_TIMELINE = '/api/v1/timelines/public'
const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home'
const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}`
const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context`
+const MASTODON_STATUS_SOURCE_URL = id => `/api/v1/statuses/${id}/source`
+const MASTODON_STATUS_HISTORY_URL = id => `/api/v1/statuses/${id}/history`
const MASTODON_USER_URL = '/api/v1/accounts'
const MASTODON_USER_LOOKUP_URL = '/api/v1/accounts/lookup'
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
@@ -65,6 +67,7 @@ const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
const MASTODON_UNBLOCK_USER_URL = id => `/api/v1/accounts/${id}/unblock`
const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
+const MASTODON_REMOVE_USER_FROM_FOLLOWERS = id => `/api/v1/accounts/${id}/remove_from_followers`
const MASTODON_SUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/subscribe`
const MASTODON_UNSUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/unsubscribe`
const MASTODON_BOOKMARK_STATUS_URL = id => `/api/v1/statuses/${id}/bookmark`
@@ -305,6 +308,13 @@ const unblockUser = ({ id, credentials }) => {
}).then((data) => data.json())
}
+const removeUserFromFollowers = ({ id, credentials }) => {
+ return fetch(MASTODON_REMOVE_USER_FROM_FOLLOWERS(id), {
+ headers: authHeaders(credentials),
+ method: 'POST'
+ }).then((data) => data.json())
+}
+
const approveUser = ({ id, credentials }) => {
const url = MASTODON_APPROVE_USER_URL(id)
return fetch(url, {
@@ -522,6 +532,31 @@ const fetchStatus = ({ id, credentials }) => {
.then((data) => parseStatus(data))
}
+const fetchStatusSource = ({ id, credentials }) => {
+ const url = MASTODON_STATUS_SOURCE_URL(id)
+ return fetch(url, { headers: authHeaders(credentials) })
+ .then((data) => {
+ if (data.ok) {
+ return data
+ }
+ throw new Error('Error fetching source', data)
+ })
+ .then((data) => data.json())
+ .then((data) => parseSource(data))
+}
+
+const fetchStatusHistory = ({ status, credentials }) => {
+ const url = MASTODON_STATUS_HISTORY_URL(status.id)
+ return promisedRequest({ url, credentials })
+ .then((data) => {
+ data.reverse()
+ return data.map((item) => {
+ item.originalStatus = status
+ return parseStatus(item)
+ })
+ })
+}
+
const tagUser = ({ tag, credentials, user }) => {
const screenName = user.screen_name
const form = {
@@ -825,6 +860,54 @@ const postStatus = ({
.then((data) => data.error ? data : parseStatus(data))
}
+const editStatus = ({
+ id,
+ credentials,
+ status,
+ spoilerText,
+ sensitive,
+ poll,
+ mediaIds = [],
+ contentType
+}) => {
+ const form = new FormData()
+ const pollOptions = poll.options || []
+
+ form.append('status', status)
+ if (spoilerText) form.append('spoiler_text', spoilerText)
+ if (sensitive) form.append('sensitive', sensitive)
+ if (contentType) form.append('content_type', contentType)
+ mediaIds.forEach(val => {
+ form.append('media_ids[]', val)
+ })
+
+ if (pollOptions.some(option => option !== '')) {
+ const normalizedPoll = {
+ expires_in: poll.expiresIn,
+ multiple: poll.multiple
+ }
+ Object.keys(normalizedPoll).forEach(key => {
+ form.append(`poll[${key}]`, normalizedPoll[key])
+ })
+
+ pollOptions.forEach(option => {
+ form.append('poll[options][]', option)
+ })
+ }
+
+ const putHeaders = authHeaders(credentials)
+
+ return fetch(MASTODON_STATUS_URL(id), {
+ body: form,
+ method: 'PUT',
+ headers: putHeaders
+ })
+ .then((response) => {
+ return response.json()
+ })
+ .then((data) => data.error ? data : parseStatus(data))
+}
+
const deleteStatus = ({ id, credentials }) => {
return fetch(MASTODON_DELETE_URL(id), {
headers: authHeaders(credentials),
@@ -1291,7 +1374,8 @@ const MASTODON_STREAMING_EVENTS = new Set([
'update',
'notification',
'delete',
- 'filters_changed'
+ 'filters_changed',
+ 'status.update'
])
const PLEROMA_STREAMING_EVENTS = new Set([
@@ -1363,6 +1447,8 @@ export const handleMastoWS = (wsEvent) => {
const data = payload ? JSON.parse(payload) : null
if (event === 'update') {
return { event, status: parseStatus(data) }
+ } else if (event === 'status.update') {
+ return { event, status: parseStatus(data) }
} else if (event === 'notification') {
return { event, notification: parseNotification(data) }
} else if (event === 'pleroma:chat_update') {
@@ -1497,6 +1583,8 @@ const apiService = {
fetchPinnedStatuses,
fetchConversation,
fetchStatus,
+ fetchStatusSource,
+ fetchStatusHistory,
fetchFriends,
exportFriends,
fetchFollowers,
@@ -1508,6 +1596,7 @@ const apiService = {
unmuteConversation,
blockUser,
unblockUser,
+ removeUserFromFollowers,
fetchUser,
fetchUserByName,
fetchUserRelationship,
@@ -1518,6 +1607,7 @@ const apiService = {
bookmarkStatus,
unbookmarkStatus,
postStatus,
+ editStatus,
deleteStatus,
uploadMedia,
setMediaDescription,
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 451427da..23061eba 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -251,6 +251,16 @@ export const parseAttachment = (data) => {
return output
}
+export const parseSource = (data) => {
+ const output = {}
+
+ output.text = data.text
+ output.spoiler_text = data.spoiler_text
+ output.content_type = data.content_type
+
+ return output
+}
+
export const parseStatus = (data) => {
const output = {}
const masto = Object.prototype.hasOwnProperty.call(data, 'account')
@@ -272,6 +282,8 @@ export const parseStatus = (data) => {
output.tags = data.tags
+ output.edited_at = data.edited_at
+
if (data.pleroma) {
const { pleroma } = data
output.text = pleroma.content ? data.pleroma.content['text/plain'] : data.content
@@ -373,6 +385,10 @@ export const parseStatus = (data) => {
output.favoritedBy = []
output.rebloggedBy = []
+ if (Object.prototype.hasOwnProperty.call(data, 'originalStatus')) {
+ Object.assign(output, data.originalStatus)
+ }
+
return output
}
diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js
index f09196aa..1eb10bb6 100644
--- a/src/services/status_poster/status_poster.service.js
+++ b/src/services/status_poster/status_poster.service.js
@@ -47,6 +47,47 @@ const postStatus = ({
})
}
+const editStatus = ({
+ store,
+ statusId,
+ status,
+ spoilerText,
+ sensitive,
+ poll,
+ media = [],
+ contentType = 'text/plain'
+}) => {
+ const mediaIds = map(media, 'id')
+
+ return apiService.editStatus({
+ id: statusId,
+ credentials: store.state.users.currentUser.credentials,
+ status,
+ spoilerText,
+ sensitive,
+ poll,
+ mediaIds,
+ contentType
+ })
+ .then((data) => {
+ if (!data.error) {
+ store.dispatch('addNewStatuses', {
+ statuses: [data],
+ timeline: 'friends',
+ showImmediately: true,
+ noIdUpdate: true // To prevent missing notices on next pull.
+ })
+ }
+ return data
+ })
+ .catch((err) => {
+ console.error('Error editing status', err)
+ return {
+ error: err.message
+ }
+ })
+}
+
const uploadMedia = ({ store, formData }) => {
const credentials = store.state.users.currentUser.credentials
return apiService.uploadMedia({ credentials, formData })
@@ -59,6 +100,7 @@ const setMediaDescription = ({ store, id, description }) => {
const statusPosterService = {
postStatus,
+ editStatus,
uploadMedia,
setMediaDescription
}
diff --git a/static/emoji.json b/static/emoji.json
deleted file mode 100644
index 12b91b3f..00000000
--- a/static/emoji.json
+++ /dev/null
@@ -1,1431 +0,0 @@
-{
- "100": "๐Ÿ’ฏ",
- "1234": "๐Ÿ”ข",
- "1st_place_medal": "๐Ÿฅ‡",
- "2nd_place_medal": "๐Ÿฅˆ",
- "3rd_place_medal": "๐Ÿฅ‰",
- "8ball": "๐ŸŽฑ",
- "a_button_blood_type": "๐Ÿ…ฐ",
- "ab": "๐Ÿ†Ž",
- "abacus": "๐Ÿงฎ",
- "abc": "๐Ÿ”ค",
- "abcd": "๐Ÿ”ก",
- "accept": "๐Ÿ‰‘",
- "adhesive_bandage": "๐Ÿฉน",
- "admission_tickets": "๐ŸŽŸ",
- "adult": "๐Ÿง‘",
- "aerial_tramway": "๐Ÿšก",
- "airplane": "โœˆ",
- "airplane_arriving": "๐Ÿ›ฌ",
- "airplane_departure": "๐Ÿ›ซ",
- "alarm_clock": "โฐ",
- "alembic": "โš—๏ธ",
- "alien": "๐Ÿ‘ฝ",
- "ambulance": "๐Ÿš‘",
- "amphora": "๐Ÿบ",
- "anchor": "โš“",
- "angel": "๐Ÿ‘ผ",
- "anger": "๐Ÿ’ข",
- "anger_right": "๐Ÿ—ฏ",
- "angry": "๐Ÿ˜ ",
- "anguished": "๐Ÿ˜ง",
- "ant": "๐Ÿœ",
- "apple": "๐ŸŽ",
- "aquarius": "โ™’",
- "aries": "โ™ˆ",
- "arrow_backward": "โ—€๏ธ",
- "arrow_double_down": "โฌ",
- "arrow_double_up": "โซ",
- "arrow_down": "โฌ‡๏ธ",
- "arrow_down_small": "๐Ÿ”ฝ",
- "arrow_forward": "โ–ถ๏ธ",
- "arrow_heading_down": "โคต๏ธ",
- "arrow_heading_up": "โคด๏ธ",
- "arrow_left": "โฌ…๏ธ",
- "arrow_lower_left": "โ†™๏ธ",
- "arrow_lower_right": "โ†˜๏ธ",
- "arrow_right": "โžก",
- "arrow_right_hook": "โ†ช๏ธ",
- "arrow_up": "โฌ†๏ธ",
- "arrow_up_down": "โ†•",
- "arrow_up_small": "๐Ÿ”ผ",
- "arrow_upper_left": "โ†–",
- "arrow_upper_right": "โ†—๏ธ",
- "arrows_clockwise": "๐Ÿ”ƒ",
- "arrows_counterclockwise": "๐Ÿ”„",
- "art": "๐ŸŽจ",
- "articulated_lorry": "๐Ÿš›",
- "artist_palette": "๐ŸŽจ",
- "asterisk": "*โƒฃ",
- "astonished": "๐Ÿ˜ฒ",
- "athletic_shoe": "๐Ÿ‘Ÿ",
- "atm": "๐Ÿง",
- "atom": "โš›",
- "atom_symbol": "โš›๏ธ",
- "auto_rickshaw": "๐Ÿ›บ",
- "automobile": "๐Ÿš—",
- "avocado": "๐Ÿฅ‘",
- "axe": "๐Ÿช“",
- "b_button_blood_type": "๐Ÿ…ฑ",
- "baby": "๐Ÿ‘ถ",
- "baby_bottle": "๐Ÿผ",
- "baby_chick": "๐Ÿค",
- "baby_symbol": "๐Ÿšผ",
- "back": "๐Ÿ”™",
- "bacon": "๐Ÿฅ“",
- "badger": "๐Ÿฆก",
- "badminton": "๐Ÿธ",
- "bagel": "๐Ÿฅฏ",
- "baggage_claim": "๐Ÿ›„",
- "baguette_bread": "๐Ÿฅ–",
- "balance_scale": "โš–๏ธ",
- "bald": "๐Ÿฆฒ",
- "ballet_shoes": "๐Ÿฉฐ",
- "balloon": "๐ŸŽˆ",
- "ballot_box": "๐Ÿ—ณ",
- "ballot_box_with_check": "โ˜‘๏ธ",
- "bamboo": "๐ŸŽ",
- "banana": "๐ŸŒ",
- "bangbang": "โ€ผ๏ธ",
- "banjo": "๐Ÿช•",
- "bank": "๐Ÿฆ",
- "bar_chart": "๐Ÿ“Š",
- "barber": "๐Ÿ’ˆ",
- "baseball": "โšพ",
- "basket": "๐Ÿงบ",
- "basketball": "๐Ÿ€",
- "basketballer": "โ›น",
- "bat": "๐Ÿฆ‡",
- "bath": "๐Ÿ›€",
- "bathtub": "๐Ÿ›",
- "battery": "๐Ÿ”‹",
- "beach_umbrella": "โ›ฑ",
- "beach_with_umbrella": "๐Ÿ–",
- "bear": "๐Ÿป",
- "beard": "๐Ÿง”",
- "bearded_person": "๐Ÿง”",
- "bed": "๐Ÿ›",
- "bee": "๐Ÿ",
- "beer": "๐Ÿบ",
- "beers": "๐Ÿป",
- "beetle": "๐Ÿž",
- "beginner": "๐Ÿ”ฐ",
- "bell": "๐Ÿ””",
- "bellhop_bell": "๐Ÿ›Ž",
- "bento": "๐Ÿฑ",
- "beverage_box": "๐Ÿงƒ",
- "bicyclist": "๐Ÿšด",
- "bike": "๐Ÿšฒ",
- "bikini": "๐Ÿ‘™",
- "billed_cap": "๐Ÿงข",
- "biohazard": "โ˜ฃ๏ธ",
- "bird": "๐Ÿฆ",
- "birthday": "๐ŸŽ‚",
- "black_circle": "โšซ",
- "black_heart": "๐Ÿ–ค",
- "black_joker": "๐Ÿƒ",
- "black_large_square": "โฌ›",
- "black_medium_small_square": "โ—พ",
- "black_medium_square": "โ—ผ",
- "black_nib": "โœ’๏ธ",
- "black_small_square": "โ–ช",
- "black_square_button": "๐Ÿ”ฒ",
- "blond_haired_person": "๐Ÿ‘ฑ",
- "blossom": "๐ŸŒผ",
- "blowfish": "๐Ÿก",
- "blue_book": "๐Ÿ“˜",
- "blue_car": "๐Ÿš™",
- "blue_circle": "๐Ÿ”ต",
- "blue_heart": "๐Ÿ’™",
- "blue_square": "๐ŸŸฆ",
- "blush": "๐Ÿ˜Š",
- "boar": "๐Ÿ—",
- "bomb": "๐Ÿ’ฃ",
- "bone": "๐Ÿฆด",
- "book": "๐Ÿ“–",
- "bookmark": "๐Ÿ”–",
- "bookmark_tabs": "๐Ÿ“‘",
- "books": "๐Ÿ“š",
- "boom": "๐Ÿ’ฅ",
- "boot": "๐Ÿ‘ข",
- "bouquet": "๐Ÿ’",
- "bow": "๐Ÿ™‡",
- "bow_and_arrow": "๐Ÿน",
- "bowl_with_spoon": "๐Ÿฅฃ",
- "bowling": "๐ŸŽณ",
- "boxing_glove": "๐ŸฅŠ",
- "boy": "๐Ÿ‘ฆ",
- "brain": "๐Ÿง ",
- "bread": "๐Ÿž",
- "breast_feeding": "๐Ÿคฑ",
- "breastfeeding": "๐Ÿคฑ",
- "brick": "๐Ÿงฑ",
- "bride_with_veil": "๐Ÿ‘ฐ",
- "bridge_at_night": "๐ŸŒ‰",
- "briefcase": "๐Ÿ’ผ",
- "briefs": "๐Ÿฉฒ",
- "broccoli": "๐Ÿฅฆ",
- "broken_heart": "๐Ÿ’”",
- "broom": "๐Ÿงน",
- "brown_circle": "๐ŸŸค",
- "brown_heart": "๐ŸคŽ",
- "bug": "๐Ÿ›",
- "building_construction": "๐Ÿ—",
- "bulb": "๐Ÿ’ก",
- "bullettrain_front": "๐Ÿš…",
- "bullettrain_side": "๐Ÿš„",
- "burrito": "๐ŸŒฏ",
- "bus": "๐ŸšŒ",
- "busstop": "๐Ÿš",
- "bust_in_silhouette": "๐Ÿ‘ค",
- "busts_in_silhouette": "๐Ÿ‘ฅ",
- "butter": "๐Ÿงˆ",
- "butterfly": "๐Ÿฆ‹",
- "cactus": "๐ŸŒต",
- "cake": "๐Ÿฐ",
- "calendar": "๐Ÿ“†",
- "call_me": "๐Ÿค™",
- "call_me_hand": "๐Ÿค™",
- "calling": "๐Ÿ“ฒ",
- "camel": "๐Ÿซ",
- "camera": "๐Ÿ“ท",
- "camera_with_flash": "๐Ÿ“ธ",
- "camping": "๐Ÿ•",
- "cancer": "โ™‹",
- "candle": "๐Ÿ•ฏ",
- "candy": "๐Ÿฌ",
- "canned_food": "๐Ÿฅซ",
- "canoe": "๐Ÿ›ถ",
- "capital_abcd": "๐Ÿ” ",
- "capricorn": "โ™‘",
- "card_file_box": "๐Ÿ—ƒ",
- "card_index": "๐Ÿ“‡",
- "card_index_dividers": "๐Ÿ—‚",
- "carousel_horse": "๐ŸŽ ",
- "carrot": "๐Ÿฅ•",
- "cat": "๐Ÿฑ",
- "cat2": "๐Ÿˆ",
- "cd": "๐Ÿ’ฟ",
- "chains": "โ›“๏ธ",
- "chair": "๐Ÿช‘",
- "champagne": "๐Ÿพ",
- "champagne_glass": "๐Ÿฅ‚",
- "chart": "๐Ÿ’น",
- "chart_with_downwards_trend": "๐Ÿ“‰",
- "chart_with_upwards_trend": "๐Ÿ“ˆ",
- "check_box_with_check": "โ˜‘",
- "check_mark": "โœ”",
- "checkered_flag": "๐Ÿ",
- "cheese": "๐Ÿง€",
- "cheese_wedge": "๐Ÿง€",
- "cherries": "๐Ÿ’",
- "cherry_blossom": "๐ŸŒธ",
- "chess_pawn": "โ™Ÿ",
- "chestnut": "๐ŸŒฐ",
- "chicken": "๐Ÿ”",
- "child": "๐Ÿง’",
- "children_crossing": "๐Ÿšธ",
- "chipmunk": "๐Ÿฟ",
- "chocolate_bar": "๐Ÿซ",
- "chopsticks": "๐Ÿฅข",
- "christmas_tree": "๐ŸŽ„",
- "church": "โ›ช",
- "cinema": "๐ŸŽฆ",
- "circled_m": "โ“‚",
- "circus_tent": "๐ŸŽช",
- "city_dusk": "๐ŸŒ†",
- "city_sunset": "๐ŸŒ‡",
- "cityscape": "๐Ÿ™",
- "cityscape_at_dusk": "๐ŸŒ†",
- "cl": "๐Ÿ†‘",
- "clap": "๐Ÿ‘",
- "clapper": "๐ŸŽฌ",
- "classical_building": "๐Ÿ›",
- "clinking_glasses": "๐Ÿฅ‚",
- "clipboard": "๐Ÿ“‹",
- "clock1": "๐Ÿ•",
- "clock10": "๐Ÿ•™",
- "clock1030": "๐Ÿ•ฅ",
- "clock11": "๐Ÿ•š",
- "clock1130": "๐Ÿ•ฆ",
- "clock12": "๐Ÿ•›",
- "clock1230": "๐Ÿ•ง",
- "clock130": "๐Ÿ•œ",
- "clock2": "๐Ÿ•‘",
- "clock230": "๐Ÿ•",
- "clock3": "๐Ÿ•’",
- "clock330": "๐Ÿ•ž",
- "clock4": "๐Ÿ•“",
- "clock430": "๐Ÿ•Ÿ",
- "clock5": "๐Ÿ•”",
- "clock530": "๐Ÿ• ",
- "clock6": "๐Ÿ••",
- "clock630": "๐Ÿ•ก",
- "clock7": "๐Ÿ•–",
- "clock730": "๐Ÿ•ข",
- "clock8": "๐Ÿ•—",
- "clock830": "๐Ÿ•ฃ",
- "clock9": "๐Ÿ•˜",
- "clock930": "๐Ÿ•ค",
- "closed_book": "๐Ÿ“•",
- "closed_lock_with_key": "๐Ÿ”",
- "closed_umbrella": "๐ŸŒ‚",
- "cloud": "โ˜๏ธ",
- "cloud_with_lightning": "๐ŸŒฉ",
- "cloud_with_lightning_and_rain": "โ›ˆ๏ธ",
- "cloud_with_rain": "๐ŸŒง",
- "cloud_with_snow": "๐ŸŒจ",
- "clown": "๐Ÿคก",
- "clown_face": "๐Ÿคก",
- "club_suit": "โ™ฃ๏ธ",
- "clubs": "โ™ฃ",
- "coat": "๐Ÿงฅ",
- "cocktail": "๐Ÿธ",
- "coconut": "๐Ÿฅฅ",
- "coffee": "โ˜•",
- "coffin": "โšฐ๏ธ",
- "cold_face": "๐Ÿฅถ",
- "cold_sweat": "๐Ÿ˜ฐ",
- "comet": "โ˜„๏ธ",
- "compass": "๐Ÿงญ",
- "compression": "๐Ÿ—œ",
- "computer": "๐Ÿ’ป",
- "computer_mouse": "๐Ÿ–ฑ",
- "confetti_ball": "๐ŸŽŠ",
- "confounded": "๐Ÿ˜–",
- "confused": "๐Ÿ˜•",
- "congratulations": "ใŠ—",
- "construction": "๐Ÿšง",
- "construction_worker": "๐Ÿ‘ท",
- "control_knobs": "๐ŸŽ›",
- "convenience_store": "๐Ÿช",
- "cookie": "๐Ÿช",
- "cooking": "๐Ÿณ",
- "cool": "๐Ÿ†’",
- "cop": "๐Ÿ‘ฎ",
- "copyright": "ยฉ",
- "corn": "๐ŸŒฝ",
- "couch_and_lamp": "๐Ÿ›‹",
- "couple": "๐Ÿ‘ซ",
- "couple_with_heart": "๐Ÿ’‘",
- "couplekiss": "๐Ÿ’",
- "cow": "๐Ÿฎ",
- "cow2": "๐Ÿ„",
- "cowboy": "๐Ÿค ",
- "cowboy_hat_face": "๐Ÿค ",
- "crab": "๐Ÿฆ€",
- "crayon": "๐Ÿ–",
- "crazy_face": "๐Ÿคช",
- "credit_card": "๐Ÿ’ณ",
- "crescent_moon": "๐ŸŒ™",
- "cricket": "๐Ÿฆ—",
- "cricket_game": "๐Ÿ",
- "crocodile": "๐ŸŠ",
- "croissant": "๐Ÿฅ",
- "cross": "โœ๏ธ",
- "crossed_fingers": "๐Ÿคž",
- "crossed_flags": "๐ŸŽŒ",
- "crossed_swords": "โš”๏ธ",
- "crown": "๐Ÿ‘‘",
- "cry": "๐Ÿ˜ข",
- "crying_cat_face": "๐Ÿ˜ฟ",
- "crystal_ball": "๐Ÿ”ฎ",
- "cucumber": "๐Ÿฅ’",
- "cup_with_straw": "๐Ÿฅค",
- "cupcake": "๐Ÿง",
- "cupid": "๐Ÿ’˜",
- "curling_stone": "๐ŸฅŒ",
- "curly_hair": "๐Ÿฆฑ",
- "curly_loop": "โžฐ",
- "currency_exchange": "๐Ÿ’ฑ",
- "curry": "๐Ÿ›",
- "custard": "๐Ÿฎ",
- "customs": "๐Ÿ›ƒ",
- "cut_of_meat": "๐Ÿฅฉ",
- "cyclone": "๐ŸŒ€",
- "dagger": "๐Ÿ—ก",
- "dancer": "๐Ÿ’ƒ",
- "dancers": "๐Ÿ‘ฏ",
- "dango": "๐Ÿก",
- "dark_skin_tone": "๐Ÿฟ",
- "dark_sunglasses": "๐Ÿ•ถ",
- "dart": "๐ŸŽฏ",
- "dash": "๐Ÿ’จ",
- "date": "๐Ÿ“…",
- "deaf_person": "๐Ÿง",
- "deciduous_tree": "๐ŸŒณ",
- "deer": "๐ŸฆŒ",
- "department_store": "๐Ÿฌ",
- "derelict_house": "๐Ÿš",
- "desert": "๐Ÿœ",
- "desert_island": "๐Ÿ",
- "desktop_computer": "๐Ÿ–ฅ",
- "detective": "๐Ÿ•ต",
- "diamond_shape_with_a_dot_inside": "๐Ÿ’ ",
- "diamond_suit": "โ™ฆ๏ธ",
- "diamonds": "โ™ฆ",
- "disappointed": "๐Ÿ˜ž",
- "disappointed_relieved": "๐Ÿ˜ฅ",
- "diving_mask": "๐Ÿคฟ",
- "diya_lamp": "๐Ÿช”",
- "dizzy": "๐Ÿ’ซ",
- "dizzy_face": "๐Ÿ˜ต",
- "dna": "๐Ÿงฌ",
- "do_not_litter": "๐Ÿšฏ",
- "dog": "๐Ÿถ",
- "dog2": "๐Ÿ•",
- "dollar": "๐Ÿ’ต",
- "dolls": "๐ŸŽŽ",
- "dolphin": "๐Ÿฌ",
- "door": "๐Ÿšช",
- "double_exclamation_mark": "โ€ผ",
- "doughnut": "๐Ÿฉ",
- "dove": "๐Ÿ•Š",
- "down_arrow": "โฌ‡",
- "downleft_arrow": "โ†™",
- "downright_arrow": "โ†˜",
- "dragon": "๐Ÿ‰",
- "dragon_face": "๐Ÿฒ",
- "dress": "๐Ÿ‘—",
- "dromedary_camel": "๐Ÿช",
- "drooling_face": "๐Ÿคค",
- "drop_of_blood": "๐Ÿฉธ",
- "droplet": "๐Ÿ’ง",
- "drum": "๐Ÿฅ",
- "duck": "๐Ÿฆ†",
- "dumpling": "๐ŸฅŸ",
- "dvd": "๐Ÿ“€",
- "e-mail": "๐Ÿ“ง",
- "eagle": "๐Ÿฆ…",
- "ear": "๐Ÿ‘‚",
- "ear_of_rice": "๐ŸŒพ",
- "ear_with_hearing_aid": "๐Ÿฆป",
- "earth_africa": "๐ŸŒ",
- "earth_americas": "๐ŸŒŽ",
- "earth_asia": "๐ŸŒ",
- "egg": "๐Ÿฅš",
- "eggplant": "๐Ÿ†",
- "eight": "8โƒฃ",
- "eight_pointed_black_star": "โœด๏ธ",
- "eight_spoked_asterisk": "โœณ๏ธ",
- "eightpointed_star": "โœด",
- "eightspoked_asterisk": "โœณ",
- "eject_button": "โ",
- "electric_plug": "๐Ÿ”Œ",
- "elephant": "๐Ÿ˜",
- "elf": "๐Ÿง",
- "end": "๐Ÿ”š",
- "envelope": "โœ‰",
- "envelope_with_arrow": "๐Ÿ“ฉ",
- "euro": "๐Ÿ’ถ",
- "european_castle": "๐Ÿฐ",
- "european_post_office": "๐Ÿค",
- "evergreen_tree": "๐ŸŒฒ",
- "exclamation": "โ—",
- "exclamation_question_mark": "โ‰",
- "exploding_head": "๐Ÿคฏ",
- "expressionless": "๐Ÿ˜‘",
- "eye": "๐Ÿ‘",
- "eyeglasses": "๐Ÿ‘“",
- "eyes": "๐Ÿ‘€",
- "face_vomiting": "๐Ÿคฎ",
- "face_with_hand_over_mouth": "๐Ÿคญ",
- "face_with_headbandage": "๐Ÿค•",
- "face_with_monocle": "๐Ÿง",
- "face_with_raised_eyebrow": "๐Ÿคจ",
- "face_with_symbols_on_mouth": "๐Ÿคฌ",
- "face_with_symbols_over_mouth": "๐Ÿคฌ",
- "face_with_thermometer": "๐Ÿค’",
- "factory": "๐Ÿญ",
- "fairy": "๐Ÿงš",
- "falafel": "๐Ÿง†",
- "fallen_leaf": "๐Ÿ‚",
- "family": "๐Ÿ‘ช",
- "fast_forward": "โฉ",
- "fax": "๐Ÿ“ ",
- "fearful": "๐Ÿ˜จ",
- "feet": "๐Ÿพ",
- "female_sign": "โ™€",
- "ferris_wheel": "๐ŸŽก",
- "ferry": "โ›ด๏ธ",
- "field_hockey": "๐Ÿ‘",
- "file_cabinet": "๐Ÿ—„",
- "file_folder": "๐Ÿ“",
- "film_frames": "๐ŸŽž",
- "film_projector": "๐Ÿ“ฝ",
- "fingers_crossed": "๐Ÿคž",
- "fire": "๐Ÿ”ฅ",
- "fire_engine": "๐Ÿš’",
- "fire_extinguisher": "๐Ÿงฏ",
- "firecracker": "๐Ÿงจ",
- "fireworks": "๐ŸŽ†",
- "first_place": "๐Ÿฅ‡",
- "first_quarter_moon": "๐ŸŒ“",
- "first_quarter_moon_with_face": "๐ŸŒ›",
- "fish": "๐ŸŸ",
- "fish_cake": "๐Ÿฅ",
- "fishing_pole_and_fish": "๐ŸŽฃ",
- "fist": "โœŠ",
- "five": "5โƒฃ",
- "flag_black": "๐Ÿด",
- "flag_white": "๐Ÿณ",
- "flags": "๐ŸŽ",
- "flamingo": "๐Ÿฆฉ",
- "flashlight": "๐Ÿ”ฆ",
- "flat_shoe": "๐Ÿฅฟ",
- "fleur-de-lis": "โšœ",
- "fleurde-lis": "โšœ๏ธ",
- "floppy_disk": "๐Ÿ’พ",
- "flower_playing_cards": "๐ŸŽด",
- "flushed": "๐Ÿ˜ณ",
- "flying_disc": "๐Ÿฅ",
- "flying_saucer": "๐Ÿ›ธ",
- "fog": "๐ŸŒซ",
- "foggy": "๐ŸŒ",
- "foot": "๐Ÿฆถ",
- "football": "๐Ÿˆ",
- "footprints": "๐Ÿ‘ฃ",
- "fork_and_knife": "๐Ÿด",
- "fork_and_knife_with_plate": "๐Ÿฝ",
- "fortune_cookie": "๐Ÿฅ ",
- "fountain": "โ›ฒ",
- "fountain_pen": "๐Ÿ–‹",
- "four": "4โƒฃ",
- "four_leaf_clover": "๐Ÿ€",
- "fox": "๐ŸฆŠ",
- "framed_picture": "๐Ÿ–ผ",
- "free": "๐Ÿ†“",
- "french_bread": "๐Ÿฅ–",
- "fried_shrimp": "๐Ÿค",
- "fries": "๐ŸŸ",
- "frog": "๐Ÿธ",
- "frowning": "๐Ÿ˜ฆ",
- "frowning_face": "โ˜น๏ธ",
- "fuelpump": "โ›ฝ",
- "full_moon": "๐ŸŒ•",
- "full_moon_with_face": "๐ŸŒ",
- "funeral_urn": "โšฑ๏ธ",
- "game_die": "๐ŸŽฒ",
- "garlic": "๐Ÿง„",
- "gear": "โš™๏ธ",
- "gem": "๐Ÿ’Ž",
- "gemini": "โ™Š",
- "genie": "๐Ÿงž",
- "ghost": "๐Ÿ‘ป",
- "gift": "๐ŸŽ",
- "gift_heart": "๐Ÿ’",
- "giraffe": "๐Ÿฆ’",
- "girl": "๐Ÿ‘ง",
- "glass_of_milk": "๐Ÿฅ›",
- "globe_with_meridians": "๐ŸŒ",
- "gloves": "๐Ÿงค",
- "goal": "๐Ÿฅ…",
- "goal_net": "๐Ÿฅ…",
- "goat": "๐Ÿ",
- "goggles": "๐Ÿฅฝ",
- "golf": "โ›ณ",
- "golfer": "๐ŸŒ",
- "gorilla": "๐Ÿฆ",
- "grapes": "๐Ÿ‡",
- "green_apple": "๐Ÿ",
- "green_book": "๐Ÿ“—",
- "green_circle": "๐ŸŸข",
- "green_heart": "๐Ÿ’š",
- "green_salad": "๐Ÿฅ—",
- "green_square": "๐ŸŸฉ",
- "grey_exclamation": "โ•",
- "grey_question": "โ”",
- "grimacing": "๐Ÿ˜ฌ",
- "grin": "๐Ÿ˜",
- "grinning": "๐Ÿ˜€",
- "guard": "๐Ÿ’‚",
- "guardsman": "๐Ÿ’‚",
- "guide_dog": "๐Ÿฆฎ",
- "guitar": "๐ŸŽธ",
- "gun": "๐Ÿ”ซ",
- "haircut": "๐Ÿ’‡",
- "hamburger": "๐Ÿ”",
- "hammer": "๐Ÿ”จ",
- "hammer_and_pick": "โš’๏ธ",
- "hammer_and_wrench": "๐Ÿ› ",
- "hamster": "๐Ÿน",
- "hand_with_fingers_splayed": "๐Ÿ–",
- "handbag": "๐Ÿ‘œ",
- "handshake": "๐Ÿค",
- "hash": "#โƒฃ",
- "hatched_chick": "๐Ÿฅ",
- "hatching_chick": "๐Ÿฃ",
- "head_bandage": "๐Ÿค•",
- "headphones": "๐ŸŽง",
- "hear_no_evil": "๐Ÿ™‰",
- "heart": "โค๏ธ",
- "heart_decoration": "๐Ÿ’Ÿ",
- "heart_exclamation": "โฃ",
- "heart_eyes": "๐Ÿ˜",
- "heart_eyes_cat": "๐Ÿ˜ป",
- "heart_suit": "โ™ฅ๏ธ",
- "heartbeat": "๐Ÿ’“",
- "heartpulse": "๐Ÿ’—",
- "hearts": "โ™ฅ",
- "heavy_check_mark": "โœ”๏ธ",
- "heavy_division_sign": "โž—",
- "heavy_dollar_sign": "๐Ÿ’ฒ",
- "heavy_minus_sign": "โž–",
- "heavy_multiplication_x": "โœ–๏ธ",
- "heavy_plus_sign": "โž•",
- "hedgehog": "๐Ÿฆ”",
- "helicopter": "๐Ÿš",
- "herb": "๐ŸŒฟ",
- "hibiscus": "๐ŸŒบ",
- "high_brightness": "๐Ÿ”†",
- "high_heel": "๐Ÿ‘ ",
- "hiking_boot": "๐Ÿฅพ",
- "hindu_temple": "๐Ÿ›•",
- "hippopotamus": "๐Ÿฆ›",
- "hockey": "๐Ÿ’",
- "hole": "๐Ÿ•ณ",
- "honey_pot": "๐Ÿฏ",
- "horse": "๐Ÿด",
- "horse_racing": "๐Ÿ‡",
- "hospital": "๐Ÿฅ",
- "hot_face": "๐Ÿฅต",
- "hot_pepper": "๐ŸŒถ",
- "hot_springs": "โ™จ",
- "hotdog": "๐ŸŒญ",
- "hotel": "๐Ÿจ",
- "hotsprings": "โ™จ๏ธ",
- "hourglass": "โŒ›",
- "hourglass_flowing_sand": "โณ",
- "house": "๐Ÿ ",
- "house_with_garden": "๐Ÿก",
- "houses": "๐Ÿ˜",
- "hugging": "๐Ÿค—",
- "hundred_points": "๐Ÿ’ฏ",
- "hushed": "๐Ÿ˜ฏ",
- "ice": "๐ŸงŠ",
- "ice_cream": "๐Ÿจ",
- "ice_hockey": "๐Ÿ’",
- "ice_skate": "โ›ธ๏ธ",
- "icecream": "๐Ÿฆ",
- "id": "๐Ÿ†”",
- "ideograph_advantage": "๐Ÿ‰",
- "imp": "๐Ÿ‘ฟ",
- "inbox_tray": "๐Ÿ“ฅ",
- "incoming_envelope": "๐Ÿ“จ",
- "index_pointing_up": "โ˜",
- "infinity": "โ™พ",
- "information": "โ„น๏ธ",
- "information_desk_person": "๐Ÿ’",
- "information_source": "โ„น",
- "innocent": "๐Ÿ˜‡",
- "input_numbers": "๐Ÿ”ข",
- "interrobang": "โ‰๏ธ",
- "iphone": "๐Ÿ“ฑ",
- "izakaya_lantern": "๐Ÿฎ",
- "jack_o_lantern": "๐ŸŽƒ",
- "japan": "๐Ÿ—พ",
- "japanese_castle": "๐Ÿฏ",
- "japanese_congratulations_button": "ใŠ—๏ธ",
- "japanese_free_of_charge_button": "๐Ÿˆš",
- "japanese_goblin": "๐Ÿ‘บ",
- "japanese_ogre": "๐Ÿ‘น",
- "japanese_reserved_button": "๐Ÿˆฏ",
- "japanese_secret_button": "ใŠ™๏ธ",
- "japanese_service_charge_button": "๐Ÿˆ‚",
- "jeans": "๐Ÿ‘–",
- "joy": "๐Ÿ˜‚",
- "joy_cat": "๐Ÿ˜น",
- "joystick": "๐Ÿ•น",
- "kaaba": "๐Ÿ•‹",
- "kangaroo": "๐Ÿฆ˜",
- "key": "๐Ÿ”‘",
- "keyboard": "โŒจ๏ธ",
- "keycap_ten": "๐Ÿ”Ÿ",
- "kick_scooter": "๐Ÿ›ด",
- "kimono": "๐Ÿ‘˜",
- "kiss": "๐Ÿ’‹",
- "kissing": "๐Ÿ˜—",
- "kissing_cat": "๐Ÿ˜ฝ",
- "kissing_closed_eyes": "๐Ÿ˜š",
- "kissing_heart": "๐Ÿ˜˜",
- "kissing_smiling_eyes": "๐Ÿ˜™",
- "kitchen_knife": "๐Ÿ”ช",
- "kite": "๐Ÿช",
- "kiwi": "๐Ÿฅ",
- "kiwi_fruit": "๐Ÿฅ",
- "knife": "๐Ÿ”ช",
- "koala": "๐Ÿจ",
- "koko": "๐Ÿˆ",
- "lab_coat": "๐Ÿฅผ",
- "label": "๐Ÿท",
- "lacrosse": "๐Ÿฅ",
- "large_blue_diamond": "๐Ÿ”ท",
- "large_orange_diamond": "๐Ÿ”ถ",
- "last_quarter_moon": "๐ŸŒ—",
- "last_quarter_moon_with_face": "๐ŸŒœ",
- "last_track_button": "โฎ๏ธ",
- "latin_cross": "โœ",
- "laughing": "๐Ÿ˜†",
- "leafy_green": "๐Ÿฅฌ",
- "leaves": "๐Ÿƒ",
- "ledger": "๐Ÿ“’",
- "left_arrow": "โฌ…",
- "left_arrow_curving_right": "โ†ช",
- "left_facing_fist": "๐Ÿค›",
- "left_luggage": "๐Ÿ›…",
- "left_right_arrow": "โ†”",
- "leftfacing_fist": "๐Ÿค›",
- "leftright_arrow": "โ†”๏ธ",
- "leftwards_arrow_with_hook": "โ†ฉ๏ธ",
- "leg": "๐Ÿฆต",
- "lemon": "๐Ÿ‹",
- "leo": "โ™Œ",
- "leopard": "๐Ÿ†",
- "level_slider": "๐ŸŽš",
- "libra": "โ™Ž",
- "light_rail": "๐Ÿšˆ",
- "light_skin_tone": "๐Ÿป",
- "link": "๐Ÿ”—",
- "linked_paperclips": "๐Ÿ–‡",
- "lion_face": "๐Ÿฆ",
- "lips": "๐Ÿ‘„",
- "lipstick": "๐Ÿ’„",
- "lizard": "๐ŸฆŽ",
- "llama": "๐Ÿฆ™",
- "lobster": "๐Ÿฆž",
- "lock": "๐Ÿ”’",
- "lock_with_ink_pen": "๐Ÿ”",
- "lollipop": "๐Ÿญ",
- "loop": "โžฟ",
- "lotion_bottle": "๐Ÿงด",
- "loud_sound": "๐Ÿ”Š",
- "loudspeaker": "๐Ÿ“ข",
- "love_hotel": "๐Ÿฉ",
- "love_letter": "๐Ÿ’Œ",
- "love_you_gesture": "๐ŸคŸ",
- "loveyou_gesture": "๐ŸคŸ",
- "low_brightness": "๐Ÿ”…",
- "luggage": "๐Ÿงณ",
- "lying_face": "๐Ÿคฅ",
- "m": "โ“‚๏ธ",
- "mag": "๐Ÿ”",
- "mag_right": "๐Ÿ”Ž",
- "mage": "๐Ÿง™",
- "magnet": "๐Ÿงฒ",
- "mahjong": "๐Ÿ€„",
- "mailbox": "๐Ÿ“ซ",
- "mailbox_closed": "๐Ÿ“ช",
- "mailbox_with_mail": "๐Ÿ“ฌ",
- "mailbox_with_no_mail": "๐Ÿ“ญ",
- "male_sign": "โ™‚",
- "man": "๐Ÿ‘จ",
- "man_dancing": "๐Ÿ•บ",
- "man_in_suit": "๐Ÿ•ด",
- "man_in_tuxedo": "๐Ÿคต",
- "man_with_chinese_cap": "๐Ÿ‘ฒ",
- "man_with_gua_pi_mao": "๐Ÿ‘ฒ",
- "man_with_turban": "๐Ÿ‘ณ",
- "mango": "๐Ÿฅญ",
- "mans_shoe": "๐Ÿ‘ž",
- "mantelpiece_clock": "๐Ÿ•ฐ",
- "manual_wheelchair": "๐Ÿฆฝ",
- "maple_leaf": "๐Ÿ",
- "martial_arts_uniform": "๐Ÿฅ‹",
- "mask": "๐Ÿ˜ท",
- "massage": "๐Ÿ’†",
- "mate": "๐Ÿง‰",
- "meat_on_bone": "๐Ÿ–",
- "mechanical_arm": "๐Ÿฆพ",
- "mechanical_leg": "๐Ÿฆฟ",
- "medal": "๐Ÿ…",
- "medical_symbol": "โš•",
- "medium_skin_tone": "๐Ÿฝ",
- "mediumdark_skin_tone": "๐Ÿพ",
- "mediumlight_skin_tone": "๐Ÿผ",
- "mega": "๐Ÿ“ฃ",
- "melon": "๐Ÿˆ",
- "memo": "๐Ÿ“",
- "menorah": "๐Ÿ•Ž",
- "mens": "๐Ÿšน",
- "merperson": "๐Ÿงœ",
- "metal": "๐Ÿค˜",
- "metro": "๐Ÿš‡",
- "microbe": "๐Ÿฆ ",
- "microphone": "๐ŸŽค",
- "microscope": "๐Ÿ”ฌ",
- "middle_finger": "๐Ÿ–•",
- "military_medal": "๐ŸŽ–",
- "milk": "๐Ÿฅ›",
- "milky_way": "๐ŸŒŒ",
- "minibus": "๐Ÿš",
- "minidisc": "๐Ÿ’ฝ",
- "mobile_phone_off": "๐Ÿ“ด",
- "money_mouth": "๐Ÿค‘",
- "money_with_wings": "๐Ÿ’ธ",
- "moneybag": "๐Ÿ’ฐ",
- "moneymouth_face": "๐Ÿค‘",
- "monkey": "๐Ÿ’",
- "monkey_face": "๐Ÿต",
- "monorail": "๐Ÿš",
- "moon_cake": "๐Ÿฅฎ",
- "mortar_board": "๐ŸŽ“",
- "mosque": "๐Ÿ•Œ",
- "mosquito": "๐ŸฆŸ",
- "motor_boat": "๐Ÿ›ฅ",
- "motor_scooter": "๐Ÿ›ต",
- "motorcycle": "๐Ÿ",
- "motorized_wheelchair": "๐Ÿฆผ",
- "motorway": "๐Ÿ›ฃ",
- "mount_fuji": "๐Ÿ—ป",
- "mountain": "โ›ฐ๏ธ",
- "mountain_bicyclist": "๐Ÿšต",
- "mountain_cableway": "๐Ÿš ",
- "mountain_railway": "๐Ÿšž",
- "mouse": "๐Ÿญ",
- "mouse2": "๐Ÿ",
- "movie_camera": "๐ŸŽฅ",
- "moyai": "๐Ÿ—ฟ",
- "mrs_claus": "๐Ÿคถ",
- "multiplication_sign": "โœ–",
- "muscle": "๐Ÿ’ช",
- "mushroom": "๐Ÿ„",
- "musical_keyboard": "๐ŸŽน",
- "musical_note": "๐ŸŽต",
- "musical_score": "๐ŸŽผ",
- "mute": "๐Ÿ”‡",
- "nail_care": "๐Ÿ’…",
- "name_badge": "๐Ÿ“›",
- "national_park": "๐Ÿž",
- "nauseated_face": "๐Ÿคข",
- "nazar_amulet": "๐Ÿงฟ",
- "necktie": "๐Ÿ‘”",
- "negative_squared_cross_mark": "โŽ",
- "nerd": "๐Ÿค“",
- "neutral_face": "๐Ÿ˜",
- "new": "๐Ÿ†•",
- "new_moon": "๐ŸŒ‘",
- "new_moon_with_face": "๐ŸŒš",
- "newspaper": "๐Ÿ“ฐ",
- "next_track_button": "โญ๏ธ",
- "ng": "๐Ÿ†–",
- "night_with_stars": "๐ŸŒƒ",
- "nine": "9โƒฃ",
- "no_bell": "๐Ÿ”•",
- "no_bicycles": "๐Ÿšณ",
- "no_entry": "โ›”",
- "no_entry_sign": "๐Ÿšซ",
- "no_good": "๐Ÿ™…",
- "no_mobile_phones": "๐Ÿ“ต",
- "no_mouth": "๐Ÿ˜ถ",
- "no_pedestrians": "๐Ÿšท",
- "no_smoking": "๐Ÿšญ",
- "non-potable_water": "๐Ÿšฑ",
- "nose": "๐Ÿ‘ƒ",
- "notebook": "๐Ÿ““",
- "notebook_with_decorative_cover": "๐Ÿ“”",
- "notes": "๐ŸŽถ",
- "nut_and_bolt": "๐Ÿ”ฉ",
- "o": "โญ•",
- "o_button_blood_type": "๐Ÿ…พ",
- "ocean": "๐ŸŒŠ",
- "octagonal_sign": "๐Ÿ›‘",
- "octopus": "๐Ÿ™",
- "oden": "๐Ÿข",
- "office": "๐Ÿข",
- "oil_drum": "๐Ÿ›ข",
- "ok": "๐Ÿ†—",
- "ok_hand": "๐Ÿ‘Œ",
- "ok_woman": "๐Ÿ™†",
- "old_key": "๐Ÿ—",
- "older_adult": "๐Ÿง“",
- "older_man": "๐Ÿ‘ด",
- "older_person": "๐Ÿง“",
- "older_woman": "๐Ÿ‘ต",
- "om_symbol": "๐Ÿ•‰",
- "on": "๐Ÿ”›",
- "oncoming_automobile": "๐Ÿš˜",
- "oncoming_bus": "๐Ÿš",
- "oncoming_fist": "๐Ÿ‘Š",
- "oncoming_police_car": "๐Ÿš”",
- "oncoming_taxi": "๐Ÿš–",
- "one": "1โƒฃ",
- "onepiece_swimsuit": "๐Ÿฉฑ",
- "onion": "๐Ÿง…",
- "open_file_folder": "๐Ÿ“‚",
- "open_hands": "๐Ÿ‘",
- "open_mouth": "๐Ÿ˜ฎ",
- "ophiuchus": "โ›Ž",
- "orange_book": "๐Ÿ“™",
- "orange_circle": "๐ŸŸ ",
- "orange_heart": "๐Ÿงก",
- "orange_square": "๐ŸŸง",
- "orangutan": "๐Ÿฆง",
- "orthodox_cross": "โ˜ฆ๏ธ",
- "otter": "๐Ÿฆฆ",
- "outbox_tray": "๐Ÿ“ค",
- "owl": "๐Ÿฆ‰",
- "ox": "๐Ÿ‚",
- "oyster": "๐Ÿฆช",
- "p_button": "๐Ÿ…ฟ",
- "package": "๐Ÿ“ฆ",
- "page_facing_up": "๐Ÿ“„",
- "page_with_curl": "๐Ÿ“ƒ",
- "pager": "๐Ÿ“Ÿ",
- "paintbrush": "๐Ÿ–Œ",
- "palm_tree": "๐ŸŒด",
- "palms_up_together": "๐Ÿคฒ",
- "pancakes": "๐Ÿฅž",
- "panda_face": "๐Ÿผ",
- "paperclip": "๐Ÿ“Ž",
- "parachute": "๐Ÿช‚",
- "parrot": "๐Ÿฆœ",
- "part_alternation_mark": "ใ€ฝ",
- "partly_sunny": "โ›…",
- "partying_face": "๐Ÿฅณ",
- "passenger_ship": "๐Ÿ›ณ",
- "passport_control": "๐Ÿ›‚",
- "pause_button": "โธ๏ธ",
- "peace": "โ˜ฎ",
- "peace_symbol": "โ˜ฎ๏ธ",
- "peach": "๐Ÿ‘",
- "peacock": "๐Ÿฆš",
- "peanuts": "๐Ÿฅœ",
- "pear": "๐Ÿ",
- "pen": "๐Ÿ–Š",
- "pencil": "๐Ÿ“",
- "pencil2": "โœ",
- "penguin": "๐Ÿง",
- "pensive": "๐Ÿ˜”",
- "people_with_bunny_ears_partying": "๐Ÿ‘ฏ",
- "people_wrestling": "๐Ÿคผ",
- "performing_arts": "๐ŸŽญ",
- "persevere": "๐Ÿ˜ฃ",
- "person": "๐Ÿง‘",
- "person_biking": "๐Ÿšด",
- "person_bouncing_ball": "โ›น๏ธ",
- "person_bowing": "๐Ÿ™‡",
- "person_cartwheeling": "๐Ÿคธ",
- "person_climbing": "๐Ÿง—",
- "person_doing_cartwheel": "๐Ÿคธ",
- "person_facepalming": "๐Ÿคฆ",
- "person_fencing": "๐Ÿคบ",
- "person_frowning": "๐Ÿ™",
- "person_gesturing_no": "๐Ÿ™…",
- "person_gesturing_ok": "๐Ÿ™†",
- "person_getting_haircut": "๐Ÿ’‡",
- "person_getting_massage": "๐Ÿ’†",
- "person_in_lotus_position": "๐Ÿง˜",
- "person_in_steamy_room": "๐Ÿง–",
- "person_juggling": "๐Ÿคน",
- "person_kneeling": "๐ŸงŽ",
- "person_mountain_biking": "๐Ÿšต",
- "person_playing_handball": "๐Ÿคพ",
- "person_playing_water_polo": "๐Ÿคฝ",
- "person_pouting": "๐Ÿ™Ž",
- "person_raising_hand": "๐Ÿ™‹",
- "person_rowing_boat": "๐Ÿšฃ",
- "person_running": "๐Ÿƒ",
- "person_shrugging": "๐Ÿคท",
- "person_standing": "๐Ÿง",
- "person_surfing": "๐Ÿ„",
- "person_swimming": "๐ŸŠ",
- "person_tipping_hand": "๐Ÿ’",
- "person_walking": "๐Ÿšถ",
- "person_wearing_turban": "๐Ÿ‘ณ",
- "person_with_blond_hair": "๐Ÿ‘ฑ",
- "person_with_pouting_face": "๐Ÿ™Ž",
- "petri_dish": "๐Ÿงซ",
- "pick": "โ›๏ธ",
- "pie": "๐Ÿฅง",
- "pig": "๐Ÿท",
- "pig2": "๐Ÿ–",
- "pig_nose": "๐Ÿฝ",
- "pill": "๐Ÿ’Š",
- "pinching_hand": "๐Ÿค",
- "pineapple": "๐Ÿ",
- "ping_pong": "๐Ÿ“",
- "pisces": "โ™“",
- "pizza": "๐Ÿ•",
- "place_of_worship": "๐Ÿ›",
- "play_button": "โ–ถ",
- "play_or_pause_button": "โฏ๏ธ",
- "play_pause": "โฏ",
- "pleading_face": "๐Ÿฅบ",
- "point_down": "๐Ÿ‘‡",
- "point_left": "๐Ÿ‘ˆ",
- "point_right": "๐Ÿ‘‰",
- "point_up": "โ˜๏ธ",
- "point_up_2": "๐Ÿ‘†",
- "police_car": "๐Ÿš“",
- "police_officer": "๐Ÿ‘ฎ",
- "poodle": "๐Ÿฉ",
- "poop": "๐Ÿ’ฉ",
- "popcorn": "๐Ÿฟ",
- "post_office": "๐Ÿฃ",
- "postal_horn": "๐Ÿ“ฏ",
- "postbox": "๐Ÿ“ฎ",
- "potable_water": "๐Ÿšฐ",
- "potato": "๐Ÿฅ”",
- "pouch": "๐Ÿ‘",
- "poultry_leg": "๐Ÿ—",
- "pound": "๐Ÿ’ท",
- "pouting_cat": "๐Ÿ˜พ",
- "pray": "๐Ÿ™",
- "prayer_beads": "๐Ÿ“ฟ",
- "pregnant_woman": "๐Ÿคฐ",
- "pretzel": "๐Ÿฅจ",
- "prince": "๐Ÿคด",
- "princess": "๐Ÿ‘ธ",
- "printer": "๐Ÿ–จ",
- "probing_cane": "๐Ÿฆฏ",
- "punch": "๐Ÿ‘Š",
- "purple_circle": "๐ŸŸฃ",
- "purple_heart": "๐Ÿ’œ",
- "purse": "๐Ÿ‘›",
- "pushpin": "๐Ÿ“Œ",
- "put_litter_in_its_place": "๐Ÿšฎ",
- "puzzle_piece": "๐Ÿงฉ",
- "question": "โ“",
- "rabbit": "๐Ÿฐ",
- "rabbit2": "๐Ÿ‡",
- "raccoon": "๐Ÿฆ",
- "racehorse": "๐ŸŽ",
- "racing_car": "๐ŸŽ",
- "radio": "๐Ÿ“ป",
- "radio_button": "๐Ÿ”˜",
- "radioactive": "โ˜ข๏ธ",
- "rage": "๐Ÿ˜ก",
- "railway_car": "๐Ÿšƒ",
- "railway_track": "๐Ÿ›ค",
- "rainbow": "๐ŸŒˆ",
- "raised_back_of_hand": "๐Ÿคš",
- "raised_hand": "โœ‹",
- "raised_hands": "๐Ÿ™Œ",
- "raising_hand": "๐Ÿ™‹",
- "ram": "๐Ÿ",
- "ramen": "๐Ÿœ",
- "rat": "๐Ÿ€",
- "razor": "๐Ÿช’",
- "receipt": "๐Ÿงพ",
- "record_button": "โบ๏ธ",
- "recycle": "โ™ป",
- "recycling_symbol": "โ™ป๏ธ",
- "red_car": "๐Ÿš—",
- "red_circle": "๐Ÿ”ด",
- "red_envelope": "๐Ÿงง",
- "red_hair": "๐Ÿฆฐ",
- "red_heart": "โค",
- "red_square": "๐ŸŸฅ",
- "regional_indicator_a": "๐Ÿ‡ฆ",
- "regional_indicator_b": "๐Ÿ‡ง",
- "regional_indicator_c": "๐Ÿ‡จ",
- "regional_indicator_d": "๐Ÿ‡ฉ",
- "regional_indicator_e": "๐Ÿ‡ช",
- "regional_indicator_f": "๐Ÿ‡ซ",
- "regional_indicator_g": "๐Ÿ‡ฌ",
- "regional_indicator_h": "๐Ÿ‡ญ",
- "regional_indicator_i": "๐Ÿ‡ฎ",
- "regional_indicator_j": "๐Ÿ‡ฏ",
- "regional_indicator_k": "๐Ÿ‡ฐ",
- "regional_indicator_l": "๐Ÿ‡ฑ",
- "regional_indicator_m": "๐Ÿ‡ฒ",
- "regional_indicator_n": "๐Ÿ‡ณ",
- "regional_indicator_o": "๐Ÿ‡ด",
- "regional_indicator_p": "๐Ÿ‡ต",
- "regional_indicator_q": "๐Ÿ‡ถ",
- "regional_indicator_r": "๐Ÿ‡ท",
- "regional_indicator_s": "๐Ÿ‡ธ",
- "regional_indicator_t": "๐Ÿ‡น",
- "regional_indicator_u": "๐Ÿ‡บ",
- "regional_indicator_v": "๐Ÿ‡ป",
- "regional_indicator_w": "๐Ÿ‡ผ",
- "regional_indicator_x": "๐Ÿ‡ฝ",
- "regional_indicator_y": "๐Ÿ‡พ",
- "regional_indicator_z": "๐Ÿ‡ฟ",
- "registered": "ยฎ",
- "relieved": "๐Ÿ˜Œ",
- "reminder_ribbon": "๐ŸŽ—",
- "repeat": "๐Ÿ”",
- "repeat_one": "๐Ÿ”‚",
- "rescue_workerโ€™s_helmet": "โ›‘๏ธ",
- "restroom": "๐Ÿšป",
- "reverse_button": "โ—€",
- "revolving_hearts": "๐Ÿ’ž",
- "rewind": "โช",
- "rhino": "๐Ÿฆ",
- "rhinoceros": "๐Ÿฆ",
- "ribbon": "๐ŸŽ€",
- "rice": "๐Ÿš",
- "rice_ball": "๐Ÿ™",
- "rice_cracker": "๐Ÿ˜",
- "rice_scene": "๐ŸŽ‘",
- "right_arrow": "โžก๏ธ",
- "right_arrow_curving_down": "โคต",
- "right_arrow_curving_left": "โ†ฉ",
- "right_arrow_curving_up": "โคด",
- "right_facing_fist": "๐Ÿคœ",
- "rightfacing_fist": "๐Ÿคœ",
- "ring": "๐Ÿ’",
- "ringed_planet": "๐Ÿช",
- "robot": "๐Ÿค–",
- "rocket": "๐Ÿš€",
- "rofl": "๐Ÿคฃ",
- "roll_of_paper": "๐Ÿงป",
- "rolledup_newspaper": "๐Ÿ—ž",
- "roller_coaster": "๐ŸŽข",
- "rolling_eyes": "๐Ÿ™„",
- "rolling_on_the_floor_laughing": "๐Ÿคฃ",
- "rooster": "๐Ÿ“",
- "rose": "๐ŸŒน",
- "rosette": "๐Ÿต",
- "rotating_light": "๐Ÿšจ",
- "round_pushpin": "๐Ÿ“",
- "rowboat": "๐Ÿšฃ",
- "rugby_football": "๐Ÿ‰",
- "runner": "๐Ÿƒ",
- "running_shirt_with_sash": "๐ŸŽฝ",
- "safety_pin": "๐Ÿงท",
- "safety_vest": "๐Ÿฆบ",
- "sagittarius": "โ™",
- "sailboat": "โ›ต",
- "sake": "๐Ÿถ",
- "salad": "๐Ÿฅ—",
- "salt": "๐Ÿง‚",
- "sandal": "๐Ÿ‘ก",
- "sandwich": "๐Ÿฅช",
- "santa": "๐ŸŽ…",
- "sari": "๐Ÿฅป",
- "satellite": "๐Ÿ“ก",
- "sauropod": "๐Ÿฆ•",
- "saxophone": "๐ŸŽท",
- "scales": "โš–",
- "scarf": "๐Ÿงฃ",
- "school": "๐Ÿซ",
- "school_satchel": "๐ŸŽ’",
- "scissors": "โœ‚",
- "scooter": "๐Ÿ›ด",
- "scorpion": "๐Ÿฆ‚",
- "scorpius": "โ™",
- "scream": "๐Ÿ˜ฑ",
- "scream_cat": "๐Ÿ™€",
- "scroll": "๐Ÿ“œ",
- "seat": "๐Ÿ’บ",
- "second_place": "๐Ÿฅˆ",
- "secret": "ใŠ™",
- "see_no_evil": "๐Ÿ™ˆ",
- "seedling": "๐ŸŒฑ",
- "selfie": "๐Ÿคณ",
- "seven": "7โƒฃ",
- "shallow_pan_of_food": "๐Ÿฅ˜",
- "shamrock": "โ˜˜๏ธ",
- "shark": "๐Ÿฆˆ",
- "shaved_ice": "๐Ÿง",
- "sheep": "๐Ÿ‘",
- "shell": "๐Ÿš",
- "shield": "๐Ÿ›ก",
- "shinto_shrine": "โ›ฉ๏ธ",
- "ship": "๐Ÿšข",
- "shirt": "๐Ÿ‘•",
- "shopping_bags": "๐Ÿ›",
- "shopping_cart": "๐Ÿ›’",
- "shorts": "๐Ÿฉณ",
- "shower": "๐Ÿšฟ",
- "shrimp": "๐Ÿฆ",
- "shushing_face": "๐Ÿคซ",
- "sign_of_the_horns": "๐Ÿค˜",
- "signal_strength": "๐Ÿ“ถ",
- "six": "6โƒฃ",
- "six_pointed_star": "๐Ÿ”ฏ",
- "skateboard": "๐Ÿ›น",
- "ski": "๐ŸŽฟ",
- "skier": "โ›ท๏ธ",
- "skull": "๐Ÿ’€",
- "skull_and_crossbones": "โ˜ ๏ธ",
- "skull_crossbones": "โ˜ ",
- "skunk": "๐Ÿฆจ",
- "sled": "๐Ÿ›ท",
- "sleeping": "๐Ÿ˜ด",
- "sleeping_accommodation": "๐Ÿ›Œ",
- "sleepy": "๐Ÿ˜ช",
- "slight_frown": "๐Ÿ™",
- "slight_smile": "๐Ÿ™‚",
- "slightly_frowning_face": "๐Ÿ™",
- "slot_machine": "๐ŸŽฐ",
- "sloth": "๐Ÿฆฅ",
- "small_airplane": "๐Ÿ›ฉ",
- "small_blue_diamond": "๐Ÿ”น",
- "small_orange_diamond": "๐Ÿ”ธ",
- "small_red_triangle": "๐Ÿ”บ",
- "small_red_triangle_down": "๐Ÿ”ป",
- "smile": "๐Ÿ˜„",
- "smile_cat": "๐Ÿ˜ธ",
- "smiley": "๐Ÿ˜ƒ",
- "smiley_cat": "๐Ÿ˜บ",
- "smiling": "โ˜บ๏ธ",
- "smiling_face": "โ˜บ",
- "smiling_face_with_hearts": "๐Ÿฅฐ",
- "smiling_imp": "๐Ÿ˜ˆ",
- "smirk": "๐Ÿ˜",
- "smirk_cat": "๐Ÿ˜ผ",
- "smoking": "๐Ÿšฌ",
- "snail": "๐ŸŒ",
- "snake": "๐Ÿ",
- "sneezing_face": "๐Ÿคง",
- "snowboarder": "๐Ÿ‚",
- "snowcapped_mountain": "๐Ÿ”",
- "snowflake": "โ„",
- "snowman": "โ›„",
- "soap": "๐Ÿงผ",
- "sob": "๐Ÿ˜ญ",
- "soccer": "โšฝ",
- "socks": "๐Ÿงฆ",
- "softball": "๐ŸฅŽ",
- "soon": "๐Ÿ”œ",
- "sos": "๐Ÿ†˜",
- "sound": "๐Ÿ”‰",
- "space_invader": "๐Ÿ‘พ",
- "spade_suit": "โ™ ๏ธ",
- "spades": "โ™ ",
- "spaghetti": "๐Ÿ",
- "sparkle": "โ‡",
- "sparkler": "๐ŸŽ‡",
- "sparkles": "โœจ",
- "sparkling_heart": "๐Ÿ’–",
- "speak_no_evil": "๐Ÿ™Š",
- "speaker": "๐Ÿ”ˆ",
- "speaking_head": "๐Ÿ—ฃ",
- "speech_balloon": "๐Ÿ’ฌ",
- "speech_left": "๐Ÿ—จ",
- "speedboat": "๐Ÿšค",
- "spider": "๐Ÿ•ท",
- "spider_web": "๐Ÿ•ธ",
- "spiral_calendar": "๐Ÿ—“",
- "spiral_notepad": "๐Ÿ—’",
- "sponge": "๐Ÿงฝ",
- "spoon": "๐Ÿฅ„",
- "squid": "๐Ÿฆ‘",
- "stadium": "๐ŸŸ",
- "star": "โญ",
- "star2": "๐ŸŒŸ",
- "star_and_crescent": "โ˜ช๏ธ",
- "star_of_david": "โœก",
- "star_struck": "๐Ÿคฉ",
- "stars": "๐ŸŒ ",
- "starstruck": "๐Ÿคฉ",
- "station": "๐Ÿš‰",
- "statue_of_liberty": "๐Ÿ—ฝ",
- "steam_locomotive": "๐Ÿš‚",
- "stethoscope": "๐Ÿฉบ",
- "stew": "๐Ÿฒ",
- "stop_button": "โน๏ธ",
- "stopwatch": "โฑ๏ธ",
- "straight_ruler": "๐Ÿ“",
- "strawberry": "๐Ÿ“",
- "stuck_out_tongue": "๐Ÿ˜›",
- "stuck_out_tongue_closed_eyes": "๐Ÿ˜",
- "stuck_out_tongue_winking_eye": "๐Ÿ˜œ",
- "studio_microphone": "๐ŸŽ™",
- "stuffed_flatbread": "๐Ÿฅ™",
- "sun": "โ˜€",
- "sun_behind_large_cloud": "๐ŸŒฅ",
- "sun_behind_rain_cloud": "๐ŸŒฆ",
- "sun_behind_small_cloud": "๐ŸŒค",
- "sun_with_face": "๐ŸŒž",
- "sunflower": "๐ŸŒป",
- "sunglasses": "๐Ÿ˜Ž",
- "sunny": "โ˜€๏ธ",
- "sunrise": "๐ŸŒ…",
- "sunrise_over_mountains": "๐ŸŒ„",
- "superhero": "๐Ÿฆธ",
- "supervillain": "๐Ÿฆน",
- "surfer": "๐Ÿ„",
- "sushi": "๐Ÿฃ",
- "suspension_railway": "๐ŸšŸ",
- "swan": "๐Ÿฆข",
- "sweat": "๐Ÿ˜“",
- "sweat_drops": "๐Ÿ’ฆ",
- "sweat_smile": "๐Ÿ˜…",
- "sweet_potato": "๐Ÿ ",
- "swimmer": "๐ŸŠ",
- "symbols": "๐Ÿ”ฃ",
- "synagogue": "๐Ÿ•",
- "syringe": "๐Ÿ’‰",
- "t_rex": "๐Ÿฆ–",
- "taco": "๐ŸŒฎ",
- "tada": "๐ŸŽ‰",
- "takeout_box": "๐Ÿฅก",
- "tanabata_tree": "๐ŸŽ‹",
- "tangerine": "๐ŸŠ",
- "taurus": "โ™‰",
- "taxi": "๐Ÿš•",
- "tea": "๐Ÿต",
- "teddy_bear": "๐Ÿงธ",
- "telephone": "โ˜Ž",
- "telephone_receiver": "๐Ÿ“ž",
- "telescope": "๐Ÿ”ญ",
- "tennis": "๐ŸŽพ",
- "tent": "โ›บ",
- "test_tube": "๐Ÿงช",
- "thermometer": "๐ŸŒก",
- "thermometer_face": "๐Ÿค’",
- "thinking": "๐Ÿค”",
- "third_place": "๐Ÿฅ‰",
- "thought_balloon": "๐Ÿ’ญ",
- "thread": "๐Ÿงต",
- "three": "3โƒฃ",
- "thumbsdown": "๐Ÿ‘Ž",
- "thumbsup": "๐Ÿ‘",
- "ticket": "๐ŸŽซ",
- "tiger": "๐Ÿฏ",
- "tiger2": "๐Ÿ…",
- "timer_clock": "โฒ๏ธ",
- "tired_face": "๐Ÿ˜ซ",
- "tm": "โ„ข",
- "toilet": "๐Ÿšฝ",
- "tokyo_tower": "๐Ÿ—ผ",
- "tomato": "๐Ÿ…",
- "tone1": "๐Ÿป",
- "tone2": "๐Ÿผ",
- "tone3": "๐Ÿฝ",
- "tone4": "๐Ÿพ",
- "tone5": "๐Ÿฟ",
- "tongue": "๐Ÿ‘…",
- "toolbox": "๐Ÿงฐ",
- "tooth": "๐Ÿฆท",
- "top": "๐Ÿ”",
- "tophat": "๐ŸŽฉ",
- "tornado": "๐ŸŒช",
- "track_next": "โญ",
- "track_previous": "โฎ",
- "trackball": "๐Ÿ–ฒ",
- "tractor": "๐Ÿšœ",
- "trade_mark": "โ„ข๏ธ",
- "traffic_light": "๐Ÿšฅ",
- "train": "๐Ÿš‹",
- "train2": "๐Ÿš†",
- "tram": "๐ŸšŠ",
- "trex": "๐Ÿฆ–",
- "triangular_flag_on_post": "๐Ÿšฉ",
- "triangular_ruler": "๐Ÿ“",
- "trident": "๐Ÿ”ฑ",
- "triumph": "๐Ÿ˜ค",
- "trolleybus": "๐ŸšŽ",
- "trophy": "๐Ÿ†",
- "tropical_drink": "๐Ÿน",
- "tropical_fish": "๐Ÿ ",
- "truck": "๐Ÿšš",
- "trumpet": "๐ŸŽบ",
- "tulip": "๐ŸŒท",
- "tumbler_glass": "๐Ÿฅƒ",
- "turkey": "๐Ÿฆƒ",
- "turtle": "๐Ÿข",
- "tv": "๐Ÿ“บ",
- "twisted_rightwards_arrows": "๐Ÿ”€",
- "two": "2โƒฃ",
- "two_hearts": "๐Ÿ’•",
- "two_men_holding_hands": "๐Ÿ‘ฌ",
- "two_women_holding_hands": "๐Ÿ‘ญ",
- "u5272": "๐Ÿˆน",
- "u5408": "๐Ÿˆด",
- "u55b6": "๐Ÿˆบ",
- "u6307": "๐Ÿˆฏ",
- "u6708": "๐Ÿˆท",
- "u6709": "๐Ÿˆถ",
- "u6e80": "๐Ÿˆต",
- "u7121": "๐Ÿˆš",
- "u7533": "๐Ÿˆธ",
- "u7981": "๐Ÿˆฒ",
- "u7a7a": "๐Ÿˆณ",
- "umbrella": "โ˜”",
- "umbrella_on_ground": "โ›ฑ๏ธ",
- "unamused": "๐Ÿ˜’",
- "underage": "๐Ÿ”ž",
- "unicorn": "๐Ÿฆ„",
- "unlock": "๐Ÿ”“",
- "up": "๐Ÿ†™",
- "up_arrow": "โฌ†",
- "updown_arrow": "โ†•๏ธ",
- "upleft_arrow": "โ†–๏ธ",
- "upright_arrow": "โ†—",
- "upside_down": "๐Ÿ™ƒ",
- "v": "โœŒ๏ธ",
- "vampire": "๐Ÿง›",
- "vertical_traffic_light": "๐Ÿšฆ",
- "vhs": "๐Ÿ“ผ",
- "vibration_mode": "๐Ÿ“ณ",
- "victory_hand": "โœŒ",
- "video_camera": "๐Ÿ“น",
- "video_game": "๐ŸŽฎ",
- "violin": "๐ŸŽป",
- "virgo": "โ™",
- "volcano": "๐ŸŒ‹",
- "volleyball": "๐Ÿ",
- "vs": "๐Ÿ†š",
- "vulcan": "๐Ÿ––",
- "vulcan_salute": "๐Ÿ––",
- "waffle": "๐Ÿง‡",
- "walking": "๐Ÿšถ",
- "waning_crescent_moon": "๐ŸŒ˜",
- "waning_gibbous_moon": "๐ŸŒ–",
- "warning": "โš ",
- "wastebasket": "๐Ÿ—‘",
- "watch": "โŒš",
- "water_buffalo": "๐Ÿƒ",
- "watermelon": "๐Ÿ‰",
- "wave": "๐Ÿ‘‹",
- "wavy_dash": "ใ€ฐ๏ธ",
- "waxing_crescent_moon": "๐ŸŒ’",
- "waxing_gibbous_moon": "๐ŸŒ”",
- "wc": "๐Ÿšพ",
- "weary": "๐Ÿ˜ฉ",
- "wedding": "๐Ÿ’’",
- "weightlifter": "๐Ÿ‹",
- "whale": "๐Ÿณ",
- "whale2": "๐Ÿ‹",
- "wheel_of_dharma": "โ˜ธ๏ธ",
- "wheelchair": "โ™ฟ",
- "white_check_mark": "โœ…",
- "white_circle": "โšช",
- "white_flower": "๐Ÿ’ฎ",
- "white_hair": "๐Ÿฆณ",
- "white_heart": "๐Ÿค",
- "white_large_square": "โฌœ",
- "white_medium_small_square": "โ—ฝ",
- "white_medium_square": "โ—ป๏ธ",
- "white_small_square": "โ–ซ๏ธ",
- "white_square_button": "๐Ÿ”ณ",
- "wilted_flower": "๐Ÿฅ€",
- "wilted_rose": "๐Ÿฅ€",
- "wind_blowing_face": "๐ŸŒฌ",
- "wind_chime": "๐ŸŽ",
- "wine_glass": "๐Ÿท",
- "wink": "๐Ÿ˜‰",
- "wolf": "๐Ÿบ",
- "woman": "๐Ÿ‘ฉ",
- "woman_with_headscarf": "๐Ÿง•",
- "womans_clothes": "๐Ÿ‘š",
- "womans_hat": "๐Ÿ‘’",
- "womens": "๐Ÿšบ",
- "woozy_face": "๐Ÿฅด",
- "world_map": "๐Ÿ—บ",
- "worried": "๐Ÿ˜Ÿ",
- "wrench": "๐Ÿ”ง",
- "writing_hand": "โœ๏ธ",
- "x": "โŒ",
- "yarn": "๐Ÿงถ",
- "yawning_face": "๐Ÿฅฑ",
- "yellow_circle": "๐ŸŸก",
- "yellow_heart": "๐Ÿ’›",
- "yellow_square": "๐ŸŸจ",
- "yen": "๐Ÿ’ด",
- "yin_yang": "โ˜ฏ๏ธ",
- "yoyo": "๐Ÿช€",
- "yum": "๐Ÿ˜‹",
- "zany_face": "๐Ÿคช",
- "zap": "โšก",
- "zebra": "๐Ÿฆ“",
- "zero": "0โƒฃ",
- "zipper_mouth": "๐Ÿค",
- "zombie": "๐ŸงŸ",
- "zzz": "๐Ÿ’ค"
-} \ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 6f891f30..941473b4 100644
--- 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"