diff --git a/src/App.scss b/src/App.scss index fac800bcd29af13e49fd1c7a8592857d7cb7d18b..ea7b54e8d8347a122048e2b53d1551192b18fa04 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 { diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 5a94194c2955330f6e7bfa9029a4d3816bd468be..5cb2acba1dbd638e225fff3888c3a0fcfeb4a974 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -246,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 7dc4b2a5da6f3705a64287aff5610a94321b567d..cd02711cc3840e2e430be02e7dc31ec570db604b 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/conversation-page/conversation-page.js b/src/components/conversation-page/conversation-page.js index 1da70ce95b8866cfe3145d037b11bef856d48813..8f996be12c26720bae830d3baf36bbe8f24c9864 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 532f785c297d111acfb85a80d8b782857c6e72c6..8cc0a55feac8505a7a51620a63a09b080104b355 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 49fa8612889fb9ff0a4250a15b40776f77d52941..72ee9c390dfd39c6690d9f78c64bc9bf156e5573 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', - 'pinnedStatusIdsObject' + '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: { - status: '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,6 +155,10 @@ const conversation = { }, toggleExpanded () { this.expanded = !this.expanded + }, + 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 f184c0717b0f60b2bb485eedbb1ed4f48acbdb49..0f1de55fd40377e0ebb818cd09969d3c25723147 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -26,6 +26,7 @@ :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/login_form/login_form.vue b/src/components/login_form/login_form.vue index 3ec7fe0c1977b81acaaceec0700512181bc2bae9..b4fdcefbf012963fefbf27409769ba23e391804a 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/password_reset/password_reset.js b/src/components/password_reset/password_reset.js new file mode 100644 index 0000000000000000000000000000000000000000..fa71e07a9426cbc18b230fbae3e57bebad9865b5 --- /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 0000000000000000000000000000000000000000..00474e95d1c213cb6f569369f67ad280aa554ebf --- /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/status/status.js b/src/components/status/status.js index b72d2f5822e5176204cff5b7bfb0b2c4333060b8..d17ba3180e335547dbaf219b8defceb580d736e6 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.status.thread_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 diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue index 3fff63f9d21e8687756fa1a902179772b0980772..4137bd5960cf887a3105cda7d27eb7f2a302233d 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/timeline/timeline.js b/src/components/timeline/timeline.js index 8df48f7f2d15b3f9693af0fac5cdb95b5c0c8b4b..0594576c46eb0db6b7fb80dff2fa790015d352e8 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 { diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 4ad51714b8e661f12cf470a113a9ff69977fdc57..f1d3903a078660e422aaef6410690dc7175df3ea 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" :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_card/user_card.js b/src/components/user_card/user_card.js index 82d3b83590d24cf4b4e7d723cba2b2daa0c9b6a6..e3bd7697894605b142f225209f996810581afb80 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 () { diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index fc18e240f433d6176a4e06f3f6f814509eca893c..0b83cf168d0fb103370e9ec89b53b249c0ebbd74 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> diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index 4251691624f317e248023e9e5dd7088993becf3e..14082e83942274d63fe0112d3b5a811ab0fad3b8 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -26,6 +26,7 @@ timeline-name="user" :user-id="userId" :pinned-status-ids="user.pinnedStatusIds" + :in-profile="true" /> <div v-if="followsTabVisible" @@ -69,6 +70,7 @@ timeline-name="media" :timeline="media" :user-id="userId" + :in-profile="true" /> <Timeline v-if="isUs" @@ -79,6 +81,7 @@ :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 8fab6c4dca63c13be0d52e0017965785eeb73bef..1aa3a4cdcc897e23f08f228089402292ffba6f3e 100644 --- a/src/components/who_to_follow/who_to_follow.js +++ b/src/components/who_to_follow/who_to_follow.js @@ -26,7 +26,7 @@ const WhoToFollow = { } 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 7d01678b30bb4cc32177cf0f595898b92bfba778..dcb56106427b8439996348f6f7320593442af4a2 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 6a9af55c8586bf5912d2b2c1a9860876f47a0e75..ddde471ab8798fbc93d8239dfc67be586f181ba3 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -608,5 +608,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/ru.json b/src/i18n/ru.json index 90ed66643a9378009cc815a737a7f330b9fef7d1..3af65f400c66ae5825d902979c256eed911a73bc 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/zh.json b/src/i18n/zh.json index da6dae5fdd719a2c1d1929d1b9a2f782e9169500..80c4e0d891e087bb567e881467a77aa603285e9a 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/services/api/api.service.js b/src/services/api/api.service.js index 4cf41e61a194325ccc2fe1a15a9a9fe190f30bed..887d7d7aab347531d72aa130123c55ea7edef6f5 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -4,7 +4,6 @@ import 'whatwg-fetch' 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' @@ -220,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, { @@ -966,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 846d9415eacf0dc8ffc69d950a3e5bf7182efafa..3c44a10cb7b5d0eb21470d31cc5521998cd4b20e 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -127,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 }) @@ -194,7 +192,6 @@ const backendInteractorService = credentials => { updateBg, updateBanner, updateProfile, - externalProfile, importBlocks, importFollows, deleteAccount, diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js index 529fdb9b078486d0378958c2d5a734cad7596247..d82ce59348aa968a04502b2005ab8417f53ddaec 100644 --- a/src/services/follow_manipulate/follow_manipulate.js +++ b/src/services/follow_manipulate/follow_manipulate.js @@ -9,10 +9,7 @@ const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => { 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 sent + fetchUser(++attempt, user, store) } }) @@ -23,7 +20,7 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => { if (updated.following || (user.locked && user.requested)) { // If we get result immediately or the account is locked, just stop. - resolve({ sent: updated.requested }) + resolve() return } @@ -35,8 +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((sent) => { - resolve({ sent }) + .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 0000000000000000000000000000000000000000..4319962503dbd15fd1db5d628d05b1e3d0e31752 --- /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/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index f186d2024e4c87617669e1fe5dbb55bafa170629..1cf7edc35217c57b58cf8d575d6a53cbf17527be 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 })