aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/App.js3
-rw-r--r--src/App.scss14
-rw-r--r--src/App.vue7
-rw-r--r--src/boot/after_store.js7
-rw-r--r--src/components/about/about.js3
-rw-r--r--src/components/about/about.vue2
-rw-r--r--src/components/autocomplete_input/autocomplete_input.js150
-rw-r--r--src/components/autocomplete_input/autocomplete_input.vue104
-rw-r--r--src/components/follow_list/follow_list.js63
-rw-r--r--src/components/follow_list/follow_list.vue34
-rw-r--r--src/components/media_upload/media_upload.js25
-rw-r--r--src/components/media_upload/media_upload.vue2
-rw-r--r--src/components/notification/notification.js4
-rw-r--r--src/components/notification/notification.vue2
-rw-r--r--src/components/notifications/notifications.scss21
-rw-r--r--src/components/post_status_form/post_status_form.js136
-rw-r--r--src/components/post_status_form/post_status_form.vue88
-rw-r--r--src/components/public_and_external_timeline/public_and_external_timeline.js2
-rw-r--r--src/components/public_and_external_timeline/public_and_external_timeline.vue2
-rw-r--r--src/components/public_timeline/public_timeline.js2
-rw-r--r--src/components/settings/settings.js16
-rw-r--r--src/components/settings/settings.vue36
-rw-r--r--src/components/side_drawer/side_drawer.js6
-rw-r--r--src/components/side_drawer/side_drawer.vue26
-rw-r--r--src/components/status/status.js42
-rw-r--r--src/components/status/status.vue79
-rw-r--r--src/components/still-image/still-image.js6
-rw-r--r--src/components/still-image/still-image.vue2
-rw-r--r--src/components/tab_switcher/tab_switcher.js2
-rw-r--r--src/components/tag_timeline/tag_timeline.js4
-rw-r--r--src/components/user_avatar/user_avatar.js29
-rw-r--r--src/components/user_avatar/user_avatar.vue42
-rw-r--r--src/components/user_card/user_card.js34
-rw-r--r--src/components/user_card/user_card.vue71
-rw-r--r--src/components/user_card_content/user_card_content.js77
-rw-r--r--src/components/user_card_content/user_card_content.vue26
-rw-r--r--src/components/user_profile/user_profile.js77
-rw-r--r--src/components/user_profile/user_profile.vue25
-rw-r--r--src/components/user_settings/user_settings.js26
-rw-r--r--src/components/user_settings/user_settings.vue25
-rw-r--r--src/i18n/de.json4
-rw-r--r--src/i18n/en.json21
-rw-r--r--src/i18n/es.json317
-rw-r--r--src/i18n/fi.json4
-rw-r--r--src/i18n/ja.json3
-rw-r--r--src/i18n/ko.json3
-rw-r--r--src/i18n/ru.json5
-rw-r--r--src/lib/persisted_state.js2
-rw-r--r--src/modules/api.js19
-rw-r--r--src/modules/config.js3
-rw-r--r--src/modules/instance.js9
-rw-r--r--src/modules/users.js67
-rw-r--r--src/services/api/api.service.js20
-rw-r--r--src/services/backend_interactor_service/backend_interactor_service.js13
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js7
-rw-r--r--src/services/follow_manipulate/follow_manipulate.js74
-rw-r--r--src/services/matcher/matcher.service.js23
-rw-r--r--src/services/mention_matcher/mention_matcher.js9
-rw-r--r--src/services/style_setter/style_setter.js2
-rw-r--r--src/services/user_profile_link_generator/user_profile_link_generator.js2
-rw-r--r--static/config.json4
-rw-r--r--static/emoji.json970
-rw-r--r--test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js4
-rw-r--r--test/unit/specs/services/matcher/matcher.spec.js (renamed from test/unit/specs/services/mention_matcher/mention_matcher.spec.js)37
64 files changed, 2364 insertions, 580 deletions
diff --git a/src/App.js b/src/App.js
index 32589b64..311bf484 100644
--- a/src/App.js
+++ b/src/App.js
@@ -81,7 +81,8 @@ export default {
},
unseenNotificationsCount () {
return this.unseenNotifications.length
- }
+ },
+ showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }
},
methods: {
scrollToTop () {
diff --git a/src/App.scss b/src/App.scss
index dedc294d..52484f59 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -725,3 +725,17 @@ nav {
margin-right: 0.8em;
}
}
+
+.login-hint {
+ text-align: center;
+
+ @media all and (min-width: 801px) {
+ display: none;
+ }
+
+ a {
+ display: inline-block;
+ padding: 1em 0px;
+ width: 100%;
+ }
+}
diff --git a/src/App.vue b/src/App.vue
index 8a4e02c4..fa5736e5 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -30,7 +30,7 @@
<user-panel></user-panel>
<nav-panel></nav-panel>
<instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
- <features-panel v-if="!currentUser"></features-panel>
+ <features-panel v-if="!currentUser && showFeaturesPanel"></features-panel>
<who-to-follow-panel v-if="currentUser && suggestionsEnabled"></who-to-follow-panel>
<notifications v-if="currentUser"></notifications>
</div>
@@ -38,6 +38,11 @@
</div>
</div>
<div class="main">
+ <div v-if="!currentUser" class="login-hint panel panel-default">
+ <router-link :to="{ name: 'login' }" class="panel-body">
+ {{ $t("login.hint") }}
+ </router-link>
+ </div>
<transition name="fade">
<router-view></router-view>
</transition>
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 5693dcc6..53ecc083 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -55,10 +55,10 @@ const afterStoreSetup = ({ store, i18n }) => {
}
copyInstanceOption('nsfwCensorImage')
- copyInstanceOption('theme')
copyInstanceOption('background')
copyInstanceOption('hidePostStats')
copyInstanceOption('hideUserStats')
+ copyInstanceOption('hideFilteredStatuses')
copyInstanceOption('logo')
store.dispatch('setInstanceOption', {
@@ -84,8 +84,10 @@ const afterStoreSetup = ({ store, i18n }) => {
copyInstanceOption('loginMethod')
copyInstanceOption('scopeCopy')
copyInstanceOption('subjectLineBehavior')
+ copyInstanceOption('postContentType')
copyInstanceOption('alwaysShowSubjectInput')
copyInstanceOption('noAttachmentLinks')
+ copyInstanceOption('showFeaturesPanel')
if ((config.chatDisabled)) {
store.dispatch('disableChat')
@@ -93,6 +95,9 @@ const afterStoreSetup = ({ store, i18n }) => {
store.dispatch('initializeSocket')
}
+ return store.dispatch('setTheme', config['theme'])
+ })
+ .then(() => {
const router = new VueRouter({
mode: 'history',
routes: routes(store),
diff --git a/src/components/about/about.js b/src/components/about/about.js
index b4433b4e..ae1cb182 100644
--- a/src/components/about/about.js
+++ b/src/components/about/about.js
@@ -7,6 +7,9 @@ const About = {
InstanceSpecificPanel,
FeaturesPanel,
TermsOfServicePanel
+ },
+ computed: {
+ showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }
}
}
diff --git a/src/components/about/about.vue b/src/components/about/about.vue
index bf87e0b8..13dec87c 100644
--- a/src/components/about/about.vue
+++ b/src/components/about/about.vue
@@ -1,7 +1,7 @@
<template>
<div class="sidebar">
<instance-specific-panel></instance-specific-panel>
- <features-panel></features-panel>
+ <features-panel v-if="showFeaturesPanel"></features-panel>
<terms-of-service-panel></terms-of-service-panel>
</div>
</template>
diff --git a/src/components/autocomplete_input/autocomplete_input.js b/src/components/autocomplete_input/autocomplete_input.js
new file mode 100644
index 00000000..2a959fd1
--- /dev/null
+++ b/src/components/autocomplete_input/autocomplete_input.js
@@ -0,0 +1,150 @@
+import Completion from '../../services/completion/completion.js'
+import { take, filter, map } from 'lodash'
+
+const AutoCompleteInput = {
+ props: [
+ 'id',
+ 'classObj',
+ 'value',
+ 'placeholder',
+ 'autoResize',
+ 'multiline',
+ 'drop',
+ 'dragoverPrevent',
+ 'paste',
+ 'keydownMetaEnter',
+ 'keyupCtrlEnter'
+ ],
+ components: {},
+ mounted () {
+ this.autoResize && this.resize(this.$refs.textarea)
+ const textLength = this.$refs.textarea.value.length
+ this.$refs.textarea.setSelectionRange(textLength, textLength)
+ },
+ data () {
+ return {
+ caret: 0,
+ highlighted: 0,
+ text: this.value
+ }
+ },
+ computed: {
+ users () {
+ return this.$store.state.users.users
+ },
+ emoji () {
+ return this.$store.state.instance.emoji || []
+ },
+ customEmoji () {
+ return this.$store.state.instance.customEmoji || []
+ },
+ textAtCaret () {
+ return (this.wordAtCaret || {}).word || ''
+ },
+ wordAtCaret () {
+ const word = Completion.wordAtPosition(this.text, this.caret - 1) || {}
+ return word
+ },
+ candidates () {
+ const firstchar = this.textAtCaret.charAt(0)
+ if (firstchar === '@') {
+ const query = this.textAtCaret.slice(1).toUpperCase()
+ const matchedUsers = filter(this.users, (user) => {
+ return user.screen_name.toUpperCase().startsWith(query) ||
+ user.name && user.name.toUpperCase().startsWith(query)
+ })
+ if (matchedUsers.length <= 0) {
+ return false
+ }
+ // eslint-disable-next-line camelcase
+ return map(take(matchedUsers, 5), ({screen_name, name, profile_image_url_original}, index) => ({
+ // eslint-disable-next-line camelcase
+ screen_name: `@${screen_name}`,
+ name: name,
+ img: profile_image_url_original,
+ highlighted: index === this.highlighted
+ }))
+ } else if (firstchar === ':') {
+ if (this.textAtCaret === ':') { return }
+ const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.startsWith(this.textAtCaret.slice(1)))
+ if (matchedEmoji.length <= 0) {
+ return false
+ }
+ return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}, index) => ({
+ screen_name: `:${shortcode}:`,
+ name: '',
+ utf: utf || '',
+ // eslint-disable-next-line camelcase
+ img: utf ? '' : this.$store.state.instance.server + image_url,
+ highlighted: index === this.highlighted
+ }))
+ } else {
+ return false
+ }
+ }
+ },
+ methods: {
+ setCaret ({target: {selectionStart}}) {
+ this.caret = selectionStart
+ },
+ cycleBackward (e) {
+ const len = this.candidates.length || 0
+ if (len > 0) {
+ e.preventDefault()
+ this.highlighted -= 1
+ if (this.highlighted < 0) {
+ this.highlighted = this.candidates.length - 1
+ }
+ } else {
+ this.highlighted = 0
+ }
+ },
+ cycleForward (e) {
+ const len = this.candidates.length || 0
+ if (len > 0) {
+ if (e.shiftKey) { return }
+ e.preventDefault()
+ this.highlighted += 1
+ if (this.highlighted >= len) {
+ this.highlighted = 0
+ }
+ } else {
+ this.highlighted = 0
+ }
+ },
+ replace (replacement) {
+ this.text = Completion.replaceWord(this.text, this.wordAtCaret, replacement)
+ const el = this.$el.querySelector('textarea')
+ el.focus()
+ this.caret = 0
+ },
+ replaceCandidate (e) {
+ const len = this.candidates.length || 0
+ if (this.textAtCaret === ':' || e.ctrlKey) { return }
+ if (len > 0) {
+ e.preventDefault()
+ const candidate = this.candidates[this.highlighted]
+ const replacement = candidate.utf || (candidate.screen_name + ' ')
+ this.text = Completion.replaceWord(this.text, this.wordAtCaret, replacement)
+ const el = this.$el.querySelector('textarea') || this.$el.querySelector('input')
+ el.focus()
+ this.caret = 0
+ this.highlighted = 0
+ }
+ },
+ resize (e) {
+ const target = e.target || e
+ if (!(target instanceof window.Element)) { return }
+ const vertPadding = Number(window.getComputedStyle(target)['padding-top'].substr(0, 1)) +
+ Number(window.getComputedStyle(target)['padding-bottom'].substr(0, 1))
+ // Auto is needed to make textbox shrink when removing lines
+ target.style.height = 'auto'
+ target.style.height = `${target.scrollHeight - vertPadding}px`
+ if (target.value === '') {
+ target.style.height = null
+ }
+ }
+ }
+}
+
+export default AutoCompleteInput
diff --git a/src/components/autocomplete_input/autocomplete_input.vue b/src/components/autocomplete_input/autocomplete_input.vue
new file mode 100644
index 00000000..56233535
--- /dev/null
+++ b/src/components/autocomplete_input/autocomplete_input.vue
@@ -0,0 +1,104 @@
+<template>
+ <div style="display: flex; flex-direction: column;">
+ <textarea
+ v-if="multiline"
+ ref="textarea"
+ rows="1"
+ :value="text" :class="classObj" :id="id" :placeholder="placeholder"
+ @input="text = $event.target.value, $emit('input', $event.target.value), autoResize && resize($event)"
+ @click="setCaret"
+ @keyup="setCaret"
+ @keydown.down="cycleForward"
+ @keydown.up="cycleBackward"
+ @keydown.shift.tab="cycleBackward"
+ @keydown.tab="cycleForward"
+ @keydown.enter="replaceCandidate"
+ @drop="drop && drop()"
+ @dragover.prevent="dragoverPrevent && dragoverPrevent()"
+ @paste="paste && paste()"
+ @keydown.meta.enter="keydownMetaEnter && keydownMetaEnter()"
+ @keyup.ctrl.enter="keyupCtrlEnter && keyupCtrlEnter()">
+ </textarea>
+ <input
+ v-else
+ ref="textarea"
+ :value="text" :class="classObj" :id="id" :placeholder="placeholder"
+ @input="text = $event.target.value, $emit('input', $event.target.value), autoResize && resize($event)"
+ @click="setCaret"
+ @keyup="setCaret"
+ @keydown.down="cycleForward"
+ @keydown.up="cycleBackward"
+ @keydown.shift.tab="cycleBackward"
+ @keydown.tab="cycleForward"
+ @keydown.enter="replaceCandidate"
+ @drop="drop && drop()"
+ @dragover.prevent="dragoverPrevent && dragoverPrevent()"
+ @paste="paste && paste()"
+ @keydown.meta.enter="keydownMetaEnter && keydownMetaEnter()"
+ @keyup.ctrl.enter="keyupCtrlEnter && keyupCtrlEnter()"/>
+ <div style="position:relative;" v-if="candidates">
+ <div class="autocomplete-panel">
+ <div v-for="candidate in candidates" @click="replace(candidate.utf || (candidate.screen_name + ' '))">
+ <div class="autocomplete" :class="{ highlighted: candidate.highlighted }">
+ <span v-if="candidate.img"><img :src="candidate.img"></img></span>
+ <span v-else>{{candidate.utf}}</span>
+ <span>{{candidate.screen_name}}<small>{{candidate.name}}</small></span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script src="./autocomplete_input.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.autocomplete-panel {
+ margin: 0 0.5em 0 0.5em;
+ border-radius: $fallback--tooltipRadius;
+ border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
+ position: absolute;
+ z-index: 1;
+ box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
+ // this doesn't match original but i don't care, making it uniform.
+ box-shadow: var(--popupShadow);
+ min-width: 75%;
+ background: $fallback--bg;
+ background: var(--bg, $fallback--bg);
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
+}
+
+.autocomplete {
+ cursor: pointer;
+ padding: 0.2em 0.4em 0.2em 0.4em;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.4);
+ display: flex;
+
+ img {
+ width: 24px;
+ height: 24px;
+ border-radius: $fallback--avatarRadius;
+ border-radius: var(--avatarRadius, $fallback--avatarRadius);
+ object-fit: contain;
+ }
+
+ span {
+ line-height: 24px;
+ margin: 0 0.1em 0 0.2em;
+ }
+
+ small {
+ margin-left: .5em;
+ color: $fallback--faint;
+ color: var(--faint, $fallback--faint);
+ }
+
+ &.highlighted {
+ background-color: $fallback--fg;
+ background-color: var(--lightBg, $fallback--fg);
+ }
+}
+</style>
diff --git a/src/components/follow_list/follow_list.js b/src/components/follow_list/follow_list.js
new file mode 100644
index 00000000..acdb216d
--- /dev/null
+++ b/src/components/follow_list/follow_list.js
@@ -0,0 +1,63 @@
+import UserCard from '../user_card/user_card.vue'
+
+const FollowList = {
+ data () {
+ return {
+ loading: false,
+ bottomedOut: false,
+ error: false
+ }
+ },
+ props: ['userId', 'showFollowers'],
+ created () {
+ window.addEventListener('scroll', this.scrollLoad)
+ if (this.entries.length === 0) {
+ this.fetchEntries()
+ }
+ },
+ destroyed () {
+ window.removeEventListener('scroll', this.scrollLoad)
+ this.$store.dispatch('clearFriendsAndFollowers', this.userId)
+ },
+ computed: {
+ user () {
+ return this.$store.getters.userById(this.userId)
+ },
+ entries () {
+ return this.showFollowers ? this.user.followers : this.user.friends
+ },
+ showActions () { return this.$store.state.users.currentUser.id === this.userId }
+ },
+ methods: {
+ fetchEntries () {
+ if (!this.loading) {
+ const command = this.showFollowers ? 'addFollowers' : 'addFriends'
+ this.loading = true
+ this.$store.dispatch(command, this.userId).then(entries => {
+ this.error = false
+ this.loading = false
+ this.bottomedOut = entries.length === 0
+ }).catch(() => {
+ this.error = true
+ this.loading = false
+ })
+ }
+ },
+ scrollLoad (e) {
+ const bodyBRect = document.body.getBoundingClientRect()
+ const height = Math.max(bodyBRect.height, -(bodyBRect.y))
+ if (this.loading === false &&
+ this.bottomedOut === false &&
+ this.$el.offsetHeight > 0 &&
+ (window.innerHeight + window.pageYOffset) >= (height - 750)
+ ) {
+ this.fetchEntries()
+ }
+ }
+ },
+ components: {
+ UserCard
+ }
+}
+
+export default FollowList
diff --git a/src/components/follow_list/follow_list.vue b/src/components/follow_list/follow_list.vue
new file mode 100644
index 00000000..7be2e7b7
--- /dev/null
+++ b/src/components/follow_list/follow_list.vue
@@ -0,0 +1,34 @@
+<template>
+ <div class="follow-list">
+ <user-card
+ v-for="entry in entries"
+ :key="entry.id" :user="entry"
+ :showFollows="!showFollowers"
+ :showActions="showActions"
+ />
+ <div class="text-center panel-footer">
+ <a v-if="error" @click="fetchEntries" class="alert error">
+ {{$t('general.generic_error')}}
+ </a>
+ <i v-else-if="loading" class="icon-spin3 animate-spin"/>
+ <span v-else-if="bottomedOut"></span>
+ <a v-else @click="fetchEntries">{{$t('general.more')}}</a>
+ </div>
+ </div>
+</template>
+
+<script src="./follow_list.js"></script>
+
+<style lang="scss">
+
+.follow-list {
+ .panel-footer {
+ padding: 10px;
+ }
+
+ .error {
+ font-size: 14px;
+ }
+}
+
+</style>
diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js
index 42d900d3..1c874faa 100644
--- a/src/components/media_upload/media_upload.js
+++ b/src/components/media_upload/media_upload.js
@@ -3,19 +3,10 @@ import statusPosterService from '../../services/status_poster/status_poster.serv
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
const mediaUpload = {
- mounted () {
- const input = this.$el.querySelector('input')
-
- input.addEventListener('change', ({target}) => {
- for (var i = 0; i < target.files.length; i++) {
- let file = target.files[i]
- this.uploadFile(file)
- }
- })
- },
data () {
return {
- uploading: false
+ uploading: false,
+ uploadReady: true
}
},
methods: {
@@ -56,6 +47,18 @@ const mediaUpload = {
} else {
e.dataTransfer.dropEffect = 'none'
}
+ },
+ clearFile () {
+ this.uploadReady = false
+ this.$nextTick(() => {
+ this.uploadReady = true
+ })
+ },
+ change ({target}) {
+ for (var i = 0; i < target.files.length; i++) {
+ let file = target.files[i]
+ this.uploadFile(file)
+ }
}
},
props: [
diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue
index 768d3565..fcdc3471 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -3,7 +3,7 @@
<label class="btn btn-default" :title="$t('tool_tip.media_upload')">
<i class="icon-spin4 animate-spin" v-if="uploading"></i>
<i class="icon-upload" v-if="!uploading"></i>
- <input type="file" style="position: fixed; top: -100em" multiple="true"></input>
+ <input type="file" v-if="uploadReady" @change="change" style="position: fixed; top: -100em" multiple="true"></input>
</label>
</div>
</template>
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index f95a329f..7d9807de 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -1,5 +1,5 @@
import Status from '../status/status.vue'
-import StillImage from '../still-image/still-image.vue'
+import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCardContent from '../user_card_content/user_card_content.vue'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@@ -13,7 +13,7 @@ const Notification = {
},
props: [ 'notification' ],
components: {
- Status, StillImage, UserCardContent
+ Status, UserAvatar, UserCardContent
},
methods: {
toggleUserExpanded () {
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index f91c90cc..a0a55cba 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -2,7 +2,7 @@
<status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
<a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
- <StillImage class='avatar-compact' :class="{'better-shadow': betterShadow}" :src="notification.action.user.profile_image_url_original"/>
+ <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
</a>
<div class='notification-right'>
<div class="usercard notification-usercard" v-if="userExpanded">
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 5c4ca1b9..bc81d45c 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -36,26 +36,7 @@
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
- .avatar-compact {
- width: 32px;
- height: 32px;
- box-shadow: var(--avatarStatusShadow);
- border-radius: $fallback--avatarAltRadius;
- border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
- overflow: hidden;
- line-height: 0;
-
- &.better-shadow {
- box-shadow: var(--avatarStatusShadowInset);
- filter: var(--avatarStatusShadowFilter)
- }
-
- &.animated::before {
- display: none;
- }
- }
-
- &:hover .animated.avatar-compact {
+ &:hover .animated.avatar {
canvas {
display: none;
}
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 88bc736f..8e30264d 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -1,8 +1,8 @@
import statusPoster from '../../services/status_poster/status_poster.service.js'
import MediaUpload from '../media_upload/media_upload.vue'
+import AutoCompleteInput from '../autocomplete_input/autocomplete_input.vue'
import fileTypeService from '../../services/file_type/file_type.service.js'
-import Completion from '../../services/completion/completion.js'
-import { take, filter, reject, map, uniqBy } from 'lodash'
+import { reject, map, uniqBy } from 'lodash'
const buildMentionsString = ({user, attentions}, currentUser) => {
let allAttentions = [...attentions]
@@ -28,13 +28,10 @@ const PostStatusForm = {
'subject'
],
components: {
- MediaUpload
+ MediaUpload,
+ AutoCompleteInput
},
mounted () {
- this.resize(this.$refs.textarea)
- const textLength = this.$refs.textarea.value.length
- this.$refs.textarea.setSelectionRange(textLength, textLength)
-
if (this.replyTo) {
this.$refs.textarea.focus()
}
@@ -61,16 +58,13 @@ const PostStatusForm = {
submitDisabled: false,
error: null,
posting: false,
- highlighted: 0,
newStatus: {
spoilerText: this.subject || '',
status: statusText,
- contentType: 'text/plain',
nsfw: false,
files: [],
visibility: scope
- },
- caret: 0
+ }
}
},
computed: {
@@ -82,59 +76,6 @@ const PostStatusForm = {
direct: { selected: this.newStatus.visibility === 'direct' }
}
},
- candidates () {
- const firstchar = this.textAtCaret.charAt(0)
- if (firstchar === '@') {
- const query = this.textAtCaret.slice(1).toUpperCase()
- const matchedUsers = filter(this.users, (user) => {
- return user.screen_name.toUpperCase().startsWith(query) ||
- user.name && user.name.toUpperCase().startsWith(query)
- })
- if (matchedUsers.length <= 0) {
- return false
- }
- // eslint-disable-next-line camelcase
- return map(take(matchedUsers, 5), ({screen_name, name, profile_image_url_original}, index) => ({
- // eslint-disable-next-line camelcase
- screen_name: `@${screen_name}`,
- name: name,
- img: profile_image_url_original,
- highlighted: index === this.highlighted
- }))
- } else if (firstchar === ':') {
- if (this.textAtCaret === ':') { return }
- const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.startsWith(this.textAtCaret.slice(1)))
- if (matchedEmoji.length <= 0) {
- return false
- }
- return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}, index) => ({
- screen_name: `:${shortcode}:`,
- name: '',
- utf: utf || '',
- // eslint-disable-next-line camelcase
- img: utf ? '' : this.$store.state.instance.server + image_url,
- highlighted: index === this.highlighted
- }))
- } else {
- return false
- }
- },
- textAtCaret () {
- return (this.wordAtCaret || {}).word || ''
- },
- wordAtCaret () {
- const word = Completion.wordAtPosition(this.newStatus.status, this.caret - 1) || {}
- return word
- },
- users () {
- return this.$store.state.users.users
- },
- emoji () {
- return this.$store.state.instance.emoji || []
- },
- customEmoji () {
- return this.$store.state.instance.customEmoji || []
- },
statusLength () {
return this.newStatus.status.length
},
@@ -167,56 +108,16 @@ const PostStatusForm = {
},
formattingOptionsEnabled () {
return this.$store.state.instance.formattingOptionsEnabled
+ },
+ defaultPostContentType () {
+ return typeof this.$store.state.config.postContentType === 'undefined'
+ ? this.$store.state.instance.postContentType
+ : this.$store.state.config.postContentType
}
},
methods: {
- replace (replacement) {
- this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement)
- const el = this.$el.querySelector('textarea')
- el.focus()
- this.caret = 0
- },
- replaceCandidate (e) {
- const len = this.candidates.length || 0
- if (this.textAtCaret === ':' || e.ctrlKey) { return }
- if (len > 0) {
- e.preventDefault()
- const candidate = this.candidates[this.highlighted]
- const replacement = candidate.utf || (candidate.screen_name + ' ')
- this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement)
- const el = this.$el.querySelector('textarea')
- el.focus()
- this.caret = 0
- this.highlighted = 0
- }
- },
- cycleBackward (e) {
- const len = this.candidates.length || 0
- if (len > 0) {
- e.preventDefault()
- this.highlighted -= 1
- if (this.highlighted < 0) {
- this.highlighted = this.candidates.length - 1
- }
- } else {
- this.highlighted = 0
- }
- },
- cycleForward (e) {
- const len = this.candidates.length || 0
- if (len > 0) {
- if (e.shiftKey) { return }
- e.preventDefault()
- this.highlighted += 1
- if (this.highlighted >= len) {
- this.highlighted = 0
- }
- } else {
- this.highlighted = 0
- }
- },
- setCaret ({target: {selectionStart}}) {
- this.caret = selectionStart
+ postStatusCopy () {
+ this.postStatus(this.newStatus)
},
postStatus (newStatus) {
if (this.posting) { return }
@@ -250,6 +151,7 @@ const PostStatusForm = {
visibility: newStatus.visibility,
contentType: newStatus.contentType
}
+ this.$refs.mediaUpload.clearFile()
this.$emit('posted')
let el = this.$el.querySelector('textarea')
el.style.height = 'auto'
@@ -300,18 +202,6 @@ const PostStatusForm = {
fileDrag (e) {
e.dataTransfer.dropEffect = 'copy'
},
- resize (e) {
- const target = e.target || e
- if (!(target instanceof window.Element)) { return }
- const vertPadding = Number(window.getComputedStyle(target)['padding-top'].substr(0, 1)) +
- Number(window.getComputedStyle(target)['padding-bottom'].substr(0, 1))
- // Auto is needed to make textbox shrink when removing lines
- target.style.height = 'auto'
- target.style.height = `${target.scrollHeight - vertPadding}px`
- if (target.value === '') {
- target.style.height = null
- }
- },
clearError () {
this.error = null
},
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 398f1871..ef3a7901 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -16,26 +16,20 @@
:placeholder="$t('post_status.content_warning')"
v-model="newStatus.spoilerText"
class="form-cw">
- <textarea
- ref="textarea"
- @click="setCaret"
- @keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control"
- @keydown.down="cycleForward"
- @keydown.up="cycleBackward"
- @keydown.shift.tab="cycleBackward"
- @keydown.tab="cycleForward"
- @keydown.enter="replaceCandidate"
- @keydown.meta.enter="postStatus(newStatus)"
- @keyup.ctrl.enter="postStatus(newStatus)"
- @drop="fileDrop"
- @dragover.prevent="fileDrag"
- @input="resize"
- @paste="paste">
- </textarea>
+ <auto-complete-input v-model="newStatus.status"
+ :classObj="{ 'form-control': true }"
+ :placeholder="$t('post_status.default')"
+ :autoResize="true"
+ :multiline="true"
+ :drop="fileDrop"
+ :dragoverPrevent="fileDrag"
+ :paste="paste"
+ :keydownMetaEnter="postStatusCopy"
+ :keyupCtrlEnter="postStatusCopy"/>
<div class="visibility-tray">
<span class="text-format" v-if="formattingOptionsEnabled">
<label for="post-content-type" class="select">
- <select id="post-content-type" v-model="newStatus.contentType" class="form-control">
+ <select id="post-content-type" v-model="defaultPostContentType" class="form-control">
<option value="text/plain">{{$t('post_status.content_type.plain_text')}}</option>
<option value="text/html">HTML</option>
<option value="text/markdown">Markdown</option>
@@ -52,19 +46,8 @@
</div>
</div>
</div>
- <div style="position:relative;" v-if="candidates">
- <div class="autocomplete-panel">
- <div v-for="candidate in candidates" @click="replace(candidate.utf || (candidate.screen_name + ' '))">
- <div class="autocomplete" :class="{ highlighted: candidate.highlighted }">
- <span v-if="candidate.img"><img :src="candidate.img"></img></span>
- <span v-else>{{candidate.utf}}</span>
- <span>{{candidate.screen_name}}<small>{{candidate.name}}</small></span>
- </div>
- </div>
- </div>
- </div>
<div class='form-bottom'>
- <media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
+ <media-upload ref="mediaUpload" @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
<p v-if="isOverLengthLimit" class="error">{{ charactersLeft }}</p>
<p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p>
@@ -250,52 +233,5 @@
cursor: pointer;
z-index: 4;
}
-
- .autocomplete-panel {
- margin: 0 0.5em 0 0.5em;
- border-radius: $fallback--tooltipRadius;
- border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
- position: absolute;
- z-index: 1;
- box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
- // this doesn't match original but i don't care, making it uniform.
- box-shadow: var(--popupShadow);
- min-width: 75%;
- background: $fallback--bg;
- background: var(--bg, $fallback--bg);
- color: $fallback--lightText;
- color: var(--lightText, $fallback--lightText);
- }
-
- .autocomplete {
- cursor: pointer;
- padding: 0.2em 0.4em 0.2em 0.4em;
- border-bottom: 1px solid rgba(0, 0, 0, 0.4);
- display: flex;
-
- img {
- width: 24px;
- height: 24px;
- border-radius: $fallback--avatarRadius;
- border-radius: var(--avatarRadius, $fallback--avatarRadius);
- object-fit: contain;
- }
-
- span {
- line-height: 24px;
- margin: 0 0.1em 0 0.2em;
- }
-
- small {
- margin-left: .5em;
- color: $fallback--faint;
- color: var(--faint, $fallback--faint);
- }
-
- &.highlighted {
- background-color: $fallback--fg;
- background-color: var(--lightBg, $fallback--fg);
- }
- }
}
</style>
diff --git a/src/components/public_and_external_timeline/public_and_external_timeline.js b/src/components/public_and_external_timeline/public_and_external_timeline.js
index 0db6efae..d45677e0 100644
--- a/src/components/public_and_external_timeline/public_and_external_timeline.js
+++ b/src/components/public_and_external_timeline/public_and_external_timeline.js
@@ -7,7 +7,7 @@ const PublicAndExternalTimeline = {
timeline () { return this.$store.state.statuses.timelines.publicAndExternal }
},
created () {
- this.$store.dispatch('startFetching', 'publicAndExternal')
+ this.$store.dispatch('startFetching', { timeline: 'publicAndExternal' })
},
destroyed () {
this.$store.dispatch('stopFetching', 'publicAndExternal')
diff --git a/src/components/public_and_external_timeline/public_and_external_timeline.vue b/src/components/public_and_external_timeline/public_and_external_timeline.vue
index aded2ead..6be9f955 100644
--- a/src/components/public_and_external_timeline/public_and_external_timeline.vue
+++ b/src/components/public_and_external_timeline/public_and_external_timeline.vue
@@ -1,5 +1,5 @@
<template>
- <Timeline :title="$t('nav.twkn')"v-bind:timeline="timeline" v-bind:timeline-name="'publicAndExternal'"/>
+ <Timeline :title="$t('nav.twkn')" v-bind:timeline="timeline" v-bind:timeline-name="'publicAndExternal'"/>
</template>
<script src="./public_and_external_timeline.js"></script>
diff --git a/src/components/public_timeline/public_timeline.js b/src/components/public_timeline/public_timeline.js
index 9b866be8..64c951ac 100644
--- a/src/components/public_timeline/public_timeline.js
+++ b/src/components/public_timeline/public_timeline.js
@@ -7,7 +7,7 @@ const PublicTimeline = {
timeline () { return this.$store.state.statuses.timelines.public }
},
created () {
- this.$store.dispatch('startFetching', 'public')
+ this.$store.dispatch('startFetching', { timeline: 'public' })
},
destroyed () {
this.$store.dispatch('stopFetching', 'public')
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index 8d138485..534a9839 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -27,6 +27,11 @@ const settings = {
: user.hideUserStats,
hideUserStatsDefault: this.$t('settings.values.' + instance.hideUserStats),
+ hideFilteredStatusesLocal: typeof user.hideFilteredStatuses === 'undefined'
+ ? instance.hideFilteredStatuses
+ : user.hideFilteredStatuses,
+ hideFilteredStatusesDefault: this.$t('settings.values.' + instance.hideFilteredStatuses),
+
notificationVisibilityLocal: user.notificationVisibility,
replyVisibilityLocal: user.replyVisibility,
loopVideoLocal: user.loopVideo,
@@ -46,6 +51,11 @@ const settings = {
: user.subjectLineBehavior,
subjectLineBehaviorDefault: instance.subjectLineBehavior,
+ postContentTypeLocal: typeof user.postContentType === 'undefined'
+ ? instance.postContentType
+ : user.postContentType,
+ postContentTypeDefault: instance.postContentType,
+
alwaysShowSubjectInputLocal: typeof user.alwaysShowSubjectInput === 'undefined'
? instance.alwaysShowSubjectInput
: user.alwaysShowSubjectInput,
@@ -96,6 +106,9 @@ const settings = {
hideUserStatsLocal (value) {
this.$store.dispatch('setOption', { name: 'hideUserStats', value })
},
+ hideFilteredStatusesLocal (value) {
+ this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value })
+ },
hideNsfwLocal (value) {
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
},
@@ -157,6 +170,9 @@ const settings = {
subjectLineBehaviorLocal (value) {
this.$store.dispatch('setOption', { name: 'subjectLineBehavior', value })
},
+ postContentTypeLocal (value) {
+ this.$store.dispatch('setOption', { name: 'postContentType', value })
+ },
stopGifs (value) {
this.$store.dispatch('setOption', { name: 'stopGifs', value })
},
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index 9953780f..dfb2e49d 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -100,6 +100,28 @@
</label>
</div>
</li>
+ <li>
+ <div>
+ {{$t('settings.post_status_content_type')}}
+ <label for="postContentType" class="select">
+ <select id="postContentType" v-model="postContentTypeLocal">
+ <option value="text/plain">
+ {{$t('settings.status_content_type_plain')}}
+ {{postContentTypeDefault == 'text/plain' ? $t('settings.instance_default_simple') : ''}}
+ </option>
+ <option value="text/html">
+ HTML
+ {{postContentTypeDefault == 'text/html' ? $t('settings.instance_default_simple') : ''}}
+ </option>
+ <option value="text/markdown">
+ Markdown
+ {{postContentTypeDefault == 'text/markdown' ? $t('settings.instance_default_simple') : ''}}
+ </option>
+ </select>
+ <i class="icon-down-open"/>
+ </label>
+ </div>
+ </li>
</ul>
</div>
@@ -205,7 +227,6 @@
</label>
</li>
</ul>
- </label>
</div>
<div>
{{$t('settings.replies_in_timeline')}}
@@ -232,11 +253,18 @@
</div>
</div>
<div class="setting-item">
- <p>{{$t('settings.filtering_explanation')}}</p>
- <textarea id="muteWords" v-model="muteWordsString"></textarea>
+ <div>
+ <p>{{$t('settings.filtering_explanation')}}</p>
+ <textarea id="muteWords" v-model="muteWordsString"></textarea>
+ </div>
+ <div>
+ <input type="checkbox" id="hideFilteredStatuses" v-model="hideFilteredStatusesLocal">
+ <label for="hideFilteredStatuses">
+ {{$t('settings.hide_filtered_statuses')}} {{$t('settings.instance_default', { value: hideFilteredStatusesDefault })}}
+ </label>
+ </div>
</div>
</div>
-
</tab-switcher>
</keep-alive>
</div>
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
index 754a57e0..40ffa1dd 100644
--- a/src/components/side_drawer/side_drawer.js
+++ b/src/components/side_drawer/side_drawer.js
@@ -26,6 +26,12 @@ const SideDrawer = {
},
suggestionsEnabled () {
return this.$store.state.instance.suggestionsEnabled
+ },
+ logo () {
+ return this.$store.state.instance.logo
+ },
+ sitename () {
+ return this.$store.state.instance.name
}
},
methods: {
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index fc90977b..a6c6f237 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -8,8 +8,11 @@
@touchmove="touchMove"
>
<div class="side-drawer-heading" @click="toggleDrawer">
- <user-card-content :user="currentUser" :switcher="false" :hideBio="true" v-if="currentUser">
- </user-card-content>
+ <user-card-content :user="currentUser" :switcher="false" :hideBio="true" v-if="currentUser"/>
+ <div class="side-drawer-logo-wrapper" v-else>
+ <img :src="logo"/>
+ <span>{{sitename}}</span>
+ </div>
</div>
<ul>
<li v-if="currentUser" @click="toggleDrawer">
@@ -141,6 +144,24 @@
background-color: var(--bg, $fallback--bg);
}
+.side-drawer-logo-wrapper {
+ display: flex;
+ align-items: center;
+ padding: 0.85em;
+
+ img {
+ flex: none;
+ height: 50px;
+ margin-right: 0.85em;
+ }
+
+ span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+}
+
.side-drawer-click-outside-closed {
flex: 0 0 0;
}
@@ -154,7 +175,6 @@
flex-direction: column;
align-items: stretch;
display: flex;
- min-height: 7em;
padding: 0;
margin: 0;
diff --git a/src/components/status/status.js b/src/components/status/status.js
index c718fe9f..0273a5be 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -4,14 +4,14 @@ import RetweetButton from '../retweet_button/retweet_button.vue'
import DeleteButton from '../delete_button/delete_button.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCardContent from '../user_card_content/user_card_content.vue'
-import StillImage from '../still-image/still-image.vue'
+import UserAvatar from '../user_avatar/user_avatar.vue'
import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
-import { mentionMatchesUrl } from 'src/services/mention_matcher/mention_matcher.js'
-import { filter, find } from 'lodash'
+import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
+import { filter, find, unescape } from 'lodash'
const Status = {
name: 'Status',
@@ -36,6 +36,7 @@ const Status = {
preview: null,
showPreview: false,
showingTall: this.inConversation && this.focused,
+ showingLongSubject: false,
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
? !this.$store.state.instance.collapseMessageWithSubject
: !this.$store.state.config.collapseMessageWithSubject,
@@ -89,6 +90,7 @@ const Status = {
retweet () { return !!this.statusoid.retweeted_status },
retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name },
retweeterHtml () { return this.statusoid.user.name_html },
+ retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) },
status () {
if (this.retweet) {
return this.statusoid.retweeted_status
@@ -108,6 +110,14 @@ const Status = {
return hits
},
muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) },
+ hideFilteredStatuses () {
+ return typeof this.$store.state.config.hideFilteredStatuses === 'undefined'
+ ? this.$store.state.instance.hideFilteredStatuses
+ : this.$store.state.config.hideFilteredStatuses
+ },
+ hideStatus () {
+ return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses)
+ },
isFocused () {
// retweet or root of an expanded conversation
if (this.focused) {
@@ -129,6 +139,9 @@ const Status = {
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
return lengthScore > 20
},
+ longSubject () {
+ return this.status.summary.length > 900
+ },
isReply () {
return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
},
@@ -196,14 +209,15 @@ const Status = {
},
replySubject () {
if (!this.status.summary) return ''
+ const decodedSummary = unescape(this.status.summary)
const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined'
? this.$store.state.instance.subjectLineBehavior
: this.$store.state.config.subjectLineBehavior
- const startsWithRe = this.status.summary.match(/^re[: ]/i)
+ const startsWithRe = decodedSummary.match(/^re[: ]/i)
if (behavior !== 'noop' && startsWithRe || behavior === 'masto') {
- return this.status.summary
+ return decodedSummary
} else if (behavior === 'email') {
- return 're: '.concat(this.status.summary)
+ return 're: '.concat(decodedSummary)
} else if (behavior === 'noop') {
return ''
}
@@ -244,7 +258,7 @@ const Status = {
DeleteButton,
PostStatusForm,
UserCardContent,
- StillImage,
+ UserAvatar,
Gallery,
LinkPreview
},
@@ -268,7 +282,7 @@ const Status = {
}
if (target.tagName === 'A') {
if (target.className.match(/mention/)) {
- const href = target.getAttribute('href')
+ const href = target.href
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
if (attn) {
event.stopPropagation()
@@ -278,6 +292,15 @@ const Status = {
return
}
}
+ if (target.className.match(/hashtag/)) {
+ // Extract tag name from link url
+ const tag = extractTagFromUrl(target.href)
+ if (tag) {
+ const link = this.generateTagLink(tag)
+ this.$router.push(link)
+ return
+ }
+ }
window.open(target.href, '_blank')
}
},
@@ -334,6 +357,9 @@ const Status = {
generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
},
+ generateTagLink (tag) {
+ return `/tag/${tag}`
+ },
setMedia () {
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
return () => this.$store.dispatch('setMedia', attachments)
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 3e3e82bf..aae365d1 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -1,5 +1,5 @@
<template>
- <div class="status-el" v-if="!hideReply && !deleted" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
+ <div class="status-el" v-if="!hideStatus" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
<template v-if="muted && !noReplyLinks">
<div class="media status container muted">
<small>
@@ -13,10 +13,12 @@
</template>
<template v-else>
<div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
- <StillImage v-if="retweet" class='avatar' :class='{ "better-shadow": betterShadow }' :src="statusoid.user.profile_image_url_original"/>
+ <UserAvatar v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/>
<div class="media-body faint">
- <a v-if="retweeterHtml" :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name" v-html="retweeterHtml"></a>
- <a v-else :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
+ <span class="user-name">
+ <router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/>
+ <router-link v-else :to="retweeterProfileLink">{{retweeter}}</router-link>
+ </span>
<i class='fa icon-retweet retweeted' :title="$t('tool_tip.repeat')"></i>
{{$t('timeline.repeated')}}
</div>
@@ -25,7 +27,7 @@
<div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet }]" :style="[ userStyle ]" class="media status">
<div v-if="!noHeading" class="media-left">
<router-link :to="userProfileLink" @click.stop.prevent.capture.native="toggleUserExpanded">
- <StillImage class='avatar' :class="{'avatar-compact': compact, 'better-shadow': betterShadow}" :src="status.user.profile_image_url_original"/>
+ <UserAvatar :compact="compact" :betterShadow="betterShadow" :src="status.user.profile_image_url_original"/>
</router-link>
</div>
<div class="status-body">
@@ -54,7 +56,7 @@
</div>
<h4 class="replies" v-if="inConversation && !noReplyLinks">
<small v-if="replies.length">Replies:</small>
- <small class="reply-link" v-for="reply in replies">
+ <small class="reply-link" v-bind:key="reply.id" v-for="reply in replies">
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}}&nbsp;</a>
</small>
</h4>
@@ -85,7 +87,12 @@
</div>
</div>
- <div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper">
+ <div class="status-content-wrapper" :class="{ 'tall-status': !showingLongSubject }" v-if="longSubject">
+ <a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="!showingLongSubject" href="#" @click.prevent="showingLongSubject=true">Show more</a>
+ <div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html"></div>
+ <a v-if="showingLongSubject" href="#" class="status-unhider" @click.prevent="showingLongSubject=false">Show less</a>
+ </div>
+ <div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper" v-else>
<a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowMore">Show more</a>
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html" v-if="!hideSubjectStatus"></div>
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary_html" v-else></div>
@@ -93,7 +100,7 @@
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">Show less</a>
</div>
- <div v-if="status.attachments && !hideSubjectStatus" class="attachments media-body">
+ <div v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)" class="attachments media-body">
<attachment
class="non-gallery"
v-for="attachment in nonGalleryAttachments"
@@ -413,7 +420,7 @@
padding: 0.4em 0.6em 0 0.6em;
margin: 0;
- .avatar {
+ .avatar.still-image {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
margin-left: 28px;
@@ -431,6 +438,8 @@
.user-name {
font-weight: bold;
+ overflow: hidden;
+ text-overflow: ellipsis;
img {
width: 14px;
@@ -497,46 +506,6 @@
color: var(--cBlue, $fallback--cBlue);
}
-.status .avatar-compact {
- width: 32px;
- height: 32px;
- box-shadow: var(--avatarStatusShadow);
- border-radius: $fallback--avatarAltRadius;
- border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
-
- &.better-shadow {
- box-shadow: var(--avatarStatusShadowInset);
- filter: var(--avatarStatusShadowFilter)
- }
-}
-
-.avatar.still-image {
- width: 48px;
- height: 48px;
- box-shadow: var(--avatarStatusShadow);
- border-radius: $fallback--avatarRadius;
- border-radius: var(--avatarRadius, $fallback--avatarRadius);
- overflow: hidden;
- position: relative;
-
- &.better-shadow {
- box-shadow: var(--avatarStatusShadowInset);
- filter: var(--avatarStatusShadowFilter)
- }
-
- img {
- width: 100%;
- height: 100%;
- }
-
- &.animated::before {
- display: none;
- }
-
- &.retweeted {
- }
-}
-
.status:hover .animated.avatar {
canvas {
display: none;
@@ -594,7 +563,7 @@ a.unmute {
@media all and (max-width: 800px) {
.status-el {
.retweet-info {
- .avatar {
+ .avatar.still-image {
margin-left: 20px;
}
}
@@ -603,14 +572,14 @@ a.unmute {
max-width: 100%;
}
- .status .avatar {
+ .status .avatar.still-image {
width: 40px;
height: 40px;
- }
- .status .avatar-compact {
- width: 32px;
- height: 32px;
+ &.avatar-compact {
+ width: 32px;
+ height: 32px;
+ }
}
}
diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js
index 5ad06dc2..02e98f19 100644
--- a/src/components/still-image/still-image.js
+++ b/src/components/still-image/still-image.js
@@ -2,7 +2,8 @@ const StillImage = {
props: [
'src',
'referrerpolicy',
- 'mimetype'
+ 'mimetype',
+ 'imageLoadError'
],
data () {
return {
@@ -23,6 +24,9 @@ const StillImage = {
canvas.width = width
canvas.height = height
canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height)
+ },
+ onError () {
+ this.imageLoadError && this.imageLoadError()
}
}
}
diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue
index 1dcb7ce6..af824fa2 100644
--- a/src/components/still-image/still-image.vue
+++ b/src/components/still-image/still-image.vue
@@ -1,7 +1,7 @@
<template>
<div class='still-image' :class='{ animated: animated }' >
<canvas ref="canvas" v-if="animated"></canvas>
- <img ref="src" :src="src" :referrerpolicy="referrerpolicy" v-on:load="onLoad"/>
+ <img ref="src" :src="src" :referrerpolicy="referrerpolicy" v-on:load="onLoad" @error="onError"/>
</div>
</template>
diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js
index f9c3f927..423df258 100644
--- a/src/components/tab_switcher/tab_switcher.js
+++ b/src/components/tab_switcher/tab_switcher.js
@@ -37,7 +37,7 @@ export default Vue.component('tab-switcher', {
return (
<div class={ classesWrapper.join(' ')}>
- <button onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button>
+ <button disabled={slot.data.attrs.disabled} onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button>
</div>
)
})
diff --git a/src/components/tag_timeline/tag_timeline.js b/src/components/tag_timeline/tag_timeline.js
index 43de4f49..41b09706 100644
--- a/src/components/tag_timeline/tag_timeline.js
+++ b/src/components/tag_timeline/tag_timeline.js
@@ -3,7 +3,7 @@ import Timeline from '../timeline/timeline.vue'
const TagTimeline = {
created () {
this.$store.commit('clearTimeline', { timeline: 'tag' })
- this.$store.dispatch('startFetching', { 'tag': this.tag })
+ this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag })
},
components: {
Timeline
@@ -15,7 +15,7 @@ const TagTimeline = {
watch: {
tag () {
this.$store.commit('clearTimeline', { timeline: 'tag' })
- this.$store.dispatch('startFetching', { 'tag': this.tag })
+ this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag })
}
},
destroyed () {
diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js
new file mode 100644
index 00000000..e513b993
--- /dev/null
+++ b/src/components/user_avatar/user_avatar.js
@@ -0,0 +1,29 @@
+import StillImage from '../still-image/still-image.vue'
+
+const UserAvatar = {
+ props: [
+ 'src',
+ 'betterShadow',
+ 'compact'
+ ],
+ data () {
+ return {
+ showPlaceholder: false
+ }
+ },
+ components: {
+ StillImage
+ },
+ computed: {
+ imgSrc () {
+ return this.showPlaceholder ? '/images/avi.png' : this.src
+ }
+ },
+ methods: {
+ imageLoadError () {
+ this.showPlaceholder = true
+ }
+ }
+}
+
+export default UserAvatar
diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue
new file mode 100644
index 00000000..6bf7123d
--- /dev/null
+++ b/src/components/user_avatar/user_avatar.vue
@@ -0,0 +1,42 @@
+<template>
+ <StillImage
+ class="avatar"
+ :class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
+ :src="imgSrc"
+ :imageLoadError="imageLoadError"
+ />
+</template>
+
+<script src="./user_avatar.js"></script>
+<style lang="scss">
+@import '../../_variables.scss';
+
+.avatar.still-image {
+ width: 48px;
+ height: 48px;
+ box-shadow: var(--avatarStatusShadow);
+ border-radius: $fallback--avatarRadius;
+ border-radius: var(--avatarRadius, $fallback--avatarRadius);
+
+ img {
+ width: 100%;
+ height: 100%;
+ }
+
+ &.better-shadow {
+ box-shadow: var(--avatarStatusShadowInset);
+ filter: var(--avatarStatusShadowFilter)
+ }
+
+ &.animated::before {
+ display: none;
+ }
+
+ &.avatar-compact {
+ width: 32px;
+ height: 32px;
+ border-radius: $fallback--avatarAltRadius;
+ border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
+ }
+}
+</style>
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 615e6487..a4c84716 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -1,24 +1,33 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
-import StillImage from '../still-image/still-image.vue'
+import UserAvatar from '../user_avatar/user_avatar.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
+import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
const UserCard = {
props: [
'user',
'showFollows',
- 'showApproval'
+ 'showApproval',
+ 'showActions'
],
data () {
return {
- userExpanded: false
+ userExpanded: false,
+ followRequestInProgress: false,
+ followRequestSent: false,
+ updated: false
}
},
components: {
UserCardContent,
- StillImage
+ UserAvatar
},
computed: {
- currentUser () { return this.$store.state.users.currentUser }
+ currentUser () { return this.$store.state.users.currentUser },
+ following () { return this.updated ? this.updated.following : this.user.following },
+ showFollow () {
+ return this.showActions && (!this.showFollows && !this.following || this.updated && !this.updated.following)
+ }
},
methods: {
toggleUserExpanded () {
@@ -34,6 +43,21 @@ const UserCard = {
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
+ },
+ followUser () {
+ this.followRequestInProgress = true
+ requestFollow(this.user, this.$store).then(({ sent, updated }) => {
+ this.followRequestInProgress = false
+ this.followRequestSent = sent
+ this.updated = updated
+ })
+ },
+ unfollowUser () {
+ this.followRequestInProgress = true
+ requestUnfollow(this.user, this.$store).then(({ updated }) => {
+ this.followRequestInProgress = false
+ this.updated = updated
+ })
}
}
}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index cf69606d..12960c02 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -1,28 +1,49 @@
<template>
<div class="card">
- <a href="#">
- <StillImage @click.prevent="toggleUserExpanded" class="avatar" :src="user.profile_image_url"/>
- </a>
+ <router-link :to="userProfileLink(user)">
+ <UserAvatar class="avatar" :compact="true" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
+ </router-link>
<div class="usercard" v-if="userExpanded">
<user-card-content :user="user" :switcher="false"></user-card-content>
</div>
<div class="name-and-screen-name" v-else>
- <div :title="user.name" v-if="user.name_html" class="user-name">
- <span v-html="user.name_html"></span>
+ <div :title="user.name" class="user-name">
+ <span v-if="user.name_html" v-html="user.name_html"></span>
+ <span v-else>{{ user.name }}</span>
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span>
</div>
- <div :title="user.name" v-else class="user-name">
- {{ user.name }}
- <span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
- {{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
- </span>
+ <div class="user-link-action">
+ <router-link class='user-screen-name' :to="userProfileLink(user)">
+ @{{user.screen_name}}
+ </router-link>
+ <button
+ v-if="showFollow"
+ class="btn btn-default"
+ @click="followUser"
+ :disabled="followRequestInProgress"
+ :title="followRequestSent ? $t('user_card.follow_again') : ''"
+ >
+ <template v-if="followRequestInProgress">
+ {{ $t('user_card.follow_progress') }}
+ </template>
+ <template v-else-if="followRequestSent">
+ {{ $t('user_card.follow_sent') }}
+ </template>
+ <template v-else>
+ {{ $t('user_card.follow') }}
+ </template>
+ </button>
+ <button v-if="showActions && showFollows && following" class="btn btn-default" @click="unfollowUser" :disabled="followRequestInProgress">
+ <template v-if="followRequestInProgress">
+ {{ $t('user_card.follow_progress') }}
+ </template>
+ <template v-else>
+ {{ $t('user_card.follow_unfollow') }}
+ </template>
+ </button>
</div>
-
- <router-link class='user-screen-name' :to="userProfileLink(user)">
- @{{user.screen_name}}
- </router-link>
</div>
<div class="approval" v-if="showApproval">
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
@@ -42,6 +63,9 @@
text-align: left;
width: 100%;
.user-name {
+ display: flex;
+ justify-content: space-between;
+
img {
object-fit: contain;
height: 16px;
@@ -49,11 +73,20 @@
vertical-align: middle;
}
}
+
+ .user-link-action {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+
+ button {
+ margin-top: 3px;
+ }
+ }
}
.follows-you {
margin-left: 2em;
- float: right;
}
.card {
@@ -69,17 +102,13 @@
border-bottom-color: var(--border, $fallback--border);
.avatar {
- margin-top: 0.2em;
- width:32px;
- height: 32px;
- border-radius: $fallback--avatarAltRadius;
- border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
+ padding: 0;
}
}
.usercard {
width: fill-available;
- margin: 0.2em 0 0.7em 0;
+ margin: 0.2em 0 0 0.7em;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
diff --git a/src/components/user_card_content/user_card_content.js b/src/components/user_card_content/user_card_content.js
index 541c73b4..7a7b89d4 100644
--- a/src/components/user_card_content/user_card_content.js
+++ b/src/components/user_card_content/user_card_content.js
@@ -1,5 +1,6 @@
-import StillImage from '../still-image/still-image.vue'
+import UserAvatar from '../user_avatar/user_avatar.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js'
+import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
export default {
@@ -79,76 +80,30 @@ export default {
set (color) {
this.$store.dispatch('setHighlight', { user: this.user.screen_name, color })
}
+ },
+ visibleRole () {
+ const validRole = (this.user.role === 'admin' || this.user.role === 'moderator')
+ const showRole = this.isOtherUser || this.user.show_role
+
+ return validRole && showRole && this.user.role
}
},
components: {
- StillImage
+ UserAvatar
},
methods: {
followUser () {
- const store = this.$store
this.followRequestInProgress = true
- store.state.api.backendInteractor.followUser(this.user.id)
- .then((followedUser) => store.commit('addNewUsers', [followedUser]))
- .then(() => {
- // For locked users we just mark it that we sent the follow request
- if (this.user.locked) {
- this.followRequestInProgress = false
- this.followRequestSent = true
- return
- }
-
- if (this.user.following) {
- // If we get result immediately, just stop.
- this.followRequestInProgress = false
- return
- }
-
- // But usually we don't get result immediately, so we ask server
- // for updated user profile to confirm if we are following them
- // Sometimes it takes several tries. Sometimes we end up not following
- // user anyway, probably because they locked themselves and we
- // don't know that yet.
- // Recursive Promise, it will call itself up to 3 times.
- const fetchUser = (attempt) => new Promise((resolve, reject) => {
- setTimeout(() => {
- store.state.api.backendInteractor.fetchUser({ id: this.user.id })
- .then((user) => store.commit('addNewUsers', [user]))
- .then(() => resolve([this.user.following, attempt]))
- .catch((e) => reject(e))
- }, 500)
- }).then(([following, attempt]) => {
- if (!following && attempt <= 3) {
- // If we BE reports that we still not following that user - retry,
- // increment attempts by one
- return fetchUser(++attempt)
- } else {
- // If we run out of attempts, just return whatever status is.
- return following
- }
- })
-
- return fetchUser(1)
- .then((following) => {
- if (following) {
- // We confirmed and everything its good.
- this.followRequestInProgress = false
- } else {
- // If after all the tries, just treat it as if user is locked
- this.followRequestInProgress = false
- this.followRequestSent = true
- }
- })
- })
+ requestFollow(this.user, this.$store).then(({sent}) => {
+ this.followRequestInProgress = false
+ this.followRequestSent = sent
+ })
},
unfollowUser () {
- const store = this.$store
this.followRequestInProgress = true
- store.state.api.backendInteractor.unfollowUser(this.user.id)
- .then((unfollowedUser) => store.commit('addNewUsers', [unfollowedUser]))
- .then(() => {
- this.followRequestInProgress = false
- })
+ requestUnfollow(this.user, this.$store).then(() => {
+ this.followRequestInProgress = false
+ })
},
blockUser () {
const store = this.$store
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
index d1034d68..7f9909c4 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card_content/user_card_content.vue
@@ -4,7 +4,7 @@
<div class='user-info'>
<div class='container'>
<router-link :to="userProfileLink(user)">
- <StillImage class="avatar" :class='{ "better-shadow": betterShadow }' :src="user.profile_image_url_original"/>
+ <UserAvatar :betterShadow="betterShadow" :src="user.profile_image_url_original"/>
</router-link>
<div class="name-and-screen-name">
<div class="top-line">
@@ -19,7 +19,9 @@
</div>
<router-link class='user-screen-name' :to="userProfileLink(user)">
- <span class="handle">@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
+ <span class="handle">@{{user.screen_name}}
+ <span class="alert staff" v-if="!hideBio && !!visibleRole">{{visibleRole}}</span>
+ </span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
<span v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
</router-link>
</div>
@@ -169,23 +171,12 @@
max-height: 56px;
.avatar {
- border-radius: $fallback--avatarRadius;
- border-radius: var(--avatarRadius, $fallback--avatarRadius);
flex: 1 0 100%;
width: 56px;
height: 56px;
box-shadow: 0px 1px 8px rgba(0,0,0,0.75);
box-shadow: var(--avatarShadow);
object-fit: cover;
-
- &.better-shadow {
- box-shadow: var(--avatarShadowInset);
- filter: var(--avatarShadowFilter)
- }
-
- &.animated::before {
- display: none;
- }
}
}
@@ -258,6 +249,15 @@
text-overflow: ellipsis;
overflow: hidden;
}
+
+ // TODO use proper colors
+ .staff {
+ text-transform: capitalize;
+ color: $fallback--text;
+ color: var(--btnText, $fallback--text);
+ background-color: $fallback--fg;
+ background-color: var(--btn, $fallback--fg);
+ }
}
.user-meta {
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 27e138b0..a22b8722 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -1,23 +1,22 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserCard from '../user_card/user_card.vue'
import Timeline from '../timeline/timeline.vue'
+import FollowList from '../follow_list/follow_list.vue'
const UserProfile = {
created () {
this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.commit('clearTimeline', { timeline: 'favorites' })
this.$store.commit('clearTimeline', { timeline: 'media' })
- this.$store.dispatch('startFetching', ['user', this.fetchBy])
- this.$store.dispatch('startFetching', ['media', this.fetchBy])
+ this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy })
+ this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy })
this.startFetchFavorites()
if (!this.user.id) {
this.$store.dispatch('fetchUser', this.fetchBy)
}
},
destroyed () {
- this.$store.dispatch('stopFetching', 'user')
- this.$store.dispatch('stopFetching', 'favorites')
- this.$store.dispatch('stopFetching', 'media')
+ this.cleanUp(this.userId)
},
computed: {
timeline () {
@@ -39,12 +38,6 @@ const UserProfile = {
return this.userId && this.$store.state.users.currentUser.id &&
this.userId === this.$store.state.users.currentUser.id
},
- friends () {
- return this.user.friends
- },
- followers () {
- return this.user.followers
- },
userInStore () {
if (this.isExternal) {
return this.$store.getters.userById(this.userId)
@@ -65,64 +58,56 @@ const UserProfile = {
},
isExternal () {
return this.$route.name === 'external-user-profile'
+ },
+ followsTabVisible () {
+ return this.isUs || !this.user.hide_follows
+ },
+ followersTabVisible () {
+ return this.isUs || !this.user.hide_followers
}
},
methods: {
- fetchFollowers () {
- const id = this.userId
- this.$store.dispatch('addFollowers', { id })
- },
- fetchFriends () {
- const id = this.userId
- this.$store.dispatch('addFriends', { id })
- },
startFetchFavorites () {
if (this.isUs) {
- this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
+ this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.fetchBy })
}
+ },
+ startUp () {
+ this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy })
+ this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy })
+
+ this.startFetchFavorites()
+ },
+ cleanUp () {
+ this.$store.dispatch('stopFetching', 'user')
+ this.$store.dispatch('stopFetching', 'favorites')
+ this.$store.dispatch('stopFetching', 'media')
+ this.$store.commit('clearTimeline', { timeline: 'user' })
+ this.$store.commit('clearTimeline', { timeline: 'favorites' })
+ this.$store.commit('clearTimeline', { timeline: 'media' })
}
},
watch: {
- // TODO get rid of this copypasta
userName () {
if (this.isExternal) {
return
}
- this.$store.dispatch('stopFetching', 'user')
- this.$store.dispatch('stopFetching', 'favorites')
- this.$store.dispatch('stopFetching', 'media')
- this.$store.commit('clearTimeline', { timeline: 'user' })
- this.$store.commit('clearTimeline', { timeline: 'favorites' })
- this.$store.commit('clearTimeline', { timeline: 'media' })
- this.$store.dispatch('startFetching', ['user', this.fetchBy])
- this.$store.dispatch('startFetching', ['media', this.fetchBy])
- this.startFetchFavorites()
+ this.cleanUp()
+ this.startUp()
},
userId () {
if (!this.isExternal) {
return
}
- this.$store.dispatch('stopFetching', 'user')
- this.$store.dispatch('stopFetching', 'favorites')
- this.$store.dispatch('stopFetching', 'media')
- this.$store.commit('clearTimeline', { timeline: 'user' })
- this.$store.commit('clearTimeline', { timeline: 'favorites' })
- this.$store.commit('clearTimeline', { timeline: 'media' })
- this.$store.dispatch('startFetching', ['user', this.fetchBy])
- this.$store.dispatch('startFetching', ['media', this.fetchBy])
- this.startFetchFavorites()
- },
- user () {
- if (this.user.id && !this.user.followers) {
- this.fetchFollowers()
- this.fetchFriends()
- }
+ this.cleanUp()
+ this.startUp()
}
},
components: {
UserCardContent,
UserCard,
- Timeline
+ Timeline,
+ FollowList
}
}
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index e53ce4cc..79461291 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -9,40 +9,28 @@
<tab-switcher :renderOnlyFocused="true">
<Timeline
:label="$t('user_card.statuses')"
+ :disabled="!user.statuses_count"
:embedded="true"
:title="$t('user_profile.timeline_title')"
:timeline="timeline"
:timeline-name="'user'"
:user-id="fetchBy"
/>
- <div :label="$t('user_card.followees')">
- <div v-if="friends">
- <user-card
- v-for="friend in friends"
- :key="friend.id"
- :user="friend"
- :showFollows="true"
- />
- </div>
+ <div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
+ <FollowList v-if="user.friends_count > 0" :userId="userId" :showFollowers="false" />
<div class="userlist-placeholder" v-else>
<i class="icon-spin3 animate-spin"></i>
</div>
</div>
- <div :label="$t('user_card.followers')">
- <div v-if="followers">
- <user-card
- v-for="follower in followers"
- :key="follower.id"
- :user="follower"
- :showFollows="false"
- />
- </div>
+ <div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
+ <FollowList v-if="user.followers_count > 0" :userId="userId" :showFollowers="true" />
<div class="userlist-placeholder" v-else>
<i class="icon-spin3 animate-spin"></i>
</div>
</div>
<Timeline
:label="$t('user_card.media')"
+ :disabled="!media.visibleStatuses.length"
:embedded="true" :title="$t('user_card.media')"
timeline-name="media"
:timeline="media"
@@ -51,6 +39,7 @@
<Timeline
v-if="isUs"
:label="$t('user_card.favorites')"
+ :disabled="!favorites.visibleStatuses.length"
:embedded="true"
:title="$t('user_card.favorites')"
timeline-name="favorites"
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 9bd8aa00..fa389c3b 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -1,16 +1,22 @@
+import { unescape } from 'lodash'
+
import TabSwitcher from '../tab_switcher/tab_switcher.js'
import StyleSwitcher from '../style_switcher/style_switcher.vue'
+import AutoCompleteInput from '../autocomplete_input/autocomplete_input.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
const UserSettings = {
data () {
return {
newName: this.$store.state.users.currentUser.name,
- newBio: this.$store.state.users.currentUser.description,
+ newBio: unescape(this.$store.state.users.currentUser.description),
newLocked: this.$store.state.users.currentUser.locked,
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
newDefaultScope: this.$store.state.users.currentUser.default_scope,
- newHideNetwork: this.$store.state.users.currentUser.hide_network,
+ hideFollows: this.$store.state.users.currentUser.hide_follows,
+ hideFollowers: this.$store.state.users.currentUser.hide_followers,
+ showRole: this.$store.state.users.currentUser.show_role,
+ role: this.$store.state.users.currentUser.role,
followList: null,
followImportError: false,
followsImported: false,
@@ -36,7 +42,8 @@ const UserSettings = {
},
components: {
StyleSwitcher,
- TabSwitcher
+ TabSwitcher,
+ AutoCompleteInput
},
computed: {
user () {
@@ -66,7 +73,10 @@ const UserSettings = {
/* eslint-disable camelcase */
const default_scope = this.newDefaultScope
const no_rich_text = this.newNoRichText
- const hide_network = this.newHideNetwork
+ const hide_follows = this.hideFollows
+ const hide_followers = this.hideFollowers
+ const show_role = this.showRole
+
/* eslint-enable camelcase */
this.$store.state.api.backendInteractor
.updateProfile({
@@ -78,7 +88,9 @@ const UserSettings = {
/* eslint-disable camelcase */
default_scope,
no_rich_text,
- hide_network
+ hide_follows,
+ hide_followers,
+ show_role
/* eslint-enable camelcase */
}}).then((user) => {
if (!user.error) {
@@ -233,7 +245,9 @@ const UserSettings = {
exportFollows () {
this.enableFollowsExport = false
this.$store.state.api.backendInteractor
- .fetchFriends({id: this.$store.state.users.currentUser.id})
+ .exportFriends({
+ id: this.$store.state.users.currentUser.id
+ })
.then((friendList) => {
this.exportPeople(friendList, 'friends.csv')
setTimeout(() => { this.enableFollowsExport = true }, 2000)
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index be42cc4a..ad7c17bd 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -9,9 +9,9 @@
<div class="setting-item" >
<h2>{{$t('settings.name_bio')}}</h2>
<p>{{$t('settings.name')}}</p>
- <input class='name-changer' id='username' v-model="newName"></input>
+ <auto-complete-input :classObj="{ 'name-changer': true }" :id="'username'" v-model="newName"/>
<p>{{$t('settings.bio')}}</p>
- <textarea class="bio" v-model="newBio"></textarea>
+ <auto-complete-input :classObj="{ bio: true }" v-model="newBio" :multiline="true"/>
<p>
<input type="checkbox" v-model="newLocked" id="account-locked">
<label for="account-locked">{{$t('settings.lock_account_description')}}</label>
@@ -30,10 +30,19 @@
<label for="account-no-rich-text">{{$t('settings.no_rich_text_description')}}</label>
</p>
<p>
- <input type="checkbox" v-model="newHideNetwork" id="account-hide-network">
- <label for="account-hide-network">{{$t('settings.hide_network_description')}}</label>
+ <input type="checkbox" v-model="hideFollows" id="account-hide-follows">
+ <label for="account-hide-follows">{{$t('settings.hide_follows_description')}}</label>
</p>
- <button :disabled='newName.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
+ <p>
+ <input type="checkbox" v-model="hideFollowers" id="account-hide-followers">
+ <label for="account-hide-followers">{{$t('settings.hide_followers_description')}}</label>
+ </p>
+ <p>
+ <input type="checkbox" v-model="showRole" id="account-show-role">
+ <label for="account-show-role" v-if="role === 'admin'">{{$t('settings.show_admin_badge')}}</label>
+ <label for="account-show-role" v-if="role === 'moderator'">{{$t('settings.show_moderator_badge')}}</label>
+ </p>
+ <button :disabled='newName && newName.length === 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
</div>
<div class="setting-item">
<h2>{{$t('settings.avatar')}}</h2>
@@ -169,7 +178,7 @@
}
.banner {
- max-width: 400px;
+ max-width: 100%;
}
.uploading {
@@ -180,5 +189,9 @@
.name-changer {
width: 100%;
}
+
+ .bg {
+ max-width: 100%;
+ }
}
</style>
diff --git a/src/i18n/de.json b/src/i18n/de.json
index c87371e6..d0bfba38 100644
--- a/src/i18n/de.json
+++ b/src/i18n/de.json
@@ -132,6 +132,7 @@
"preload_images": "Bilder vorausladen",
"hide_post_stats": "Beitragsstatistiken verbergen (z.B. die Anzahl der Favoriten)",
"hide_user_stats": "Benutzerstatistiken verbergen (z.B. die Anzahl der Follower)",
+ "hide_filtered_statuses": "Gefilterte Beiträge verbergen",
"import_followers_from_a_csv_file": "Importiere Follower, denen du folgen möchtest, aus einer CSV-Datei",
"import_theme": "Farbschema laden",
"inputRadius": "Eingabefelder",
@@ -155,7 +156,8 @@
"notification_visibility_mentions": "Erwähnungen",
"notification_visibility_repeats": "Wiederholungen",
"no_rich_text_description": "Rich-Text Formatierungen von allen Beiträgen entfernen",
- "hide_network_description": "Zeige nicht, wem ich folge und wer mir folgt",
+ "hide_follows_description": "Zeige nicht, wem ich folge",
+ "hide_followers_description": "Zeige nicht, wer mir folgt",
"nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind",
"panelRadius": "Panel",
"pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist",
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 29ac2b9a..c664fbfa 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -17,7 +17,9 @@
},
"general": {
"apply": "Apply",
- "submit": "Submit"
+ "submit": "Submit",
+ "more": "More",
+ "generic_error": "An error occured"
},
"login": {
"login": "Log in",
@@ -26,7 +28,8 @@
"password": "Password",
"placeholder": "e.g. lain",
"register": "Register",
- "username": "Username"
+ "username": "Username",
+ "hint": "Log in to join the discussion"
},
"nav": {
"about": "About",
@@ -137,6 +140,7 @@
"use_one_click_nsfw": "Open NSFW attachments with just one click",
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
+ "hide_filtered_statuses": "Hide filtered statuses",
"import_followers_from_a_csv_file": "Import follows from a csv file",
"import_theme": "Load preset",
"inputRadius": "Input fields",
@@ -162,7 +166,10 @@
"notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats",
"no_rich_text_description": "Strip rich text formatting from all posts",
- "hide_network_description": "Don't show who I'm following and who's following me",
+ "hide_follows_description": "Don't show who I'm following",
+ "hide_followers_description": "Don't show who's following me",
+ "show_admin_badge": "Show Admin badge in my profile",
+ "show_moderator_badge": "Show Moderator badge in my profile",
"nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
"panelRadius": "Panels",
"pause_on_unfocused": "Pause streaming when tab is not focused",
@@ -189,6 +196,8 @@
"subject_line_email": "Like email: \"re: subject\"",
"subject_line_mastodon": "Like mastodon: copy as is",
"subject_line_noop": "Do not copy",
+ "post_status_content_type": "Post status content type",
+ "status_content_type_plain": "Plain text",
"stop_gifs": "Play-on-hover GIFs",
"streaming": "Enable automatic streaming of new posts when scrolled to the top",
"text": "Text",
@@ -364,9 +373,9 @@
},
"upload":{
"error": {
- "base": "Upload failed.",
- "file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
- "default": "Try again later"
+ "base": "Upload failed.",
+ "file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
+ "default": "Try again later"
},
"file_size_units": {
"B": "B",
diff --git a/src/i18n/es.json b/src/i18n/es.json
index 3391c6af..29c8aec4 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -2,99 +2,386 @@
"chat": {
"title": "Chat"
},
+ "features_panel": {
+ "chat": "Chat",
+ "gopher": "Gopher",
+ "media_proxy": "Media proxy",
+ "scope_options": "Opciones del alcance de la visibilidad",
+ "text_limit": "Límite de carácteres",
+ "title": "Características",
+ "who_to_follow": "A quién seguir"
+ },
"finder": {
"error_fetching_user": "Error al buscar usuario",
"find_user": "Encontrar usuario"
},
"general": {
"apply": "Aplicar",
- "submit": "Enviar"
+ "submit": "Enviar",
+ "more": "Más",
+ "generic_error": "Ha ocurrido un error"
},
"login": {
"login": "Identificación",
+ "description": "Identificación con OAuth",
"logout": "Salir",
"password": "Contraseña",
"placeholder": "p.ej. lain",
"register": "Registrar",
- "username": "Usuario"
+ "username": "Usuario",
+ "hint": "Inicia sesión para unirte a la discusión"
},
"nav": {
+ "about": "Sobre",
+ "back": "Volver",
"chat": "Chat Local",
+ "friend_requests": "Solicitudes de amistad",
"mentions": "Menciones",
+ "dms": "Mensajes Directo",
"public_tl": "Línea Temporal Pública",
"timeline": "Línea Temporal",
- "twkn": "Toda La Red Conocida"
+ "twkn": "Toda La Red Conocida",
+ "user_search": "Búsqueda de Usuarios",
+ "who_to_follow": "A quién seguir",
+ "preferences": "Preferencias"
},
"notifications": {
+ "broken_favorite": "Estado desconocido, buscándolo...",
+ "favorited_you": "le gusta tu estado",
"followed_you": "empezó a seguirte",
+ "load_older": "Cargar notificaciones antiguas",
"notifications": "Notificaciones",
- "read": "¡Leído!"
+ "read": "¡Leído!",
+ "repeated_you": "repite tu estado",
+ "no_more_notifications": "No hay más notificaciones"
},
"post_status": {
+ "new_status": "Publicar un nuevo estado",
+ "account_not_locked_warning": "Tu cuenta no está {0}. Cualquiera puede seguirte y leer las entradas para Solo-Seguidores.",
+ "account_not_locked_warning_link": "bloqueada",
+ "attachments_sensitive": "Contenido sensible",
+ "content_type": {
+ "plain_text": "Texto Plano"
+ },
+ "content_warning": "Tema (opcional)",
"default": "Acabo de aterrizar en L.A.",
- "posting": "Publicando"
+ "direct_warning": "Esta entrada solo será visible para los usuarios mencionados.",
+ "posting": "Publicando",
+ "scope": {
+ "direct": "Directo - Solo para los usuarios mencionados.",
+ "private": "Solo-Seguidores - Solo tus seguidores leeran la entrada",
+ "public": "Público - Entradas visibles en las Líneas Temporales Públicas",
+ "unlisted": "Sin Listar - Entradas no visibles en las Líneas Temporales Públicas"
+ }
},
"registration": {
"bio": "Biografía",
"email": "Correo electrónico",
"fullname": "Nombre a mostrar",
"password_confirm": "Confirmación de contraseña",
- "registration": "Registro"
+ "registration": "Registro",
+ "token": "Token de invitación",
+ "captcha": "CAPTCHA",
+ "new_captcha": "Click en la imagen para obtener un nuevo captca",
+ "validations": {
+ "username_required": "no puede estar vacío",
+ "fullname_required": "no puede estar vacío",
+ "email_required": "no puede estar vacío",
+ "password_required": "no puede estar vacío",
+ "password_confirmation_required": "no puede estar vacío",
+ "password_confirmation_match": "la contraseña no coincide"
+ }
},
"settings": {
+ "attachmentRadius": "Adjuntos",
"attachments": "Adjuntos",
"autoload": "Activar carga automática al llegar al final de la página",
"avatar": "Avatar",
- "background": "Segundo plano",
+ "avatarAltRadius": "Avatares (Notificaciones)",
+ "avatarRadius": "Avatares",
+ "background": "Fondo",
"bio": "Biografía",
+ "btnRadius": "Botones",
+ "cBlue": "Azul (Responder, seguir)",
+ "cGreen": "Verde (Retweet)",
+ "cOrange": "Naranja (Favorito)",
+ "cRed": "Rojo (Cancelar)",
+ "change_password": "Cambiar contraseña",
+ "change_password_error": "Hubo un problema cambiando la contraseña.",
+ "changed_password": "Contraseña cambiada correctamente!",
+ "collapse_subject": "Colapsar entradas con tema",
+ "composing": "Redactando",
+ "confirm_new_password": "Confirmar la nueva contraseña",
"current_avatar": "Tu avatar actual",
- "current_profile_banner": "Cabecera actual",
+ "current_password": "Contraseña actual",
+ "current_profile_banner": "Tu cabecera actual",
+ "data_import_export_tab": "Importar / Exportar Datos",
+ "default_vis": "Alcance de visibilidad por defecto",
+ "delete_account": "Eliminar la cuenta",
+ "delete_account_description": "Eliminar para siempre la cuenta y todos los mensajes.",
+ "delete_account_error": "Hubo un error al eliminar tu cuenta. Si el fallo persiste, ponte en contacto con el administrador de tu instancia.",
+ "delete_account_instructions": "Escribe tu contraseña para confirmar la eliminación de tu cuenta.",
+ "avatar_size_instruction": "El tamaño mínimo recomendado para el avatar es de 150X150 píxeles.",
+ "export_theme": "Exportar tema",
"filtering": "Filtros",
"filtering_explanation": "Todos los estados que contengan estas palabras serán silenciados, una por línea",
+ "follow_export": "Exportar personas que tú sigues",
+ "follow_export_button": "Exporta tus seguidores a un archivo csv",
+ "follow_export_processing": "Procesando, en breve se te preguntará para guardar el archivo",
"follow_import": "Importar personas que tú sigues",
"follow_import_error": "Error al importal el archivo",
"follows_imported": "¡Importado! Procesarlos llevará tiempo.",
"foreground": "Primer plano",
+ "general": "General",
"hide_attachments_in_convo": "Ocultar adjuntos en las conversaciones",
"hide_attachments_in_tl": "Ocultar adjuntos en la línea temporal",
- "import_followers_from_a_csv_file": "Importar personas que tú sigues apartir de un archivo csv",
- "links": "Links",
+ "hide_isp": "Ocultar el panel específico de la instancia",
+ "preload_images": "Precargar las imágenes",
+ "use_one_click_nsfw": "Abrir los adjuntos NSFW con un solo click.",
+ "hide_post_stats": "Ocultar las estadísticas de las entradas (p.ej. el número de favoritos)",
+ "hide_user_stats": "Ocultar las estadísticas del usuario (p.ej. el número de seguidores)",
+ "import_followers_from_a_csv_file": "Importar personas que tú sigues a partir de un archivo csv",
+ "import_theme": "Importar tema",
+ "inputRadius": "Campos de entrada",
+ "checkboxRadius": "Casillas de verificación",
+ "instance_default": "(por defecto: {value})",
+ "instance_default_simple": "(por defecto)",
+ "interface": "Interfaz",
+ "interfaceLanguage": "Idioma",
+ "invalid_theme_imported": "El archivo importado no es un tema válido de Pleroma. No se han realizado cambios.",
+ "limited_availability": "No disponible en tu navegador",
+ "links": "Enlaces",
+ "lock_account_description": "Restringir el acceso a tu cuenta solo a seguidores admitidos",
+ "loop_video": "Vídeos en bucle",
+ "loop_video_silent_only": "Bucle solo en vídeos sin sonido (p.ej. \"gifs\" de Mastodon)",
+ "play_videos_in_modal": "Reproducir los vídeos directamente en el visor de medios",
+ "use_contain_fit": "No recortar los adjuntos en miniaturas",
"name": "Nombre",
"name_bio": "Nombre y Biografía",
+ "new_password": "Nueva contraseña",
+ "notification_visibility": "Tipos de notificaciones a mostrar",
+ "notification_visibility_follows": "Nuevos seguidores",
+ "notification_visibility_likes": "Me gustan (Likes)",
+ "notification_visibility_mentions": "Menciones",
+ "notification_visibility_repeats": "Repeticiones (Repeats)",
+ "no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas",
+ "hide_follows_description": "No mostrar a quién sigo",
+ "hide_followers_description": "No mostrar quién me sigue",
+ "show_admin_badge": "Mostrar la placa de administrador en mi perfil",
+ "show_moderator_badge": "Mostrar la placa de moderador en mi perfil",
"nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW",
+ "panelRadius": "Paneles",
+ "pause_on_unfocused": "Parar la transmisión cuando no estés en foco.",
"presets": "Por defecto",
"profile_background": "Fondo del Perfil",
- "profile_banner": "Cabecera del perfil",
- "reply_link_preview": "Activar la previsualización del enlace de responder al pasar el ratón por encima",
+ "profile_banner": "Cabecera del Perfil",
+ "profile_tab": "Perfil",
+ "radii_help": "Estable el redondeo de las esquinas del interfaz (en píxeles)",
+ "replies_in_timeline": "Réplicas en la línea temporal",
+ "reply_link_preview": "Activar la previsualización del enlace de responder al pasar el ratón por encim",
+ "reply_visibility_all": "Mostrar todas las réplicas",
+ "reply_visibility_following": "Solo mostrar réplicas para mí o usuarios a los que sigo",
+ "reply_visibility_self": "Solo mostrar réplicas para mí",
+ "saving_err": "Error al guardar los ajustes",
+ "saving_ok": "Ajustes guardados",
+ "security_tab": "Seguridad",
+ "scope_copy": "Copiar la visibilidad cuando contestamos (En los mensajes directos (MDs) siempre se copia)",
"set_new_avatar": "Cambiar avatar",
"set_new_profile_background": "Cambiar fondo del perfil",
- "set_new_profile_banner": "Cambiar cabecera",
+ "set_new_profile_banner": "Cambiar cabecera del perfil",
"settings": "Ajustes",
+ "subject_input_always_show": "Mostrar siempre el campo del tema",
+ "subject_line_behavior": "Copiar el tema en las contestaciones",
+ "subject_line_email": "Tipo email: \"re: tema\"",
+ "subject_line_mastodon": "Tipo mastodon: copiar como es",
+ "subject_line_noop": "No copiar",
+ "post_status_content_type": "Formato de publicación",
+ "status_content_type_plain": "Texto plano",
+ "stop_gifs": "Iniciar GIFs al pasar el ratón",
"streaming": "Habilite la transmisión automática de nuevas publicaciones cuando se desplaza hacia la parte superior",
"text": "Texto",
"theme": "Tema",
"theme_help": "Use códigos de color hexadecimales (#rrggbb) para personalizar su tema de colores.",
- "user_settings": "Ajustes de Usuario"
+ "theme_help_v2_1": "También puede invalidar los colores y la opacidad de ciertos componentes si activa la casilla de verificación, use el botón \"Borrar todo\" para deshacer los cambios.",
+ "theme_help_v2_2": "Los iconos debajo de algunas entradas son indicadores de contraste de fondo/texto, desplace el ratón para obtener información detallada. Tenga en cuenta que cuando se utilizan indicadores de contraste de transparencia se muestra el peor caso posible.",
+ "tooltipRadius": "Información/alertas",
+ "user_settings": "Ajustes de Usuario",
+ "values": {
+ "false": "no",
+ "true": "sí"
+ },
+ "notifications": "Notificaciones",
+ "enable_web_push_notifications": "Habilitar las notificiaciones en el navegador",
+ "style": {
+ "switcher": {
+ "keep_color": "Mantener colores",
+ "keep_shadows": "Mantener sombras",
+ "keep_opacity": "Mantener opacidad",
+ "keep_roundness": "Mantener redondeces",
+ "keep_fonts": "Mantener fuentes",
+ "save_load_hint": "Las opciones \"Mantener\" conservan las opciones configuradas actualmente al seleccionar o cargar temas, también almacena dichas opciones al exportar un tema. Cuando se desactiven todas las casillas de verificación, el tema de exportación lo guardará todo.",
+ "reset": "Reiniciar",
+ "clear_all": "Limpiar todo",
+ "clear_opacity": "Limpiar opacidad"
+ },
+ "common": {
+ "color": "Color",
+ "opacity": "Opacidad",
+ "contrast": {
+ "hint": "El ratio de contraste es {ratio}. {level} {context}",
+ "level": {
+ "aa": "Cumple con la pauta de nivel AA (mínimo)",
+ "aaa": "Cumple con la pauta de nivel AAA (recomendado)",
+ "bad": "No cumple con las pautas de accesibilidad"
+ },
+ "context": {
+ "18pt": "para textos grandes (+18pt)",
+ "text": "para textos"
+ }
+ }
+ },
+ "common_colors": {
+ "_tab_label": "Común",
+ "main": "Colores comunes",
+ "foreground_hint": "Vea la pestaña \"Avanzado\" para un control más detallado",
+ "rgbo": "Iconos, acentos, insignias"
+ },
+ "advanced_colors": {
+ "_tab_label": "Avanzado",
+ "alert": "Fondo de Alertas",
+ "alert_error": "Error",
+ "badge": "Fondo de Insignias",
+ "badge_notification": "Notificaciones",
+ "panel_header": "Cabecera del panel",
+ "top_bar": "Barra superior",
+ "borders": "Bordes",
+ "buttons": "Botones",
+ "inputs": "Campos de entrada",
+ "faint_text": "Texto desvanecido"
+ },
+ "radii": {
+ "_tab_label": "Redondez"
+ },
+ "shadows": {
+ "_tab_label": "Sombra e iluminación",
+ "component": "Componente",
+ "override": "Sobreescribir",
+ "shadow_id": "Sombra #{value}",
+ "blur": "Difuminar",
+ "spread": "Cantidad",
+ "inset": "Insertada",
+ "hint": "Para las sombras, también puede usar --variable como un valor de color para usar las variables CSS3. Tenga en cuenta que establecer la opacidad no funcionará en este caso.",
+ "filter_hint": {
+ "always_drop_shadow": "Advertencia, esta sombra siempre usa {0} cuando el navegador lo soporta.",
+ "drop_shadow_syntax": "{0} no soporta el parámetro {1} y la palabra clave {2}.",
+ "avatar_inset": "Tenga en cuenta que la combinación de sombras insertadas como no-insertadas en los avatares, puede dar resultados inesperados con los avatares transparentes.",
+ "spread_zero": "Sombras con una cantidad > 0 aparecerá como si estuviera puesto a cero",
+ "inset_classic": "Las sombras insertadas estarán usando {0}"
+ },
+ "components": {
+ "panel": "Panel",
+ "panelHeader": "Cabecera del panel",
+ "topBar": "Barra superior",
+ "avatar": "Avatar del usuario (en la vista del perfil)",
+ "avatarStatus": "Avatar del usuario (en la vista de la entrada)",
+ "popup": "Ventanas y textos emergentes (popups & tooltips)",
+ "button": "Botones",
+ "buttonHover": "Botón (encima)",
+ "buttonPressed": "Botón (presionado)",
+ "buttonPressedHover": "Botón (presionado+encima)",
+ "input": "Campo de entrada"
+ }
+ },
+ "fonts": {
+ "_tab_label": "Fuentes",
+ "help": "Seleccione la fuente para utilizar para los elementos de la interfaz de usuario. Para \"personalizado\", debe ingresar el nombre exacto de la fuente tal como aparece en el sistema.",
+ "components": {
+ "interface": "Interfaz",
+ "input": "Campos de entrada",
+ "post": "Texto de publicaciones",
+ "postCode": "Texto monoespaciado en publicación (texto enriquecido)"
+ },
+ "family": "Nombre de la fuente",
+ "size": "Tamaño (en px)",
+ "weight": "Peso (negrita)",
+ "custom": "Personalizado"
+ },
+ "preview": {
+ "header": "Vista previa",
+ "content": "Contenido",
+ "error": "Ejemplo de error",
+ "button": "Botón",
+ "text": "Un montón de {0} y {1}",
+ "mono": "contenido",
+ "input": "Acaba de aterrizar en L.A.",
+ "faint_link": "manual útil",
+ "fine_print": "¡Lea nuestro {0} para aprender nada útil!",
+ "header_faint": "Esto está bien",
+ "checkbox": "He revisado los términos y condiciones",
+ "link": "un bonito enlace"
+ }
+ }
},
"timeline": {
+ "collapse": "Colapsar",
"conversation": "Conversación",
"error_fetching": "Error al cargar las actualizaciones",
"load_older": "Cargar actualizaciones anteriores",
+ "no_retweet_hint": "La publicación está marcada como solo para seguidores o directa y no se puede repetir",
+ "repeated": "repetida",
"show_new": "Mostrar lo nuevo",
- "up_to_date": "Actualizado"
+ "up_to_date": "Actualizado",
+ "no_more_statuses": "No hay más estados"
},
"user_card": {
+ "approve": "Aprovar",
"block": "Bloquear",
"blocked": "¡Bloqueado!",
+ "deny": "Denegar",
+ "favorites": "Favoritos",
"follow": "Seguir",
+ "follow_sent": "¡Solicitud enviada!",
+ "follow_progress": "Solicitando…",
+ "follow_again": "¿Enviar solicitud de nuevo?",
+ "follow_unfollow": "Dejar de seguir",
"followees": "Siguiendo",
"followers": "Seguidores",
"following": "¡Siguiendo!",
"follows_you": "¡Te sigue!",
+ "its_you": "¡Eres tú!",
+ "media": "Media",
"mute": "Silenciar",
"muted": "Silenciado",
"per_day": "por día",
"remote_follow": "Seguir",
"statuses": "Estados"
+ },
+ "user_profile": {
+ "timeline_title": "Linea temporal del usuario"
+ },
+ "who_to_follow": {
+ "more": "Más",
+ "who_to_follow": "A quién seguir"
+ },
+ "tool_tip": {
+ "media_upload": "Subir Medios",
+ "repeat": "Repetir",
+ "reply": "Contestar",
+ "favorite": "Favorito",
+ "user_settings": "Ajustes de usuario"
+ },
+ "upload":{
+ "error": {
+ "base": "Subida fallida.",
+ "file_too_big": "Archivo demasiado grande [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
+ "default": "Inténtalo más tarde"
+ },
+ "file_size_units": {
+ "B": "B",
+ "KiB": "KiB",
+ "MiB": "MiB",
+ "GiB": "GiB",
+ "TiB": "TiB"
+ }
}
}
diff --git a/src/i18n/fi.json b/src/i18n/fi.json
index 0d62f295..5a0c1ea8 100644
--- a/src/i18n/fi.json
+++ b/src/i18n/fi.json
@@ -17,7 +17,9 @@
},
"general": {
"apply": "Aseta",
- "submit": "Lähetä"
+ "submit": "Lähetä",
+ "more": "Lisää",
+ "generic_error": "Virhe tapahtui"
},
"login": {
"login": "Kirjaudu sisään",
diff --git a/src/i18n/ja.json b/src/i18n/ja.json
index 161856f0..afce03a4 100644
--- a/src/i18n/ja.json
+++ b/src/i18n/ja.json
@@ -157,7 +157,8 @@
"notification_visibility_mentions": "メンション",
"notification_visibility_repeats": "リピート",
"no_rich_text_description": "リッチテキストをつかわない",
- "hide_network_description": "わたしがフォローしているひとと、わたしをフォローしているひとを、みせない",
+ "hide_follows_description": "フォローしている人を表示しない",
+ "hide_followers_description": "フォローしている人を表示しない",
"nsfw_clickthrough": "NSFWなファイルをかくす",
"panelRadius": "パネル",
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
diff --git a/src/i18n/ko.json b/src/i18n/ko.json
index 4b69df07..f9e4dfa3 100644
--- a/src/i18n/ko.json
+++ b/src/i18n/ko.json
@@ -156,7 +156,8 @@
"notification_visibility_mentions": "멘션",
"notification_visibility_repeats": "반복",
"no_rich_text_description": "모든 게시물의 서식을 지우기",
- "hide_network_description": "내 팔로우와 팔로워를 숨기기",
+ "hide_follows_description": "내가 팔로우하는 사람을 표시하지 않음",
+ "hide_followers_description": "나를 따르는 사람을 보여주지 마라.",
"nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화",
"panelRadius": "패널",
"pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기",
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 0887bb59..4b0bd4b4 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -127,7 +127,10 @@
"notification_visibility_mentions": "Упоминания",
"notification_visibility_repeats": "Повторы",
"no_rich_text_description": "Убрать форматирование из всех постов",
- "hide_network_description": "Не показывать кого я читаю и кто меня читает",
+ "hide_follows_description": "Не показывать кого я читаю",
+ "hide_followers_description": "Не показывать кто читает меня",
+ "show_admin_badge": "Показывать значок администратора в моем профиле",
+ "show_moderator_badge": "Показывать значок модератора в моем профиле",
"nsfw_clickthrough": "Включить скрытие NSFW вложений",
"panelRadius": "Панели",
"pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",
diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js
index ccd92633..6f7202ce 100644
--- a/src/lib/persisted_state.js
+++ b/src/lib/persisted_state.js
@@ -48,7 +48,7 @@ export default function createPersistedState ({
return getState(key, storage).then((savedState) => {
return store => {
try {
- if (typeof savedState === 'object') {
+ if (savedState !== null && typeof savedState === 'object') {
// build user cache
const usersState = savedState.users || {}
usersState.usersObject = {}
diff --git a/src/modules/api.js b/src/modules/api.js
index 7bda13e7..31cb55c6 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -1,5 +1,4 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
-import {isArray} from 'lodash'
import { Socket } from 'phoenix'
const api = {
@@ -34,20 +33,12 @@ const api = {
}
},
actions: {
- startFetching (store, timeline) {
- let userId = false
-
- // This is for user timelines
- if (isArray(timeline)) {
- userId = timeline[1]
- timeline = timeline[0]
- }
-
+ startFetching (store, {timeline = 'friends', tag = false, userId = false}) {
// Don't start fetching if we already are.
- if (!store.state.fetchers[timeline]) {
- const fetcher = store.state.backendInteractor.startFetching({timeline, store, userId})
- store.commit('addFetcher', {timeline, fetcher})
- }
+ if (store.state.fetchers[timeline]) return
+
+ const fetcher = store.state.backendInteractor.startFetching({ timeline, store, userId, tag })
+ store.commit('addFetcher', { timeline, fetcher })
},
stopFetching (store, timeline) {
const fetcher = store.state.fetchers[timeline]
diff --git a/src/modules/config.js b/src/modules/config.js
index c9528f6f..71f71376 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -30,7 +30,8 @@ const defaultState = {
interfaceLanguage: browserLocale,
scopeCopy: undefined, // instance default
subjectLineBehavior: undefined, // instance default
- alwaysShowSubjectInput: undefined // instance default
+ alwaysShowSubjectInput: undefined, // instance default
+ postContentType: undefined // instance default
}
const config = {
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 4ad41873..59c6b91c 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -21,13 +21,16 @@ const defaultState = {
collapseMessageWithSubject: false,
hidePostStats: false,
hideUserStats: false,
+ hideFilteredStatuses: true,
disableChat: false,
scopeCopy: true,
subjectLineBehavior: 'email',
+ postContentType: 'text/plain',
loginMethod: 'password',
nsfwCensorImage: undefined,
vapidPublicKey: undefined,
noAttachmentLinks: false,
+ showFeaturesPanel: true,
// Nasty stuff
pleromaBackend: true,
@@ -63,9 +66,11 @@ const instance = {
case 'name':
dispatch('setPageTitle')
break
- case 'theme':
- setPreset(value, commit)
}
+ },
+ setTheme ({ commit }, themeName) {
+ commit('setInstanceOption', { name: 'theme', value: themeName })
+ return setPreset(themeName, commit)
}
}
}
diff --git a/src/modules/users.js b/src/modules/users.js
index d6ab47ea..4d56ec6f 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,5 +1,5 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
-import { compact, map, each, merge } from 'lodash'
+import { compact, map, each, merge, find } from 'lodash'
import { set } from 'vue'
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
import oauthApi from '../services/new_api/oauth'
@@ -52,13 +52,35 @@ export const mutations = {
state.loggingIn = false
},
// TODO Clean after ourselves?
- addFriends (state, { id, friends }) {
+ addFriends (state, { id, friends, page }) {
const user = state.usersObject[id]
- user.friends = friends
+ each(friends, friend => {
+ if (!find(user.friends, { id: friend.id })) {
+ user.friends.push(friend)
+ }
+ })
+ user.friendsPage = page + 1
},
- addFollowers (state, { id, followers }) {
+ addFollowers (state, { id, followers, page }) {
const user = state.usersObject[id]
- user.followers = followers
+ each(followers, follower => {
+ if (!find(user.followers, { id: follower.id })) {
+ user.followers.push(follower)
+ }
+ })
+ user.followersPage = page + 1
+ },
+ // Because frontend doesn't have a reason to keep these stuff in memory
+ // outside of viewing someones user profile.
+ clearFriendsAndFollowers (state, userKey) {
+ const user = state.usersObject[userKey]
+ if (!user) {
+ return
+ }
+ user.friends = []
+ user.followers = []
+ user.friendsPage = 0
+ user.followersPage = 0
},
addNewUsers (state, users) {
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
@@ -115,13 +137,34 @@ const users = {
store.rootState.api.backendInteractor.fetchUser({ id })
.then((user) => store.commit('addNewUsers', [user]))
},
- addFriends ({ rootState, commit }, { id }) {
- rootState.api.backendInteractor.fetchFriends({ id })
- .then((friends) => commit('addFriends', { id, friends }))
+ addFriends ({ rootState, commit }, fetchBy) {
+ return new Promise((resolve, reject) => {
+ const user = rootState.users.usersObject[fetchBy]
+ const page = user.friendsPage || 1
+ rootState.api.backendInteractor.fetchFriends({ id: user.id, page })
+ .then((friends) => {
+ commit('addFriends', { id: user.id, friends, page })
+ resolve(friends)
+ }).catch(() => {
+ reject()
+ })
+ })
+ },
+ addFollowers ({ rootState, commit }, fetchBy) {
+ return new Promise((resolve, reject) => {
+ const user = rootState.users.usersObject[fetchBy]
+ const page = user.followersPage || 1
+ rootState.api.backendInteractor.fetchFollowers({ id: user.id, page })
+ .then((followers) => {
+ commit('addFollowers', { id: user.id, followers, page })
+ resolve(followers)
+ }).catch(() => {
+ reject()
+ })
+ })
},
- addFollowers ({ rootState, commit }, { id }) {
- rootState.api.backendInteractor.fetchFollowers({ id })
- .then((followers) => commit('addFollowers', { id, followers }))
+ clearFriendsAndFollowers ({ commit }, userKey) {
+ commit('clearFriendsAndFollowers', userKey)
},
registerPushNotifications (store) {
const token = store.state.currentUser.credentials
@@ -228,7 +271,7 @@ const users = {
}
// Start getting fresh posts.
- store.dispatch('startFetching', 'friends')
+ store.dispatch('startFetching', { timeline: 'friends' })
// Get user mutes and follower info
store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 5b0d8650..92daa04e 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -130,7 +130,7 @@ const updateBanner = ({credentials, params}) => {
// description
const updateProfile = ({credentials, params}) => {
// Always include these fields, because they might be empty or false
- const fields = ['description', 'locked', 'no_rich_text', 'hide_network']
+ const fields = ['description', 'locked', 'no_rich_text', 'hide_follows', 'hide_followers', 'show_role']
let url = PROFILE_UPDATE_URL
const form = new FormData()
@@ -247,15 +247,28 @@ const fetchUser = ({id, credentials}) => {
.then((data) => parseUser(data))
}
-const fetchFriends = ({id, credentials}) => {
+const fetchFriends = ({id, page, credentials}) => {
let url = `${FRIENDS_URL}?user_id=${id}`
+ if (page) {
+ url = url + `&page=${page}`
+ }
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
.then((data) => data.map(parseUser))
}
-const fetchFollowers = ({id, credentials}) => {
+const exportFriends = ({id, credentials}) => {
+ let url = `${FRIENDS_URL}?user_id=${id}&export=true`
+ return fetch(url, { headers: authHeaders(credentials) })
+ .then((data) => data.json())
+ .then((data) => data.map(parseUser))
+}
+
+const fetchFollowers = ({id, page, credentials}) => {
let url = `${FOLLOWERS_URL}?user_id=${id}`
+ if (page) {
+ url = url + `&page=${page}`
+ }
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
.then((data) => data.map(parseUser))
@@ -530,6 +543,7 @@ const apiService = {
fetchConversation,
fetchStatus,
fetchFriends,
+ exportFriends,
fetchFollowers,
followUser,
unfollowUser,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index f44f52b6..80c5cc5e 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -10,12 +10,16 @@ const backendInteractorService = (credentials) => {
return apiService.fetchConversation({id, credentials})
}
- const fetchFriends = ({id}) => {
- return apiService.fetchFriends({id, credentials})
+ const fetchFriends = ({id, page}) => {
+ return apiService.fetchFriends({id, page, credentials})
}
- const fetchFollowers = ({id}) => {
- return apiService.fetchFollowers({id, credentials})
+ const exportFriends = ({id}) => {
+ return apiService.exportFriends({id, credentials})
+ }
+
+ const fetchFollowers = ({id, page}) => {
+ return apiService.fetchFollowers({id, page, credentials})
}
const fetchAllFollowing = ({username}) => {
@@ -78,6 +82,7 @@ const backendInteractorService = (credentials) => {
fetchStatus,
fetchConversation,
fetchFriends,
+ exportFriends,
fetchFollowers,
followUser,
unfollowUser,
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 74422a49..828c48f9 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -90,6 +90,8 @@ export const parseUser = (data) => {
output.statusnet_blocking = data.statusnet_blocking
output.is_local = data.is_local
+ output.role = data.role
+ output.show_role = data.show_role
output.follows_you = data.follows_you
@@ -100,7 +102,8 @@ export const parseUser = (data) => {
output.rights = data.rights
output.no_rich_text = data.no_rich_text
output.default_scope = data.default_scope
- output.hide_network = data.hide_network
+ output.hide_follows = data.hide_follows
+ output.hide_followers = data.hide_followers
output.background_image = data.background_image
// on mastoapi this info is contained in a "relationship"
output.following = data.following
@@ -112,6 +115,8 @@ export const parseUser = (data) => {
output.locked = data.locked
output.followers_count = data.followers_count
output.statuses_count = data.statuses_count
+ output.friends = []
+ output.followers = []
return output
}
diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js
new file mode 100644
index 00000000..1e9bd679
--- /dev/null
+++ b/src/services/follow_manipulate/follow_manipulate.js
@@ -0,0 +1,74 @@
+const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => {
+ setTimeout(() => {
+ store.state.api.backendInteractor.fetchUser({ id: user.id })
+ .then((user) => store.commit('addNewUsers', [user]))
+ .then(() => resolve([user.following, attempt]))
+ .catch((e) => reject(e))
+ }, 500)
+}).then(([following, attempt]) => {
+ if (!following && attempt <= 3) {
+ // If we BE reports that we still not following that user - retry,
+ // increment attempts by one
+ return fetchUser(++attempt, user, store)
+ } else {
+ // If we run out of attempts, just return whatever status is.
+ return following
+ }
+})
+
+export const requestFollow = (user, store) => new Promise((resolve, reject) => {
+ store.state.api.backendInteractor.followUser(user.id)
+ .then((updated) => {
+ store.commit('addNewUsers', [updated])
+
+ // For locked users we just mark it that we sent the follow request
+ if (updated.locked) {
+ resolve({
+ sent: true,
+ updated
+ })
+ }
+
+ if (updated.following) {
+ // If we get result immediately, just stop.
+ resolve({
+ sent: false,
+ updated
+ })
+ }
+
+ // But usually we don't get result immediately, so we ask server
+ // for updated user profile to confirm if we are following them
+ // Sometimes it takes several tries. Sometimes we end up not following
+ // user anyway, probably because they locked themselves and we
+ // don't know that yet.
+ // Recursive Promise, it will call itself up to 3 times.
+
+ return fetchUser(1, user, store)
+ .then((following) => {
+ if (following) {
+ // We confirmed and everything's good.
+ resolve({
+ sent: false,
+ updated
+ })
+ } else {
+ // If after all the tries, just treat it as if user is locked
+ resolve({
+ sent: false,
+ updated
+ })
+ }
+ })
+ })
+})
+
+export const requestUnfollow = (user, store) => new Promise((resolve, reject) => {
+ store.state.api.backendInteractor.unfollowUser(user.id)
+ .then((updated) => {
+ store.commit('addNewUsers', [updated])
+ resolve({
+ updated
+ })
+ })
+})
diff --git a/src/services/matcher/matcher.service.js b/src/services/matcher/matcher.service.js
new file mode 100644
index 00000000..b6c4e909
--- /dev/null
+++ b/src/services/matcher/matcher.service.js
@@ -0,0 +1,23 @@
+export const mentionMatchesUrl = (attention, url) => {
+ if (url === attention.statusnet_profile_url) {
+ return true
+ }
+ const [namepart, instancepart] = attention.screen_name.split('@')
+ const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g')
+
+ return !!url.match(matchstring)
+}
+
+/**
+ * Extract tag name from pleroma or mastodon url.
+ * i.e https://bikeshed.party/tag/photo or https://quey.org/tags/sky
+ * @param {string} url
+ */
+export const extractTagFromUrl = (url) => {
+ const regex = /tag[s]*\/(\w+)$/g
+ const result = regex.exec(url)
+ if (!result) {
+ return false
+ }
+ return result[1]
+}
diff --git a/src/services/mention_matcher/mention_matcher.js b/src/services/mention_matcher/mention_matcher.js
deleted file mode 100644
index 2c1ed970..00000000
--- a/src/services/mention_matcher/mention_matcher.js
+++ /dev/null
@@ -1,9 +0,0 @@
-
-export const mentionMatchesUrl = (attention, url) => {
- if (url === attention.statusnet_profile_url) {
- return true
- }
- const [namepart, instancepart] = attention.screen_name.split('@')
- const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g')
- return !!url.match(matchstring)
-}
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 10e7ed9b..d0b6ccbf 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -480,7 +480,7 @@ const getThemes = () => {
}
const setPreset = (val, commit) => {
- getThemes().then((themes) => {
+ return getThemes().then((themes) => {
const theme = themes[val] ? themes[val] : themes['pleroma-dark']
const isV1 = Array.isArray(theme)
const data = isV1 ? {} : theme.theme
diff --git a/src/services/user_profile_link_generator/user_profile_link_generator.js b/src/services/user_profile_link_generator/user_profile_link_generator.js
index bca2c9cd..a214ca48 100644
--- a/src/services/user_profile_link_generator/user_profile_link_generator.js
+++ b/src/services/user_profile_link_generator/user_profile_link_generator.js
@@ -8,6 +8,6 @@ const generateProfileLink = (id, screenName, restrictedNicknames) => {
}
}
-const isExternal = screenName => screenName.includes('@')
+const isExternal = screenName => screenName && screenName.includes('@')
export default generateProfileLink
diff --git a/static/config.json b/static/config.json
index 24e26696..533a5b08 100644
--- a/static/config.json
+++ b/static/config.json
@@ -13,11 +13,13 @@
"collapseMessageWithSubject": false,
"scopeCopy": true,
"subjectLineBehavior": "email",
+ "postContentType": "text/plain",
"alwaysShowSubjectInput": true,
"hidePostStats": false,
"hideUserStats": false,
"loginMethod": "password",
"webPushNotifications": false,
"noAttachmentLinks": false,
- "nsfwCensorImage": ""
+ "nsfwCensorImage": "",
+ "showFeaturesPanel": true
}
diff --git a/static/emoji.json b/static/emoji.json
index 0117bac1..ae93d17e 100644
--- a/static/emoji.json
+++ b/static/emoji.json
@@ -1 +1,969 @@
-{"womans_clothes": "\ud83d\udc5a", "cookie": "\ud83c\udf6a", "woman_with_headscarf": "\ud83e\uddd5", "no_smoking": "\ud83d\udead", "e-mail": "\ud83d\udce7", "regional_indicator_d": "\ud83c\udde9", "oncoming_bus": "\ud83d\ude8d", "knife": "\ud83d\udd2a", "person_getting_haircut": "\ud83d\udc87", "grimacing": "\ud83d\ude2c", "ophiuchus": "\u26ce", "regional_indicator_q": "\ud83c\uddf6", "thinking": "\ud83e\udd14", "signal_strength": "\ud83d\udcf6", "cactus": "\ud83c\udf35", "bullettrain_front": "\ud83d\ude85", "floppy_disk": "\ud83d\udcbe", "doughnut": "\ud83c\udf69", "tv": "\ud83d\udcfa", "1234": "\ud83d\udd22", "anguished": "\ud83d\ude27", "clock1030": "\ud83d\udd65", "u7533": "\ud83c\ude38", "speak_no_evil": "\ud83d\ude4a", "chart_with_upwards_trend": "\ud83d\udcc8", "trophy": "\ud83c\udfc6", "musical_score": "\ud83c\udfbc", "chestnut": "\ud83c\udf30", "clock1130": "\ud83d\udd66", "abcd": "\ud83d\udd21", "syringe": "\ud83d\udc89", "shrimp": "\ud83e\udd90", "pisces": "\u2653", "left_facing_fist": "\ud83e\udd1b", "bar_chart": "\ud83d\udcca", "eagle": "\ud83e\udd85", "woman": "\ud83d\udc69", "keycap_ten": "\ud83d\udd1f", "yellow_heart": "\ud83d\udc9b", "croissant": "\ud83e\udd50", "mosque": "\ud83d\udd4c", "rice_ball": "\ud83c\udf59", "volcano": "\ud83c\udf0b", "baggage_claim": "\ud83d\udec4", "family": "\ud83d\udc6a", "beetle": "\ud83d\udc1e", "older_adult": "\ud83e\uddd3", "clock830": "\ud83d\udd63", "bacon": "\ud83e\udd53", "sound": "\ud83d\udd09", "no_bicycles": "\ud83d\udeb3", "rewind": "\u23ea", "adult": "\ud83e\uddd1", "scream_cat": "\ud83d\ude40", "person_playing_water_polo": "\ud83e\udd3d", "blue_car": "\ud83d\ude99", "smiley": "\ud83d\ude03", "kaaba": "\ud83d\udd4b", "twisted_rightwards_arrows": "\ud83d\udd00", "last_quarter_moon": "\ud83c\udf17", "first_place": "\ud83e\udd47", "joy_cat": "\ud83d\ude39", "sleeping": "\ud83d\ude34", "basketball": "\ud83c\udfc0", "pray": "\ud83d\ude4f", "trumpet": "\ud83c\udfba", "purple_heart": "\ud83d\udc9c", "broken_heart": "\ud83d\udc94", "astonished": "\ud83d\ude32", "soccer": "\u26bd", "princess": "\ud83d\udc78", "ant": "\ud83d\udc1c", "pig": "\ud83d\udc37", "vhs": "\ud83d\udcfc", "scream": "\ud83d\ude31", "mouse": "\ud83d\udc2d", "field_hockey": "\ud83c\udfd1", "ab": "\ud83c\udd8e", "tokyo_tower": "\ud83d\uddfc", "girl": "\ud83d\udc67", "u55b6": "\ud83c\ude3a", "guard": "\ud83d\udc82", "regional_indicator_s": "\ud83c\uddf8", "tulip": "\ud83c\udf37", "capital_abcd": "\ud83d\udd20", "beginner": "\ud83d\udd30", "couplekiss": "\ud83d\udc8f", "u5408": "\ud83c\ude34", "black_medium_small_square": "\u25fe", "paperclip": "\ud83d\udcce", "hedgehog": "\ud83e\udd94", "musical_note": "\ud83c\udfb5", "pill": "\ud83d\udc8a", "blue_heart": "\ud83d\udc99", "mens": "\ud83d\udeb9", "third_place": "\ud83e\udd49", "stew": "\ud83c\udf72", "prince": "\ud83e\udd34", "mortar_board": "\ud83c\udf93", "clock6": "\ud83d\udd55", "beer": "\ud83c\udf7a", "person_tipping_hand": "\ud83d\udc81", "triangular_ruler": "\ud83d\udcd0", "regional_indicator_y": "\ud83c\uddfe", "person_facepalming": "\ud83e\udd26", "steam_locomotive": "\ud83d\ude82", "fire_engine": "\ud83d\ude92", "horse": "\ud83d\udc34", "ribbon": "\ud83c\udf80", "white_large_square": "\u2b1c", "smirk": "\ud83d\ude0f", "genie": "\ud83e\uddde", "tangerine": "\ud83c\udf4a", "cl": "\ud83c\udd91", "japanese_goblin": "\ud83d\udc7a", "regional_indicator_u": "\ud83c\uddfa", "ring": "\ud83d\udc8d", "roller_coaster": "\ud83c\udfa2", "100": "\ud83d\udcaf", "clock12": "\ud83d\udd5b", "two_hearts": "\ud83d\udc95", "anger": "\ud83d\udca2", "black_circle": "\u26ab", "revolving_hearts": "\ud83d\udc9e", "space_invader": "\ud83d\udc7e", "bell": "\ud83d\udd14", "point_up_2": "\ud83d\udc46", "person_mountain_biking": "\ud83d\udeb5", "flags": "\ud83c\udf8f", "pushpin": "\ud83d\udccc", "large_blue_diamond": "\ud83d\udd37", "fairy": "\ud83e\uddda", "european_post_office": "\ud83c\udfe4", "statue_of_liberty": "\ud83d\uddfd", "man": "\ud83d\udc68", "microphone": "\ud83c\udfa4", "inbox_tray": "\ud83d\udce5", "bath": "\ud83d\udec0", "person_gesturing_ok": "\ud83d\ude46", "clap": "\ud83d\udc4f", "confused": "\ud83d\ude15", "fortune_cookie": "\ud83e\udd60", "kissing_closed_eyes": "\ud83d\ude1a", "kissing_heart": "\ud83d\ude18", "tropical_fish": "\ud83d\udc20", "taco": "\ud83c\udf2e", "kimono": "\ud83d\udc58", "u7a7a": "\ud83c\ude33", "rat": "\ud83d\udc00", "taurus": "\u2649", "shopping_cart": "\ud83d\uded2", "womans_hat": "\ud83d\udc52", "blossom": "\ud83c\udf3c", "moyai": "\ud83d\uddff", "clock130": "\ud83d\udd5c", "telescope": "\ud83d\udd2d", "running_shirt_with_sash": "\ud83c\udfbd", "person_running": "\ud83c\udfc3", "dizzy": "\ud83d\udcab", "crescent_moon": "\ud83c\udf19", "boom": "\ud83d\udca5", "restroom": "\ud83d\udebb", "fist": "\u270a", "white_flower": "\ud83d\udcae", "clown": "\ud83e\udd21", "neutral_face": "\ud83d\ude10", "id": "\ud83c\udd94", "carrot": "\ud83e\udd55", "rice_scene": "\ud83c\udf91", "foggy": "\ud83c\udf01", "turtle": "\ud83d\udc22", "mailbox_with_mail": "\ud83d\udcec", "baseball": "\u26be", "grin": "\ud83d\ude01", "bathtub": "\ud83d\udec1", "feet": "\ud83d\udc3e", "small_red_triangle": "\ud83d\udd3a", "camel": "\ud83d\udc2b", "aquarius": "\u2652", "face_with_symbols_over_mouth": "\ud83e\udd2c", "handbag": "\ud83d\udc5c", "date": "\ud83d\udcc5", "nail_care": "\ud83d\udc85", "satellite": "\ud83d\udce1", "candy": "\ud83c\udf6c", "white_medium_small_square": "\u25fd", "clock930": "\ud83d\udd64", "fearful": "\ud83d\ude28", "fork_and_knife": "\ud83c\udf74", "person_wearing_turban": "\ud83d\udc73", "confounded": "\ud83d\ude16", "helicopter": "\ud83d\ude81", "arrow_double_down": "\u23ec", "convenience_store": "\ud83c\udfea", "ghost": "\ud83d\udc7b", "bus": "\ud83d\ude8c", "waning_gibbous_moon": "\ud83c\udf16", "bank": "\ud83c\udfe6", "department_store": "\ud83c\udfec", "hockey": "\ud83c\udfd2", "fingers_crossed": "\ud83e\udd1e", "blond_haired_person": "\ud83d\udc71", "mag": "\ud83d\udd0d", "cut_of_meat": "\ud83e\udd69", "wink": "\ud83d\ude09", "railway_car": "\ud83d\ude83", "face_vomiting": "\ud83e\udd2e", "star_struck": "\ud83e\udd29", "first_quarter_moon_with_face": "\ud83c\udf1b", "octagonal_sign": "\ud83d\uded1", "hospital": "\ud83c\udfe5", "monkey": "\ud83d\udc12", "curly_loop": "\u27b0", "avocado": "\ud83e\udd51", "earth_americas": "\ud83c\udf0e", "flashlight": "\ud83d\udd26", "8ball": "\ud83c\udfb1", "clock630": "\ud83d\udd61", "boar": "\ud83d\udc17", "birthday": "\ud83c\udf82", "crocodile": "\ud83d\udc0a", "confetti_ball": "\ud83c\udf8a", "door": "\ud83d\udeaa", "school_satchel": "\ud83c\udf92", "peanuts": "\ud83e\udd5c", "regional_indicator_m": "\ud83c\uddf2", "bust_in_silhouette": "\ud83d\udc64", "sweat_drops": "\ud83d\udca6", "tongue": "\ud83d\udc45", "mag_right": "\ud83d\udd0e", "t_rex": "\ud83e\udd96", "post_office": "\ud83c\udfe3", "shell": "\ud83d\udc1a", "disappointed_relieved": "\ud83d\ude25", "card_index": "\ud83d\udcc7", "oncoming_automobile": "\ud83d\ude98", "passport_control": "\ud83d\udec2", "cherry_blossom": "\ud83c\udf38", "shallow_pan_of_food": "\ud83e\udd58", "heartbeat": "\ud83d\udc93", "crazy_face": "\ud83e\udd2a", "grapes": "\ud83c\udf47", "symbols": "\ud83d\udd23", "gift": "\ud83c\udf81", "scorpion": "\ud83e\udd82", "wedding": "\ud83d\udc92", "last_quarter_moon_with_face": "\ud83c\udf1c", "love_letter": "\ud83d\udc8c", "postal_horn": "\ud83d\udcef", "stuffed_flatbread": "\ud83e\udd59", "heavy_dollar_sign": "\ud83d\udcb2", "love_hotel": "\ud83c\udfe9", "yen": "\ud83d\udcb4", "person_in_steamy_room": "\ud83e\uddd6", "palm_tree": "\ud83c\udf34", "name_badge": "\ud83d\udcdb", "clock430": "\ud83d\udd5f", "bike": "\ud83d\udeb2", "snail": "\ud83d\udc0c", "bowling": "\ud83c\udfb3", "umbrella": "\u2614", "sleeping_accommodation": "\ud83d\udecc", "fireworks": "\ud83c\udf86", "closed_book": "\ud83d\udcd5", "city_sunset": "\ud83c\udf07", "persevere": "\ud83d\ude23", "bento": "\ud83c\udf71", "nut_and_bolt": "\ud83d\udd29", "page_facing_up": "\ud83d\udcc4", "snowman": "\u26c4", "two_women_holding_hands": "\ud83d\udc6d", "regional_indicator_o": "\ud83c\uddf4", "calling": "\ud83d\udcf2", "person_shrugging": "\ud83e\udd37", "sneezing_face": "\ud83e\udd27", "arrows_clockwise": "\ud83d\udd03", "no_pedestrians": "\ud83d\udeb7", "potato": "\ud83e\udd54", "cheese": "\ud83e\uddc0", "full_moon": "\ud83c\udf15", "mount_fuji": "\ud83d\uddfb", "sob": "\ud83d\ude2d", "construction": "\ud83d\udea7", "head_bandage": "\ud83e\udd15", "sailboat": "\u26f5", "slight_frown": "\ud83d\ude41", "ping_pong": "\ud83c\udfd3", "hatched_chick": "\ud83d\udc25", "sun_with_face": "\ud83c\udf1e", "seedling": "\ud83c\udf31", "repeat_one": "\ud83d\udd02", "muscle": "\ud83d\udcaa", "bridge_at_night": "\ud83c\udf09", "raised_hands": "\ud83d\ude4c", "house": "\ud83c\udfe0", "nerd": "\ud83e\udd13", "penguin": "\ud83d\udc27", "peach": "\ud83c\udf51", "dumpling": "\ud83e\udd5f", "watch": "\u231a", "womens": "\ud83d\udeba", "round_pushpin": "\ud83d\udccd", "alarm_clock": "\u23f0", "relieved": "\ud83d\ude0c", "sagittarius": "\u2650", "busstop": "\ud83d\ude8f", "regional_indicator_a": "\ud83c\udde6", "sandal": "\ud83d\udc61", "whale2": "\ud83d\udc0b", "book": "\ud83d\udcd6", "sweat": "\ud83d\ude13", "movie_camera": "\ud83c\udfa5", "clock230": "\ud83d\udd5d", "tiger": "\ud83d\udc2f", "tractor": "\ud83d\ude9c", "smile": "\ud83d\ude04", "vertical_traffic_light": "\ud83d\udea6", "exploding_head": "\ud83e\udd2f", "raised_hand": "\u270b", "smoking": "\ud83d\udeac", "page_with_curl": "\ud83d\udcc3", "exclamation": "\u2757", "fish": "\ud83d\udc1f", "mans_shoe": "\ud83d\udc5e", "sos": "\ud83c\udd98", "unlock": "\ud83d\udd13", "dolls": "\ud83c\udf8e", "ear_of_rice": "\ud83c\udf3e", "cat2": "\ud83d\udc08", "u7121": "\ud83c\ude1a", "repeat": "\ud83d\udd01", "cool": "\ud83c\udd92", "minibus": "\ud83d\ude90", "aerial_tramway": "\ud83d\udea1", "key": "\ud83d\udd11", "child": "\ud83e\uddd2", "camera": "\ud83d\udcf7", "sunflower": "\ud83c\udf3b", "white_check_mark": "\u2705", "white_square_button": "\ud83d\udd33", "banana": "\ud83c\udf4c", "milky_way": "\ud83c\udf0c", "person_gesturing_no": "\ud83d\ude45", "sushi": "\ud83c\udf63", "heart_eyes_cat": "\ud83d\ude3b", "guitar": "\ud83c\udfb8", "pie": "\ud83e\udd67", "calendar": "\ud83d\udcc6", "bear": "\ud83d\udc3b", "person_in_lotus_position": "\ud83e\uddd8", "clock10": "\ud83d\udd59", "top": "\ud83d\udd1d", "fuelpump": "\u26fd", "rainbow": "\ud83c\udf08", "snowboarder": "\ud83c\udfc2", "drum": "\ud83e\udd41", "leaves": "\ud83c\udf43", "first_quarter_moon": "\ud83c\udf13", "spoon": "\ud83e\udd44", "pouting_cat": "\ud83d\ude3e", "shaved_ice": "\ud83c\udf67", "unamused": "\ud83d\ude12", "train2": "\ud83d\ude86", "clock1230": "\ud83d\udd67", "regional_indicator_r": "\ud83c\uddf7", "fast_forward": "\u23e9", "accept": "\ud83c\ude51", "hammer": "\ud83d\udd28", "panda_face": "\ud83d\udc3c", "briefcase": "\ud83d\udcbc", "package": "\ud83d\udce6", "flag_black": "\ud83c\udff4", "smiling_imp": "\ud83d\ude08", "sunrise_over_mountains": "\ud83c\udf04", "airplane_departure": "\ud83d\udeeb", "tiger2": "\ud83d\udc05", "non-potable_water": "\ud83d\udeb1", "bird": "\ud83d\udc26", "barber": "\ud83d\udc88", "cry": "\ud83d\ude22", "billed_cap": "\ud83e\udde2", "pouch": "\ud83d\udc5d", "link": "\ud83d\udd17", "zebra": "\ud83e\udd93", "kiss": "\ud83d\udc8b", "scorpius": "\u264f", "prayer_beads": "\ud83d\udcff", "high_brightness": "\ud83d\udd06", "kissing_smiling_eyes": "\ud83d\ude19", "rhino": "\ud83e\udd8f", "left_luggage": "\ud83d\udec5", "o": "\u2b55", "crying_cat_face": "\ud83d\ude3f", "clock8": "\ud83d\udd57", "dress": "\ud83d\udc57", "clock7": "\ud83d\udd56", "bowl_with_spoon": "\ud83e\udd63", "rolling_eyes": "\ud83d\ude44", "fax": "\ud83d\udce0", "worried": "\ud83d\ude1f", "grey_question": "\u2754", "saxophone": "\ud83c\udfb7", "burrito": "\ud83c\udf2f", "salad": "\ud83e\udd57", "regional_indicator_z": "\ud83c\uddff", "bikini": "\ud83d\udc59", "milk": "\ud83e\udd5b", "stars": "\ud83c\udf20", "lips": "\ud83d\udc44", "cd": "\ud83d\udcbf", "weary": "\ud83d\ude29", "face_with_raised_eyebrow": "\ud83e\udd28", "lizard": "\ud83e\udd8e", "tone1": "\ud83c\udffb", "bullettrain_side": "\ud83d\ude84", "nose": "\ud83d\udc43", "innocent": "\ud83d\ude07", "wilted_rose": "\ud83e\udd40", "mahjong": "\ud83c\udc04", "factory": "\ud83c\udfed", "people_wrestling": "\ud83e\udd3c", "mailbox": "\ud83d\udceb", "rage": "\ud83d\ude21", "wheelchair": "\u267f", "x": "\u274c", "flower_playing_cards": "\ud83c\udfb4", "nauseated_face": "\ud83e\udd22", "underage": "\ud83d\udd1e", "ideograph_advantage": "\ud83c\ude50", "high_heel": "\ud83d\udc60", "dizzy_face": "\ud83d\ude35", "stuck_out_tongue": "\ud83d\ude1b", "mailbox_with_no_mail": "\ud83d\udced", "orange_heart": "\ud83e\udde1", "raised_back_of_hand": "\ud83e\udd1a", "footprints": "\ud83d\udc63", "notebook_with_decorative_cover": "\ud83d\udcd4", "mask": "\ud83d\ude37", "sunglasses": "\ud83d\ude0e", "pancakes": "\ud83e\udd5e", "regional_indicator_f": "\ud83c\uddeb", "dog": "\ud83d\udc36", "pig2": "\ud83d\udc16", "ng": "\ud83c\udd96", "unicorn": "\ud83e\udd84", "triumph": "\ud83d\ude24", "eggplant": "\ud83c\udf46", "egg": "\ud83e\udd5a", "office": "\ud83c\udfe2", "goat": "\ud83d\udc10", "handshake": "\ud83e\udd1d", "star": "\u2b50", "rugby_football": "\ud83c\udfc9", "call_me": "\ud83e\udd19", "rice_cracker": "\ud83c\udf58", "droplet": "\ud83d\udca7", "badminton": "\ud83c\udff8", "waxing_crescent_moon": "\ud83c\udf12", "ocean": "\ud83c\udf0a", "slot_machine": "\ud83c\udfb0", "wine_glass": "\ud83c\udf77", "elephant": "\ud83d\udc18", "blowfish": "\ud83d\udc21", "ledger": "\ud83d\udcd2", "money_mouth": "\ud83e\udd11", "heart_decoration": "\ud83d\udc9f", "arrow_down_small": "\ud83d\udd3d", "station": "\ud83d\ude89", "man_with_chinese_cap": "\ud83d\udc72", "vampire": "\ud83e\udddb", "pencil": "\ud83d\udcdd", "cyclone": "\ud83c\udf00", "mushroom": "\ud83c\udf44", "sandwich": "\ud83e\udd6a", "champagne": "\ud83c\udf7e", "expressionless": "\ud83d\ude11", "cold_sweat": "\ud83d\ude30", "maple_leaf": "\ud83c\udf41", "dromedary_camel": "\ud83d\udc2a", "vs": "\ud83c\udd9a", "person_fencing": "\ud83e\udd3a", "straight_ruler": "\ud83d\udccf", "baby_bottle": "\ud83c\udf7c", "currency_exchange": "\ud83d\udcb1", "regional_indicator_h": "\ud83c\udded", "stuck_out_tongue_closed_eyes": "\ud83d\ude1d", "closed_lock_with_key": "\ud83d\udd10", "eyes": "\ud83d\udc40", "water_buffalo": "\ud83d\udc03", "lock_with_ink_pen": "\ud83d\udd0f", "heavy_plus_sign": "\u2795", "bookmark": "\ud83d\udd16", "soon": "\ud83d\udd1c", "orange_book": "\ud83d\udcd9", "pineapple": "\ud83c\udf4d", "clock9": "\ud83d\udd58", "small_blue_diamond": "\ud83d\udd39", "black_large_square": "\u2b1b", "person_surfing": "\ud83c\udfc4", "leo": "\u264c", "merperson": "\ud83e\udddc", "canoe": "\ud83d\udef6", "rooster": "\ud83d\udc13", "hear_no_evil": "\ud83d\ude49", "corn": "\ud83c\udf3d", "takeout_box": "\ud83e\udd61", "oncoming_taxi": "\ud83d\ude96", "taxi": "\ud83d\ude95", "chart": "\ud83d\udcb9", "goal": "\ud83e\udd45", "melon": "\ud83c\udf48", "notes": "\ud83c\udfb6", "sparkler": "\ud83c\udf87", "dolphin": "\ud83d\udc2c", "speedboat": "\ud83d\udea4", "cancer": "\u264b", "sled": "\ud83d\udef7", "tanabata_tree": "\ud83c\udf8b", "train": "\ud83d\ude8b", "christmas_tree": "\ud83c\udf84", "two_men_holding_hands": "\ud83d\udc6c", "back": "\ud83d\udd19", "balloon": "\ud83c\udf88", "checkered_flag": "\ud83c\udfc1", "loop": "\u27bf", "wc": "\ud83d\udebe", "jeans": "\ud83d\udc56", "green_apple": "\ud83c\udf4f", "crown": "\ud83d\udc51", "cowboy": "\ud83e\udd20", "postbox": "\ud83d\udcee", "volleyball": "\ud83c\udfd0", "upside_down": "\ud83d\ude43", "cricket": "\ud83e\udd97", "custard": "\ud83c\udf6e", "rose": "\ud83c\udf39", "eyeglasses": "\ud83d\udc53", "oncoming_police_car": "\ud83d\ude94", "atm": "\ud83c\udfe7", "flying_saucer": "\ud83d\udef8", "alien": "\ud83d\udc7d", "hamster": "\ud83d\udc39", "trident": "\ud83d\udd31", "disappointed": "\ud83d\ude1e", "cow": "\ud83d\udc2e", "police_officer": "\ud83d\udc6e", "popcorn": "\ud83c\udf7f", "baby_chick": "\ud83d\udc24", "video_camera": "\ud83d\udcf9", "zzz": "\ud83d\udca4", "person_climbing": "\ud83e\uddd7", "star2": "\ud83c\udf1f", "ok": "\ud83c\udd97", "capricorn": "\u2651", "chicken": "\ud83d\udc14", "arrow_double_up": "\u23eb", "zombie": "\ud83e\udddf", "closed_umbrella": "\ud83c\udf02", "person_walking": "\ud83d\udeb6", "lemon": "\ud83c\udf4b", "heartpulse": "\ud83d\udc97", "regional_indicator_i": "\ud83c\uddee", "sauropod": "\ud83e\udd95", "u7981": "\ud83c\ude32", "regional_indicator_w": "\ud83c\uddfc", "evergreen_tree": "\ud83c\udf32", "mobile_phone_off": "\ud83d\udcf4", "koko": "\ud83c\ude01", "poop": "\ud83d\udca9", "cup_with_straw": "\ud83e\udd64", "leopard": "\ud83d\udc06", "radio_button": "\ud83d\udd18", "mega": "\ud83d\udce3", "metal": "\ud83e\udd18", "shushing_face": "\ud83e\udd2b", "stuck_out_tongue_winking_eye": "\ud83d\ude1c", "octopus": "\ud83d\udc19", "boxing_glove": "\ud83e\udd4a", "person_juggling": "\ud83e\udd39", "money_with_wings": "\ud83d\udcb8", "dollar": "\ud83d\udcb5", "bride_with_veil": "\ud83d\udc70", "second_place": "\ud83e\udd48", "spaghetti": "\ud83c\udf5d", "waning_crescent_moon": "\ud83c\udf18", "football": "\ud83c\udfc8", "white_circle": "\u26aa", "full_moon_with_face": "\ud83c\udf1d", "selfie": "\ud83e\udd33", "tone3": "\ud83c\udffd", "rabbit": "\ud83d\udc30", "computer": "\ud83d\udcbb", "clock11": "\ud83d\udd5a", "heavy_minus_sign": "\u2796", "synagogue": "\ud83d\udd4d", "hourglass": "\u231b", "gem": "\ud83d\udc8e", "person_doing_cartwheel": "\ud83e\udd38", "new_moon_with_face": "\ud83c\udf1a", "sunrise": "\ud83c\udf05", "regional_indicator_x": "\ud83c\uddfd", "open_file_folder": "\ud83d\udcc2", "gift_heart": "\ud83d\udc9d", "tada": "\ud83c\udf89", "green_heart": "\ud83d\udc9a", "battery": "\ud83d\udd0b", "regional_indicator_t": "\ud83c\uddf9", "wrench": "\ud83d\udd27", "aries": "\u2648", "man_in_tuxedo": "\ud83e\udd35", "regional_indicator_e": "\ud83c\uddea", "regional_indicator_l": "\ud83c\uddf1", "cake": "\ud83c\udf70", "clapper": "\ud83c\udfac", "japanese_castle": "\ud83c\udfef", "crystal_ball": "\ud83d\udd2e", "golf": "\u26f3", "no_mobile_phones": "\ud83d\udcf5", "person_biking": "\ud83d\udeb4", "icecream": "\ud83c\udf66", "mage": "\ud83e\uddd9", "bookmark_tabs": "\ud83d\udcd1", "tone4": "\ud83c\udffe", "mountain_cableway": "\ud83d\udea0", "person_playing_handball": "\ud83e\udd3e", "bulb": "\ud83d\udca1", "clock330": "\ud83d\udd5e", "metro": "\ud83d\ude87", "wave": "\ud83d\udc4b", "whale": "\ud83d\udc33", "strawberry": "\ud83c\udf53", "hatching_chick": "\ud83d\udc23", "trolleybus": "\ud83d\ude8e", "lollipop": "\ud83c\udf6d", "clipboard": "\ud83d\udccb", "point_right": "\ud83d\udc49", "u6307": "\ud83c\ude2f", "santa": "\ud83c\udf85", "hibiscus": "\ud83c\udf3a", "green_book": "\ud83d\udcd7", "skull": "\ud83d\udc80", "tumbler_glass": "\ud83e\udd43", "clock2": "\ud83d\udd51", "open_mouth": "\ud83d\ude2e", "bouquet": "\ud83d\udc90", "champagne_glass": "\ud83e\udd42", "poodle": "\ud83d\udc29", "hushed": "\ud83d\ude2f", "earth_asia": "\ud83c\udf0f", "face_with_monocle": "\ud83e\uddd0", "libra": "\u264e", "clock5": "\ud83d\udd54", "ambulance": "\ud83d\ude91", "u5272": "\ud83c\ude39", "lipstick": "\ud83d\udc84", "apple": "\ud83c\udf4e", "headphones": "\ud83c\udfa7", "turkey": "\ud83e\udd83", "pretzel": "\ud83e\udd68", "bug": "\ud83d\udc1b", "school": "\ud83c\udfeb", "speaker": "\ud83d\udd08", "boot": "\ud83d\udc62", "cat": "\ud83d\udc31", "dancer": "\ud83d\udc83", "no_entry": "\u26d4", "kissing_cat": "\ud83d\ude3d", "art": "\ud83c\udfa8", "coat": "\ud83e\udde5", "credit_card": "\ud83d\udcb3", "customs": "\ud83d\udec3", "broccoli": "\ud83e\udd66", "point_left": "\ud83d\udc48", "canned_food": "\ud83e\udd6b", "sheep": "\ud83d\udc11", "person_bowing": "\ud83d\ude47", "scroll": "\ud83d\udcdc", "martial_arts_uniform": "\ud83e\udd4b", "amphora": "\ud83c\udffa", "thought_balloon": "\ud83d\udcad", "no_bell": "\ud83d\udd15", "musical_keyboard": "\ud83c\udfb9", "people_with_bunny_ears_partying": "\ud83d\udc6f", "european_castle": "\ud83c\udff0", "punch": "\ud83d\udc4a", "camera_with_flash": "\ud83d\udcf8", "regional_indicator_p": "\ud83c\uddf5", "red_car": "\ud83d\ude97", "regional_indicator_j": "\ud83c\uddef", "owl": "\ud83e\udd89", "chart_with_downwards_trend": "\ud83d\udcc9", "older_woman": "\ud83d\udc75", "gemini": "\u264a", "incoming_envelope": "\ud83d\udce8", "waxing_gibbous_moon": "\ud83c\udf14", "toilet": "\ud83d\udebd", "dragon_face": "\ud83d\udc32", "koala": "\ud83d\udc28", "tone5": "\ud83c\udfff", "kiwi": "\ud83e\udd5d", "dash": "\ud83d\udca8", "imp": "\ud83d\udc7f", "tent": "\u26fa", "regional_indicator_b": "\ud83c\udde7", "monorail": "\ud83d\ude9d", "ox": "\ud83d\udc02", "giraffe": "\ud83e\udd92", "new": "\ud83c\udd95", "person_raising_hand": "\ud83d\ude4b", "japan": "\ud83d\uddfe", "rice": "\ud83c\udf5a", "ticket": "\ud83c\udfab", "rotating_light": "\ud83d\udea8", "loudspeaker": "\ud83d\udce2", "person_getting_massage": "\ud83d\udc86", "loud_sound": "\ud83d\udd0a", "hugging": "\ud83e\udd17", "herb": "\ud83c\udf3f", "baby": "\ud83d\udc76", "angel": "\ud83d\udc7c", "athletic_shoe": "\ud83d\udc5f", "euro": "\ud83d\udcb6", "ram": "\ud83d\udc0f", "large_orange_diamond": "\ud83d\udd36", "red_circle": "\ud83d\udd34", "ferris_wheel": "\ud83c\udfa1", "drooling_face": "\ud83e\udd24", "microscope": "\ud83d\udd2c", "middle_finger": "\ud83d\udd95", "pager": "\ud83d\udcdf", "pensive": "\ud83d\ude14", "potable_water": "\ud83d\udeb0", "abc": "\ud83d\udd24", "four_leaf_clover": "\ud83c\udf40", "vulcan": "\ud83d\udd96", "french_bread": "\ud83e\udd56", "motor_scooter": "\ud83d\udef5", "moneybag": "\ud83d\udcb0", "sparkles": "\u2728", "gloves": "\ud83e\udde4", "envelope_with_arrow": "\ud83d\udce9", "thumbsdown": "\ud83d\udc4e", "regional_indicator_g": "\ud83c\uddec", "video_game": "\ud83c\udfae", "on": "\ud83d\udd1b", "open_hands": "\ud83d\udc50", "monkey_face": "\ud83d\udc35", "mountain_railway": "\ud83d\ude9e", "bee": "\ud83d\udc1d", "scooter": "\ud83d\udef4", "fishing_pole_and_fish": "\ud83c\udfa3", "smiley_cat": "\ud83d\ude3a", "heart_eyes": "\ud83d\ude0d", "horse_racing": "\ud83c\udfc7", "ear": "\ud83d\udc42", "blue_circle": "\ud83d\udd35", "crossed_flags": "\ud83c\udf8c", "black_joker": "\ud83c\udccf", "six_pointed_star": "\ud83d\udd2f", "fountain": "\u26f2", "free": "\ud83c\udd93", "tennis": "\ud83c\udfbe", "yum": "\ud83d\ude0b", "fried_shrimp": "\ud83c\udf64", "dragon": "\ud83d\udc09", "purse": "\ud83d\udc5b", "clock1": "\ud83d\udd50", "airplane_arriving": "\ud83d\udeec", "cucumber": "\ud83e\udd52", "man_dancing": "\ud83d\udd7a", "clock730": "\ud83d\udd62", "deer": "\ud83e\udd8c", "meat_on_bone": "\ud83c\udf56", "bomb": "\ud83d\udca3", "night_with_stars": "\ud83c\udf03", "snake": "\ud83d\udc0d", "ramen": "\ud83c\udf5c", "end": "\ud83d\udd1a", "do_not_litter": "\ud83d\udeaf", "joy": "\ud83d\ude02", "light_rail": "\ud83d\ude88", "game_die": "\ud83c\udfb2", "violin": "\ud83c\udfbb", "tone2": "\ud83c\udffc", "tropical_drink": "\ud83c\udf79", "love_you_gesture": "\ud83e\udd1f", "cherries": "\ud83c\udf52", "traffic_light": "\ud83d\udea5", "iphone": "\ud83d\udcf1", "socks": "\ud83e\udde6", "wind_chime": "\ud83c\udf90", "no_entry_sign": "\ud83d\udeab", "elf": "\ud83e\udddd", "squid": "\ud83e\udd91", "person_pouting": "\ud83d\ude4e", "smile_cat": "\ud83d\ude38", "beers": "\ud83c\udf7b", "minidisc": "\ud83d\udcbd", "clock4": "\ud83d\udd53", "ice_cream": "\ud83c\udf68", "cocktail": "\ud83c\udf78", "clock3": "\ud83d\udd52", "frowning": "\ud83d\ude26", "hamburger": "\ud83c\udf54", "brain": "\ud83e\udde0", "heavy_division_sign": "\u2797", "tophat": "\ud83c\udfa9", "no_mouth": "\ud83d\ude36", "ski": "\ud83c\udfbf", "right_facing_fist": "\ud83e\udd1c", "mailbox_closed": "\ud83d\udcea", "chocolate_bar": "\ud83c\udf6b", "rabbit2": "\ud83d\udc07", "honey_pot": "\ud83c\udf6f", "izakaya_lantern": "\ud83c\udfee", "articulated_lorry": "\ud83d\ude9b", "face_with_hand_over_mouth": "\ud83e\udd2d", "japanese_ogre": "\ud83d\udc79", "zap": "\u26a1", "rocket": "\ud83d\ude80", "pizza": "\ud83c\udf55", "pound": "\ud83d\udcb7", "person_swimming": "\ud83c\udfca", "anchor": "\u2693", "coconut": "\ud83e\udd65", "sparkling_heart": "\ud83d\udc96", "older_man": "\ud83d\udc74", "mouse2": "\ud83d\udc01", "angry": "\ud83d\ude20", "up": "\ud83c\udd99", "gorilla": "\ud83e\udd8d", "children_crossing": "\ud83d\udeb8", "smirk_cat": "\ud83d\ude3c", "pregnant_woman": "\ud83e\udd30", "electric_plug": "\ud83d\udd0c", "dog2": "\ud83d\udc15", "question": "\u2753", "carousel_horse": "\ud83c\udfa0", "church": "\u26ea", "outbox_tray": "\ud83d\udce4", "cinema": "\ud83c\udfa6", "flushed": "\ud83d\ude33", "blush": "\ud83d\ude0a", "medal": "\ud83c\udfc5", "coffee": "\u2615", "gun": "\ud83d\udd2b", "city_dusk": "\ud83c\udf06", "watermelon": "\ud83c\udf49", "cricket_game": "\ud83c\udfcf", "shower": "\ud83d\udebf", "mute": "\ud83d\udd07", "breast_feeding": "\ud83e\udd31", "sweat_smile": "\ud83d\ude05", "construction_worker": "\ud83d\udc77", "cow2": "\ud83d\udc04", "arrows_counterclockwise": "\ud83d\udd04", "u6e80": "\ud83c\ude35", "grinning": "\ud83d\ude00", "globe_with_meridians": "\ud83c\udf10", "diamond_shape_with_a_dot_inside": "\ud83d\udca0", "deciduous_tree": "\ud83c\udf33", "shark": "\ud83e\udd88", "tram": "\ud83d\ude8a", "person_rowing_boat": "\ud83d\udea3", "chopsticks": "\ud83e\udd62", "black_heart": "\ud83d\udda4", "seat": "\ud83d\udcba", "kissing": "\ud83d\ude17", "laughing": "\ud83d\ude06", "slight_smile": "\ud83d\ude42", "radio": "\ud83d\udcfb", "arrow_up_small": "\ud83d\udd3c", "dango": "\ud83c\udf61", "rofl": "\ud83e\udd23", "see_no_evil": "\ud83d\ude48", "thermometer_face": "\ud83e\udd12", "hotdog": "\ud83c\udf2d", "virgo": "\u264d", "poultry_leg": "\ud83c\udf57", "hotel": "\ud83c\udfe8", "wolf": "\ud83d\udc3a", "curry": "\ud83c\udf5b", "regional_indicator_v": "\ud83c\uddfb", "crab": "\ud83e\udd80", "tired_face": "\ud83d\ude2b", "place_of_worship": "\ud83d\uded0", "ok_hand": "\ud83d\udc4c", "speech_balloon": "\ud83d\udcac", "sleepy": "\ud83d\ude2a", "earth_africa": "\ud83c\udf0d", "police_car": "\ud83d\ude93", "small_red_triangle_down": "\ud83d\udd3b", "bearded_person": "\ud83e\uddd4", "curling_stone": "\ud83e\udd4c", "scarf": "\ud83e\udde3", "fire": "\ud83d\udd25", "file_folder": "\ud83d\udcc1", "zipper_mouth": "\ud83e\udd10", "new_moon": "\ud83c\udf11", "regional_indicator_n": "\ud83c\uddf3", "negative_squared_cross_mark": "\u274e", "newspaper": "\ud83d\udcf0", "dvd": "\ud83d\udcc0", "pear": "\ud83c\udf50", "partly_sunny": "\u26c5", "black_square_button": "\ud83d\udd32", "low_brightness": "\ud83d\udd05", "sake": "\ud83c\udf76", "bow_and_arrow": "\ud83c\udff9", "cooking": "\ud83c\udf73", "fish_cake": "\ud83c\udf65", "tomato": "\ud83c\udf45", "couple_with_heart": "\ud83d\udc91", "telephone_receiver": "\ud83d\udcde", "triangular_flag_on_post": "\ud83d\udea9", "jack_o_lantern": "\ud83c\udf83", "blue_book": "\ud83d\udcd8", "clock530": "\ud83d\udd60", "u6709": "\ud83c\ude36", "palms_up_together": "\ud83e\udd32", "lion_face": "\ud83e\udd81", "lock": "\ud83d\udd12", "duck": "\ud83e\udd86", "truck": "\ud83d\ude9a", "oden": "\ud83c\udf62", "busts_in_silhouette": "\ud83d\udc65", "hourglass_flowing_sand": "\u23f3", "frog": "\ud83d\udc38", "fox": "\ud83e\udd8a", "bread": "\ud83c\udf5e", "put_litter_in_its_place": "\ud83d\udeae", "couple": "\ud83d\udc6b", "bamboo": "\ud83c\udf8d", "regional_indicator_c": "\ud83c\udde8", "menorah": "\ud83d\udd4e", "circus_tent": "\ud83c\udfaa", "lying_face": "\ud83e\udd25", "small_orange_diamond": "\ud83d\udd38", "ship": "\ud83d\udea2", "person_frowning": "\ud83d\ude4d", "racehorse": "\ud83d\udc0e", "thumbsup": "\ud83d\udc4d", "cupid": "\ud83d\udc98", "robot": "\ud83e\udd16", "fallen_leaf": "\ud83c\udf42", "pig_nose": "\ud83d\udc3d", "vibration_mode": "\ud83d\udcf3", "necktie": "\ud83d\udc54", "boy": "\ud83d\udc66", "house_with_garden": "\ud83c\udfe1", "point_down": "\ud83d\udc47", "grey_exclamation": "\u2755", "books": "\ud83d\udcda", "regional_indicator_k": "\ud83c\uddf0", "shirt": "\ud83d\udc55", "fries": "\ud83c\udf5f", "dart": "\ud83c\udfaf", "tea": "\ud83c\udf75", "mrs_claus": "\ud83e\udd36", "suspension_railway": "\ud83d\ude9f", "baby_symbol": "\ud83d\udebc", "sweet_potato": "\ud83c\udf60", "butterfly": "\ud83e\udd8b", "performing_arts": "\ud83c\udfad", "notebook": "\ud83d\udcd3", "bat": "\ud83e\udd87"} \ No newline at end of file
+{
+ "womans_clothes": "\ud83d\udc5a",
+ "cookie": "\ud83c\udf6a",
+ "woman_with_headscarf": "\ud83e\uddd5",
+ "no_smoking": "\ud83d\udead",
+ "e-mail": "\ud83d\udce7",
+ "regional_indicator_d": "\ud83c\udde9",
+ "oncoming_bus": "\ud83d\ude8d",
+ "knife": "\ud83d\udd2a",
+ "person_getting_haircut": "\ud83d\udc87",
+ "grimacing": "\ud83d\ude2c",
+ "ophiuchus": "\u26ce",
+ "regional_indicator_q": "\ud83c\uddf6",
+ "thinking": "\ud83e\udd14",
+ "signal_strength": "\ud83d\udcf6",
+ "cactus": "\ud83c\udf35",
+ "bullettrain_front": "\ud83d\ude85",
+ "floppy_disk": "\ud83d\udcbe",
+ "doughnut": "\ud83c\udf69",
+ "tv": "\ud83d\udcfa",
+ "1234": "\ud83d\udd22",
+ "anguished": "\ud83d\ude27",
+ "clock1030": "\ud83d\udd65",
+ "u7533": "\ud83c\ude38",
+ "speak_no_evil": "\ud83d\ude4a",
+ "chart_with_upwards_trend": "\ud83d\udcc8",
+ "trophy": "\ud83c\udfc6",
+ "musical_score": "\ud83c\udfbc",
+ "chestnut": "\ud83c\udf30",
+ "clock1130": "\ud83d\udd66",
+ "abcd": "\ud83d\udd21",
+ "syringe": "\ud83d\udc89",
+ "shrimp": "\ud83e\udd90",
+ "pisces": "\u2653",
+ "left_facing_fist": "\ud83e\udd1b",
+ "bar_chart": "\ud83d\udcca",
+ "eagle": "\ud83e\udd85",
+ "woman": "\ud83d\udc69",
+ "keycap_ten": "\ud83d\udd1f",
+ "yellow_heart": "\ud83d\udc9b",
+ "croissant": "\ud83e\udd50",
+ "mosque": "\ud83d\udd4c",
+ "rice_ball": "\ud83c\udf59",
+ "volcano": "\ud83c\udf0b",
+ "baggage_claim": "\ud83d\udec4",
+ "family": "\ud83d\udc6a",
+ "beetle": "\ud83d\udc1e",
+ "older_adult": "\ud83e\uddd3",
+ "clock830": "\ud83d\udd63",
+ "bacon": "\ud83e\udd53",
+ "sound": "\ud83d\udd09",
+ "no_bicycles": "\ud83d\udeb3",
+ "rewind": "\u23ea",
+ "adult": "\ud83e\uddd1",
+ "scream_cat": "\ud83d\ude40",
+ "person_playing_water_polo": "\ud83e\udd3d",
+ "blue_car": "\ud83d\ude99",
+ "smiley": "\ud83d\ude03",
+ "kaaba": "\ud83d\udd4b",
+ "twisted_rightwards_arrows": "\ud83d\udd00",
+ "last_quarter_moon": "\ud83c\udf17",
+ "first_place": "\ud83e\udd47",
+ "joy_cat": "\ud83d\ude39",
+ "sleeping": "\ud83d\ude34",
+ "basketball": "\ud83c\udfc0",
+ "pray": "\ud83d\ude4f",
+ "trumpet": "\ud83c\udfba",
+ "purple_heart": "\ud83d\udc9c",
+ "broken_heart": "\ud83d\udc94",
+ "astonished": "\ud83d\ude32",
+ "soccer": "\u26bd",
+ "princess": "\ud83d\udc78",
+ "ant": "\ud83d\udc1c",
+ "pig": "\ud83d\udc37",
+ "vhs": "\ud83d\udcfc",
+ "scream": "\ud83d\ude31",
+ "mouse": "\ud83d\udc2d",
+ "field_hockey": "\ud83c\udfd1",
+ "ab": "\ud83c\udd8e",
+ "tokyo_tower": "\ud83d\uddfc",
+ "girl": "\ud83d\udc67",
+ "u55b6": "\ud83c\ude3a",
+ "guard": "\ud83d\udc82",
+ "regional_indicator_s": "\ud83c\uddf8",
+ "tulip": "\ud83c\udf37",
+ "capital_abcd": "\ud83d\udd20",
+ "beginner": "\ud83d\udd30",
+ "couplekiss": "\ud83d\udc8f",
+ "u5408": "\ud83c\ude34",
+ "black_medium_small_square": "\u25fe",
+ "paperclip": "\ud83d\udcce",
+ "hedgehog": "\ud83e\udd94",
+ "musical_note": "\ud83c\udfb5",
+ "pill": "\ud83d\udc8a",
+ "blue_heart": "\ud83d\udc99",
+ "mens": "\ud83d\udeb9",
+ "third_place": "\ud83e\udd49",
+ "stew": "\ud83c\udf72",
+ "prince": "\ud83e\udd34",
+ "mortar_board": "\ud83c\udf93",
+ "clock6": "\ud83d\udd55",
+ "beer": "\ud83c\udf7a",
+ "person_tipping_hand": "\ud83d\udc81",
+ "triangular_ruler": "\ud83d\udcd0",
+ "regional_indicator_y": "\ud83c\uddfe",
+ "person_facepalming": "\ud83e\udd26",
+ "steam_locomotive": "\ud83d\ude82",
+ "fire_engine": "\ud83d\ude92",
+ "horse": "\ud83d\udc34",
+ "ribbon": "\ud83c\udf80",
+ "white_large_square": "\u2b1c",
+ "smirk": "\ud83d\ude0f",
+ "genie": "\ud83e\uddde",
+ "tangerine": "\ud83c\udf4a",
+ "cl": "\ud83c\udd91",
+ "japanese_goblin": "\ud83d\udc7a",
+ "regional_indicator_u": "\ud83c\uddfa",
+ "ring": "\ud83d\udc8d",
+ "roller_coaster": "\ud83c\udfa2",
+ "100": "\ud83d\udcaf",
+ "clock12": "\ud83d\udd5b",
+ "two_hearts": "\ud83d\udc95",
+ "anger": "\ud83d\udca2",
+ "black_circle": "\u26ab",
+ "revolving_hearts": "\ud83d\udc9e",
+ "space_invader": "\ud83d\udc7e",
+ "bell": "\ud83d\udd14",
+ "point_up_2": "\ud83d\udc46",
+ "person_mountain_biking": "\ud83d\udeb5",
+ "flags": "\ud83c\udf8f",
+ "pushpin": "\ud83d\udccc",
+ "large_blue_diamond": "\ud83d\udd37",
+ "fairy": "\ud83e\uddda",
+ "european_post_office": "\ud83c\udfe4",
+ "statue_of_liberty": "\ud83d\uddfd",
+ "man": "\ud83d\udc68",
+ "microphone": "\ud83c\udfa4",
+ "inbox_tray": "\ud83d\udce5",
+ "bath": "\ud83d\udec0",
+ "person_gesturing_ok": "\ud83d\ude46",
+ "clap": "\ud83d\udc4f",
+ "confused": "\ud83d\ude15",
+ "fortune_cookie": "\ud83e\udd60",
+ "kissing_closed_eyes": "\ud83d\ude1a",
+ "kissing_heart": "\ud83d\ude18",
+ "tropical_fish": "\ud83d\udc20",
+ "taco": "\ud83c\udf2e",
+ "kimono": "\ud83d\udc58",
+ "u7a7a": "\ud83c\ude33",
+ "rat": "\ud83d\udc00",
+ "taurus": "\u2649",
+ "shopping_cart": "\ud83d\uded2",
+ "womans_hat": "\ud83d\udc52",
+ "blossom": "\ud83c\udf3c",
+ "moyai": "\ud83d\uddff",
+ "clock130": "\ud83d\udd5c",
+ "telescope": "\ud83d\udd2d",
+ "running_shirt_with_sash": "\ud83c\udfbd",
+ "person_running": "\ud83c\udfc3",
+ "dizzy": "\ud83d\udcab",
+ "crescent_moon": "\ud83c\udf19",
+ "boom": "\ud83d\udca5",
+ "restroom": "\ud83d\udebb",
+ "fist": "\u270a",
+ "white_flower": "\ud83d\udcae",
+ "clown": "\ud83e\udd21",
+ "neutral_face": "\ud83d\ude10",
+ "id": "\ud83c\udd94",
+ "carrot": "\ud83e\udd55",
+ "rice_scene": "\ud83c\udf91",
+ "foggy": "\ud83c\udf01",
+ "turtle": "\ud83d\udc22",
+ "mailbox_with_mail": "\ud83d\udcec",
+ "baseball": "\u26be",
+ "grin": "\ud83d\ude01",
+ "bathtub": "\ud83d\udec1",
+ "feet": "\ud83d\udc3e",
+ "small_red_triangle": "\ud83d\udd3a",
+ "camel": "\ud83d\udc2b",
+ "aquarius": "\u2652",
+ "face_with_symbols_over_mouth": "\ud83e\udd2c",
+ "handbag": "\ud83d\udc5c",
+ "date": "\ud83d\udcc5",
+ "nail_care": "\ud83d\udc85",
+ "satellite": "\ud83d\udce1",
+ "candy": "\ud83c\udf6c",
+ "white_medium_small_square": "\u25fd",
+ "clock930": "\ud83d\udd64",
+ "fearful": "\ud83d\ude28",
+ "fork_and_knife": "\ud83c\udf74",
+ "person_wearing_turban": "\ud83d\udc73",
+ "confounded": "\ud83d\ude16",
+ "helicopter": "\ud83d\ude81",
+ "arrow_double_down": "\u23ec",
+ "convenience_store": "\ud83c\udfea",
+ "ghost": "\ud83d\udc7b",
+ "bus": "\ud83d\ude8c",
+ "waning_gibbous_moon": "\ud83c\udf16",
+ "bank": "\ud83c\udfe6",
+ "department_store": "\ud83c\udfec",
+ "hockey": "\ud83c\udfd2",
+ "fingers_crossed": "\ud83e\udd1e",
+ "blond_haired_person": "\ud83d\udc71",
+ "mag": "\ud83d\udd0d",
+ "cut_of_meat": "\ud83e\udd69",
+ "wink": "\ud83d\ude09",
+ "railway_car": "\ud83d\ude83",
+ "face_vomiting": "\ud83e\udd2e",
+ "star_struck": "\ud83e\udd29",
+ "first_quarter_moon_with_face": "\ud83c\udf1b",
+ "octagonal_sign": "\ud83d\uded1",
+ "hospital": "\ud83c\udfe5",
+ "monkey": "\ud83d\udc12",
+ "curly_loop": "\u27b0",
+ "avocado": "\ud83e\udd51",
+ "earth_americas": "\ud83c\udf0e",
+ "flashlight": "\ud83d\udd26",
+ "8ball": "\ud83c\udfb1",
+ "clock630": "\ud83d\udd61",
+ "boar": "\ud83d\udc17",
+ "birthday": "\ud83c\udf82",
+ "crocodile": "\ud83d\udc0a",
+ "confetti_ball": "\ud83c\udf8a",
+ "door": "\ud83d\udeaa",
+ "school_satchel": "\ud83c\udf92",
+ "peanuts": "\ud83e\udd5c",
+ "regional_indicator_m": "\ud83c\uddf2",
+ "bust_in_silhouette": "\ud83d\udc64",
+ "sweat_drops": "\ud83d\udca6",
+ "tongue": "\ud83d\udc45",
+ "mag_right": "\ud83d\udd0e",
+ "t_rex": "\ud83e\udd96",
+ "post_office": "\ud83c\udfe3",
+ "shell": "\ud83d\udc1a",
+ "disappointed_relieved": "\ud83d\ude25",
+ "card_index": "\ud83d\udcc7",
+ "oncoming_automobile": "\ud83d\ude98",
+ "passport_control": "\ud83d\udec2",
+ "cherry_blossom": "\ud83c\udf38",
+ "shallow_pan_of_food": "\ud83e\udd58",
+ "heart": "\u2764\ufe0f",
+ "heartbeat": "\ud83d\udc93",
+ "crazy_face": "\ud83e\udd2a",
+ "grapes": "\ud83c\udf47",
+ "symbols": "\ud83d\udd23",
+ "gift": "\ud83c\udf81",
+ "scorpion": "\ud83e\udd82",
+ "wedding": "\ud83d\udc92",
+ "last_quarter_moon_with_face": "\ud83c\udf1c",
+ "love_letter": "\ud83d\udc8c",
+ "postal_horn": "\ud83d\udcef",
+ "stuffed_flatbread": "\ud83e\udd59",
+ "heavy_dollar_sign": "\ud83d\udcb2",
+ "love_hotel": "\ud83c\udfe9",
+ "yen": "\ud83d\udcb4",
+ "person_in_steamy_room": "\ud83e\uddd6",
+ "palm_tree": "\ud83c\udf34",
+ "name_badge": "\ud83d\udcdb",
+ "clock430": "\ud83d\udd5f",
+ "bike": "\ud83d\udeb2",
+ "snail": "\ud83d\udc0c",
+ "bowling": "\ud83c\udfb3",
+ "umbrella": "\u2614",
+ "sleeping_accommodation": "\ud83d\udecc",
+ "fireworks": "\ud83c\udf86",
+ "closed_book": "\ud83d\udcd5",
+ "city_sunset": "\ud83c\udf07",
+ "persevere": "\ud83d\ude23",
+ "bento": "\ud83c\udf71",
+ "nut_and_bolt": "\ud83d\udd29",
+ "page_facing_up": "\ud83d\udcc4",
+ "snowman": "\u26c4",
+ "two_women_holding_hands": "\ud83d\udc6d",
+ "regional_indicator_o": "\ud83c\uddf4",
+ "calling": "\ud83d\udcf2",
+ "person_shrugging": "\ud83e\udd37",
+ "sneezing_face": "\ud83e\udd27",
+ "arrows_clockwise": "\ud83d\udd03",
+ "no_pedestrians": "\ud83d\udeb7",
+ "potato": "\ud83e\udd54",
+ "cheese": "\ud83e\uddc0",
+ "full_moon": "\ud83c\udf15",
+ "mount_fuji": "\ud83d\uddfb",
+ "sob": "\ud83d\ude2d",
+ "construction": "\ud83d\udea7",
+ "head_bandage": "\ud83e\udd15",
+ "sailboat": "\u26f5",
+ "slight_frown": "\ud83d\ude41",
+ "ping_pong": "\ud83c\udfd3",
+ "hatched_chick": "\ud83d\udc25",
+ "sun_with_face": "\ud83c\udf1e",
+ "seedling": "\ud83c\udf31",
+ "repeat_one": "\ud83d\udd02",
+ "muscle": "\ud83d\udcaa",
+ "bridge_at_night": "\ud83c\udf09",
+ "raised_hands": "\ud83d\ude4c",
+ "house": "\ud83c\udfe0",
+ "nerd": "\ud83e\udd13",
+ "penguin": "\ud83d\udc27",
+ "peach": "\ud83c\udf51",
+ "dumpling": "\ud83e\udd5f",
+ "watch": "\u231a",
+ "womens": "\ud83d\udeba",
+ "round_pushpin": "\ud83d\udccd",
+ "alarm_clock": "\u23f0",
+ "relieved": "\ud83d\ude0c",
+ "sagittarius": "\u2650",
+ "busstop": "\ud83d\ude8f",
+ "regional_indicator_a": "\ud83c\udde6",
+ "sandal": "\ud83d\udc61",
+ "whale2": "\ud83d\udc0b",
+ "book": "\ud83d\udcd6",
+ "sweat": "\ud83d\ude13",
+ "movie_camera": "\ud83c\udfa5",
+ "clock230": "\ud83d\udd5d",
+ "tiger": "\ud83d\udc2f",
+ "tractor": "\ud83d\ude9c",
+ "smile": "\ud83d\ude04",
+ "vertical_traffic_light": "\ud83d\udea6",
+ "exploding_head": "\ud83e\udd2f",
+ "raised_hand": "\u270b",
+ "smoking": "\ud83d\udeac",
+ "page_with_curl": "\ud83d\udcc3",
+ "exclamation": "\u2757",
+ "fish": "\ud83d\udc1f",
+ "mans_shoe": "\ud83d\udc5e",
+ "sos": "\ud83c\udd98",
+ "unlock": "\ud83d\udd13",
+ "dolls": "\ud83c\udf8e",
+ "ear_of_rice": "\ud83c\udf3e",
+ "cat2": "\ud83d\udc08",
+ "u7121": "\ud83c\ude1a",
+ "repeat": "\ud83d\udd01",
+ "cool": "\ud83c\udd92",
+ "minibus": "\ud83d\ude90",
+ "aerial_tramway": "\ud83d\udea1",
+ "key": "\ud83d\udd11",
+ "child": "\ud83e\uddd2",
+ "camera": "\ud83d\udcf7",
+ "sunflower": "\ud83c\udf3b",
+ "white_check_mark": "\u2705",
+ "white_square_button": "\ud83d\udd33",
+ "banana": "\ud83c\udf4c",
+ "milky_way": "\ud83c\udf0c",
+ "person_gesturing_no": "\ud83d\ude45",
+ "sushi": "\ud83c\udf63",
+ "heart_eyes_cat": "\ud83d\ude3b",
+ "guitar": "\ud83c\udfb8",
+ "pie": "\ud83e\udd67",
+ "calendar": "\ud83d\udcc6",
+ "bear": "\ud83d\udc3b",
+ "person_in_lotus_position": "\ud83e\uddd8",
+ "clock10": "\ud83d\udd59",
+ "top": "\ud83d\udd1d",
+ "fuelpump": "\u26fd",
+ "rainbow": "\ud83c\udf08",
+ "snowboarder": "\ud83c\udfc2",
+ "drum": "\ud83e\udd41",
+ "leaves": "\ud83c\udf43",
+ "first_quarter_moon": "\ud83c\udf13",
+ "spoon": "\ud83e\udd44",
+ "pouting_cat": "\ud83d\ude3e",
+ "shaved_ice": "\ud83c\udf67",
+ "unamused": "\ud83d\ude12",
+ "train2": "\ud83d\ude86",
+ "clock1230": "\ud83d\udd67",
+ "regional_indicator_r": "\ud83c\uddf7",
+ "fast_forward": "\u23e9",
+ "accept": "\ud83c\ude51",
+ "hammer": "\ud83d\udd28",
+ "panda_face": "\ud83d\udc3c",
+ "briefcase": "\ud83d\udcbc",
+ "package": "\ud83d\udce6",
+ "flag_black": "\ud83c\udff4",
+ "smiling_imp": "\ud83d\ude08",
+ "sunrise_over_mountains": "\ud83c\udf04",
+ "airplane_departure": "\ud83d\udeeb",
+ "tiger2": "\ud83d\udc05",
+ "non-potable_water": "\ud83d\udeb1",
+ "bird": "\ud83d\udc26",
+ "barber": "\ud83d\udc88",
+ "cry": "\ud83d\ude22",
+ "billed_cap": "\ud83e\udde2",
+ "pouch": "\ud83d\udc5d",
+ "link": "\ud83d\udd17",
+ "zebra": "\ud83e\udd93",
+ "kiss": "\ud83d\udc8b",
+ "scorpius": "\u264f",
+ "prayer_beads": "\ud83d\udcff",
+ "high_brightness": "\ud83d\udd06",
+ "kissing_smiling_eyes": "\ud83d\ude19",
+ "rhino": "\ud83e\udd8f",
+ "left_luggage": "\ud83d\udec5",
+ "o": "\u2b55",
+ "crying_cat_face": "\ud83d\ude3f",
+ "clock8": "\ud83d\udd57",
+ "dress": "\ud83d\udc57",
+ "clock7": "\ud83d\udd56",
+ "bowl_with_spoon": "\ud83e\udd63",
+ "rolling_eyes": "\ud83d\ude44",
+ "fax": "\ud83d\udce0",
+ "worried": "\ud83d\ude1f",
+ "grey_question": "\u2754",
+ "saxophone": "\ud83c\udfb7",
+ "burrito": "\ud83c\udf2f",
+ "salad": "\ud83e\udd57",
+ "regional_indicator_z": "\ud83c\uddff",
+ "bikini": "\ud83d\udc59",
+ "milk": "\ud83e\udd5b",
+ "stars": "\ud83c\udf20",
+ "lips": "\ud83d\udc44",
+ "cd": "\ud83d\udcbf",
+ "weary": "\ud83d\ude29",
+ "face_with_raised_eyebrow": "\ud83e\udd28",
+ "lizard": "\ud83e\udd8e",
+ "tone1": "\ud83c\udffb",
+ "bullettrain_side": "\ud83d\ude84",
+ "nose": "\ud83d\udc43",
+ "innocent": "\ud83d\ude07",
+ "wilted_rose": "\ud83e\udd40",
+ "mahjong": "\ud83c\udc04",
+ "factory": "\ud83c\udfed",
+ "people_wrestling": "\ud83e\udd3c",
+ "mailbox": "\ud83d\udceb",
+ "rage": "\ud83d\ude21",
+ "wheelchair": "\u267f",
+ "x": "\u274c",
+ "flower_playing_cards": "\ud83c\udfb4",
+ "nauseated_face": "\ud83e\udd22",
+ "underage": "\ud83d\udd1e",
+ "ideograph_advantage": "\ud83c\ude50",
+ "high_heel": "\ud83d\udc60",
+ "dizzy_face": "\ud83d\ude35",
+ "stuck_out_tongue": "\ud83d\ude1b",
+ "mailbox_with_no_mail": "\ud83d\udced",
+ "orange_heart": "\ud83e\udde1",
+ "raised_back_of_hand": "\ud83e\udd1a",
+ "footprints": "\ud83d\udc63",
+ "notebook_with_decorative_cover": "\ud83d\udcd4",
+ "mask": "\ud83d\ude37",
+ "sunglasses": "\ud83d\ude0e",
+ "pancakes": "\ud83e\udd5e",
+ "regional_indicator_f": "\ud83c\uddeb",
+ "dog": "\ud83d\udc36",
+ "pig2": "\ud83d\udc16",
+ "ng": "\ud83c\udd96",
+ "unicorn": "\ud83e\udd84",
+ "triumph": "\ud83d\ude24",
+ "eggplant": "\ud83c\udf46",
+ "egg": "\ud83e\udd5a",
+ "office": "\ud83c\udfe2",
+ "goat": "\ud83d\udc10",
+ "handshake": "\ud83e\udd1d",
+ "star": "\u2b50",
+ "rugby_football": "\ud83c\udfc9",
+ "call_me": "\ud83e\udd19",
+ "rice_cracker": "\ud83c\udf58",
+ "droplet": "\ud83d\udca7",
+ "badminton": "\ud83c\udff8",
+ "waxing_crescent_moon": "\ud83c\udf12",
+ "ocean": "\ud83c\udf0a",
+ "slot_machine": "\ud83c\udfb0",
+ "wine_glass": "\ud83c\udf77",
+ "elephant": "\ud83d\udc18",
+ "blowfish": "\ud83d\udc21",
+ "ledger": "\ud83d\udcd2",
+ "money_mouth": "\ud83e\udd11",
+ "heart_decoration": "\ud83d\udc9f",
+ "arrow_down_small": "\ud83d\udd3d",
+ "station": "\ud83d\ude89",
+ "man_with_chinese_cap": "\ud83d\udc72",
+ "vampire": "\ud83e\udddb",
+ "pencil": "\ud83d\udcdd",
+ "cyclone": "\ud83c\udf00",
+ "mushroom": "\ud83c\udf44",
+ "sandwich": "\ud83e\udd6a",
+ "champagne": "\ud83c\udf7e",
+ "expressionless": "\ud83d\ude11",
+ "cold_sweat": "\ud83d\ude30",
+ "maple_leaf": "\ud83c\udf41",
+ "dromedary_camel": "\ud83d\udc2a",
+ "vs": "\ud83c\udd9a",
+ "person_fencing": "\ud83e\udd3a",
+ "straight_ruler": "\ud83d\udccf",
+ "baby_bottle": "\ud83c\udf7c",
+ "currency_exchange": "\ud83d\udcb1",
+ "regional_indicator_h": "\ud83c\udded",
+ "stuck_out_tongue_closed_eyes": "\ud83d\ude1d",
+ "closed_lock_with_key": "\ud83d\udd10",
+ "eyes": "\ud83d\udc40",
+ "water_buffalo": "\ud83d\udc03",
+ "lock_with_ink_pen": "\ud83d\udd0f",
+ "heavy_plus_sign": "\u2795",
+ "bookmark": "\ud83d\udd16",
+ "soon": "\ud83d\udd1c",
+ "orange_book": "\ud83d\udcd9",
+ "pineapple": "\ud83c\udf4d",
+ "clock9": "\ud83d\udd58",
+ "small_blue_diamond": "\ud83d\udd39",
+ "black_large_square": "\u2b1b",
+ "person_surfing": "\ud83c\udfc4",
+ "leo": "\u264c",
+ "merperson": "\ud83e\udddc",
+ "canoe": "\ud83d\udef6",
+ "rooster": "\ud83d\udc13",
+ "hear_no_evil": "\ud83d\ude49",
+ "corn": "\ud83c\udf3d",
+ "takeout_box": "\ud83e\udd61",
+ "oncoming_taxi": "\ud83d\ude96",
+ "taxi": "\ud83d\ude95",
+ "chart": "\ud83d\udcb9",
+ "goal": "\ud83e\udd45",
+ "melon": "\ud83c\udf48",
+ "notes": "\ud83c\udfb6",
+ "sparkler": "\ud83c\udf87",
+ "dolphin": "\ud83d\udc2c",
+ "speedboat": "\ud83d\udea4",
+ "cancer": "\u264b",
+ "sled": "\ud83d\udef7",
+ "tanabata_tree": "\ud83c\udf8b",
+ "train": "\ud83d\ude8b",
+ "christmas_tree": "\ud83c\udf84",
+ "two_men_holding_hands": "\ud83d\udc6c",
+ "back": "\ud83d\udd19",
+ "balloon": "\ud83c\udf88",
+ "checkered_flag": "\ud83c\udfc1",
+ "loop": "\u27bf",
+ "wc": "\ud83d\udebe",
+ "jeans": "\ud83d\udc56",
+ "green_apple": "\ud83c\udf4f",
+ "crown": "\ud83d\udc51",
+ "cowboy": "\ud83e\udd20",
+ "postbox": "\ud83d\udcee",
+ "volleyball": "\ud83c\udfd0",
+ "upside_down": "\ud83d\ude43",
+ "cricket": "\ud83e\udd97",
+ "custard": "\ud83c\udf6e",
+ "rose": "\ud83c\udf39",
+ "eyeglasses": "\ud83d\udc53",
+ "oncoming_police_car": "\ud83d\ude94",
+ "atm": "\ud83c\udfe7",
+ "flying_saucer": "\ud83d\udef8",
+ "alien": "\ud83d\udc7d",
+ "hamster": "\ud83d\udc39",
+ "trident": "\ud83d\udd31",
+ "disappointed": "\ud83d\ude1e",
+ "cow": "\ud83d\udc2e",
+ "police_officer": "\ud83d\udc6e",
+ "popcorn": "\ud83c\udf7f",
+ "baby_chick": "\ud83d\udc24",
+ "video_camera": "\ud83d\udcf9",
+ "zzz": "\ud83d\udca4",
+ "person_climbing": "\ud83e\uddd7",
+ "star2": "\ud83c\udf1f",
+ "ok": "\ud83c\udd97",
+ "capricorn": "\u2651",
+ "chicken": "\ud83d\udc14",
+ "arrow_double_up": "\u23eb",
+ "zombie": "\ud83e\udddf",
+ "closed_umbrella": "\ud83c\udf02",
+ "person_walking": "\ud83d\udeb6",
+ "lemon": "\ud83c\udf4b",
+ "heartpulse": "\ud83d\udc97",
+ "regional_indicator_i": "\ud83c\uddee",
+ "sauropod": "\ud83e\udd95",
+ "u7981": "\ud83c\ude32",
+ "regional_indicator_w": "\ud83c\uddfc",
+ "evergreen_tree": "\ud83c\udf32",
+ "mobile_phone_off": "\ud83d\udcf4",
+ "koko": "\ud83c\ude01",
+ "poop": "\ud83d\udca9",
+ "cup_with_straw": "\ud83e\udd64",
+ "leopard": "\ud83d\udc06",
+ "radio_button": "\ud83d\udd18",
+ "mega": "\ud83d\udce3",
+ "metal": "\ud83e\udd18",
+ "shushing_face": "\ud83e\udd2b",
+ "stuck_out_tongue_winking_eye": "\ud83d\ude1c",
+ "octopus": "\ud83d\udc19",
+ "boxing_glove": "\ud83e\udd4a",
+ "person_juggling": "\ud83e\udd39",
+ "money_with_wings": "\ud83d\udcb8",
+ "dollar": "\ud83d\udcb5",
+ "bride_with_veil": "\ud83d\udc70",
+ "second_place": "\ud83e\udd48",
+ "spaghetti": "\ud83c\udf5d",
+ "waning_crescent_moon": "\ud83c\udf18",
+ "football": "\ud83c\udfc8",
+ "white_circle": "\u26aa",
+ "full_moon_with_face": "\ud83c\udf1d",
+ "selfie": "\ud83e\udd33",
+ "tone3": "\ud83c\udffd",
+ "rabbit": "\ud83d\udc30",
+ "computer": "\ud83d\udcbb",
+ "clock11": "\ud83d\udd5a",
+ "heavy_minus_sign": "\u2796",
+ "synagogue": "\ud83d\udd4d",
+ "hourglass": "\u231b",
+ "gem": "\ud83d\udc8e",
+ "person_doing_cartwheel": "\ud83e\udd38",
+ "new_moon_with_face": "\ud83c\udf1a",
+ "sunrise": "\ud83c\udf05",
+ "regional_indicator_x": "\ud83c\uddfd",
+ "open_file_folder": "\ud83d\udcc2",
+ "gift_heart": "\ud83d\udc9d",
+ "tada": "\ud83c\udf89",
+ "green_heart": "\ud83d\udc9a",
+ "battery": "\ud83d\udd0b",
+ "regional_indicator_t": "\ud83c\uddf9",
+ "wrench": "\ud83d\udd27",
+ "aries": "\u2648",
+ "man_in_tuxedo": "\ud83e\udd35",
+ "regional_indicator_e": "\ud83c\uddea",
+ "regional_indicator_l": "\ud83c\uddf1",
+ "cake": "\ud83c\udf70",
+ "clapper": "\ud83c\udfac",
+ "japanese_castle": "\ud83c\udfef",
+ "crystal_ball": "\ud83d\udd2e",
+ "golf": "\u26f3",
+ "no_mobile_phones": "\ud83d\udcf5",
+ "person_biking": "\ud83d\udeb4",
+ "icecream": "\ud83c\udf66",
+ "mage": "\ud83e\uddd9",
+ "bookmark_tabs": "\ud83d\udcd1",
+ "tone4": "\ud83c\udffe",
+ "mountain_cableway": "\ud83d\udea0",
+ "person_playing_handball": "\ud83e\udd3e",
+ "bulb": "\ud83d\udca1",
+ "clock330": "\ud83d\udd5e",
+ "metro": "\ud83d\ude87",
+ "wave": "\ud83d\udc4b",
+ "whale": "\ud83d\udc33",
+ "strawberry": "\ud83c\udf53",
+ "hatching_chick": "\ud83d\udc23",
+ "trolleybus": "\ud83d\ude8e",
+ "lollipop": "\ud83c\udf6d",
+ "clipboard": "\ud83d\udccb",
+ "point_right": "\ud83d\udc49",
+ "u6307": "\ud83c\ude2f",
+ "santa": "\ud83c\udf85",
+ "hibiscus": "\ud83c\udf3a",
+ "green_book": "\ud83d\udcd7",
+ "skull": "\ud83d\udc80",
+ "tumbler_glass": "\ud83e\udd43",
+ "clock2": "\ud83d\udd51",
+ "open_mouth": "\ud83d\ude2e",
+ "bouquet": "\ud83d\udc90",
+ "champagne_glass": "\ud83e\udd42",
+ "poodle": "\ud83d\udc29",
+ "hushed": "\ud83d\ude2f",
+ "earth_asia": "\ud83c\udf0f",
+ "face_with_monocle": "\ud83e\uddd0",
+ "libra": "\u264e",
+ "clock5": "\ud83d\udd54",
+ "ambulance": "\ud83d\ude91",
+ "u5272": "\ud83c\ude39",
+ "lipstick": "\ud83d\udc84",
+ "apple": "\ud83c\udf4e",
+ "headphones": "\ud83c\udfa7",
+ "turkey": "\ud83e\udd83",
+ "pretzel": "\ud83e\udd68",
+ "bug": "\ud83d\udc1b",
+ "school": "\ud83c\udfeb",
+ "speaker": "\ud83d\udd08",
+ "boot": "\ud83d\udc62",
+ "cat": "\ud83d\udc31",
+ "dancer": "\ud83d\udc83",
+ "no_entry": "\u26d4",
+ "kissing_cat": "\ud83d\ude3d",
+ "art": "\ud83c\udfa8",
+ "coat": "\ud83e\udde5",
+ "credit_card": "\ud83d\udcb3",
+ "customs": "\ud83d\udec3",
+ "broccoli": "\ud83e\udd66",
+ "point_left": "\ud83d\udc48",
+ "canned_food": "\ud83e\udd6b",
+ "sheep": "\ud83d\udc11",
+ "person_bowing": "\ud83d\ude47",
+ "scroll": "\ud83d\udcdc",
+ "martial_arts_uniform": "\ud83e\udd4b",
+ "amphora": "\ud83c\udffa",
+ "thought_balloon": "\ud83d\udcad",
+ "no_bell": "\ud83d\udd15",
+ "musical_keyboard": "\ud83c\udfb9",
+ "people_with_bunny_ears_partying": "\ud83d\udc6f",
+ "european_castle": "\ud83c\udff0",
+ "punch": "\ud83d\udc4a",
+ "camera_with_flash": "\ud83d\udcf8",
+ "regional_indicator_p": "\ud83c\uddf5",
+ "red_car": "\ud83d\ude97",
+ "regional_indicator_j": "\ud83c\uddef",
+ "owl": "\ud83e\udd89",
+ "chart_with_downwards_trend": "\ud83d\udcc9",
+ "older_woman": "\ud83d\udc75",
+ "gemini": "\u264a",
+ "incoming_envelope": "\ud83d\udce8",
+ "waxing_gibbous_moon": "\ud83c\udf14",
+ "toilet": "\ud83d\udebd",
+ "dragon_face": "\ud83d\udc32",
+ "koala": "\ud83d\udc28",
+ "tone5": "\ud83c\udfff",
+ "kiwi": "\ud83e\udd5d",
+ "dash": "\ud83d\udca8",
+ "imp": "\ud83d\udc7f",
+ "tent": "\u26fa",
+ "regional_indicator_b": "\ud83c\udde7",
+ "monorail": "\ud83d\ude9d",
+ "ox": "\ud83d\udc02",
+ "giraffe": "\ud83e\udd92",
+ "new": "\ud83c\udd95",
+ "person_raising_hand": "\ud83d\ude4b",
+ "japan": "\ud83d\uddfe",
+ "rice": "\ud83c\udf5a",
+ "ticket": "\ud83c\udfab",
+ "rotating_light": "\ud83d\udea8",
+ "loudspeaker": "\ud83d\udce2",
+ "person_getting_massage": "\ud83d\udc86",
+ "loud_sound": "\ud83d\udd0a",
+ "hugging": "\ud83e\udd17",
+ "herb": "\ud83c\udf3f",
+ "baby": "\ud83d\udc76",
+ "angel": "\ud83d\udc7c",
+ "athletic_shoe": "\ud83d\udc5f",
+ "euro": "\ud83d\udcb6",
+ "ram": "\ud83d\udc0f",
+ "large_orange_diamond": "\ud83d\udd36",
+ "red_circle": "\ud83d\udd34",
+ "ferris_wheel": "\ud83c\udfa1",
+ "drooling_face": "\ud83e\udd24",
+ "microscope": "\ud83d\udd2c",
+ "middle_finger": "\ud83d\udd95",
+ "pager": "\ud83d\udcdf",
+ "pensive": "\ud83d\ude14",
+ "potable_water": "\ud83d\udeb0",
+ "abc": "\ud83d\udd24",
+ "four_leaf_clover": "\ud83c\udf40",
+ "vulcan": "\ud83d\udd96",
+ "french_bread": "\ud83e\udd56",
+ "motor_scooter": "\ud83d\udef5",
+ "moneybag": "\ud83d\udcb0",
+ "sparkles": "\u2728",
+ "gloves": "\ud83e\udde4",
+ "envelope_with_arrow": "\ud83d\udce9",
+ "thumbsdown": "\ud83d\udc4e",
+ "regional_indicator_g": "\ud83c\uddec",
+ "video_game": "\ud83c\udfae",
+ "on": "\ud83d\udd1b",
+ "open_hands": "\ud83d\udc50",
+ "monkey_face": "\ud83d\udc35",
+ "mountain_railway": "\ud83d\ude9e",
+ "bee": "\ud83d\udc1d",
+ "scooter": "\ud83d\udef4",
+ "fishing_pole_and_fish": "\ud83c\udfa3",
+ "smiley_cat": "\ud83d\ude3a",
+ "heart_eyes": "\ud83d\ude0d",
+ "horse_racing": "\ud83c\udfc7",
+ "ear": "\ud83d\udc42",
+ "blue_circle": "\ud83d\udd35",
+ "crossed_flags": "\ud83c\udf8c",
+ "black_joker": "\ud83c\udccf",
+ "six_pointed_star": "\ud83d\udd2f",
+ "fountain": "\u26f2",
+ "free": "\ud83c\udd93",
+ "tennis": "\ud83c\udfbe",
+ "yum": "\ud83d\ude0b",
+ "fried_shrimp": "\ud83c\udf64",
+ "dragon": "\ud83d\udc09",
+ "purse": "\ud83d\udc5b",
+ "clock1": "\ud83d\udd50",
+ "airplane_arriving": "\ud83d\udeec",
+ "cucumber": "\ud83e\udd52",
+ "man_dancing": "\ud83d\udd7a",
+ "clock730": "\ud83d\udd62",
+ "deer": "\ud83e\udd8c",
+ "meat_on_bone": "\ud83c\udf56",
+ "bomb": "\ud83d\udca3",
+ "night_with_stars": "\ud83c\udf03",
+ "snake": "\ud83d\udc0d",
+ "ramen": "\ud83c\udf5c",
+ "end": "\ud83d\udd1a",
+ "do_not_litter": "\ud83d\udeaf",
+ "joy": "\ud83d\ude02",
+ "light_rail": "\ud83d\ude88",
+ "game_die": "\ud83c\udfb2",
+ "violin": "\ud83c\udfbb",
+ "tone2": "\ud83c\udffc",
+ "tropical_drink": "\ud83c\udf79",
+ "love_you_gesture": "\ud83e\udd1f",
+ "cherries": "\ud83c\udf52",
+ "traffic_light": "\ud83d\udea5",
+ "iphone": "\ud83d\udcf1",
+ "socks": "\ud83e\udde6",
+ "wind_chime": "\ud83c\udf90",
+ "no_entry_sign": "\ud83d\udeab",
+ "elf": "\ud83e\udddd",
+ "squid": "\ud83e\udd91",
+ "person_pouting": "\ud83d\ude4e",
+ "smile_cat": "\ud83d\ude38",
+ "beers": "\ud83c\udf7b",
+ "minidisc": "\ud83d\udcbd",
+ "clock4": "\ud83d\udd53",
+ "ice_cream": "\ud83c\udf68",
+ "cocktail": "\ud83c\udf78",
+ "clock3": "\ud83d\udd52",
+ "frowning": "\ud83d\ude26",
+ "hamburger": "\ud83c\udf54",
+ "brain": "\ud83e\udde0",
+ "heavy_division_sign": "\u2797",
+ "tophat": "\ud83c\udfa9",
+ "no_mouth": "\ud83d\ude36",
+ "ski": "\ud83c\udfbf",
+ "right_facing_fist": "\ud83e\udd1c",
+ "mailbox_closed": "\ud83d\udcea",
+ "chocolate_bar": "\ud83c\udf6b",
+ "rabbit2": "\ud83d\udc07",
+ "honey_pot": "\ud83c\udf6f",
+ "izakaya_lantern": "\ud83c\udfee",
+ "articulated_lorry": "\ud83d\ude9b",
+ "face_with_hand_over_mouth": "\ud83e\udd2d",
+ "japanese_ogre": "\ud83d\udc79",
+ "zap": "\u26a1",
+ "rocket": "\ud83d\ude80",
+ "pizza": "\ud83c\udf55",
+ "pound": "\ud83d\udcb7",
+ "person_swimming": "\ud83c\udfca",
+ "anchor": "\u2693",
+ "coconut": "\ud83e\udd65",
+ "sparkling_heart": "\ud83d\udc96",
+ "older_man": "\ud83d\udc74",
+ "mouse2": "\ud83d\udc01",
+ "angry": "\ud83d\ude20",
+ "up": "\ud83c\udd99",
+ "gorilla": "\ud83e\udd8d",
+ "children_crossing": "\ud83d\udeb8",
+ "smirk_cat": "\ud83d\ude3c",
+ "pregnant_woman": "\ud83e\udd30",
+ "electric_plug": "\ud83d\udd0c",
+ "dog2": "\ud83d\udc15",
+ "question": "\u2753",
+ "carousel_horse": "\ud83c\udfa0",
+ "church": "\u26ea",
+ "outbox_tray": "\ud83d\udce4",
+ "cinema": "\ud83c\udfa6",
+ "flushed": "\ud83d\ude33",
+ "blush": "\ud83d\ude0a",
+ "medal": "\ud83c\udfc5",
+ "coffee": "\u2615",
+ "gun": "\ud83d\udd2b",
+ "city_dusk": "\ud83c\udf06",
+ "watermelon": "\ud83c\udf49",
+ "cricket_game": "\ud83c\udfcf",
+ "shower": "\ud83d\udebf",
+ "mute": "\ud83d\udd07",
+ "breast_feeding": "\ud83e\udd31",
+ "sweat_smile": "\ud83d\ude05",
+ "construction_worker": "\ud83d\udc77",
+ "cow2": "\ud83d\udc04",
+ "arrows_counterclockwise": "\ud83d\udd04",
+ "u6e80": "\ud83c\ude35",
+ "grinning": "\ud83d\ude00",
+ "globe_with_meridians": "\ud83c\udf10",
+ "diamond_shape_with_a_dot_inside": "\ud83d\udca0",
+ "deciduous_tree": "\ud83c\udf33",
+ "shark": "\ud83e\udd88",
+ "tram": "\ud83d\ude8a",
+ "person_rowing_boat": "\ud83d\udea3",
+ "chopsticks": "\ud83e\udd62",
+ "black_heart": "\ud83d\udda4",
+ "seat": "\ud83d\udcba",
+ "kissing": "\ud83d\ude17",
+ "laughing": "\ud83d\ude06",
+ "slight_smile": "\ud83d\ude42",
+ "radio": "\ud83d\udcfb",
+ "arrow_up_small": "\ud83d\udd3c",
+ "dango": "\ud83c\udf61",
+ "rofl": "\ud83e\udd23",
+ "see_no_evil": "\ud83d\ude48",
+ "thermometer_face": "\ud83e\udd12",
+ "hotdog": "\ud83c\udf2d",
+ "virgo": "\u264d",
+ "poultry_leg": "\ud83c\udf57",
+ "hotel": "\ud83c\udfe8",
+ "wolf": "\ud83d\udc3a",
+ "curry": "\ud83c\udf5b",
+ "regional_indicator_v": "\ud83c\uddfb",
+ "crab": "\ud83e\udd80",
+ "tired_face": "\ud83d\ude2b",
+ "place_of_worship": "\ud83d\uded0",
+ "ok_hand": "\ud83d\udc4c",
+ "speech_balloon": "\ud83d\udcac",
+ "sleepy": "\ud83d\ude2a",
+ "earth_africa": "\ud83c\udf0d",
+ "police_car": "\ud83d\ude93",
+ "small_red_triangle_down": "\ud83d\udd3b",
+ "bearded_person": "\ud83e\uddd4",
+ "curling_stone": "\ud83e\udd4c",
+ "scarf": "\ud83e\udde3",
+ "fire": "\ud83d\udd25",
+ "file_folder": "\ud83d\udcc1",
+ "zipper_mouth": "\ud83e\udd10",
+ "new_moon": "\ud83c\udf11",
+ "regional_indicator_n": "\ud83c\uddf3",
+ "negative_squared_cross_mark": "\u274e",
+ "newspaper": "\ud83d\udcf0",
+ "dvd": "\ud83d\udcc0",
+ "pear": "\ud83c\udf50",
+ "partly_sunny": "\u26c5",
+ "black_square_button": "\ud83d\udd32",
+ "low_brightness": "\ud83d\udd05",
+ "sake": "\ud83c\udf76",
+ "bow_and_arrow": "\ud83c\udff9",
+ "cooking": "\ud83c\udf73",
+ "fish_cake": "\ud83c\udf65",
+ "tomato": "\ud83c\udf45",
+ "couple_with_heart": "\ud83d\udc91",
+ "telephone_receiver": "\ud83d\udcde",
+ "triangular_flag_on_post": "\ud83d\udea9",
+ "jack_o_lantern": "\ud83c\udf83",
+ "blue_book": "\ud83d\udcd8",
+ "clock530": "\ud83d\udd60",
+ "u6709": "\ud83c\ude36",
+ "palms_up_together": "\ud83e\udd32",
+ "lion_face": "\ud83e\udd81",
+ "lock": "\ud83d\udd12",
+ "duck": "\ud83e\udd86",
+ "truck": "\ud83d\ude9a",
+ "oden": "\ud83c\udf62",
+ "busts_in_silhouette": "\ud83d\udc65",
+ "hourglass_flowing_sand": "\u23f3",
+ "frog": "\ud83d\udc38",
+ "fox": "\ud83e\udd8a",
+ "bread": "\ud83c\udf5e",
+ "put_litter_in_its_place": "\ud83d\udeae",
+ "couple": "\ud83d\udc6b",
+ "bamboo": "\ud83c\udf8d",
+ "regional_indicator_c": "\ud83c\udde8",
+ "menorah": "\ud83d\udd4e",
+ "circus_tent": "\ud83c\udfaa",
+ "lying_face": "\ud83e\udd25",
+ "small_orange_diamond": "\ud83d\udd38",
+ "ship": "\ud83d\udea2",
+ "person_frowning": "\ud83d\ude4d",
+ "racehorse": "\ud83d\udc0e",
+ "thumbsup": "\ud83d\udc4d",
+ "cupid": "\ud83d\udc98",
+ "robot": "\ud83e\udd16",
+ "fallen_leaf": "\ud83c\udf42",
+ "pig_nose": "\ud83d\udc3d",
+ "vibration_mode": "\ud83d\udcf3",
+ "necktie": "\ud83d\udc54",
+ "boy": "\ud83d\udc66",
+ "house_with_garden": "\ud83c\udfe1",
+ "point_down": "\ud83d\udc47",
+ "grey_exclamation": "\u2755",
+ "books": "\ud83d\udcda",
+ "regional_indicator_k": "\ud83c\uddf0",
+ "shirt": "\ud83d\udc55",
+ "fries": "\ud83c\udf5f",
+ "dart": "\ud83c\udfaf",
+ "tea": "\ud83c\udf75",
+ "mrs_claus": "\ud83e\udd36",
+ "suspension_railway": "\ud83d\ude9f",
+ "baby_symbol": "\ud83d\udebc",
+ "sweet_potato": "\ud83c\udf60",
+ "butterfly": "\ud83e\udd8b",
+ "performing_arts": "\ud83c\udfad",
+ "notebook": "\ud83d\udcd3",
+ "bat": "\ud83e\udd87"
+}
diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
index 703fecf1..6245361c 100644
--- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
+++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
@@ -241,7 +241,7 @@ describe('API Entities normalizer', () => {
notice: makeMockStatusQvitter({ id: 444 }),
from_profile: makeMockUserQvitter({ id: 'spurdo' })
})
- expect(parseNotification(notif)).to.have.property('id', '123')
+ expect(parseNotification(notif)).to.have.property('id', 123)
expect(parseNotification(notif)).to.have.property('seen', false)
expect(parseNotification(notif)).to.have.deep.property('status.id', '444')
expect(parseNotification(notif)).to.have.deep.property('action.id', '444')
@@ -259,7 +259,7 @@ describe('API Entities normalizer', () => {
is_seen: 1,
from_profile: makeMockUserQvitter({ id: 'spurdo' })
})
- expect(parseNotification(notif)).to.have.property('id', '123')
+ expect(parseNotification(notif)).to.have.property('id', 123)
expect(parseNotification(notif)).to.have.property('type', 'like')
expect(parseNotification(notif)).to.have.property('seen', true)
expect(parseNotification(notif)).to.have.deep.property('status.id', '4412')
diff --git a/test/unit/specs/services/mention_matcher/mention_matcher.spec.js b/test/unit/specs/services/matcher/matcher.spec.js
index 4f6f58ff..7a2494f0 100644
--- a/test/unit/specs/services/mention_matcher/mention_matcher.spec.js
+++ b/test/unit/specs/services/matcher/matcher.spec.js
@@ -1,4 +1,4 @@
-import * as MentionMatcher from 'src/services/mention_matcher/mention_matcher.js'
+import * as MatcherService from 'src/services/matcher/matcher.service.js'
const localAttn = () => ({
id: 123,
@@ -16,48 +16,67 @@ const externalAttn = () => ({
statusnet_profile_url: 'https://instance.com/users/person'
})
-describe('MentionMatcher', () => {
- describe.only('mentionMatchesUrl', () => {
+describe('MatcherService', () => {
+ describe('mentionMatchesUrl', () => {
it('should match local mention', () => {
const attention = localAttn()
const url = 'https://instance.com/users/person'
- expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true)
+ expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(true)
})
it('should not match a local mention with same name but different instance', () => {
const attention = localAttn()
const url = 'https://website.com/users/person'
- expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false)
+ expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(false)
})
it('should match external pleroma mention', () => {
const attention = externalAttn()
const url = 'https://instance.com/users/person'
- expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true)
+ expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(true)
})
it('should not match external pleroma mention with same name but different instance', () => {
const attention = externalAttn()
const url = 'https://website.com/users/person'
- expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false)
+ expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(false)
})
it('should match external mastodon mention', () => {
const attention = externalAttn()
const url = 'https://instance.com/@person'
- expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true)
+ expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(true)
})
it('should not match external mastodon mention with same name but different instance', () => {
const attention = externalAttn()
const url = 'https://website.com/@person'
- expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false)
+ expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(false)
+ })
+ })
+ describe('extractTagFromUrl', () => {
+ it('should return tag name from valid pleroma url', () => {
+ const url = 'https://website.com/tag/photo'
+
+ expect(MatcherService.extractTagFromUrl(url)).to.eql('photo')
+ })
+
+ it('should return tag name from valid mastodon url', () => {
+ const url = 'https://website.com/tags/sky'
+
+ expect(MatcherService.extractTagFromUrl(url)).to.eql('sky')
+ })
+
+ it('should not return string but false if invalid url', () => {
+ const url = 'https://website.com/users/sky'
+
+ expect(MatcherService.extractTagFromUrl(url)).to.eql(false)
})
})
})