diff options
Diffstat (limited to 'src')
71 files changed, 2060 insertions, 666 deletions
diff --git a/src/App.scss b/src/App.scss index 7c6970c1..a0d1a804 100644 --- a/src/App.scss +++ b/src/App.scss @@ -628,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; @@ -661,6 +671,10 @@ nav { border-radius: var(--inputRadius, $fallback--inputRadius); } +.button-icon { + font-size: 1.2em; +} + @keyframes shakeError { 0% { transform: translateX(0); @@ -705,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; @@ -723,7 +727,7 @@ nav { .login-hint { text-align: center; - + @media all and (min-width: 801px) { display: none; } diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 7e972026..76affe2d 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -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; diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue index 4ede15e9..77fb0aa0 100644 --- a/src/components/basic_user_card/basic_user_card.vue +++ b/src/components/basic_user_card/basic_user_card.vue @@ -1,26 +1,22 @@ <template> <div class="user-card"> <router-link :to="userProfileLink(user)"> - <UserAvatar class="avatar" :compact="true" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/> + <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 class="user-card-primary-area"> - <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> - <router-link class='user-screen-name' :to="userProfileLink(user)"> - @{{user.screen_name}} - </router-link> - </div> + <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 class="user-card-secondary-area"> - <slot name="secondary-area"></slot> + <div> + <router-link class="user-card-screen-name" :to="userProfileLink(user)"> + @{{user.screen_name}} + </router-link> </div> + <slot></slot> </div> </div> </template> @@ -46,30 +42,21 @@ margin-left: 0.7em; text-align: left; flex: 1; - display: flex; - align-items: flex-start; - justify-content: space-between; + min-width: 0; } - &-primary-area { - flex: 1; - .user-name { - img { - object-fit: contain; - height: 16px; - width: 16px; - vertical-align: middle; - } + &-user-name { + img { + object-fit: contain; + height: 16px; + width: 16px; + vertical-align: middle; } } - &-secondary-area { - flex: none; - } - &-expanded-content { flex: 1; - margin: 0.2em 0 0 0.7em; + margin-left: 0.7em; border-radius: $fallback--panelRadius; border-radius: var(--panelRadius, $fallback--panelRadius); border-style: solid; diff --git a/src/components/block_card/block_card.vue b/src/components/block_card/block_card.vue index ed7fe30b..8eb56e25 100644 --- a/src/components/block_card/block_card.vue +++ b/src/components/block_card/block_card.vue @@ -1,6 +1,6 @@ <template> <basic-user-card :user="user"> - <template slot="secondary-area"> + <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') }} @@ -17,8 +17,18 @@ {{ $t('user_card.block') }} </template> </button> - </template> + </div> </basic-user-card> </template> -<script src="./block_card.js"></script>
\ No newline at end of file +<script src="./block_card.js"></script> + +<style lang="scss"> +.block-card-content-container { + margin-top: 0.5em; + text-align: right; + button { + width: 10em; + } +} +</style> diff --git a/src/components/follow_card/follow_card.js b/src/components/follow_card/follow_card.js new file mode 100644 index 00000000..425c9c3e --- /dev/null +++ b/src/components/follow_card/follow_card.js @@ -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 diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue new file mode 100644 index 00000000..6cb064eb --- /dev/null +++ b/src/components/follow_card/follow_card.vue @@ -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> diff --git a/src/components/follow_list/follow_list.js b/src/components/follow_list/follow_list.js deleted file mode 100644 index 9777c87e..00000000 --- a/src/components/follow_list/follow_list.js +++ /dev/null @@ -1,68 +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() - } - } - }, - watch: { - 'user': 'fetchEntries' - }, - components: { - UserCard - } -} - -export default FollowList diff --git a/src/components/follow_list/follow_list.vue b/src/components/follow_list/follow_list.vue deleted file mode 100644 index 27102edf..00000000 --- a/src/components/follow_list/follow_list.vue +++ /dev/null @@ -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> diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js new file mode 100644 index 00000000..1a00a1c1 --- /dev/null +++ b/src/components/follow_request_card/follow_request_card.js @@ -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 diff --git a/src/components/follow_request_card/follow_request_card.vue b/src/components/follow_request_card/follow_request_card.vue new file mode 100644 index 00000000..4a3bbba4 --- /dev/null +++ b/src/components/follow_request_card/follow_request_card.vue @@ -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> diff --git a/src/components/follow_requests/follow_requests.js b/src/components/follow_requests/follow_requests.js index 11a228aa..704a76c6 100644 --- a/src/components/follow_requests/follow_requests.js +++ b/src/components/follow_requests/follow_requests.js @@ -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) }) - } } } diff --git a/src/components/follow_requests/follow_requests.vue b/src/components/follow_requests/follow_requests.vue index 87dc4194..b83c2d68 100644 --- a/src/components/follow_requests/follow_requests.vue +++ b/src/components/follow_requests/follow_requests.vue @@ -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> diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue index 3f90caa9..ea525c95 100644 --- a/src/components/gallery/gallery.vue +++ b/src/components/gallery/gallery.vue @@ -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 { diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js index 990c0370..49d51846 100644 --- a/src/components/image_cropper/image_cropper.js +++ b/src/components/image_cropper/image_cropper.js @@ -67,7 +67,7 @@ const ImageCropper = { submit () { this.submitting = true this.avatarUploadError = null - this.submitHandler(this.cropper, this.filename) + this.submitHandler(this.cropper, this.file) .then(() => this.destroy()) .catch((err) => { this.submitError = err @@ -88,14 +88,14 @@ const ImageCropper = { 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(fileInput.files[0]) - this.filename = fileInput.files[0].name || 'unknown' - this.$emit('changed', fileInput.files[0], reader) + reader.readAsDataURL(this.file) + this.$emit('changed', this.file, reader) } }, clearError () { diff --git a/src/components/link-preview/link-preview.vue b/src/components/link-preview/link-preview.vue index e4a247c5..64b1a58b 100644 --- a/src/components/link-preview/link-preview.vue +++ b/src/components/link-preview/link-preview.vue @@ -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; diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index ea5d7ea4..aa3f7605 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -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 } } } diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 1a269adf..7a7212fb 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -20,8 +20,8 @@ <li v-if='currentUser && currentUser.locked'> <router-link :to="{ name: 'friend-requests' }"> {{ $t("nav.friend_requests")}} - <span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count"> - {{currentUser.follow_request_count}} + <span v-if='followRequestCount > 0' class="badge follow-request-count"> + {{followRequestCount}} </span> </router-link> </li> diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index a0a55cba..87925cfc 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -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)"> diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index b3364afc..2240c10a 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -126,7 +126,7 @@ } .timeago { - font-size: 12px; + margin-right: .2em; } .icon-retweet.lit { diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 5085570b..40d24b97 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -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"> diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index 23c1acdb..6e2dff7b 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -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 }) } } } diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index f5e00995..5041b3a3 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -137,6 +137,10 @@ <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> </li> @@ -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> @@ -316,6 +320,10 @@ min-width: 10em; padding: 0 2em; } + + .number-input { + max-width: 6em; + } } .select-multiple { display: flex; diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 40ffa1dd..b5c49059 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -32,6 +32,9 @@ const SideDrawer = { }, sitename () { return this.$store.state.instance.name + }, + followRequestCount () { + return this.$store.state.api.followRequests.length } }, methods: { diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 8eca7b8c..6996380d 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -45,8 +45,8 @@ <li v-if="currentUser && currentUser.locked" @click="toggleDrawer"> <router-link to='/friend-requests'> {{ $t("nav.friend_requests") }} - <span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count"> - {{currentUser.follow_request_count}} + <span v-if='followRequestCount > 0' class="badge follow-request-count"> + {{followRequestCount}} </span> </router-link> diff --git a/src/components/status/status.js b/src/components/status/status.js index 0273a5be..fbbca6c4 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -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: { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 3fc5b486..4dd20362 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -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}} </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; } - .media-heading { - flex-wrap: nowrap; - line-height: 18px; + .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-left { + .media-heading { 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; } - .replies { - line-height: 16px; + + .reply-to { + display: flex; } - .reply-link { - margin-right: 0.2em; + + .reply-to-text { + overflow: hidden; + text-overflow: ellipsis; + margin: 0 0.4em 0 0.2em; } - } - .media-heading-right { - display: inline-flex; - flex-shrink: 0; - flex-wrap: nowrap; - margin-left: .25em; - align-self: baseline; + .replies-separator { + margin-left: 0.4em; + } - .timeago { - margin-right: 0.2em; + .replies { + 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; } } diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 25c553cb..655bfb3f 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -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 = { @@ -44,8 +43,7 @@ const Timeline = { }, components: { Status, - StatusOrConversation, - UserCard + StatusOrConversation }, created () { const store = this.$store @@ -70,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 }) @@ -101,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 } }) diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js deleted file mode 100644 index 28e22f09..00000000 --- a/src/components/user_card/user_card.js +++ /dev/null @@ -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 diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue deleted file mode 100644 index ce4edb3c..00000000 --- a/src/components/user_card/user_card.vue +++ /dev/null @@ -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> diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue index a3d24eb1..702c3385 100644 --- a/src/components/user_card_content/user_card_content.vue +++ b/src/components/user_card_content/user_card_content.vue @@ -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> diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 37179ce1..cdf1cee9 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -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 } } diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index 09fb93de..8090efa5 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -6,7 +6,7 @@ :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" @@ -18,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')" @@ -55,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> diff --git a/src/components/user_search/user_search.js b/src/components/user_search/user_search.js index fe67b2ad..55040826 100644 --- a/src/components/user_search/user_search.js +++ b/src/components/user_search/user_search.js @@ -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' diff --git a/src/components/user_search/user_search.vue b/src/components/user_search/user_search.vue index b39e10f4..1269eea6 100644 --- a/src/components/user_search/user_search.vue +++ b/src/components/user_search/user_search.vue @@ -13,7 +13,7 @@ <i class="icon-spin3 animate-spin"/> </div> <div v-else 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> diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index 06e72112..c0ab759c 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -1,7 +1,6 @@ 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' @@ -62,6 +61,9 @@ const UserSettings = { activeTab: 'profile' } }, + created () { + this.$store.dispatch('fetchTokens') + }, components: { StyleSwitcher, TabSwitcher, @@ -89,6 +91,15 @@ const UserSettings = { }, 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: { @@ -146,8 +157,8 @@ const UserSettings = { } reader.readAsDataURL(file) }, - submitAvatar (cropper) { - const img = cropper.getCroppedCanvas().toDataURL('image/jpeg') + 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]) @@ -308,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) + } } } } diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index 983cbda0..a1123638 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -122,6 +122,30 @@ </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> <div v-if="deletingAccount"> @@ -213,5 +237,17 @@ border-radius: $fallback--avatarRadius; border-radius: var(--avatarRadius, $fallback--avatarRadius); } + + .oauth-tokens { + width: 100%; + + th { + text-align: left; + } + + .actions { + text-align: right; + } + } } </style> diff --git a/src/components/who_to_follow/who_to_follow.js b/src/components/who_to_follow/who_to_follow.js index 82098fc2..be0b8827 100644 --- a/src/components/who_to_follow/who_to_follow.js +++ b/src/components/who_to_follow/who_to_follow.js @@ -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 { diff --git a/src/components/who_to_follow/who_to_follow.vue b/src/components/who_to_follow/who_to_follow.vue index df2e03c8..1630f5ac 100644 --- a/src/components/who_to_follow/who_to_follow.vue +++ b/src/components/who_to_follow/who_to_follow.vue @@ -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> diff --git a/src/hocs/with_load_more/with_load_more.js b/src/hocs/with_load_more/with_load_more.js index e862a39b..74979b87 100644 --- a/src/hocs/with_load_more/with_load_more.js +++ b/src/hocs/with_load_more/with_load_more.js @@ -1,15 +1,17 @@ import Vue from 'vue' -import filter from 'lodash/filter' 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 - childPropName = 'entries' // name of the prop to be passed into the wrapped component + 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 = WrappedComponent.props || [] - const props = filter(originalProps, v => v !== 'entries') + const originalProps = Object.keys(getComponentProps(WrappedComponent)) + const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames) return Vue.component('withLoadMore', { render (createElement) { @@ -56,6 +58,7 @@ const withLoadMore = ({ }, destroyed () { window.removeEventListener('scroll', this.scrollLoad) + destroy && destroy(this.$props, this.$store) }, methods: { fetchEntries () { diff --git a/src/hocs/with_subscription/with_subscription.js b/src/hocs/with_subscription/with_subscription.js index 1ac67cba..679409cf 100644 --- a/src/hocs/with_subscription/with_subscription.js +++ b/src/hocs/with_subscription/with_subscription.js @@ -1,16 +1,16 @@ import Vue from 'vue' -import reject from 'lodash/reject' import isEmpty from 'lodash/isEmpty' -import omit from 'lodash/omit' +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 + 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 = WrappedComponent.props || [] - const props = reject(originalProps, v => v === 'content') + const originalProps = Object.keys(getComponentProps(WrappedComponent)) + const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames) return Vue.component('withSubscription', { props: [ @@ -21,7 +21,7 @@ const withSubscription = ({ if (!this.error && !this.loading) { const props = { props: { - ...omit(this.$props, 'refresh'), + ...this.$props, [childPropName]: this.fetchedData }, on: this.$listeners, diff --git a/src/i18n/ar.json b/src/i18n/ar.json index ac7d0f1a..242dab78 100644 --- a/src/i18n/ar.json +++ b/src/i18n/ar.json @@ -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": "النماذج", diff --git a/src/i18n/ca.json b/src/i18n/ca.json index fa517e22..d2f285df 100644 --- a/src/i18n/ca.json +++ b/src/i18n/ca.json @@ -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", diff --git a/src/i18n/cs.json b/src/i18n/cs.json new file mode 100644 index 00000000..6326032c --- /dev/null +++ b/src/i18n/cs.json @@ -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" + } + } +} diff --git a/src/i18n/de.json b/src/i18n/de.json index d0bfba38..07d44348 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -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", diff --git a/src/i18n/en.json b/src/i18n/en.json index 64753f1d..c5a4a90d 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -106,6 +106,7 @@ } }, "settings": { + "app_name": "App name", "attachmentRadius": "Attachments", "attachments": "Attachments", "autoload": "Enable automatic loading when scrolled to the bottom", @@ -149,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", @@ -188,6 +190,11 @@ "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", @@ -353,6 +360,10 @@ "no_more_statuses": "No more statuses", "no_statuses": "No statuses" }, + "status": { + "reply_to": "Reply to", + "replies_list": "Replies:" + }, "user_card": { "approve": "Approve", "block": "Block", @@ -383,7 +394,9 @@ "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", diff --git a/src/i18n/eo.json b/src/i18n/eo.json index ed4b50e3..2438b4d5 100644 --- a/src/i18n/eo.json +++ b/src/i18n/eo.json @@ -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" + } } } diff --git a/src/i18n/es.json b/src/i18n/es.json index d14e7a31..167e8c42 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -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", diff --git a/src/i18n/fi.json b/src/i18n/fi.json index 5a0c1ea8..4f0ffb4b 100644 --- a/src/i18n/fi.json +++ b/src/i18n/fi.json @@ -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ä", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 129b7d7c..1209556a 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -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", diff --git a/src/i18n/ga.json b/src/i18n/ga.json index 64461202..5be9297a 100644 --- a/src/i18n/ga.json +++ b/src/i18n/ga.json @@ -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", diff --git a/src/i18n/he.json b/src/i18n/he.json index 99ae9551..213e6170 100644 --- a/src/i18n/he.json +++ b/src/i18n/he.json @@ -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": "ערכים קבועים מראש", diff --git a/src/i18n/it.json b/src/i18n/it.json index 8f69e7c1..385d21aa 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -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", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 7849aa20..b51fa7fd 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -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": "プリセット", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index f9e4dfa3..336e464f 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -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": "프리셋", diff --git a/src/i18n/messages.js b/src/i18n/messages.js index 1adadc32..ab697948 100644 --- a/src/i18n/messages.js +++ b/src/i18n/messages.js @@ -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'), diff --git a/src/i18n/nb.json b/src/i18n/nb.json index 0f4dca58..39e054f7 100644 --- a/src/i18n/nb.json +++ b/src/i18n/nb.json @@ -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", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index bb388a90..799e22b9 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -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", diff --git a/src/i18n/oc.json b/src/i18n/oc.json index 2ce666c6..ef32f83b 100644 --- a/src/i18n/oc.json +++ b/src/i18n/oc.json @@ -2,50 +2,81 @@ "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 d’un utilizaire", + "error_fetching_user": "Error pendent la cèrca d’un utilizaire", "find_user": "Cercar un utilizaire" }, "general": { "apply": "Aplicar", - "submit": "Mandar" + "submit": "Mandar", + "more": "Mai", + "generic_error": "Una error s’es producha", + "optional": "opcional" + }, + "image_cropper": { + "crop_picture": "Talhar l’imatge", + "save": "Salvar", + "cancel": "Anullar" }, "login": { "login": "Connexion", + "description": "Connexion via OAuth", "logout": "Desconnexion", "password": "Senhal", "placeholder": "e.g. lain", "register": "Se marcar", - "username": "Nom d’utilizaire" + "username": "Nom d’utilizaire", + "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 d’actualitat", "twkn": "Lo malhum conegut", - "friend_requests": "Demandas d'abonament" + "user_search": "Cèrca d’utilizaires", + "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 d’estatuts 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 l’imatge 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 d’animes 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 l’aplicacion", "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 s’es 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 s’es producha en suprimir lo compte. S’aquò ten d’arribar mercés de contactar vòstre administrador d’instà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 d’avatar 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 d’un 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 l’interfàcia (en pixèls)", - "reply_link_preview": "Activar l’apercebut en passar la mirga", - "set_new_avatar": "Cambiar l’avatar", - "set_new_profile_background": "Cambiar l’imatge 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 l’utilizaire (ex. lo nombre de seguidors)", + "hide_filtered_statuses": "Amagar los estatuts filtrats", + "import_followers_from_a_csv_file": "Importar los seguidors d’un 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 l’interfà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 d’amagat", + "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 l’onglet 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 l’interfàcia (en pixèls)", "replies_in_timeline": "Responsas del flux", + "reply_link_preview": "Activar l’apercebut 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 s’es 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" + } } }
\ No newline at end of file diff --git a/src/i18n/pl.json b/src/i18n/pl.json index a3952d4f..2e1d7488 100644 --- a/src/i18n/pl.json +++ b/src/i18n/pl.json @@ -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", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 544eacdf..39ff6c63 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -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" + } } } diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 4b0bd4b4..6799cc96 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -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": "Пресеты", diff --git a/src/i18n/zh.json b/src/i18n/zh.json index 7ad23c57..089a98e2 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -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": "预置", diff --git a/src/main.js b/src/main.js index adeb0550..a3265e3a 100644 --- a/src/main.js +++ b/src/main.js @@ -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. diff --git a/src/modules/config.js b/src/modules/config.js index 71f71376..1c30c203 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -8,6 +8,7 @@ const defaultState = { collapseMessageWithSubject: undefined, // instance default hideAttachments: false, hideAttachmentsInConv: false, + maxThumbnails: 16, hideNsfw: true, preloadImage: true, loopVideo: true, diff --git a/src/modules/oauth_tokens.js b/src/modules/oauth_tokens.js new file mode 100644 index 00000000..00ac1431 --- /dev/null +++ b/src/modules/oauth_tokens.js @@ -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 diff --git a/src/modules/users.js b/src/modules/users.js index 77df7168..093af497 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -72,14 +72,20 @@ 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) { @@ -140,7 +146,7 @@ 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) { @@ -189,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 + }) + }, + clearFriends ({ commit }, userId) { + commit('clearFriends', userId) }, - clearFriendsAndFollowers ({ commit }, userKey) { - commit('clearFriendsAndFollowers', userKey) + clearFollowers ({ commit }, userId) { + commit('clearFollowers', userId) }, registerPushNotifications (store) { const token = store.state.currentUser.credentials diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 3d2e8823..2de87026 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -47,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 @@ -244,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)) } @@ -531,6 +540,23 @@ const fetchBlocks = ({page, credentials}) => { }) } +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) @@ -573,6 +599,8 @@ const apiService = { setUserMute, fetchMutes, fetchBlocks, + fetchOAuthTokens, + revokeOAuthToken, register, getCaptcha, updateAvatar, diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 43c914d9..7e972d7b 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -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}) => { @@ -65,6 +65,8 @@ 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) @@ -96,6 +98,8 @@ const backendInteractorService = (credentials) => { setUserMute, fetchMutes, fetchBlocks, + fetchOAuthTokens, + revokeOAuthToken, register, getCaptcha, updateAvatar, diff --git a/src/services/component_utils/component_utils.js b/src/services/component_utils/component_utils.js new file mode 100644 index 00000000..77ea14a1 --- /dev/null +++ b/src/services/component_utils/component_utils.js @@ -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 +} diff --git a/src/services/errors/errors.js b/src/services/errors/errors.js new file mode 100644 index 00000000..548f3c68 --- /dev/null +++ b/src/services/errors/errors.js @@ -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 diff --git a/src/services/follow_request_fetcher/follow_request_fetcher.service.js b/src/services/follow_request_fetcher/follow_request_fetcher.service.js new file mode 100644 index 00000000..125ff3e1 --- /dev/null +++ b/src/services/follow_request_fetcher/follow_request_fetcher.service.js @@ -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 |
