diff options
Diffstat (limited to 'src')
79 files changed, 2775 insertions, 512 deletions
@@ -8,9 +8,10 @@ import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_pan import ChatPanel from './components/chat_panel/chat_panel.vue' import MediaModal from './components/media_modal/media_modal.vue' import SideDrawer from './components/side_drawer/side_drawer.vue' -import MobilePostStatusModal from './components/mobile_post_status_modal/mobile_post_status_modal.vue' +import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue' import MobileNav from './components/mobile_nav/mobile_nav.vue' import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue' +import PostStatusModal from './components/post_status_modal/post_status_modal.vue' import { windowWidth } from './services/window_utils/window_utils' export default { @@ -26,9 +27,10 @@ export default { ChatPanel, MediaModal, SideDrawer, - MobilePostStatusModal, + MobilePostStatusButton, MobileNav, - UserReportingModal + UserReportingModal, + PostStatusModal }, data: () => ({ mobileActivePanel: 'timeline', @@ -89,7 +91,11 @@ export default { sitename () { return this.$store.state.instance.name }, chat () { return this.$store.state.chat.channel.state === 'joined' }, suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, - showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel }, + showInstanceSpecificPanel () { + return this.$store.state.instance.showInstanceSpecificPanel && + !this.$store.state.config.hideISP && + this.$store.state.instance.instanceSpecificPanelContent + }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, isMobileLayout () { return this.$store.state.interface.mobileLayout } }, diff --git a/src/App.scss b/src/App.scss index 1299e05d..ea7b54e8 100644 --- a/src/App.scss +++ b/src/App.scss @@ -16,7 +16,7 @@ background-position: 0 50%; } -i { +i[class^='icon-'] { user-select: none; } @@ -49,6 +49,10 @@ body { overflow-x: hidden; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + + &.hidden { + display: none; + } } a { @@ -549,23 +553,6 @@ nav { color: var(--faint, $fallback--faint); box-shadow: 0px 0px 4px rgba(0,0,0,.6); box-shadow: var(--topBarShadow); - - .back-button { - display: block; - max-width: 99px; - transition-property: opacity, max-width; - transition-duration: 300ms; - transition-timing-function: ease-out; - - i { - margin: 0 1em; - } - - &.hidden { - opacity: 0; - max-width: 5px; - } - } } .fade-enter-active, .fade-leave-active { @@ -601,12 +588,6 @@ nav { overflow-y: scroll; } - nav { - .back-button { - display: none; - } - } - .sidebar-bounds { overflow: hidden; max-height: 100vh; diff --git a/src/App.vue b/src/App.vue index bf6e62e2..f1086e60 100644 --- a/src/App.vue +++ b/src/App.vue @@ -107,7 +107,9 @@ :floating="true" class="floating-chat mobile-hidden" /> + <MobilePostStatusButton /> <UserReportingModal /> + <PostStatusModal /> <portal-target name="modal" /> </div> </template> diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 3799359f..5cb2acba 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -109,12 +109,6 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { copyInstanceOption('noAttachmentLinks') copyInstanceOption('showFeaturesPanel') - if ((config.chatDisabled)) { - store.dispatch('disableChat') - } else { - store.dispatch('initializeSocket') - } - return store.dispatch('setTheme', config['theme']) } @@ -252,6 +246,7 @@ const getNodeInfo = async ({ store }) => { store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) + store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames }) store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats }) diff --git a/src/boot/routes.js b/src/boot/routes.js index 7dc4b2a5..cd02711c 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -9,6 +9,7 @@ import UserProfile from 'components/user_profile/user_profile.vue' import Search from 'components/search/search.vue' import Settings from 'components/settings/settings.vue' import Registration from 'components/registration/registration.vue' +import PasswordReset from 'components/password_reset/password_reset.vue' import UserSettings from 'components/user_settings/user_settings.vue' import FollowRequests from 'components/follow_requests/follow_requests.vue' import OAuthCallback from 'components/oauth_callback/oauth_callback.vue' @@ -46,6 +47,7 @@ export default (store) => { { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, { name: 'settings', path: '/settings', component: Settings }, { name: 'registration', path: '/registration', component: Registration }, + { name: 'password-reset', path: '/password-reset', component: PasswordReset }, { name: 'registration-token', path: '/registration/:token', component: Registration }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute }, diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index ec326c45..af16e302 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -190,6 +190,7 @@ .video { width: 100%; + height: 100%; } .play-icon { @@ -286,7 +287,7 @@ } img { - image-orientation: from-image; + image-orientation: from-image; // NOTE: only FF supports this } } } diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue index 568e9359..8a02174e 100644 --- a/src/components/basic_user_card/basic_user_card.vue +++ b/src/components/basic_user_card/basic_user_card.vue @@ -87,6 +87,7 @@ &-expanded-content { flex: 1; margin-left: 0.7em; + min-width: 0; } } </style> diff --git a/src/components/conversation-page/conversation-page.js b/src/components/conversation-page/conversation-page.js index 1da70ce9..8f996be1 100644 --- a/src/components/conversation-page/conversation-page.js +++ b/src/components/conversation-page/conversation-page.js @@ -5,12 +5,8 @@ const conversationPage = { Conversation }, computed: { - statusoid () { - const id = this.$route.params.id - const statuses = this.$store.state.statuses.allStatusesObject - const status = statuses[id] - - return status + statusId () { + return this.$route.params.id } } } diff --git a/src/components/conversation-page/conversation-page.vue b/src/components/conversation-page/conversation-page.vue index 532f785c..8cc0a55f 100644 --- a/src/components/conversation-page/conversation-page.vue +++ b/src/components/conversation-page/conversation-page.vue @@ -2,7 +2,7 @@ <conversation :collapsable="false" is-page="true" - :statusoid="statusoid" + :status-id="statusId" /> </template> diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index a2b3aeab..72ee9c39 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -1,4 +1,4 @@ -import { reduce, filter, findIndex, clone } from 'lodash' +import { reduce, filter, findIndex, clone, get } from 'lodash' import Status from '../status/status.vue' const sortById = (a, b) => { @@ -39,10 +39,11 @@ const conversation = { } }, props: [ - 'statusoid', + 'statusId', 'collapsable', 'isPage', - 'showPinned' + 'pinnedStatusIdsObject', + 'inProfile' ], created () { if (this.isPage) { @@ -51,21 +52,17 @@ const conversation = { }, computed: { status () { - return this.statusoid + return this.$store.state.statuses.allStatusesObject[this.statusId] }, - statusId () { - if (this.statusoid.retweeted_status) { - return this.statusoid.retweeted_status.id + originalStatusId () { + if (this.status.retweeted_status) { + return this.status.retweeted_status.id } else { - return this.statusoid.id + return this.statusId } }, conversationId () { - if (this.statusoid.retweeted_status) { - return this.statusoid.retweeted_status.statusnet_conversation_id - } else { - return this.statusoid.statusnet_conversation_id - } + return this.getConversationId(this.statusId) }, conversation () { if (!this.status) { @@ -77,7 +74,7 @@ const conversation = { } const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId]) - const statusIndex = findIndex(conversation, { id: this.statusId }) + const statusIndex = findIndex(conversation, { id: this.originalStatusId }) if (statusIndex !== -1) { conversation[statusIndex] = this.status } @@ -110,7 +107,15 @@ const conversation = { Status }, watch: { - '$route': 'fetchConversation', + statusId (newVal, oldVal) { + const newConversationId = this.getConversationId(newVal) + const oldConversationId = this.getConversationId(oldVal) + if (newConversationId && oldConversationId && newConversationId === oldConversationId) { + this.setHighlight(this.originalStatusId) + } else { + this.fetchConversation() + } + }, expanded (value) { if (value) { this.fetchConversation() @@ -120,24 +125,25 @@ const conversation = { methods: { fetchConversation () { if (this.status) { - this.$store.state.api.backendInteractor.fetchConversation({ id: this.status.id }) + this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusId }) .then(({ ancestors, descendants }) => { this.$store.dispatch('addNewStatuses', { statuses: ancestors }) this.$store.dispatch('addNewStatuses', { statuses: descendants }) + this.setHighlight(this.originalStatusId) }) - .then(() => this.setHighlight(this.statusId)) } else { - const id = this.$route.params.id - this.$store.state.api.backendInteractor.fetchStatus({ id }) - .then((status) => this.$store.dispatch('addNewStatuses', { statuses: [status] })) - .then(() => this.fetchConversation()) + this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId }) + .then((status) => { + this.$store.dispatch('addNewStatuses', { statuses: [status] }) + this.fetchConversation() + }) } }, getReplies (id) { return this.replies[id] || [] }, focused (id) { - return (this.isExpanded) && id === this.status.id + return (this.isExpanded) && id === this.statusId }, setHighlight (id) { if (!id) return @@ -149,9 +155,10 @@ const conversation = { }, toggleExpanded () { this.expanded = !this.expanded - if (!this.expanded) { - this.setHighlight(null) - } + }, + getConversationId (statusId) { + const status = this.$store.state.statuses.allStatusesObject[statusId] + return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id')) } } } diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 5a900607..0f1de55f 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -21,11 +21,12 @@ :inline-expanded="collapsable && isExpanded" :statusoid="status" :expandable="!isExpanded" - :show-pinned="showPinned" + :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]" :focused="focused(status.id)" :in-conversation="isExpanded" :highlight="getHighlight()" :replies="getReplies(status.id)" + :in-profile="inProfile" class="status-fadein panel-body" @goto="setHighlight" @toggleExpanded="toggleExpanded" diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js index 2ec72729..5ac73e97 100644 --- a/src/components/extra_buttons/extra_buttons.js +++ b/src/components/extra_buttons/extra_buttons.js @@ -16,6 +16,16 @@ const ExtraButtons = { this.$store.dispatch('unpinStatus', this.status.id) .then(() => this.$emit('onSuccess')) .catch(err => this.$emit('onError', err.error.error)) + }, + muteConversation () { + this.$store.dispatch('muteConversation', this.status.id) + .then(() => this.$emit('onSuccess')) + .catch(err => this.$emit('onError', err.error.error)) + }, + unmuteConversation () { + this.$store.dispatch('unmuteConversation', this.status.id) + .then(() => this.$emit('onSuccess')) + .catch(err => this.$emit('onError', err.error.error)) } }, computed: { @@ -31,8 +41,8 @@ const ExtraButtons = { canPin () { return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted') }, - enabled () { - return this.canPin || this.canDelete + canMute () { + return !!this.currentUser } } } diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index cdad1666..6781a4f8 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -1,6 +1,6 @@ <template> <v-popover - v-if="enabled" + v-if="canDelete || canMute || canPin" trigger="click" placement="top" class="extra-button-popover" @@ -10,6 +10,20 @@ <div slot="popover"> <div class="dropdown-menu"> <button + v-if="canMute && !status.thread_muted" + class="dropdown-item dropdown-item-icon" + @click.prevent="muteConversation" + > + <i class="icon-eye-off" /><span>{{ $t("status.mute_conversation") }}</span> + </button> + <button + v-if="canMute && status.thread_muted" + class="dropdown-item dropdown-item-icon" + @click.prevent="unmuteConversation" + > + <i class="icon-eye-off" /><span>{{ $t("status.unmute_conversation") }}</span> + </button> + <button v-if="!status.pinned && canPin" v-close-popover class="dropdown-item dropdown-item-icon" diff --git a/src/components/features_panel/features_panel.js b/src/components/features_panel/features_panel.js index 5f0b7b25..5f80a079 100644 --- a/src/components/features_panel/features_panel.js +++ b/src/components/features_panel/features_panel.js @@ -1,8 +1,6 @@ const FeaturesPanel = { computed: { - chat: function () { - return this.$store.state.instance.chatAvailable && (!this.$store.state.chatDisabled) - }, + chat: function () { return this.$store.state.instance.chatAvailable }, gopher: function () { return this.$store.state.instance.gopherAvailable }, whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled }, mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable }, diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue index 6adfb76c..6169d294 100644 --- a/src/components/gallery/gallery.vue +++ b/src/components/gallery/gallery.vue @@ -61,13 +61,17 @@ } &.contain-fit { - img, video { + img, + video, + canvas { object-fit: contain; } } &.cover-fit { - img, video { + img, + video, + canvas { object-fit: cover; } } diff --git a/src/components/instance_specific_panel/instance_specific_panel.js b/src/components/instance_specific_panel/instance_specific_panel.js index 9bb5e945..09e3d055 100644 --- a/src/components/instance_specific_panel/instance_specific_panel.js +++ b/src/components/instance_specific_panel/instance_specific_panel.js @@ -2,9 +2,6 @@ const InstanceSpecificPanel = { computed: { instanceSpecificPanelContent () { return this.$store.state.instance.instanceSpecificPanelContent - }, - show () { - return !this.$store.state.config.hideISP } } } diff --git a/src/components/instance_specific_panel/instance_specific_panel.vue b/src/components/instance_specific_panel/instance_specific_panel.vue index a7cf6b48..7448ca06 100644 --- a/src/components/instance_specific_panel/instance_specific_panel.vue +++ b/src/components/instance_specific_panel/instance_specific_panel.vue @@ -1,8 +1,5 @@ <template> - <div - v-if="show" - class="instance-specific-panel" - > + <div class="instance-specific-panel"> <div class="panel panel-default"> <div class="panel-body"> <!-- eslint-disable vue/no-v-html --> @@ -14,6 +11,3 @@ </template> <script src="./instance_specific_panel.js" ></script> - -<style lang="scss"> -</style> diff --git a/src/components/interactions/interactions.js b/src/components/interactions/interactions.js index d4e3cc17..1f8a9de9 100644 --- a/src/components/interactions/interactions.js +++ b/src/components/interactions/interactions.js @@ -13,8 +13,8 @@ const Interactions = { } }, methods: { - onModeSwitch (index, dataset) { - this.filterMode = tabModeDict[dataset.filter] + onModeSwitch (key) { + this.filterMode = tabModeDict[key] } }, components: { diff --git a/src/components/interactions/interactions.vue b/src/components/interactions/interactions.vue index d71c99d5..08cee343 100644 --- a/src/components/interactions/interactions.vue +++ b/src/components/interactions/interactions.vue @@ -10,18 +10,15 @@ :on-switch="onModeSwitch" > <span - data-tab-dummy - data-filter="mentions" + key="mentions" :label="$t('nav.mentions')" /> <span - data-tab-dummy - data-filter="likes+repeats" + key="likes+repeats" :label="$t('interactions.favs_repeats')" /> <span - data-tab-dummy - data-filter="follows" + key="follows" :label="$t('interactions.follows')" /> </tab-switcher> diff --git a/src/components/link-preview/link-preview.js b/src/components/link-preview/link-preview.js index 2f6da55e..444aafbe 100644 --- a/src/components/link-preview/link-preview.js +++ b/src/components/link-preview/link-preview.js @@ -5,6 +5,11 @@ const LinkPreview = { 'size', 'nsfw' ], + data () { + return { + imageLoaded: false + } + }, computed: { useImage () { // Currently BE shoudn't give cards if tagged NSFW, this is a bit paranoid @@ -15,6 +20,15 @@ const LinkPreview = { useDescription () { return this.card.description && /\S/.test(this.card.description) } + }, + created () { + if (this.useImage) { + const newImg = new Image() + newImg.onload = () => { + this.imageLoaded = true + } + newImg.src = this.card.image + } } } diff --git a/src/components/link-preview/link-preview.vue b/src/components/link-preview/link-preview.vue index 493774c2..69171977 100644 --- a/src/components/link-preview/link-preview.vue +++ b/src/components/link-preview/link-preview.vue @@ -7,7 +7,7 @@ rel="noopener" > <div - v-if="useImage" + v-if="useImage && imageLoaded" class="card-image" :class="{ 'small-image': size === 'small' }" > diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue index 3ec7fe0c..b4fdcefb 100644 --- a/src/components/login_form/login_form.vue +++ b/src/components/login_form/login_form.vue @@ -33,6 +33,11 @@ type="password" > </div> + <div class="form-group"> + <router-link :to="{name: 'password-reset'}"> + {{ $t('password_reset.forgot_password') }} + </router-link> + </div> </template> <div diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 0543e677..ab5a36a5 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -63,6 +63,7 @@ max-width: 90%; max-height: 90%; box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5); + image-orientation: from-image; // NOTE: only FF supports this } .modal-view-button-arrow { diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js index 9b341a3b..c2bb76ee 100644 --- a/src/components/mobile_nav/mobile_nav.js +++ b/src/components/mobile_nav/mobile_nav.js @@ -1,14 +1,12 @@ import SideDrawer from '../side_drawer/side_drawer.vue' import Notifications from '../notifications/notifications.vue' -import MobilePostStatusModal from '../mobile_post_status_modal/mobile_post_status_modal.vue' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import GestureService from '../../services/gesture_service/gesture_service' const MobileNav = { components: { SideDrawer, - Notifications, - MobilePostStatusModal + Notifications }, data: () => ({ notificationsCloseGesture: undefined, diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index f67b7ff8..d1c24e56 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -70,7 +70,6 @@ ref="sideDrawer" :logout="logout" /> - <MobilePostStatusModal /> </div> </template> diff --git a/src/components/mobile_post_status_modal/mobile_post_status_modal.js b/src/components/mobile_post_status_button/mobile_post_status_button.js index 3cec23c6..3e77148a 100644 --- a/src/components/mobile_post_status_modal/mobile_post_status_modal.js +++ b/src/components/mobile_post_status_button/mobile_post_status_button.js @@ -1,14 +1,9 @@ -import PostStatusForm from '../post_status_form/post_status_form.vue' import { debounce } from 'lodash' -const MobilePostStatusModal = { - components: { - PostStatusForm - }, +const MobilePostStatusButton = { data () { return { hidden: false, - postFormOpen: false, scrollingDown: false, inputActive: false, oldScrollPos: 0, @@ -28,8 +23,8 @@ const MobilePostStatusModal = { window.removeEventListener('resize', this.handleOSK) }, computed: { - currentUser () { - return this.$store.state.users.currentUser + isLoggedIn () { + return !!this.$store.state.users.currentUser }, isHidden () { return this.autohideFloatingPostButton && (this.hidden || this.inputActive) @@ -57,17 +52,7 @@ const MobilePostStatusModal = { window.removeEventListener('scroll', this.handleScrollEnd) }, openPostForm () { - this.postFormOpen = true - this.hidden = true - - const el = this.$el.querySelector('textarea') - this.$nextTick(function () { - el.focus() - }) - }, - closePostForm () { - this.postFormOpen = false - this.hidden = false + this.$store.dispatch('openPostStatusModal') }, handleOSK () { // This is a big hack: we're guessing from changed window sizes if the @@ -105,4 +90,4 @@ const MobilePostStatusModal = { } } -export default MobilePostStatusModal +export default MobilePostStatusButton diff --git a/src/components/mobile_post_status_modal/mobile_post_status_modal.vue b/src/components/mobile_post_status_button/mobile_post_status_button.vue index 5db7584b..9cf45de3 100644 --- a/src/components/mobile_post_status_modal/mobile_post_status_modal.vue +++ b/src/components/mobile_post_status_button/mobile_post_status_button.vue @@ -1,23 +1,5 @@ <template> - <div v-if="currentUser"> - <div - v-show="postFormOpen" - class="post-form-modal-view modal-view" - @click="closePostForm" - > - <div - class="post-form-modal-panel panel" - @click.stop="" - > - <div class="panel-heading"> - {{ $t('post_status.new_status') }} - </div> - <PostStatusForm - class="panel-body" - @posted="closePostForm" - /> - </div> - </div> + <div v-if="isLoggedIn"> <button class="new-status-button" :class="{ 'hidden': isHidden }" @@ -28,22 +10,11 @@ </div> </template> -<script src="./mobile_post_status_modal.js"></script> +<script src="./mobile_post_status_button.js"></script> <style lang="scss"> @import '../../_variables.scss'; -.post-form-modal-view { - max-height: 100%; - display: block; -} - -.post-form-modal-panel { - flex-shrink: 0; - margin: 25% 0 4em 0; - width: 100%; -} - .new-status-button { width: 5em; height: 5em; diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index 896c6d52..8e817f3b 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -9,7 +9,8 @@ const Notification = { data () { return { userExpanded: false, - betterShadow: this.$store.state.interface.browserSupport.cssFilter + betterShadow: this.$store.state.interface.browserSupport.cssFilter, + unmuted: false } }, props: [ 'notification' ], @@ -23,11 +24,14 @@ const Notification = { toggleUserExpanded () { this.userExpanded = !this.userExpanded }, - userProfileLink (user) { + generateUserProfileLink (user) { return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) }, getUser (notification) { return this.$store.state.users.usersObject[notification.from_profile.id] + }, + toggleMute () { + this.unmuted = !this.unmuted } }, computed: { @@ -47,6 +51,12 @@ const Notification = { return this.userInStore } return this.notification.from_profile + }, + userProfileLink () { + return this.generateUserProfileLink(this.user) + }, + needMute () { + return this.user.muted } } } diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index bafcd026..1f192c77 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -4,104 +4,126 @@ :compact="true" :statusoid="notification.status" /> - <div - v-else - class="non-mention" - :class="[userClass, { highlighted: userStyle }]" - :style="[ userStyle ]" - > - <a - class="avatar-container" - :href="notification.from_profile.statusnet_profile_url" - @click.stop.prevent.capture="toggleUserExpanded" + <div v-else> + <div + v-if="needMute && !unmuted" + class="container muted" > - <UserAvatar - :compact="true" - :better-shadow="betterShadow" - :user="notification.from_profile" - /> - </a> - <div class="notification-right"> - <UserCard - v-if="userExpanded" - :user="getUser(notification)" - :rounded="true" - :bordered="true" - /> - <span class="notification-details"> - <div class="name-and-action"> - <!-- eslint-disable vue/no-v-html --> - <span - v-if="!!notification.from_profile.name_html" - class="username" - :title="'@'+notification.from_profile.screen_name" - v-html="notification.from_profile.name_html" - /> - <!-- eslint-enable vue/no-v-html --> - <span - v-else - class="username" - :title="'@'+notification.from_profile.screen_name" - >{{ notification.from_profile.name }}</span> - <span v-if="notification.type === 'like'"> - <i class="fa icon-star lit" /> - <small>{{ $t('notifications.favorited_you') }}</small> - </span> - <span v-if="notification.type === 'repeat'"> - <i - class="fa icon-retweet lit" - :title="$t('tool_tip.repeat')" + <small> + <router-link :to="userProfileLink"> + {{ notification.from_profile.screen_name }} + </router-link> + </small> + <a + href="#" + class="unmute" + @click.prevent="toggleMute" + ><i class="button-icon icon-eye-off" /></a> + </div> + <div + v-else + class="non-mention" + :class="[userClass, { highlighted: userStyle }]" + :style="[ userStyle ]" + > + <a + class="avatar-container" + :href="notification.from_profile.statusnet_profile_url" + @click.stop.prevent.capture="toggleUserExpanded" + > + <UserAvatar + :compact="true" + :better-shadow="betterShadow" + :user="notification.from_profile" + /> + </a> + <div class="notification-right"> + <UserCard + v-if="userExpanded" + :user="getUser(notification)" + :rounded="true" + :bordered="true" + /> + <span class="notification-details"> + <div class="name-and-action"> + <!-- eslint-disable vue/no-v-html --> + <span + v-if="!!notification.from_profile.name_html" + class="username" + :title="'@'+notification.from_profile.screen_name" + v-html="notification.from_profile.name_html" /> - <small>{{ $t('notifications.repeated_you') }}</small> - </span> - <span v-if="notification.type === 'follow'"> - <i class="fa icon-user-plus lit" /> - <small>{{ $t('notifications.followed_you') }}</small> - </span> - </div> + <!-- eslint-enable vue/no-v-html --> + <span + v-else + class="username" + :title="'@'+notification.from_profile.screen_name" + >{{ notification.from_profile.name }}</span> + <span v-if="notification.type === 'like'"> + <i class="fa icon-star lit" /> + <small>{{ $t('notifications.favorited_you') }}</small> + </span> + <span v-if="notification.type === 'repeat'"> + <i + class="fa icon-retweet lit" + :title="$t('tool_tip.repeat')" + /> + <small>{{ $t('notifications.repeated_you') }}</small> + </span> + <span v-if="notification.type === 'follow'"> + <i class="fa icon-user-plus lit" /> + <small>{{ $t('notifications.followed_you') }}</small> + </span> + </div> + <div + v-if="notification.type === 'follow'" + class="timeago" + > + <span class="faint"> + <Timeago + :time="notification.created_at" + :auto-update="240" + /> + </span> + </div> + <div + v-else + class="timeago" + > + <router-link + v-if="notification.status" + :to="{ name: 'conversation', params: { id: notification.status.id } }" + class="faint-link" + > + <Timeago + :time="notification.created_at" + :auto-update="240" + /> + </router-link> + </div> + <a + v-if="needMute" + href="#" + @click.prevent="toggleMute" + ><i class="button-icon icon-eye-off" /></a> + </span> <div v-if="notification.type === 'follow'" - class="timeago" - > - <span class="faint"> - <Timeago - :time="notification.created_at" - :auto-update="240" - /> - </span> - </div> - <div - v-else - class="timeago" + class="follow-text" > - <router-link - v-if="notification.status" - :to="{ name: 'conversation', params: { id: notification.status.id } }" - class="faint-link" - > - <Timeago - :time="notification.created_at" - :auto-update="240" - /> + <router-link :to="userProfileLink"> + @{{ notification.from_profile.screen_name }} </router-link> </div> - </span> - <div - v-if="notification.type === 'follow'" - class="follow-text" - > - <router-link :to="userProfileLink(notification.from_profile)"> - @{{ notification.from_profile.screen_name }} - </router-link> + <template v-else> + <status + class="faint" + :compact="true" + :statusoid="notification.action" + :no-heading="true" + /> + </template> </div> - <template v-else> - <status - class="faint" - :compact="true" - :statusoid="notification.action" - :no-heading="true" - /> - </template> </div> </div> </template> diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index 622d12f4..71876b14 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -33,7 +33,6 @@ .notification { box-sizing: border-box; - display: flex; border-bottom: 1px solid; border-color: $fallback--border; border-color: var(--border, $fallback--border); @@ -47,6 +46,10 @@ } } + .muted { + padding: .25em .6em; + } + .non-mention { display: flex; flex: 1; diff --git a/src/components/password_reset/password_reset.js b/src/components/password_reset/password_reset.js new file mode 100644 index 00000000..fa71e07a --- /dev/null +++ b/src/components/password_reset/password_reset.js @@ -0,0 +1,62 @@ +import { mapState } from 'vuex' +import passwordResetApi from '../../services/new_api/password_reset.js' + +const passwordReset = { + data: () => ({ + user: { + email: '' + }, + isPending: false, + success: false, + throttled: false, + error: null + }), + computed: { + ...mapState({ + signedIn: (state) => !!state.users.currentUser, + instance: state => state.instance + }), + mailerEnabled () { + return this.instance.mailerEnabled + } + }, + created () { + if (this.signedIn) { + this.$router.push({ name: 'root' }) + } + }, + methods: { + dismissError () { + this.error = null + }, + submit () { + this.isPending = true + const email = this.user.email + const instance = this.instance.server + + passwordResetApi({ instance, email }).then(({ status }) => { + this.isPending = false + this.user.email = '' + + if (status === 204) { + this.success = true + this.error = null + } else if (status === 404 || status === 400) { + this.error = this.$t('password_reset.not_found') + this.$nextTick(() => { + this.$refs.email.focus() + }) + } else if (status === 429) { + this.throttled = true + this.error = this.$t('password_reset.too_many_requests') + } + }).catch(() => { + this.isPending = false + this.user.email = '' + this.error = this.$t('general.generic_error') + }) + } + } +} + +export default passwordReset diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue new file mode 100644 index 00000000..00474e95 --- /dev/null +++ b/src/components/password_reset/password_reset.vue @@ -0,0 +1,116 @@ +<template> + <div class="settings panel panel-default"> + <div class="panel-heading"> + {{ $t('password_reset.password_reset') }} + </div> + <div class="panel-body"> + <form + class="password-reset-form" + @submit.prevent="submit" + > + <div class="container"> + <div v-if="!mailerEnabled"> + <p> + {{ $t('password_reset.password_reset_disabled') }} + </p> + </div> + <div v-else-if="success || throttled"> + <p v-if="success"> + {{ $t('password_reset.check_email') }} + </p> + <div class="form-group text-center"> + <router-link :to="{name: 'root'}"> + {{ $t('password_reset.return_home') }} + </router-link> + </div> + </div> + <div v-else> + <p> + {{ $t('password_reset.instruction') }} + </p> + <div class="form-group"> + <input + ref="email" + v-model="user.email" + :disabled="isPending" + :placeholder="$t('password_reset.placeholder')" + class="form-control" + type="input" + > + </div> + <div class="form-group"> + <button + :disabled="isPending" + type="submit" + class="btn btn-default btn-block" + > + {{ $t('general.submit') }} + </button> + </div> + </div> + <p + v-if="error" + class="alert error notice-dismissible" + > + <span>{{ error }}</span> + <a + class="button-icon dismiss" + @click.prevent="dismissError()" + > + <i class="icon-cancel" /> + </a> + </p> + </div> + </form> + </div> + </div> +</template> + +<script src="./password_reset.js"></script> +<style lang="scss"> +@import '../../_variables.scss'; + +.password-reset-form { + display: flex; + flex-direction: column; + align-items: center; + margin: 0.6em; + + .container { + display: flex; + flex: 1 0; + flex-direction: column; + margin-top: 0.6em; + max-width: 18rem; + } + + .form-group { + display: flex; + flex-direction: column; + margin-bottom: 1em; + padding: 0.3em 0.0em 0.3em; + line-height: 24px; + } + + .error { + text-align: center; + animation-name: shakeError; + animation-duration: 0.4s; + animation-timing-function: ease-in-out; + } + + .alert { + padding: 0.5em; + margin: 0.3em 0.0em 1em; + } + + .notice-dismissible { + padding-right: 2rem; + } + + .icon-cancel { + cursor: pointer; + } +} + +</style> diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 40bbf6d4..dc4b419c 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -8,7 +8,7 @@ import fileTypeService from '../../services/file_type/file_type.service.js' import { reject, map, uniqBy } from 'lodash' import suggestor from '../emoji-input/suggestor.js' -const buildMentionsString = ({ user, attentions }, currentUser) => { +const buildMentionsString = ({ user, attentions = [] }, currentUser) => { let allAttentions = [...attentions] allAttentions.unshift(user) diff --git a/src/components/post_status_modal/post_status_modal.js b/src/components/post_status_modal/post_status_modal.js new file mode 100644 index 00000000..1033ba11 --- /dev/null +++ b/src/components/post_status_modal/post_status_modal.js @@ -0,0 +1,32 @@ +import PostStatusForm from '../post_status_form/post_status_form.vue' + +const PostStatusModal = { + components: { + PostStatusForm + }, + computed: { + isLoggedIn () { + return !!this.$store.state.users.currentUser + }, + isOpen () { + return this.isLoggedIn && this.$store.state.postStatus.modalActivated + }, + params () { + return this.$store.state.postStatus.params || {} + } + }, + watch: { + isOpen (val) { + if (val) { + this.$nextTick(() => this.$el.querySelector('textarea').focus()) + } + } + }, + methods: { + closeModal () { + this.$store.dispatch('closePostStatusModal') + } + } +} + +export default PostStatusModal diff --git a/src/components/post_status_modal/post_status_modal.vue b/src/components/post_status_modal/post_status_modal.vue new file mode 100644 index 00000000..3f8eec69 --- /dev/null +++ b/src/components/post_status_modal/post_status_modal.vue @@ -0,0 +1,43 @@ +<template> + <div + v-if="isOpen" + class="post-form-modal-view modal-view" + @click="closeModal" + > + <div + class="post-form-modal-panel panel" + @click.stop="" + > + <div class="panel-heading"> + {{ $t('post_status.new_status') }} + </div> + <PostStatusForm + class="panel-body" + v-bind="params" + @posted="closeModal" + /> + </div> + </div> +</template> + +<script src="./post_status_modal.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.post-form-modal-view { + align-items: flex-start; +} + +.post-form-modal-panel { + flex-shrink: 0; + margin-top: 25%; + margin-bottom: 2em; + width: 100%; + max-width: 700px; + + @media (orientation: landscape) { + margin-top: 8%; + } +} +</style> diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue index e0fa214a..5bb06a4f 100644 --- a/src/components/registration/registration.vue +++ b/src/components/registration/registration.vue @@ -268,6 +268,7 @@ $validations-cRed: #f04124; textarea { min-height: 100px; + resize: vertical; } .form-group { diff --git a/src/components/search/search.js b/src/components/search/search.js index b434e127..8e903052 100644 --- a/src/components/search/search.js +++ b/src/components/search/search.js @@ -75,8 +75,8 @@ const Search = { const length = this[tabName].length return length === 0 ? '' : ` (${length})` }, - onResultTabSwitch (_index, dataset) { - this.currenResultTab = dataset.filter + onResultTabSwitch (key) { + this.currenResultTab = key }, getActiveTab () { if (this.visibleStatuses.length > 0) { diff --git a/src/components/search/search.vue b/src/components/search/search.vue index 4350e672..746bbaa2 100644 --- a/src/components/search/search.vue +++ b/src/components/search/search.vue @@ -31,21 +31,18 @@ <tab-switcher ref="tabSwitcher" :on-switch="onResultTabSwitch" - :custom-active="currenResultTab" + :active-tab="currenResultTab" > <span - data-tab-dummy - data-filter="statuses" + key="statuses" :label="$t('user_card.statuses') + resultCount('visibleStatuses')" /> <span - data-tab-dummy - data-filter="people" + key="people" :label="$t('search.people') + resultCount('users')" /> <span - data-tab-dummy - data-filter="hashtags" + key="hashtags" :label="$t('search.hashtags') + resultCount('hashtags')" /> </tab-switcher> diff --git a/src/components/status/status.js b/src/components/status/status.js index 3c172e5b..d17ba318 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -29,7 +29,8 @@ const Status = { 'isPreview', 'noHeading', 'inlineExpanded', - 'showPinned' + 'showPinned', + 'inProfile' ], data () { return { @@ -117,7 +118,7 @@ const Status = { return hits }, - muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) }, + muted () { return !this.unmuted && ((!this.inProfile && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) }, hideFilteredStatuses () { return typeof this.$store.state.config.hideFilteredStatuses === 'undefined' ? this.$store.state.instance.hideFilteredStatuses @@ -335,7 +336,7 @@ const Status = { return } } - if (target.className.match(/hashtag/)) { + if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) { // Extract tag name from link url const tag = extractTagFromUrl(target.href) if (tag) { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index ab506632..64218f6e 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -32,7 +32,7 @@ </template> <template v-else> <div - v-if="showPinned && statusoid.pinned" + v-if="showPinned" class="status-pin" > <i class="fa icon-pin faint" /> diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue index 3fff63f9..4137bd59 100644 --- a/src/components/still-image/still-image.vue +++ b/src/components/still-image/still-image.vue @@ -7,8 +7,10 @@ v-if="animated" ref="canvas" /> + <!-- NOTE: key is required to force to re-render img tag when src is changed --> <img ref="src" + :key="src" :src="src" :referrerpolicy="referrerpolicy" @load="onLoad" diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js index a5fe019c..08d5d08f 100644 --- a/src/components/tab_switcher/tab_switcher.js +++ b/src/components/tab_switcher/tab_switcher.js @@ -4,12 +4,22 @@ import './tab_switcher.scss' export default Vue.component('tab-switcher', { name: 'TabSwitcher', - props: ['renderOnlyFocused', 'onSwitch', 'customActive'], + props: ['renderOnlyFocused', 'onSwitch', 'activeTab'], data () { return { active: this.$slots.default.findIndex(_ => _.tag) } }, + computed: { + activeIndex () { + // In case of controlled component + if (this.activeTab) { + return this.$slots.default.findIndex(slot => this.activeTab === slot.key) + } else { + return this.active + } + } + }, beforeUpdate () { const currentSlot = this.$slots.default[this.active] if (!currentSlot.tag) { @@ -17,21 +27,13 @@ export default Vue.component('tab-switcher', { } }, methods: { - activateTab (index, dataset) { + activateTab (index) { return () => { if (typeof this.onSwitch === 'function') { - this.onSwitch.call(null, index, this.$slots.default[index].elm.dataset) + this.onSwitch.call(null, this.$slots.default[index].key) } this.active = index } - }, - isActiveTab (index) { - const customActiveIndex = this.$slots.default.findIndex(slot => { - const dataFilter = slot.data && slot.data.attrs && slot.data.attrs['data-filter'] - return this.customActive && this.customActive === dataFilter - }) - - return customActiveIndex > -1 ? customActiveIndex === index : index === this.active } }, render (h) { @@ -41,13 +43,13 @@ export default Vue.component('tab-switcher', { const classesTab = ['tab'] const classesWrapper = ['tab-wrapper'] - if (this.isActiveTab(index)) { + if (this.activeIndex === index) { classesTab.push('active') classesWrapper.push('active') } if (slot.data.attrs.image) { return ( - <div class={ classesWrapper.join(' ')}> + <div class={classesWrapper.join(' ')}> <button disabled={slot.data.attrs.disabled} onClick={this.activateTab(index)} @@ -59,7 +61,7 @@ export default Vue.component('tab-switcher', { ) } return ( - <div class={ classesWrapper.join(' ')}> + <div class={classesWrapper.join(' ')}> <button disabled={slot.data.attrs.disabled} onClick={this.activateTab(index)} @@ -71,7 +73,7 @@ export default Vue.component('tab-switcher', { const contents = this.$slots.default.map((slot, index) => { if (!slot.tag) return - const active = index === this.active + const active = this.activeIndex === index if (this.renderOnlyFocused) { return active ? <div class="active">{slot}</div> diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index aac3869f..0594576c 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -25,7 +25,8 @@ const Timeline = { 'tag', 'embedded', 'count', - 'pinnedStatusIds' + 'pinnedStatusIds', + 'inProfile' ], data () { return { @@ -58,7 +59,10 @@ const Timeline = { excludedStatusIdsObject () { const ids = getExcludedStatusIdsByPinning(this.timeline.visibleStatuses, this.pinnedStatusIds) // Convert id array to object - return keyBy(ids, id => id) + return keyBy(ids) + }, + pinnedStatusIdsObject () { + return keyBy(this.pinnedStatusIds) } }, components: { diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 0cb4b3ef..f1d3903a 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -33,9 +33,10 @@ v-if="timeline.statusesObject[statusId]" :key="statusId + '-pinned'" class="status-fadein" - :statusoid="timeline.statusesObject[statusId]" + :status-id="statusId" :collapsable="true" - :show-pinned="true" + :pinned-status-ids-object="pinnedStatusIdsObject" + :in-profile="inProfile" /> </template> <template v-for="status in timeline.visibleStatuses"> @@ -43,8 +44,9 @@ v-if="!excludedStatusIdsObject[status.id]" :key="status.id" class="status-fadein" - :statusoid="status" + :status-id="status.id" :collapsable="true" + :in-profile="inProfile" /> </template> </div> diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js index a42b9c71..4adf8211 100644 --- a/src/components/user_avatar/user_avatar.js +++ b/src/components/user_avatar/user_avatar.js @@ -16,7 +16,7 @@ const UserAvatar = { }, computed: { imgSrc () { - return this.showPlaceholder ? '/images/avi.png' : this.src + return this.showPlaceholder ? '/images/avi.png' : this.user.profile_image_url_original } }, methods: { diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue index 811efd3c..9ffb28d8 100644 --- a/src/components/user_avatar/user_avatar.vue +++ b/src/components/user_avatar/user_avatar.vue @@ -3,7 +3,7 @@ class="avatar" :alt="user.screen_name" :title="user.screen_name" - :src="user.profile_image_url_original" + :src="imgSrc" :class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }" :image-load-error="imageLoadError" /> diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 82d3b835..0c200ad1 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -11,7 +11,6 @@ export default { data () { return { followRequestInProgress: false, - followRequestSent: false, hideUserStatsLocal: typeof this.$store.state.config.hideUserStats === 'undefined' ? this.$store.state.instance.hideUserStats : this.$store.state.config.hideUserStats, @@ -112,9 +111,8 @@ export default { followUser () { const store = this.$store this.followRequestInProgress = true - requestFollow(this.user, store).then(({ sent }) => { + requestFollow(this.user, store).then(() => { this.followRequestInProgress = false - this.followRequestSent = sent }) }, unfollowUser () { @@ -170,6 +168,9 @@ export default { } this.$store.dispatch('setMedia', [attachment]) this.$store.dispatch('setCurrent', attachment) + }, + mentionUser () { + this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user }) } } } diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index fc18e240..f25d16d3 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -135,13 +135,13 @@ <button class="btn btn-default btn-block" :disabled="followRequestInProgress" - :title="followRequestSent ? $t('user_card.follow_again') : ''" + :title="user.requested ? $t('user_card.follow_again') : ''" @click="followUser" > <template v-if="followRequestInProgress"> {{ $t('user_card.follow_progress') }} </template> - <template v-else-if="followRequestSent"> + <template v-else-if="user.requested"> {{ $t('user_card.follow_sent') }} </template> <template v-else> @@ -190,6 +190,15 @@ <div> <button + class="btn btn-default btn-block" + @click="mentionUser" + > + {{ $t('user_card.mention') }} + </button> + </div> + + <div> + <button v-if="user.muted" class="btn btn-default btn-block pressed" @click="unmuteUser" diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue index c92630e3..e859d612 100644 --- a/src/components/user_panel/user_panel.vue +++ b/src/components/user_panel/user_panel.vue @@ -11,7 +11,7 @@ rounded="top" /> <div class="panel-footer"> - <post-status-form v-if="user" /> + <post-status-form /> </div> </div> <auth-form diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 39b99dac..00055707 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -22,21 +22,23 @@ const FriendList = withLoadMore({ additionalPropNames: ['userId'] })(List) +const defaultTabKey = 'statuses' + const UserProfile = { data () { return { error: false, - userId: null + userId: null, + tab: defaultTabKey } }, created () { - // Make sure that timelines used in this page are empty - this.cleanUp() const routeParams = this.$route.params this.load(routeParams.name || routeParams.id) + this.tab = get(this.$route, 'query.tab', defaultTabKey) }, destroyed () { - this.cleanUp() + this.stopFetching() }, computed: { timeline () { @@ -67,17 +69,36 @@ const UserProfile = { }, methods: { load (userNameOrId) { + const startFetchingTimeline = (timeline, userId) => { + // Clear timeline only if load another user's profile + if (userId !== this.$store.state.statuses.timelines[timeline].userId) { + this.$store.commit('clearTimeline', { timeline }) + } + this.$store.dispatch('startFetchingTimeline', { timeline, userId }) + } + + const loadById = (userId) => { + this.userId = userId + startFetchingTimeline('user', userId) + startFetchingTimeline('media', userId) + if (this.isUs) { + startFetchingTimeline('favorites', userId) + } + // Fetch all pinned statuses immediately + this.$store.dispatch('fetchPinnedStatuses', userId) + } + + // Reset view + this.userId = null + this.error = false + // Check if user data is already loaded in store const user = this.$store.getters.findUser(userNameOrId) if (user) { - this.userId = user.id - this.fetchTimelines() + loadById(user.id) } else { this.$store.dispatch('fetchUser', userNameOrId) - .then(({ id }) => { - this.userId = id - this.fetchTimelines() - }) + .then(({ id }) => loadById(id)) .catch((reason) => { const errorMessage = get(reason, 'error.error') if (errorMessage === 'No user with such user_id') { // Known error @@ -90,40 +111,33 @@ const UserProfile = { }) } }, - fetchTimelines () { - const userId = this.userId - this.$store.dispatch('startFetchingTimeline', { timeline: 'user', userId }) - this.$store.dispatch('startFetchingTimeline', { timeline: 'media', userId }) - if (this.isUs) { - this.$store.dispatch('startFetchingTimeline', { timeline: 'favorites', userId }) - } - // Fetch all pinned statuses immediately - this.$store.dispatch('fetchPinnedStatuses', userId) - }, - cleanUp () { + stopFetching () { this.$store.dispatch('stopFetching', 'user') this.$store.dispatch('stopFetching', 'favorites') this.$store.dispatch('stopFetching', 'media') - this.$store.commit('clearTimeline', { timeline: 'user' }) - this.$store.commit('clearTimeline', { timeline: 'favorites' }) - this.$store.commit('clearTimeline', { timeline: 'media' }) + }, + switchUser (userNameOrId) { + this.stopFetching() + this.load(userNameOrId) + }, + onTabSwitch (tab) { + this.tab = tab + this.$router.replace({ query: { tab } }) } }, watch: { '$route.params.id': function (newVal) { if (newVal) { - this.cleanUp() - this.load(newVal) + this.switchUser(newVal) } }, '$route.params.name': function (newVal) { if (newVal) { - this.cleanUp() - this.load(newVal) + this.switchUser(newVal) } }, - $route () { - this.$refs.tabSwitcher.activateTab(0)() + '$route.query': function (newVal) { + this.tab = newVal.tab || defaultTabKey } }, components: { diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index cffa28f1..14082e83 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -12,22 +12,25 @@ rounded="top" /> <tab-switcher - ref="tabSwitcher" + :active-tab="tab" :render-only-focused="true" + :on-switch="onTabSwitch" > - <div :label="$t('user_card.statuses')"> - <Timeline - :count="user.statuses_count" - :embedded="true" - :title="$t('user_profile.timeline_title')" - :timeline="timeline" - timeline-name="user" - :user-id="userId" - :pinned-status-ids="user.pinnedStatusIds" - /> - </div> + <Timeline + key="statuses" + :label="$t('user_card.statuses')" + :count="user.statuses_count" + :embedded="true" + :title="$t('user_profile.timeline_title')" + :timeline="timeline" + timeline-name="user" + :user-id="userId" + :pinned-status-ids="user.pinnedStatusIds" + :in-profile="true" + /> <div v-if="followsTabVisible" + key="followees" :label="$t('user_card.followees')" :disabled="!user.friends_count" > @@ -42,6 +45,7 @@ </div> <div v-if="followersTabVisible" + key="followers" :label="$t('user_card.followers')" :disabled="!user.followers_count" > @@ -58,6 +62,7 @@ </FollowerList> </div> <Timeline + key="media" :label="$t('user_card.media')" :disabled="!media.visibleStatuses.length" :embedded="true" @@ -65,15 +70,18 @@ timeline-name="media" :timeline="media" :user-id="userId" + :in-profile="true" /> <Timeline v-if="isUs" + key="favorites" :label="$t('user_card.favorites')" :disabled="!favorites.visibleStatuses.length" :embedded="true" :title="$t('user_card.favorites')" timeline-name="favorites" :timeline="favorites" + :in-profile="true" /> </tab-switcher> </div> diff --git a/src/components/who_to_follow/who_to_follow.js b/src/components/who_to_follow/who_to_follow.js index f8100257..1aa3a4cd 100644 --- a/src/components/who_to_follow/who_to_follow.js +++ b/src/components/who_to_follow/who_to_follow.js @@ -21,11 +21,12 @@ const WhoToFollow = { name: i.display_name, screen_name: i.acct, profile_image_url: i.avatar || '/images/avi.png', - profile_image_url_original: i.avatar || '/images/avi.png' + profile_image_url_original: i.avatar || '/images/avi.png', + statusnet_profile_url: i.url } this.users.push(user) - this.$store.state.api.backendInteractor.externalProfile(user.screen_name) + this.$store.state.api.backendInteractor.fetchUser({ id: user.screen_name }) .then((externalUser) => { if (!externalUser.error) { this.$store.commit('addNewUsers', [externalUser]) diff --git a/src/components/who_to_follow_panel/who_to_follow_panel.js b/src/components/who_to_follow_panel/who_to_follow_panel.js index 7d01678b..dcb56106 100644 --- a/src/components/who_to_follow_panel/who_to_follow_panel.js +++ b/src/components/who_to_follow_panel/who_to_follow_panel.js @@ -13,7 +13,7 @@ function showWhoToFollow (panel, reply) { toFollow.img = img toFollow.name = name - panel.$store.state.api.backendInteractor.externalProfile(name) + panel.$store.state.api.backendInteractor.fetchUser({ id: name }) .then((externalUser) => { if (!externalUser.error) { panel.$store.commit('addNewUsers', [externalUser]) diff --git a/src/i18n/en.json b/src/i18n/en.json index 60a3e284..e4af507e 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -262,7 +262,7 @@ "loop_video": "Loop videos", "loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")", "mutes_tab": "Mutes", - "play_videos_in_modal": "Play videos directly in the media viewer", + "play_videos_in_modal": "Play videos in a popup frame", "use_contain_fit": "Don't crop the attachment in thumbnails", "name": "Name", "name_bio": "Name & Bio", @@ -508,7 +508,9 @@ "pinned": "Pinned", "delete_confirm": "Do you really want to delete this status?", "reply_to": "Reply to", - "replies_list": "Replies:" + "replies_list": "Replies:", + "mute_conversation": "Mute conversation", + "unmute_conversation": "Unmute conversation" }, "user_card": { "approve": "Approve", @@ -527,6 +529,7 @@ "follows_you": "Follows you!", "its_you": "It's you!", "media": "Media", + "mention": "Mention", "mute": "Mute", "muted": "Muted", "per_day": "per day", @@ -606,5 +609,16 @@ "person_talking": "{count} person talking", "people_talking": "{count} people talking", "no_results": "No results" + }, + "password_reset": { + "forgot_password": "Forgot password?", + "password_reset": "Password reset", + "instruction": "Enter your email address or username. We will send you a link to reset your password.", + "placeholder": "Your email or username", + "check_email": "Check your email for a link to reset your password.", + "return_home": "Return to the home page", + "not_found": "We couldn't find that email or username.", + "too_many_requests": "You have reached the limit of attempts, try again later.", + "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator." } } diff --git a/src/i18n/es.json b/src/i18n/es.json index f88e90f7..91c7f383 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -9,9 +9,9 @@ "features_panel": { "chat": "Chat", "gopher": "Gopher", - "media_proxy": "Media proxy", + "media_proxy": "Proxy de medios", "scope_options": "Opciones del alcance de la visibilidad", - "text_limit": "Límite de carácteres", + "text_limit": "Límite de caracteres", "title": "Características", "who_to_follow": "A quién seguir" }, @@ -47,19 +47,19 @@ "login": { "login": "Identificación", "description": "Identificación con OAuth", - "logout": "Salir", + "logout": "Cerrar sesión", "password": "Contraseña", "placeholder": "p.ej. lain", - "register": "Registrar", + "register": "Registrarse", "username": "Usuario", "hint": "Inicia sesión para unirte a la discusión", - "authentication_code": "Código de autentificación", + "authentication_code": "Código de autenticación", "enter_recovery_code": "Inserta el código de recuperación", - "enter_two_factor_code": "Inserta el código de doble factor", + "enter_two_factor_code": "Inserta el código de dos factores", "recovery_code": "Código de recuperación", "heading" : { - "totp" : "Autentificación de doble factor", - "recovery" : "Recuperación de doble factor" + "totp" : "Autenticación de dos factores", + "recovery" : "Recuperación de dos factores" } }, "media_modal": { @@ -67,13 +67,13 @@ "next": "Siguiente" }, "nav": { - "about": "Sobre", + "about": "Acerca de", "back": "Volver", "chat": "Chat Local", - "friend_requests": "Solicitudes de amistad", + "friend_requests": "Solicitudes de seguimiento", "mentions": "Menciones", "interactions": "Interacciones", - "dms": "Mensajes Directo", + "dms": "Mensajes Directos", "public_tl": "Línea Temporal Pública", "timeline": "Línea Temporal", "twkn": "Toda La Red Conocida", @@ -89,7 +89,7 @@ "load_older": "Cargar notificaciones antiguas", "notifications": "Notificaciones", "read": "¡Leído!", - "repeated_you": "repite tu estado", + "repeated_you": "repitió tu estado", "no_more_notifications": "No hay más notificaciones" }, "polls": { @@ -100,7 +100,7 @@ "vote": "Votar", "type": "Tipo de encuesta", "single_choice": "Elección única", - "multiple_choices": "Múltiples elecciones", + "multiple_choices": "Elección múltiple", "expiry": "Tiempo de vida de la encuesta", "expires_in": "La encuensta termina en {0}", "expired": "La encuesta terminó hace {0}", @@ -112,7 +112,7 @@ "interactions": { "favs_repeats": "Favoritos y Repetidos", "follows": "Nuevos seguidores", - "load_older": "Cargar interacciones antiguas" + "load_older": "Cargar interacciones más antiguas" }, "post_status": { "new_status": "Publicar un nuevo estado", @@ -137,20 +137,20 @@ }, "scope": { "direct": "Directo - Solo para los usuarios mencionados.", - "private": "Solo-Seguidores - Solo tus seguidores leeran la publicación", + "private": "Solo-seguidores - Solo tus seguidores leerán la publicación", "public": "Público - Entradas visibles en las Líneas Temporales Públicas", - "unlisted": "Sin Listar - Entradas no visibles en las Líneas Temporales Públicas" + "unlisted": "Sin listar - Entradas no visibles en las Líneas Temporales Públicas" } }, "registration": { "bio": "Biografía", "email": "Correo electrónico", "fullname": "Nombre a mostrar", - "password_confirm": "Confirmación de contraseña", + "password_confirm": "Confirmar contraseña", "registration": "Registro", "token": "Token de invitación", "captcha": "CAPTCHA", - "new_captcha": "Click en la imagen para obtener un nuevo captca", + "new_captcha": "Haz click en la imagen para obtener un nuevo captcha", "username_placeholder": "p.ej. lain", "fullname_placeholder": "p.ej. Lain Iwakura", "bio_placeholder": "e.g.\nHola, soy un ejemplo.\nAquí puedes poner algo representativo tuyo... o no.", @@ -164,7 +164,7 @@ } }, "selectable_list": { - "select_all": "Seleccionarlo todo" + "select_all": "Seleccionar todo" }, "settings": { "app_name": "Nombre de la aplicación", @@ -175,8 +175,8 @@ "setup_otp" : "Configurar OTP", "wait_pre_setup_otp" : "preconfiguración OTP", "confirm_and_enable" : "Confirmar y habilitar OTP", - "title": "Autentificación de Doble Factor", - "generate_new_recovery_codes" : "Generar nuevos códigos de recuperación", + "title": "Autentificación de dos factores", + "generate_new_recovery_codes" : "Generar códigos de recuperación nuevos", "warning_of_generate_new_codes" : "Cuando generas nuevos códigos de recuperación, los antiguos dejarán de funcionar.", "recovery_codes" : "Códigos de recuperación.", "waiting_a_recovery_codes": "Recibiendo códigos de respaldo", @@ -184,16 +184,16 @@ "authentication_methods" : "Métodos de autentificación", "scan": { "title": "Escanear", - "desc": "Usando su aplicación de doble factor, escanee este código QR o ingrese la clave de texto:", + "desc": "Usando su aplicación de dos factores, escanee este código QR o ingrese la clave de texto:", "secret_code": "Clave" }, "verify": { - "desc": "Para habilitar la autenticación de doble factor, ingrese el código de su aplicación 2FA:" + "desc": "Para habilitar la autenticación de dos factores, ingrese el código de su aplicación 2FA:" } }, "attachmentRadius": "Adjuntos", "attachments": "Adjuntos", - "autoload": "Activar carga automática al llegar al final de la página", + "autoload": "Habilitar carga automática al llegar al final de la página", "avatar": "Avatar", "avatarAltRadius": "Avatares (Notificaciones)", "avatarRadius": "Avatares", @@ -227,12 +227,12 @@ "delete_account_instructions": "Escribe tu contraseña para confirmar la eliminación de tu cuenta.", "avatar_size_instruction": "El tamaño mínimo recomendado para el avatar es de 150X150 píxeles.", "export_theme": "Exportar tema", - "filtering": "Filtros", + "filtering": "Filtrado", "filtering_explanation": "Todos los estados que contengan estas palabras serán silenciados, una por línea", "follow_export": "Exportar personas que tú sigues", - "follow_export_button": "Exporta tus seguidores a un archivo csv", + "follow_export_button": "Exporta tus seguidores a un fichero csv", "follow_import": "Importar personas que tú sigues", - "follow_import_error": "Error al importal el archivo", + "follow_import_error": "Error al importar el fichero", "follows_imported": "¡Importado! Procesarlos llevará tiempo.", "foreground": "Primer plano", "general": "General", @@ -262,7 +262,7 @@ "loop_video": "Vídeos en bucle", "loop_video_silent_only": "Bucle solo en vídeos sin sonido (p.ej. \"gifs\" de Mastodon)", "mutes_tab": "Silenciados", - "play_videos_in_modal": "Reproducir los vídeos directamente en el visor de medios", + "play_videos_in_modal": "Reproducir los vídeos en un marco emergente", "use_contain_fit": "No recortar los adjuntos en miniaturas", "name": "Nombre", "name_bio": "Nombre y Biografía", @@ -277,8 +277,8 @@ "no_mutes": "No hay usuarios sinlenciados", "hide_follows_description": "No mostrar a quién sigo", "hide_followers_description": "No mostrar quién me sigue", - "show_admin_badge": "Mostrar la placa de administrador en mi perfil", - "show_moderator_badge": "Mostrar la placa de moderador en mi perfil", + "show_admin_badge": "Mostrar la insignia de Administrador en mi perfil", + "show_moderator_badge": "Mostrar la insignia de Moderador en mi perfil", "nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW", "oauth_tokens": "Tokens de OAuth", "token": "Token", @@ -291,13 +291,13 @@ "profile_background": "Fondo del Perfil", "profile_banner": "Cabecera del Perfil", "profile_tab": "Perfil", - "radii_help": "Estable el redondeo de las esquinas del interfaz (en píxeles)", + "radii_help": "Estable el redondeo de las esquinas de la interfaz (en píxeles)", "replies_in_timeline": "Réplicas en la línea temporal", - "reply_link_preview": "Activar la previsualización del enlace de responder al pasar el ratón por encim", + "reply_link_preview": "Activar la previsualización del enlace de responder al pasar el ratón por encima", "reply_visibility_all": "Mostrar todas las réplicas", "reply_visibility_following": "Solo mostrar réplicas para mí o usuarios a los que sigo", "reply_visibility_self": "Solo mostrar réplicas para mí", - "autohide_floating_post_button": "Ocultar automáticamente el botón 'Nueva Publicación' (móvil)", + "autohide_floating_post_button": "Ocultar automáticamente el botón 'Nueva Publicación' (para móviles)", "saving_err": "Error al guardar los ajustes", "saving_ok": "Ajustes guardados", "search_user_to_block": "Buscar usuarios a bloquear", @@ -306,25 +306,25 @@ "scope_copy": "Copiar la visibilidad de la publicación cuando contestamos (En los mensajes directos (MDs) siempre se copia)", "minimal_scopes_mode": "Minimizar las opciones de publicación", "set_new_avatar": "Cambiar avatar", - "set_new_profile_background": "Cambiar fondo del perfil", - "set_new_profile_banner": "Cambiar cabecera del perfil", + "set_new_profile_background": "Cambiar el fondo del perfil", + "set_new_profile_banner": "Cambiar la cabecera del perfil", "settings": "Ajustes", "subject_input_always_show": "Mostrar siempre el campo del tema", - "subject_line_behavior": "Copiar el tema en las contestaciones", - "subject_line_email": "Tipo email: \"re: tema\"", - "subject_line_mastodon": "Tipo mastodon: copiar como es", + "subject_line_behavior": "Copiar el tema en las respuestas", + "subject_line_email": "Como email: \"re: tema\"", + "subject_line_mastodon": "Como mastodon: copiar como es", "subject_line_noop": "No copiar", "post_status_content_type": "Formato de publicación", "stop_gifs": "Iniciar GIFs al pasar el ratón", - "streaming": "Habilite la transmisión automática de nuevas publicaciones cuando se desplaza hacia la parte superior", + "streaming": "Habilitar la transmisión automática de nuevas publicaciones cuando se desplaza hacia la parte superior", "text": "Texto", "theme": "Tema", "theme_help": "Use códigos de color hexadecimales (#rrggbb) para personalizar su tema de colores.", - "theme_help_v2_1": "También puede invalidar los colores y la opacidad de ciertos componentes si activa la casilla de verificación, use el botón \"Borrar todo\" para deshacer los cambios.", - "theme_help_v2_2": "Los iconos debajo de algunas entradas son indicadores de contraste de fondo/texto, desplace el ratón para obtener información detallada. Tenga en cuenta que cuando se utilizan indicadores de contraste de transparencia se muestra el peor caso posible.", + "theme_help_v2_1": "También puede invalidar los colores y la opacidad de ciertos componentes si activa la casilla de verificación. Use el botón \"Borrar todo\" para deshacer los cambios.", + "theme_help_v2_2": "Los iconos debajo de algunas entradas son indicadores de contraste de fondo/texto, desplace el ratón por encima para obtener información más detallada. Tenga en cuenta que cuando se utilizan indicadores de contraste de transparencia se muestra el peor caso posible.", "tooltipRadius": "Información/alertas", "upload_a_photo": "Subir una foto", - "user_settings": "Ajustes de Usuario", + "user_settings": "Ajustes del Usuario", "values": { "false": "no", "true": "sí" @@ -395,14 +395,14 @@ "shadow_id": "Sombra #{value}", "blur": "Difuminar", "spread": "Cantidad", - "inset": "Insertada", + "inset": "Sombra interior", "hint": "Para las sombras, también puede usar --variable como un valor de color para usar las variables CSS3. Tenga en cuenta que establecer la opacidad no funcionará en este caso.", "filter_hint": { "always_drop_shadow": "Advertencia, esta sombra siempre usa {0} cuando el navegador lo soporta.", "drop_shadow_syntax": "{0} no soporta el parámetro {1} y la palabra clave {2}.", - "avatar_inset": "Tenga en cuenta que la combinación de sombras insertadas como no-insertadas en los avatares, puede dar resultados inesperados con los avatares transparentes.", + "avatar_inset": "Tenga en cuenta que la combinación de sombras interiores como no-interiores en los avatares, puede dar resultados inesperados con los avatares transparentes.", "spread_zero": "Sombras con una cantidad > 0 aparecerá como si estuviera puesto a cero", - "inset_classic": "Las sombras insertadas estarán usando {0}" + "inset_classic": "Las sombras interiores estarán usando {0}" }, "components": { "panel": "Panel", @@ -420,7 +420,7 @@ }, "fonts": { "_tab_label": "Fuentes", - "help": "Seleccione la fuente para utilizar para los elementos de la interfaz de usuario. Para \"personalizado\", debe ingresar el nombre exacto de la fuente tal como aparece en el sistema.", + "help": "Seleccione la fuente a utilizar para los elementos de la interfaz de usuario. Para \"personalizar\", debe ingresar el nombre exacto de la fuente tal como aparece en el sistema.", "components": { "interface": "Interfaz", "input": "Campos de entrada", @@ -479,7 +479,7 @@ "second_short": "{0}s", "seconds_short": "{0}s", "week": "{0} semana", - "weeks": "{0} semana", + "weeks": "{0} semanas", "week_short": "{0}sem", "weeks_short": "{0}sem", "year": "{0} año", @@ -507,11 +507,13 @@ "unpin": "Desclavar de tu perfil", "pinned": "Fijado", "delete_confirm": "¿Realmente quieres borrar la publicación?", - "reply_to": "Responder a", - "replies_list": "Respuestas:" + "reply_to": "Respondiendo a", + "replies_list": "Respuestas:", + "mute_conversation": "Silenciar la conversación", + "unmute_conversation": "Mostrar la conversación" }, "user_card": { - "approve": "Aprovar", + "approve": "Aprobar", "block": "Bloquear", "blocked": "¡Bloqueado!", "deny": "Denegar", @@ -538,8 +540,8 @@ "unblock": "Desbloquear", "unblock_progress": "Desbloqueando...", "block_progress": "Bloqueando...", - "unmute": "Desenmudecer", - "unmute_progress": "Sesenmudeciendo...", + "unmute": "Quitar silencio", + "unmute_progress": "Quitando silencio...", "mute_progress": "Silenciando...", "admin_menu": { "moderation": "Moderación", @@ -549,7 +551,7 @@ "revoke_moderator": "Revocar permisos de Moderador", "activate_account": "Activar cuenta", "deactivate_account": "Desactivar cuenta", - "delete_account": "Borrar cuenta", + "delete_account": "Eliminar cuenta", "force_nsfw": "Marcar todas las publicaciones como NSFW (no es seguro/apropiado para el trabajo)", "strip_media": "Eliminar archivos multimedia de las publicaciones", "force_unlisted": "Forzar que se publique en el modo -Sin Listar-", @@ -557,12 +559,12 @@ "disable_remote_subscription": "No permitir que usuarios de instancias remotas te siga.", "disable_any_subscription": "No permitir que ningún usuario te siga", "quarantine": "No permitir publicaciones de usuarios de instancias remotas", - "delete_user": "Borrar usuario", + "delete_user": "Eliminar usuario", "delete_user_confirmation": "¿Estás completamente seguro? Esta acción no se puede deshacer." } }, "user_profile": { - "timeline_title": "Linea temporal del usuario", + "timeline_title": "Linea Temporal del Usuario", "profile_does_not_exist": "Lo sentimos, este perfil no existe.", "profile_loading_error": "Lo sentimos, hubo un error al cargar este perfil." }, @@ -602,9 +604,20 @@ }, "search": { "people": "Personas", - "hashtags": "Hashtags", + "hashtags": "Etiquetas", "person_talking": "{count} personas hablando", "people_talking": "{count} gente hablando", "no_results": "Sin resultados" + }, + "password_reset": { + "forgot_password": "¿Contraseña olvidada?", + "password_reset": "Restablecer la contraseña", + "instruction": "Ingrese su dirección de correo electrónico o nombre de usuario. Le enviaremos un enlace para restablecer su contraseña.", + "placeholder": "Su correo electrónico o nombre de usuario", + "check_email": "Revise su correo electrónico para obtener un enlace para restablecer su contraseña.", + "return_home": "Volver a la página de inicio", + "not_found": "No pudimos encontrar ese correo electrónico o nombre de usuario.", + "too_many_requests": "Has alcanzado el límite de intentos, vuelve a intentarlo más tarde.", + "password_reset_disabled": "El restablecimiento de contraseñas está deshabilitado. Póngase en contacto con el administrador de su instancia." } }
\ No newline at end of file diff --git a/src/i18n/eu.json b/src/i18n/eu.json new file mode 100644 index 00000000..ad8f4c05 --- /dev/null +++ b/src/i18n/eu.json @@ -0,0 +1,623 @@ +{ + "chat": { + "title": "Txata" + }, + "exporter": { + "export": "Esportatu", + "processing": "Prozesatzen, zure fitxategia deskargatzeko eskatuko zaizu laster" + }, + "features_panel": { + "chat": "Txata", + "gopher": "Ghoper", + "media_proxy": "Media proxy", + "scope_options": "Ikusgaitasun aukerak", + "text_limit": "Testu limitea", + "title": "Ezaugarriak", + "who_to_follow": "Nori jarraitu" + }, + "finder": { + "error_fetching_user": "Errorea erabiltzailea eskuratzen", + "find_user": "Bilatu erabiltzailea" + }, + "general": { + "apply": "Aplikatu", + "submit": "Bidali", + "more": "Gehiago", + "generic_error": "Errore bat gertatu da", + "optional": "Hautazkoa", + "show_more": "Gehiago erakutsi", + "show_less": "Gutxiago erakutsi", + "cancel": "Ezeztatu", + "disable": "Ezgaitu", + "enable": "Gaitu", + "confirm": "Baieztatu", + "verify": "Egiaztatu" + }, + "image_cropper": { + "crop_picture": "Moztu argazkia", + "save": "Gorde", + "save_without_cropping": "Gorde moztu gabe", + "cancel": "Ezeztatu" + }, + "importer": { + "submit": "Bidali", + "success": "Ondo inportatu da.", + "error": "Errore bat gertatu da fitxategi hau inportatzerakoan." + }, + "login": { + "login": "Saioa hasi", + "description": "OAuth-ekin saioa hasi", + "logout": "Saioa itxi", + "password": "Pasahitza", + "placeholder": "adibidez Lain", + "register": "Erregistratu", + "username": "Erabiltzaile-izena", + "hint": "Hasi saioa eztabaidan parte-hartzeko", + "authentication_code": "Autentifikazio kodea", + "enter_recovery_code": "Sartu berreskuratze kodea", + "enter_two_factor_code": "Sartu bi-faktore kodea", + "recovery_code": "Berreskuratze kodea", + "heading": { + "totp": "Bi-faktore autentifikazioa", + "recovery": "Bi-faktore berreskuratzea" + } + }, + "media_modal": { + "previous": "Aurrekoa", + "next": "Hurrengoa" + }, + "nav": { + "about": "Honi buruz", + "back": "Atzera", + "chat": "Txat lokala", + "friend_requests": "Jarraitzeko eskaerak", + "mentions": "Aipamenak", + "interactions": "Interakzioak", + "dms": "Zuzeneko Mezuak", + "public_tl": "Denbora-lerro Publikoa", + "timeline": "Denbora-lerroa", + "twkn": "Ezagutzen den Sarea", + "user_search": "Erabiltzailea Bilatu", + "search": "Bilatu", + "who_to_follow": "Nori jarraitu", + "preferences": "Hobespenak" + }, + "notifications": { + "broken_favorite": "Egoera ezezaguna, bilatzen...", + "favorited_you": "zure mezua gogoko du", + "followed_you": "Zu jarraitzen zaitu", + "load_older": "Kargatu jakinarazpen zaharragoak", + "notifications": "Jakinarazpenak", + "read": "Irakurrita!", + "repeated_you": "zure mezua errepikatu du", + "no_more_notifications": "Ez dago jakinarazpen gehiago" + }, + "polls": { + "add_poll": "Inkesta gehitu", + "add_option": "Gehitu aukera", + "option": "Aukera", + "votes": "Bozkak", + "vote": "Bozka", + "type": "Inkesta mota", + "single_choice": "Aukera bakarra", + "multiple_choices": "Aukera anizkoitza", + "expiry": "Inkestaren iraupena", + "expires_in": "Inkesta {0} bukatzen da", + "expired": "Inkesta {0} bukatu zen", + "not_enough_options": "Aukera gutxiegi inkestan" + }, + "stickers": { + "add_sticker": "Pegatina gehitu" + }, + "interactions": { + "favs_repeats": "Errepikapen eta gogokoak", + "follows": "Jarraitzaile berriak", + "load_older": "Kargatu elkarrekintza zaharragoak" + }, + "post_status": { + "new_status": "Mezu berri bat idatzi", + "account_not_locked_warning": "Zure kontua ez dago {0}. Edozeinek jarraitzen hastearekin, zure mezuak irakur ditzake.", + "account_not_locked_warning_link": "Blokeatuta", + "attachments_sensitive": "Nabarmendu eranskinak hunkigarri gisa ", + "content_type": { + "text/plain": "Testu arrunta", + "text/html": "HTML", + "text/markdown": "Markdown", + "text/bbcode": "BBCode" + }, + "content_warning": "Gaia (hautazkoa)", + "default": "Iadanik Los Angeles-en", + "direct_warning_to_all": "Mezu hau aipatutako erabiltzaile guztientzat ikusgai egongo da.", + "direct_warning_to_first_only": "Mezu hau ikusgai egongo da bakarrik hasieran aipatzen diren erabiltzaileei.", + "posting": "Argitaratzen", + "scope_notice": { + "public": "Mezu hau guztiontzat ikusgai izango da", + "private": "Mezu hau zure jarraitzaileek soilik ikusiko dute", + "unlisted": "Mezu hau ez da argitaratuko Denbora-lerro Publikoan ezta Ezagutzen den Sarean" + }, + "scope": { + "direct": "Zuzena: Bidali aipatutako erabiltzaileei besterik ez", + "private": "Jarraitzaileentzako bakarrik: Bidali jarraitzaileentzat bakarrik", + "public": "Publikoa: Bistaratu denbora-lerro publikoetan", + "unlisted": "Zerrendatu gabea: ez bidali denbora-lerro publikoetara" + } + }, + "registration": { + "bio": "Biografia", + "email": "E-posta", + "fullname": "Erakutsi izena", + "password_confirm": "Pasahitza berretsi", + "registration": "Izena ematea", + "token": "Gonbidapen txartela", + "captcha": "CAPTCHA", + "new_captcha": "Klikatu irudia captcha berri bat lortzeko", + "username_placeholder": "Adibidez lain", + "fullname_placeholder": "Adibidez Lain Iwakura", + "bio_placeholder": "Adidibez.\nKaixo, Lain naiz.\nFedibertsoa gustokoa dut eta euskeraz hitzegiten dut.", + "validations": { + "username_required": "Ezin da hutsik utzi", + "fullname_required": "Ezin da hutsik utzi", + "email_required": "Ezin da hutsik utzi", + "password_required": "Ezin da hutsik utzi", + "password_confirmation_required": "Ezin da hutsik utzi", + "password_confirmation_match": "Pasahitzaren berdina izan behar du" + } + }, + "selectable_list": { + "select_all": "Hautatu denak" + }, + "settings": { + "app_name": "App izena", + "security": "Segurtasuna", + "enter_current_password_to_confirm": "Sar ezazu zure egungo pasahitza zure identitatea baieztatzeko", + "mfa": { + "otp": "OTP", + "setup_otp": "OTP konfiguratu", + "wait_pre_setup_otp": "OTP aurredoitzen", + "confirm_and_enable": "Baieztatu eta gaitu OTP", + "title": "Bi-faktore autentifikazioa", + "generate_new_recovery_codes": "Sortu berreskuratze kode berriak", + "warning_of_generate_new_codes": "Berreskuratze kode berriak sortzean, zure berreskuratze kode zaharrak ez dute balioko", + "recovery_codes": "Berreskuratze kodea", + "waiting_a_recovery_codes": "Babes-kopia kodeak jasotzen...", + "recovery_codes_warning": "Idatzi edo gorde kodeak leku seguruan - bestela ez dituzu berriro ikusiko. Zure 2FA aplikaziorako sarbidea eta berreskuratze kodeak galduz gero, zure kontutik blokeatuta egongo zara.", + "authentication_methods": "Autentifikazio metodoa", + "scan": { + "title": "Eskaneatu", + "desc": "Zure bi-faktore aplikazioa erabiliz, eskaneatu QR kode hau edo idatzi testu-gakoa:", + "secret_code": "Giltza" + }, + "verify": { + "desc": "Bi-faktore autentifikazioa gaitzeko, sar ezazu bi-faktore kodea zure app-tik" + } + }, + "attachmentRadius": "Eranskinak", + "attachments": "Eranskinak", + "autoload": "Gaitu karga automatikoa beheraino mugitzean", + "avatar": "Avatarra", + "avatarAltRadius": "Avatarra (Aipamenak)", + "avatarRadius": "Avatarrak", + "background": "Atzeko planoa", + "bio": "Biografia", + "block_export": "Bloke esportatzea", + "block_export_button": "Esportatu zure blokeak csv fitxategi batera", + "block_import": "Bloke inportazioa", + "block_import_error": "Errorea blokeak inportatzen", + "blocks_imported": "Blokeak inportaturik! Hauek prozesatzeak denbora hartuko du.", + "blocks_tab": "Blokeak", + "btnRadius": "Botoiak", + "cBlue": "Urdina (erantzun, jarraitu)", + "cGreen": "Berdea (Bertxiotu)", + "cOrange": "Laranja (Gogokoa)", + "cRed": "Gorria (ezeztatu)", + "change_password": "Pasahitza aldatu", + "change_password_error": "Arazao bat egon da zure pasahitza aldatzean", + "changed_password": "Pasahitza ondo aldatu da!", + "collapse_subject": "Bildu gaia daukaten mezuak", + "composing": "Idazten", + "confirm_new_password": "Baieztatu pasahitz berria", + "current_avatar": "Zure uneko avatarra", + "current_password": "Indarrean den pasahitza", + "current_profile_banner": "Zure profilaren banner-a", + "data_import_export_tab": "Datuak Inportatu / Esportatu", + "default_vis": "Lehenetsitako ikusgaitasunak", + "delete_account": "Ezabatu kontua", + "delete_account_description": "Betirako ezabatu zure kontua eta zure mezu guztiak", + "delete_account_error": "Arazo bat gertatu da zure kontua ezabatzerakoan. Arazoa jarraitu eskero, administratzailearekin harremanetan jarri.", + "delete_account_instructions": "Idatzi zure pasahitza kontua ezabatzeko.", + "avatar_size_instruction": "Avatar irudien gomendatutako gutxieneko tamaina 150x150 pixel dira.", + "export_theme": "Gorde aurre-ezarpena", + "filtering": "Iragazten", + "filtering_explanation": "Hitz hauek dituzten mezu guztiak isilduak izango dira. Lerro bakoitzeko bat", + "follow_export": "Jarraitzen dituzunak esportatu", + "follow_export_button": "Esportatu zure jarraitzaileak csv fitxategi batean", + "follow_import": "Jarraitzen dituzunak inportatu", + "follow_import_error": "Errorea jarraitzaileak inportatzerakoan", + "follows_imported": "Jarraitzaileak inportatuta! Prozesatzeak denbora pixka bat iraungo du.", + "foreground": "Aurreko planoa", + "general": "Orokorra", + "hide_attachments_in_convo": "Ezkutatu eranskinak elkarrizketatan ", + "hide_attachments_in_tl": "Ezkutatu eranskinak donbora-lerroan", + "hide_muted_posts": "Ezkutatu mutututako erabiltzaileen mezuak", + "max_thumbnails": "Mezu bakoitzeko argazki-miniatura kopuru maximoa", + "hide_isp": "Instantziari buruzko panela ezkutatu", + "preload_images": "Argazkiak aurrekargatu", + "use_one_click_nsfw": "Ireki eduki hunkigarria duten eranskinak klik batekin", + "hide_post_stats": "Ezkutatu mezuaren estatistikak (adibidez faborito kopurua)", + "hide_user_stats": "Ezkutatu erabiltzaile estatistikak (adibidez jarraitzaile kopurua)", + "hide_filtered_statuses": "Ezkutatu iragazitako mezuak", + "import_blocks_from_a_csv_file": "Blokeatutakoak inportatu CSV fitxategi batetik", + "import_followers_from_a_csv_file": "Inportatu jarraitzaileak csv fitxategi batetik", + "import_theme": "Kargatu aurre-ezarpena", + "inputRadius": "Sarrera eremuak", + "checkboxRadius": "Kuadrotxoak", + "instance_default": "(lehenetsia: {value})", + "instance_default_simple": "(lehenetsia)", + "interface": "Interfazea", + "interfaceLanguage": "Interfaze hizkuntza", + "invalid_theme_imported": "Hautatutako fitxategia ez da onartutako Pleroma gaia. Ez da zure gaian aldaketarik burutu.", + "limited_availability": "Ez dago erabilgarri zure nabigatzailean", + "links": "Estekak", + "lock_account_description": "Mugatu zure kontua soilik onartutako jarraitzaileei", + "loop_video": "Begizta bideoak", + "loop_video_silent_only": "Soinu gabeko bideoak begiztatu bakarrik (adibidez Mastodon-eko gif-ak)", + "mutes_tab": "Mututuak", + "play_videos_in_modal": "Erreproduzitu bideoak zuzenean multimedia erreproduzigailuan", + "use_contain_fit": "Eranskinak ez moztu miniaturetan", + "name": "Izena", + "name_bio": "Izena eta biografia", + "new_password": "Pasahitz berria", + "notification_visibility": "Erakusteko jakinarazpen motak", + "notification_visibility_follows": "Jarraitzaileak", + "notification_visibility_likes": "Gogokoak", + "notification_visibility_mentions": "Aipamenak", + "notification_visibility_repeats": "Errepikapenak", + "no_rich_text_description": "Kendu testu-formatu aberastuak mezu guztietatik", + "no_blocks": "Ez daude erabiltzaile blokeatutak", + "no_mutes": "Ez daude erabiltzaile mututuak", + "hide_follows_description": "Ez erakutsi nor jarraitzen ari naizen", + "hide_followers_description": "Ez erakutsi nor ari den ni jarraitzen", + "show_admin_badge": "Erakutsi Administratzaile etiketa nire profilan", + "show_moderator_badge": "Erakutsi Moderatzaile etiketa nire profilan", + "nsfw_clickthrough": "Gaitu klika hunkigarri eranskinak ezkutatzeko", + "oauth_tokens": "OAuth tokenak", + "token": "Tokena", + "refresh_token": "Berrgin Tokena", + "valid_until": "Baliozkoa Arte", + "revoke_token": "Ezeztatu", + "panelRadius": "Panelak", + "pause_on_unfocused": "Eguneraketa automatikoa gelditu fitxatik kanpo", + "presets": "Aurrezarpenak", + "profile_background": "Profilaren atzeko planoa", + "profile_banner": "Profilaren Banner-a", + "profile_tab": "Profila", + "radii_help": "Konfiguratu interfazearen ertzen biribiltzea (pixeletan)", + "replies_in_timeline": "Denbora-lerroko erantzunak", + "reply_link_preview": "Gaitu erantzun-estekaren aurrebista arratoiarekin", + "reply_visibility_all": "Erakutsi erantzun guztiak", + "reply_visibility_following": "Erakutsi bakarrik niri zuzendutako edo nik jarraitutako erabiltzaileen erantzunak", + "reply_visibility_self": "Erakutsi bakarrik niri zuzendutako erantzunak", + "autohide_floating_post_button": "Automatikoki ezkutatu Mezu Berriaren botoia (sakelako)", + "saving_err": "Errorea ezarpenak gordetzean", + "saving_ok": "Ezarpenak gordeta", + "search_user_to_block": "Bilatu zein blokeatu nahi duzun", + "search_user_to_mute": "Bilatu zein isilarazi nahi duzun", + "security_tab": "Segurtasuna", + "scope_copy": "Ikusgaitasun aukerak kopiatu mezua erantzuterakoan (Zuzeneko Mezuak beti kopiatzen dute)", + "minimal_scopes_mode": "Bildu ikusgaitasun aukerak", + "set_new_avatar": "Ezarri avatar berria", + "set_new_profile_background": "Ezarri atzeko plano berria", + "set_new_profile_banner": "Ezarri profil banner berria", + "settings": "Ezarpenak", + "subject_input_always_show": "Erakutsi beti gaiaren eremua", + "subject_line_behavior": "Gaia kopiatu erantzuterakoan", + "subject_line_email": "E-maila bezala: \"re: gaia\"", + "subject_line_mastodon": "Mastodon bezala: kopiatu den bezala", + "subject_line_noop": "Ez kopiatu", + "post_status_content_type": "Argitarapen formatua", + "stop_gifs": "GIF-a iniziatu arratoia gainean jarrita", + "streaming": "Gaitu mezu berrien karga goraino mugitzean", + "text": "Testua", + "theme": "Gaia", + "theme_help": "Erabili hex-kolore kodeak (#rrggbb) gaiaren koloreak pertsonalizatzeko.", + "theme_help_v2_1": "Zenbait osagaien koloreak eta opakutasuna ezeztatu ditzakezu kontrol-laukia aktibatuz, \"Garbitu dena\" botoia erabili aldaketak deusezteko.", + "theme_help_v2_2": "Sarreren batzuen azpian dauden ikonoak atzeko planoaren eta testuaren arteko kontrastearen adierazleak dira, kokatu arratoia gainean informazio zehatza eskuratzeko. Kontuan izan gardentasun kontrasteen adierazleek erabiltzen direnean, kasurik okerrena erakusten dutela.", + "tooltipRadius": "Argibideak/alertak", + "upload_a_photo": "Argazkia kargatu", + "user_settings": "Erabiltzaile Ezarpenak", + "values": { + "false": "ez", + "true": "bai" + }, + "notifications": "Jakinarazpenak", + "notification_setting": "Jaso pertsona honen jakinarazpenak:", + "notification_setting_follows": "Jarraitutako erabiltzaileak", + "notification_setting_non_follows": "Jarraitzen ez dituzun erabiltzaileak", + "notification_setting_followers": "Zu jarraitzen zaituzten erabiltzaileak", + "notification_setting_non_followers": "Zu jarraitzen ez zaituzten erabiltzaileak", + "notification_mutes": "Erabiltzaile jakin baten jakinarazpenak jasotzeari uzteko, isilarazi ezazu.", + "notification_blocks": "Erabiltzaile bat blokeatzeak jakinarazpen guztiak gelditzen ditu eta harpidetza ezeztatu.", + "enable_web_push_notifications": "Gaitu web jakinarazpenak", + "style": { + "switcher": { + "keep_color": "Mantendu koloreak", + "keep_shadows": "Mantendu itzalak", + "keep_opacity": "Mantendu opakotasuna", + "keep_roundness": "Mantendu biribiltasuna", + "keep_fonts": "Mantendu iturriak", + "save_load_hint": "\"Mantendu\" aukerak uneko konfiguratutako aukerak gordetzen ditu gaiak hautatzerakoan edo kargatzean, gai hauek esportatze garaian ere gordetzen ditu. Kontrol-lauki guztiak garbitzen direnean, esportazio-gaiak dena gordeko du.", + "reset": "Berrezarri", + "clear_all": "Garbitu dena", + "clear_opacity": "Garbitu opakotasuna" + }, + "common": { + "color": "Kolorea", + "opacity": "Opakotasuna", + "contrast": { + "hint": "Kontrastearen erlazioa {ratio} da, {level} {context}", + "level": { + "aa": "AA Mailako gidaliburua betetzen du (gutxienezkoa)", + "aaa": "AAA Mailako gidaliburua betetzen du (gomendatua)", + "bad": "ez ditu irisgarritasun arauak betetzen" + }, + "context": { + "18pt": "testu handientzat (+18pt)", + "text": "testuentzat" + } + } + }, + "common_colors": { + "_tab_label": "Ohikoa", + "main": "Ohiko koloreak", + "foreground_hint": "Ikusi \"Aurreratua\" fitxa kontrol zehatzagoa lortzeko", + "rgbo": "Ikono, azentu eta etiketak" + }, + "advanced_colors": { + "_tab_label": "Aurreratua", + "alert": "Alerten atzeko planoa", + "alert_error": "Errorea", + "badge": "Etiketen atzeko planoa", + "badge_notification": "Jakinarazpenak", + "panel_header": "Panelaren goiburua", + "top_bar": "Goiko barra", + "borders": "Ertzak", + "buttons": "Botoiak", + "inputs": "Sarrera eremuak", + "faint_text": "Testu itzalita" + }, + "radii": { + "_tab_label": "Biribiltasuna" + }, + "shadows": { + "_tab_label": "Itzal eta argiak", + "component": "Atala", + "override": "Berridatzi", + "shadow_id": "Itzala #{value}", + "blur": "Lausotu", + "spread": "Hedapena", + "inset": "Barrutik", + "hint": "Itzaletarako ere erabil dezakezu --aldagarri kolore balio gisa CSS3 aldagaiak erabiltzeko. Kontuan izan opakutasuna ezartzeak ez duela kasu honetan funtzionatuko.", + "filter_hint": { + "always_drop_shadow": "Kontuz, itzal honek beti erabiltzen du {0} nabigatzaileak onartzen duenean.", + "drop_shadow_syntax": "{0} ez du onartzen {1} parametroa eta {2} gako-hitza.", + "avatar_inset": "Kontuan izan behar da barruko eta kanpoko itzal konbinazioak, ez esparotako emaitzak ager daitezkeela atzeko plano gardena duten Avatarretan.", + "spread_zero": "Hedapena > 0 duten itzalak zero izango balitz bezala agertuko dira", + "inset_classic": "Barruko itzalak {0} erabiliko dute" + }, + "components": { + "panel": "Panela", + "panelHeader": "Panel goiburua", + "topBar": "Goiko barra", + "avatar": "Erabiltzailearen avatarra (profilan)", + "avatarStatus": "Erabiltzailearen avatarra (mezuetan)", + "popup": "Popup-ak eta argibideak", + "button": "Botoia", + "buttonHover": "Botoia (gainean)", + "buttonPressed": "Botoai (sakatuta)", + "buttonPressedHover": "Botoia (sakatuta+gainean)", + "input": "Sarrera eremuak" + } + }, + "fonts": { + "_tab_label": "Letra-tipoak", + "help": "Aukeratu letra-tipoak erabiltzailearen interfazean erabiltzeko. \"Pertsonalizatua\" letra-tipoan, sisteman agertzen den izen berdinarekin idatzi behar duzu.", + "components": { + "interface": "Interfazea", + "input": "Sarrera eremuak", + "post": "Mezuen testua", + "postCode": "Tarte-bakarreko testua mezuetan (testu-formatu aberastuak)" + }, + "family": "Letra-tipoaren izena", + "size": "Tamaina (px)", + "weight": "Pisua (lodiera)", + "custom": "Pertsonalizatua" + }, + "preview": { + "header": "Aurrebista", + "content": "Edukia", + "error": "Adibide errorea", + "button": "Botoia", + "text": "Hamaika {0} eta {1}", + "mono": "edukia", + "input": "Jadanik Los Angeles-en", + "faint_link": "laguntza", + "fine_print": "Irakurri gure {0} ezer erabilgarria ikasteko!", + "header_faint": "Ondo dago", + "checkbox": "Baldintzak berrikusi ditut", + "link": "esteka polita" + } + }, + "version": { + "title": "Bertsioa", + "backend_version": "Backend Bertsio", + "frontend_version": "Frontend Bertsioa" + } + }, + "time": { + "day": "{0} egun", + "days": "{0} egun", + "day_short": "{0}e", + "days_short": "{0}e", + "hour": "{0} ordu", + "hours": "{0} ordu", + "hour_short": "{0}o", + "hours_short": "{0}o", + "in_future": "{0} barru", + "in_past": "duela {0}", + "minute": "{0} minutu", + "minutes": "{0} minutu", + "minute_short": "{0}min", + "minutes_short": "{0}min", + "month": "{0} hilabete", + "months": "{0} hilabete", + "month_short": "{0}h", + "months_short": "{0}h", + "now": "oraintxe bertan", + "now_short": "orain", + "second": "{0} segundu", + "seconds": "{0} segundu", + "second_short": "{0}s", + "seconds_short": "{0}s", + "week": "{0} aste", + "weeks": "{0} aste", + "week_short": "{0}a", + "weeks_short": "{0}a", + "year": "{0} urte", + "years": "{0} urte", + "year_short": "{0}u", + "years_short": "{0}u" + }, + "timeline": { + "collapse": "Bildu", + "conversation": "Elkarrizketa", + "error_fetching": "Errorea eguneraketak eskuratzen", + "load_older": "Kargatu mezu zaharragoak", + "no_retweet_hint": "Mezu hau jarraitzailentzako bakarrik markatuta dago eta ezin da errepikatu", + "repeated": "Errepikatuta", + "show_new": "Berriena erakutsi", + "up_to_date": "Eguneratuta", + "no_more_statuses": "Ez daude mezu gehiago", + "no_statuses": "Mezurik gabe" + }, + "status": { + "favorites": "Gogokoak", + "repeats": "Errepikapenak", + "delete": "Mezua ezabatu", + "pin": "Profilan ainguratu", + "unpin": "Aingura ezeztatu profilatik", + "pinned": "Ainguratuta", + "delete_confirm": "Mezu hau benetan ezabatu nahi duzu?", + "reply_to": "Erantzuten", + "replies_list": "Erantzunak:", + "mute_conversation": "Elkarrizketa isilarazi", + "unmute_conversation": "Elkarrizketa aktibatu" + }, + "user_card": { + "approve": "Onartu", + "block": "Blokeatu", + "blocked": "Blokeatuta!", + "deny": "Ukatu", + "favorites": "Gogokoak", + "follow": "Jarraitu", + "follow_sent": "Eskaera bidalita!", + "follow_progress": "Eskatzen...", + "follow_again": "Eskaera berriro bidali?", + "follow_unfollow": "Jarraitzeari utzi", + "followees": "Jarraitzen", + "followers": "Jarraitzaileak", + "following": "Jarraitzen!", + "follows_you": "Jarraitzen dizu!", + "its_you": "Zu zara!", + "media": "Multimedia", + "mute": "Isilarazi", + "muted": "Isilduta", + "per_day": "eguneko", + "remote_follow": "Jarraitu", + "report": "Berri eman", + "statuses": "Mezuak", + "subscribe": "Harpidetu", + "unsubscribe": "Harpidetza ezeztatu", + "unblock": "Blokeoa kendu", + "unblock_progress": "Blokeoa ezeztatzen...", + "block_progress": "Blokeatzen...", + "unmute": "Isiltasuna kendu", + "unmute_progress": "Isiltasuna kentzen...", + "mute_progress": "Isiltzen...", + "admin_menu": { + "moderation": "Moderazioa", + "grant_admin": "Administratzaile baimena", + "revoke_admin": "Ezeztatu administratzaile baimena", + "grant_moderator": "Moderatzaile baimena", + "revoke_moderator": "Ezeztatu moderatzaile baimena", + "activate_account": "Aktibatu kontua", + "deactivate_account": "Desaktibatu kontua", + "delete_account": "Ezabatu kontua", + "force_nsfw": "Markatu mezu guztiak hunkigarri gisa", + "strip_media": "Kendu multimedia mezuetatik", + "force_unlisted": "Behartu mezuak listatu gabekoak izatea", + "sandbox": "Behartu zure jarraitzaileentzako bakarrik argitaratzera", + "disable_remote_subscription": "Ez utzi istantzia kanpoko erabiltzaileak zuri jarraitzea", + "disable_any_subscription": "Ez utzi beste erabiltzaileak zuri jarraitzea", + "quarantine": "Ez onartu mezuak beste instantzietatik", + "delete_user": "Erabiltzailea ezabatu", + "delete_user_confirmation": "Erabat ziur zaude? Ekintza hau ezin da desegin." + } + }, + "user_profile": { + "timeline_title": "Erabiltzailearen denbora-lerroa", + "profile_does_not_exist": "Barkatu, profil hau ez da existitzen.", + "profile_loading_error": "Barkatu, errore bat gertatu da profila kargatzean." + }, + "user_reporting": { + "title": "{0}-ri buruz berri ematen", + "add_comment_description": "Zure kexa moderatzaileei bidaliko da. Nahi baduzu zure kexaren zergatia idatz dezakezu:", + "additional_comments": "Iruzkin gehiago", + "forward_description": "Kontu hau beste instantzia batekoa da. Nahi duzu txostenaren kopia bat bidali ere?", + "forward_to": "{0}-ri birbidali", + "submit": "Bidali", + "generic_error": "Errore bat gertatu da zure eskaera prozesatzerakoan." + }, + "who_to_follow": { + "more": "Gehiago", + "who_to_follow": "Nori jarraitu" + }, + "tool_tip": { + "media_upload": "Multimedia igo", + "repeat": "Errepikatu", + "reply": "Erantzun", + "favorite": "Gogokoa", + "user_settings": "Erabiltzaile ezarpenak" + }, + "upload": { + "error": { + "base": "Igoerak huts egin du.", + "file_too_big": "Artxiboa haundiegia [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "Saiatu berriro geroago" + }, + "file_size_units": { + "B": "B", + "KiB": "KiB", + "MiB": "MiB", + "GiB": "GiB", + "TiB": "TiB" + } + }, + "search": { + "people": "Erabiltzaileak", + "hashtags": "Traolak", + "person_talking": "{count} pertsona hitzegiten", + "people_talking": "{count} jende hitzegiten", + "no_results": "Emaitzarik ez" + }, + "password_reset": { + "forgot_password": "Pasahitza ahaztua?", + "password_reset": "Pasahitza berrezarri", + "instruction": "Idatzi zure helbide elektronikoa edo erabiltzaile izena. Pasahitza berrezartzeko esteka bidaliko dizugu.", + "placeholder": "Zure e-posta edo erabiltzaile izena", + "check_email": "Begiratu zure posta elektronikoa pasahitza berrezarri ahal izateko.", + "return_home": "Itzuli hasierara", + "not_found": "Ezin izan dugu helbide elektroniko edo erabiltzaile hori aurkitu.", + "too_many_requests": "Saiakera gehiegi burutu ditzu, saiatu berriro geroxeago.", + "password_reset_disabled": "Pasahitza berrezartzea debekatuta dago. Mesedez, jarri harremanetan instantzia administratzailearekin." + } +}
\ No newline at end of file diff --git a/src/i18n/fi.json b/src/i18n/fi.json index f4179495..e7ed5408 100644 --- a/src/i18n/fi.json +++ b/src/i18n/fi.json @@ -278,8 +278,15 @@ "status": { "favorites": "Tykkäykset", "repeats": "Toistot", + "delete": "Poista", + "pin": "Kiinnitä profiiliisi", + "unpin": "Poista kiinnitys", + "pinned": "Kiinnitetty", + "delete_confirm": "Haluatko varmasti postaa viestin?", "reply_to": "Vastaus", - "replies_list": "Vastaukset:" + "replies_list": "Vastaukset:", + "mute_conversation": "Hiljennä keskustelu", + "unmute_conversation": "Poista hiljennys" }, "user_card": { "approve": "Hyväksy", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index c77f28a6..b4c6015d 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -78,6 +78,7 @@ "timeline": "タイムライン", "twkn": "つながっているすべてのネットワーク", "user_search": "ユーザーをさがす", + "search": "さがす", "who_to_follow": "おすすめユーザー", "preferences": "せってい" }, @@ -105,6 +106,9 @@ "expired": "いれふだは {0} まえに、おわりました", "not_enough_options": "ユニークなオプションが、たりません" }, + "stickers": { + "add_sticker": "ステッカーをふやす" + }, "interactions": { "favs_repeats": "リピートとおきにいり", "follows": "あたらしいフォロー", @@ -506,7 +510,9 @@ "pinned": "ピンどめ", "delete_confirm": "ほんとうに、このステータスを、けしてもいいですか?", "reply_to": "へんしん:", - "replies_list": "へんしん:" + "replies_list": "へんしん:", + "mute_conversation": "スレッドをミュートする", + "unmute_conversation": "スレッドをミュートするのをやめる" }, "user_card": { "approve": "うけいれ", @@ -531,6 +537,8 @@ "remote_follow": "リモートフォロー", "report": "つうほう", "statuses": "ステータス", + "subscribe": "サブスクライブ", + "unsubscribe": "サブスクライブをやめる", "unblock": "ブロックをやめる", "unblock_progress": "ブロックをとりけしています...", "block_progress": "ブロックしています...", @@ -595,5 +603,12 @@ "GiB": "GiB", "TiB": "TiB" } + }, + "search": { + "people": "ひとびと", + "hashtags": "ハッシュタグ", + "person_talking": "{count} にんが、はなしています", + "people_talking": "{count} にんが、はなしています", + "no_results": "みつかりませんでした" } } diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json index 992a2fa6..42bb53d4 100644 --- a/src/i18n/ja_pedantic.json +++ b/src/i18n/ja_pedantic.json @@ -78,6 +78,7 @@ "timeline": "タイムライン", "twkn": "接続しているすべてのネットワーク", "user_search": "ユーザーを探す", + "search": "検索", "who_to_follow": "おすすめユーザー", "preferences": "設定" }, @@ -105,6 +106,9 @@ "expired": "投票は {0} 前に終了しました", "not_enough_options": "相異なる選択肢が不足しています" }, + "stickers": { + "add_sticker": "ステッカーを追加" + }, "interactions": { "favs_repeats": "リピートとお気に入り", "follows": "新しいフォロワー", @@ -506,7 +510,9 @@ "pinned": "ピン留め", "delete_confirm": "本当にこのステータスを削除してもよろしいですか?", "reply_to": "返信", - "replies_list": "返信:" + "replies_list": "返信:", + "mute_conversation": "スレッドをミュート", + "unmute_conversation": "スレッドのミュートを解除" }, "user_card": { "approve": "受け入れ", @@ -531,6 +537,8 @@ "remote_follow": "リモートフォロー", "report": "通報", "statuses": "ステータス", + "subscribe": "購読", + "unsubscribe": "購読を解除", "unblock": "ブロック解除", "unblock_progress": "ブロックを解除しています...", "block_progress": "ブロックしています...", @@ -595,5 +603,12 @@ "GiB": "GiB", "TiB": "TiB" } + }, + "search": { + "people": "人々", + "hashtags": "ハッシュタグ", + "person_talking": "{count} 人が話しています", + "people_talking": "{count} 人が話しています", + "no_results": "見つかりませんでした" } } diff --git a/src/i18n/messages.js b/src/i18n/messages.js index 404a4079..89c8a8c8 100644 --- a/src/i18n/messages.js +++ b/src/i18n/messages.js @@ -16,6 +16,7 @@ const messages = { eo: require('./eo.json'), es: require('./es.json'), et: require('./et.json'), + eu: require('./eu.json'), fi: require('./fi.json'), fr: require('./fr.json'), ga: require('./ga.json'), @@ -32,6 +33,7 @@ const messages = { pt: require('./pt.json'), ro: require('./ro.json'), ru: require('./ru.json'), + te: require('./te.json'), zh: require('./zh.json') } diff --git a/src/i18n/nb.json b/src/i18n/nb.json index 298dc0b9..248b05bc 100644 --- a/src/i18n/nb.json +++ b/src/i18n/nb.json @@ -2,14 +2,18 @@ "chat": { "title": "Nettprat" }, + "exporter": { + "export": "Eksporter", + "processing": "Arbeider, du vil snart bli spurt om å laste ned filen din" + }, "features_panel": { "chat": "Nettprat", "gopher": "Gopher", "media_proxy": "Media proxy", "scope_options": "Velg mottakere", - "text_limit": "Tekst-grense", + "text_limit": "Tekstgrense", "title": "Egenskaper", - "who_to_follow": "Hvem å følge" + "who_to_follow": "Kontoer å følge" }, "finder": { "error_fetching_user": "Feil ved henting av bruker", @@ -17,23 +21,66 @@ }, "general": { "apply": "Bruk", - "submit": "Send" + "submit": "Send", + "more": "Mer", + "generic_error": "Det oppsto en feil", + "optional": "valgfritt", + "show_more": "Vis mer", + "show_less": "Vis mindre", + "cancel": "Avbryt", + "disable": "Slå av", + "enable": "Slå på", + "confirm": "Godta", + "verify": "Godkjenn" + }, + "image_cropper": { + "crop_picture": "Minsk bilde", + "save": "Lagre", + "save_without_cropping": "Lagre uten å minske bildet", + "cancel": "Avbryt" + }, + "importer": { + "submit": "Send", + "success": "Importering fullført", + "error": "Det oppsto en feil under importering av denne filen" }, "login": { "login": "Logg inn", + "description": "Log inn med OAuth", "logout": "Logg ut", "password": "Passord", "placeholder": "f. eks lain", "register": "Registrer", - "username": "Brukernavn" + "username": "Brukernavn", + "hint": "Logg inn for å delta i diskusjonen", + "authentication_code": "Verifikasjonskode", + "enter_recovery_code": "Skriv inn en gjenopprettingskode", + "enter_two_factor_code": "Skriv inn en to-faktors kode", + "recovery_code": "Gjenopprettingskode", + "heading" : { + "totp" : "To-faktors autentisering", + "recovery" : "To-faktors gjenoppretting" + } + }, + "media_modal": { + "previous": "Forrige", + "next": "Neste" }, "nav": { + "about": "Om", + "back": "Tilbake", "chat": "Lokal nettprat", "friend_requests": "Følgeforespørsler", "mentions": "Nevnt", + "interactions": "Interaksjooner", + "dms": "Direktemeldinger", "public_tl": "Offentlig Tidslinje", "timeline": "Tidslinje", - "twkn": "Det hele kjente nettverket" + "twkn": "Det hele kjente nettverket", + "user_search": "Søk etter brukere", + "search": "Søk", + "who_to_follow": "Kontoer å følge", + "preferences": "Innstillinger" }, "notifications": { "broken_favorite": "Ukjent status, leter etter den...", @@ -42,19 +89,52 @@ "load_older": "Last eldre varsler", "notifications": "Varslinger", "read": "Les!", - "repeated_you": "Gjentok din status" + "repeated_you": "Gjentok din status", + "no_more_notifications": "Ingen gjenstående varsler" + }, + "polls": { + "add_poll": "Legg til undersøkelse", + "add_option": "Legg til svaralternativ", + "option": "Svaralternativ", + "votes": "stemmer", + "vote": "Stem", + "type": "Undersøkelsestype", + "single_choice": "Enkeltvalg", + "multiple_choices": "Flervalg", + "expiry": "Undersøkelsestid", + "expires_in": "Undersøkelsen er over om {0}", + "expired": "Undersøkelsen ble ferdig {0} siden", + "not_enough_options": "For få unike svaralternativer i undersøkelsen" + }, + "stickers": { + "add_sticker": "Legg til klistremerke" + }, + "interactions": { + "favs_repeats": "Gjentakelser og favoritter", + "follows": "Nye følgere", + "load_older": "Last eldre interaksjoner" }, "post_status": { + "new_status": "Legg ut ny status", "account_not_locked_warning": "Kontoen din er ikke {0}. Hvem som helst kan følge deg for å se dine statuser til følgere", "account_not_locked_warning_link": "låst", "attachments_sensitive": "Merk vedlegg som sensitive", "content_type": { - "text/plain": "Klar tekst" + "text/plain": "Klar tekst", + "text/html": "HTML", + "text/markdown": "Markdown", + "text/bbcode": "BBCode" }, "content_warning": "Tema (valgfritt)", "default": "Landet akkurat i L.A.", - "direct_warning": "Denne statusen vil kun bli sett av nevnte brukere", + "direct_warning_to_all": "Denne statusen vil være synlig av nevnte brukere", + "direct_warning_to_first_only": "Denne statusen vil være synlig for de brukerene som blir nevnt først i statusen.", "posting": "Publiserer", + "scope_notice": { + "public": "Denne statusen vil være synlig for alle", + "private": "Denne statusen vil være synlig for dine følgere", + "unlisted": "Denne statusen vil ikke være synlig i Offentlig Tidslinje eller Det Hele Kjente Nettverket" + }, "scope": { "direct": "Direkte, publiser bare til nevnte brukere", "private": "Bare følgere, publiser bare til brukere som følger deg", @@ -68,9 +148,49 @@ "fullname": "Visningsnavn", "password_confirm": "Bekreft passord", "registration": "Registrering", - "token": "Invitasjons-bevis" + "token": "Invitasjons-bevis", + "captcha": "CAPTCHA", + "new_captcha": "Trykk på bildet for å få en ny captcha", + "username_placeholder": "f.eks. Lain Iwakura", + "fullname_placeholder": "f.eks. Lain Iwakura", + "bio_placeholder": "e.g.\nHei, jeg er Lain.\nJeg er en animert jente som bor i forstaden i Japan. Du kjenner meg kanskje fra the Wired.", + "validations": { + "username_required": "kan ikke stå tomt", + "fullname_required": "kan ikke stå tomt", + "email_required": "kan ikke stå tomt", + "password_required": "kan ikke stå tomt", + "password_confirmation_required": "kan ikke stå tomt", + "password_confirmation_match": "skal være det samme som passord" + } + }, + "selectable_list": { + "select_all": "Velg alle" }, "settings": { + "app_name": "Applikasjonsnavn", + "security": "Sikkerhet", + "enter_current_password_to_confirm": "Skriv inn ditt nåverende passord for å bekrefte din identitet", + "mfa": { + "otp" : "OTP", + "setup_otp" : "Set opp OTP", + "wait_pre_setup_otp" : "forhåndsstiller OTP", + "confirm_and_enable" : "Bekreft og slå på OTP", + "title": "To-faktors autentisering", + "generate_new_recovery_codes" : "Generer nye gjenopprettingskoder", + "warning_of_generate_new_codes" : "Når du genererer nye gjenopprettingskoder, vil de gamle slutte å fungere.", + "recovery_codes" : "Gjenopprettingskoder.", + "waiting_a_recovery_codes": "Mottar gjenopprettingskoder...", + "recovery_codes_warning" : "Skriv disse kodene ned eller plasser dem ett sikkert sted - ellers så vil du ikke se dem igjen. Dersom du mister tilgang til din to-faktors app og dine gjenopprettingskoder, vil du bli stengt ute av kontoen din.", + "authentication_methods" : "Autentiseringsmetoder", + "scan": { + "title": "Skann", + "desc": "Ved hjelp av din to-faktors applikasjon, skann denne QR-koden eller skriv inn tekstnøkkelen", + "secret_code": "Nøkkel" + }, + "verify": { + "desc": "For å skru på to-faktors autentisering, skriv inn koden i fra din to-faktors app:" + } + }, "attachmentRadius": "Vedlegg", "attachments": "Vedlegg", "autoload": "Automatisk lasting når du blar ned til bunnen", @@ -79,6 +199,12 @@ "avatarRadius": "Profilbilde", "background": "Bakgrunn", "bio": "Biografi", + "block_export": "Eksporter blokkeringer", + "block_export_button": "Eksporter blokkeringer til en csv fil", + "block_import": "Import blokkeringer", + "block_import_error": "Det oppsto en feil under importering av blokkeringer", + "blocks_imported": "Blokkeringer importert, det vil ta litt å prossesere dem", + "blocks_tab": "Blokkeringer", "btnRadius": "Knapper", "cBlue": "Blå (Svar, følg)", "cGreen": "Grønn (Gjenta)", @@ -88,6 +214,7 @@ "change_password_error": "Feil ved endring av passord", "changed_password": "Passord endret", "collapse_subject": "Sammenfold statuser med tema", + "composing": "komponering", "confirm_new_password": "Bekreft nytt passord", "current_avatar": "Ditt nåværende profilbilde", "current_password": "Nåværende passord", @@ -95,15 +222,15 @@ "data_import_export_tab": "Data import / eksport", "default_vis": "Standard visnings-omfang", "delete_account": "Slett konto", - "delete_account_description": "Slett din konto og alle dine statuser", + "delete_account_description": "Fjern din konto og alle dine meldinger for alltid.", "delete_account_error": "Det oppsto et problem ved sletting av kontoen din, hvis dette problemet forblir kontakt din administrator", "delete_account_instructions": "Skriv inn ditt passord i feltet nedenfor for å bekrefte sletting av konto", + "avatar_size_instruction": "Den anbefalte minste-størrelsen for profilbilder er 150x150 piksler", "export_theme": "Lagre tema", "filtering": "Filtrering", "filtering_explanation": "Alle statuser som inneholder disse ordene vil bli dempet, en kombinasjon av tegn per linje", "follow_export": "Eksporter følginger", "follow_export_button": "Eksporter følgingene dine til en .csv fil", - "follow_export_processing": "Jobber, du vil snart bli spurt om å laste ned filen din.", "follow_import": "Importer følginger", "follow_import_error": "Feil ved importering av følginger.", "follows_imported": "Følginger importert! Behandling vil ta litt tid.", @@ -111,10 +238,22 @@ "general": "Generell", "hide_attachments_in_convo": "Gjem vedlegg i samtaler", "hide_attachments_in_tl": "Gjem vedlegg på tidslinje", + "hide_muted_posts": "Gjem statuser i fra gjemte brukere", + "max_thumbnails": "Maks antall forhåndsbilder per status", + "hide_isp": "Gjem instans-spesifikt panel", + "preload_images": "Forhåndslast bilder", + "use_one_click_nsfw": "Åpne sensitive vedlegg med ett klikk", + "hide_post_stats": "Gjem status statistikk (f.eks. antall likes", + "hide_user_stats": "Gjem bruker statistikk (f.eks. antall følgere)", + "hide_filtered_statuses": "Gjem filtrerte statuser", + "import_blocks_from_a_csv_file": "Importer blokkeringer fra en csv fil", "import_followers_from_a_csv_file": "Importer følginger fra en csv fil", "import_theme": "Last tema", - "inputRadius": "Input felt", + "inputRadius": "Tekst felt", + "checkboxRadius": "Sjekkbokser", "instance_default": "(standard: {value})", + "instance_default_simple": "(standard)", + "interface": "Grensesnitt", "interfaceLanguage": "Grensesnitt-språk", "invalid_theme_imported": "Den valgte filen er ikke ett støttet Pleroma-tema, ingen endringer til ditt tema ble gjort", "limited_availability": "Ikke tilgjengelig i din nettleser", @@ -122,6 +261,9 @@ "lock_account_description": "Begrens din konto til bare godkjente følgere", "loop_video": "Gjenta videoer", "loop_video_silent_only": "Gjenta bare videoer uten lyd, (for eksempel Mastodon sine \"gifs\")", + "mutes_tab": "Dempinger", + "play_videos_in_modal": "Spill videoer direkte i media-avspilleren", + "use_contain_fit": "Ikke minsk vedlegget i forhåndsvisninger", "name": "Navn", "name_bio": "Navn & Biografi", "new_password": "Nytt passord", @@ -131,10 +273,16 @@ "notification_visibility_mentions": "Nevnt", "notification_visibility_repeats": "Gjentakelser", "no_rich_text_description": "Fjern all formatering fra statuser", + "no_blocks": "Ingen blokkeringer", + "no_mutes": "Ingen dempinger", + "hide_follows_description": "Ikke hvis hvem jeg følger", + "hide_followers_description": "Ikke hvis hvem som følger meg", + "show_admin_badge": "Hvis ett administratormerke på min profil", + "show_moderator_badge": "Hvis ett moderatormerke på min profil", "nsfw_clickthrough": "Krev trykk for å vise statuser som kan være upassende", "oauth_tokens": "OAuth Tokens", "token": "Pollett", - "refresh_token": "Refresh Token", + "refresh_token": "Fornyingspolett", "valid_until": "Gyldig til", "revoke_token": "Tilbakekall", "panelRadius": "Panel", @@ -149,25 +297,196 @@ "reply_visibility_all": "Vis alle svar", "reply_visibility_following": "Vis bare svar som er til meg eller folk jeg følger", "reply_visibility_self": "Vis bare svar som er til meg", + "autohide_floating_post_button": "Skjul Ny Status knapp automatisk (mobil)", "saving_err": "Feil ved lagring av innstillinger", "saving_ok": "Innstillinger lagret", + "search_user_to_block": "Søk etter hvem du vil blokkere", + "search_user_to_mute": "Søk etter hvem du vil dempe", "security_tab": "Sikkerhet", + "scope_copy": "Kopier mottakere når du svarer noen (Direktemeldinger blir alltid kopiert", + "minimal_scopes_mode": "Minimaliser mottakervalg", "set_new_avatar": "Rediger profilbilde", "set_new_profile_background": "Rediger profil-bakgrunn", "set_new_profile_banner": "Sett ny profil-banner", "settings": "Innstillinger", + "subject_input_always_show": "Alltid hvis tema-felt", + "subject_line_behavior": "Kopier tema når du svarer", + "subject_line_email": "Som email: \"re: tema\"", + "subject_line_mastodon": "Som mastodon: kopier som den er", + "subject_line_noop": "Ikke koper", + "post_status_content_type": "Status innholdstype", "stop_gifs": "Spill av GIFs når du holder over dem", "streaming": "Automatisk strømming av nye statuser når du har bladd til toppen", "text": "Tekst", "theme": "Tema", "theme_help": "Bruk heksadesimale fargekoder (#rrggbb) til å endre farge-temaet ditt.", + "theme_help_v2_1": "Du kan også overskrive noen komponenter sine farger og opasitet ved å sjekke av sjekkboksen, bruk \"Nullstill alt\" knappen for å fjerne alle overskrivelser.", + "theme_help_v2_2": "Ikoner under noen av innstillingene er bakgrunn/tekst kontrast indikatorer, hold over dem for detaljert informasjon. Vennligst husk at disse indikatorene viser det verste utfallet.", "tooltipRadius": "Verktøytips/advarsler", + "upload_a_photo": "Last opp ett bilde", "user_settings": "Brukerinstillinger", "values": { "false": "nei", "true": "ja" + }, + "notifications": "Varsler", + "notification_setting": "Motta varsler i fra:", + "notification_setting_follows": "Brukere du følger", + "notification_setting_non_follows": "Brukere du ikke følger", + "notification_setting_followers": "Brukere som følger deg", + "notification_setting_non_followers": "Brukere som ikke følger deg", + "notification_mutes": "For å stoppe å motta varsler i fra en spesifikk bruker, kan du dempe dem.", + "notification_blocks": "Hvis du blokkerer en bruker vil det stoppe alle varsler og i tilleg få dem til å slutte å følge deg", + "enable_web_push_notifications": "Skru på pushnotifikasjoner i nettlesere", + "style": { + "switcher": { + "keep_color": "Behold farger", + "keep_shadows": "Behold skygger", + "keep_opacity": "Behold opasitet", + "keep_roundness": "Behold rundhet", + "keep_fonts": "Behold fonter", + "save_load_hint": "\"Behold\" alternativer beholder de instillingene som er satt når du velger eller laster inn temaer, det lagrer også disse alternativene når du eksporterer ett tema, Når alle sjekkboksene er tomme, vil alt bli lagret når du eksporterer ett tema.", + "reset": "Still in på nytt", + "clear_all": "Nullstill alt", + "clear_opacity": "Nullstill opasitet" + }, + "common": { + "color": "Farge", + "opacity": "Opasitet", + "contrast": { + "hint": "Kontrast forholdet er {ratio}, it {level} {context}", + "level": { + "aa": "møter Nivå AA retningslinje (minimal)", + "aaa": "møter Nivå AAA retningslinje (recommended)", + "bad": "møter ingen tilgjengeligshetsretningslinjer" + }, + "context": { + "18pt": "for stor (18pt+) tekst", + "text": "for tekst" + } + } + }, + "common_colors": { + "_tab_label": "Vanlig", + "main": "Vanlige farger", + "foreground_hint": "Se \"Avansert\" fanen for mer detaljert kontroll", + "rgbo": "Ikoner, aksenter, merker" + }, + "advanced_colors": { + "_tab_label": "Avansert", + "alert": "Varslingsbakgrunn", + "alert_error": "Feil", + "badge": "Merkebakgrunn", + "badge_notification": "Varsling", + "panel_header": "Panelhode", + "top_bar": "Topplinje", + "borders": "Kanter", + "buttons": "Knapper", + "inputs": "Tekstfelt", + "faint_text": "Svak tekst" + }, + "radii": { + "_tab_label": "Rundhet" + }, + "shadows": { + "_tab_label": "Skygger og belysning", + "component": "Komponent", + "override": "Overskriv", + "shadow_id": "Skygge #{value}", + "blur": "Uklarhet", + "spread": "Spredning", + "inset": "Insett", + "hint": "For skygger kan du sette --variable som en fargeveerdi for å bruke CSS3 variabler. Vær oppmerksom på at å sette opasitet da ikke vil fungere her.", + "filter_hint": { + "always_drop_shadow": "Advarsel, denne skyggen bruker alltid {0} når nettleseren støtter det.", + "drop_shadow_syntax": "{0} støtter ikke {1} parameter og {2} nøkkelord.", + "avatar_inset": "Vær oppmerksom på at å kombinere både insatte og uinsatte skygger på profilbilder kan gi uforventede resultater med gjennomsiktige profilbilder.", + "spread_zero": "Skygger med spredning > 0 vil fremstå som de var satt til 0", + "inset_classic": "Insette skygger vil bruke {0}" + }, + "components": { + "panel": "Panel", + "panelHeader": "Panelhode", + "topBar": "Topplinje", + "avatar": "Profilbilde (i profilvisning)", + "avatarStatus": "Profilbilde (i statusvisning)", + "popup": "Popups og tooltips", + "button": "Knapp", + "buttonHover": "Knapp (holdt)", + "buttonPressed": "Knapp (nedtrykt)", + "buttonPressedHover": "Knapp (nedtrykt+holdt)", + "input": "Tekstfelt" + } + }, + "fonts": { + "_tab_label": "Fonter", + "help": "Velg font til elementene i brukergrensesnittet. For \"egendefinert\" må du skrive inn det nøyaktige font-navnet som det fremstår på systemet", + "components": { + "interface": "Grensesnitt", + "input": "Tekstfelt", + "post": "Statustekst", + "postCode": "Monospaced tekst i en status (rik tekst)" + }, + "family": "Font naavn", + "size": "Størrelse (i piksler)", + "weight": "Vekt (dristighet)", + "custom": "Egendefinert" + }, + "preview": { + "header": "Forhåndsvisning", + "content": "Innhold", + "error": "Eksempel feil", + "button": "Knapp", + "text": "Mye mer {0} og {1}", + "mono": "innhold", + "input": "Landet akkurat i L.A.", + "faint_link": "hjelpfull brukerveiledning", + "fine_print": "Les vår {0} for å lære ingenting nyttig!", + "header_faint": "Dette er OK", + "checkbox": "Jeg har skumlest vilkår og betingelser", + "link": "en flott liten link" + } + }, + "version": { + "title": "Versjon", + "backend_version": "Backend Versjon", + "frontend_version": "Frontend Versjon" } }, + "time": { + "day": "{0} dag", + "days": "{0} dager", + "day_short": "{0}d", + "days_short": "{0}d", + "hour": "{0} time", + "hours": "{0} timer", + "hour_short": "{0}t", + "hours_short": "{0}t", + "in_future": "om {0}", + "in_past": "{0} siden", + "minute": "{0} minutt", + "minutes": "{0} minutter", + "minute_short": "{0}min", + "minutes_short": "{0}min", + "month": "{0} måned", + "months": "{0} måneder", + "month_short": "{0}md.", + "months_short": "{0}md.", + "now": "akkurat nå", + "now_short": "nå", + "second": "{0} sekund", + "seconds": "{0} sekunder", + "second_short": "{0}s", + "seconds_short": "{0}s", + "week": "{0} uke", + "weeks": "{0} uker", + "week_short": "{0}u", + "weeks_short": "{0}u", + "year": "{0} år", + "years": "{0} år", + "year_short": "{0}år", + "years_short": "{0}år" + }, "timeline": { "collapse": "Sammenfold", "conversation": "Samtale", @@ -176,29 +495,116 @@ "no_retweet_hint": "Status er markert som bare til følgere eller direkte og kan ikke gjentas", "repeated": "gjentok", "show_new": "Vis nye", - "up_to_date": "Oppdatert" + "up_to_date": "Oppdatert", + "no_more_statuses": "Ingen flere statuser", + "no_statuses": "Ingen statuser" + }, + "status": { + "favorites": "Favoritter", + "repeats": "Gjentakelser", + "delete": "Slett status", + "pin": "Fremhev på profil", + "unpin": "Fjern fremhevelse", + "pinned": "Fremhevet", + "delete_confirm": "Har du virkelig lyst til å slette denne statusen?", + "reply_to": "Svar til", + "replies_list": "Svar:" }, "user_card": { "approve": "Godkjenn", "block": "Blokker", "blocked": "Blokkert!", "deny": "Avslå", + "favorites": "Favoritter", "follow": "Følg", + "follow_sent": "Forespørsel sendt!", + "follow_progress": "Forespør…", + "follow_again": "Gjenta forespørsel?", + "follow_unfollow": "Avfølg", "followees": "Følger", "followers": "Følgere", "following": "Følger!", "follows_you": "Følger deg!", + "its_you": "Det er deg!", + "media": "Media", "mute": "Demp", "muted": "Dempet", "per_day": "per dag", "remote_follow": "Følg eksternt", - "statuses": "Statuser" + "report": "Rapport", + "statuses": "Statuser", + "subscribe": "Abonner", + "unsubscribe": "Avabonner", + "unblock": "Fjern blokkering", + "unblock_progress": "Fjerner blokkering...", + "block_progress": "Blokkerer...", + "unmute": "Fjern demping", + "unmute_progress": "Fjerner demping...", + "mute_progress": "Demper...", + "admin_menu": { + "moderation": "Moderering", + "grant_admin": "Gi Administrator", + "revoke_admin": "Fjern Administrator", + "grant_moderator": "Gi Moderator", + "revoke_moderator": "Fjern Moderator", + "activate_account": "Aktiver konto", + "deactivate_account": "Deaktiver kontro", + "delete_account": "Slett konto", + "force_nsfw": "Merk alle statuser som sensitive", + "strip_media": "Fjern media i fra statuser", + "force_unlisted": "Tving statuser til å være uopplistet", + "sandbox": "Tving statuser til å bare vises til følgere", + "disable_remote_subscription": "Fjern mulighet til å følge brukeren fra andre instanser", + "disable_any_subscription": "Fjern mulighet til å følge brukeren", + "quarantine": "Gjør at statuser fra brukeren ikke kan sendes til andre instanser", + "delete_user": "Slett bruker", + "delete_user_confirmation": "Er du helt sikker? Denne handlingen kan ikke omgjøres." + } }, "user_profile": { - "timeline_title": "Bruker-tidslinje" + "timeline_title": "Bruker-tidslinje", + "profile_does_not_exist": "Beklager, denne profilen eksisterer ikke.", + "profile_loading_error": "Beklager, det oppsto en feil under lasting av denne profilen." + }, + "user_reporting": { + "title": "Rapporterer {0}", + "add_comment_description": "Rapporten blir sent til moderatorene av din instans. Du kan gi en forklaring på hvorfor du rapporterer denne kontoen under:", + "additional_comments": "Videre kommentarer", + "forward_description": "Denne kontoen er fra en annen server, vil du sende en kopi av rapporten til dem også?", + "forward_to": "Videresend til {0}", + "submit": "Send", + "generic_error": "Det oppsto en feil under behandling av din forespørsel." }, "who_to_follow": { "more": "Mer", - "who_to_follow": "Hvem å følge" + "who_to_follow": "Kontoer å følge" + }, + "tool_tip": { + "media_upload": "Last opp media", + "repeat": "Gjenta", + "reply": "Svar", + "favorite": "Lik", + "user_settings": "Brukerinnstillinger" + }, + "upload":{ + "error": { + "base": "Det oppsto en feil under opplastning.", + "file_too_big": "Fil for stor [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "Prøv igjen senere" + }, + "file_size_units": { + "B": "B", + "KiB": "KiB", + "MiB": "MiB", + "GiB": "GiB", + "TiB": "TiB" + } + }, + "search": { + "people": "Folk", + "hashtags": "Emneknagger", + "person_talking": "{count} person snakker om dette", + "people_talking": "{count} personer snakker om dette", + "no_results": "Ingen resultater" } } diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 90ed6664..3af65f40 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -389,5 +389,16 @@ "person_talking": "Популярно у {count} человека", "people_talking": "Популярно у {count} человек", "no_results": "Ничего не найдено" + }, + "password_reset": { + "forgot_password": "Забыли пароль?", + "password_reset": "Сброс пароля", + "instruction": "Введите ваш email или имя пользователя, и мы отправим вам ссылку для сброса пароля.", + "placeholder": "Ваш email или имя пользователя", + "check_email": "Проверьте ваш email и перейдите по ссылке для сброса пароля.", + "return_home": "Вернуться на главную страницу", + "not_found": "Мы не смогли найти аккаунт с таким email-ом или именем пользователя.", + "too_many_requests": "Вы исчерпали допустимое количество попыток, попробуйте позже.", + "password_reset_disabled": "Сброс пароля отключен. Cвяжитесь с администратором вашего сервера." } } diff --git a/src/i18n/te.json b/src/i18n/te.json new file mode 100644 index 00000000..f0953d97 --- /dev/null +++ b/src/i18n/te.json @@ -0,0 +1,352 @@ +{ + "chat.title": "చాట్", + "features_panel.chat": "చాట్", + "features_panel.gopher": "గోఫర్", + "features_panel.media_proxy": "మీడియా ప్రాక్సీ", + "features_panel.scope_options": "స్కోప్ ఎంపికలు", + "features_panel.text_limit": "వచన పరిమితి", + "features_panel.title": "లక్షణాలు", + "features_panel.who_to_follow": "ఎవరిని అనుసరించాలి", + "finder.error_fetching_user": "వినియోగదారుని పొందడంలో లోపం", + "finder.find_user": "వినియోగదారుని కనుగొనండి", + "general.apply": "వర్తించు", + "general.submit": "సమర్పించు", + "general.more": "మరిన్ని", + "general.generic_error": "ఒక తప్పిదం సంభవించినది", + "general.optional": "ఐచ్చికం", + "image_cropper.crop_picture": "చిత్రాన్ని కత్తిరించండి", + "image_cropper.save": "దాచు", + "image_cropper.save_without_cropping": "కత్తిరించకుండా సేవ్ చేయి", + "image_cropper.cancel": "రద్దుచేయి", + "login.login": "లాగిన్", + "login.description": "OAuth తో లాగిన్ అవ్వండి", + "login.logout": "లాగౌట్", + "login.password": "సంకేతపదము", + "login.placeholder": "ఉదా. lain", + "login.register": "నమోదు చేసుకోండి", + "login.username": "వాడుకరి పేరు", + "login.hint": "చర్చలో చేరడానికి లాగిన్ అవ్వండి", + "media_modal.previous": "ముందరి పుట", + "media_modal.next": "తరువాత", + "nav.about": "గురించి", + "nav.back": "వెనక్కి", + "nav.chat": "స్థానిక చాట్", + "nav.friend_requests": "అనుసరించడానికి అభ్యర్థనలు", + "nav.mentions": "ప్రస్తావనలు", + "nav.dms": "నేరుగా పంపిన సందేశాలు", + "nav.public_tl": "ప్రజా కాలక్రమం", + "nav.timeline": "కాలక్రమం", + "nav.twkn": "మొత్తం తెలిసిన నెట్వర్క్", + "nav.user_search": "వాడుకరి శోధన", + "nav.who_to_follow": "ఎవరిని అనుసరించాలి", + "nav.preferences": "ప్రాధాన్యతలు", + "notifications.broken_favorite": "తెలియని స్థితి, దాని కోసం శోధిస్తోంది...", + "notifications.favorited_you": "మీ స్థితిని ఇష్టపడ్డారు", + "notifications.followed_you": "మిమ్మల్ని అనుసరించారు", + "notifications.load_older": "పాత నోటిఫికేషన్లను లోడ్ చేయండి", + "notifications.notifications": "ప్రకటనలు", + "notifications.read": "చదివాను!", + "notifications.repeated_you": "మీ స్థితిని పునరావృతం చేసారు", + "notifications.no_more_notifications": "ఇక నోటిఫికేషన్లు లేవు", + "post_status.new_status": "క్రొత్త స్థితిని పోస్ట్ చేయండి", + "post_status.account_not_locked_warning": "మీ ఖాతా {౦} కాదు. ఎవరైనా మిమ్మల్ని అనుసరించి అనుచరులకు మాత్రమే ఉద్దేశించిన పోస్టులను చూడవచ్చు.", + "post_status.account_not_locked_warning_link": "తాళం వేయబడినది", + "post_status.attachments_sensitive": "జోడింపులను సున్నితమైనవిగా గుర్తించండి", + "post_status.content_type.text/plain": "సాధారణ అక్షరాలు", + "post_status.content_type.text/html": "హెచ్టిఎమ్ఎల్", + "post_status.content_type.text/markdown": "మార్క్డౌన్", + "post_status.content_warning": "విషయం (ఐచ్ఛికం)", + "post_status.default": "ఇప్పుడే విజయవాడలో దిగాను.", + "post_status.direct_warning": "ఈ పోస్ట్ మాత్రమే పేర్కొన్న వినియోగదారులకు మాత్రమే కనిపిస్తుంది.", + "post_status.posting": "పోస్ట్ చేస్తున్నా", + "post_status.scope.direct": "ప్రత్యక్ష - పేర్కొన్న వినియోగదారులకు మాత్రమే పోస్ట్ చేయబడుతుంది", + "post_status.scope.private": "అనుచరులకు మాత్రమే - అనుచరులకు మాత్రమే పోస్ట్ చేయబడుతుంది", + "post_status.scope.public": "పబ్లిక్ - ప్రజా కాలక్రమాలకు పోస్ట్ చేయబడుతుంది", + "post_status.scope.unlisted": "జాబితా చేయబడనిది - ప్రజా కాలక్రమాలకు పోస్ట్ చేయవద్దు", + "registration.bio": "బయో", + "registration.email": "ఈ మెయిల్", + "registration.fullname": "ప్రదర్శన పేరు", + "registration.password_confirm": "పాస్వర్డ్ నిర్ధారణ", + "registration.registration": "నమోదు", + "registration.token": "ఆహ్వాన టోకెన్", + "registration.captcha": "కాప్చా", + "registration.new_captcha": "కొత్త కాప్చా పొందుటకు చిత్రం మీద క్లిక్ చేయండి", + "registration.username_placeholder": "ఉదా. lain", + "registration.fullname_placeholder": "ఉదా. Lain Iwakura", + "registration.bio_placeholder": "e.g.\nHi, I'm Lain.\nI’m an anime girl living in suburban Japan. You may know me from the Wired.", + "registration.validations.username_required": "ఖాళీగా విడిచిపెట్టరాదు", + "registration.validations.fullname_required": "ఖాళీగా విడిచిపెట్టరాదు", + "registration.validations.email_required": "ఖాళీగా విడిచిపెట్టరాదు", + "registration.validations.password_required": "ఖాళీగా విడిచిపెట్టరాదు", + "registration.validations.password_confirmation_required": "ఖాళీగా విడిచిపెట్టరాదు", + "registration.validations.password_confirmation_match": "సంకేతపదం వలె ఉండాలి", + "settings.app_name": "అనువర్తన పేరు", + "settings.attachmentRadius": "జోడింపులు", + "settings.attachments": "జోడింపులు", + "settings.autoload": "క్రిందికి స్క్రోల్ చేయబడినప్పుడు స్వయంచాలక లోడింగ్ని ప్రారంభించు", + "settings.avatar": "అవతారం", + "settings.avatarAltRadius": "అవతారాలు (ప్రకటనలు)", + "settings.avatarRadius": "అవతారాలు", + "settings.background": "బ్యాక్గ్రౌండు", + "settings.bio": "బయో", + "settings.blocks_tab": "బ్లాక్లు", + "settings.btnRadius": "బటన్లు", + "settings.cBlue": "నీలం (ప్రత్యుత్తరం, అనుసరించండి)", + "settings.cGreen": "Green (Retweet)", + "settings.cOrange": "ఆరెంజ్ (ఇష్టపడు)", + "settings.cRed": "Red (Cancel)", + "settings.change_password": "పాస్వర్డ్ మార్చండి", + "settings.change_password_error": "మీ పాస్వర్డ్ను మార్చడంలో సమస్య ఉంది.", + "settings.changed_password": "పాస్వర్డ్ విజయవంతంగా మార్చబడింది!", + "settings.collapse_subject": "Collapse posts with subjects", + "settings.composing": "Composing", + "settings.confirm_new_password": "కొత్త పాస్వర్డ్ను నిర్ధారించండి", + "settings.current_avatar": "మీ ప్రస్తుత అవతారం", + "settings.current_password": "ప్రస్తుత పాస్వర్డ్", + "settings.current_profile_banner": "మీ ప్రస్తుత ప్రొఫైల్ బ్యానర్", + "settings.data_import_export_tab": "Data Import / Export", + "settings.default_vis": "Default visibility scope", + "settings.delete_account": "Delete Account", + "settings.delete_account_description": "మీ ఖాతా మరియు మీ అన్ని సందేశాలను శాశ్వతంగా తొలగించండి.", + "settings.delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.", + "settings.delete_account_instructions": "ఖాతా తొలగింపును నిర్ధారించడానికి దిగువ ఇన్పుట్లో మీ పాస్వర్డ్ను టైప్ చేయండి.", + "settings.avatar_size_instruction": "అవతార్ చిత్రాలకు సిఫార్సు చేసిన కనీస పరిమాణం 150x150 పిక్సెల్స్.", + "settings.export_theme": "Save preset", + "settings.filtering": "వడపోత", + "settings.filtering_explanation": "All statuses containing these words will be muted, one per line", + "settings.follow_export": "Follow export", + "settings.follow_export_button": "Export your follows to a csv file", + "settings.follow_export_processing": "Processing, you'll soon be asked to download your file", + "settings.follow_import": "Follow import", + "settings.follow_import_error": "అనుచరులను దిగుమతి చేయడంలో లోపం", + "settings.follows_imported": "Follows imported! Processing them will take a while.", + "settings.foreground": "Foreground", + "settings.general": "General", + "settings.hide_attachments_in_convo": "సంభాషణలలో జోడింపులను దాచు", + "settings.hide_attachments_in_tl": "కాలక్రమంలో జోడింపులను దాచు", + "settings.hide_muted_posts": "మ్యూట్ చేసిన వినియోగదారుల యొక్క పోస్ట్లను దాచిపెట్టు", + "settings.max_thumbnails": "Maximum amount of thumbnails per post", + "settings.hide_isp": "Hide instance-specific panel", + "settings.preload_images": "Preload images", + "settings.use_one_click_nsfw": "కేవలం ఒక క్లిక్ తో NSFW జోడింపులను తెరవండి", + "settings.hide_post_stats": "Hide post statistics (e.g. the number of favorites)", + "settings.hide_user_stats": "Hide user statistics (e.g. the number of followers)", + "settings.hide_filtered_statuses": "Hide filtered statuses", + "settings.import_followers_from_a_csv_file": "Import follows from a csv file", + "settings.import_theme": "Load preset", + "settings.inputRadius": "Input fields", + "settings.checkboxRadius": "Checkboxes", + "settings.instance_default": "(default: {value})", + "settings.instance_default_simple": "(default)", + "settings.interface": "Interface", + "settings.interfaceLanguage": "Interface language", + "settings.invalid_theme_imported": "The selected file is not a supported Pleroma theme. No changes to your theme were made.", + "settings.limited_availability": "మీ బ్రౌజర్లో అందుబాటులో లేదు", + "settings.links": "Links", + "settings.lock_account_description": "మీ ఖాతాను ఆమోదించిన అనుచరులకు మాత్రమే పరిమితం చేయండి", + "settings.loop_video": "Loop videos", + "settings.loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")", + "settings.mutes_tab": "మ్యూట్ చేయబడినవి", + "settings.play_videos_in_modal": "మీడియా వీక్షికలో నేరుగా వీడియోలను ప్లే చేయి", + "settings.use_contain_fit": "అటాచ్మెంట్ సూక్ష్మచిత్రాలను కత్తిరించవద్దు", + "settings.name": "Name", + "settings.name_bio": "పేరు & బయో", + "settings.new_password": "కొత్త సంకేతపదం", + "settings.notification_visibility": "చూపించవలసిన నోటిఫికేషన్ రకాలు", + "settings.notification_visibility_follows": "Follows", + "settings.notification_visibility_likes": "ఇష్టాలు", + "settings.notification_visibility_mentions": "ప్రస్తావనలు", + "settings.notification_visibility_repeats": "పునఃప్రసారాలు", + "settings.no_rich_text_description": "అన్ని పోస్ట్ల నుండి రిచ్ టెక్స్ట్ ఫార్మాటింగ్ను స్ట్రిప్ చేయండి", + "settings.no_blocks": "బ్లాక్స్ లేవు", + "settings.no_mutes": "మ్యూట్లు లేవు", + "settings.hide_follows_description": "నేను ఎవరిని అనుసరిస్తున్నానో చూపించవద్దు", + "settings.hide_followers_description": "నన్ను ఎవరు అనుసరిస్తున్నారో చూపవద్దు", + "settings.show_admin_badge": "నా ప్రొఫైల్ లో అడ్మిన్ బ్యాడ్జ్ చూపించు", + "settings.show_moderator_badge": "నా ప్రొఫైల్లో మోడరేటర్ బ్యాడ్జ్ని చూపించు", + "settings.nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding", + "settings.oauth_tokens": "OAuth tokens", + "settings.token": "Token", + "settings.refresh_token": "Refresh Token", + "settings.valid_until": "Valid Until", + "settings.revoke_token": "Revoke", + "settings.panelRadius": "Panels", + "settings.pause_on_unfocused": "Pause streaming when tab is not focused", + "settings.presets": "Presets", + "settings.profile_background": "Profile Background", + "settings.profile_banner": "Profile Banner", + "settings.profile_tab": "Profile", + "settings.radii_help": "Set up interface edge rounding (in pixels)", + "settings.replies_in_timeline": "Replies in timeline", + "settings.reply_link_preview": "Enable reply-link preview on mouse hover", + "settings.reply_visibility_all": "Show all replies", + "settings.reply_visibility_following": "Only show replies directed at me or users I'm following", + "settings.reply_visibility_self": "Only show replies directed at me", + "settings.saving_err": "Error saving settings", + "settings.saving_ok": "Settings saved", + "settings.security_tab": "Security", + "settings.scope_copy": "Copy scope when replying (DMs are always copied)", + "settings.set_new_avatar": "Set new avatar", + "settings.set_new_profile_background": "Set new profile background", + "settings.set_new_profile_banner": "Set new profile banner", + "settings.settings": "Settings", + "settings.subject_input_always_show": "Always show subject field", + "settings.subject_line_behavior": "Copy subject when replying", + "settings.subject_line_email": "Like email: \"re: subject\"", + "settings.subject_line_mastodon": "Like mastodon: copy as is", + "settings.subject_line_noop": "Do not copy", + "settings.post_status_content_type": "Post status content type", + "settings.stop_gifs": "Play-on-hover GIFs", + "settings.streaming": "Enable automatic streaming of new posts when scrolled to the top", + "settings.text": "Text", + "settings.theme": "Theme", + "settings.theme_help": "Use hex color codes (#rrggbb) to customize your color theme.", + "settings.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.", + "settings.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.", + "settings.tooltipRadius": "Tooltips/alerts", + "settings.upload_a_photo": "Upload a photo", + "settings.user_settings": "User Settings", + "settings.values.false": "no", + "settings.values.true": "yes", + "settings.notifications": "Notifications", + "settings.enable_web_push_notifications": "Enable web push notifications", + "settings.style.switcher.keep_color": "Keep colors", + "settings.style.switcher.keep_shadows": "Keep shadows", + "settings.style.switcher.keep_opacity": "Keep opacity", + "settings.style.switcher.keep_roundness": "Keep roundness", + "settings.style.switcher.keep_fonts": "Keep fonts", + "settings.style.switcher.save_load_hint": "\"Keep\" options preserve currently set options when selecting or loading themes, it also stores said options when exporting a theme. When all checkboxes unset, exporting theme will save everything.", + "settings.style.switcher.reset": "Reset", + "settings.style.switcher.clear_all": "Clear all", + "settings.style.switcher.clear_opacity": "Clear opacity", + "settings.style.common.color": "Color", + "settings.style.common.opacity": "Opacity", + "settings.style.common.contrast.hint": "Contrast ratio is {ratio}, it {level} {context}", + "settings.style.common.contrast.level.aa": "meets Level AA guideline (minimal)", + "settings.style.common.contrast.level.aaa": "meets Level AAA guideline (recommended)", + "settings.style.common.contrast.level.bad": "doesn't meet any accessibility guidelines", + "settings.style.common.contrast.context.18pt": "for large (18pt+) text", + "settings.style.common.contrast.context.text": "for text", + "settings.style.common_colors._tab_label": "Common", + "settings.style.common_colors.main": "Common colors", + "settings.style.common_colors.foreground_hint": "See \"Advanced\" tab for more detailed control", + "settings.style.common_colors.rgbo": "Icons, accents, badges", + "settings.style.advanced_colors._tab_label": "Advanced", + "settings.style.advanced_colors.alert": "Alert background", + "settings.style.advanced_colors.alert_error": "Error", + "settings.style.advanced_colors.badge": "Badge background", + "settings.style.advanced_colors.badge_notification": "Notification", + "settings.style.advanced_colors.panel_header": "Panel header", + "settings.style.advanced_colors.top_bar": "Top bar", + "settings.style.advanced_colors.borders": "Borders", + "settings.style.advanced_colors.buttons": "Buttons", + "settings.style.advanced_colors.inputs": "Input fields", + "settings.style.advanced_colors.faint_text": "Faded text", + "settings.style.radii._tab_label": "Roundness", + "settings.style.shadows._tab_label": "Shadow and lighting", + "settings.style.shadows.component": "Component", + "settings.style.shadows.override": "Override", + "settings.style.shadows.shadow_id": "Shadow #{value}", + "settings.style.shadows.blur": "Blur", + "settings.style.shadows.spread": "Spread", + "settings.style.shadows.inset": "Inset", + "settings.style.shadows.hint": "For shadows you can also use --variable as a color value to use CSS3 variables. Please note that setting opacity won't work in this case.", + "settings.style.shadows.filter_hint.always_drop_shadow": "Warning, this shadow always uses {0} when browser supports it.", + "settings.style.shadows.filter_hint.drop_shadow_syntax": "{0} does not support {1} parameter and {2} keyword.", + "settings.style.shadows.filter_hint.avatar_inset": "Please note that combining both inset and non-inset shadows on avatars might give unexpected results with transparent avatars.", + "settings.style.shadows.filter_hint.spread_zero": "Shadows with spread > 0 will appear as if it was set to zero", + "settings.style.shadows.filter_hint.inset_classic": "Inset shadows will be using {0}", + "settings.style.shadows.components.panel": "Panel", + "settings.style.shadows.components.panelHeader": "Panel header", + "settings.style.shadows.components.topBar": "Top bar", + "settings.style.shadows.components.avatar": "User avatar (in profile view)", + "settings.style.shadows.components.avatarStatus": "User avatar (in post display)", + "settings.style.shadows.components.popup": "Popups and tooltips", + "settings.style.shadows.components.button": "Button", + "settings.style.shadows.components.buttonHover": "Button (hover)", + "settings.style.shadows.components.buttonPressed": "Button (pressed)", + "settings.style.shadows.components.buttonPressedHover": "Button (pressed+hover)", + "settings.style.shadows.components.input": "Input field", + "settings.style.fonts._tab_label": "Fonts", + "settings.style.fonts.help": "Select font to use for elements of UI. For \"custom\" you have to enter exact font name as it appears in system.", + "settings.style.fonts.components.interface": "Interface", + "settings.style.fonts.components.input": "Input fields", + "settings.style.fonts.components.post": "Post text", + "settings.style.fonts.components.postCode": "Monospaced text in a post (rich text)", + "settings.style.fonts.family": "Font name", + "settings.style.fonts.size": "Size (in px)", + "settings.style.fonts.weight": "Weight (boldness)", + "settings.style.fonts.custom": "Custom", + "settings.style.preview.header": "Preview", + "settings.style.preview.content": "Content", + "settings.style.preview.error": "Example error", + "settings.style.preview.button": "Button", + "settings.style.preview.text": "A bunch of more {0} and {1}", + "settings.style.preview.mono": "content", + "settings.style.preview.input": "Just landed in L.A.", + "settings.style.preview.faint_link": "helpful manual", + "settings.style.preview.fine_print": "Read our {0} to learn nothing useful!", + "settings.style.preview.header_faint": "This is fine", + "settings.style.preview.checkbox": "I have skimmed over terms and conditions", + "settings.style.preview.link": "a nice lil' link", + "settings.version.title": "Version", + "settings.version.backend_version": "Backend Version", + "settings.version.frontend_version": "Frontend Version", + "timeline.collapse": "Collapse", + "timeline.conversation": "Conversation", + "timeline.error_fetching": "Error fetching updates", + "timeline.load_older": "Load older statuses", + "timeline.no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated", + "timeline.repeated": "repeated", + "timeline.show_new": "Show new", + "timeline.up_to_date": "Up-to-date", + "timeline.no_more_statuses": "No more statuses", + "timeline.no_statuses": "No statuses", + "status.reply_to": "Reply to", + "status.replies_list": "Replies:", + "user_card.approve": "Approve", + "user_card.block": "Block", + "user_card.blocked": "Blocked!", + "user_card.deny": "Deny", + "user_card.favorites": "Favorites", + "user_card.follow": "Follow", + "user_card.follow_sent": "Request sent!", + "user_card.follow_progress": "Requesting…", + "user_card.follow_again": "Send request again?", + "user_card.follow_unfollow": "Unfollow", + "user_card.followees": "Following", + "user_card.followers": "Followers", + "user_card.following": "Following!", + "user_card.follows_you": "Follows you!", + "user_card.its_you": "It's you!", + "user_card.media": "Media", + "user_card.mute": "Mute", + "user_card.muted": "Muted", + "user_card.per_day": "per day", + "user_card.remote_follow": "Remote follow", + "user_card.statuses": "Statuses", + "user_card.unblock": "Unblock", + "user_card.unblock_progress": "Unblocking...", + "user_card.block_progress": "Blocking...", + "user_card.unmute": "Unmute", + "user_card.unmute_progress": "Unmuting...", + "user_card.mute_progress": "Muting...", + "user_profile.timeline_title": "User Timeline", + "user_profile.profile_does_not_exist": "Sorry, this profile does not exist.", + "user_profile.profile_loading_error": "Sorry, there was an error loading this profile.", + "who_to_follow.more": "More", + "who_to_follow.who_to_follow": "Who to follow", + "tool_tip.media_upload": "Upload Media", + "tool_tip.repeat": "Repeat", + "tool_tip.reply": "Reply", + "tool_tip.favorite": "Favorite", + "tool_tip.user_settings": "User Settings", + "upload.error.base": "Upload failed.", + "upload.error.file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "upload.error.default": "Try again later", + "upload.file_size_units.B": "B", + "upload.file_size_units.KiB": "KiB", + "upload.file_size_units.MiB": "MiB", + "upload.file_size_units.GiB": "GiB", + "upload.file_size_units.TiB": "TiB" +} diff --git a/src/i18n/zh.json b/src/i18n/zh.json index da6dae5f..80c4e0d8 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -2,6 +2,10 @@ "chat": { "title": "聊天" }, + "exporter": { + "export": "导出", + "processing": "正在处理,稍后会提示您下载文件" + }, "features_panel": { "chat": "聊天", "gopher": "Gopher", @@ -17,23 +21,66 @@ }, "general": { "apply": "应用", - "submit": "提交" + "submit": "提交", + "more": "更多", + "generic_error": "发生一个错误", + "optional": "可选项", + "show_more": "显示更多", + "show_less": "显示更少", + "cancel": "取消", + "disable": "禁用", + "enable": "启用", + "confirm": "确认", + "verify": "验证" + }, + "image_cropper": { + "crop_picture": "裁剪图片", + "save": "保存", + "save_without_cropping": "保存未经裁剪的图片", + "cancel": "取消" + }, + "importer": { + "submit": "提交", + "success": "导入成功。", + "error": "导入此文件时出现一个错误。" }, "login": { "login": "登录", + "description": "用 OAuth 登录", "logout": "登出", "password": "密码", "placeholder": "例如:lain", "register": "注册", - "username": "用户名" + "username": "用户名", + "hint": "登录后加入讨论", + "authentication_code": "验证码", + "enter_recovery_code": "输入一个恢复码", + "enter_two_factor_code": "输入一个双重因素验证码", + "recovery_code": "恢复码", + "heading" : { + "totp" : "双重因素验证", + "recovery" : "双重因素恢复" + } + }, + "media_modal": { + "previous": "往前", + "next": "往后" }, "nav": { + "about": "关于", + "back": "Back", "chat": "本地聊天", "friend_requests": "关注请求", "mentions": "提及", + "interactions": "互动", + "dms": "私信", "public_tl": "公共时间线", "timeline": "时间线", - "twkn": "所有已知网络" + "twkn": "所有已知网络", + "user_search": "用户搜索", + "search": "搜索", + "who_to_follow": "推荐关注", + "preferences": "偏好设置" }, "notifications": { "broken_favorite": "未知的状态,正在搜索中...", @@ -42,24 +89,57 @@ "load_older": "加载更早的通知", "notifications": "通知", "read": "阅读!", - "repeated_you": "转发了你的状态" + "repeated_you": "转发了你的状态", + "no_more_notifications": "没有更多的通知" + }, + "polls": { + "add_poll": "增加问卷调查", + "add_option": "增加选项", + "option": "选项", + "votes": "投票", + "vote": "投票", + "type": "问卷类型", + "single_choice": "单选项", + "multiple_choices": "多选项", + "expiry": "问卷的时间", + "expires_in": "投票于 {0} 内结束", + "expired": "投票 {0} 前已结束", + "not_enough_options": "投票的选项太少" + }, + "stickers": { + "add_sticker": "添加贴纸" + }, + "interactions": { + "favs_repeats": "转发和收藏", + "follows": "新的关注着", + "load_older": "加载更早的互动" }, "post_status": { + "new_status": "发布新状态", "account_not_locked_warning": "你的帐号没有 {0}。任何人都可以关注你并浏览你的上锁内容。", "account_not_locked_warning_link": "上锁", "attachments_sensitive": "标记附件为敏感内容", "content_type": { - "text/plain": "纯文本" + "text/plain": "纯文本", + "text/html": "HTML", + "text/markdown": "Markdown", + "text/bbcode": "BBCode" }, "content_warning": "主题(可选)", "default": "刚刚抵达上海", - "direct_warning": "本条内容只有被提及的用户能够看到。", + "direct_warning_to_all": "本条内容只有被提及的用户能够看到。", + "direct_warning_to_first_only": "本条内容只有被在消息开始处提及的用户能够看到。", "posting": "发送", + "scope_notice": { + "public": "本条内容可以被所有人看到", + "private": "关注你的人才能看到本条内容", + "unlisted": "本条内容既不在公共时间线,也不会在所有已知网络上可见" + }, "scope": { "direct": "私信 - 只发送给被提及的用户", "private": "仅关注者 - 只有关注了你的人能看到", "public": "公共 - 发送到公共时间轴", - "unlisted": "不公开 - 所有人可见,但不会发送到公共时间轴" + "unlisted": "不公开 - 不会发送到公共时间轴" } }, "registration": { @@ -68,9 +148,49 @@ "fullname": "全名", "password_confirm": "确认密码", "registration": "注册", - "token": "邀请码" + "token": "邀请码", + "captcha": "CAPTCHA", + "new_captcha": "点击图片获取新的验证码", + "username_placeholder": "例如: lain", + "fullname_placeholder": "例如: Lain Iwakura", + "bio_placeholder": "例如:\n你好, 我是 Lain.\n我是一个住在上海的宅男。你可能在某处见过我。", + "validations": { + "username_required": "不能留空", + "fullname_required": "不能留空", + "email_required": "不能留空", + "password_required": "不能留空", + "password_confirmation_required": "不能留空", + "password_confirmation_match": "密码不一致" + } + }, + "selectable_list": { + "select_all": "选择全部" }, "settings": { + "app_name": "App 名称", + "security": "安全", + "enter_current_password_to_confirm": "输入你当前密码来确认你的身份", + "mfa": { + "otp" : "OTP", + "setup_otp" : "设置 OTP", + "wait_pre_setup_otp" : "预设 OTP", + "confirm_and_enable" : "确认并启用 OTP", + "title": "双因素验证", + "generate_new_recovery_codes" : "生成新的恢复码", + "warning_of_generate_new_codes" : "当你生成新的恢复码时,你的就恢复码就失效了。", + "recovery_codes" : "恢复码。", + "waiting_a_recovery_codes": "接受备份码。。。", + "recovery_codes_warning" : "抄写这些号码,或者保存在安全的地方。这些号码不会再次显示。如果你无法访问你的 2FA app,也丢失了你的恢复码,你的账号就再也无法登录了。", + "authentication_methods" : "身份验证方法", + "scan": { + "title": "扫一下", + "desc": "使用你的双因素验证 app,扫描这个二维码,或者输入这些文字密钥:", + "secret_code": "密钥" + }, + "verify": { + "desc": "要启用双因素验证,请把你的双因素验证 app 里的数字输入:" + } + }, "attachmentRadius": "附件", "attachments": "附件", "autoload": "启用滚动到底部时的自动加载", @@ -79,6 +199,12 @@ "avatarRadius": "头像", "background": "背景", "bio": "简介", + "block_export": "拉黑名单导出", + "block_export_button": "导出你的拉黑名单到一个 csv 文件", + "block_import": "拉黑名单导入", + "block_import_error": "导入拉黑名单出错", + "blocks_imported": "拉黑名单导入成功!需要一点时间来处理。", + "blocks_tab": "块", "btnRadius": "按钮", "cBlue": "蓝色(回复,关注)", "cGreen": "绿色(转发)", @@ -88,6 +214,7 @@ "change_password_error": "修改密码的时候出了点问题。", "changed_password": "成功修改了密码!", "collapse_subject": "折叠带主题的内容", + "composing": "正在书写", "confirm_new_password": "确认新密码", "current_avatar": "当前头像", "current_password": "当前密码", @@ -98,12 +225,12 @@ "delete_account_description": "永久删除你的帐号和所有消息。", "delete_account_error": "删除账户时发生错误,如果一直删除不了,请联系实例管理员。", "delete_account_instructions": "在下面输入你的密码来确认删除账户", + "avatar_size_instruction": "推荐的头像图片最小的尺寸是 150x150 像素。", "export_theme": "导出预置主题", "filtering": "过滤器", "filtering_explanation": "所有包含以下词汇的内容都会被隐藏,一行一个", "follow_export": "导出关注", "follow_export_button": "将关注导出成 csv 文件", - "follow_export_processing": "正在处理,过一会儿就可以下载你的文件了", "follow_import": "导入关注", "follow_import_error": "导入关注时错误", "follows_imported": "关注已导入!尚需要一些时间来处理。", @@ -111,12 +238,22 @@ "general": "通用", "hide_attachments_in_convo": "在对话中隐藏附件", "hide_attachments_in_tl": "在时间线上隐藏附件", + "hide_muted_posts": "不显示被隐藏的用户的帖子", + "max_thumbnails": "最多再每个帖子所能显示的缩略图数量", + "hide_isp": "隐藏指定实例的面板H", + "preload_images": "预载图片", + "use_one_click_nsfw": "点击一次以打开工作场所不适宜的附件", "hide_post_stats": "隐藏推文相关的统计数据(例如:收藏的次数)", "hide_user_stats": "隐藏用户的统计数据(例如:关注者的数量)", + "hide_filtered_statuses": "隐藏过滤的状态", + "import_blocks_from_a_csv_file": "从 csv 文件中导入拉黑名单", "import_followers_from_a_csv_file": "从 csv 文件中导入关注", "import_theme": "导入预置主题", "inputRadius": "输入框", + "checkboxRadius": "复选框", "instance_default": "(默认:{value})", + "instance_default_simple": "(默认)", + "interface": "界面", "interfaceLanguage": "界面语言", "invalid_theme_imported": "您所选择的主题文件不被 Pleroma 支持,因此主题未被修改。", "limited_availability": "在您的浏览器中无法使用", @@ -124,6 +261,9 @@ "lock_account_description": "你需要手动审核关注请求", "loop_video": "循环视频", "loop_video_silent_only": "只循环没有声音的视频(例如:Mastodon 里的“GIF”)", + "mutes_tab": "隐藏", + "play_videos_in_modal": "在弹出框内播放视频", + "use_contain_fit": "生成缩略图时不要裁剪附件。", "name": "名字", "name_bio": "名字及简介", "new_password": "新密码", @@ -133,9 +273,15 @@ "notification_visibility_mentions": "提及", "notification_visibility_repeats": "转发", "no_rich_text_description": "不显示富文本格式", + "no_blocks": "没有拉黑的", + "no_mutes": "没有隐藏", + "hide_follows_description": "不要显示我所关注的人", + "hide_followers_description": "不要显示关注我的人", + "show_admin_badge": "显示管理徽章", + "show_moderator_badge": "显示版主徽章", "nsfw_clickthrough": "将不和谐附件隐藏,点击才能打开", "oauth_tokens": "OAuth令牌", - "token": "代币", + "token": "令牌", "refresh_token": "刷新令牌", "valid_until": "有效期至", "revoke_token": "撤消", @@ -151,25 +297,196 @@ "reply_visibility_all": "显示所有回复", "reply_visibility_following": "只显示发送给我的回复/发送给我关注的用户的回复", "reply_visibility_self": "只显示发送给我的回复", + "autohide_floating_post_button": "自动隐藏新帖子的按钮(移动设备)", "saving_err": "保存设置时发生错误", "saving_ok": "设置已保存", + "search_user_to_block": "搜索你想屏蔽的用户", + "search_user_to_mute": "搜索你想要隐藏的用户", "security_tab": "安全", + "scope_copy": "回复时的复制范围(私信是总是复制的)", + "minimal_scopes_mode": "最小发文范围", "set_new_avatar": "设置新头像", "set_new_profile_background": "设置新的个人资料背景", "set_new_profile_banner": "设置新的横幅图片", "settings": "设置", + "subject_input_always_show": "总是显示主题框", + "subject_line_behavior": "回复时复制主题", + "subject_line_email": "比如电邮: \"re: 主题\"", + "subject_line_mastodon": "比如 mastodon: copy as is", + "subject_line_noop": "不要复制", + "post_status_content_type": "发文状态内容类型", "stop_gifs": "鼠标悬停时播放GIF", "streaming": "开启滚动到顶部时的自动推送", "text": "文本", "theme": "主题", "theme_help": "使用十六进制代码(#rrggbb)来设置主题颜色。", + "theme_help_v2_1": "你也可以通过切换复选框来覆盖某些组件的颜色和透明。使用“清除所有”来清楚所有覆盖设置。", + "theme_help_v2_2": "某些条目下的图标是背景或文本对比指示器,鼠标悬停可以获取详细信息。请记住,使用透明度来显示最差的情况。", "tooltipRadius": "提醒", + "upload_a_photo": "上传照片", "user_settings": "用户设置", "values": { "false": "否", "true": "是" + }, + "notifications": "通知", + "notification_setting": "通知来源:", + "notification_setting_follows": "你所关注的用户", + "notification_setting_non_follows": "你没有关注的用户", + "notification_setting_followers": "关注你的用户", + "notification_setting_non_followers": "没有关注你的用户", + "notification_mutes": "要停止收到某个指定的用户的通知,请使用隐藏功能。", + "notification_blocks": "拉黑一个用户会停掉所有他的通知,等同于取消关注。", + "enable_web_push_notifications": "启用 web 推送通知", + "style": { + "switcher": { + "keep_color": "保留颜色", + "keep_shadows": "保留阴影", + "keep_opacity": "保留透明度", + "keep_roundness": "保留圆角", + "keep_fonts": "保留字体", + "save_load_hint": "\"保留\" 选项在选择或加载主题时保留当前设置的选项,在导出主题时还会存储上述选项。当所有复选框未设置时,导出主题将保存所有内容。", + "reset": "重置", + "clear_all": "清除全部", + "clear_opacity": "清除透明度" + }, + "common": { + "color": "颜色", + "opacity": "透明度", + "contrast": { + "hint": "对比度是 {ratio}, 它 {level} {context}", + "level": { + "aa": "符合 AA 等级准则(最低)", + "aaa": "符合 AAA 等级准则(推荐)", + "bad": "不符合任何辅助功能指南" + }, + "context": { + "18pt": "大字文本 (18pt+)", + "text": "文本" + } + } + }, + "common_colors": { + "_tab_label": "常规", + "main": "常用颜色", + "foreground_hint": "点击”高级“ 标签进行细致的控制", + "rgbo": "图标,口音,徽章" + }, + "advanced_colors": { + "_tab_label": "高级", + "alert": "提醒或警告背景色", + "alert_error": "错误", + "badge": "徽章背景", + "badge_notification": "通知", + "panel_header": "面板标题", + "top_bar": "顶栏", + "borders": "边框", + "buttons": "按钮", + "inputs": "输入框", + "faint_text": "灰度文字" + }, + "radii": { + "_tab_label": "圆角" + }, + "shadows": { + "_tab_label": "阴影和照明", + "component": "组件", + "override": "覆盖", + "shadow_id": "阴影 #{value}", + "blur": "模糊", + "spread": "扩散", + "inset": "插入内部", + "hint": "对于阴影你还可以使用 --variable 作为颜色值来使用 CSS3 变量。请注意,这种情况下,透明设置将不起作用。", + "filter_hint": { + "always_drop_shadow": "警告,此阴影设置会总是使用 {0} ,如果浏览器支持的话。", + "drop_shadow_syntax": "{0} 不支持参数 {1} 和关键词 {2} 。", + "avatar_inset": "请注意组合两个内部和非内部的阴影到头像上,在透明头像上可能会有意料之外的效果。", + "spread_zero": "阴影的扩散 > 0 会同设置成零一样", + "inset_classic": "插入内部的阴影会使用 {0}" + }, + "components": { + "panel": "面板", + "panelHeader": "面板标题", + "topBar": "顶栏", + "avatar": "用户头像(在个人资料栏)", + "avatarStatus": "用户头像(在帖子显示栏)", + "popup": "弹窗和工具提示", + "button": "按钮", + "buttonHover": "按钮(悬停)", + "buttonPressed": "按钮(按下)", + "buttonPressedHover": "按钮(按下和悬停)", + "input": "输入框" + } + }, + "fonts": { + "_tab_label": "字体", + "help": "给用户界面的元素选择字体。选择 “自选”的你必须输入确切的字体名称。", + "components": { + "interface": "界面", + "input": "输入框", + "post": "发帖文字", + "postCode": "帖子中使用等间距文字(富文本)" + }, + "family": "字体名称", + "size": "大小 (in px)", + "weight": "字重 (粗体))", + "custom": "自选" + }, + "preview": { + "header": "预览", + "content": "内容", + "error": "例子错误", + "button": "按钮", + "text": "有堆 {0} 和 {1}", + "mono": "内容", + "input": "刚刚抵达上海", + "faint_link": "帮助菜单", + "fine_print": "阅读我们的 {0} 学不到什么东东!", + "header_faint": "这很正常", + "checkbox": "我已经浏览了 TOC", + "link": "一个很棒的摇滚链接" + } + }, + "version": { + "title": "版本", + "backend_version": "后端版本", + "frontend_version": "前端版本" } }, + "time": { + "day": "{0} 天", + "days": "{0} 天", + "day_short": "{0}d", + "days_short": "{0}d", + "hour": "{0} 小时", + "hours": "{0} 小时", + "hour_short": "{0}h", + "hours_short": "{0}h", + "in_future": "还有 {0}", + "in_past": "{0} 之前", + "minute": "{0} 分钟", + "minutes": "{0} 分钟", + "minute_short": "{0}min", + "minutes_short": "{0}min", + "month": "{0} 月", + "months": "{0} 月", + "month_short": "{0}mo", + "months_short": "{0}mo", + "now": "刚刚", + "now_short": "刚刚", + "second": "{0} 秒", + "seconds": "{0} 秒", + "second_short": "{0}s", + "seconds_short": "{0}s", + "week": "{0} 周", + "weeks": "{0} 周", + "week_short": "{0}w", + "weeks_short": "{0}w", + "year": "{0} 年", + "years": "{0} 年", + "year_short": "{0}y", + "years_short": "{0}y" + }, "timeline": { "collapse": "折叠", "conversation": "对话", @@ -178,29 +495,129 @@ "no_retweet_hint": "这条内容仅关注者可见,或者是私信,因此不能转发。", "repeated": "已转发", "show_new": "显示新内容", - "up_to_date": "已是最新" + "up_to_date": "已是最新", + "no_more_statuses": "没有更多的状态", + "no_statuses": "没有状态更新" + }, + "status": { + "favorites": "收藏", + "repeats": "转发", + "delete": "删除状态", + "pin": "在个人资料置顶", + "unpin": "取消在个人资料置顶", + "pinned": "置顶", + "delete_confirm": "你真的想要删除这条状态吗?", + "reply_to": "回复", + "replies_list": "回复:", + "mute_conversation": "隐藏对话", + "unmute_conversation": "对话取消隐藏" }, "user_card": { "approve": "允许", "block": "屏蔽", "blocked": "已屏蔽!", "deny": "拒绝", + "favorites": "收藏", "follow": "关注", + "follow_sent": "请求已发送!", + "follow_progress": "请求中", + "follow_again": "再次发送请求?", + "follow_unfollow": "取消关注", "followees": "正在关注", "followers": "关注者", "following": "正在关注!", "follows_you": "关注了你!", + "its_you": "就是你!!", + "media": "媒体", "mute": "隐藏", "muted": "已隐藏", "per_day": "每天", "remote_follow": "跨站关注", - "statuses": "状态" + "report": "报告", + "statuses": "状态", + "subscribe": "订阅", + "unsubscribe": "退订", + "unblock": "取消拉黑", + "unblock_progress": "取消拉黑中...", + "block_progress": "拉黑中...", + "unmute": "取消隐藏", + "unmute_progress": "取消隐藏中...", + "mute_progress": "隐藏中...", + "admin_menu": { + "moderation": "权限", + "grant_admin": "赋予管理权限", + "revoke_admin": "撤销管理权限", + "grant_moderator": "赋予版主权限", + "revoke_moderator": "撤销版主权限", + "activate_account": "激活账号", + "deactivate_account": "关闭账号", + "delete_account": "删除账号", + "force_nsfw": "标记所有的帖子都是 - 工作场合不适", + "strip_media": "从帖子里删除媒体文件", + "force_unlisted": "强制帖子为不公开", + "sandbox": "强制帖子为只有关注者可看", + "disable_remote_subscription": "禁止从远程实例关注用户", + "disable_any_subscription": "完全禁止关注用户", + "quarantine": "从联合实例中禁止用户帖子", + "delete_user": "删除用户", + "delete_user_confirmation": "你确认吗?此操作无法撤销。" + } }, "user_profile": { - "timeline_title": "用户时间线" + "timeline_title": "用户时间线", + "profile_does_not_exist": "抱歉,此个人资料不存在。", + "profile_loading_error": "抱歉,载入个人资料时出错。" + }, + "user_reporting": { + "title": "报告 {0}", + "add_comment_description": "此报告会发送给你的实例管理员。你可以在下面提供更多详细信息解释报告的缘由:", + "additional_comments": "其它信息", + "forward_description": "这个账号是从另外一个服务器。同时发送一个副本到那里?", + "forward_to": "转发 {0}", + "submit": "提交", + "generic_error": "当处理你的请求时,发生了一个错误。" }, "who_to_follow": { "more": "更多", "who_to_follow": "推荐关注" + }, + "tool_tip": { + "media_upload": "上传多媒体", + "repeat": "转发", + "reply": "回复", + "favorite": "收藏", + "user_settings": "用户设置" + }, + "upload":{ + "error": { + "base": "上传不成功。", + "file_too_big": "文件太大了 [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "迟些再试" + }, + "file_size_units": { + "B": "B", + "KiB": "KiB", + "MiB": "MiB", + "GiB": "GiB", + "TiB": "TiB" + } + }, + "search": { + "people": "人", + "hashtags": "Hashtags", + "person_talking": "{count} 人谈论", + "people_talking": "{count} 人谈论", + "no_results": "没有搜索结果" + }, + "password_reset": { + "forgot_password": "忘记密码了?", + "password_reset": "重置密码", + "instruction": "输入你的电邮地址或者用户名,我们将发送一个链接到你的邮箱,用于重置密码。", + "placeholder": "你的电邮地址或者用户名", + "check_email": "检查你的邮箱,会有一个链接用于重置密码。", + "return_home": "回到首页", + "not_found": "我们无法找到匹配的邮箱地址或者用户名。", + "too_many_requests": "你触发了尝试的限制,请稍后再试。", + "password_reset_disabled": "密码重置已经被禁用。请联系你的实例管理员。" } } diff --git a/src/main.js b/src/main.js index b3256e8e..a43d31e2 100644 --- a/src/main.js +++ b/src/main.js @@ -15,6 +15,7 @@ import mediaViewerModule from './modules/media_viewer.js' import oauthTokensModule from './modules/oauth_tokens.js' import reportsModule from './modules/reports.js' import pollsModule from './modules/polls.js' +import postStatusModule from './modules/postStatus.js' import VueI18n from 'vue-i18n' @@ -76,7 +77,8 @@ const persistedStateOptions = { mediaViewer: mediaViewerModule, oauthTokens: oauthTokensModule, reports: reportsModule, - polls: pollsModule + polls: pollsModule, + postStatus: postStatusModule }, plugins: [persistedState, pushNotifications], strict: false // Socket modifies itself, let's ignore this for now. diff --git a/src/modules/api.js b/src/modules/api.js index d51b31f3..eb6a7980 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -6,7 +6,6 @@ const api = { backendInteractor: backendInteractorService(), fetchers: {}, socket: null, - chatDisabled: false, followRequests: [] }, mutations: { @@ -25,9 +24,6 @@ const api = { setSocket (state, socket) { state.socket = socket }, - setChatDisabled (state, value) { - state.chatDisabled = value - }, setFollowRequests (state, value) { state.followRequests = value } @@ -55,17 +51,20 @@ const api = { setWsToken (store, token) { store.commit('setWsToken', token) }, - initializeSocket (store) { + initializeSocket ({ dispatch, commit, state, rootState }) { // Set up websocket connection - if (!store.state.chatDisabled) { - const token = store.state.wsToken + const token = state.wsToken + if (rootState.instance.chatAvailable && typeof token !== 'undefined' && state.socket === null) { const socket = new Socket('/socket', { params: { token } }) socket.connect() - store.dispatch('initializeChat', socket) + + commit('setSocket', socket) + dispatch('initializeChat', socket) } }, - disableChat (store) { - store.commit('setChatDisabled', true) + disconnectFromSocket ({ commit, state }) { + state.socket && state.socket.disconnect() + commit('setSocket', null) }, removeFollowRequest (store, request) { let requests = store.state.followRequests.filter((it) => it !== request) diff --git a/src/modules/chat.js b/src/modules/chat.js index e1b03bca..c798549d 100644 --- a/src/modules/chat.js +++ b/src/modules/chat.js @@ -1,16 +1,12 @@ const chat = { state: { messages: [], - channel: { state: '' }, - socket: null + channel: { state: '' } }, mutations: { setChannel (state, channel) { state.channel = channel }, - setSocket (state, socket) { - state.socket = socket - }, addMessage (state, message) { state.messages.push(message) state.messages = state.messages.slice(-19, 20) @@ -20,12 +16,8 @@ const chat = { } }, actions: { - disconnectFromChat (store) { - store.state.socket && store.state.socket.disconnect() - }, initializeChat (store, socket) { const channel = socket.channel('chat:public') - store.commit('setSocket', socket) channel.on('new_msg', (msg) => { store.commit('addMessage', msg) }) diff --git a/src/modules/instance.js b/src/modules/instance.js index 93b56577..7d602aa1 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -79,6 +79,11 @@ const instance = { case 'name': dispatch('setPageTitle') break + case 'chatAvailable': + if (value) { + dispatch('initializeSocket') + } + break } }, setTheme ({ commit }, themeName) { diff --git a/src/modules/postStatus.js b/src/modules/postStatus.js new file mode 100644 index 00000000..638c1fb2 --- /dev/null +++ b/src/modules/postStatus.js @@ -0,0 +1,25 @@ +const postStatus = { + state: { + params: null, + modalActivated: false + }, + mutations: { + openPostStatusModal (state, params) { + state.params = params + state.modalActivated = true + }, + closePostStatusModal (state) { + state.modalActivated = false + } + }, + actions: { + openPostStatusModal ({ commit }, params) { + commit('openPostStatusModal', params) + }, + closePostStatusModal ({ commit }) { + commit('closePostStatusModal') + } + } +} + +export default postStatus diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 7d5d5a67..918065d2 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -426,9 +426,13 @@ export const mutations = { newStatus.favoritedBy.push(user) } }, - setPinned (state, status) { + setMutedStatus (state, status) { const newStatus = state.allStatusesObject[status.id] - newStatus.pinned = status.pinned + newStatus.thread_muted = status.thread_muted + + if (newStatus.thread_muted !== undefined) { + state.conversationsObject[newStatus.statusnet_conversation_id].forEach(status => { status.thread_muted = newStatus.thread_muted }) + } }, setRetweeted (state, { status, value }) { const newStatus = state.allStatusesObject[status.id] @@ -556,13 +560,21 @@ const statuses = { rootState.api.backendInteractor.fetchPinnedStatuses(userId) .then(statuses => dispatch('addNewStatuses', { statuses, timeline: 'user', userId, showImmediately: true, noIdUpdate: true })) }, - pinStatus ({ rootState, commit }, statusId) { + pinStatus ({ rootState, dispatch }, statusId) { return rootState.api.backendInteractor.pinOwnStatus(statusId) - .then((status) => commit('setPinned', status)) + .then((status) => dispatch('addNewStatuses', { statuses: [status] })) }, - unpinStatus ({ rootState, commit }, statusId) { + unpinStatus ({ rootState, dispatch }, statusId) { rootState.api.backendInteractor.unpinOwnStatus(statusId) - .then((status) => commit('setPinned', status)) + .then((status) => dispatch('addNewStatuses', { statuses: [status] })) + }, + muteConversation ({ rootState, commit }, statusId) { + return rootState.api.backendInteractor.muteConversation(statusId) + .then((status) => commit('setMutedStatus', status)) + }, + unmuteConversation ({ rootState, commit }, statusId) { + return rootState.api.backendInteractor.unmuteConversation(statusId) + .then((status) => commit('setMutedStatus', status)) }, retweet ({ rootState, commit }, status) { // Optimistic retweeting... diff --git a/src/modules/users.js b/src/modules/users.js index 10def9cd..4d02f8d7 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -3,7 +3,6 @@ import oauthApi from '../services/new_api/oauth.js' import { compact, map, each, merge, last, concat, uniq } from 'lodash' import { set } from 'vue' import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js' -import { humanizeErrors } from './errors' // TODO: Unify with mergeOrAdd in statuses.js export const mergeOrAdd = (arr, obj, item) => { @@ -165,7 +164,7 @@ export const mutations = { state.currentUser.muteIds.push(muteId) } }, - setPinned (state, status) { + setPinnedToUser (state, status) { const user = state.usersObject[status.user.id] const index = user.pinnedStatusIds.indexOf(status.id) if (status.pinned && index === -1) { @@ -339,13 +338,13 @@ const users = { // Reconnect users to statuses store.commit('setUserForStatus', status) // Set pinned statuses to user - store.commit('setPinned', status) + store.commit('setPinnedToUser', status) }) each(compact(map(statuses, 'retweeted_status')), (status) => { // Reconnect users to retweets store.commit('setUserForStatus', status) // Set pinned retweets to user - store.commit('setPinned', status) + store.commit('setPinnedToUser', status) }) }, addNewNotifications (store, { notifications }) { @@ -382,16 +381,8 @@ const users = { store.dispatch('loginUser', data.access_token) } catch (e) { let errors = e.message - // replace ap_id with username - if (typeof errors === 'object') { - if (errors.ap_id) { - errors.username = errors.ap_id - delete errors.ap_id - } - errors = humanizeErrors(errors) - } store.commit('signUpFailure', errors) - throw Error(errors) + throw e } }, async getCaptcha (store) { @@ -419,7 +410,7 @@ const users = { }) .then(() => { store.commit('clearCurrentUser') - store.dispatch('disconnectFromChat') + store.dispatch('disconnectFromSocket') store.commit('clearToken') store.dispatch('stopFetching', 'friends') store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index d4ad1c4e..887d7d7a 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1,18 +1,14 @@ import { each, map, concat, last } from 'lodash' import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js' import 'whatwg-fetch' -import { StatusCodeError } from '../errors/errors' +import { RegistrationError, StatusCodeError } from '../errors/errors' /* eslint-env browser */ -const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json' const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json' const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import' const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' const CHANGE_PASSWORD_URL = '/api/pleroma/change_password' -const FOLLOW_REQUESTS_URL = '/api/pleroma/friend_requests' -const APPROVE_USER_URL = '/api/pleroma/friendships/approve' -const DENY_USER_URL = '/api/pleroma/friendships/deny' const TAG_USER_URL = '/api/pleroma/admin/users/tag' const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}` const ACTIVATION_STATUS_URL = screenName => `/api/pleroma/admin/users/${screenName}/activation_status` @@ -40,6 +36,9 @@ const MASTODON_FOLLOW_URL = id => `/api/v1/accounts/${id}/follow` const MASTODON_UNFOLLOW_URL = id => `/api/v1/accounts/${id}/unfollow` const MASTODON_FOLLOWING_URL = id => `/api/v1/accounts/${id}/following` const MASTODON_FOLLOWERS_URL = id => `/api/v1/accounts/${id}/followers` +const MASTODON_FOLLOW_REQUESTS_URL = '/api/v1/follow_requests' +const MASTODON_APPROVE_USER_URL = id => `/api/v1/follow_requests/${id}/authorize` +const MASTODON_DENY_USER_URL = id => `/api/v1/follow_requests/${id}/reject` const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = '/api/v1/timelines/direct' const MASTODON_PUBLIC_TIMELINE = '/api/v1/timelines/public' const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home' @@ -67,6 +66,8 @@ const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials' const MASTODON_REPORT_USER_URL = '/api/v1/reports' const MASTODON_PIN_OWN_STATUS = id => `/api/v1/statuses/${id}/pin` const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin` +const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute` +const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute` const MASTODON_SEARCH_2 = `/api/v2/search` const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search' @@ -199,12 +200,11 @@ const register = ({ params, credentials }) => { ...rest }) }) - .then((response) => [response.ok, response]) - .then(([ok, response]) => { - if (ok) { + .then((response) => { + if (response.ok) { return response.json() } else { - return response.json().then((error) => { throw new Error(error) }) + return response.json().then((error) => { throw new RegistrationError(error) }) } }) } @@ -219,14 +219,6 @@ const authHeaders = (accessToken) => { } } -const externalProfile = ({ profileUrl, credentials }) => { - let url = `${EXTERNAL_PROFILE_URL}?profileurl=${profileUrl}` - return fetch(url, { - headers: authHeaders(credentials), - method: 'GET' - }).then((data) => data.json()) -} - const followUser = ({ id, credentials }) => { let url = MASTODON_FOLLOW_URL(id) return fetch(url, { @@ -253,6 +245,16 @@ const unpinOwnStatus = ({ id, credentials }) => { .then((data) => parseStatus(data)) } +const muteConversation = ({ id, credentials }) => { + return promisedRequest({ url: MASTODON_MUTE_CONVERSATION(id), credentials, method: 'POST' }) + .then((data) => parseStatus(data)) +} + +const unmuteConversation = ({ id, credentials }) => { + return promisedRequest({ url: MASTODON_UNMUTE_CONVERSATION(id), credentials, method: 'POST' }) + .then((data) => parseStatus(data)) +} + const blockUser = ({ id, credentials }) => { return fetch(MASTODON_BLOCK_USER_URL(id), { headers: authHeaders(credentials), @@ -268,7 +270,7 @@ const unblockUser = ({ id, credentials }) => { } const approveUser = ({ id, credentials }) => { - let url = `${APPROVE_USER_URL}?user_id=${id}` + let url = MASTODON_APPROVE_USER_URL(id) return fetch(url, { headers: authHeaders(credentials), method: 'POST' @@ -276,7 +278,7 @@ const approveUser = ({ id, credentials }) => { } const denyUser = ({ id, credentials }) => { - let url = `${DENY_USER_URL}?user_id=${id}` + let url = MASTODON_DENY_USER_URL(id) return fetch(url, { headers: authHeaders(credentials), method: 'POST' @@ -352,9 +354,10 @@ const fetchFollowers = ({ id, maxId, sinceId, limit = 20, credentials }) => { } const fetchFollowRequests = ({ credentials }) => { - const url = FOLLOW_REQUESTS_URL + const url = MASTODON_FOLLOW_REQUESTS_URL return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) + .then((data) => data.map(parseUser)) } const fetchConversation = ({ id, credentials }) => { @@ -921,6 +924,8 @@ const apiService = { unfollowUser, pinOwnStatus, unpinOwnStatus, + muteConversation, + unmuteConversation, blockUser, unblockUser, fetchUser, @@ -952,7 +957,6 @@ const apiService = { updateBg, updateProfile, updateBanner, - externalProfile, importBlocks, importFollows, deleteAccount, diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index bdfe0465..3c44a10c 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -117,6 +117,8 @@ const backendInteractorService = credentials => { const fetchPinnedStatuses = (id) => apiService.fetchPinnedStatuses({ credentials, id }) const pinOwnStatus = (id) => apiService.pinOwnStatus({ credentials, id }) const unpinOwnStatus = (id) => apiService.unpinOwnStatus({ credentials, id }) + const muteConversation = (id) => apiService.muteConversation({ credentials, id }) + const unmuteConversation = (id) => apiService.unmuteConversation({ credentials, id }) const getCaptcha = () => apiService.getCaptcha() const register = (params) => apiService.register({ credentials, params }) @@ -125,8 +127,6 @@ const backendInteractorService = credentials => { const updateBanner = ({ banner }) => apiService.updateBanner({ credentials, banner }) const updateProfile = ({ params }) => apiService.updateProfile({ credentials, params }) - const externalProfile = (profileUrl) => apiService.externalProfile({ profileUrl, credentials }) - const importBlocks = (file) => apiService.importBlocks({ file, credentials }) const importFollows = (file) => apiService.importFollows({ file, credentials }) @@ -178,6 +178,8 @@ const backendInteractorService = credentials => { fetchPinnedStatuses, pinOwnStatus, unpinOwnStatus, + muteConversation, + unmuteConversation, tagUser, untagUser, addRight, @@ -190,7 +192,6 @@ const backendInteractorService = credentials => { updateBg, updateBanner, updateProfile, - externalProfile, importBlocks, importFollows, deleteAccount, diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 8b4d2594..7438cd90 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -65,6 +65,7 @@ export const parseUser = (data) => { if (relationship) { output.follows_you = relationship.followed_by + output.requested = relationship.requested output.following = relationship.following output.statusnet_blocking = relationship.blocking output.muted = relationship.muting @@ -223,6 +224,7 @@ export const parseStatus = (data) => { output.statusnet_conversation_id = data.pleroma.conversation_id output.is_local = pleroma.local output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct + output.thread_muted = pleroma.thread_muted } else { output.text = data.content output.summary = data.spoiler_text @@ -240,6 +242,7 @@ export const parseStatus = (data) => { output.external_url = data.url output.poll = data.poll output.pinned = data.pinned + output.muted = data.muted } else { output.favorited = data.favorited output.fave_num = data.fave_num diff --git a/src/services/errors/errors.js b/src/services/errors/errors.js index 548f3c68..590552da 100644 --- a/src/services/errors/errors.js +++ b/src/services/errors/errors.js @@ -1,3 +1,5 @@ +import { humanizeErrors } from '../../modules/errors' + export function StatusCodeError (statusCode, body, options, response) { this.name = 'StatusCodeError' this.statusCode = statusCode @@ -12,3 +14,36 @@ export function StatusCodeError (statusCode, body, options, response) { } StatusCodeError.prototype = Object.create(Error.prototype) StatusCodeError.prototype.constructor = StatusCodeError + +export class RegistrationError extends Error { + constructor (error) { + super() + if (Error.captureStackTrace) { + Error.captureStackTrace(this) + } + + try { + // the error is probably a JSON object with a single key, "errors", whose value is another JSON object containing the real errors + if (typeof error === 'string') { + error = JSON.parse(error) + if (error.hasOwnProperty('error')) { + error = JSON.parse(error.error) + } + } + + if (typeof error === 'object') { + // replace ap_id with username + if (error.ap_id) { + error.username = error.ap_id + delete error.ap_id + } + this.message = humanizeErrors(error) + } else { + this.message = error + } + } catch (e) { + // can't parse it, so just treat it like a string + this.message = error + } + } +} diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js index b2486e7c..d82ce593 100644 --- a/src/services/follow_manipulate/follow_manipulate.js +++ b/src/services/follow_manipulate/follow_manipulate.js @@ -2,17 +2,14 @@ const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => { setTimeout(() => { store.state.api.backendInteractor.fetchUser({ id: user.id }) .then((user) => store.commit('addNewUsers', [user])) - .then(() => resolve([user.following, attempt])) + .then(() => resolve([user.following, user.requested, user.locked, attempt])) .catch((e) => reject(e)) }, 500) -}).then(([following, attempt]) => { - if (!following && attempt <= 3) { +}).then(([following, sent, locked, attempt]) => { + if (!following && !(locked && sent) && attempt <= 3) { // If we BE reports that we still not following that user - retry, // increment attempts by one - return fetchUser(++attempt, user, store) - } else { - // If we run out of attempts, just return whatever status is. - return following + fetchUser(++attempt, user, store) } }) @@ -21,14 +18,10 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => { .then((updated) => { store.commit('updateUserRelationship', [updated]) - // For locked users we just mark it that we sent the follow request - if (updated.locked) { - resolve({ sent: true }) - } - - if (updated.following) { - // If we get result immediately, just stop. - resolve({ sent: false }) + if (updated.following || (user.locked && user.requested)) { + // If we get result immediately or the account is locked, just stop. + resolve() + return } // But usually we don't get result immediately, so we ask server @@ -39,14 +32,8 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => { // Recursive Promise, it will call itself up to 3 times. return fetchUser(1, user, store) - .then((following) => { - if (following) { - // We confirmed and everything's good. - resolve({ sent: false }) - } else { - // If after all the tries, just treat it as if user is locked - resolve({ sent: false }) - } + .then(() => { + resolve() }) }) }) diff --git a/src/services/new_api/password_reset.js b/src/services/new_api/password_reset.js new file mode 100644 index 00000000..43199625 --- /dev/null +++ b/src/services/new_api/password_reset.js @@ -0,0 +1,18 @@ +import { reduce } from 'lodash' + +const MASTODON_PASSWORD_RESET_URL = `/auth/password` + +const resetPassword = ({ instance, email }) => { + const params = { email } + const query = reduce(params, (acc, v, k) => { + const encoded = `${k}=${encodeURIComponent(v)}` + return `${acc}&${encoded}` + }, '') + const url = `${instance}${MASTODON_PASSWORD_RESET_URL}?${query}` + + return window.fetch(url, { + method: 'POST' + }) +} + +export default resetPassword diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index f9ec3f6e..b6c4cf80 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -10,6 +10,11 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => { const args = { credentials } const rootState = store.rootState || store.state const timelineData = rootState.statuses.notifications + const hideMutedPosts = typeof rootState.config.hideMutedPosts === 'undefined' + ? rootState.instance.hideMutedPosts + : rootState.config.hideMutedPosts + + args['withMuted'] = !hideMutedPosts args['timeline'] = 'notifications' if (older) { diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index f186d202..1cf7edc3 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -22,7 +22,7 @@ const setStyle = (href, commit) => { ***/ const head = document.head const body = document.body - body.style.display = 'none' + body.classList.add('hidden') const cssEl = document.createElement('link') cssEl.setAttribute('rel', 'stylesheet') cssEl.setAttribute('href', href) @@ -46,7 +46,7 @@ const setStyle = (href, commit) => { head.appendChild(styleEl) // const styleSheet = styleEl.sheet - body.style.display = 'initial' + body.classList.remove('hidden') } cssEl.addEventListener('load', setDynamic) @@ -75,7 +75,7 @@ const applyTheme = (input, commit) => { const { rules, theme } = generatePreset(input) const head = document.head const body = document.body - body.style.display = 'none' + body.classList.add('hidden') const styleEl = document.createElement('style') head.appendChild(styleEl) @@ -86,7 +86,7 @@ const applyTheme = (input, commit) => { styleSheet.insertRule(`body { ${rules.colors} }`, 'index-max') styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max') styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max') - body.style.display = 'initial' + body.classList.remove('hidden') // commit('setOption', { name: 'colors', value: htmlColors }) // commit('setOption', { name: 'radii', value: radii }) |
