Merge branch 'develop' into 'feat/conversation-muting'

# Conflicts:
#   src/components/extra_buttons/extra_buttons.js
#   src/components/extra_buttons/extra_buttons.vue
This commit is contained in:
Shpuld Shpludson 2019-07-26 12:44:32 +00:00
commit d3f6b581d1
28 changed files with 638 additions and 177 deletions

View file

@ -24,9 +24,6 @@ var devMiddleware = require('webpack-dev-middleware')(compiler, {
stats: {
colors: true,
chunks: false
},
headers: {
'content-security-policy': "base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; script-src 'self' 'unsafe-eval';"
}
})

View file

@ -25,14 +25,13 @@
"localforage": "^1.5.0",
"object-path": "^0.11.3",
"phoenix": "^1.3.0",
"popper.js": "^1.14.7",
"portal-vue": "^2.1.4",
"sanitize-html": "^1.13.0",
"v-click-outside": "^2.1.1",
"v-tooltip": "^2.0.2",
"vue": "^2.5.13",
"vue-chat-scroll": "^1.2.1",
"vue-i18n": "^7.3.2",
"vue-popperjs": "^2.0.3",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.3.4",
"vuelidate": "^0.7.4",
@ -81,8 +80,8 @@
"json-loader": "^0.5.4",
"karma": "^3.0.0",
"karma-coverage": "^1.1.1",
"karma-mocha": "^1.2.0",
"karma-firefox-launcher": "^1.1.0",
"karma-mocha": "^1.2.0",
"karma-sinon-chai": "^2.0.2",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.26",

View file

@ -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 })

View file

@ -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 }
]

View file

@ -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 */
}

View file

@ -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: {

View file

@ -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);
}

View file

@ -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)
},

View file

@ -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>

View file

@ -1,71 +1,99 @@
@import '../../_variables.scss';
.popper-wrapper {
.tooltip.popover {
z-index: 8;
}
.popper-wrapper .popper__arrow {
.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);
}
.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"] {
&[x-placement^="top"] {
margin-bottom: 5px;
}
.popper-wrapper[x-placement^="top"] .popper__arrow {
.popover-arrow {
border-width: 5px 5px 0 5px;
border-color: $fallback--bg transparent transparent transparent;
border-color: var(--bg, $fallback--bg) transparent transparent transparent;
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"] {
&[x-placement^="bottom"] {
margin-top: 5px;
}
.popper-wrapper[x-placement^="bottom"] .popper__arrow {
.popover-arrow {
border-width: 0 5px 5px 5px;
border-color: transparent transparent $fallback--bg transparent;
border-color: transparent transparent var(--bg, $fallback--bg) transparent;
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^="right"] {
&[x-placement^="right"] {
margin-left: 5px;
}
.popper-wrapper[x-placement^="right"] .popper__arrow {
.popover-arrow {
border-width: 5px 5px 5px 0;
border-color: transparent $fallback--bg transparent transparent;
border-color: transparent var(--bg, $fallback--bg) transparent transparent;
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^="left"] {
&[x-placement^="left"] {
margin-right: 5px;
}
.popper-wrapper[x-placement^="left"] .popper__arrow {
.popover-arrow {
border-width: 5px 0 5px 5px;
border-color: transparent transparent transparent $fallback--bg;
border-color: transparent transparent transparent var(--bg, $fallback--bg);
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;
}
}
&[aria-hidden='true'] {
visibility: hidden;
opacity: 0;
transition: opacity .15s, visibility .15s;
}
&[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;

View file

@ -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
},

View file

@ -157,6 +157,17 @@
@uploaded="addMediaFile"
@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;
}

View file

@ -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))

View file

@ -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;
}

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -53,6 +53,12 @@
background: transparent;
z-index: 5;
}
img {
max-height: 26px;
vertical-align: top;
margin-top: -5px;
}
}
&:not(.active) {

View file

@ -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;

View file

@ -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 () {

View file

@ -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",

View file

@ -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"
}
}

View file

@ -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": "したにスクロールしたとき、じどうてきによみこむ。",

View file

@ -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": "下にスクロールしたとき、自動的に読み込む。",

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -5459,9 +5459,10 @@ pngjs@^3.3.0:
version "3.3.3"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b"
popper.js@^1.14.7:
version "1.14.7"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e"
popper.js@^1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2"
integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
portal-vue@^2.1.4:
version "2.1.4"
@ -7198,6 +7199,15 @@ v-click-outside@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/v-click-outside/-/v-click-outside-2.1.3.tgz#b7297abe833a439dc0895e6418a494381e64b5e7"
v-tooltip@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/v-tooltip/-/v-tooltip-2.0.2.tgz#8610d9eece2cc44fd66c12ef2f12eec6435cab9b"
integrity sha512-xQ+qzOFfywkLdjHknRPgMMupQNS8yJtf9Utd5Dxiu/0n4HtrxqsgDtN2MLZ0LKbburtSAQgyypuE/snM8bBZhw==
dependencies:
lodash "^4.17.11"
popper.js "^1.15.0"
vue-resize "^0.4.5"
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@ -7272,11 +7282,10 @@ vue-loader@^14.0.0:
vue-style-loader "^4.0.1"
vue-template-es2015-compiler "^1.6.0"
vue-popperjs@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/vue-popperjs/-/vue-popperjs-2.0.3.tgz#7c446d0ba7c63170ccb33a02669d0df4efc3d8cd"
dependencies:
popper.js "^1.14.7"
vue-resize@^0.4.5:
version "0.4.5"
resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-0.4.5.tgz#4777a23042e3c05620d9cbda01c0b3cc5e32dcea"
integrity sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg==
vue-router@^3.0.1:
version "3.0.2"