Merge branch 'develop' into 'master'

Update master

See merge request pleroma/pleroma-fe!646
This commit is contained in:
Shpuld Shpludson 2019-03-07 15:11:11 +00:00
commit 7e9c8c3d21
98 changed files with 3089 additions and 1863 deletions

View File

@ -11,7 +11,7 @@ module.exports = {
'html'
],
// add your custom rules here
'rules': {
rules: {
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await

View File

@ -17,6 +17,7 @@
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-lodash": "^3.2.11",
"chromatism": "^3.0.0",
"cropperjs": "^1.4.3",
"diff": "^3.0.1",
"karma-mocha-reporter": "^2.2.1",
"localforage": "^1.5.0",
@ -27,6 +28,7 @@
"sass-loader": "^4.0.2",
"vue": "^2.5.13",
"vue-chat-scroll": "^1.2.1",
"vue-compose": "^0.7.1",
"vue-i18n": "^7.3.2",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.3.4",

View File

@ -181,8 +181,7 @@ input, textarea, .select {
color: $fallback--text;
color: var(--text, $fallback--text);
}
&:disabled,
{
&:disabled {
&,
& + label,
& + label::before {
@ -629,6 +628,16 @@ nav {
color: $fallback--faint;
color: var(--faint, $fallback--faint);
}
.faint-link {
color: $fallback--faint;
color: var(--faint, $fallback--faint);
&:hover {
text-decoration: underline;
}
}
@media all and (min-width: 800px) {
.logo {
opacity: 1 !important;
@ -649,10 +658,6 @@ nav {
color: var(--lightText, $fallback--lightText);
}
.text-format {
float: right;
}
div {
padding-top: 5px;
}
@ -666,6 +671,10 @@ nav {
border-radius: var(--inputRadius, $fallback--inputRadius);
}
.button-icon {
font-size: 1.2em;
}
@keyframes shakeError {
0% {
transform: translateX(0);
@ -710,16 +719,6 @@ nav {
margin: 0.5em 0 0.5em 0;
}
.button-icon {
font-size: 1.2em;
}
.status .status-actions {
div {
max-width: 4em;
}
}
.menu-button {
display: block;
margin-right: 0.8em;
@ -728,7 +727,7 @@ nav {
.login-hint {
text-align: center;
@media all and (min-width: 801px) {
display: none;
}

View File

@ -88,7 +88,7 @@
.attachment {
position: relative;
margin: 0.5em 0.5em 0em 0em;
margin-top: 0.5em;
align-self: flex-start;
line-height: 0;

View File

@ -0,0 +1,28 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const BasicUserCard = {
props: [
'user'
],
data () {
return {
userExpanded: false
}
},
components: {
UserCardContent,
UserAvatar
},
methods: {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
}
}
}
export default BasicUserCard

View File

@ -0,0 +1,79 @@
<template>
<div class="user-card">
<router-link :to="userProfileLink(user)">
<UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
</router-link>
<div class="user-card-expanded-content" v-if="userExpanded">
<user-card-content :user="user" :switcher="false"></user-card-content>
</div>
<div class="user-card-collapsed-content" v-else>
<div :title="user.name" class="user-card-user-name">
<span v-if="user.name_html" v-html="user.name_html"></span>
<span v-else>{{ user.name }}</span>
</div>
<div>
<router-link class="user-card-screen-name" :to="userProfileLink(user)">
@{{user.screen_name}}
</router-link>
</div>
<slot></slot>
</div>
</div>
</template>
<script src="./basic_user_card.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.user-card {
display: flex;
flex: 1 0;
padding-top: 0.6em;
padding-right: 1em;
padding-bottom: 0.6em;
padding-left: 1em;
border-bottom: 1px solid;
margin: 0;
border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border);
&-collapsed-content {
margin-left: 0.7em;
text-align: left;
flex: 1;
min-width: 0;
}
&-user-name {
img {
object-fit: contain;
height: 16px;
width: 16px;
vertical-align: middle;
}
}
&-expanded-content {
flex: 1;
margin-left: 0.7em;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-width: 1px;
overflow: hidden;
.panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
p {
margin-bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,37 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const BlockCard = {
props: ['userId'],
data () {
return {
progress: false
}
},
computed: {
user () {
return this.$store.getters.userById(this.userId)
},
blocked () {
return this.user.statusnet_blocking
}
},
components: {
BasicUserCard
},
methods: {
unblockUser () {
this.progress = true
this.$store.dispatch('unblockUser', this.user.id).then(() => {
this.progress = false
})
},
blockUser () {
this.progress = true
this.$store.dispatch('blockUser', this.user.id).then(() => {
this.progress = false
})
}
}
}
export default BlockCard

View File

@ -0,0 +1,34 @@
<template>
<basic-user-card :user="user">
<div class="block-card-content-container">
<button class="btn btn-default" @click="unblockUser" :disabled="progress" v-if="blocked">
<template v-if="progress">
{{ $t('user_card.unblock_progress') }}
</template>
<template v-else>
{{ $t('user_card.unblock') }}
</template>
</button>
<button class="btn btn-default" @click="blockUser" :disabled="progress" v-else>
<template v-if="progress">
{{ $t('user_card.block_progress') }}
</template>
<template v-else>
{{ $t('user_card.block') }}
</template>
</button>
</div>
</basic-user-card>
</template>
<script src="./block_card.js"></script>
<style lang="scss">
.block-card-content-container {
margin-top: 0.5em;
text-align: right;
button {
width: 10em;
}
}
</style>

View File

@ -3,8 +3,8 @@
<div class="panel panel-default">
<div class="panel-heading timeline-heading" :class="{ 'chat-heading': floating }" @click.stop.prevent="togglePanel">
<div class="title">
{{$t('chat.title')}}
<i class="icon-cancel" style="float: right;" v-if="floating"></i>
<span>{{$t('chat.title')}}</span>
<i class="icon-cancel" v-if="floating"></i>
</div>
</div>
<div class="chat-window" v-chat-scroll>
@ -98,4 +98,11 @@
resize: none;
}
}
.chat-panel {
.title {
display: flex;
justify-content: space-between;
}
}
</style>

View File

@ -0,0 +1,45 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
const FollowCard = {
props: [
'user',
'noFollowsYou'
],
data () {
return {
inProgress: false,
requestSent: false,
updated: false
}
},
components: {
BasicUserCard
},
computed: {
isMe () { return this.$store.state.users.currentUser.id === this.user.id },
following () { return this.updated ? this.updated.following : this.user.following },
showFollow () {
return !this.following || this.updated && !this.updated.following
}
},
methods: {
followUser () {
this.inProgress = true
requestFollow(this.user, this.$store).then(({ sent, updated }) => {
this.inProgress = false
this.requestSent = sent
this.updated = updated
})
},
unfollowUser () {
this.inProgress = true
requestUnfollow(this.user, this.$store).then(({ updated }) => {
this.inProgress = false
this.updated = updated
})
}
}
}
export default FollowCard

View File

@ -0,0 +1,53 @@
<template>
<basic-user-card :user="user">
<div class="follow-card-content-container">
<span class="faint" v-if="!noFollowsYou && user.follows_you">
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span>
<button
v-if="showFollow"
class="btn btn-default"
@click="followUser"
:disabled="inProgress"
:title="requestSent ? $t('user_card.follow_again') : ''"
>
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else-if="requestSent">
{{ $t('user_card.follow_sent') }}
</template>
<template v-else>
{{ $t('user_card.follow') }}
</template>
</button>
<button v-if="following" class="btn btn-default pressed" @click="unfollowUser" :disabled="inProgress">
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else>
{{ $t('user_card.follow_unfollow') }}
</template>
</button>
</div>
</basic-user-card>
</template>
<script src="./follow_card.js"></script>
<style lang="scss">
.follow-card-content-container {
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
line-height: 1.5em;
.btn {
margin-top: 0.5em;
margin-left: auto;
width: 10em;
}
}
</style>

View File

@ -1,65 +0,0 @@
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
},
showFollowsYou () {
return !this.showFollowers || (this.showFollowers && this.userId !== this.$store.state.users.currentUser.id)
}
},
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

View File

@ -1,33 +0,0 @@
<template>
<div class="follow-list">
<user-card
v-for="entry in entries"
:key="entry.id" :user="entry"
:noFollowsYou="!showFollowsYou"
/>
<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>

View File

@ -0,0 +1,20 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const FollowRequestCard = {
props: ['user'],
components: {
BasicUserCard
},
methods: {
approveUser () {
this.$store.state.api.backendInteractor.approveUser(this.user.id)
this.$store.dispatch('removeFollowRequest', this.user)
},
denyUser () {
this.$store.state.api.backendInteractor.denyUser(this.user.id)
this.$store.dispatch('removeFollowRequest', this.user)
}
}
}
export default FollowRequestCard

View File

@ -0,0 +1,29 @@
<template>
<basic-user-card :user="user">
<div class="follow-request-card-content-container">
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
<button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button>
</div>
</basic-user-card>
</template>
<script src="./follow_request_card.js"></script>
<style lang="scss">
.follow-request-card-content-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
button {
margin-top: 0.5em;
margin-right: 0.5em;
flex: 1 1;
max-width: 12em;
min-width: 8em;
&:last-child {
margin-right: 0;
}
}
}
</style>

View File

@ -1,22 +1,13 @@
import UserCard from '../user_card/user_card.vue'
import FollowRequestCard from '../follow_request_card/follow_request_card.vue'
const FollowRequests = {
components: {
UserCard
},
created () {
this.updateRequests()
FollowRequestCard
},
computed: {
requests () {
return this.$store.state.api.followRequests
}
},
methods: {
updateRequests () {
this.$store.state.api.backendInteractor.fetchFollowRequests()
.then((requests) => { this.$store.commit('setFollowRequests', requests) })
}
}
}

View File

@ -4,7 +4,7 @@
{{$t('nav.friend_requests')}}
</div>
<div class="panel-body">
<user-card v-for="request in requests" :key="request.id" :user="request" :showFollows="false" :showApproval="true"></user-card>
<FollowRequestCard v-for="request in requests" :key="request.id" :user="request"/>
</div>
</div>
</template>

View File

@ -27,7 +27,6 @@
align-content: stretch;
flex-grow: 1;
margin-top: 0.5em;
margin-bottom: 0.25em;
.attachments, .attachment {
margin: 0 0.5em 0 0;
@ -36,6 +35,9 @@
box-sizing: border-box;
// to make failed images a bit more noticeable on chromium
min-width: 2em;
&:last-child {
margin: 0;
}
}
.image-attachment {

View File

@ -0,0 +1,128 @@
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
const ImageCropper = {
props: {
trigger: {
type: [String, window.Element],
required: true
},
submitHandler: {
type: Function,
required: true
},
cropperOptions: {
type: Object,
default () {
return {
aspectRatio: 1,
autoCropArea: 1,
viewMode: 1,
movable: false,
zoomable: false,
guides: false
}
}
},
mimes: {
type: String,
default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon'
},
saveButtonLabel: {
type: String
},
cancelButtonLabel: {
type: String
}
},
data () {
return {
cropper: undefined,
dataUrl: undefined,
filename: undefined,
submitting: false,
submitError: null
}
},
computed: {
saveText () {
return this.saveButtonLabel || this.$t('image_cropper.save')
},
cancelText () {
return this.cancelButtonLabel || this.$t('image_cropper.cancel')
},
submitErrorMsg () {
return this.submitError && this.submitError instanceof Error ? this.submitError.toString() : this.submitError
}
},
methods: {
destroy () {
if (this.cropper) {
this.cropper.destroy()
}
this.$refs.input.value = ''
this.dataUrl = undefined
this.$emit('close')
},
submit () {
this.submitting = true
this.avatarUploadError = null
this.submitHandler(this.cropper, this.file)
.then(() => this.destroy())
.catch((err) => {
this.submitError = err
})
.finally(() => {
this.submitting = false
})
},
pickImage () {
this.$refs.input.click()
},
createCropper () {
this.cropper = new Cropper(this.$refs.img, this.cropperOptions)
},
getTriggerDOM () {
return typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger)
},
readFile () {
const fileInput = this.$refs.input
if (fileInput.files != null && fileInput.files[0] != null) {
this.file = fileInput.files[0]
let reader = new window.FileReader()
reader.onload = (e) => {
this.dataUrl = e.target.result
this.$emit('open')
}
reader.readAsDataURL(this.file)
this.$emit('changed', this.file, reader)
}
},
clearError () {
this.submitError = null
}
},
mounted () {
// listen for click event on trigger
const trigger = this.getTriggerDOM()
if (!trigger) {
this.$emit('error', 'No image make trigger found.', 'user')
} else {
trigger.addEventListener('click', this.pickImage)
}
// listen for input file changes
const fileInput = this.$refs.input
fileInput.addEventListener('change', this.readFile)
},
beforeDestroy: function () {
// remove the event listeners
const trigger = this.getTriggerDOM()
if (trigger) {
trigger.removeEventListener('click', this.pickImage)
}
const fileInput = this.$refs.input
fileInput.removeEventListener('change', this.readFile)
}
}
export default ImageCropper

View File

@ -0,0 +1,42 @@
<template>
<div class="image-cropper">
<div v-if="dataUrl">
<div class="image-cropper-image-container">
<img ref="img" :src="dataUrl" alt="" @load.stop="createCropper" />
</div>
<div class="image-cropper-buttons-wrapper">
<button class="btn" type="button" :disabled="submitting" @click="submit" v-text="saveText"></button>
<button class="btn" type="button" :disabled="submitting" @click="destroy" v-text="cancelText"></button>
<i class="icon-spin4 animate-spin" v-if="submitting"></i>
</div>
<div class="alert error" v-if="submitError">
{{submitErrorMsg}}
<i class="button-icon icon-cancel" @click="clearError"></i>
</div>
</div>
<input ref="input" type="file" class="image-cropper-img-input" :accept="mimes">
</div>
</template>
<script src="./image_cropper.js"></script>
<style lang="scss">
.image-cropper {
&-img-input {
display: none;
}
&-image-container {
position: relative;
img {
display: block;
max-width: 100%;
}
}
&-buttons-wrapper {
margin-top: 15px;
}
}
</style>

View File

@ -23,10 +23,7 @@
flex-direction: row;
cursor: pointer;
overflow: hidden;
// TODO: clean up the random margins in attachments, this makes preview line
// up with attachments...
margin-right: 0.5em;
margin-top: 0.5em;
.card-image {
flex-shrink: 0;

View File

@ -11,27 +11,62 @@ const MediaModal = {
showing () {
return this.$store.state.mediaViewer.activated
},
media () {
return this.$store.state.mediaViewer.media
},
currentIndex () {
return this.$store.state.mediaViewer.currentIndex
},
currentMedia () {
return this.$store.state.mediaViewer.media[this.currentIndex]
return this.media[this.currentIndex]
},
canNavigate () {
return this.media.length > 1
},
type () {
return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null
}
},
created () {
document.addEventListener('keyup', e => {
if (e.keyCode === 27 && this.showing) { // escape
this.hide()
}
})
},
methods: {
hide () {
this.$store.dispatch('closeMediaViewer')
},
goPrev () {
if (this.canNavigate) {
const prevIndex = this.currentIndex === 0 ? this.media.length - 1 : (this.currentIndex - 1)
this.$store.dispatch('setCurrent', this.media[prevIndex])
}
},
goNext () {
if (this.canNavigate) {
const nextIndex = this.currentIndex === this.media.length - 1 ? 0 : (this.currentIndex + 1)
this.$store.dispatch('setCurrent', this.media[nextIndex])
}
},
handleKeyupEvent (e) {
if (this.showing && e.keyCode === 27) { // escape
this.hide()
}
},
handleKeydownEvent (e) {
if (!this.showing) {
return
}
if (e.keyCode === 39) { // arrow right
this.goNext()
} else if (e.keyCode === 37) { // arrow left
this.goPrev()
}
}
},
mounted () {
document.addEventListener('keyup', this.handleKeyupEvent)
document.addEventListener('keydown', this.handleKeydownEvent)
},
destroyed () {
document.removeEventListener('keyup', this.handleKeyupEvent)
document.removeEventListener('keydown', this.handleKeydownEvent)
}
}

View File

@ -8,6 +8,22 @@
:controls="true"
@click.stop.native="">
</VideoAttachment>
<button
:title="$t('media_modal.previous')"
class="modal-view-button-arrow modal-view-button-arrow--prev"
v-if="canNavigate"
@click.stop.prevent="goPrev"
>
<i class="icon-left-open arrow-icon" />
</button>
<button
:title="$t('media_modal.next')"
class="modal-view-button-arrow modal-view-button-arrow--next"
v-if="canNavigate"
@click.stop.prevent="goNext"
>
<i class="icon-right-open arrow-icon" />
</button>
</div>
</template>
@ -19,15 +35,29 @@
.modal-view {
z-index: 1000;
position: fixed;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.5);
cursor: pointer;
&:hover {
.modal-view-button-arrow {
opacity: 0.75;
&:focus,
&:hover {
outline: none;
box-shadow: none;
}
&:hover {
opacity: 1;
}
}
}
}
.modal-image {
@ -35,4 +65,49 @@
max-height: 90%;
box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
}
.modal-view-button-arrow {
position: absolute;
display: block;
top: 50%;
margin-top: -50px;
width: 70px;
height: 100px;
border: 0;
padding: 0;
opacity: 0;
box-shadow: none;
background: none;
appearance: none;
overflow: visible;
cursor: pointer;
transition: opacity 333ms cubic-bezier(.4,0,.22,1);
.arrow-icon {
position: absolute;
top: 35px;
height: 30px;
width: 32px;
font-size: 14px;
line-height: 30px;
color: #FFF;
text-align: center;
background-color: rgba(0,0,0,.3);
}
&--prev {
left: 0;
.arrow-icon {
left: 6px;
}
}
&--next {
right: 0;
.arrow-icon {
right: 6px;
}
}
}
</style>

View File

@ -0,0 +1,37 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const MuteCard = {
props: ['userId'],
data () {
return {
progress: false
}
},
computed: {
user () {
return this.$store.getters.userById(this.userId)
},
muted () {
return this.user.muted
}
},
components: {
BasicUserCard
},
methods: {
unmuteUser () {
this.progress = true
this.$store.dispatch('unmuteUser', this.user.id).then(() => {
this.progress = false
})
},
muteUser () {
this.progress = true
this.$store.dispatch('muteUser', this.user.id).then(() => {
this.progress = false
})
}
}
}
export default MuteCard

View File

@ -0,0 +1,24 @@
<template>
<basic-user-card :user="user">
<template slot="secondary-area">
<button class="btn btn-default" @click="unmuteUser" :disabled="progress" v-if="muted">
<template v-if="progress">
{{ $t('user_card.unmute_progress') }}
</template>
<template v-else>
{{ $t('user_card.unmute') }}
</template>
</button>
<button class="btn btn-default" @click="muteUser" :disabled="progress" v-else>
<template v-if="progress">
{{ $t('user_card.mute_progress') }}
</template>
<template v-else>
{{ $t('user_card.mute') }}
</template>
</button>
</template>
</basic-user-card>
</template>
<script src="./mute_card.js"></script>

View File

@ -1,10 +1,23 @@
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
const NavPanel = {
created () {
if (this.currentUser && this.currentUser.locked) {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
followRequestFetcher.startFetching({ store, credentials })
}
},
computed: {
currentUser () {
return this.$store.state.users.currentUser
},
chat () {
return this.$store.state.chat.channel
},
followRequestCount () {
return this.$store.state.api.followRequests.length
}
}
}

View File

@ -19,7 +19,10 @@
</li>
<li v-if='currentUser && currentUser.locked'>
<router-link :to="{ name: 'friend-requests' }">
{{ $t("nav.friend_requests") }}
{{ $t("nav.friend_requests")}}
<span v-if='followRequestCount > 0' class="badge follow-request-count">
{{followRequestCount}}
</span>
</router-link>
</li>
<li>
@ -52,6 +55,12 @@
padding: 0;
}
.follow-request-count {
margin: -6px 10px;
background-color: $fallback--bg;
background-color: var(--input, $fallback--faint);
}
.nav-panel li {
border-bottom: 1px solid;
border-color: $fallback--border;

View File

@ -25,7 +25,11 @@
<small>{{$t('notifications.followed_you')}}</small>
</span>
</div>
<small class="timeago"><router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
<div class="timeago">
<router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
<timeago :since="notification.action.created_at" :auto-update="240"></timeago>
</router-link>
</div>
</span>
<div class="follow-text" v-if="notification.type === 'follow'">
<router-link :to="userProfileLink(notification.action.user)">

View File

@ -103,6 +103,7 @@
flex: 1 1 0;
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
.name-and-action {
flex: 1;
@ -123,9 +124,9 @@
object-fit: contain
}
}
.timeago {
float: right;
font-size: 12px;
margin-right: .2em;
}
.icon-retweet.lit {

View File

@ -30,7 +30,9 @@
@drop="fileDrop"
@dragover.prevent="fileDrag"
@input="resize"
@paste="paste">
@paste="paste"
:disabled="posting"
>
</textarea>
<div class="visibility-tray">
<span class="text-format" v-if="formattingOptionsEnabled">
@ -118,6 +120,14 @@
}
}
.post-status-form {
.visibility-tray {
display: flex;
justify-content: space-between;
flex-direction: row-reverse;
}
}
.post-status-form, .login {
.form-bottom {
display: flex;

View File

@ -9,7 +9,7 @@
<div class='text-fields'>
<div class='form-group' :class="{ 'form-group--error': $v.user.username.$error }">
<label class='form--label' for='sign-up-username'>{{$t('login.username')}}</label>
<input :disabled="isPending" v-model.trim='$v.user.username.$model' class='form-control' id='sign-up-username' placeholder='e.g. lain'>
<input :disabled="isPending" v-model.trim='$v.user.username.$model' class='form-control' id='sign-up-username' :placeholder="$t('registration.username_placeholder')">
</div>
<div class="form-error" v-if="$v.user.username.$dirty">
<ul>
@ -21,7 +21,7 @@
<div class='form-group' :class="{ 'form-group--error': $v.user.fullname.$error }">
<label class='form--label' for='sign-up-fullname'>{{$t('registration.fullname')}}</label>
<input :disabled="isPending" v-model.trim='$v.user.fullname.$model' class='form-control' id='sign-up-fullname' placeholder='e.g. Lain Iwakura'>
<input :disabled="isPending" v-model.trim='$v.user.fullname.$model' class='form-control' id='sign-up-fullname' :placeholder="$t('registration.fullname_placeholder')">
</div>
<div class="form-error" v-if="$v.user.fullname.$dirty">
<ul>
@ -44,8 +44,8 @@
</div>
<div class='form-group'>
<label class='form--label' for='bio'>{{$t('registration.bio')}}</label>
<input :disabled="isPending" v-model='user.bio' class='form-control' id='bio'>
<label class='form--label' for='bio'>{{$t('registration.bio')}} ({{$t('general.optional')}})</label>
<textarea :disabled="isPending" v-model='user.bio' class='form-control' id='bio' :placeholder="$t('registration.bio_placeholder')"></textarea>
</div>
<div class='form-group' :class="{ 'form-group--error': $v.user.password.$error }">
@ -139,6 +139,10 @@ $validations-cRed: #f04124;
flex-direction: column;
}
textarea {
min-height: 100px;
}
.form-group {
display: flex;
flex-direction: column;

View File

@ -12,6 +12,7 @@ const settings = {
return {
hideAttachmentsLocal: user.hideAttachments,
hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
maxThumbnails: user.maxThumbnails,
hideNsfwLocal: user.hideNsfw,
useOneClickNsfw: user.useOneClickNsfw,
hideISPLocal: user.hideISP,
@ -186,6 +187,10 @@ const settings = {
},
useContainFit (value) {
this.$store.dispatch('setOption', { name: 'useContainFit', value })
},
maxThumbnails (value) {
value = this.maxThumbnails = Math.floor(Math.max(value, 0))
this.$store.dispatch('setOption', { name: 'maxThumbnails', value })
}
}
}

View File

@ -136,6 +136,10 @@
<input type="checkbox" id="hideAttachmentsInConv" v-model="hideAttachmentsInConvLocal">
<label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label>
</li>
<li>
<label for="maxThumbnails">{{$t('settings.max_thumbnails')}}</label>
<input class="number-input" type="number" id="maxThumbnails" v-model.number="maxThumbnails" min="0" step="1">
</li>
<li>
<input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
<label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
@ -146,7 +150,7 @@
<label for="preloadImage">{{$t('settings.preload_images')}}</label>
</li>
<li>
<input type="checkbox" id="useOneClickNsfw" v-model="useOneClickNsfw">
<input :disabled="!hideNsfwLocal" type="checkbox" id="useOneClickNsfw" v-model="useOneClickNsfw">
<label for="useOneClickNsfw">{{$t('settings.use_one_click_nsfw')}}</label>
</li>
</ul>
@ -311,25 +315,15 @@
color: $fallback--cRed;
}
.old-avatar {
width: 128px;
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
}
.new-avatar {
object-fit: cover;
width: 128px;
height: 128px;
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
}
.btn {
min-height: 28px;
min-width: 10em;
padding: 0 2em;
}
.number-input {
max-width: 6em;
}
}
.select-multiple {
display: flex;

View File

@ -32,6 +32,9 @@ const SideDrawer = {
},
sitename () {
return this.$store.state.instance.name
},
followRequestCount () {
return this.$store.state.api.followRequests.length
}
},
methods: {

View File

@ -45,6 +45,10 @@
<li v-if="currentUser && currentUser.locked" @click="toggleDrawer">
<router-link to='/friend-requests'>
{{ $t("nav.friend_requests") }}
<span v-if='followRequestCount > 0' class="badge follow-request-count">
{{followRequestCount}}
</span>
</router-link>
</li>
<li @click="toggleDrawer">

View File

@ -23,7 +23,7 @@ const Status = {
'highlight',
'compact',
'replies',
'noReplyLinks',
'isPreview',
'noHeading',
'inlineExpanded'
],
@ -40,8 +40,7 @@ const Status = {
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
? !this.$store.state.instance.collapseMessageWithSubject
: !this.$store.state.config.collapseMessageWithSubject,
betterShadow: this.$store.state.interface.browserSupport.cssFilter,
maxAttachments: 9
betterShadow: this.$store.state.interface.browserSupport.cssFilter
}
},
computed: {
@ -225,7 +224,7 @@ const Status = {
attachmentSize () {
if ((this.$store.state.config.hideAttachments && !this.inConversation) ||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation) ||
(this.status.attachments.length > this.maxAttachments)) {
(this.status.attachments.length > this.maxThumbnails)) {
return 'hide'
} else if (this.compact) {
return 'small'
@ -249,6 +248,9 @@ const Status = {
return this.status.attachments.filter(
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
)
},
maxThumbnails () {
return this.$store.state.config.maxThumbnails
}
},
components: {

View File

@ -1,6 +1,6 @@
<template>
<div class="status-el" v-if="!hideStatus" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
<template v-if="muted && !noReplyLinks">
<template v-if="muted && !isPreview">
<div class="media status container muted">
<small>
<router-link :to="userProfileLink">
@ -13,7 +13,7 @@
</template>
<template v-else>
<div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
<UserAvatar v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/>
<UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/>
<div class="media-body faint">
<span class="user-name">
<router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/>
@ -31,57 +31,69 @@
</router-link>
</div>
<div class="status-body">
<div class="usercard media-body" v-if="userExpanded">
<div class="usercard" v-if="userExpanded">
<user-card-content :user="status.user" :switcher="false"></user-card-content>
</div>
<div v-if="!noHeading" class="media-body container media-heading">
<div class="media-heading-left">
<div class="name-and-links">
<div v-if="!noHeading" class="media-heading">
<div class="heading-name-row">
<div class="name-and-account-name">
<h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4>
<h4 class="user-name" v-else>{{status.user.name}}</h4>
<span class="links">
<router-link :to="userProfileLink">
{{status.user.screen_name}}
</router-link>
<span v-if="isReply" class="faint reply-info">
<i class="icon-right-open"></i>
<router-link :to="replyProfileLink">
{{replyToName}}
</router-link>
</span>
<a v-if="isReply && !noReplyLinks" href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)" :aria-label="$t('tool_tip.reply')">
<i class="button-icon icon-reply" @mouseenter="replyEnter(status.in_reply_to_status_id, $event)" @mouseout="replyLeave()"></i>
<router-link class="account-name" :to="userProfileLink">
{{status.user.screen_name}}
</router-link>
</div>
<span class="heading-right">
<router-link class="timeago faint-link" :to="{ name: 'conversation', params: { id: status.id } }">
<timeago :since="status.created_at" :auto-update="60"></timeago>
</router-link>
<div class="button-icon visibility-icon" v-if="status.visibility">
<i :class="visibilityIcon(status.visibility)" :title="status.visibility | capitalize"></i>
</div>
<a :href="status.external_url" target="_blank" v-if="!status.is_local && !isPreview" class="source_url" title="Source">
<i class="button-icon icon-link-ext-alt"></i>
</a>
<template v-if="expandable && !isPreview">
<a href="#" @click.prevent="toggleExpanded" title="Expand">
<i class="button-icon icon-plus-squared"></i>
</a>
</template>
<a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="button-icon icon-eye-off"></i></a>
</span>
</div>
<div class="heading-reply-row">
<div v-if="isReply" class="reply-to-and-accountname">
<a class="reply-to"
href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
:aria-label="$t('tool_tip.reply')"
@mouseenter.prevent.stop="replyEnter(status.in_reply_to_status_id, $event)"
@mouseleave.prevent.stop="replyLeave()"
>
<i class="button-icon icon-reply" v-if="!isPreview"></i>
<span class="faint-link reply-to-text">{{$t('status.reply_to')}}</span>
</a>
<router-link :to="replyProfileLink">
{{replyToName}}
</router-link>
<span class="faint replies-separator" v-if="replies && replies.length">
-
</span>
</div>
<h4 class="replies" v-if="inConversation && !noReplyLinks">
<small v-if="replies.length">Replies:</small>
<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>
</div>
<div class="media-heading-right">
<router-link class="timeago" :to="{ name: 'conversation', params: { id: status.id } }">
<timeago :since="status.created_at" :auto-update="60"></timeago>
</router-link>
<div class="button-icon visibility-icon" v-if="status.visibility">
<i :class="visibilityIcon(status.visibility)" :title="status.visibility | capitalize"></i>
<div class="replies" v-if="inConversation && !isPreview">
<span class="faint" v-if="replies && replies.length">{{$t('status.replies_list')}}</span>
<span class="reply-link faint" v-if="replies" v-for="reply in replies">
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}}</a>
</span>
</div>
<a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url" title="Source">
<i class="button-icon icon-link-ext-alt"></i>
</a>
<template v-if="expandable">
<a href="#" @click.prevent="toggleExpanded" title="Expand">
<i class="button-icon icon-plus-squared"></i>
</a>
</template>
<a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="button-icon icon-eye-off"></i></a>
</div>
</div>
<div v-if="showPreview" class="status-preview-container">
<status class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status>
<status class="status-preview" v-if="preview" :isPreview="true" :statusoid="preview" :compact=true></status>
<div class="status-preview status-preview-loading" v-else>
<i class="icon-spin4 animate-spin"></i>
</div>
@ -123,7 +135,7 @@
<link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" />
</div>
<div v-if="!noHeading && !noReplyLinks" class='status-actions media-body'>
<div v-if="!noHeading && !isPreview" class='status-actions media-body'>
<div v-if="loggedIn">
<a href="#" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')">
<i class="button-icon icon-reply" :class="{'icon-reply-active': replying}"></i>
@ -147,6 +159,8 @@
<style lang="scss">
@import '../../_variables.scss';
$status-margin: 0.75em;
.status-body {
flex: 1;
min-width: 0;
@ -202,13 +216,16 @@
}
}
.media-left {
margin-right: $status-margin;
}
.status-el {
hyphens: auto;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
border-left-width: 0px;
line-height: 18px;
min-width: 0;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
@ -229,22 +246,34 @@
.media-body {
flex: 1;
padding: 0;
margin: 0 0 0.25em 0.8em;
}
.usercard {
margin-bottom: .7em
margin: 0;
margin-bottom: $status-margin;
}
.user-name {
white-space: nowrap;
font-size: 14px;
overflow: hidden;
flex-shrink: 0;
max-width: 85%;
font-weight: bold;
img {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
}
.media-heading {
flex-wrap: nowrap;
line-height: 18px;
}
.media-heading-left {
padding: 0;
vertical-align: bottom;
flex-basis: 100%;
margin-bottom: 0.5em;
a {
display: inline-block;
@ -254,83 +283,102 @@
small {
font-weight: lighter;
}
h4 {
white-space: nowrap;
font-size: 14px;
margin-right: 0.25em;
overflow: hidden;
text-overflow: ellipsis;
}
.name-and-links {
.heading-name-row {
padding: 0;
flex: 1 0;
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
line-height: 18px;
.name-and-account-name {
display: flex;
min-width: 0;
}
.user-name {
margin-right: .45em;
flex-shrink: 1;
margin-right: 0.4em;
overflow: hidden;
text-overflow: ellipsis;
}
img {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
.account-name {
min-width: 1.6em;
margin-right: 0.4em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1 1 0;
}
}
.links {
.heading-right {
display: flex;
flex-shrink: 0;
}
.timeago {
margin-right: 0.2em;
}
.heading-reply-row {
align-content: baseline;
font-size: 12px;
color: $fallback--link;
color: var(--link, $fallback--link);
line-height: 18px;
max-width: 100%;
display: flex;
flex-wrap: wrap;
align-items: stretch;
a {
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
& > span {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
& > a:last-child {
flex-shrink: 0;
}
.reply-to-and-accountname {
display: flex;
height: 18px;
margin-right: 0.5em;
overflow: hidden;
max-width: 100%;
.icon-reply {
transform: scaleX(-1);
}
}
.reply-info {
display: flex;
}
.reply-to {
display: flex;
}
.reply-to-text {
overflow: hidden;
text-overflow: ellipsis;
margin: 0 0.4em 0 0.2em;
}
.replies-separator {
margin-left: 0.4em;
}
.replies {
line-height: 16px;
}
.reply-link {
margin-right: 0.2em;
}
}
.media-heading-right {
display: inline-flex;
flex-shrink: 0;
flex-wrap: nowrap;
margin-left: .25em;
align-self: baseline;
.timeago {
margin-right: 0.2em;
line-height: 18px;
font-size: 12px;
align-self: last baseline;
display: flex;
flex-wrap: wrap;
& > * {
margin-right: 0.4em;
}
}
> * {
margin-left: 0.2em;
}
a:hover i {
color: $fallback--text;
color: var(--text, $fallback--text);
.reply-link {
height: 17px;
}
}
@ -366,14 +414,19 @@
}
.status-content {
margin-right: 0.5em;
font-family: var(--postFont, sans-serif);
line-height: 1.4em;
img, video {
max-width: 100%;
max-height: 400px;
vertical-align: middle;
object-fit: contain;
&.emoji {
width: 32px;
height: 32px;
}
}
blockquote {
@ -390,9 +443,11 @@
}
p {
margin: 0;
margin-top: 0.2em;
margin-bottom: 0.5em;
margin: 0 0 1em 0;
}
p:last-child {
margin: 0 0 0 0;
}
h1 {
@ -417,7 +472,7 @@
}
.retweet-info {
padding: 0.4em 0.6em 0 0.6em;
padding: 0.4em $status-margin;
margin: 0;
.avatar.still-image {
@ -488,10 +543,10 @@
.status-actions {
width: 100%;
display: flex;
margin-top: $status-margin;
div, favorite-button {
padding-top: 0.25em;
max-width: 6em;
max-width: 4em;
flex: 1;
}
}
@ -517,9 +572,9 @@
.status {
display: flex;
padding: 0.6em;
padding: $status-margin;
&.is-retweet {
padding-top: 0.1em;
padding-top: 0;
}
}

View File

@ -1,7 +1,6 @@
import Status from '../status/status.vue'
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue'
import UserCard from '../user_card/user_card.vue'
import { throttle } from 'lodash'
const Timeline = {
@ -11,7 +10,8 @@ const Timeline = {
'title',
'userId',
'tag',
'embedded'
'embedded',
'count'
],
data () {
return {
@ -43,8 +43,7 @@ const Timeline = {
},
components: {
Status,
StatusOrConversation,
UserCard
StatusOrConversation
},
created () {
const store = this.$store
@ -53,6 +52,8 @@ const Timeline = {
window.addEventListener('scroll', this.scrollLoad)
if (this.timelineName === 'friends' && !credentials) { return false }
timelineFetcher.fetchAndUpdate({
store,
credentials,
@ -67,14 +68,21 @@ const Timeline = {
document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
this.unfocused = document.hidden
}
window.addEventListener('keydown', this.handleShortKey)
},
destroyed () {
window.removeEventListener('scroll', this.scrollLoad)
window.removeEventListener('keydown', this.handleShortKey)
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
},
methods: {
handleShortKey (e) {
if (e.key === '.') this.showNewStatuses()
},
showNewStatuses () {
if (this.newStatusCount === 0) return
if (this.timeline.flushMarker !== 0) {
this.$store.commit('clearTimeline', { timeline: this.timelineName })
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
@ -98,7 +106,7 @@ const Timeline = {
tag: this.tag
}).then(statuses => {
store.commit('setLoading', { timeline: this.timelineName, value: false })
if (statuses.length === 0) {
if (statuses && statuses.length === 0) {
this.bottomedOut = true
}
})

View File

@ -20,7 +20,10 @@
</div>
</div>
<div :class="classes.footer">
<div v-if="bottomedOut" class="new-status-notification text-center panel-footer faint">
<div v-if="count===0" class="new-status-notification text-center panel-footer faint">
{{$t('timeline.no_statuses')}}
</div>
<div v-else-if="bottomedOut" class="new-status-notification text-center panel-footer faint">
{{$t('timeline.no_more_statuses')}}
</div>
<a v-else-if="!timeline.loading" href="#" v-on:click.prevent='fetchOlderStatuses()'>

View File

@ -1,64 +0,0 @@
import UserCardContent from '../user_card_content/user_card_content.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',
'noFollowsYou',
'showApproval'
],
data () {
return {
userExpanded: false,
followRequestInProgress: false,
followRequestSent: false,
updated: false
}
},
components: {
UserCardContent,
UserAvatar
},
computed: {
currentUser () { return this.$store.state.users.currentUser },
following () { return this.updated ? this.updated.following : this.user.following },
showFollow () {
return !this.showApproval && (!this.following || this.updated && !this.updated.following)
}
},
methods: {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
approveUser () {
this.$store.state.api.backendInteractor.approveUser(this.user.id)
this.$store.dispatch('removeFollowRequest', this.user)
},
denyUser () {
this.$store.state.api.backendInteractor.denyUser(this.user.id)
this.$store.dispatch('removeFollowRequest', this.user)
},
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
})
}
}
}
export default UserCard

View File

@ -1,159 +0,0 @@
<template>
<div class="card">
<router-link :to="userProfileLink(user)">
<UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
</router-link>
<div class="user-card-main-content">
<div class="usercard" v-if="userExpanded">
<user-card-content :user="user" :switcher="false"></user-card-content>
</div>
<div class="name-and-screen-name" v-if="!userExpanded">
<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>
</div>
<div class="user-link-action">
<router-link class='user-screen-name' :to="userProfileLink(user)">
@{{user.screen_name}}
</router-link>
</div>
</div>
<div class="follow-box" v-if="!userExpanded">
<span class="faint" v-if="!noFollowsYou && user.follows_you">
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span>
<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="following" class="btn btn-default pressed" @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>
<div class="approval" v-if="showApproval">
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
<button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button>
</div>
</div>
</div>
</template>
<script src="./user_card.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.user-card-main-content {
display: flex;
flex-direction: column;
flex: 1 1 100%;
margin-left: 0.7em;
min-width: 0;
}
.name-and-screen-name {
text-align: left;
width: 100%;
.user-name {
img {
object-fit: contain;
height: 16px;
width: 16px;
vertical-align: middle;
}
}
.user-link-action {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
}
.card {
display: flex;
flex: 1 0;
padding-top: 0.6em;
padding-right: 1em;
padding-bottom: 0.6em;
padding-left: 1em;
border-bottom: 1px solid;
margin: 0;
border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border);
.avatar {
padding: 0;
}
.follow-box {
text-align: center;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
line-height: 1.5em;
.btn {
margin-top: 0.5em;
margin-left: auto;
width: 10em;
}
}
}
.usercard {
width: fill-available;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-width: 1px;
overflow: hidden;
.panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
p {
margin-bottom: 0;
}
}
.approval {
display: flex;
flex-direction: row;
flex-wrap: wrap;
button {
margin-top: 0.5em;
margin-right: 0.5em;
flex: 1 1;
max-width: 12em;
min-width: 8em;
}
}
</style>

View File

@ -13,7 +13,7 @@
<router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser">
<i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i>
</router-link>
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser">
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local">
<i class="icon-link-ext usersettings"></i>
</a>
</div>
@ -222,6 +222,14 @@
overflow: hidden;
flex: 1 1 auto;
margin-right: 1em;
font-size: 15px;
img {
object-fit: contain;
height: 16px;
width: 16px;
vertical-align: middle;
}
}
.user-screen-name {
@ -386,4 +394,24 @@
}
}
.usercard {
width: fill-available;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-width: 1px;
overflow: hidden;
.panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
p {
margin-bottom: 0;
}
}
</style>

View File

@ -8,6 +8,7 @@ const UserFinder = {
methods: {
findUser (username) {
this.$router.push({ name: 'user-search', query: { query: username } })
this.$refs.userSearchInput.focus()
},
toggleHidden () {
this.hidden = !this.hidden

View File

@ -4,7 +4,7 @@
<i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
<a href="#" v-if="hidden" :title="$t('finder.find_user')"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
<template v-else>
<input class="user-finder-input" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/>
<input class="user-finder-input" ref="userSearchInput" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/>
<button class="btn search-button" @click="findUser(username)">
<i class="icon-search"/>
</button>

View File

@ -1,9 +1,39 @@
import { compose } from 'vue-compose'
import get from 'lodash/get'
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserCard from '../user_card/user_card.vue'
import FollowCard from '../follow_card/follow_card.vue'
import Timeline from '../timeline/timeline.vue'
import FollowList from '../follow_list/follow_list.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more'
import withList from '../../hocs/with_list/with_list'
const FollowerList = compose(
withLoadMore({
fetch: (props, $store) => $store.dispatch('addFollowers', props.userId),
select: (props, $store) => get($store.getters.userById(props.userId), 'followers', []),
destory: (props, $store) => $store.dispatch('clearFollowers', props.userId),
childPropName: 'entries',
additionalPropNames: ['userId']
}),
withList({ getEntryProps: user => ({ user }) })
)(FollowCard)
const FriendList = compose(
withLoadMore({
fetch: (props, $store) => $store.dispatch('addFriends', props.userId),
select: (props, $store) => get($store.getters.userById(props.userId), 'friends', []),
destory: (props, $store) => $store.dispatch('clearFriends', props.userId),
childPropName: 'entries',
additionalPropNames: ['userId']
}),
withList({ getEntryProps: user => ({ user }) })
)(FollowCard)
const UserProfile = {
data () {
return {
error: false
}
},
created () {
this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.commit('clearTimeline', { timeline: 'favorites' })
@ -13,6 +43,16 @@ const UserProfile = {
this.startFetchFavorites()
if (!this.user.id) {
this.$store.dispatch('fetchUser', this.fetchBy)
.catch((reason) => {
const errorMessage = get(reason, 'error.error')
if (errorMessage === 'No user with such user_id') { // Known error
this.error = this.$t('user_profile.profile_does_not_exist')
} else if (errorMessage) {
this.error = errorMessage
} else {
this.error = this.$t('user_profile.profile_loading_error')
}
})
}
},
destroyed () {
@ -101,13 +141,16 @@ const UserProfile = {
}
this.cleanUp()
this.startUp()
},
$route () {
this.$refs.tabSwitcher.activateTab(0)()
}
},
components: {
UserCardContent,
UserCard,
Timeline,
FollowList
FollowerList,
FriendList
}
}

View File

@ -6,10 +6,11 @@
:switcher="true"
:selected="timeline.viewing"
/>
<tab-switcher :renderOnlyFocused="true">
<tab-switcher :renderOnlyFocused="true" ref="tabSwitcher">
<Timeline
:label="$t('user_card.statuses')"
:disabled="!user.statuses_count"
:count="user.statuses_count"
:embedded="true"
:title="$t('user_profile.timeline_title')"
:timeline="timeline"
@ -17,16 +18,10 @@
:user-id="fetchBy"
/>
<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>
<FriendList :userId="userId" />
</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>
<FollowerList :userId="userId" :entryProps="{noFollowsYou: isUs}" />
</div>
<Timeline
:label="$t('user_card.media')"
@ -54,7 +49,8 @@
</div>
</div>
<div class="panel-body">
<i class="icon-spin3 animate-spin"></i>
<span v-if="error">{{ error }}</span>
<i class="icon-spin3 animate-spin" v-else></i>
</div>
</div>
</div>

View File

@ -1,8 +1,8 @@
import UserCard from '../user_card/user_card.vue'
import FollowCard from '../follow_card/follow_card.vue'
import userSearchApi from '../../services/new_api/user_search.js'
const userSearch = {
components: {
UserCard
FollowCard
},
props: [
'query'
@ -10,7 +10,8 @@ const userSearch = {
data () {
return {
username: '',
users: []
users: [],
loading: false
}
},
mounted () {
@ -24,14 +25,17 @@ const userSearch = {
methods: {
newQuery (query) {
this.$router.push({ name: 'user-search', query: { query } })
this.$refs.userSearchInput.focus()
},
search (query) {
if (!query) {
this.users = []
return
}
this.loading = true
userSearchApi.search({query, store: this.$store})
.then((res) => {
this.loading = false
this.users = res
})
}

View File

@ -4,13 +4,16 @@
{{$t('nav.user_search')}}
</div>
<div class="user-search-input-container">
<input class="user-finder-input" @keyup.enter="newQuery(username)" v-model="username" :placeholder="$t('finder.find_user')"/>
<input class="user-finder-input" ref="userSearchInput" @keyup.enter="newQuery(username)" v-model="username" :placeholder="$t('finder.find_user')"/>
<button class="btn search-button" @click="newQuery(username)">
<i class="icon-search"/>
</button>
</div>
<div class="panel-body">
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card>
<div v-if="loading" class="text-center loading-icon">
<i class="icon-spin3 animate-spin"/>
</div>
<div v-else class="panel-body">
<FollowCard v-for="user in users" :key="user.id" :user="user"/>
</div>
</div>
</template>
@ -27,4 +30,8 @@
margin-left: 0.5em;
}
}
.loading-icon {
padding: 1em;
}
</style>

View File

@ -1,8 +1,32 @@
import { unescape } from 'lodash'
import { compose } from 'vue-compose'
import unescape from 'lodash/unescape'
import get from 'lodash/get'
import TabSwitcher from '../tab_switcher/tab_switcher.js'
import ImageCropper from '../image_cropper/image_cropper.vue'
import StyleSwitcher from '../style_switcher/style_switcher.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
import BlockCard from '../block_card/block_card.vue'
import MuteCard from '../mute_card/mute_card.vue'
import withSubscription from '../../hocs/with_subscription/with_subscription'
import withList from '../../hocs/with_list/with_list'
const BlockList = compose(
withSubscription({
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
childPropName: 'entries'
}),
withList({ getEntryProps: userId => ({ userId }) })
)(BlockCard)
const MuteList = compose(
withSubscription({
fetch: (props, $store) => $store.dispatch('fetchMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
childPropName: 'entries'
}),
withList({ getEntryProps: userId => ({ userId }) })
)(MuteCard)
const UserSettings = {
data () {
@ -20,14 +44,12 @@ const UserSettings = {
followImportError: false,
followsImported: false,
enableFollowsExport: true,
avatarUploading: false,
pickAvatarBtnVisible: true,
bannerUploading: false,
backgroundUploading: false,
followListUploading: false,
avatarPreview: null,
bannerPreview: null,
backgroundPreview: null,
avatarUploadError: null,
bannerUploadError: null,
backgroundUploadError: null,
deletingAccount: false,
@ -39,9 +61,15 @@ const UserSettings = {
activeTab: 'profile'
}
},
created () {
this.$store.dispatch('fetchTokens')
},
components: {
StyleSwitcher,
TabSwitcher
TabSwitcher,
ImageCropper,
BlockList,
MuteList
},
computed: {
user () {
@ -60,6 +88,18 @@ const UserSettings = {
private: { selected: this.newDefaultScope === 'private' },
direct: { selected: this.newDefaultScope === 'direct' }
}
},
currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice
},
oauthTokens () {
return this.$store.state.oauthTokens.tokens.map(oauthToken => {
return {
id: oauthToken.id,
appName: oauthToken.app_name,
validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
}
})
}
},
methods: {
@ -117,35 +157,15 @@ const UserSettings = {
}
reader.readAsDataURL(file)
},
submitAvatar () {
if (!this.avatarPreview) { return }
let img = this.avatarPreview
// eslint-disable-next-line no-undef
let imginfo = new Image()
let cropX, cropY, cropW, cropH
imginfo.src = img
if (imginfo.height > imginfo.width) {
cropX = 0
cropW = imginfo.width
cropY = Math.floor((imginfo.height - imginfo.width) / 2)
cropH = imginfo.width
} else {
cropY = 0
cropH = imginfo.height
cropX = Math.floor((imginfo.width - imginfo.height) / 2)
cropW = imginfo.height
}
this.avatarUploading = true
this.$store.state.api.backendInteractor.updateAvatar({params: {img, cropX, cropY, cropW, cropH}}).then((user) => {
submitAvatar (cropper, file) {
const img = cropper.getCroppedCanvas().toDataURL(file.type)
return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => {
if (!user.error) {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
this.avatarPreview = null
} else {
this.avatarUploadError = this.$t('upload.error.base') + user.error
throw new Error(this.$t('upload.error.base') + user.error)
}
this.avatarUploading = false
})
},
clearUploadError (slot) {
@ -299,6 +319,11 @@ const UserSettings = {
logout () {
this.$store.dispatch('logout')
this.$router.replace('/')
},
revokeToken (id) {
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
this.$store.dispatch('revokeToken', id)
}
}
}
}

View File

@ -1,7 +1,20 @@
<template>
<div class="settings panel panel-default">
<div class="panel-heading">
{{$t('settings.user_settings')}}
<div class="title">
{{$t('settings.user_settings')}}
</div>
<transition name="fade">
<template v-if="currentSaveStateNotice">
<div @click.prevent class="alert error" v-if="currentSaveStateNotice.error">
{{ $t('settings.saving_err') }}
</div>
<div @click.prevent class="alert transparent" v-if="!currentSaveStateNotice.error">
{{ $t('settings.saving_ok') }}
</div>
</template>
</transition>
</div>
<div class="panel-body profile-edit">
<tab-switcher>
@ -48,19 +61,10 @@
<h2>{{$t('settings.avatar')}}</h2>
<p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
<p>{{$t('settings.current_avatar')}}</p>
<img :src="user.profile_image_url_original" class="old-avatar"></img>
<img :src="user.profile_image_url_original" class="current-avatar"></img>
<p>{{$t('settings.set_new_avatar')}}</p>
<img class="new-avatar" v-bind:src="avatarPreview" v-if="avatarPreview">
</img>
<div>
<input type="file" @change="uploadFile('avatar', $event)" ></input>
</div>
<i class="icon-spin4 animate-spin" v-if="avatarUploading"></i>
<button class="btn btn-default" v-else-if="avatarPreview" @click="submitAvatar">{{$t('general.submit')}}</button>
<div class='alert error' v-if="avatarUploadError">
Error: {{ avatarUploadError }}
<i class="button-icon icon-cancel" @click="clearUploadError('avatar')"></i>
</div>
<button class="btn" type="button" id="pick-avatar" v-show="pickAvatarBtnVisible">{{$t('settings.upload_a_photo')}}</button>
<image-cropper trigger="#pick-avatar" :submitHandler="submitAvatar" @open="pickAvatarBtnVisible=false" @close="pickAvatarBtnVisible=true" />
</div>
<div class="setting-item">
<h2>{{$t('settings.profile_banner')}}</h2>
@ -117,6 +121,30 @@
<p v-if="changePasswordError">{{changePasswordError}}</p>
</div>
<div class="setting-item">
<h2>{{$t('settings.oauth_tokens')}}</h2>
<table class="oauth-tokens">
<thead>
<tr>
<th>{{$t('settings.app_name')}}</th>
<th>{{$t('settings.valid_until')}}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="oauthToken in oauthTokens" :key="oauthToken.id">
<td>{{oauthToken.appName}}</td>
<td>{{oauthToken.validUntil}}</td>
<td class="actions">
<button class="btn btn-default" @click="revokeToken(oauthToken.id)">
{{$t('settings.revoke_token')}}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="setting-item">
<h2>{{$t('settings.delete_account')}}</h2>
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
@ -158,6 +186,12 @@
<h2>{{$t('settings.follow_export_processing')}}</h2>
</div>
</div>
<div :label="$t('settings.blocks_tab')">
<block-list :refresh="true">
<template slot="empty">{{$t('settings.no_blocks')}}</template>
</block-list>
</div>
</tab-switcher>
</div>
</div>
@ -167,6 +201,8 @@
</script>
<style lang="scss">
@import '../../_variables.scss';
.profile-edit {
.bio {
margin: 0;
@ -193,5 +229,25 @@
.bg {
max-width: 100%;
}
.current-avatar {
display: block;
width: 150px;
height: 150px;
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
}
.oauth-tokens {
width: 100%;
th {
text-align: left;
}
.actions {
text-align: right;
}
}
}
</style>

View File

@ -1,9 +1,9 @@
import apiService from '../../services/api/api.service.js'
import UserCard from '../user_card/user_card.vue'
import FollowCard from '../follow_card/follow_card.vue'
const WhoToFollow = {
components: {
UserCard
FollowCard
},
data () {
return {

View File

@ -4,7 +4,7 @@
{{$t('who_to_follow.who_to_follow')}}
</div>
<div class="panel-body">
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card>
<FollowCard v-for="user in users" :key="user.id" :user="user"/>
</div>
</div>
</template>

View File

@ -0,0 +1,40 @@
import Vue from 'vue'
import map from 'lodash/map'
import isEmpty from 'lodash/isEmpty'
import './with_list.scss'
const defaultEntryPropsGetter = entry => ({ entry })
const defaultKeyGetter = entry => entry.id
const withList = ({
getEntryProps = defaultEntryPropsGetter, // function to accept entry and index values and return props to be passed into the item component
getKey = defaultKeyGetter // funciton to accept entry and index values and return key prop value
}) => (ItemComponent) => (
Vue.component('withList', {
props: [
'entries', // array of entry
'entryProps', // additional props to be passed into each entry
'entryListeners' // additional event listeners to be passed into each entry
],
render (createElement) {
return (
<div class="with-list">
{map(this.entries, (entry, index) => {
const props = {
key: getKey(entry, index),
props: {
...this.$props.entryProps,
...getEntryProps(entry, index)
},
on: this.$props.entryListeners
}
return <ItemComponent {...props} />
})}
{isEmpty(this.entries) && this.$slots.empty && <div class="with-list-empty-content faint">{this.$slots.empty}</div>}
</div>
)
}
})
)
export default withList

View File

@ -0,0 +1,6 @@
.with-list {
&-empty-content {
text-align: center;
padding: 10px;
}
}

View File

@ -0,0 +1,94 @@
import Vue from 'vue'
import isEmpty from 'lodash/isEmpty'
import { getComponentProps } from '../../services/component_utils/component_utils'
import './with_load_more.scss'
const withLoadMore = ({
fetch, // function to fetch entries and return a promise
select, // function to select data from store
destroy, // function called at "destroyed" lifecycle
childPropName = 'entries', // name of the prop to be passed into the wrapped component
additionalPropNames = [] // additional prop name list of the wrapper component
}) => (WrappedComponent) => {
const originalProps = Object.keys(getComponentProps(WrappedComponent))
const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames)
return Vue.component('withLoadMore', {
render (createElement) {
const props = {
props: {
...this.$props,
[childPropName]: this.entries
},
on: this.$listeners,
scopedSlots: this.$scopedSlots
}
const children = Object.entries(this.$slots).map(([key, value]) => createElement('template', { slot: key }, value))
return (
<div class="with-load-more">
<WrappedComponent {...props}>
{children}
</WrappedComponent>
<div class="with-load-more-footer">
{this.error && <a onClick={this.fetchEntries} class="alert error">{this.$t('general.generic_error')}</a>}
{!this.error && this.loading && <i class="icon-spin3 animate-spin"/>}
{!this.error && !this.loading && !this.bottomedOut && <a onClick={this.fetchEntries}>{this.$t('general.more')}</a>}
</div>
</div>
)
},
props,
data () {
return {
loading: false,
bottomedOut: false,
error: false
}
},
computed: {
entries () {
return select(this.$props, this.$store) || []
}
},
created () {
window.addEventListener('scroll', this.scrollLoad)
if (this.entries.length === 0) {
this.fetchEntries()
}
},
destroyed () {
window.removeEventListener('scroll', this.scrollLoad)
destroy && destroy(this.$props, this.$store)
},
methods: {
fetchEntries () {
if (!this.loading) {
this.loading = true
this.error = false
fetch(this.$props, this.$store)
.then((newEntries) => {
this.loading = false
this.bottomedOut = isEmpty(newEntries)
})
.catch(() => {
this.loading = false
this.error = true
})
}
},
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()
}
}
}
})
}
export default withLoadMore

View File

@ -0,0 +1,10 @@
.with-load-more {
&-footer {
padding: 10px;
text-align: center;
.error {
font-size: 14px;
}
}
}

View File

@ -0,0 +1,84 @@
import Vue from 'vue'
import isEmpty from 'lodash/isEmpty'
import { getComponentProps } from '../../services/component_utils/component_utils'
import './with_subscription.scss'
const withSubscription = ({
fetch, // function to fetch entries and return a promise
select, // function to select data from store
childPropName = 'content', // name of the prop to be passed into the wrapped component
additionalPropNames = [] // additional prop name list of the wrapper component
}) => (WrappedComponent) => {
const originalProps = Object.keys(getComponentProps(WrappedComponent))
const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames)
return Vue.component('withSubscription', {
props: [
...props,
'refresh' // boolean saying to force-fetch data whenever created
],
render (createElement) {
if (!this.error && !this.loading) {
const props = {
props: {
...this.$props,
[childPropName]: this.fetchedData
},
on: this.$listeners,
scopedSlots: this.$scopedSlots
}
const children = Object.entries(this.$slots).map(([key, value]) => createElement('template', { slot: key }, value))
return (
<div class="with-subscription">
<WrappedComponent {...props}>
{children}
</WrappedComponent>
</div>
)
} else {
return (
<div class="with-subscription-loading">
{this.error
? <a onClick={this.fetchData} class="alert error">{this.$t('general.generic_error')}</a>
: <i class="icon-spin3 animate-spin"/>
}
</div>
)
}
},
data () {
return {
loading: false,
error: false
}
},
computed: {
fetchedData () {
return select(this.$props, this.$store)
}
},
created () {
if (this.refresh || isEmpty(this.fetchedData)) {
this.fetchData()
}
},
methods: {
fetchData () {
if (!this.loading) {
this.loading = true
this.error = false
fetch(this.$props, this.$store)
.then(() => {
this.loading = false
})
.catch(() => {
this.error = true
this.loading = false
})
}
}
}
})
}
export default withSubscription

View File

@ -0,0 +1,10 @@
.with-subscription {
&-loading {
padding: 10px;
text-align: center;
.error {
font-size: 14px;
}
}
}

View File

@ -134,6 +134,11 @@
"notification_visibility_mentions": "الإشارات",
"notification_visibility_repeats": "",
"nsfw_clickthrough": "",
"oauth_tokens": "رموز OAuth",
"token": "رمز",
"refresh_token": "رمز التحديث",
"valid_until": "صالح حتى",
"revoke_token": "سحب",
"panelRadius": "",
"pause_on_unfocused": "",
"presets": "النماذج",

View File

@ -132,6 +132,11 @@
"notification_visibility_repeats": "Republica una entrada meva",
"no_rich_text_description": "Neteja el formatat de text de totes les entrades",
"nsfw_clickthrough": "Amaga el contingut NSFW darrer d'una imatge clicable",
"oauth_tokens": "Llistats OAuth",
"token": "Token",
"refresh_token": "Actualitza el token",
"valid_until": "Vàlid fins",
"revoke_token": "Revocar",
"panelRadius": "Panells",
"pause_on_unfocused": "Pausa la reproducció en continu quan la pestanya perdi el focus",
"presets": "Temes",

427
src/i18n/cs.json Normal file
View File

@ -0,0 +1,427 @@
{
"chat": {
"title": "Chat"
},
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
"media_proxy": "Mediální proxy",
"scope_options": "Možnosti rozsahů",
"text_limit": "Textový limit",
"title": "Vlastnosti",
"who_to_follow": "Koho sledovat"
},
"finder": {
"error_fetching_user": "Chyba při načítání uživatele",
"find_user": "Najít uživatele"
},
"general": {
"apply": "Použít",
"submit": "Odeslat",
"more": "Více",
"generic_error": "Vyskytla se chyba",
"optional": "volitelné"
},
"image_cropper": {
"crop_picture": "Oříznout obrázek",
"save": "Uložit",
"cancel": "Zrušit"
},
"login": {
"login": "Přihlásit",
"description": "Přihlásit pomocí OAuth",
"logout": "Odhlásit",
"password": "Heslo",
"placeholder": "např. lain",
"register": "Registrovat",
"username": "Uživatelské jméno",
"hint": "Chcete-li se přidat do diskuze, přihlaste se"
},
"media_modal": {
"previous": "Předchozí",
"next": "Další"
},
"nav": {
"about": "O instanci",
"back": "Zpět",
"chat": "Místní chat",
"friend_requests": "Požadavky o sledování",
"mentions": "Zmínky",
"dms": "Přímé zprávy",
"public_tl": "Veřejná časová osa",
"timeline": "Časová osa",
"twkn": "Celá známá síť",
"user_search": "Hledání uživatelů",
"who_to_follow": "Koho sledovat",
"preferences": "Předvolby"
},
"notifications": {
"broken_favorite": "Neznámý příspěvek, hledám jej…",
"favorited_you": "si oblíbil/a váš příspěvek",
"followed_you": "vás nyní sleduje",
"load_older": "Načíst starší oznámení",
"notifications": "Oznámení",
"read": "Číst!",
"repeated_you": "zopakoval/a váš příspěvek",
"no_more_notifications": "Žádná další oznámení"
},
"post_status": {
"new_status": "Napsat nový příspěvek",
"account_not_locked_warning": "Váš účet není {0}. Kdokoliv vás může sledovat a vidět vaše příspěvky pouze pro sledující.",
"account_not_locked_warning_link": "uzamčen",
"attachments_sensitive": "Označovat přílohy jako citlivé",
"content_type": {
"plain_text": "Prostý text"
},
"content_warning": "Předmět (volitelný)",
"default": "Právě jsem přistál v L.A.",
"direct_warning": "Tento příspěvek uvidí pouze všichni zmínění uživatelé.",
"posting": "Přispívání",
"scope": {
"direct": "Přímý - Poslat pouze zmíněným uživatelům",
"private": "Pouze pro sledující - Poslat pouze sledujícím",
"public": "Veřejný - Poslat na veřejné časové osy",
"unlisted": "Neuvedený - Neposlat na veřejné časové osy"
}
},
"registration": {
"bio": "O vás",
"email": "E-mail",
"fullname": "Zobrazované jméno",
"password_confirm": "Potvrzení hesla",
"registration": "Registrace",
"token": "Token pozvánky",
"captcha": "CAPTCHA",
"new_captcha": "Kliknutím na obrázek získáte novou CAPTCHA",
"username_placeholder": "např. lain",
"fullname_placeholder": "např. Lain Iwakura",
"bio_placeholder": "např.\nNazdar, jsem Lain\nJsem anime dívka a žiji v příměstském Japonsku. Možná mě znáte z Wired.",
"validations": {
"username_required": "nemůže být prázdné",
"fullname_required": "nemůže být prázdné",
"email_required": "nemůže být prázdný",
"password_required": "nemůže být prázdné",
"password_confirmation_required": "nemůže být prázdné",
"password_confirmation_match": "musí být stejné jako heslo"
}
},
"settings": {
"app_name": "Název aplikace",
"attachmentRadius": "Přílohy",
"attachments": "Přílohy",
"autoload": "Povolit automatické načítání při rolování dolů",
"avatar": "Avatar",
"avatarAltRadius": "Avatary (oznámení)",
"avatarRadius": "Avatary",
"background": "Pozadí",
"bio": "O vás",
"blocks_tab": "Blokování",
"btnRadius": "Tlačítka",
"cBlue": "Modrá (Odpovědět, sledovat)",
"cGreen": "Zelená (Zopakovat)",
"cOrange": "Oranžová (Oblíbit)",
"cRed": "Červená (Zrušit)",
"change_password": "Změnit heslo",
"change_password_error": "Při změně vašeho hesla se vyskytla chyba.",
"changed_password": "Heslo bylo úspěšně změněno!",
"collapse_subject": "Zabalit příspěvky s předměty",
"composing": "Komponování",
"confirm_new_password": "Potvrďte nové heslo",
"current_avatar": "Váš současný avatar",
"current_password": "Současné heslo",
"current_profile_banner": "Váš současný profilový banner",
"data_import_export_tab": "Import/export dat",
"default_vis": "Výchozí rozsah viditelnosti",
"delete_account": "Smazat účet",
"delete_account_description": "Trvale smaže váš účet a všechny vaše příspěvky.",
"delete_account_error": "Při mazání vašeho účtu nastala chyba. Pokud tato chyba bude trvat, kontaktujte prosím admministrátora vaší instance.",
"delete_account_instructions": "Pro potvrzení smazání účtu napište své heslo do pole níže.",
"avatar_size_instruction": "Doporučená minimální velikost pro avatarové obrázky je 150x150 pixelů.",
"export_theme": "Uložit přednastavení",
"filtering": "Filtrování",
"filtering_explanation": "Všechny příspěvky obsahující tato slova budou skryty. Napište jedno slovo na každý řádek",
"follow_export": "Export sledovaných",
"follow_export_button": "Exportovat vaše sledované do souboru CSV",
"follow_export_processing": "Zpracovávám, brzy si budete moci stáhnout váš soubor",
"follow_import": "Import sledovaných",
"follow_import_error": "Chyba při importování sledovaných",
"follows_imported": "Sledovaní importováni! Jejich zpracování bude chvilku trvat.",
"foreground": "Popředí",
"general": "Obecné",
"hide_attachments_in_convo": "Skrývat přílohy v konverzacích",
"hide_attachments_in_tl": "Skrývat přílohy v časové ose",
"max_thumbnails": "Maximální počet miniatur na příspěvek",
"hide_isp": "Skrýt panel specifický pro instanci",
"preload_images": "Přednačítat obrázky",
"use_one_click_nsfw": "Otevírat citlivé přílohy pouze jedním kliknutím",
"hide_post_stats": "Skrývat statistiky příspěvků (např. počet oblíbení)",
"hide_user_stats": "Skrývat statistiky uživatelů (např. počet sledujících)",
"hide_filtered_statuses": "Skrývat filtrované příspěvky",
"import_followers_from_a_csv_file": "Importovat sledované ze souboru CSV",
"import_theme": "Načíst přednastavení",
"inputRadius": "Vstupní pole",
"checkboxRadius": "Zaškrtávací pole",
"instance_default": "(výchozí: {value})",
"instance_default_simple": "(výchozí)",
"interface": "Rozhraní",
"interfaceLanguage": "Jazyk rozhraní",
"invalid_theme_imported": "Zvolený soubor není podporovaný motiv Pleroma. Nebyly provedeny žádné změny s vaším motivem.",
"limited_availability": "Nedostupné ve vašem prohlížeči",
"links": "Odkazy",
"lock_account_description": "Omezit váš účet pouze na schválené sledující",
"loop_video": "Opakovat videa",
"loop_video_silent_only": "Opakovat pouze videa beze zvuku (t.j. „GIFy“ na Mastodonu)",
"mutes_tab": "Ignorování",
"play_videos_in_modal": "Přehrávat videa přímo v prohlížeči médií",
"use_contain_fit": "Neořezávat přílohu v miniaturách",
"name": "Jméno",
"name_bio": "Jméno a popis",
"new_password": "Nové heslo",
"notification_visibility": "Typy oznámení k zobrazení",
"notification_visibility_follows": "Sledující",
"notification_visibility_likes": "Oblíbení",
"notification_visibility_mentions": "Zmínky",
"notification_visibility_repeats": "Zopakování",
"no_rich_text_description": "Odstranit ze všech příspěvků formátování textu",
"no_blocks": "Žádná blokování",
"no_mutes": "Žádná ignorování",
"hide_follows_description": "Nezobrazovat, koho sleduji",
"hide_followers_description": "Nezobrazovat, kdo mě sleduje",
"show_admin_badge": "Zobrazovat v mém profilu odznak administrátora",
"show_moderator_badge": "Zobrazovat v mém profilu odznak moderátora",
"nsfw_clickthrough": "Povolit prokliknutelné skrývání citlivých příloh",
"oauth_tokens": "Tokeny OAuth",
"token": "Token",
"refresh_token": "Obnovit token",
"valid_until": "Platný do",
"revoke_token": "Odvolat",
"panelRadius": "Panely",
"pause_on_unfocused": "Pozastavit streamování, pokud není záložka prohlížeče v soustředění",
"presets": "Přednastavení",
"profile_background": "Profilové pozadí",
"profile_banner": "Profilový banner",
"profile_tab": "Profil",
"radii_help": "Nastavit zakulacení rohů rozhraní (v pixelech)",
"replies_in_timeline": "Odpovědi v časové ose",
"reply_link_preview": "Povolit náhledy odkazu pro odpověď při přejetí myši",
"reply_visibility_all": "Zobrazit všechny odpovědiShow all replies",
"reply_visibility_following": "Zobrazit pouze odpovědi směřované na mě nebo uživatele, které sleduji",
"reply_visibility_self": "Zobrazit pouze odpovědi směřované na mě",
"saving_err": "Chyba při ukládání nastavení",
"saving_ok": "Nastavení uložena",
"security_tab": "Bezpečnost",
"scope_copy": "Kopírovat rozsah při odpovídání (přímé zprávy jsou vždy kopírovány)",
"set_new_avatar": "Nastavit nový avatar",
"set_new_profile_background": "Nastavit nové profilové pozadí",
"set_new_profile_banner": "Nastavit nový profilový banner",
"settings": "Nastavení",
"subject_input_always_show": "Vždy zobrazit pole pro předmět",
"subject_line_behavior": "Kopírovat předmět při odpovídání",
"subject_line_email": "Jako u e-mailu: „re: předmět“",
"subject_line_mastodon": "Jako u Mastodonu: zkopírovat tak, jak je",
"subject_line_noop": "Nekopírovat",
"post_status_content_type": "Publikovat typ obsahu příspěvku",
"status_content_type_plain": "Prostý text",
"stop_gifs": "Přehrávat GIFy při přejetí myši",
"streaming": "Povolit automatické streamování nových příspěvků při rolování nahoru",
"text": "Text",
"theme": "Motiv",
"theme_help": "Použijte hexadecimální barevné kódy (#rrggbb) pro přizpůsobení vašeho barevného motivu.",
"theme_help_v2_1": "Zaškrtnutím pole můžete také přepsat barvy a průhlednost některých komponentů, pro smazání všech přednastavení použijte tlačítko „Smazat vše“.",
"theme_help_v2_2": "Ikony pod některými položkami jsou indikátory kontrastu pozadí/textu, pro detailní informace nad nimi přejeďte myší. Prosím berte na vědomí, že při používání kontrastu průhlednosti ukazují indikátory nejhorší možný případ.",
"tooltipRadius": "Popisky/upozornění",
"upload_a_photo": "Nahrát fotku",
"user_settings": "Uživatelská nastavení",
"values": {
"false": "ne",
"true": "ano"
},
"notifications": "Oznámení",
"enable_web_push_notifications": "Povolit webová push oznámení",
"style": {
"switcher": {
"keep_color": "Ponechat barvy",
"keep_shadows": "Ponechat stíny",
"keep_opacity": "Ponechat průhlednost",
"keep_roundness": "Ponechat kulatost",
"keep_fonts": "Keep fonts",
"save_load_hint": "Možnosti „Ponechat“ dočasně ponechávají aktuálně nastavené možností při volení či nahrávání motivů, také tyto možnosti ukládají při exportování motivu. Pokud není žádné pole zaškrtnuto, uloží export motivu všechno.",
"reset": "Resetovat",
"clear_all": "Vymazat vše",
"clear_opacity": "Vymazat průhlednost"
},
"common": {
"color": "Barva",
"opacity": "Průhlednost",
"contrast": {
"hint": "Poměr kontrastu je {ratio}, {level} {context}",
"level": {
"aa": "splňuje směrnici úrovně AA (minimální)",
"aaa": "splňuje směrnici úrovně AAA (doporučováno)",
"bad": "nesplňuje žádné směrnice přístupnosti"
},
"context": {
"18pt": "pro velký (18+ bodů) text",
"text": "pro text"
}
}
},
"common_colors": {
"_tab_label": "Obvyklé",
"main": "Obvyklé barvy",
"foreground_hint": "Pro detailnější kontrolu viz záložka „Pokročilé“",
"rgbo": "Ikony, odstíny, odznaky"
},
"advanced_colors": {
"_tab_label": "Pokročilé",
"alert": "Pozadí upozornění",
"alert_error": "Chyba",
"badge": "Pozadí odznaků",
"badge_notification": "Oznámení",
"panel_header": "Záhlaví panelu",
"top_bar": "Vrchní pruh",
"borders": "Okraje",
"buttons": "Tlačítka",
"inputs": "Vstupní pole",
"faint_text": "Vybledlý text"
},
"radii": {
"_tab_label": "Kulatost"
},
"shadows": {
"_tab_label": "Stín a osvětlení",
"component": "Komponent",
"override": "Přepsat",
"shadow_id": "Stín #{value}",
"blur": "Rozmazání",
"spread": "Rozsah",
"inset": "Vsazení",
"hint": "Pro stíny můžete také použít --variable jako hodnotu barvy pro použití proměnných CSS3. Prosím berte na vědomí, že nastavení průhlednosti v tomto případě nebude fungovat.",
"filter_hint": {
"always_drop_shadow": "Varování, tento stín vždy používá {0}, když to prohlížeč podporuje.",
"drop_shadow_syntax": "{0} nepodporuje parametr {1} a klíčové slovo {2}.",
"avatar_inset": "Prosím berte na vědomí, že kombinování vsazených i nevsazených stínů u avatarů může u průhledných avatarů dát neočekávané výsledky.",
"spread_zero": "Stíny s rozsahem > 0 se zobrazí, jako kdyby byl rozsah nastaven na nulu",
"inset_classic": "Vsazené stíny budou používat {0}"
},
"components": {
"panel": "Panel",
"panelHeader": "Záhlaví panelu",
"topBar": "Vrchní pruh",
"avatar": "Avatar uživatele (v zobrazení profilu)",
"avatarStatus": "Avatar uživatele (v zobrazení příspěvku)",
"popup": "Vyskakovací okna a popisky",
"button": "Tlačítko",
"buttonHover": "Tlačítko (přejetí myši)",
"buttonPressed": "Tlačítko (stisknuto)",
"buttonPressedHover": "Button (stisknuto+přejetí myši)",
"input": "Vstupní pole"
}
},
"fonts": {
"_tab_label": "Písma",
"help": "Zvolte písmo, které bude použito pro prvky rozhraní. U možnosti „vlastní“ musíte zadat přesný název písma tak, jak se zobrazuje v systému.",
"components": {
"interface": "Rozhraní",
"input": "Vstupní pole",
"post": "Text příspěvků",
"postCode": "Neproporcionální text v příspěvku (formátovaný text)"
},
"family": "Název písma",
"size": "Velikost (v pixelech)",
"weight": "Tloušťka",
"custom": "Vlastní"
},
"preview": {
"header": "Náhled",
"content": "Obsah",
"error": "Příklad chyby",
"button": "Tlačítko",
"text": "Spousta dalšího {0} a {1}",
"mono": "obsahu",
"input": "Just landed in L.A.",
"faint_link": "pomocný manuál",
"fine_print": "Přečtěte si náš {0} a nenaučte se nic užitečného!",
"header_faint": "Tohle je v pohodě",
"checkbox": "Pročetl/a jsem podmínky používání",
"link": "hezký malý odkaz"
}
}
},
"timeline": {
"collapse": "Zabalit",
"conversation": "Konverzace",
"error_fetching": "Chyba při načítání aktualizací",
"load_older": "Načíst starší příspěvky",
"no_retweet_hint": "Příspěvek je označen jako pouze pro sledující či přímý a nemůže být zopakován",
"repeated": "zopakoval/a",
"show_new": "Zobrazit nové",
"up_to_date": "Aktuální",
"no_more_statuses": "Žádné další příspěvky",
"no_statuses": "Žádné příspěvky"
},
"status": {
"reply_to": "Odpovědět uživateli",
"replies_list": "Odpovědi:"
},
"user_card": {
"approve": "Schválit",
"block": "Blokovat",
"blocked": "Blokován/a!",
"deny": "Zamítnout",
"favorites": "Oblíbené",
"follow": "Sledovat",
"follow_sent": "Požadavek odeslán!",
"follow_progress": "Odeslílám požadavek…",
"follow_again": "Odeslat požadavek znovu?",
"follow_unfollow": "Přestat sledovat",
"followees": "Sledovaní",
"followers": "Sledující",
"following": "Sledujete!",
"follows_you": "Sleduje vás!",
"its_you": "Jste to vy!",
"media": "Média",
"mute": "Ignorovat",
"muted": "Ignorován/a",
"per_day": "za den",
"remote_follow": "Vzdálené sledování",
"statuses": "Příspěvky",
"unblock": "Odblokovat",
"unblock_progress": "Odblokuji…",
"block_progress": "Blokuji…",
"unmute": "Přestat ignorovat",
"unmute_progress": "Ruším ignorování…",
"mute_progress": "Ignoruji…"
},
"user_profile": {
"timeline_title": "Uživatelská časová osa",
"profile_does_not_exist": "Omlouváme se, tento profil neexistuje.",
"profile_loading_error": "Omlouváme se, při načítání tohoto profilu se vyskytla chyba."
},
"who_to_follow": {
"more": "Více",
"who_to_follow": "Koho sledovat"
},
"tool_tip": {
"media_upload": "Nahrát média",
"repeat": "Zopakovat",
"reply": "Odpovědět",
"favorite": "Oblíbit",
"user_settings": "Uživatelské nastavení"
},
"upload":{
"error": {
"base": "Nahrávání selhalo.",
"file_too_big": "Soubor je úříliš velký [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Zkuste to znovu později"
},
"file_size_units": {
"B": "B",
"KiB": "KiB",
"MiB": "MiB",
"GiB": "GiB",
"TiB": "TiB"
}
}
}

View File

@ -159,6 +159,11 @@
"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",
"oauth_tokens": "OAuth-Token",
"token": "Zeichen",
"refresh_token": "Token aktualisieren",
"valid_until": "Gültig bis",
"revoke_token": "Widerrufen",
"panelRadius": "Panel",
"pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist",
"presets": "Voreinstellungen",

View File

@ -19,7 +19,13 @@
"apply": "Apply",
"submit": "Submit",
"more": "More",
"generic_error": "An error occured"
"generic_error": "An error occured",
"optional": "optional"
},
"image_cropper": {
"crop_picture": "Crop picture",
"save": "Save",
"cancel": "Cancel"
},
"login": {
"login": "Log in",
@ -31,6 +37,10 @@
"username": "Username",
"hint": "Log in to join the discussion"
},
"media_modal": {
"previous": "Previous",
"next": "Next"
},
"nav": {
"about": "About",
"back": "Back",
@ -83,6 +93,9 @@
"token": "Invite token",
"captcha": "CAPTCHA",
"new_captcha": "Click the image to get a new captcha",
"username_placeholder": "e.g. lain",
"fullname_placeholder": "e.g. Lain Iwakura",
"bio_placeholder": "e.g.\nHi, I'm Lain\nIm an anime girl living in suburban Japan. You may know me from the Wired.",
"validations": {
"username_required": "cannot be left blank",
"fullname_required": "cannot be left blank",
@ -93,6 +106,7 @@
}
},
"settings": {
"app_name": "App name",
"attachmentRadius": "Attachments",
"attachments": "Attachments",
"autoload": "Enable automatic loading when scrolled to the bottom",
@ -101,6 +115,7 @@
"avatarRadius": "Avatars",
"background": "Background",
"bio": "Bio",
"blocks_tab": "Blocks",
"btnRadius": "Buttons",
"cBlue": "Blue (Reply, follow)",
"cGreen": "Green (Retweet)",
@ -135,6 +150,7 @@
"general": "General",
"hide_attachments_in_convo": "Hide attachments in conversations",
"hide_attachments_in_tl": "Hide attachments in timeline",
"max_thumbnails": "Maximum amount of thumbnails per post",
"hide_isp": "Hide instance-specific panel",
"preload_images": "Preload images",
"use_one_click_nsfw": "Open NSFW attachments with just one click",
@ -155,6 +171,7 @@
"lock_account_description": "Restrict your account to approved followers only",
"loop_video": "Loop videos",
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
"mutes_tab": "Mutes",
"play_videos_in_modal": "Play videos directly in the media viewer",
"use_contain_fit": "Don't crop the attachment in thumbnails",
"name": "Name",
@ -166,11 +183,18 @@
"notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats",
"no_rich_text_description": "Strip rich text formatting from all posts",
"no_blocks": "No blocks",
"no_mutes": "No mutes",
"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",
"oauth_tokens": "OAuth tokens",
"token": "Token",
"refresh_token": "Refresh Token",
"valid_until": "Valid Until",
"revoke_token": "Revoke",
"panelRadius": "Panels",
"pause_on_unfocused": "Pause streaming when tab is not focused",
"presets": "Presets",
@ -206,6 +230,7 @@
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
"tooltipRadius": "Tooltips/alerts",
"upload_a_photo": "Upload a photo",
"user_settings": "User Settings",
"values": {
"false": "no",
@ -332,7 +357,12 @@
"repeated": "repeated",
"show_new": "Show new",
"up_to_date": "Up-to-date",
"no_more_statuses": "No more statuses"
"no_more_statuses": "No more statuses",
"no_statuses": "No statuses"
},
"status": {
"reply_to": "Reply to",
"replies_list": "Replies:"
},
"user_card": {
"approve": "Approve",
@ -355,10 +385,18 @@
"muted": "Muted",
"per_day": "per day",
"remote_follow": "Remote follow",
"statuses": "Statuses"
"statuses": "Statuses",
"unblock": "Unblock",
"unblock_progress": "Unblocking...",
"block_progress": "Blocking...",
"unmute": "Unmute",
"unmute_progress": "Unmuting...",
"mute_progress": "Muting..."
},
"user_profile": {
"timeline_title": "User Timeline"
"timeline_title": "User Timeline",
"profile_does_not_exist": "Sorry, this profile does not exist.",
"profile_loading_error": "Sorry, there was an error loading this profile."
},
"who_to_follow": {
"more": "More",

View File

@ -2,118 +2,421 @@
"chat": {
"title": "Babilejo"
},
"features_panel": {
"chat": "Babilejo",
"gopher": "Gopher",
"media_proxy": "Aŭdvidaĵa prokurilo",
"scope_options": "Agordoj de amplekso",
"text_limit": "Teksta limo",
"title": "Funkcioj",
"who_to_follow": "Kiun aboni"
},
"finder": {
"error_fetching_user": "Eraro alportante uzanton",
"find_user": "Trovi uzanton"
},
"general": {
"apply": "Apliki",
"submit": "Sendi"
"submit": "Sendi",
"more": "Pli",
"generic_error": "Eraro okazis",
"optional": "Malnepra"
},
"image_cropper": {
"crop_picture": "Tondi bildon",
"save": "Konservi",
"cancel": "Nuligi"
},
"login": {
"login": "Ensaluti",
"logout": "Elsaluti",
"login": "Saluti",
"description": "Saluti per OAuth",
"logout": "Adiaŭi",
"password": "Pasvorto",
"placeholder": "ekz. lain",
"register": "Registriĝi",
"username": "Salutnomo"
"username": "Salutnomo",
"hint": "Salutu por partopreni la diskutadon"
},
"media_modal": {
"previous": "Antaŭa",
"next": "Sekva"
},
"nav": {
"about": "Pri",
"back": "Reen",
"chat": "Loka babilejo",
"friend_requests": "Abonaj petoj",
"mentions": "Mencioj",
"dms": "Rektaj mesaĝoj",
"public_tl": "Publika tempolinio",
"timeline": "Tempolinio",
"twkn": "La tuta konata reto"
"twkn": "La tuta konata reto",
"user_search": "Serĉi uzantojn",
"who_to_follow": "Kiun aboni",
"preferences": "Agordoj"
},
"notifications": {
"broken_favorite": "Nekonata stato, serĉante ĝin…",
"favorited_you": "ŝatis vian staton",
"followed_you": "ekabonis vin",
"load_older": "Enlegi pli malnovajn sciigojn",
"notifications": "Sciigoj",
"read": "Legite!",
"repeated_you": "ripetis vian staton"
"repeated_you": "ripetis vian staton",
"no_more_notifications": "Neniuj pliaj sciigoj"
},
"post_status": {
"new_status": "Afiŝi novan staton",
"account_not_locked_warning": "Via konto ne estas {0}. Iu ajn povas vin aboni por vidi viajn afiŝoj nur por abonantoj.",
"account_not_locked_warning_link": "ŝlosita",
"attachments_sensitive": "Marki kunsendaĵojn kiel konsternajn",
"content_type": {
"plain_text": "Plata teksto"
},
"content_warning": "Temo (malnepra)",
"default": "Ĵus alvenis al la Universala Kongreso!",
"posting": "Afiŝante"
"direct_warning": "Ĉi tiu afiŝo estos videbla nur por ĉiuj menciitaj uzantoj.",
"posting": "Afiŝante",
"scope": {
"direct": "Rekta Afiŝi nur al menciitaj uzantoj",
"private": "Nur abonantoj Afiŝi nur al abonantoj",
"public": "Publika Afiŝi al publikaj tempolinioj",
"unlisted": "Nelistigita Ne afiŝi al publikaj tempolinioj"
}
},
"registration": {
"bio": "Priskribo",
"email": "Retpoŝtadreso",
"fullname": "Vidiga nomo",
"password_confirm": "Konfirmo de pasvorto",
"registration": "Registriĝo"
"registration": "Registriĝo",
"token": "Invita ĵetono",
"captcha": "TESTO DE HOMECO",
"new_captcha": "Alklaku la bildon por akiri novan teston",
"username_placeholder": "ekz. lain",
"fullname_placeholder": "ekz. Lain Iwakura",
"bio_placeholder": "ekz.\nSaluton, mi estas Lain\nMi estas animea knabino vivante en Japanujo. Eble vi konas min de la retejo «Wired».",
"validations": {
"username_required": "ne povas resti malplena",
"fullname_required": "ne povas resti malplena",
"email_required": "ne povas resti malplena",
"password_required": "ne povas resti malplena",
"password_confirmation_required": "ne povas resti malplena",
"password_confirmation_match": "samu la pasvorton"
}
},
"settings": {
"app_name": "Nomo de aplikaĵo",
"attachmentRadius": "Kunsendaĵoj",
"attachments": "Kunsendaĵoj",
"autoload": "Ŝalti memfaran ŝarĝadon ĉe subo de paĝo",
"autoload": "Ŝalti memfaran enlegadon ĉe subo de paĝo",
"avatar": "Profilbildo",
"avatarAltRadius": "Profilbildoj (sciigoj)",
"avatarRadius": "Profilbildoj",
"background": "Fono",
"bio": "Priskribo",
"blocks_tab": "Baroj",
"btnRadius": "Butonoj",
"cBlue": "Blua (Respondo, abono)",
"cGreen": "Verda (Kunhavigo)",
"cOrange": "Oranĝa (Ŝato)",
"cRed": "Ruĝa (Nuligo)",
"change_password": "Ŝanĝi pasvorton",
"change_password_error": "Okazis eraro dum ŝanĝo de via pasvorto.",
"changed_password": "Pasvorto sukcese ŝanĝiĝis!",
"collapse_subject": "Maletendi afiŝojn kun temoj",
"composing": "Verkante",
"confirm_new_password": "Konfirmu novan pasvorton",
"current_avatar": "Via nuna profilbildo",
"current_password": "Nuna pasvorto",
"current_profile_banner": "Via nuna profila rubando",
"data_import_export_tab": "Enporto / Elporto de datenoj",
"default_vis": "Implicita videbleca amplekso",
"delete_account": "Forigi konton",
"delete_account_description": "Por ĉiam forigi vian konton kaj ĉiujn viajn mesaĝojn",
"delete_account_error": "Okazis eraro dum forigo de via kanto. Se tio daŭre okazados, bonvolu kontakti la administranton de via nodo.",
"delete_account_instructions": "Entajpu sube vian pasvorton por konfirmi forigon de konto.",
"avatar_size_instruction": "La rekomendata malpleja grando de profilbildoj estas 150×150 bilderoj.",
"export_theme": "Konservi antaŭagordon",
"filtering": "Filtrado",
"filtering_explanation": "Ĉiuj statoj kun tiuj ĉi vortoj silentiĝos, po unu linie",
"filtering_explanation": "Ĉiuj statoj kun tiuj ĉi vortoj silentiĝos, po unu linio",
"follow_export": "Abona elporto",
"follow_export_button": "Elporti viajn abonojn al CSV-dosiero",
"follow_export_processing": "Traktante; baldaŭ vi ricevos peton elŝuti la dosieron",
"follow_import": "Abona enporto",
"follow_import_error": "Eraro enportante abonojn",
"follows_imported": "Abonoj enportiĝis! Traktado daŭros iom.",
"foreground": "Malfono",
"general": "Ĝenerala",
"hide_attachments_in_convo": "Kaŝi kunsendaĵojn en interparoloj",
"hide_attachments_in_tl": "Kaŝi kunsendaĵojn en tempolinio",
"max_thumbnails": "Plej multa nombro da bildetoj po afiŝo",
"hide_isp": "Kaŝi nodo-propran breton",
"preload_images": "Antaŭ-enlegi bildojn",
"use_one_click_nsfw": "Malfermi konsternajn kunsendaĵojn per nur unu klako",
"hide_post_stats": "Kaŝi statistikon de afiŝoj (ekz. nombron da ŝatoj)",
"hide_user_stats": "Kaŝi statistikon de uzantoj (ekz. nombron da abonantoj)",
"hide_filtered_statuses": "Kaŝi filtritajn statojn",
"import_followers_from_a_csv_file": "Enporti abonojn el CSV-dosiero",
"import_theme": "Enlegi antaŭagordojn",
"inputRadius": "Enigaj kampoj",
"checkboxRadius": "Markbutonoj",
"instance_default": "(implicita: {value})",
"instance_default_simple": "(implicita)",
"interface": "Fasado",
"interfaceLanguage": "Lingvo de fasado",
"invalid_theme_imported": "La elektita dosiero ne estas subtenata haŭto de Pleromo. Neniuj ŝanĝoj al via haŭto okazis.",
"limited_availability": "Nehavebla en via foliumilo",
"links": "Ligiloj",
"lock_account_description": "Limigi vian konton al nur abonantoj aprobitaj",
"loop_video": "Ripetadi filmojn",
"loop_video_silent_only": "Ripetadi nur filmojn sen sono (ekz. la \"GIF-ojn\" de Mastodon)",
"mutes_tab": "Silentigoj",
"play_videos_in_modal": "Ludi filmojn rekte en la aŭdvidaĵa spektilo",
"use_contain_fit": "Ne tondi la kunsendaĵon en bildetoj",
"name": "Nomo",
"name_bio": "Nomo kaj priskribo",
"new_password": "Nova pasvorto",
"notification_visibility": "Montrotaj specoj de sciigoj",
"notification_visibility_follows": "Abonoj",
"notification_visibility_likes": "Ŝatoj",
"notification_visibility_mentions": "Mencioj",
"notification_visibility_repeats": "Ripetoj",
"no_rich_text_description": "Forigi riĉtekstajn formojn de ĉiuj afiŝoj",
"no_blocks": "Neniuj baroj",
"no_mutes": "Neniuj silentigoj",
"hide_follows_description": "Ne montri kiun mi sekvas",
"hide_followers_description": "Ne montri kiu min sekvas",
"show_admin_badge": "Montri la insignon de administranto en mia profilo",
"show_moderator_badge": "Montri la insignon de kontrolanto en mia profilo",
"nsfw_clickthrough": "Ŝalti traklakan kaŝon de konsternaj kunsendaĵoj",
"panelRadius": "Paneloj",
"oauth_tokens": "Ĵetonoj de OAuth",
"token": "Ĵetono",
"refresh_token": "Ĵetono de novigo",
"valid_until": "Valida ĝis",
"revoke_token": "Senvalidigi",
"panelRadius": "Bretoj",
"pause_on_unfocused": "Paŭzigi elsendfluon kiam langeto ne estas fokusata",
"presets": "Antaŭagordoj",
"profile_background": "Profila fono",
"profile_banner": "Profila rubando",
"radii_help": "Agordi fasadan rondigon de randoj (rastrumere)",
"reply_link_preview": "Ŝalti respond-ligilan antaŭvidon dum ŝvebo",
"profile_tab": "Profilo",
"radii_help": "Agordi fasadan rondigon de randoj (bildere)",
"replies_in_timeline": "Respondoj en tempolinio",
"reply_link_preview": "Ŝalti respond-ligilan antaŭvidon dum musa ŝvebo",
"reply_visibility_all": "Montri ĉiujn respondojn",
"reply_visibility_following": "Montri nur respondojn por mi aŭ miaj abonatoj",
"reply_visibility_self": "Montri nur respondojn por mi",
"saving_err": "Eraro dum konservo de agordoj",
"saving_ok": "Agordoj konserviĝis",
"security_tab": "Sekureco",
"scope_copy": "Kopii amplekson por respondo (rektaj mesaĝoj ĉiam kopiiĝas)",
"set_new_avatar": "Agordi novan profilbildon",
"set_new_profile_background": "Agordi novan profilan fonon",
"set_new_profile_banner": "Agordi novan profilan rubandon",
"settings": "Agordoj",
"stop_gifs": "Movi GIF-bildojn dum ŝvebo",
"subject_input_always_show": "Ĉiam montri teman kampon",
"subject_line_behavior": "Kopii temon por respondo",
"subject_line_email": "Kiel retpoŝto: \"re: temo\"",
"subject_line_mastodon": "Kiel Mastodon: kopii senŝanĝe",
"subject_line_noop": "Ne kopii",
"post_status_content_type": "Afiŝi specon de la enhavo de la stato",
"status_content_type_plain": "Plata teksto",
"stop_gifs": "Movi GIF-bildojn dum musa ŝvebo",
"streaming": "Ŝalti memfaran fluigon de novaj afiŝoj ĉe la supro de la paĝo",
"text": "Teksto",
"theme": "Etoso",
"theme_help": "Uzu deksesumajn kolorkodojn (#rrvvbb) por adapti vian koloran etoson.",
"theme": "Haŭto",
"theme_help": "Uzu deksesumajn kolorkodojn (#rrvvbb) por adapti vian koloran haŭton.",
"theme_help_v2_1": "Vi ankaŭ povas superagordi la kolorojn kaj travideblecon de kelkaj eroj per marko de la markbutono; uzu la butonon \"Vakigi ĉion\" por forigi ĉîujn superagordojn.",
"theme_help_v2_2": "Bildsimboloj sub kelkaj eroj estas indikiloj de kontrasto inter fono kaj teksto; muse ŝvebu por detalaj informoj. Bonvolu memori, ke la indikilo montras la plej malbonan okazeblon dum sia uzo.",
"tooltipRadius": "Ŝpruchelpiloj/avertoj",
"user_settings": "Uzantaj agordoj"
"upload_a_photo": "Alŝuti foton",
"user_settings": "Agordoj de uzanto",
"values": {
"false": "ne",
"true": "jes"
},
"notifications": "Sciigoj",
"enable_web_push_notifications": "Ŝalti retajn puŝajn sciigojn",
"style": {
"switcher": {
"keep_color": "Konservi kolorojn",
"keep_shadows": "Konservi ombrojn",
"keep_opacity": "Konservi maltravideblecon",
"keep_roundness": "Konservi rondecon",
"keep_fonts": "Konservi tiparojn",
"save_load_hint": "Elektebloj de \"konservi\" konservas la nuntempajn agordojn dum elektado aŭ enlegado de haŭtoj. Ĝi ankaŭ konservas tiujn agordojn dum elportado de haŭto. Kun ĉiuj markbutonoj nemarkitaj, elporto de la haŭto ĉion konservos.",
"reset": "Restarigi",
"clear_all": "Vakigi ĉion",
"clear_opacity": "Vakigi maltravideblecon"
},
"common": {
"color": "Koloro",
"opacity": "Maltravidebleco",
"contrast": {
"hint": "Proporcio de kontrasto estas {ratio}, ĝi {level} {context}",
"level": {
"aa": "plenumas la gvidilon je nivelo AA (malpleja)",
"aaa": "plenumas la gvidilon je nivela AAA (rekomendita)",
"bad": "plenumas neniujn faciluzajn gvidilojn"
},
"context": {
"18pt": "por granda (18pt+) teksto",
"text": "por teksto"
}
}
},
"common_colors": {
"_tab_label": "Komunaj",
"main": "Komunaj koloroj",
"foreground_hint": "Vidu langeton \"Specialaj\" por pli detalaj agordoj",
"rgbo": "Bildsimboloj, emfazoj, insignoj"
},
"advanced_colors": {
"_tab_label": "Specialaj",
"alert": "Averta fono",
"alert_error": "Eraro",
"badge": "Insigna fono",
"badge_notification": "Sciigo",
"panel_header": "Kapo de breto",
"top_bar": "Supra breto",
"borders": "Limoj",
"buttons": "Butonoj",
"inputs": "Enigaj kampoj",
"faint_text": "Malvigla teksto"
},
"radii": {
"_tab_label": "Rondeco"
},
"shadows": {
"_tab_label": "Ombro kaj lumo",
"component": "Ero",
"override": "Transpasi",
"shadow_id": "Ombro #{value}",
"blur": "Malklarigo",
"spread": "Vastigo",
"inset": "Internigo",
"hint": "Por ombroj vi ankaŭ povas uzi --variable kiel koloran valoron, por uzi variantojn de CSS3. Bonvolu rimarki, ke tiuokaze agordoj de maltravidebleco ne funkcios.",
"filter_hint": {
"always_drop_shadow": "Averto: ĉi tiu ombro ĉiam uzas {0} kiam la foliumilo ĝin subtenas.",
"drop_shadow_syntax": "{0} ne subtenas parametron {1} kaj ŝlosilvorton {2}.",
"avatar_inset": "Bonvolu rimarki, ke agordi ambaŭ internajn kaj eksterajn ombrojn por profilbildoj povas redoni neatenditajn rezultojn ĉe profilbildoj travideblaj.",
"spread_zero": "Ombroj kun vastigo > 0 aperos kvazaŭ ĝi estus fakte nulo",
"inset_classic": "Internaj ombroj uzos {0}"
},
"components": {
"panel": "Breto",
"panelHeader": "Kapo de breto",
"topBar": "Supra breto",
"avatar": "Profilbildo de uzanto (en profila vido)",
"avatarStatus": "Profilbildo de uzanto (en afiŝa vido)",
"popup": "Ŝprucaĵoj",
"button": "Butono",
"buttonHover": "Butono (je ŝvebo)",
"buttonPressed": "Butono (premita)",
"buttonPressedHover": "Butono (premita je ŝvebo)",
"input": "Eniga kampo"
}
},
"fonts": {
"_tab_label": "Tiparoj",
"help": "Elektu tiparon uzotan por eroj de la fasado. Por \"propra\" vi devas enigi la precizan nomon de tiparo tiel, kiel ĝi aperas en la sistemo",
"components": {
"interface": "Fasado",
"input": "Enigaj kampoj",
"post": "Teksto de afiŝo",
"postCode": "Egallarĝa teksto en afiŝo (riĉteksto)"
},
"family": "Nomo de tiparo",
"size": "Grando (en bilderoj)",
"weight": "Pezo (graseco)",
"custom": "Propra"
},
"preview": {
"header": "Antaŭrigardo",
"content": "Enhavo",
"error": "Ekzempla eraro",
"button": "Butono",
"text": "Kelko da pliaj {0} kaj {1}",
"mono": "enhavo",
"input": "Ĵus alvenis al la Universala Kongreso!",
"faint_link": "helpan manlibron",
"fine_print": "Legu nian {0} por nenion utilan ekscii!",
"header_faint": "Tio estas en ordo",
"checkbox": "Mi legetis la kondiĉojn de uzado",
"link": "bela eta ligil"
}
}
},
"timeline": {
"collapse": "Maletendi",
"conversation": "Interparolo",
"error_fetching": "Eraro dum ĝisdatigo",
"load_older": "Montri pli malnovajn statojn",
"repeated": "ripetata",
"no_retweet_hint": "Afiŝo estas markita kiel rekta aŭ nur por abonantoj, kaj ne eblas ĝin ripeti",
"repeated": "ripetita",
"show_new": "Montri novajn",
"up_to_date": "Ĝisdata"
"up_to_date": "Ĝisdata",
"no_more_statuses": "Neniuj pliaj statoj",
"no_statuses": "Neniuj statoj"
},
"user_card": {
"approve": "Aprobi",
"block": "Bari",
"blocked": "Barita!",
"deny": "Rifuzi",
"favorites": "Ŝatataj",
"follow": "Aboni",
"follow_sent": "Peto sendiĝis!",
"follow_progress": "Petanta…",
"follow_again": "Ĉu sendi peton denove?",
"follow_unfollow": "Malaboni",
"followees": "Abonatoj",
"followers": "Abonantoj",
"following": "Abonanta!",
"follows_you": "Abonas vin!",
"its_you": "Tio estas vi!",
"media": "Aŭdvidaĵoj",
"mute": "Silentigi",
"muted": "Silentigitaj",
"per_day": "tage",
"remote_follow": "Fore aboni",
"statuses": "Statoj"
"statuses": "Statoj",
"unblock": "Malbari",
"unblock_progress": "Malbaranta…",
"block_progress": "Baranta…",
"unmute": "Malsilentigi",
"unmute_progress": "Malsilentiganta…",
"mute_progress": "Silentiganta…"
},
"user_profile": {
"timeline_title": "Uzanta tempolinio"
"timeline_title": "Uzanta tempolinio",
"profile_does_not_exist": "Pardonu, ĉi tiu profilo ne ekzistas.",
"profile_loading_error": "Pardonu, eraro okazis dum enlegado de ĉi tiu profilo."
},
"who_to_follow": {
"more": "Pli",
"who_to_follow": "Kiun aboni"
},
"tool_tip": {
"media_upload": "Alŝuti aŭdvidaĵon",
"repeat": "Ripeti",
"reply": "Respondi",
"favorite": "Ŝati",
"user_settings": "Agordoj de uzanto"
},
"upload":{
"error": {
"base": "Alŝuto malsukcesis.",
"file_too_big": "Dosiero estas tro granda [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Reprovu pli poste"
},
"file_size_units": {
"B": "B",
"KiB": "KiB",
"MiB": "MiB",
"GiB": "GiB",
"TiB": "TiB"
}
}
}

View File

@ -171,6 +171,11 @@
"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",
"oauth_tokens": "Tokens de OAuth",
"token": "Token",
"refresh_token": "Actualizar el token",
"valid_until": "Válido hasta",
"revoke_token": "Revocar",
"panelRadius": "Paneles",
"pause_on_unfocused": "Parar la transmisión cuando no estés en foco.",
"presets": "Por defecto",

View File

@ -133,6 +133,7 @@
"general": "Yleinen",
"hide_attachments_in_convo": "Piilota liitteet keskusteluissa",
"hide_attachments_in_tl": "Piilota liitteet aikajanalla",
"max_thumbnails": "Suurin sallittu määrä liitteitä esikatselussa",
"hide_isp": "Piilota palvelimenkohtainen ruutu",
"preload_images": "Esilataa kuvat",
"use_one_click_nsfw": "Avaa NSFW-liitteet yhdellä painalluksella",
@ -165,6 +166,11 @@
"no_rich_text_description": "Älä näytä tekstin muotoilua.",
"hide_network_description": "Älä näytä seurauksiani tai seuraajiani",
"nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse",
"oauth_tokens": "OAuth-merkit",
"token": "Token",
"refresh_token": "Päivitä token",
"valid_until": "Voimassa asti",
"revoke_token": "Peruuttaa",
"panelRadius": "Ruudut",
"pause_on_unfocused": "Pysäytä automaattinen viestien näyttö välilehden ollessa pois fokuksesta",
"presets": "Valmiit teemat",
@ -215,6 +221,10 @@
"up_to_date": "Ajantasalla",
"no_more_statuses": "Ei enempää viestejä"
},
"status": {
"reply_to": "Vastaus",
"replies_list": "Vastaukset:"
},
"user_card": {
"approve": "Hyväksy",
"block": "Estä",

View File

@ -137,6 +137,11 @@
"notification_visibility_mentions": "Mentionnés",
"notification_visibility_repeats": "Partages",
"nsfw_clickthrough": "Masquer les images marquées comme contenu adulte ou sensible",
"oauth_tokens": "Jetons OAuth",
"token": "Jeton",
"refresh_token": "Refresh Token",
"valid_until": "Valable jusque",
"revoke_token": "Révoquer",
"panelRadius": "Fenêtres",
"pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas centré",
"presets": "Thèmes prédéfinis",

View File

@ -134,6 +134,11 @@
"notification_visibility_repeats": "Atphostáil",
"no_rich_text_description": "Bain formáidiú téacs saibhir ó gach post",
"nsfw_clickthrough": "Cumasaigh an ceangaltán NSFW cliceáil ar an gcnaipe",
"oauth_tokens": "Tocanna OAuth",
"token": "Token",
"refresh_token": "Athnuachan Comórtas",
"valid_until": "Bailí Go dtí",
"revoke_token": "Athghairm",
"panelRadius": "Painéil",
"pause_on_unfocused": "Sruthú ar sos nuair a bhíonn an fócas caillte",
"presets": "Réamhshocruithe",

View File

@ -129,6 +129,11 @@
"notification_visibility_mentions": "אזכורים",
"notification_visibility_repeats": "חזרות",
"nsfw_clickthrough": "החל החבאת צירופים לא בטוחים לצפיה בעת עבודה בעזרת לחיצת עכבר",
"oauth_tokens": "אסימוני OAuth",
"token": "אסימון",
"refresh_token": "רענון האסימון",
"valid_until": "בתוקף עד",
"revoke_token": "בטל",
"panelRadius": "פאנלים",
"pause_on_unfocused": "השהה זרימת הודעות כשהחלון לא בפוקוס",
"presets": "ערכים קבועים מראש",

View File

@ -93,6 +93,11 @@
"notification_visibility_mentions": "Menzioni",
"notification_visibility_repeats": "Condivisioni",
"no_rich_text_description": "Togli la formattazione del testo da tutti i post",
"oauth_tokens": "Token OAuth",
"token": "Token",
"refresh_token": "Aggiorna token",
"valid_until": "Valido fino a",
"revoke_token": "Revocare",
"panelRadius": "Pannelli",
"pause_on_unfocused": "Metti in pausa l'aggiornamento continuo quando la scheda non è in primo piano",
"presets": "Valori predefiniti",

View File

@ -171,6 +171,11 @@
"show_admin_badge": "アドミンのしるしをみる",
"show_moderator_badge": "モデレーターのしるしをみる",
"nsfw_clickthrough": "NSFWなファイルをかくす",
"oauth_tokens": "OAuthトークン",
"token": "トークン",
"refresh_token": "トークンを更新",
"valid_until": "まで有効",
"revoke_token": "取り消す",
"panelRadius": "パネル",
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
"presets": "プリセット",

View File

@ -159,6 +159,11 @@
"hide_follows_description": "내가 팔로우하는 사람을 표시하지 않음",
"hide_followers_description": "나를 따르는 사람을 보여주지 마라.",
"nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화",
"oauth_tokens": "OAuth 토큰",
"token": "토큰",
"refresh_token": "토큰 새로 고침",
"valid_until": "까지 유효하다",
"revoke_token": "취소",
"panelRadius": "패널",
"pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기",
"presets": "프리셋",

View File

@ -10,6 +10,7 @@
const messages = {
ar: require('./ar.json'),
ca: require('./ca.json'),
cs: require('./cs.json'),
de: require('./de.json'),
en: require('./en.json'),
eo: require('./eo.json'),

View File

@ -132,6 +132,11 @@
"notification_visibility_repeats": "Gjentakelser",
"no_rich_text_description": "Fjern all formatering fra statuser",
"nsfw_clickthrough": "Krev trykk for å vise statuser som kan være upassende",
"oauth_tokens": "OAuth Tokens",
"token": "Pollett",
"refresh_token": "Refresh Token",
"valid_until": "Gyldig til",
"revoke_token": "Tilbakekall",
"panelRadius": "Panel",
"pause_on_unfocused": "Stopp henting av poster når vinduet ikke er i fokus",
"presets": "Forhåndsdefinerte tema",

View File

@ -159,6 +159,11 @@
"no_rich_text_description": "Strip rich text formattering van alle posts",
"hide_network_description": "Toon niet wie mij volgt en wie ik volg.",
"nsfw_clickthrough": "Schakel doorklikbaar verbergen van NSFW bijlages in",
"oauth_tokens": "OAuth-tokens",
"token": "Token",
"refresh_token": "Token vernieuwen",
"valid_until": "Geldig tot",
"revoke_token": "Intrekken",
"panelRadius": "Panelen",
"pause_on_unfocused": "Pauzeer streamen wanneer de tab niet gefocused is",
"presets": "Presets",

View File

@ -1,51 +1,82 @@
{
"chat": {
"title": "Messatjariá"
},
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
"media_proxy": "Servidor mandatari mèdia",
"scope_options": "Nivèls de confidencialitat",
"text_limit": "Limita de tèxte",
"title": "Foncionalitats",
"who_to_follow": "Qual seguir"
},
"finder": {
"error_fetching_user": "Error pendent la recèrca dun utilizaire",
"error_fetching_user": "Error pendent la cèrca dun utilizaire",
"find_user": "Cercar un utilizaire"
},
"general": {
"apply": "Aplicar",
"submit": "Mandar"
"submit": "Mandar",
"more": "Mai",
"generic_error": "Una error ses producha",
"optional": "opcional"
},
"image_cropper": {
"crop_picture": "Talhar limatge",
"save": "Salvar",
"cancel": "Anullar"
},
"login": {
"login": "Connexion",
"description": "Connexion via OAuth",
"logout": "Desconnexion",
"password": "Senhal",
"placeholder": "e.g. lain",
"register": "Se marcar",
"username": "Nom dutilizaire"
"username": "Nom dutilizaire",
"hint": "Connectatz-vos per participar a la discutida"
},
"media_modal": {
"previous": "Precedent",
"next": "Seguent"
},
"nav": {
"about": "A prepaus",
"back": "Tornar",
"chat": "Chat local",
"friend_requests": "Demandas de seguiment",
"mentions": "Notificacions",
"dms": "Messatges privats",
"public_tl": "Estatuts locals",
"timeline": "Flux dactualitat",
"twkn": "Lo malhum conegut",
"friend_requests": "Demandas d'abonament"
"user_search": "Cèrca dutilizaires",
"who_to_follow": "Qual seguir",
"preferences": "Preferéncias"
},
"notifications": {
"broken_favorite": "Estatut desconegut, sèm a lo cercar...",
"favorited_you": "a aimat vòstre estatut",
"followed_you": "vos a seguit",
"load_older": "Cargar las notificaciones mai ancianas",
"notifications": "Notficacions",
"read": "Legit !",
"read": "Legit !",
"repeated_you": "a repetit vòstre estatut",
"broken_favorite": "Estatut desconegut, sèm a lo cercar...",
"load_older": "Cargar las notificaciones mai ancianas"
"no_more_notifications": "Pas mai de notificacions"
},
"post_status": {
"content_warning": "Avís de contengut (opcional)",
"default": "Escrivètz aquí vòstre estatut.",
"posting": "Mandadís",
"new_status": "Publicar destatuts novèls",
"account_not_locked_warning": "Vòstre compte es pas {0}. Qual que siá pòt vos seguir per veire vòstras publicacions destinadas pas qu'a vòstres seguidors.",
"account_not_locked_warning_link": "clavat",
"attachments_sensitive": "Marcar las pèças juntas coma sensiblas",
"content_type": {
"plain_text": "Tèxte brut"
},
"content_warning": "Avís de contengut (opcional)",
"default": "Escrivètz aquí vòstre estatut.",
"direct_warning": "Aquesta publicacion serà pas que visibla pels utilizaires mencionats.",
"posting": "Mandadís",
"scope": {
"direct": "Dirècte - Publicar pels utilizaires mencionats solament",
"private": "Seguidors solament - Publicar pels sols seguidors",
@ -59,9 +90,23 @@
"fullname": "Nom complèt",
"password_confirm": "Confirmar lo senhal",
"registration": "Inscripcion",
"token": "Geton de convidat"
"token": "Geton de convidat",
"captcha": "CAPTCHA",
"new_captcha": "Clicatz limatge per obténer una nòva captcha",
"username_placeholder": "e.g. lain",
"fullname_placeholder": "e.g. Lain Iwakura",
"bio_placeholder": "e.g.\nHi, Soi lo Lain\nSoi afocada danimes e vivi al Japan. Benlèu que me coneissètz de the Wired.",
"validations": {
"username_required": "pòt pas èsser void",
"fullname_required": "pòt pas èsser void",
"email_required": "pòt pas èsser void",
"password_required": "pòt pas èsser void",
"password_confirmation_required": "pòt pas èsser void",
"password_confirmation_match": "deu èsser lo meteis senhal"
}
},
"settings": {
"app_name": "Nom de laplicacion",
"attachmentRadius": "Pèças juntas",
"attachments": "Pèças juntas",
"autoload": "Activar lo cargament automatic un còp arribat al cap de la pagina",
@ -70,6 +115,7 @@
"avatarRadius": "Avatars",
"background": "Rèire plan",
"bio": "Biografia",
"blocks_tab": "Blocatges",
"btnRadius": "Botons",
"cBlue": "Blau (Respondre, seguir)",
"cGreen": "Verd (Repartajar)",
@ -78,15 +124,21 @@
"change_password": "Cambiar lo senhal",
"change_password_error": "Una error ses producha en cambiant lo senhal.",
"changed_password": "Senhal corrèctament cambiat !",
"collapse_subject": "Replegar las publicacions amb de subjèctes",
"composing": "Escritura",
"confirm_new_password": "Confirmatz lo nòu senhal",
"current_avatar": "Vòstre avatar actual",
"current_password": "Senhal actual",
"current_profile_banner": "Bandièra actuala del perfil",
"data_import_export_tab": "Importar / Exportar las donadas",
"default_vis": "Nivèl de visibilitat per defaut",
"delete_account": "Suprimir lo compte",
"delete_account_description": "Suprimir vòstre compte e los messatges per sempre.",
"delete_account_error": "Una error ses producha en suprimir lo compte. Saquò ten darribar mercés de contactar vòstre administrador dinstància.",
"delete_account_instructions": "Picatz vòstre senhal dins lo camp tèxte çai-jos per confirmar la supression del compte.",
"filtering": "Filtre",
"avatar_size_instruction": "La talha minimum recomandada pels imatges davatar es 150x150 pixèls.",
"export_theme": "Enregistrar la preconfiguracion",
"filtering": "Filtratge",
"filtering_explanation": "Totes los estatuts amb aqueles mots seràn en silenci, un mot per linha",
"follow_export": "Exportar los abonaments",
"follow_export_button": "Exportar vòstres abonaments dins un fichièr csv",
@ -95,62 +147,91 @@
"follow_import_error": "Error en important los seguidors",
"follows_imported": "Seguidors importats. Lo tractament pòt trigar una estona.",
"foreground": "Endavant",
"general": "General",
"hide_attachments_in_convo": "Rescondre las pèças juntas dins las conversacions",
"hide_attachments_in_tl": "Rescondre las pèças juntas",
"import_followers_from_a_csv_file": "Importar los seguidors dun fichièr csv",
"inputRadius": "Camps tèxte",
"links": "Ligams",
"name": "Nom",
"name_bio": "Nom & Bio",
"new_password": "Nòu senhal",
"nsfw_clickthrough": "Activar lo clic per mostrar los imatges marcats coma pels adults o sensibles",
"panelRadius": "Panèls",
"presets": "Pre-enregistrats",
"profile_background": "Imatge de fons",
"profile_banner": "Bandièra del perfil",
"radii_help": "Configurar los caires arredondits de linterfàcia (en pixèls)",
"reply_link_preview": "Activar lapercebut en passar la mirga",
"set_new_avatar": "Cambiar lavatar",
"set_new_profile_background": "Cambiar limatge de fons",
"set_new_profile_banner": "Cambiar de bandièra",
"settings": "Paramètres",
"stop_gifs": "Lançar los GIFs al subrevòl",
"streaming": "Activar lo cargament automatic dels novèls estatus en anar amont",
"text": "Tèxte",
"theme": "Tèma",
"theme_help": "Emplegatz los còdis de color hex (#rrggbb) per personalizar vòstre tèma de color.",
"tooltipRadius": "Astúcias/Alèrta",
"user_settings": "Paramètres utilizaire",
"collapse_subject": "Replegar las publicacions amb de subjèctes",
"data_import_export_tab": "Importar / Exportar las donadas",
"default_vis": "Nivèl de visibilitat per defaut",
"export_theme": "Enregistrar la preconfiguracion",
"general": "General",
"max_thumbnails": "Nombre maximum de vinhetas per publicacion",
"hide_isp": "Amagar lo panèl especial instància",
"preload_images": "Precargar los imatges",
"use_one_click_nsfw": "Dobrir las pèças juntas NSFW amb un clic",
"hide_post_stats": "Amagar los estatistics de publicacion (ex. lo ombre de favorits)",
"hide_user_stats": "Amagar las estatisticas de lutilizaire (ex. lo nombre de seguidors)",
"hide_filtered_statuses": "Amagar los estatuts filtrats",
"import_followers_from_a_csv_file": "Importar los seguidors dun fichièr csv",
"import_theme": "Cargar un tèma",
"instance_default": "(defaut : {value})",
"inputRadius": "Camps tèxte",
"checkboxRadius": "Casas de marcar",
"instance_default": "(defaut : {value})",
"instance_default_simple": "(defaut)",
"interface": "Interfàcia",
"interfaceLanguage": "Lenga de linterfàcia",
"invalid_theme_imported": "Lo fichièr seleccionat es pas un tèma Pleroma valid. Cap de cambiament es estat fach a vòstre tèma.",
"limited_availability": "Pas disponible per vòstre navigador",
"links": "Ligams",
"lock_account_description": "Limitar vòstre compte als seguidors acceptats solament",
"loop_video": "Bocla vidèo",
"loop_video_silent_only": "Legir en bocla solament las vidèos sens son (coma los « Gifs » de Mastodon)",
"notification_visibility": "Tipes de notificacion de mostrar",
"loop_video_silent_only": "Legir en bocla solament las vidèos sens son (coma los « Gifs » de Mastodon)",
"mutes_tab": "Agamats",
"play_videos_in_modal": "Legir las vidèoas dirèctament dins la visualizaira mèdia",
"use_contain_fit": "Talhar pas las pèças juntas per las vinhetas",
"name": "Nom",
"name_bio": "Nom & Bio",
"new_password": "Nòu senhal",
"notification_visibility_follows": "Abonaments",
"notification_visibility_likes": "Aiman",
"notification_visibility_likes": "Aimar",
"notification_visibility_mentions": "Mencions",
"notification_visibility_repeats": "Repeticions",
"notification_visibility": "Tipes de notificacion de mostrar",
"no_rich_text_description": "Netejar lo format tèxte de totas las publicacions",
"no_blocks": "Cap de blocatge",
"no_mutes": "Cap damagat",
"hide_follows_description": "Mostrar pas qual seguissi",
"hide_followers_description": "Mostrar pas qual me seguisson",
"show_admin_badge": "Mostrar lo badge Admin badge al perfil meu",
"show_moderator_badge": "Mostrar lo badge Moderator al perfil meu",
"nsfw_clickthrough": "Activar lo clic per mostrar los imatges marcats coma pels adults o sensibles",
"oauth_tokens": "Listats OAuth",
"token": "Geton",
"refresh_token": "Actualizar lo geton",
"valid_until": "Valid fins a",
"revoke_token": "Revocar",
"panelRadius": "Panèls",
"pause_on_unfocused": "Pausar la difusion quand longlet es pas seleccionat",
"presets": "Pre-enregistrats",
"profile_background": "Imatge de fons",
"profile_banner": "Bandièra del perfil",
"profile_tab": "Perfil",
"radii_help": "Configurar los caires arredondits de linterfàcia (en pixèls)",
"replies_in_timeline": "Responsas del flux",
"reply_link_preview": "Activar lapercebut en passar la mirga",
"reply_visibility_all": "Mostrar totas las responsas",
"reply_visibility_following": "Mostrar pas que las responsas que me son destinada a ieu o un utilizaire que seguissi",
"reply_visibility_self": "Mostrar pas que las responsas que me son destinadas",
"saving_err": "Error en enregistrant los paramètres",
"saving_ok": "Paramètres enregistrats",
"scope_copy": "Copiar lo nivèl de confidencialitat per las responsas (Totjorn aissí pels Messatges Dirèctes)",
"security_tab": "Seguretat",
"set_new_avatar": "Definir un nòu avatar",
"set_new_profile_background": "Definir un nòu fons de perfil",
"set_new_profile_banner": "Definir una nòva bandièra de perfil",
"settings": "Paramètres",
"subject_input_always_show": "Totjorn mostrar lo camp de subjècte",
"subject_line_behavior": "Copiar lo subjècte per las responsas",
"subject_line_email": "Coma los corrièls: \"re: subjècte\"",
"subject_line_mastodon": "Coma mastodon: copiar tal coma es",
"subject_line_noop": "Copiar pas",
"post_status_content_type": "Publicar lo tipe de contengut dels estatuts",
"status_content_type_plain": "Tèxte brut",
"stop_gifs": "Lançar los GIFs al subrevòl",
"streaming": "Activar lo cargament automatic dels novèls estatus en anar amont",
"text": "Tèxt",
"theme": "Tèma",
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
"theme_help": "Emplegatz los còdis de color hex (#rrggbb) per personalizar vòstre tèma de color.",
"tooltipRadius": "Astúcias/alèrtas",
"upload_a_photo": "Enviar una fotografia",
"user_settings": "Paramètres utilizaire",
"values": {
"false": "non",
"true": "òc"
@ -166,36 +247,67 @@
"up_to_date": "A jorn",
"no_retweet_hint": "La publicacion marcada coma pels seguidors solament o dirècte pòt pas èsser repetida"
},
"status": {
"reply_to": "Respondre à",
"replies_list": "Responsas:"
},
"user_card": {
"approve": "Validar",
"block": "Blocar",
"blocked": "Blocat !",
"deny": "Refusar",
"favorites": "Favorits",
"follow": "Seguir",
"follow_sent": "Demanda enviada!",
"follow_progress": "Demanda…",
"follow_again": "Tornar enviar la demanda?",
"follow_unfollow": "Quitar de seguir",
"followees": "Abonaments",
"followers": "Seguidors",
"following": "Seguit !",
"follows_you": "Vos sèc !",
"following": "Seguit !",
"follows_you": "Vos sèc !",
"its_you": "Sètz vos!",
"media": "Mèdia",
"mute": "Amagar",
"muted": "Amagat",
"per_day": "per jorn",
"remote_follow": "Seguir a distància",
"statuses": "Estatuts",
"approve": "Validar",
"deny": "Refusar"
"unblock": "Desblocar",
"unblock_progress": "Desblocatge...",
"block_progress": "Blocatge...",
"unmute": "Tornar mostrar",
"unmute_progress": "Afichatge...",
"mute_progress": "A amagar..."
},
"user_profile": {
"timeline_title": "Flux utilizaire"
},
"features_panel": {
"chat": "Discutida",
"gopher": "Gopher",
"media_proxy": "Servidor mandatari dels mèdias",
"scope_options": "Opcions d'encastres",
"text_limit": "Limit de tèxte",
"title": "Foncionalitats",
"who_to_follow": "Qui seguir"
"timeline_title": "Flux utilizaire",
"profile_does_not_exist": "Aqueste perfil existís pas.",
"profile_loading_error": "Una error ses producha en cargant aqueste perfil."
},
"who_to_follow": {
"more": "Mai",
"who_to_follow": "Qui seguir"
"who_to_follow": "Qual seguir"
},
"tool_tip": {
"media_upload": "Enviar un mèdia",
"repeat": "Repetir",
"reply": "Respondre",
"favorite": "aimar",
"user_settings": "Paramètres utilizaire"
},
"upload":{
"error": {
"base": "Mandadís fracassat.",
"file_too_big": "Fichièr tròp grand [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Tornatz ensajar mai tard"
},
"file_size_units": {
"B": "o",
"KiB": "Kio",
"MiB": "Mio",
"GiB": "Gio",
"TiB": "Tio"
}
}
}

View File

@ -86,6 +86,11 @@
"name_bio": "Imię i bio",
"new_password": "Nowe hasło",
"nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)",
"oauth_tokens": "Tokeny OAuth",
"token": "Token",
"refresh_token": "Odśwież token",
"valid_until": "Ważne do",
"revoke_token": "Odwołać",
"panelRadius": "Panele",
"presets": "Gotowe motywy",
"profile_background": "Tło profilu",

View File

@ -2,116 +2,425 @@
"chat": {
"title": "Chat"
},
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
"media_proxy": "Proxy de mídia",
"scope_options": "Opções de privacidade",
"text_limit": "Limite de caracteres",
"title": "Funções",
"who_to_follow": "Quem seguir"
},
"finder": {
"error_fetching_user": "Erro procurando usuário",
"error_fetching_user": "Erro ao procurar usuário",
"find_user": "Buscar usuário"
},
"general": {
"apply": "Aplicar",
"submit": "Enviar"
"submit": "Enviar",
"more": "Mais",
"generic_error": "Houve um erro",
"optional": "opcional"
},
"image_cropper": {
"crop_picture": "Cortar imagem",
"save": "Salvar",
"cancel": "Cancelar"
},
"login": {
"login": "Entrar",
"description": "Entrar com OAuth",
"logout": "Sair",
"password": "Senha",
"placeholder": "p.e. lain",
"register": "Registrar",
"username": "Usuário"
"username": "Usuário",
"hint": "Entre para participar da discussão"
},
"media_modal": {
"previous": "Anterior",
"next": "Próximo"
},
"nav": {
"about": "Sobre",
"back": "Voltar",
"chat": "Chat local",
"friend_requests": "Solicitações de seguidores",
"mentions": "Menções",
"dms": "Mensagens diretas",
"public_tl": "Linha do tempo pública",
"timeline": "Linha do tempo",
"twkn": "Toda a rede conhecida"
"twkn": "Toda a rede conhecida",
"user_search": "Busca de usuário",
"who_to_follow": "Quem seguir",
"preferences": "Preferências"
},
"notifications": {
"broken_favorite": "Status desconhecido, buscando...",
"favorited_you": "favoritou sua postagem",
"followed_you": "seguiu você",
"load_older": "Carregar notificações antigas",
"notifications": "Notificações",
"read": "Lido!",
"repeated_you": "repetiu sua postagem"
"repeated_you": "repetiu sua postagem",
"no_more_notifications": "Mais nenhuma notificação"
},
"post_status": {
"new_status": "Postar novo status",
"account_not_locked_warning": "Sua conta não está {0}. Qualquer pessoa pode te seguir para ver seus posts restritos.",
"account_not_locked_warning_link": "fechada",
"attachments_sensitive": "Marcar anexos como sensíveis",
"content_type": {
"plain_text": "Texto puro"
},
"content_warning": "Assunto (opcional)",
"default": "Acabei de chegar no Rio!",
"posting": "Publicando"
"direct_warning": "Este post será visível apenas para os usuários mencionados.",
"posting": "Publicando",
"scope": {
"direct": "Direto - Enviar somente aos usuários mencionados",
"private": "Apenas para seguidores - Enviar apenas para seguidores",
"public": "Público - Enviar a linhas do tempo públicas",
"unlisted": "Não listado - Não enviar a linhas do tempo públicas"
}
},
"registration": {
"bio": "Biografia",
"email": "Correio eletrônico",
"fullname": "Nome para exibição",
"password_confirm": "Confirmação de senha",
"registration": "Registro"
"registration": "Registro",
"token": "Código do convite",
"captcha": "CAPTCHA",
"new_captcha": "Clique na imagem para carregar um novo captcha",
"username_placeholder": "p. ex. lain",
"fullname_placeholder": "p. ex. Lain Iwakura",
"bio_placeholder": "e.g.\nOi, sou Lain\nSou uma garota que vive no subúrbio do Japão. Você deve me conhecer da Rede.",
"validations": {
"username_required": "não pode ser deixado em branco",
"fullname_required": "não pode ser deixado em branco",
"email_required": "não pode ser deixado em branco",
"password_required": "não pode ser deixado em branco",
"password_confirmation_required": "não pode ser deixado em branco",
"password_confirmation_match": "deve ser idêntica à senha"
}
},
"settings": {
"app_name": "Nome do aplicativo",
"attachmentRadius": "Anexos",
"attachments": "Anexos",
"autoload": "Habilitar carregamento automático quando a rolagem chegar ao fim.",
"avatar": "Avatar",
"avatarAltRadius": "Avatares (Notificações)",
"avatarRadius": "Avatares",
"background": "Plano de Fundo",
"background": "Pano de Fundo",
"bio": "Biografia",
"blocks_tab": "Blocos",
"btnRadius": "Botões",
"cBlue": "Azul (Responder, seguir)",
"cGreen": "Verde (Repetir)",
"cOrange": "Laranja (Favoritar)",
"cRed": "Vermelho (Cancelar)",
"change_password": "Mudar senha",
"change_password_error": "Houve um erro ao modificar sua senha.",
"changed_password": "Senha modificada com sucesso!",
"collapse_subject": "Esconder posts com assunto",
"composing": "Escrevendo",
"confirm_new_password": "Confirmar nova senha",
"current_avatar": "Seu avatar atual",
"current_password": "Sua senha atual",
"current_profile_banner": "Sua capa de perfil atual",
"data_import_export_tab": "Importação/exportação de dados",
"default_vis": "Opção de privacidade padrão",
"delete_account": "Deletar conta",
"delete_account_description": "Deletar sua conta e mensagens permanentemente.",
"delete_account_error": "Houve um problema ao deletar sua conta. Se ele persistir, por favor entre em contato com o/a administrador/a da instância.",
"delete_account_instructions": "Digite sua senha no campo abaixo para confirmar a exclusão da conta.",
"avatar_size_instruction": "O tamanho mínimo recomendado para imagens de avatar é 150x150 pixels.",
"export_theme": "Salvar predefinições",
"filtering": "Filtragem",
"filtering_explanation": "Todas as postagens contendo estas palavras serão silenciadas, uma por linha.",
"follow_import": "Importar seguidas",
"follow_export": "Exportar quem você segue",
"follow_export_button": "Exportar quem você segue para um arquivo CSV",
"follow_export_processing": "Processando. Em breve você receberá a solicitação de download do arquivo",
"follow_import": "Importar quem você segue",
"follow_import_error": "Erro ao importar seguidores",
"follows_imported": "Seguidores importados! O processamento pode demorar um pouco.",
"foreground": "Primeiro Plano",
"general": "Geral",
"hide_attachments_in_convo": "Ocultar anexos em conversas",
"hide_attachments_in_tl": "Ocultar anexos na linha do tempo.",
"max_thumbnails": "Número máximo de miniaturas por post",
"hide_isp": "Esconder painel específico da instância",
"preload_images": "Pré-carregar imagens",
"use_one_click_nsfw": "Abrir anexos sensíveis com um clique",
"hide_post_stats": "Esconder estatísticas de posts (p. ex. número de favoritos)",
"hide_user_stats": "Esconder estatísticas do usuário (p. ex. número de seguidores)",
"hide_filtered_statuses": "Esconder posts filtrados",
"import_followers_from_a_csv_file": "Importe seguidores a partir de um arquivo CSV",
"import_theme": "Carregar pré-definição",
"inputRadius": "Campos de entrada",
"checkboxRadius": "Checkboxes",
"instance_default": "(padrão: {value})",
"instance_default_simple": "(padrão)",
"interface": "Interface",
"interfaceLanguage": "Idioma da interface",
"invalid_theme_imported": "O arquivo selecionado não é um tema compatível com o Pleroma. Nenhuma mudança no tema foi feita.",
"limited_availability": "Indisponível para seu navegador",
"links": "Links",
"lock_account_description": "Restringir sua conta a seguidores aprovados",
"loop_video": "Repetir vídeos",
"loop_video_silent_only": "Repetir apenas vídeos sem som (como os \"gifs\" do Mastodon)",
"mutes_tab": "Silenciados",
"play_videos_in_modal": "Tocar vídeos diretamente no visualizador de mídia",
"use_contain_fit": "Não cortar o anexo na miniatura",
"name": "Nome",
"name_bio": "Nome & Biografia",
"nsfw_clickthrough": "Habilitar clique para ocultar anexos NSFW",
"new_password": "Nova senha",
"notification_visibility": "Tipos de notificação para mostrar",
"notification_visibility_follows": "Seguidos",
"notification_visibility_likes": "Favoritos",
"notification_visibility_mentions": "Menções",
"notification_visibility_repeats": "Repetições",
"no_rich_text_description": "Remover formatação de todos os posts",
"no_blocks": "Sem bloqueios",
"no_mutes": "Sem silenciados",
"hide_follows_description": "Não mostrar quem estou seguindo",
"hide_followers_description": "Não mostrar quem me segue",
"show_admin_badge": "Mostrar distintivo de Administrador em meu perfil",
"show_moderator_badge": "Mostrar título de Moderador em meu perfil",
"nsfw_clickthrough": "Habilitar clique para ocultar anexos sensíveis",
"oauth_tokens": "Token OAuth",
"token": "Token",
"refresh_token": "Atualizar Token",
"valid_until": "Válido até",
"revoke_token": "Revogar",
"panelRadius": "Paineis",
"pause_on_unfocused": "Parar transmissão quando a aba não estiver em primeiro plano",
"presets": "Predefinições",
"profile_background": "Plano de fundo de perfil",
"profile_background": "Pano de fundo de perfil",
"profile_banner": "Capa de perfil",
"profile_tab": "Perfil",
"radii_help": "Arredondar arestas da interface (em píxeis)",
"replies_in_timeline": "Respostas na linha do tempo",
"reply_link_preview": "Habilitar a pré-visualização de link de respostas ao passar o mouse.",
"reply_visibility_all": "Mostrar todas as respostas",
"reply_visibility_following": "Só mostrar respostas direcionadas a mim ou a usuários que sigo",
"reply_visibility_self": "Só mostrar respostas direcionadas a mim",
"saving_err": "Erro ao salvar configurações",
"saving_ok": "Configurações salvas",
"security_tab": "Segurança",
"scope_copy": "Copiar opções de privacidade ao responder (Mensagens diretas sempre copiam)",
"set_new_avatar": "Alterar avatar",
"set_new_profile_background": "Alterar o plano de fundo de perfil",
"set_new_profile_banner": "Alterar capa de perfil",
"settings": "Configurações",
"subject_input_always_show": "Sempre mostrar campo de assunto",
"subject_line_behavior": "Copiar assunto ao responder",
"subject_line_email": "Como em email: \"re: assunto\"",
"subject_line_mastodon": "Como o Mastodon: copiar como está",
"subject_line_noop": "Não copiar",
"post_status_content_type": "Postar tipo de conteúdo do status",
"status_content_type_plain": "Texto puro",
"stop_gifs": "Reproduzir GIFs ao passar o cursor em cima",
"streaming": "Habilitar o fluxo automático de postagens quando ao topo da página",
"text": "Texto",
"theme": "Tema",
"theme_help": "Use cores em código hexadecimal (#rrggbb) para personalizar seu esquema de cores.",
"tooltipRadius": "Dicass/alertas",
"user_settings": "Configurações de Usuário"
"theme_help_v2_1": "Você também pode sobrescrever as cores e opacidade de alguns componentes ao modificar o checkbox, use \"Limpar todos\" para limpar todas as modificações.",
"theme_help_v2_2": "Alguns ícones sob registros são indicadores de fundo/contraste de textos, passe por cima para informações detalhadas. Tenha ciência de que os indicadores de contraste não funcionam muito bem com transparência.",
"tooltipRadius": "Dicas/alertas",
"upload_a_photo": "Enviar uma foto",
"user_settings": "Configurações de Usuário",
"values": {
"false": "não",
"true": "sim"
},
"notifications": "Notifications",
"enable_web_push_notifications": "Habilitar notificações web push",
"style": {
"switcher": {
"keep_color": "Manter cores",
"keep_shadows": "Manter sombras",
"keep_opacity": "Manter opacidade",
"keep_roundness": "Manter arredondado",
"keep_fonts": "Manter fontes",
"save_load_hint": "Manter as opções preserva as opções atuais ao selecionar ou carregar temas; também salva as opções ao exportar um tempo. Quanto todos os campos estiverem desmarcados, tudo será salvo ao exportar o tema.",
"reset": "Voltar ao padrão",
"clear_all": "Limpar tudo",
"clear_opacity": "Limpar opacidade"
},
"common": {
"color": "Cor",
"opacity": "Opacidade",
"contrast": {
"hint": "A taxa de contraste é {ratio}, {level} {context}",
"level": {
"aa": "padrão Nível AA (mínimo)",
"aaa": "padrão Nível AAA (recomendado)",
"bad": "nenhum padrão de acessibilidade"
},
"context": {
"18pt": "para textos longos (18pt+)",
"text": "para texto"
}
}
},
"common_colors": {
"_tab_label": "Comum",
"main": "Cores Comuns",
"foreground_hint": "Configurações mais detalhadas na aba\"Avançado\"",
"rgbo": "Ícones, acentuação, distintivos"
},
"advanced_colors": {
"_tab_label": "Avançado",
"alert": "Fundo de alerta",
"alert_error": "Erro",
"badge": "Fundo do distintivo",
"badge_notification": "Notificação",
"panel_header": "Topo do painel",
"top_bar": "Barra do topo",
"borders": "Bordas",
"buttons": "Botões",
"inputs": "Caixas de entrada",
"faint_text": "Texto esmaecido"
},
"radii": {
"_tab_label": "Arredondado"
},
"shadows": {
"_tab_label": "Luz e sombra",
"component": "Componente",
"override": "Sobrescrever",
"shadow_id": "Sombra #{value}",
"blur": "Borrado",
"spread": "Difusão",
"inset": "Inserção",
"hint": "Para as sombras você também pode usar --variável como valor de cor para utilizar variáveis do CSS3. Tenha em mente que configurar a opacidade não será possível neste caso.",
"filter_hint": {
"always_drop_shadow": "Atenção, esta sombra sempre utiliza {0} quando compatível com o navegador.",
"drop_shadow_syntax": "{0} não é compatível com o parâmetro {1} e a palavra-chave {2}.",
"avatar_inset": "Tenha em mente que combinar as sombras de inserção e a não-inserção em avatares pode causar resultados inesperados em avatares transparentes.",
"spread_zero": "Sombras com uma difusão > 0 aparecerão como se fossem definidas como 0.",
"inset_classic": "Sombras de inserção utilizarão {0}"
},
"components": {
"panel": "Painel",
"panelHeader": "Topo do painel",
"topBar": "Barra do topo",
"avatar": "Avatar do usuário (na visualização do perfil)",
"avatarStatus": "Avatar do usuário (na exibição de posts)",
"popup": "Dicas e notificações",
"button": "Botão",
"buttonHover": "Botão (em cima)",
"buttonPressed": "Botão (pressionado)",
"buttonPressedHover": "Botão (pressionado+em cima)",
"input": "Campo de entrada"
}
},
"fonts": {
"_tab_label": "Fontes",
"help": "Selecionar fonte dos elementos da interface. Para fonte \"personalizada\" você deve entrar exatamente o nome da fonte no sistema.",
"components": {
"interface": "Interface",
"input": "Campo de entrada",
"post": "Postar texto",
"postCode": "Texto monoespaçado em post (formatação rica)"
},
"family": "Nome da fonte",
"size": "Tamanho (em px)",
"weight": "Peso",
"custom": "Personalizada"
},
"preview": {
"header": "Pré-visualizar",
"content": "Conteúdo",
"error": "Erro de exemplo",
"button": "Botão",
"text": "Vários {0} e {1}",
"mono": "conteúdo",
"input": "Acabei de chegar no Rio!",
"faint_link": "manual útil",
"fine_print": "Leia nosso {0} para não aprender nada!",
"header_faint": "Está ok!",
"checkbox": "Li os termos e condições",
"link": "um belo link"
}
}
},
"timeline": {
"collapse": "Esconder",
"conversation": "Conversa",
"error_fetching": "Erro buscando atualizações",
"error_fetching": "Erro ao buscar atualizações",
"load_older": "Carregar postagens antigas",
"no_retweet_hint": "Posts apenas para seguidores ou diretos não podem ser repetidos",
"repeated": "Repetido",
"show_new": "Mostrar novas",
"up_to_date": "Atualizado"
"up_to_date": "Atualizado",
"no_more_statuses": "Sem mais posts",
"no_statuses": "Sem posts"
},
"status": {
"reply_to": "Responder a",
"replies_list": "Respostas:"
},
"user_card": {
"approve": "Aprovar",
"block": "Bloquear",
"blocked": "Bloqueado!",
"deny": "Negar",
"favorites": "Favoritos",
"follow": "Seguir",
"follow_sent": "Pedido enviado!",
"follow_progress": "Enviando…",
"follow_again": "Enviar solicitação novamente?",
"follow_unfollow": "Deixar de seguir",
"followees": "Seguindo",
"followers": "Seguidores",
"following": "Seguindo!",
"follows_you": "Segue você!",
"its_you": "É você!",
"media": "Mídia",
"mute": "Silenciar",
"muted": "Silenciado",
"per_day": "por dia",
"remote_follow": "Seguidor Remoto",
"statuses": "Postagens"
"statuses": "Postagens",
"unblock": "Desbloquear",
"unblock_progress": "Desbloqueando...",
"block_progress": "Bloqueando...",
"unmute": "Retirar silêncio",
"unmute_progress": "Retirando silêncio...",
"mute_progress": "Silenciando..."
},
"user_profile": {
"timeline_title": "Linha do tempo do usuário"
"timeline_title": "Linha do tempo do usuário",
"profile_does_not_exist": "Desculpe, este perfil não existe.",
"profile_loading_error": "Desculpe, houve um erro ao carregar este perfil."
},
"who_to_follow": {
"more": "Mais",
"who_to_follow": "Quem seguir"
},
"tool_tip": {
"media_upload": "Envio de mídia",
"repeat": "Repetir",
"reply": "Responder",
"favorite": "Favoritar",
"user_settings": "Configurações do usuário"
},
"upload":{
"error": {
"base": "Falha no envio.",
"file_too_big": "Arquivo grande demais [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Tente novamente mais tarde"
},
"file_size_units": {
"B": "B",
"KiB": "KiB",
"MiB": "MiB",
"GiB": "GiB",
"TiB": "TiB"
}
}
}

View File

@ -132,6 +132,11 @@
"show_admin_badge": "Показывать значок администратора в моем профиле",
"show_moderator_badge": "Показывать значок модератора в моем профиле",
"nsfw_clickthrough": "Включить скрытие NSFW вложений",
"oauth_tokens": "OAuth токены",
"token": "Токен",
"refresh_token": "Рефреш токен",
"valid_until": "Годен до",
"revoke_token": "Удалить",
"panelRadius": "Панели",
"pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",
"presets": "Пресеты",

View File

@ -134,6 +134,11 @@
"notification_visibility_repeats": "转发",
"no_rich_text_description": "不显示富文本格式",
"nsfw_clickthrough": "将不和谐附件隐藏,点击才能打开",
"oauth_tokens": "OAuth令牌",
"token": "代币",
"refresh_token": "刷新令牌",
"valid_until": "有效期至",
"revoke_token": "撤消",
"panelRadius": "面板",
"pause_on_unfocused": "在离开页面时暂停时间线推送",
"presets": "预置",

View File

@ -84,12 +84,12 @@ export default function createPersistedState ({
setState(key, reducer(state, paths), storage)
.then(success => {
if (typeof success !== 'undefined') {
if (mutation.type === 'setOption') {
if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') {
store.dispatch('settingsSaved', { success })
}
}
}, error => {
if (mutation.type === 'setOption') {
if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') {
store.dispatch('settingsSaved', { error })
}
})

View File

@ -11,6 +11,7 @@ import configModule from './modules/config.js'
import chatModule from './modules/chat.js'
import oauthModule from './modules/oauth.js'
import mediaViewerModule from './modules/media_viewer.js'
import oauthTokensModule from './modules/oauth_tokens.js'
import VueTimeago from 'vue-timeago'
import VueI18n from 'vue-i18n'
@ -29,8 +30,9 @@ const currentLocale = (window.navigator.language || 'en').split('-')[0]
Vue.use(Vuex)
Vue.use(VueRouter)
Vue.use(VueTimeago, {
locale: currentLocale === 'ja' ? 'ja' : 'en',
locale: currentLocale === 'cs' ? 'cs' : currentLocale === 'ja' ? 'ja' : 'en',
locales: {
'cs': require('../static/timeago-cs.json'),
'en': require('../static/timeago-en.json'),
'ja': require('../static/timeago-ja.json')
}
@ -64,7 +66,8 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
config: configModule,
chat: chatModule,
oauth: oauthModule,
mediaViewer: mediaViewerModule
mediaViewer: mediaViewerModule,
oauthTokens: oauthTokensModule
},
plugins: [persistedState, pushNotifications],
strict: false // Socket modifies itself, let's ignore this for now.

View File

@ -8,6 +8,7 @@ const defaultState = {
collapseMessageWithSubject: undefined, // instance default
hideAttachments: false,
hideAttachmentsInConv: false,
maxThumbnails: 16,
hideNsfw: true,
preloadImage: true,
loopVideo: true,

View File

@ -0,0 +1,26 @@
const oauthTokens = {
state: {
tokens: []
},
actions: {
fetchTokens ({rootState, commit}) {
rootState.api.backendInteractor.fetchOAuthTokens().then((tokens) => {
commit('swapTokens', tokens)
})
},
revokeToken ({rootState, commit, state}, id) {
rootState.api.backendInteractor.revokeOAuthToken(id).then((response) => {
if (response.status === 201) {
commit('swapTokens', state.tokens.filter(token => token.id !== id))
}
})
}
},
mutations: {
swapTokens (state, tokens) {
state.tokens = tokens
}
}
}
export default oauthTokens

View File

@ -10,6 +10,7 @@ const emptyTl = (userId = 0) => ({
visibleStatusesObject: {},
newStatusCount: 0,
maxId: 0,
minId: 0,
minVisibleId: 0,
loading: false,
followers: [],
@ -117,16 +118,21 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
const timelineObject = state.timelines[timeline]
const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0
const older = timeline && maxNew < timelineObject.maxId
const minNew = statuses.length > 0 ? minBy(statuses, 'id').id : 0
const newer = timeline && maxNew > timelineObject.maxId && statuses.length > 0
const older = timeline && (minNew < timelineObject.minId || timelineObject.minId === 0) && statuses.length > 0
if (timeline && !noIdUpdate && statuses.length > 0 && !older) {
if (!noIdUpdate && newer) {
timelineObject.maxId = maxNew
}
if (!noIdUpdate && older) {
timelineObject.minId = minNew
}
// This makes sure that user timeline won't get data meant for other
// user. I.e. opening different user profiles makes request which could
// return data late after user already viewing different user profile
if (timeline === 'user' && timelineObject.userId !== userId) {
if ((timeline === 'user' || timeline === 'media') && timelineObject.userId !== userId) {
return
}
@ -255,12 +261,9 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
processor(status)
})
// Keep the visible statuses sorted
// Keep the visible statuses sorted
if (timeline) {
sortTimeline(timelineObject)
if ((older || timelineObject.minVisibleId <= 0) && statuses.length > 0) {
timelineObject.minVisibleId = minBy(statuses, 'id').id
}
}
}
@ -303,6 +306,8 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
setTimeout(notification.close.bind(notification), 5000)
}
}
} else if (notification.seen) {
state.notifications.idStore[notification.id].seen = true
}
})
}

View File

@ -72,19 +72,31 @@ export const mutations = {
},
// 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]
clearFriends (state, userId) {
const user = state.usersObject[userId]
if (!user) {
return
}
user.friends = []
user.followers = []
user.friendsPage = 0
},
clearFollowers (state, userId) {
const user = state.usersObject[userId]
if (!user) {
return
}
user.followers = []
user.followersPage = 0
},
addNewUsers (state, users) {
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
},
saveBlocks (state, blockIds) {
state.currentUser.blockIds = blockIds
},
saveMutes (state, muteIds) {
state.currentUser.muteIds = muteIds
},
setUserForStatus (state, status) {
status.user = state.usersObject[status.user.id]
},
@ -134,7 +146,39 @@ const users = {
getters,
actions: {
fetchUser (store, id) {
store.rootState.api.backendInteractor.fetchUser({ id })
return store.rootState.api.backendInteractor.fetchUser({ id })
.then((user) => store.commit('addNewUsers', [user]))
},
fetchBlocks (store) {
return store.rootState.api.backendInteractor.fetchBlocks()
.then((blocks) => {
store.commit('saveBlocks', map(blocks, 'id'))
store.commit('addNewUsers', blocks)
return blocks
})
},
blockUser (store, id) {
return store.rootState.api.backendInteractor.blockUser(id)
.then((user) => store.commit('addNewUsers', [user]))
},
unblockUser (store, id) {
return store.rootState.api.backendInteractor.unblockUser(id)
.then((user) => store.commit('addNewUsers', [user]))
},
fetchMutes (store) {
return store.rootState.api.backendInteractor.fetchMutes()
.then((mutedUsers) => {
each(mutedUsers, (user) => { user.muted = true })
store.commit('addNewUsers', mutedUsers)
store.commit('saveMutes', map(mutedUsers, 'id'))
})
},
muteUser (store, id) {
return store.state.api.backendInteractor.setUserMute({ id, muted: true })
.then((user) => store.commit('addNewUsers', [user]))
},
unmuteUser (store, id) {
return store.state.api.backendInteractor.setUserMute({ id, muted: false })
.then((user) => store.commit('addNewUsers', [user]))
},
addFriends ({ rootState, commit }, fetchBy) {
@ -151,20 +195,19 @@ const users = {
})
},
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()
})
})
const user = rootState.users.usersObject[fetchBy]
const page = user.followersPage || 1
return rootState.api.backendInteractor.fetchFollowers({ id: user.id, page })
.then((followers) => {
commit('addFollowers', { id: user.id, followers, page })
return followers
})
},
clearFriendsAndFollowers ({ commit }, userKey) {
commit('clearFriendsAndFollowers', userKey)
clearFriends ({ commit }, userId) {
commit('clearFriends', userId)
},
clearFollowers ({ commit }, userId) {
commit('clearFollowers', userId)
},
registerPushNotifications (store) {
const token = store.state.currentUser.credentials
@ -231,8 +274,14 @@ const users = {
store.commit('setToken', result.access_token)
store.dispatch('loginUser', result.access_token)
} else {
let data = await response.json()
let errors = humanizeErrors(JSON.parse(data.error))
const data = await response.json()
let errors = JSON.parse(data.error)
// replace ap_id with username
if (errors.ap_id) {
errors.username = errors.ap_id
delete errors.ap_id
}
errors = humanizeErrors(errors)
store.commit('signUpFailure', errors)
throw Error(errors)
}
@ -257,6 +306,8 @@ const users = {
const user = data
// user.credentials = userCredentials
user.credentials = accessToken
user.blockIds = []
user.muteIds = []
commit('setCurrentUser', user)
commit('addNewUsers', [user])
@ -273,11 +324,8 @@ const users = {
// Start getting fresh posts.
store.dispatch('startFetching', { timeline: 'friends' })
// Get user mutes and follower info
store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
each(mutedUsers, (user) => { user.muted = true })
store.commit('addNewUsers', mutedUsers)
})
// Get user mutes
store.dispatch('fetchMutes')
// Fetch our friends
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })

View File

@ -18,6 +18,7 @@ const MENTIONS_URL = '/api/statuses/mentions.json'
const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json'
const FOLLOWERS_URL = '/api/statuses/followers.json'
const FRIENDS_URL = '/api/statuses/friends.json'
const BLOCKS_URL = '/api/statuses/blocks.json'
const FOLLOWING_URL = '/api/friendships/create.json'
const UNFOLLOWING_URL = '/api/friendships/destroy.json'
const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json'
@ -46,6 +47,7 @@ const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
import { each, map } from 'lodash'
import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
import 'whatwg-fetch'
import { StatusCodeError } from '../errors/errors'
const oldfetch = window.fetch
@ -243,7 +245,15 @@ const denyUser = ({id, credentials}) => {
const fetchUser = ({id, credentials}) => {
let url = `${USER_URL}?user_id=${id}`
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
.then((response) => {
return new Promise((resolve, reject) => response.json()
.then((json) => {
if (!response.ok) {
return reject(new StatusCodeError(response.status, json, { url }, response))
}
return resolve(json)
}))
})
.then((data) => parseUser(data))
}
@ -519,6 +529,34 @@ const fetchMutes = ({credentials}) => {
}).then((data) => data.json())
}
const fetchBlocks = ({page, credentials}) => {
return fetch(BLOCKS_URL, {
headers: authHeaders(credentials)
}).then((data) => {
if (data.ok) {
return data.json()
}
throw new Error('Error fetching blocks', data)
})
}
const fetchOAuthTokens = ({credentials}) => {
const url = '/api/oauth_tokens.json'
return fetch(url, {
headers: authHeaders(credentials)
}).then((data) => data.json())
}
const revokeOAuthToken = ({id, credentials}) => {
const url = `/api/oauth_tokens/${id}`
return fetch(url, {
headers: authHeaders(credentials),
method: 'DELETE'
})
}
const suggestions = ({credentials}) => {
return fetch(SUGGESTIONS_URL, {
headers: authHeaders(credentials)
@ -560,6 +598,9 @@ const apiService = {
fetchAllFollowing,
setUserMute,
fetchMutes,
fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
register,
getCaptcha,
updateAvatar,

View File

@ -54,8 +54,8 @@ const backendInteractorService = (credentials) => {
return apiService.denyUser({credentials, id})
}
const startFetching = ({timeline, store, userId = false}) => {
return timelineFetcherService.startFetching({timeline, store, credentials, userId})
const startFetching = ({timeline, store, userId = false, tag}) => {
return timelineFetcherService.startFetching({timeline, store, credentials, userId, tag})
}
const setUserMute = ({id, muted = true}) => {
@ -63,7 +63,10 @@ const backendInteractorService = (credentials) => {
}
const fetchMutes = () => apiService.fetchMutes({credentials})
const fetchBlocks = (params) => apiService.fetchBlocks({credentials, ...params})
const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials})
const fetchOAuthTokens = () => apiService.fetchOAuthTokens({credentials})
const revokeOAuthToken = (id) => apiService.revokeOAuthToken({id, credentials})
const getCaptcha = () => apiService.getCaptcha()
const register = (params) => apiService.register(params)
@ -94,6 +97,9 @@ const backendInteractorService = (credentials) => {
startFetching,
setUserMute,
fetchMutes,
fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
register,
getCaptcha,
updateAvatar,

View File

@ -0,0 +1,10 @@
import isFunction from 'lodash/isFunction'
const getComponentOptions = (Component) => (isFunction(Component)) ? Component.options : Component
const getComponentProps = (Component) => getComponentOptions(Component).props
export {
getComponentOptions,
getComponentProps
}

View File

@ -117,6 +117,9 @@ export const parseUser = (data) => {
output.statuses_count = data.statuses_count
output.friends = []
output.followers = []
if (data.pleroma) {
output.follow_request_count = data.pleroma.follow_request_count
}
return output
}

View File

@ -0,0 +1,14 @@
export function StatusCodeError (statusCode, body, options, response) {
this.name = 'StatusCodeError'
this.statusCode = statusCode
this.message = statusCode + ' - ' + (JSON && JSON.stringify ? JSON.stringify(body) : body)
this.error = body // legacy attribute
this.options = options
this.response = response
if (Error.captureStackTrace) { // required for non-V8 environments
Error.captureStackTrace(this)
}
}
StatusCodeError.prototype = Object.create(Error.prototype)
StatusCodeError.prototype.constructor = StatusCodeError

View File

@ -0,0 +1,21 @@
import apiService from '../api/api.service.js'
const fetchAndUpdate = ({ store, credentials }) => {
return apiService.fetchFollowRequests({ credentials })
.then((requests) => {
store.commit('setFollowRequests', requests)
}, () => {})
.catch(() => {})
}
const startFetching = ({credentials, store}) => {
fetchAndUpdate({ credentials, store })
const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store })
return setInterval(boundFetchAndUpdate, 10000)
}
const followRequestFetcher = {
startFetching
}
export default followRequestFetcher

View File

@ -16,7 +16,17 @@ const fetchAndUpdate = ({store, credentials, older = false}) => {
args['until'] = timelineData.minId
}
} else {
args['since'] = timelineData.maxId
// load unread notifications repeadedly to provide consistency between browser tabs
const notifications = timelineData.data
const unread = notifications.filter(n => !n.seen).map(n => n.id)
if (!unread.length) {
args['since'] = timelineData.maxId
} else {
args['since'] = Math.min(...unread) - 1
if (timelineData.maxId !== Math.max(...unread)) {
args['until'] = Math.max(...unread, args['since'] + 20)
}
}
}
args['timeline'] = 'notifications'

View File

@ -21,7 +21,7 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
const timelineData = rootState.statuses.timelines[camelCase(timeline)]
if (older) {
args['until'] = until || timelineData.minVisibleId
args['until'] = until || timelineData.minId
} else {
args['since'] = timelineData.maxId
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

10
static/timeago-cs.json Normal file
View File

@ -0,0 +1,10 @@
[
"teď",
["%s s", "%s s"],
["%s min", "%s min"],
["%s h", "%s h"],
["%s d", "%s d"],
["%s týd", "%s týd"],
["%s měs", "%s měs"],
["%s r", "%s l"]
]

1169
yarn.lock

File diff suppressed because it is too large Load Diff