aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/boot/after_store.js32
-rw-r--r--src/boot/routes.js22
-rw-r--r--src/components/emoji-input/suggestor.js21
-rw-r--r--src/components/extra_buttons/extra_buttons.js24
-rw-r--r--src/components/extra_buttons/extra_buttons.vue38
-rw-r--r--src/components/moderation_tools/moderation_tools.js7
-rw-r--r--src/components/moderation_tools/moderation_tools.vue28
-rw-r--r--src/components/popper/popper.scss147
-rw-r--r--src/components/post_status_form/post_status_form.js24
-rw-r--r--src/components/post_status_form/post_status_form.vue24
-rw-r--r--src/components/status/status.js15
-rw-r--r--src/components/status/status.vue5
-rw-r--r--src/components/sticker_picker/sticker_picker.js52
-rw-r--r--src/components/sticker_picker/sticker_picker.vue62
-rw-r--r--src/components/tab_switcher/tab_switcher.js14
-rw-r--r--src/components/tab_switcher/tab_switcher.scss6
-rw-r--r--src/components/user_card/user_card.vue4
-rw-r--r--src/components/user_settings/user_settings.js3
-rw-r--r--src/i18n/en.json3
-rw-r--r--src/i18n/es.json123
-rw-r--r--src/i18n/ja.json53
-rw-r--r--src/i18n/ja_pedantic.json53
-rw-r--r--src/main.js2
-rw-r--r--src/services/api/api.service.js16
-rw-r--r--src/services/backend_interactor_service/backend_interactor_service.js4
25 files changed, 619 insertions, 163 deletions
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 3fcbc246..3799359f 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -148,6 +148,37 @@ const getInstancePanel = async ({ store }) => {
}
}
+const getStickers = async ({ store }) => {
+ try {
+ const res = await window.fetch('/static/stickers.json')
+ if (res.ok) {
+ const values = await res.json()
+ const stickers = (await Promise.all(
+ Object.entries(values).map(async ([name, path]) => {
+ const resPack = await window.fetch(path + 'pack.json')
+ var meta = {}
+ if (resPack.ok) {
+ meta = await resPack.json()
+ }
+ return {
+ pack: name,
+ path,
+ meta
+ }
+ })
+ )).sort((a, b) => {
+ return a.meta.title.localeCompare(b.meta.title)
+ })
+ store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
+ } else {
+ throw (res)
+ }
+ } catch (e) {
+ console.warn("Can't load stickers")
+ console.warn(e)
+ }
+}
+
const getStaticEmoji = async ({ store }) => {
try {
const res = await window.fetch('/static/emoji.json')
@@ -286,6 +317,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
setConfig({ store }),
getTOS({ store }),
getInstancePanel({ store }),
+ getStickers({ store }),
getStaticEmoji({ store }),
getCustomEmoji({ store }),
getNodeInfo({ store })
diff --git a/src/boot/routes.js b/src/boot/routes.js
index 22641f83..7dc4b2a5 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -19,6 +19,14 @@ import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
import About from 'components/about/about.vue'
export default (store) => {
+ const validateAuthenticatedRoute = (to, from, next) => {
+ if (store.state.users.currentUser) {
+ next()
+ } else {
+ next(store.state.instance.redirectRootNoLogin || '/main/all')
+ }
+ }
+
return [
{ name: 'root',
path: '/',
@@ -30,23 +38,23 @@ export default (store) => {
},
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
- { name: 'friends', path: '/main/friends', component: FriendsTimeline },
+ { name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
- { name: 'interactions', path: '/users/:username/interactions', component: Interactions },
- { name: 'dms', path: '/users/:username/dms', component: DMs },
+ { name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
+ { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration },
{ name: 'registration-token', path: '/registration/:token', component: Registration },
- { name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
- { name: 'user-settings', path: '/user-settings', component: UserSettings },
- { name: 'notifications', path: '/:username/notifications', component: Notifications },
+ { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
+ { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },
+ { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
{ name: 'login', path: '/login', component: AuthForm },
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
- { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow },
+ { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
{ name: 'about', path: '/about', component: About },
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
]
diff --git a/src/components/emoji-input/suggestor.js b/src/components/emoji-input/suggestor.js
index a7ac203e..aec5c39d 100644
--- a/src/components/emoji-input/suggestor.js
+++ b/src/components/emoji-input/suggestor.js
@@ -1,20 +1,27 @@
+import { debounce } from 'lodash'
/**
* suggest - generates a suggestor function to be used by emoji-input
* data: object providing source information for specific types of suggestions:
* data.emoji - optional, an array of all emoji available i.e.
* (state.instance.emoji + state.instance.customEmoji)
* data.users - optional, an array of all known users
+ * updateUsersList - optional, a function to search and append to users
*
* Depending on data present one or both (or none) can be present, so if field
* doesn't support user linking you can just provide only emoji.
*/
+
+const debounceUserSearch = debounce((data, input) => {
+ data.updateUsersList(input)
+}, 500, { leading: true, trailing: false })
+
export default data => input => {
const firstChar = input[0]
if (firstChar === ':' && data.emoji) {
return suggestEmoji(data.emoji)(input)
}
if (firstChar === '@' && data.users) {
- return suggestUsers(data.users)(input)
+ return suggestUsers(data)(input)
}
return []
}
@@ -38,9 +45,11 @@ export const suggestEmoji = emojis => input => {
})
}
-export const suggestUsers = users => input => {
+export const suggestUsers = data => input => {
const noPrefix = input.toLowerCase().substr(1)
- return users.filter(
+ const users = data.users
+
+ const newUsers = users.filter(
user =>
user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix)
@@ -75,5 +84,11 @@ export const suggestUsers = users => input => {
imageUrl: profile_image_url_original,
replacement: '@' + screen_name + ' '
}))
+
+ // BE search users if there are no matches
+ if (newUsers.length === 0 && data.updateUsersList) {
+ debounceUserSearch(data, noPrefix)
+ }
+ return newUsers
/* eslint-enable camelcase */
}
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
index 56b2c41e..8d123293 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -1,35 +1,18 @@
-import Popper from 'vue-popperjs/src/component/popper.js.vue'
-
const ExtraButtons = {
props: [ 'status' ],
- components: {
- Popper
- },
- data () {
- return {
- showDropDown: false,
- showPopper: true
- }
- },
methods: {
deleteStatus () {
- this.refreshPopper()
const confirmed = window.confirm(this.$t('status.delete_confirm'))
if (confirmed) {
this.$store.dispatch('deleteStatus', { id: this.status.id })
}
},
- toggleMenu () {
- this.showDropDown = !this.showDropDown
- },
pinStatus () {
- this.refreshPopper()
this.$store.dispatch('pinStatus', this.status.id)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
},
unpinStatus () {
- this.refreshPopper()
this.$store.dispatch('unpinStatus', this.status.id)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
@@ -45,13 +28,6 @@ const ExtraButtons = {
this.$store.dispatch('unmuteConversation', this.status.id)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
- },
- refreshPopper () {
- this.showPopper = false
- this.showDropDown = false
- setTimeout(() => {
- this.showPopper = true
- })
}
},
computed: {
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index 5027be1b..fc800072 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -1,18 +1,13 @@
<template>
- <Popper
- v-if="showPopper"
+ <v-popover
+ v-if="enabled"
trigger="click"
- append-to-body
- :options="{
- placement: 'top',
- modifiers: {
- arrow: { enabled: true },
- offset: { offset: '0, 5px' },
- }
- }"
- @hide="showDropDown = false"
+ placement="top"
+ class="extra-button-popover"
+ :offset="5"
+ :container="false"
>
- <div class="popper-wrapper">
+ <div slot="popover">
<div class="dropdown-menu">
<button
v-if="!status.muted"
@@ -30,6 +25,7 @@
</button>
<button
v-if="!status.pinned && canPin"
+ v-close-popover
class="dropdown-item dropdown-item-icon"
@click.prevent="pinStatus"
>
@@ -37,6 +33,7 @@
</button>
<button
v-if="status.pinned && canPin"
+ v-close-popover
class="dropdown-item dropdown-item-icon"
@click.prevent="unpinStatus"
>
@@ -44,6 +41,7 @@
</button>
<button
v-if="canDelete"
+ v-close-popover
class="dropdown-item dropdown-item-icon"
@click.prevent="deleteStatus"
>
@@ -51,17 +49,10 @@
</button>
</div>
</div>
- <div
- slot="reference"
- class="button-icon"
- @click="toggleMenu"
- >
- <i
- class="icon-ellipsis"
- :class="{'icon-clicked': showDropDown}"
- />
+ <div class="button-icon">
+ <i class="icon-ellipsis" />
</div>
- </Popper>
+ </v-popover>
</template>
<script src="./extra_buttons.js" ></script>
@@ -73,7 +64,8 @@
.icon-ellipsis {
cursor: pointer;
- &:hover, &.icon-clicked {
+ &:hover,
+ .extra-button-popover.open & {
color: $fallback--text;
color: var(--text, $fallback--text);
}
diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js
index 11de9f93..8aadc8c5 100644
--- a/src/components/moderation_tools/moderation_tools.js
+++ b/src/components/moderation_tools/moderation_tools.js
@@ -1,5 +1,4 @@
import DialogModal from '../dialog_modal/dialog_modal.vue'
-import Popper from 'vue-popperjs/src/component/popper.js.vue'
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
const STRIP_MEDIA = 'mrf_tag:media-strip'
@@ -29,8 +28,7 @@ const ModerationTools = {
}
},
components: {
- DialogModal,
- Popper
+ DialogModal
},
computed: {
tagsSet () {
@@ -41,9 +39,6 @@ const ModerationTools = {
}
},
methods: {
- toggleMenu () {
- this.showDropDown = !this.showDropDown
- },
hasTag (tagName) {
return this.tagsSet.has(tagName)
},
diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue
index f1ab67a6..d97ca3aa 100644
--- a/src/components/moderation_tools/moderation_tools.vue
+++ b/src/components/moderation_tools/moderation_tools.vue
@@ -1,18 +1,15 @@
<template>
<div>
- <Popper
+ <v-popover
trigger="click"
- append-to-body
- :options="{
- placement: 'bottom-end',
- modifiers: {
- arrow: { enabled: true },
- offset: { offset: '0, 5px' },
- }
- }"
+ class="moderation-tools-popover"
+ :container="false"
+ placement="bottom-end"
+ :offset="5"
+ @show="showDropDown = true"
@hide="showDropDown = false"
>
- <div class="popper-wrapper">
+ <div slot="popover">
<div class="dropdown-menu">
<span v-if="user.is_local">
<button
@@ -127,14 +124,12 @@
</div>
</div>
<button
- slot="reference"
class="btn btn-default btn-block"
:class="{ pressed: showDropDown }"
- @click="toggleMenu"
>
{{ $t('user_card.admin_menu.moderation') }}
</button>
- </Popper>
+ </v-popover>
<portal to="modal">
<DialogModal
v-if="showDeleteUserDialog"
@@ -188,4 +183,11 @@
}
}
+.moderation-tools-popover {
+ height: 100%;
+ .trigger {
+ display: flex !important;
+ height: 100%;
+ }
+}
</style>
diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss
index cfc5c8e7..279b01be 100644
--- a/src/components/popper/popper.scss
+++ b/src/components/popper/popper.scss
@@ -1,71 +1,99 @@
@import '../../_variables.scss';
-.popper-wrapper {
+.tooltip.popover {
z-index: 8;
-}
-.popper-wrapper .popper__arrow {
- width: 0;
- height: 0;
- border-style: solid;
- position: absolute;
- margin: 5px;
-}
+ .popover-inner {
+ box-shadow: 1px 1px 4px rgba(0,0,0,.6);
+ box-shadow: var(--panelShadow);
+ border-radius: $fallback--btnRadius;
+ border-radius: var(--btnRadius, $fallback--btnRadius);
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
+ }
-.popper-wrapper[x-placement^="top"] {
- margin-bottom: 5px;
-}
+ .popover-arrow {
+ width: 0;
+ height: 0;
+ border-style: solid;
+ position: absolute;
+ margin: 5px;
+ border-color: $fallback--bg;
+ border-color: var(--bg, $fallback--bg);
+ z-index: 1;
+ }
-.popper-wrapper[x-placement^="top"] .popper__arrow {
- border-width: 5px 5px 0 5px;
- border-color: $fallback--bg transparent transparent transparent;
- border-color: var(--bg, $fallback--bg) transparent transparent transparent;
- bottom: -5px;
- left: calc(50% - 5px);
- margin-top: 0;
- margin-bottom: 0;
-}
+ &[x-placement^="top"] {
+ margin-bottom: 5px;
+
+ .popover-arrow {
+ border-width: 5px 5px 0 5px;
+ border-left-color: transparent !important;
+ border-right-color: transparent !important;
+ border-bottom-color: transparent !important;
+ bottom: -5px;
+ left: calc(50% - 5px);
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+ }
-.popper-wrapper[x-placement^="bottom"] {
- margin-top: 5px;
-}
+ &[x-placement^="bottom"] {
+ margin-top: 5px;
+
+ .popover-arrow {
+ border-width: 0 5px 5px 5px;
+ border-left-color: transparent !important;
+ border-right-color: transparent !important;
+ border-top-color: transparent !important;
+ top: -5px;
+ left: calc(50% - 5px);
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+ }
-.popper-wrapper[x-placement^="bottom"] .popper__arrow {
- border-width: 0 5px 5px 5px;
- border-color: transparent transparent $fallback--bg transparent;
- border-color: transparent transparent var(--bg, $fallback--bg) transparent;
- top: -5px;
- left: calc(50% - 5px);
- margin-top: 0;
- margin-bottom: 0;
-}
+ &[x-placement^="right"] {
+ margin-left: 5px;
+
+ .popover-arrow {
+ border-width: 5px 5px 5px 0;
+ border-left-color: transparent !important;
+ border-top-color: transparent !important;
+ border-bottom-color: transparent !important;
+ left: -5px;
+ top: calc(50% - 5px);
+ margin-left: 0;
+ margin-right: 0;
+ }
+ }
-.popper-wrapper[x-placement^="right"] {
- margin-left: 5px;
-}
+ &[x-placement^="left"] {
+ margin-right: 5px;
-.popper-wrapper[x-placement^="right"] .popper__arrow {
- border-width: 5px 5px 5px 0;
- border-color: transparent $fallback--bg transparent transparent;
- border-color: transparent var(--bg, $fallback--bg) transparent transparent;
- left: -5px;
- top: calc(50% - 5px);
- margin-left: 0;
- margin-right: 0;
-}
+ .popover-arrow {
+ border-width: 5px 0 5px 5px;
+ border-top-color: transparent !important;
+ border-right-color: transparent !important;
+ border-bottom-color: transparent !important;
+ right: -5px;
+ top: calc(50% - 5px);
+ margin-left: 0;
+ margin-right: 0;
+ }
+ }
-.popper-wrapper[x-placement^="left"] {
- margin-right: 5px;
-}
+ &[aria-hidden='true'] {
+ visibility: hidden;
+ opacity: 0;
+ transition: opacity .15s, visibility .15s;
+ }
-.popper-wrapper[x-placement^="left"] .popper__arrow {
- border-width: 5px 0 5px 5px;
- border-color: transparent transparent transparent $fallback--bg;
- border-color: transparent transparent transparent var(--bg, $fallback--bg);
- right: -5px;
- top: calc(50% - 5px);
- margin-left: 0;
- margin-right: 0;
+ &[aria-hidden='false'] {
+ visibility: visible;
+ opacity: 1;
+ transition: opacity .15s;
+ }
}
.dropdown-menu {
@@ -76,13 +104,6 @@
list-style: none;
max-width: 100vw;
z-index: 10;
- box-shadow: 1px 1px 4px rgba(0,0,0,.6);
- box-shadow: var(--panelShadow);
- border: none;
- border-radius: $fallback--btnRadius;
- border-radius: var(--btnRadius, $fallback--btnRadius);
- background-color: $fallback--bg;
- background-color: var(--bg, $fallback--bg);
.dropdown-divider {
height: 0;
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index ef6b0fce..40bbf6d4 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -3,6 +3,7 @@ import MediaUpload from '../media_upload/media_upload.vue'
import ScopeSelector from '../scope_selector/scope_selector.vue'
import EmojiInput from '../emoji-input/emoji-input.vue'
import PollForm from '../poll/poll_form.vue'
+import StickerPicker from '../sticker_picker/sticker_picker.vue'
import fileTypeService from '../../services/file_type/file_type.service.js'
import { reject, map, uniqBy } from 'lodash'
import suggestor from '../emoji-input/suggestor.js'
@@ -34,6 +35,7 @@ const PostStatusForm = {
MediaUpload,
EmojiInput,
PollForm,
+ StickerPicker,
ScopeSelector
},
mounted () {
@@ -82,7 +84,8 @@ const PostStatusForm = {
contentType
},
caret: 0,
- pollFormVisible: false
+ pollFormVisible: false,
+ stickerPickerVisible: false
}
},
computed: {
@@ -104,7 +107,8 @@ const PostStatusForm = {
...this.$store.state.instance.emoji,
...this.$store.state.instance.customEmoji
],
- users: this.$store.state.users.users
+ users: this.$store.state.users.users,
+ updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
})
},
emojiSuggestor () {
@@ -157,6 +161,12 @@ const PostStatusForm = {
safeDMEnabled () {
return this.$store.state.instance.safeDM
},
+ stickersAvailable () {
+ if (this.$store.state.instance.stickers) {
+ return this.$store.state.instance.stickers.length > 0
+ }
+ return 0
+ },
pollsAvailable () {
return this.$store.state.instance.pollsAvailable &&
this.$store.state.instance.pollLimits.max_options >= 2
@@ -212,6 +222,7 @@ const PostStatusForm = {
poll: {}
}
this.pollFormVisible = false
+ this.stickerPickerVisible = false
this.$refs.mediaUpload.clearFile()
this.clearPollForm()
this.$emit('posted')
@@ -228,6 +239,7 @@ const PostStatusForm = {
addMediaFile (fileInfo) {
this.newStatus.files.push(fileInfo)
this.enableSubmit()
+ this.stickerPickerVisible = false
},
removeMediaFile (fileInfo) {
let index = this.newStatus.files.indexOf(fileInfo)
@@ -287,6 +299,14 @@ const PostStatusForm = {
changeVis (visibility) {
this.newStatus.visibility = visibility
},
+ toggleStickerPicker () {
+ this.stickerPickerVisible = !this.stickerPickerVisible
+ },
+ clearStickerPicker () {
+ if (this.$refs.stickerPicker) {
+ this.$refs.stickerPicker.clear()
+ }
+ },
togglePollForm () {
this.pollFormVisible = !this.pollFormVisible
},
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 1f389eda..d29d47e4 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -158,6 +158,17 @@
@upload-failed="uploadFailed"
/>
<div
+ v-if="stickersAvailable"
+ class="sticker-icon"
+ >
+ <i
+ :title="$t('stickers.add_sticker')"
+ class="icon-picture btn btn-default"
+ :class="{ selected: stickerPickerVisible }"
+ @click="toggleStickerPicker"
+ />
+ </div>
+ <div
v-if="pollsAvailable"
class="poll-icon"
>
@@ -169,7 +180,6 @@
/>
</div>
</div>
-
<button
v-if="posting"
disabled
@@ -248,6 +258,11 @@
<label for="filesSensitive">{{ $t('post_status.attachments_sensitive') }}</label>
</div>
</form>
+ <sticker-picker
+ v-if="stickerPickerVisible"
+ ref="stickerPicker"
+ @uploaded="addMediaFile"
+ />
</div>
</template>
@@ -310,7 +325,7 @@
}
}
- .poll-icon {
+ .poll-icon, .sticker-icon {
font-size: 26px;
flex: 1;
@@ -320,6 +335,11 @@
}
}
+ .sticker-icon {
+ flex: 0;
+ min-width: 50px;
+ }
+
.icon-chart-bar {
cursor: pointer;
}
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 7911d40d..3c172e5b 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -110,8 +110,9 @@ const Status = {
},
muteWordHits () {
const statusText = this.status.text.toLowerCase()
+ const statusSummary = this.status.summary.toLowerCase()
const hits = filter(this.muteWords, (muteWord) => {
- return statusText.includes(muteWord.toLowerCase())
+ return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())
})
return hits
@@ -280,6 +281,11 @@ const Status = {
},
tags () {
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
+ },
+ hidePostStats () {
+ return typeof this.$store.state.config.hidePostStats === 'undefined'
+ ? this.$store.state.instance.hidePostStats
+ : this.$store.state.config.hidePostStats
}
},
components: {
@@ -316,11 +322,8 @@ const Status = {
this.error = undefined
},
linkClicked (event) {
- let { target } = event
- if (target.tagName === 'SPAN') {
- target = target.parentNode
- }
- if (target.tagName === 'A') {
+ const target = event.target.closest('.status-content a')
+ if (target) {
if (target.className.match(/mention/)) {
const href = target.href
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 30969256..ab506632 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -344,7 +344,7 @@
<transition name="fade">
<div
- v-if="isFocused && combinedFavsAndRepeatsUsers.length > 0"
+ v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0"
class="favs-repeated-users"
>
<div class="stats">
@@ -820,11 +820,12 @@ $status-margin: 0.75em;
}
.status-actions {
+ position: relative;
width: 100%;
display: flex;
margin-top: $status-margin;
- div, favorite-button {
+ > * {
max-width: 4em;
flex: 1;
}
diff --git a/src/components/sticker_picker/sticker_picker.js b/src/components/sticker_picker/sticker_picker.js
new file mode 100644
index 00000000..a6dcded3
--- /dev/null
+++ b/src/components/sticker_picker/sticker_picker.js
@@ -0,0 +1,52 @@
+/* eslint-env browser */
+import statusPosterService from '../../services/status_poster/status_poster.service.js'
+import TabSwitcher from '../tab_switcher/tab_switcher.js'
+
+const StickerPicker = {
+ components: [
+ TabSwitcher
+ ],
+ data () {
+ return {
+ meta: {
+ stickers: []
+ },
+ path: ''
+ }
+ },
+ computed: {
+ pack () {
+ return this.$store.state.instance.stickers || []
+ }
+ },
+ methods: {
+ clear () {
+ this.meta = {
+ stickers: []
+ }
+ },
+ pick (sticker, name) {
+ const store = this.$store
+ // TODO remove this workaround by finding a way to bypass reuploads
+ fetch(sticker)
+ .then((res) => {
+ res.blob().then((blob) => {
+ var file = new File([blob], name, { mimetype: 'image/png' })
+ var formData = new FormData()
+ formData.append('file', file)
+ statusPosterService.uploadMedia({ store, formData })
+ .then((fileData) => {
+ this.$emit('uploaded', fileData)
+ this.clear()
+ }, (error) => {
+ console.warn("Can't attach sticker")
+ console.warn(error)
+ this.$emit('upload-failed', 'default')
+ })
+ })
+ })
+ }
+ }
+}
+
+export default StickerPicker
diff --git a/src/components/sticker_picker/sticker_picker.vue b/src/components/sticker_picker/sticker_picker.vue
new file mode 100644
index 00000000..938204c8
--- /dev/null
+++ b/src/components/sticker_picker/sticker_picker.vue
@@ -0,0 +1,62 @@
+<template>
+ <div
+ class="sticker-picker"
+ >
+ <div
+ class="sticker-picker-panel"
+ >
+ <tab-switcher
+ :render-only-focused="true"
+ >
+ <div
+ v-for="stickerpack in pack"
+ :key="stickerpack.path"
+ :image-tooltip="stickerpack.meta.title"
+ :image="stickerpack.path + stickerpack.meta.tabIcon"
+ class="sticker-picker-content"
+ >
+ <div
+ v-for="sticker in stickerpack.meta.stickers"
+ :key="sticker"
+ class="sticker"
+ @click="pick(stickerpack.path + sticker, stickerpack.meta.title)"
+ >
+ <img
+ :src="stickerpack.path + sticker"
+ >
+ </div>
+ </div>
+ </tab-switcher>
+ </div>
+ </div>
+</template>
+
+<script src="./sticker_picker.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.sticker-picker {
+ .sticker-picker-panel {
+ display: inline-block;
+ width: 100%;
+ .sticker-picker-content {
+ max-height: 300px;
+ overflow-y: scroll;
+ overflow-x: auto;
+ .sticker {
+ display: inline-block;
+ width: 20%;
+ height: 20%;
+ img {
+ width: 100%;
+ &:hover {
+ filter: drop-shadow(0 0 5px var(--link, $fallback--link));
+ }
+ }
+ }
+ }
+ }
+}
+
+</style>
diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js
index 67835231..a5fe019c 100644
--- a/src/components/tab_switcher/tab_switcher.js
+++ b/src/components/tab_switcher/tab_switcher.js
@@ -45,7 +45,19 @@ export default Vue.component('tab-switcher', {
classesTab.push('active')
classesWrapper.push('active')
}
-
+ if (slot.data.attrs.image) {
+ return (
+ <div class={ classesWrapper.join(' ')}>
+ <button
+ disabled={slot.data.attrs.disabled}
+ onClick={this.activateTab(index)}
+ class={classesTab.join(' ')}>
+ <img src={slot.data.attrs.image} title={slot.data.attrs['image-tooltip']}/>
+ {slot.data.attrs.label ? '' : slot.data.attrs.label}
+ </button>
+ </div>
+ )
+ }
return (
<div class={ classesWrapper.join(' ')}>
<button
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index f7449439..4eeb42e0 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -53,6 +53,12 @@
background: transparent;
z-index: 5;
}
+
+ img {
+ max-height: 26px;
+ vertical-align: top;
+ margin-top: -5px;
+ }
}
&:not(.active) {
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index f987fbbb..9e142480 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -283,7 +283,6 @@
.user-card {
background-size: cover;
- overflow: hidden;
.panel-heading {
padding: .5em 0;
@@ -298,6 +297,8 @@
word-wrap: break-word;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%);
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%);
+ border-bottom-right-radius: inherit;
+ border-bottom-left-radius: inherit;
}
p {
@@ -503,6 +504,7 @@
}
}
.user-interactions {
+ position: relative;
display: flex;
flex-flow: row wrap;
justify-content: space-between;
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 0c3b2aef..b5a7f0df 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -91,7 +91,8 @@ const UserSettings = {
...this.$store.state.instance.emoji,
...this.$store.state.instance.customEmoji
],
- users: this.$store.state.users.users
+ users: this.$store.state.users.users,
+ updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
})
},
emojiSuggestor () {
diff --git a/src/i18n/en.json b/src/i18n/en.json
index ec90cfcf..6230675c 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -106,6 +106,9 @@
"expired": "Poll ended {0} ago",
"not_enough_options": "Too few unique options in poll"
},
+ "stickers": {
+ "add_sticker": "Add Sticker"
+ },
"interactions": {
"favs_repeats": "Repeats and Favorites",
"follows": "New follows",
diff --git a/src/i18n/es.json b/src/i18n/es.json
index 2e38f859..bf87937a 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -27,7 +27,11 @@
"optional": "opcional",
"show_more": "Mostrar más",
"show_less": "Mostrar menos",
- "cancel": "Cancelar"
+ "cancel": "Cancelar",
+ "disable": "Inhabilitar",
+ "enable": "Habilitar",
+ "confirm": "Confirmar",
+ "verify": "Verificar"
},
"image_cropper": {
"crop_picture": "Recortar la foto",
@@ -48,7 +52,15 @@
"placeholder": "p.ej. lain",
"register": "Registrar",
"username": "Usuario",
- "hint": "Inicia sesión para unirte a la discusión"
+ "hint": "Inicia sesión para unirte a la discusión",
+ "authentication_code": "Código de autentificación",
+ "enter_recovery_code": "Inserta el código de recuperación",
+ "enter_two_factor_code": "Inserta el código de doble factor",
+ "recovery_code": "Código de recuperación",
+ "heading" : {
+ "totp" : "Autentificación de doble factor",
+ "recovery" : "Recuperación de doble factor"
+ }
},
"media_modal": {
"previous": "Anterior",
@@ -60,11 +72,13 @@
"chat": "Chat Local",
"friend_requests": "Solicitudes de amistad",
"mentions": "Menciones",
+ "interactions": "Interacciones",
"dms": "Mensajes Directo",
"public_tl": "Línea Temporal Pública",
"timeline": "Línea Temporal",
"twkn": "Toda La Red Conocida",
"user_search": "Búsqueda de Usuarios",
+ "search": "Buscar",
"who_to_follow": "A quién seguir",
"preferences": "Preferencias"
},
@@ -78,6 +92,25 @@
"repeated_you": "repite tu estado",
"no_more_notifications": "No hay más notificaciones"
},
+ "polls": {
+ "add_poll": "Añadir encuesta",
+ "add_option": "Añadir opción",
+ "option": "Opción",
+ "votes": "votos",
+ "vote": "Votar",
+ "type": "Tipo de encuesta",
+ "single_choice": "Elección única",
+ "multiple_choices": "Múltiples elecciones",
+ "expiry": "Tiempo de vida de la encuesta",
+ "expires_in": "La encuensta termina en {0}",
+ "expired": "La encuesta terminó hace {0}",
+ "not_enough_options": "Muy pocas opciones únicas en la encuesta"
+ },
+ "interactions": {
+ "favs_repeats": "Favoritos y Repetidos",
+ "follows": "Nuevos seguidores",
+ "load_older": "Cargar interacciones antiguas"
+ },
"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.",
@@ -91,9 +124,14 @@
},
"content_warning": "Tema (opcional)",
"default": "Acabo de aterrizar en L.A.",
- "direct_warning": "Esta publicación solo será visible para los usuarios mencionados.",
+ "direct_warning_to_all": "Esta publicación será visible para todos los usarios mencionados.",
"direct_warning_to_first_only": "Esta publicación solo será visible para los usuarios mencionados al comienzo del mensaje.",
"posting": "Publicando",
+ "scope_notice": {
+ "public": "Esta publicación será visible para todo el mundo",
+ "private": "Esta publicación solo será visible para tus seguidores.",
+ "unlisted": "Esta publicación no será visible en la Línea Temporal Pública ni en Toda La Red Conocida"
+ },
"scope": {
"direct": "Directo - Solo para los usuarios mencionados.",
"private": "Solo-Seguidores - Solo tus seguidores leeran la publicación",
@@ -127,6 +165,29 @@
},
"settings": {
"app_name": "Nombre de la aplicación",
+ "security": "Seguridad",
+ "enter_current_password_to_confirm": "Introduce la contraseña actual para confirmar tu identidad",
+ "mfa": {
+ "otp" : "OTP",
+ "setup_otp" : "Configurar OTP",
+ "wait_pre_setup_otp" : "preconfiguración OTP",
+ "confirm_and_enable" : "Confirmar y habilitar OTP",
+ "title": "Autentificación de Doble Factor",
+ "generate_new_recovery_codes" : "Generar nuevos códigos de recuperación",
+ "warning_of_generate_new_codes" : "Cuando generas nuevos códigos de recuperación, los antiguos dejarán de funcionar.",
+ "recovery_codes" : "Códigos de recuperación.",
+ "waiting_a_recovery_codes": "Recibiendo códigos de respaldo",
+ "recovery_codes_warning" : "Anote los códigos o guárdelos en un lugar seguro, de lo contrario no los volverá a ver. Si pierde el acceso a su aplicación 2FA y los códigos de recuperación, su cuenta quedará bloqueada.",
+ "authentication_methods" : "Métodos de autentificación",
+ "scan": {
+ "title": "Escanear",
+ "desc": "Usando su aplicación de doble factor, escanee este código QR o ingrese la clave de texto:",
+ "secret_code": "Clave"
+ },
+ "verify": {
+ "desc": "Para habilitar la autenticación de doble factor, ingrese el código de su aplicación 2FA:"
+ }
+ },
"attachmentRadius": "Adjuntos",
"attachments": "Adjuntos",
"autoload": "Activar carga automática al llegar al final de la página",
@@ -233,6 +294,7 @@
"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í",
+ "autohide_floating_post_button": "Ocultar automáticamente el botón 'Nueva Publicación' (móvil)",
"saving_err": "Error al guardar los ajustes",
"saving_ok": "Ajustes guardados",
"search_user_to_block": "Buscar usuarios a bloquear",
@@ -265,6 +327,13 @@
"true": "sí"
},
"notifications": "Notificaciones",
+ "notification_setting": "Recibir notificaciones de:",
+ "notification_setting_follows": "Usuarios que sigues",
+ "notification_setting_non_follows": "Usuarios que no sigues",
+ "notification_setting_followers": "Usuarios que te siguen",
+ "notification_setting_non_followers": "Usuarios que no te siguen",
+ "notification_mutes": "Para dejar de recibir notificaciones de un usuario específico, siléncialo.",
+ "notification_blocks": "El bloqueo de un usuario detiene todas las notificaciones y también las cancela.",
"enable_web_push_notifications": "Habilitar las notificiaciones en el navegador",
"style": {
"switcher": {
@@ -381,6 +450,40 @@
"frontend_version": "Versión del Frontend"
}
},
+ "time": {
+ "day": "{0} día",
+ "days": "{0} días",
+ "day_short": "{0}d",
+ "days_short": "{0}d",
+ "hour": "{0} hora",
+ "hours": "{0} horas",
+ "hour_short": "{0}h",
+ "hours_short": "{0}h",
+ "in_future": "en {0}",
+ "in_past": "hace {0}",
+ "minute": "{0} minuto",
+ "minutes": "{0} minutos",
+ "minute_short": "{0}min",
+ "minutes_short": "{0}min",
+ "month": "{0} mes",
+ "months": "{0} meses",
+ "month_short": "{0}m",
+ "months_short": "{0}m",
+ "now": "justo ahora",
+ "now_short": "ahora",
+ "second": "{0} segundo",
+ "seconds": "{0} segundos",
+ "second_short": "{0}s",
+ "seconds_short": "{0}s",
+ "week": "{0} semana",
+ "weeks": "{0} semana",
+ "week_short": "{0}sem",
+ "weeks_short": "{0}sem",
+ "year": "{0} año",
+ "years": "{0} años",
+ "year_short": "{0}a",
+ "years_short": "{0}a"
+ },
"timeline": {
"collapse": "Colapsar",
"conversation": "Conversación",
@@ -396,6 +499,11 @@
"status": {
"favorites": "Favoritos",
"repeats": "Repetidos",
+ "delete": "Eliminar publicación",
+ "pin": "Fijar en tu perfil",
+ "unpin": "Desclavar de tu perfil",
+ "pinned": "Fijado",
+ "delete_confirm": "¿Realmente quieres borrar la publicación?",
"reply_to": "Responder a",
"replies_list": "Respuestas:"
},
@@ -422,6 +530,8 @@
"remote_follow": "Seguir",
"report": "Reportar",
"statuses": "Estados",
+ "subscribe": "Suscribirse",
+ "unsubscribe": "Desuscribirse",
"unblock": "Desbloquear",
"unblock_progress": "Desbloqueando...",
"block_progress": "Bloqueando...",
@@ -486,5 +596,12 @@
"GiB": "GiB",
"TiB": "TiB"
}
+ },
+ "search": {
+ "people": "Personas",
+ "hashtags": "Hashtags",
+ "person_talking": "{count} personas hablando",
+ "people_talking": "{count} gente hablando",
+ "no_results": "Sin resultados"
}
} \ No newline at end of file
diff --git a/src/i18n/ja.json b/src/i18n/ja.json
index 559bb4c9..c77f28a6 100644
--- a/src/i18n/ja.json
+++ b/src/i18n/ja.json
@@ -27,7 +27,11 @@
"optional": "かかなくてもよい",
"show_more": "つづきをみる",
"show_less": "たたむ",
- "cancel": "キャンセル"
+ "cancel": "キャンセル",
+ "disable": "なし",
+ "enable": "あり",
+ "confirm": "たしかめる",
+ "verify": "たしかめる"
},
"image_cropper": {
"crop_picture": "がぞうをきりぬく",
@@ -48,7 +52,15 @@
"placeholder": "れい: lain",
"register": "はじめる",
"username": "ユーザーめい",
- "hint": "はなしあいにくわわるには、ログインしてください"
+ "hint": "はなしあいにくわわるには、ログインしてください",
+ "authentication_code": "にんしょうコード",
+ "enter_recovery_code": "リカバリーコードをいれてください",
+ "enter_two_factor_code": "2-ファクターコードをいれてください",
+ "recovery_code": "リカバリーコード",
+ "heading" : {
+ "totp" : "2-ファクターにんしょう",
+ "recovery" : "2-ファクターリカバリー"
+ }
},
"media_modal": {
"previous": "まえ",
@@ -79,6 +91,20 @@
"repeated_you": "あなたのステータスがリピートされました",
"no_more_notifications": "つうちはありません"
},
+ "polls": {
+ "add_poll": "いれふだをはじめる",
+ "add_option": "オプションをふやす",
+ "option": "オプション",
+ "votes": "いれふだ",
+ "vote": "ふだをいれる",
+ "type": "いれふだのかた",
+ "single_choice": "ひとつえらぶ",
+ "multiple_choices": "いくつでもえらべる",
+ "expiry": "いれふだのながさ",
+ "expires_in": "いれふだは {0} で、おわります",
+ "expired": "いれふだは {0} まえに、おわりました",
+ "not_enough_options": "ユニークなオプションが、たりません"
+ },
"interactions": {
"favs_repeats": "リピートとおきにいり",
"follows": "あたらしいフォロー",
@@ -139,6 +165,29 @@
},
"settings": {
"app_name": "アプリのなまえ",
+ "security": "セキュリティ",
+ "enter_current_password_to_confirm": "あなたのアイデンティティをたしかめるため、あなたのいまのパスワードをかいてください",
+ "mfa": {
+ "otp" : "OTP",
+ "setup_otp" : "OTPをつくる",
+ "wait_pre_setup_otp" : "OTPをよういしています",
+ "confirm_and_enable" : "OTPをたしかめて、ゆうこうにする",
+ "title": "2-ファクターにんしょう",
+ "generate_new_recovery_codes" : "あたらしいリカバリーコードをつくる",
+ "warning_of_generate_new_codes" : "あたらしいリカバリーコードをつくったら、ふるいコードはつかえなくなります。",
+ "recovery_codes" : "リカバリーコード。",
+ "waiting_a_recovery_codes": "バックアップコードをうけとっています...",
+ "recovery_codes_warning" : "コードをかきうつすか、ひとにみられないところにセーブしてください。そうでなければ、あなたはこのコードをふたたびみることはできません。もしあなたが、2FAアプリのアクセスをうしなって、なおかつ、リカバリーコードもおもいだせないならば、あなたはあなたのアカウントから、しめだされます。",
+ "authentication_methods" : "にんしょうメソッド",
+ "scan": {
+ "title": "スキャン",
+ "desc": "あなたの2-ファクターアプリをつかって、このQRコードをスキャンするか、テキストキーをうちこんでください:",
+ "secret_code": "キー"
+ },
+ "verify": {
+ "desc": "2-ファクターにんしょうをつかうには、あなたの2-ファクターアプリのコードをいれてください:"
+ }
+ },
"attachmentRadius": "ファイル",
"attachments": "ファイル",
"autoload": "したにスクロールしたとき、じどうてきによみこむ。",
diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json
index 345e1f1c..992a2fa6 100644
--- a/src/i18n/ja_pedantic.json
+++ b/src/i18n/ja_pedantic.json
@@ -27,7 +27,11 @@
"optional": "省略可",
"show_more": "もっと見る",
"show_less": "たたむ",
- "cancel": "キャンセル"
+ "cancel": "キャンセル",
+ "disable": "無効",
+ "enable": "有効",
+ "confirm": "確認",
+ "verify": "検査"
},
"image_cropper": {
"crop_picture": "画像を切り抜く",
@@ -48,7 +52,15 @@
"placeholder": "例: lain",
"register": "登録",
"username": "ユーザー名",
- "hint": "会話に加わるには、ログインしてください"
+ "hint": "会話に加わるには、ログインしてください",
+ "authentication_code": "認証コード",
+ "enter_recovery_code": "リカバリーコードを入力してください",
+ "enter_two_factor_code": "2段階認証コードを入力してください",
+ "recovery_code": "リカバリーコード",
+ "heading" : {
+ "totp" : "2段階認証",
+ "recovery" : "2段階リカバリー"
+ }
},
"media_modal": {
"previous": "前",
@@ -79,6 +91,20 @@
"repeated_you": "あなたのステータスがリピートされました",
"no_more_notifications": "通知はありません"
},
+ "polls": {
+ "add_poll": "投票を追加",
+ "add_option": "選択肢を追加",
+ "option": "選択肢",
+ "votes": "票",
+ "vote": "投票",
+ "type": "投票の形式",
+ "single_choice": "択一式",
+ "multiple_choices": "複数選択式",
+ "expiry": "投票期間",
+ "expires_in": "投票は {0} で終了します",
+ "expired": "投票は {0} 前に終了しました",
+ "not_enough_options": "相異なる選択肢が不足しています"
+ },
"interactions": {
"favs_repeats": "リピートとお気に入り",
"follows": "新しいフォロワー",
@@ -139,6 +165,29 @@
},
"settings": {
"app_name": "アプリの名称",
+ "security": "セキュリティ",
+ "enter_current_password_to_confirm": "あなたのアイデンティティを証明するため、現在のパスワードを入力してください",
+ "mfa": {
+ "otp" : "OTP",
+ "setup_otp" : "OTPのセットアップ",
+ "wait_pre_setup_otp" : "OTPのプリセット",
+ "confirm_and_enable" : "OTPの確認と有効化",
+ "title": "2段階認証",
+ "generate_new_recovery_codes" : "新しいリカバリーコードを生成",
+ "warning_of_generate_new_codes" : "新しいリカバリーコードを生成すると、古いコードは使用できなくなります。",
+ "recovery_codes" : "リカバリーコード。",
+ "waiting_a_recovery_codes": "バックアップコードを受信しています...",
+ "recovery_codes_warning" : "コードを紙に書くか、安全な場所に保存してください。そうでなければ、あなたはコードを再び見ることはできません。もし2段階認証アプリのアクセスを喪失し、なおかつ、リカバリーコードもないならば、あなたは自分のアカウントから閉め出されます。",
+ "authentication_methods" : "認証方法",
+ "scan": {
+ "title": "スキャン",
+ "desc": "あなたの2段階認証アプリを使って、このQRコードをスキャンするか、テキストキーを入力してください:",
+ "secret_code": "キー"
+ },
+ "verify": {
+ "desc": "2段階認証を有効にするには、あなたの2段階認証アプリのコードを入力してください:"
+ }
+ },
"attachmentRadius": "ファイル",
"attachments": "ファイル",
"autoload": "下にスクロールしたとき、自動的に読み込む。",
diff --git a/src/main.js b/src/main.js
index 3287fa2b..b3256e8e 100644
--- a/src/main.js
+++ b/src/main.js
@@ -26,6 +26,7 @@ import messages from './i18n/messages.js'
import VueChatScroll from 'vue-chat-scroll'
import VueClickOutside from 'v-click-outside'
import PortalVue from 'portal-vue'
+import VTooltip from 'v-tooltip'
import afterStoreSetup from './boot/after_store.js'
@@ -37,6 +38,7 @@ Vue.use(VueI18n)
Vue.use(VueChatScroll)
Vue.use(VueClickOutside)
Vue.use(PortalVue)
+Vue.use(VTooltip)
const i18n = new VueI18n({
// By default, use the browser locale, we will update it if neccessary
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 670f7fca..1f6e1ebd 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -70,6 +70,7 @@ const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin`
const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
const MASTODON_SEARCH_2 = `/api/v2/search`
+const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
const oldfetch = window.fetch
@@ -865,6 +866,18 @@ const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
})
}
+const searchUsers = ({ credentials, query }) => {
+ return promisedRequest({
+ url: MASTODON_USER_SEARCH_URL,
+ params: {
+ q: query,
+ resolve: true
+ },
+ credentials
+ })
+ .then((data) => data.map(parseUser))
+}
+
const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
let url = MASTODON_SEARCH_2
let params = []
@@ -974,7 +987,8 @@ const apiService = {
fetchRebloggedByUsers,
reportUser,
updateNotificationSettings,
- search2
+ search2,
+ searchUsers
}
export default apiService
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 0f40edf1..846d9415 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -152,6 +152,7 @@ const backendInteractorService = credentials => {
const unretweet = (id) => apiService.unretweet({ id, credentials })
const search2 = ({ q, resolve, limit, offset, following }) =>
apiService.search2({ credentials, q, resolve, limit, offset, following })
+ const searchUsers = (query) => apiService.searchUsers({ query, credentials })
const backendInteractorServiceInstance = {
fetchStatus,
@@ -216,7 +217,8 @@ const backendInteractorService = credentials => {
retweet,
unretweet,
updateNotificationSettings,
- search2
+ search2,
+ searchUsers
}
return backendInteractorServiceInstance