diff options
Diffstat (limited to 'src')
73 files changed, 2316 insertions, 342 deletions
diff --git a/src/App.scss b/src/App.scss index e2e2d079..e1e1bdd0 100644 --- a/src/App.scss +++ b/src/App.scss @@ -279,7 +279,7 @@ input, textarea, .select, .input { + label::before { flex-shrink: 0; display: inline-block; - content: '✔'; + content: '✓'; transition: color 200ms; width: 1.1em; height: 1.1em; @@ -941,6 +941,9 @@ nav { min-height: 1.3rem; max-height: 1.3rem; line-height: 1.3rem; + max-width: 10em; + overflow: hidden; + text-overflow: ellipsis; } .chat-layout { diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 63e0ceba..19c713d5 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -28,7 +28,7 @@ :href="attachment.url" :alt="attachment.description" :title="attachment.description" - @click.prevent="toggleHidden" + @click.prevent.stop="toggleHidden" > <img :key="nsfwImage" @@ -80,6 +80,8 @@ class="video" :attachment="attachment" :controls="allowPlay" + @play="$emit('play')" + @pause="$emit('pause')" /> <i v-if="!allowPlay" @@ -93,6 +95,8 @@ :alt="attachment.description" :title="attachment.description" controls + @play="$emit('play')" + @pause="$emit('pause')" /> <div diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js index 9c4e5b05..34e723d0 100644 --- a/src/components/chat/chat.js +++ b/src/components/chat/chat.js @@ -5,6 +5,7 @@ import ChatMessage from '../chat_message/chat_message.vue' import PostStatusForm from '../post_status_form/post_status_form.vue' import ChatTitle from '../chat_title/chat_title.vue' import chatService from '../../services/chat_service/chat_service.js' +import { promiseInterval } from '../../services/promise_interval/promise_interval.js' import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.js' const BOTTOMED_OUT_OFFSET = 10 @@ -204,9 +205,9 @@ const Chat = { } }, readChat () { - if (!(this.currentChatMessageService && this.currentChatMessageService.lastMessage)) { return } + if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return } if (document.hidden) { return } - const lastReadId = this.currentChatMessageService.lastMessage.id + const lastReadId = this.currentChatMessageService.maxId this.$store.dispatch('readChat', { id: this.currentChat.id, lastReadId }) }, bottomedOut (offset) { @@ -244,9 +245,9 @@ const Chat = { const chatId = chatMessageService.chatId const fetchOlderMessages = !!maxId - const sinceId = fetchLatest && chatMessageService.lastMessage && chatMessageService.lastMessage.id + const sinceId = fetchLatest && chatMessageService.maxId - this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId }) + return this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId }) .then((messages) => { // Clear the current chat in case we're recovering from a ws connection loss. if (isFirstFetch) { @@ -287,7 +288,7 @@ const Chat = { }, doStartFetching () { this.$store.dispatch('startFetchingCurrentChat', { - fetcher: () => setInterval(() => this.fetchChat({ fetchLatest: true }), 5000) + fetcher: () => promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000) }) this.fetchChat({ isFirstFetch: true }) }, @@ -303,7 +304,11 @@ const Chat = { return this.backendInteractor.sendChatMessage(params) .then(data => { - this.$store.dispatch('addChatMessages', { chatId: this.currentChat.id, messages: [data] }).then(() => { + this.$store.dispatch('addChatMessages', { + chatId: this.currentChat.id, + messages: [data], + updateMaxId: false + }).then(() => { this.$nextTick(() => { this.handleResize() // When the posting form size changes because of a media attachment, we need an extra resize diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss index 240beea4..7d4ff60c 100644 --- a/src/components/chat_message/chat_message.scss +++ b/src/components/chat_message/chat_message.scss @@ -2,7 +2,7 @@ .chat-message-wrapper { &.hovered-message-chain { - .animated.avatar { + .animated.Avatar { canvas { display: none; } diff --git a/src/components/chat_panel/chat_panel.vue b/src/components/chat_panel/chat_panel.vue index ca529b5a..570435e7 100644 --- a/src/components/chat_panel/chat_panel.vue +++ b/src/components/chat_panel/chat_panel.vue @@ -63,7 +63,7 @@ @click.stop.prevent="togglePanel" > <div class="title"> - <i class="icon-comment-empty" /> + <i class="icon-megaphone" /> {{ $t('shoutbox.title') }} </div> </div> diff --git a/src/components/contrast_ratio/contrast_ratio.vue b/src/components/contrast_ratio/contrast_ratio.vue index ba92bc17..9dc871b6 100644 --- a/src/components/contrast_ratio/contrast_ratio.vue +++ b/src/components/contrast_ratio/contrast_ratio.vue @@ -39,13 +39,16 @@ export default { props: { large: { - required: false + required: false, + type: Boolean, + default: false }, // TODO: Make theme switcher compute theme initially so that contrast // component won't be called without contrast data contrast: { required: false, - type: Object + type: Object, + default: () => ({}) } }, computed: { diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 45fb2bf6..069c0b40 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -44,7 +44,8 @@ const conversation = { 'isPage', 'pinnedStatusIdsObject', 'inProfile', - 'profileUserId' + 'profileUserId', + 'virtualHidden' ], created () { if (this.isPage) { @@ -52,6 +53,13 @@ const conversation = { } }, computed: { + hideStatus () { + if (this.$refs.statusComponent && this.$refs.statusComponent[0]) { + return this.virtualHidden && this.$refs.statusComponent[0].suspendable + } else { + return this.virtualHidden + } + }, status () { return this.$store.state.statuses.allStatusesObject[this.statusId] }, @@ -102,6 +110,10 @@ const conversation = { }, isExpanded () { return this.expanded || this.isPage + }, + hiddenStyle () { + const height = (this.status && this.status.virtualHeight) || '120px' + return this.virtualHidden ? { height } : {} } }, components: { @@ -121,6 +133,12 @@ const conversation = { if (value) { this.fetchConversation() } + }, + virtualHidden (value) { + this.$store.dispatch( + 'setVirtualHeight', + { statusId: this.statusId, height: `${this.$el.clientHeight}px` } + ) } }, methods: { diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 997a4d10..e0b9fcc5 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -1,5 +1,7 @@ <template> <div + v-if="!hideStatus" + :style="hiddenStyle" class="Conversation" :class="{ '-expanded' : isExpanded, 'panel' : isExpanded }" > @@ -18,6 +20,7 @@ <status v-for="status in conversation" :key="status.id" + ref="statusComponent" :inline-expanded="collapsable && isExpanded" :statusoid="status" :expandable="!isExpanded" @@ -33,6 +36,10 @@ @toggleExpanded="toggleExpanded" /> </div> + <div + v-else + :style="hiddenStyle" + /> </template> <script src="./conversation.js"></script> @@ -53,8 +60,8 @@ .conversation-status { border-color: $fallback--border; border-color: var(--border, $fallback--border); - border-left: 4px solid $fallback--cRed; - border-left: 4px solid var(--cRed, $fallback--cRed); + border-left-color: $fallback--cRed; + border-left-color: var(--cRed, $fallback--cRed); } .conversation-status:last-child { diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js index 0f397b59..3ad80df3 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -8,7 +8,20 @@ const LOAD_EMOJI_BY = 60 const LOAD_EMOJI_MARGIN = 64 const filterByKeyword = (list, keyword = '') => { - return list.filter(x => x.displayText.includes(keyword)) + if (keyword === '') return list + + const keywordLowercase = keyword.toLowerCase() + let orderedEmojiList = [] + for (const emoji of list) { + const indexOfKeyword = emoji.displayText.toLowerCase().indexOf(keywordLowercase) + if (indexOfKeyword > -1) { + if (!Array.isArray(orderedEmojiList[indexOfKeyword])) { + orderedEmojiList[indexOfKeyword] = [] + } + orderedEmojiList[indexOfKeyword].push(emoji) + } + } + return orderedEmojiList.flat() } const EmojiPicker = { diff --git a/src/components/follow_requests/follow_requests.vue b/src/components/follow_requests/follow_requests.vue index 5fa4cf39..41f19db8 100644 --- a/src/components/follow_requests/follow_requests.vue +++ b/src/components/follow_requests/follow_requests.vue @@ -1,7 +1,9 @@ <template> <div class="settings panel panel-default"> <div class="panel-heading"> - {{ $t('nav.friend_requests') }} + <div class="title"> + {{ $t('nav.friend_requests') }} + </div> </div> <div class="panel-body"> <FollowRequestCard diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue index b2d5acc5..60fa6ceb 100644 --- a/src/components/moderation_tools/moderation_tools.vue +++ b/src/components/moderation_tools/moderation_tools.vue @@ -178,7 +178,7 @@ box-shadow: var(--inputShadow); &.menu-checkbox-checked::after { - content: '✔'; + content: '✓'; } } diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index f8459fd1..4f944c95 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -7,12 +7,12 @@ :to="{ name: timelinesRoute }" :class="onTimelineRoute && 'router-link-active'" > - <i class="button-icon icon-home-2" /> {{ $t("nav.timelines") }} + <i class="button-icon icon-home-2" />{{ $t("nav.timelines") }} </router-link> </li> <li v-if="currentUser"> <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> - <i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }} + <i class="button-icon icon-bell-alt" />{{ $t("nav.interactions") }} </router-link> </li> <li v-if="currentUser && pleromaChatMessagesAvailable"> @@ -23,12 +23,12 @@ > {{ unreadChatCount }} </div> - <i class="button-icon icon-chat" /> {{ $t("nav.chats") }} + <i class="button-icon icon-chat" />{{ $t("nav.chats") }} </router-link> </li> <li v-if="currentUser && currentUser.locked"> <router-link :to="{ name: 'friend-requests' }"> - <i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }} + <i class="button-icon icon-user-plus" />{{ $t("nav.friend_requests") }} <span v-if="followRequestCount > 0" class="badge follow-request-count" @@ -39,7 +39,7 @@ </li> <li> <router-link :to="{ name: 'about' }"> - <i class="button-icon icon-info-circled" /> {{ $t("nav.about") }} + <i class="button-icon icon-info-circled" />{{ $t("nav.about") }} </router-link> </li> </ul> @@ -125,6 +125,10 @@ } } +.nav-panel .button-icon { + margin-right: 0.5em; +} + .nav-panel .button-icon:before { width: 1.1em; } diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index 1bd91995..c6b2a5b5 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -39,7 +39,7 @@ word-wrap: break-word; word-break: break-word; - &:hover .animated.avatar { + &:hover .animated.Avatar { canvas { display: none; } diff --git a/src/components/password_reset/password_reset.js b/src/components/password_reset/password_reset.js index 62e74e30..5d21d720 100644 --- a/src/components/password_reset/password_reset.js +++ b/src/components/password_reset/password_reset.js @@ -47,11 +47,6 @@ const passwordReset = { 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') diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue index ceba9eea..5f54b416 100644 --- a/src/components/poll/poll.vue +++ b/src/components/poll/poll.vue @@ -17,6 +17,7 @@ <span class="result-percentage"> {{ percentageForOption(option.votes_count) }}% </span> + <!-- eslint-disable-next-line vue/no-v-html --> <span v-html="option.title_html" /> </div> <div @@ -96,6 +97,7 @@ align-items: center; padding: 0.1em 0.25em; z-index: 1; + word-break: break-word; } .result-percentage { width: 3.5em; diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index e7094bec..ad149506 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -555,6 +555,9 @@ const PostStatusForm = { }, updateIdempotencyKey () { this.idempotencyKey = Date.now().toString() + }, + openProfileTab () { + this.$store.dispatch('openSettingsModalTab', 'profile') } } } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 520c03ea..d67d9ae9 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -23,9 +23,12 @@ tag="p" class="visibility-notice" > - <router-link :to="{ name: 'user-settings' }"> + <a + href="#" + @click="openProfileTab" + > {{ $t('post_status.account_not_locked_warning_link') }} - </router-link> + </a> </i18n> <p v-if="!hideScopeNotice && newStatus.visibility === 'public'" diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js index abcf0455..dd71e546 100644 --- a/src/components/react_button/react_button.js +++ b/src/components/react_button/react_button.js @@ -1,5 +1,4 @@ import Popover from '../popover/popover.vue' -import { mapGetters } from 'vuex' const ReactButton = { props: ['status'], @@ -29,13 +28,23 @@ const ReactButton = { emojis () { if (this.filterWord !== '') { const filterWordLowercase = this.filterWord.toLowerCase() - return this.$store.state.instance.emoji.filter(emoji => - emoji.displayText.toLowerCase().includes(filterWordLowercase) - ) + let orderedEmojiList = [] + for (const emoji of this.$store.state.instance.emoji) { + const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase) + if (indexOfFilterWord > -1) { + if (!Array.isArray(orderedEmojiList[indexOfFilterWord])) { + orderedEmojiList[indexOfFilterWord] = [] + } + orderedEmojiList[indexOfFilterWord].push(emoji) + } + } + return orderedEmojiList.flat() } return this.$store.state.instance.emoji || [] }, - ...mapGetters(['mergedConfig']) + mergedConfig () { + return this.$store.getters.mergedConfig + } } } diff --git a/src/components/reply_button/reply_button.js b/src/components/reply_button/reply_button.js new file mode 100644 index 00000000..22957650 --- /dev/null +++ b/src/components/reply_button/reply_button.js @@ -0,0 +1,12 @@ + +const ReplyButton = { + name: 'ReplyButton', + props: ['status', 'replying'], + computed: { + loggedIn () { + return !!this.$store.state.users.currentUser + } + } +} + +export default ReplyButton diff --git a/src/components/reply_button/reply_button.vue b/src/components/reply_button/reply_button.vue new file mode 100644 index 00000000..b2904b5c --- /dev/null +++ b/src/components/reply_button/reply_button.vue @@ -0,0 +1,21 @@ +<template> + <div> + <i + v-if="loggedIn" + class="button-icon button-reply icon-reply" + :title="$t('tool_tip.reply')" + :class="{'-active': replying}" + @click.prevent="$emit('toggle')" + /> + <i + v-else + class="button-icon button-reply -disabled icon-reply" + :title="$t('tool_tip.reply')" + /> + <span v-if="status.replies_count > 0"> + {{ status.replies_count }} + </span> + </div> +</template> + +<script src="./reply_button.js"></script> diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js index d9a0f92e..5a41f22d 100644 --- a/src/components/retweet_button/retweet_button.js +++ b/src/components/retweet_button/retweet_button.js @@ -1,4 +1,3 @@ -import { mapGetters } from 'vuex' const RetweetButton = { props: ['status', 'loggedIn', 'visibility'], @@ -28,7 +27,9 @@ const RetweetButton = { 'animate-spin': this.animated } }, - ...mapGetters(['mergedConfig']) + mergedConfig () { + return this.$store.getters.mergedConfig + } } } diff --git a/src/components/settings_modal/settings_modal_content.js b/src/components/settings_modal/settings_modal_content.js index 48101a90..ef1a5ffa 100644 --- a/src/components/settings_modal/settings_modal_content.js +++ b/src/components/settings_modal/settings_modal_content.js @@ -27,6 +27,34 @@ const SettingsModalContent = { computed: { isLoggedIn () { return !!this.$store.state.users.currentUser + }, + open () { + return this.$store.state.interface.settingsModalState !== 'hidden' + } + }, + methods: { + onOpen () { + const targetTab = this.$store.state.interface.settingsModalTargetTab + // We're being told to open in specific tab + if (targetTab) { + const tabIndex = this.$refs.tabSwitcher.$slots.default.findIndex(elm => { + return elm.data && elm.data.attrs['data-tab-name'] === targetTab + }) + if (tabIndex >= 0) { + this.$refs.tabSwitcher.setTab(tabIndex) + } + } + // Clear the state of target tab, so that next time settings is opened + // it doesn't force it. + this.$store.dispatch('clearSettingsModalTargetTab') + } + }, + mounted () { + this.onOpen() + }, + watch: { + open: function (value) { + if (value) this.onOpen() } } } diff --git a/src/components/settings_modal/settings_modal_content.vue b/src/components/settings_modal/settings_modal_content.vue index 2156844f..bc30a0ff 100644 --- a/src/components/settings_modal/settings_modal_content.vue +++ b/src/components/settings_modal/settings_modal_content.vue @@ -8,6 +8,7 @@ <div :label="$t('settings.general')" icon="wrench" + data-tab-name="general" > <GeneralTab /> </div> @@ -15,6 +16,7 @@ v-if="isLoggedIn" :label="$t('settings.profile_tab')" icon="user" + data-tab-name="profile" > <ProfileTab /> </div> @@ -22,18 +24,21 @@ v-if="isLoggedIn" :label="$t('settings.security_tab')" icon="lock" + data-tab-name="security" > <SecurityTab /> </div> <div :label="$t('settings.filtering')" icon="filter" + data-tab-name="filtering" > <FilteringTab /> </div> <div :label="$t('settings.theme')" icon="brush" + data-tab-name="theme" > <ThemeTab /> </div> @@ -41,6 +46,7 @@ v-if="isLoggedIn" :label="$t('settings.notifications')" icon="bell-ringing-o" + data-tab-name="notifications" > <NotificationsTab /> </div> @@ -48,6 +54,7 @@ v-if="isLoggedIn" :label="$t('settings.data_import_export_tab')" icon="download" + data-tab-name="dataImportExport" > <DataImportExportTab /> </div> @@ -56,12 +63,14 @@ :label="$t('settings.mutes_and_blocks')" :fullHeight="true" icon="eye-off" + data-tab-name="mutesAndBlocks" > <MutesAndBlocksTab /> </div> <div :label="$t('settings.version.title')" icon="info-circled" + data-tab-name="version" > <VersionTab /> </div> diff --git a/src/components/settings_modal/tabs/data_import_export_tab.js b/src/components/settings_modal/tabs/data_import_export_tab.js index 168f89e1..f4b736d2 100644 --- a/src/components/settings_modal/tabs/data_import_export_tab.js +++ b/src/components/settings_modal/tabs/data_import_export_tab.js @@ -1,6 +1,7 @@ import Importer from 'src/components/importer/importer.vue' import Exporter from 'src/components/exporter/exporter.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' +import { mapState } from 'vuex' const DataImportExportTab = { data () { @@ -18,21 +19,26 @@ const DataImportExportTab = { Checkbox }, computed: { - user () { - return this.$store.state.users.currentUser - } + ...mapState({ + backendInteractor: (state) => state.api.backendInteractor, + user: (state) => state.users.currentUser + }) }, methods: { getFollowsContent () { - return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id }) + return this.backendInteractor.exportFriends({ id: this.user.id }) .then(this.generateExportableUsersContent) }, getBlocksContent () { - return this.$store.state.api.backendInteractor.fetchBlocks() + return this.backendInteractor.fetchBlocks() + .then(this.generateExportableUsersContent) + }, + getMutesContent () { + return this.backendInteractor.fetchMutes() .then(this.generateExportableUsersContent) }, importFollows (file) { - return this.$store.state.api.backendInteractor.importFollows({ file }) + return this.backendInteractor.importFollows({ file }) .then((status) => { if (!status) { throw new Error('failed') @@ -40,7 +46,15 @@ const DataImportExportTab = { }) }, importBlocks (file) { - return this.$store.state.api.backendInteractor.importBlocks({ file }) + return this.backendInteractor.importBlocks({ file }) + .then((status) => { + if (!status) { + throw new Error('failed') + } + }) + }, + importMutes (file) { + return this.backendInteractor.importMutes({ file }) .then((status) => { if (!status) { throw new Error('failed') diff --git a/src/components/settings_modal/tabs/data_import_export_tab.vue b/src/components/settings_modal/tabs/data_import_export_tab.vue index b5d0f5ed..a406077d 100644 --- a/src/components/settings_modal/tabs/data_import_export_tab.vue +++ b/src/components/settings_modal/tabs/data_import_export_tab.vue @@ -36,6 +36,23 @@ :export-button-label="$t('settings.block_export_button')" /> </div> + <div class="setting-item"> + <h2>{{ $t('settings.mute_import') }}</h2> + <p>{{ $t('settings.import_mutes_from_a_csv_file') }}</p> + <Importer + :submit-handler="importMutes" + :success-message="$t('settings.mutes_imported')" + :error-message="$t('settings.mute_import_error')" + /> + </div> + <div class="setting-item"> + <h2>{{ $t('settings.mute_export') }}</h2> + <Exporter + :get-content="getMutesContent" + filename="mutes.csv" + :export-button-label="$t('settings.mute_export_button')" + /> + </div> </div> </template> diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 7f06d0bd..13482de7 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -58,6 +58,11 @@ {{ $t('settings.emoji_reactions_on_timeline') }} </Checkbox> </li> + <li> + <Checkbox v-model="virtualScrolling"> + {{ $t('settings.virtual_scrolling') }} + </Checkbox> + </li> </ul> </div> diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue index d57894de..5328c350 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue @@ -278,7 +278,7 @@ /> <ContrastRatio :contrast="previewContrast.alertErrorText" - large="true" + large /> <ColorInput v-model="alertWarningColorLocal" @@ -294,7 +294,7 @@ /> <ContrastRatio :contrast="previewContrast.alertWarningText" - large="true" + large /> <ColorInput v-model="alertNeutralColorLocal" @@ -310,7 +310,7 @@ /> <ContrastRatio :contrast="previewContrast.alertNeutralText" - large="true" + large /> <OpacityInput v-model="alertOpacityLocal" @@ -334,7 +334,7 @@ /> <ContrastRatio :contrast="previewContrast.badgeNotificationText" - large="true" + large /> </div> <div class="color-item"> @@ -359,7 +359,7 @@ /> <ContrastRatio :contrast="previewContrast.panelText" - large="true" + large /> <ColorInput v-model="panelLinkColorLocal" @@ -369,7 +369,7 @@ /> <ContrastRatio :contrast="previewContrast.panelLink" - large="true" + large /> </div> <div class="color-item"> @@ -740,57 +740,57 @@ <ColorInput v-model="chatBgColorLocal" name="chatBgColor" - :fallback="previewTheme.colors.bg || 1" + :fallback="previewTheme.colors.bg" :label="$t('settings.background')" /> <h5>{{ $t('settings.style.advanced_colors.chat.incoming') }}</h5> <ColorInput v-model="chatMessageIncomingBgColorLocal" name="chatMessageIncomingBgColor" - :fallback="previewTheme.colors.bg || 1" + :fallback="previewTheme.colors.bg" :label="$t('settings.background')" /> <ColorInput v-model="chatMessageIncomingTextColorLocal" name="chatMessageIncomingTextColor" - :fallback="previewTheme.colors.text || 1" + :fallback="previewTheme.colors.text" :label="$t('settings.text')" /> <ColorInput v-model="chatMessageIncomingLinkColorLocal" name="chatMessageIncomingLinkColor" - :fallback="previewTheme.colors.link || 1" + :fallback="previewTheme.colors.link" :label="$t('settings.links')" /> <ColorInput v-model="chatMessageIncomingBorderColorLocal" name="chatMessageIncomingBorderLinkColor" - :fallback="previewTheme.colors.fg || 1" + :fallback="previewTheme.colors.fg" :label="$t('settings.style.advanced_colors.chat.border')" /> <h5>{{ $t('settings.style.advanced_colors.chat.outgoing') }}</h5> <ColorInput v-model="chatMessageOutgoingBgColorLocal" name="chatMessageOutgoingBgColor" - :fallback="previewTheme.colors.bg || 1" + :fallback="previewTheme.colors.bg" :label="$t('settings.background')" /> <ColorInput v-model="chatMessageOutgoingTextColorLocal" name="chatMessageOutgoingTextColor" - :fallback="previewTheme.colors.text || 1" + :fallback="previewTheme.colors.text" :label="$t('settings.text')" /> <ColorInput v-model="chatMessageOutgoingLinkColorLocal" name="chatMessageOutgoingLinkColor" - :fallback="previewTheme.colors.link || 1" + :fallback="previewTheme.colors.link" :label="$t('settings.links')" /> <ColorInput v-model="chatMessageOutgoingBorderColorLocal" name="chatMessageOutgoingBorderLinkColor" - :fallback="previewTheme.colors.bg || 1" + :fallback="previewTheme.colors.bg" :label="$t('settings.style.advanced_colors.chat.border')" /> </div> diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 0587ee02..eda5a68c 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -90,7 +90,7 @@ @click="toggleDrawer" > <router-link :to="{ name: 'chat' }"> - <i class="button-icon icon-chat" /> {{ $t("nav.chat") }} + <i class="button-icon icon-megaphone" /> {{ $t("shoutbox.title") }} </router-link> </li> </ul> diff --git a/src/components/status/status.js b/src/components/status/status.js index d263da68..e48b2eb8 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -1,3 +1,4 @@ +import ReplyButton from '../reply_button/reply_button.vue' import FavoriteButton from '../favorite_button/favorite_button.vue' import ReactButton from '../react_button/react_button.vue' import RetweetButton from '../retweet_button/retweet_button.vue' @@ -15,11 +16,11 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { muteWordHits } from '../../services/status_parser/status_parser.js' import { unescape, uniqBy } from 'lodash' -import { mapGetters, mapState } from 'vuex' const Status = { name: 'Status', components: { + ReplyButton, FavoriteButton, ReactButton, RetweetButton, @@ -54,6 +55,8 @@ const Status = { replying: false, unmuted: false, userExpanded: false, + mediaPlaying: [], + suspendable: true, error: null } }, @@ -157,7 +160,7 @@ const Status = { return this.mergedConfig.hideFilteredStatuses }, hideStatus () { - return this.deleted || (this.muted && this.hideFilteredStatuses) + return (this.muted && this.hideFilteredStatuses) || this.virtualHidden }, isFocused () { // retweet or root of an expanded conversation @@ -207,11 +210,18 @@ const Status = { hidePostStats () { return this.mergedConfig.hidePostStats }, - ...mapGetters(['mergedConfig']), - ...mapState({ - betterShadow: state => state.interface.browserSupport.cssFilter, - currentUser: state => state.users.currentUser - }) + currentUser () { + return this.$store.state.users.currentUser + }, + betterShadow () { + return this.$store.state.interface.browserSupport.cssFilter + }, + mergedConfig () { + return this.$store.getters.mergedConfig + }, + isSuspendable () { + return !this.replying && this.mediaPlaying.length === 0 + } }, methods: { visibilityIcon (visibility) { @@ -251,6 +261,12 @@ const Status = { }, generateUserProfileLink (id, name) { return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) + }, + addMediaPlaying (id) { + this.mediaPlaying.push(id) + }, + removeMediaPlaying (id) { + this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id) } }, watch: { @@ -280,6 +296,9 @@ const Status = { if (this.isFocused && this.statusFromGlobalRepository.favoritedBy && this.statusFromGlobalRepository.favoritedBy.length !== num) { this.$store.dispatch('fetchFavs', this.status.id) } + }, + 'isSuspendable': function (val) { + this.suspendable = val } }, filters: { diff --git a/src/components/status/status.scss b/src/components/status/status.scss index 8d292d3f..66a91c1e 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -25,6 +25,23 @@ $status-margin: 0.75em; --icon: var(--selectedPostIcon, $fallback--icon); } + &.-conversation { + border-left-width: 4px; + border-left-style: solid; + } + + .gravestone { + padding: $status-margin; + color: $fallback--faint; + color: var(--faint, $fallback--faint); + display: flex; + + .deleted-text { + margin: 0.5em 0; + align-items: center; + } + } + .status-container { display: flex; padding: $status-margin; diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 282ad37d..ffae32fc 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -16,7 +16,7 @@ /> </div> <template v-if="muted && !isPreview"> - <div class="status-csontainer muted"> + <div class="status-container muted"> <small class="status-username"> <i v-if="muted && retweet" @@ -95,6 +95,7 @@ </div> <div + v-if="!deleted" :class="[userClass, { highlighted: userStyle, '-repeat': retweet && !inConversation }]" :style="[ userStyle ]" class="status-container" @@ -227,6 +228,7 @@ </span> </a> </StatusPopover> + <span v-else class="reply-to-no-popover" @@ -272,6 +274,8 @@ :no-heading="noHeading" :highlight="highlight" :focused="isFocused" + @mediaplay="addMediaPlaying($event)" + @mediapause="removeMediaPlaying($event)" /> <transition name="fade"> @@ -320,21 +324,11 @@ v-if="!noHeading && !isPreview" class="status-actions" > - <div> - <i - v-if="loggedIn" - class="button-icon button-reply icon-reply" - :title="$t('tool_tip.reply')" - :class="{'-active': replying}" - @click.prevent="toggleReplying" - /> - <i - v-else - class="button-icon button-reply -disabled icon-reply" - :title="$t('tool_tip.reply')" - /> - <span v-if="status.replies_count > 0">{{ status.replies_count }}</span> - </div> + <reply-button + :replying="replying" + :status="status" + @toggle="toggleReplying" + /> <retweet-button :visibility="status.visibility" :logged-in="loggedIn" @@ -354,6 +348,26 @@ @onSuccess="clearError" /> </div> + + </div> + </div> + <div + v-else + class="gravestone" + > + <div class="left-side"> + <UserAvatar :compact="compact" /> + </div> + <div class="right-side"> + <div class="deleted-text"> + {{ $t('status.status_deleted') }} + </div> + <reply-button + v-if="replying" + :replying="replying" + :status="status" + @toggle="toggleReplying" + /> </div> </div> <div @@ -376,4 +390,5 @@ </template> <script src="./status.js" ></script> + <style src="./status.scss" lang="scss"></style> diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue index fb469a2f..f7fb5ee2 100644 --- a/src/components/status_content/status_content.vue +++ b/src/components/status_content/status_content.vue @@ -72,6 +72,10 @@ class="icon-doc" /> <span + v-if="status.poll && status.poll.options" + class="icon-chart-bar" + /> + <span v-if="status.card" class="icon-link" /> @@ -86,7 +90,7 @@ </a> </div> - <div v-if="status.poll && status.poll.options"> + <div v-if="status.poll && status.poll.options && !hideSubjectStatus"> <poll :base-poll="status.poll" /> </div> @@ -103,6 +107,8 @@ :attachment="attachment" :allow-play="true" :set-media="setMedia()" + @play="$emit('mediaplay', attachment.id)" + @pause="$emit('mediapause', attachment.id)" /> <gallery v-if="galleryAttachments.length > 0" diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js index ab40bbd7..8044e994 100644 --- a/src/components/still-image/still-image.js +++ b/src/components/still-image/still-image.js @@ -19,14 +19,16 @@ const StillImage = { }, methods: { onLoad () { - this.imageLoadHandler && this.imageLoadHandler(this.$refs.src) + const image = this.$refs.src + if (!image) return + this.imageLoadHandler && this.imageLoadHandler(image) const canvas = this.$refs.canvas if (!canvas) return - const width = this.$refs.src.naturalWidth - const height = this.$refs.src.naturalHeight + const width = image.naturalWidth + const height = image.naturalHeight canvas.width = width canvas.height = height - canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height) + canvas.getContext('2d').drawImage(image, 0, 0, width, height) }, onError () { this.imageLoadError && this.imageLoadError() diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js index 40b5b3ca..9c1da354 100644 --- a/src/components/tab_switcher/tab_switcher.js +++ b/src/components/tab_switcher/tab_switcher.js @@ -60,16 +60,19 @@ export default Vue.component('tab-switcher', { } }, methods: { - activateTab (index) { + clickTab (index) { return (e) => { e.preventDefault() - if (typeof this.onSwitch === 'function') { - this.onSwitch.call(null, this.$slots.default[index].key) - } - this.active = index - if (this.scrollableTabs) { - this.$refs.contents.scrollTop = 0 - } + this.setTab(index) + } + }, + setTab (index) { + if (typeof this.onSwitch === 'function') { + this.onSwitch.call(null, this.$slots.default[index].key) + } + this.active = index + if (this.scrollableTabs) { + this.$refs.contents.scrollTop = 0 } } }, @@ -88,7 +91,7 @@ export default Vue.component('tab-switcher', { <div class={classesWrapper.join(' ')}> <button disabled={slot.data.attrs.disabled} - onClick={this.activateTab(index)} + onClick={this.clickTab(index)} class={classesTab.join(' ')}> <img src={slot.data.attrs.image} title={slot.data.attrs['image-tooltip']}/> {slot.data.attrs.label ? '' : slot.data.attrs.label} @@ -100,7 +103,7 @@ export default Vue.component('tab-switcher', { <div class={classesWrapper.join(' ')}> <button disabled={slot.data.attrs.disabled} - onClick={this.activateTab(index)} + onClick={this.clickTab(index)} class={classesTab.join(' ')} type="button" > diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 5a7f7a78..17680542 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -33,7 +33,8 @@ const Timeline = { return { paused: false, unfocused: false, - bottomedOut: false + bottomedOut: false, + virtualScrollIndex: 0 } }, components: { @@ -78,6 +79,16 @@ const Timeline = { }, pinnedStatusIdsObject () { return keyBy(this.pinnedStatusIds) + }, + statusesToDisplay () { + const amount = this.timeline.visibleStatuses.length + const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80)) + const min = Math.max(0, this.virtualScrollIndex - statusesPerSide) + const max = Math.min(amount, this.virtualScrollIndex + statusesPerSide) + return this.timeline.visibleStatuses.slice(min, max).map(_ => _.id) + }, + virtualScrollingEnabled () { + return this.$store.getters.mergedConfig.virtualScrolling } }, created () { @@ -85,7 +96,7 @@ const Timeline = { const credentials = store.state.users.currentUser.credentials const showImmediately = this.timeline.visibleStatuses.length === 0 - window.addEventListener('scroll', this.scrollLoad) + window.addEventListener('scroll', this.handleScroll) if (store.state.api.fetchers[this.timelineName]) { return false } @@ -104,9 +115,10 @@ const Timeline = { this.unfocused = document.hidden } window.addEventListener('keydown', this.handleShortKey) + setTimeout(this.determineVisibleStatuses, 250) }, destroyed () { - window.removeEventListener('scroll', this.scrollLoad) + window.removeEventListener('scroll', this.handleScroll) window.removeEventListener('keydown', this.handleShortKey) if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false) this.$store.commit('setLoading', { timeline: this.timelineName, value: false }) @@ -146,6 +158,48 @@ const Timeline = { } }) }, 1000, this), + determineVisibleStatuses () { + if (!this.$refs.timeline) return + if (!this.virtualScrollingEnabled) return + + const statuses = this.$refs.timeline.children + const cappedScrollIndex = Math.max(0, Math.min(this.virtualScrollIndex, statuses.length - 1)) + + if (statuses.length === 0) return + + const height = Math.max(document.body.offsetHeight, window.pageYOffset) + + const centerOfScreen = window.pageYOffset + (window.innerHeight * 0.5) + + // Start from approximating the index of some visible status by using the + // the center of the screen on the timeline. + let approxIndex = Math.floor(statuses.length * (centerOfScreen / height)) + let err = statuses[approxIndex].getBoundingClientRect().y + + // if we have a previous scroll index that can be used, test if it's + // closer than the previous approximation, use it if so + + const virtualScrollIndexY = statuses[cappedScrollIndex].getBoundingClientRect().y + if (Math.abs(err) > virtualScrollIndexY) { + approxIndex = cappedScrollIndex + err = virtualScrollIndexY + } + + // if the status is too far from viewport, check the next/previous ones if + // they happen to be better + while (err < -20 && approxIndex < statuses.length - 1) { + err += statuses[approxIndex].offsetHeight + approxIndex++ + } + while (err > window.innerHeight + 100 && approxIndex > 0) { + approxIndex-- + err -= statuses[approxIndex].offsetHeight + } + + // this status is now the center point for virtual scrolling and visible + // statuses will be nearby statuses before and after it + this.virtualScrollIndex = approxIndex + }, scrollLoad (e) { const bodyBRect = document.body.getBoundingClientRect() const height = Math.max(bodyBRect.height, -(bodyBRect.y)) @@ -155,6 +209,10 @@ const Timeline = { this.fetchOlderStatuses() } }, + handleScroll: throttle(function (e) { + this.determineVisibleStatuses() + this.scrollLoad(e) + }, 200), handleVisibilityChange () { this.unfocused = document.hidden } diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 2ff933e9..c1e2f44b 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -32,7 +32,10 @@ </div> </div> <div :class="classes.body"> - <div class="timeline"> + <div + ref="timeline" + class="timeline" + > <template v-for="statusId in pinnedStatusIds"> <conversation v-if="timeline.statusesObject[statusId]" @@ -54,6 +57,7 @@ :collapsable="true" :in-profile="inProfile" :profile-user-id="userId" + :virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)" /> </template> </div> diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue index be512d60..b7e5f2da 100644 --- a/src/components/timeline_menu/timeline_menu.vue +++ b/src/components/timeline_menu/timeline_menu.vue @@ -138,15 +138,11 @@ &:last-child { border: none; } - - i { - margin: 0 0.5em; - } } a { display: block; - padding: 0.6em 0; + padding: 0.6em 0.65em; &:hover { background-color: $fallback--lightBg; @@ -174,6 +170,10 @@ text-decoration: underline; } } + + i { + margin-right: 0.5em; + } } } diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue index e4e4127c..eb3d375e 100644 --- a/src/components/user_avatar/user_avatar.vue +++ b/src/components/user_avatar/user_avatar.vue @@ -1,5 +1,6 @@ <template> <StillImage + v-if="user" class="Avatar" :alt="user.screen_name" :title="user.screen_name" @@ -7,6 +8,11 @@ :class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }" :image-load-error="imageLoadError" /> + <div + v-else + class="Avatar -placeholder" + :class="{ 'avatar-compact': compact }" + /> </template> <script src="./user_avatar.js"></script> @@ -42,5 +48,10 @@ border-radius: $fallback--avatarAltRadius; border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); } + + &.-placeholder { + background-color: $fallback--fg; + background-color: var(--fg, $fallback--fg); + } } </style> diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 07fce79a..041bb80f 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -354,7 +354,7 @@ align-items: flex-start; max-height: 56px; - .avatar { + .Avatar { flex: 1 0 100%; width: 56px; height: 56px; @@ -364,7 +364,7 @@ } } - &:hover .avatar { + &:hover .Avatar { --still-image-img: visible; --still-image-canvas: hidden; } diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index c7c67c0a..b26499b4 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -156,8 +156,7 @@ .user-profile-field { display: flex; - margin: 0.25em auto; - max-width: 32em; + margin: 0.25em; border: 1px solid var(--border, $fallback--border); border-radius: $fallback--inputRadius; border-radius: var(--inputRadius, $fallback--inputRadius); diff --git a/src/components/video_attachment/video_attachment.js b/src/components/video_attachment/video_attachment.js index f0ca7e89..107b8985 100644 --- a/src/components/video_attachment/video_attachment.js +++ b/src/components/video_attachment/video_attachment.js @@ -3,27 +3,48 @@ const VideoAttachment = { props: ['attachment', 'controls'], data () { return { - loopVideo: this.$store.getters.mergedConfig.loopVideo + blocksSuspend: false, + // Start from true because removing "loop" property seems buggy in Vue + hasAudio: true + } + }, + computed: { + loopVideo () { + if (this.$store.getters.mergedConfig.loopVideoSilentOnly) { + return !this.hasAudio + } + return this.$store.getters.mergedConfig.loopVideo } }, methods: { - onVideoDataLoad (e) { + onPlaying (e) { + this.setHasAudio(e) + if (this.loopVideo) { + this.$emit('play', { looping: true }) + return + } + this.$emit('play') + }, + onPaused (e) { + this.$emit('pause') + }, + setHasAudio (e) { const target = e.srcElement || e.target + // If hasAudio is false, we've already marked this video to not have audio, + // a video can't gain audio out of nowhere so don't bother checking again. + if (!this.hasAudio) return if (typeof target.webkitAudioDecodedByteCount !== 'undefined') { // non-zero if video has audio track - if (target.webkitAudioDecodedByteCount > 0) { - this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly - } - } else if (typeof target.mozHasAudio !== 'undefined') { + if (target.webkitAudioDecodedByteCount > 0) return + } + if (typeof target.mozHasAudio !== 'undefined') { // true if video has audio track - if (target.mozHasAudio) { - this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly - } - } else if (typeof target.audioTracks !== 'undefined') { - if (target.audioTracks.length > 0) { - this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly - } + if (target.mozHasAudio) return + } + if (typeof target.audioTracks !== 'undefined') { + if (target.audioTracks.length > 0) return } + this.hasAudio = false } } } diff --git a/src/components/video_attachment/video_attachment.vue b/src/components/video_attachment/video_attachment.vue index 1ffed4e0..a4bf01e8 100644 --- a/src/components/video_attachment/video_attachment.vue +++ b/src/components/video_attachment/video_attachment.vue @@ -7,7 +7,8 @@ :alt="attachment.description" :title="attachment.description" playsinline - @loadeddata="onVideoDataLoad" + @playing="onPlaying" + @pause="onPaused" /> </template> diff --git a/src/i18n/ca.json b/src/i18n/ca.json index f2bcdd06..b15b69f7 100644 --- a/src/i18n/ca.json +++ b/src/i18n/ca.json @@ -9,7 +9,8 @@ "scope_options": "Opcions d'abast i visibilitat", "text_limit": "Límit de text", "title": "Funcionalitats", - "who_to_follow": "A qui seguir" + "who_to_follow": "A qui seguir", + "pleroma_chat_messages": "Xat de Pleroma" }, "finder": { "error_fetching_user": "No s'ha pogut carregar l'usuari/a", @@ -17,7 +18,21 @@ }, "general": { "apply": "Aplica", - "submit": "Desa" + "submit": "Desa", + "close": "Tanca", + "verify": "Verifica", + "confirm": "Confirma", + "enable": "Habilita", + "disable": "Deshabilitar", + "cancel": "Cancel·la", + "show_less": "Mostra menys", + "show_more": "Mostra més", + "optional": "opcional", + "retry": "Prova de nou", + "error_retry": "Si us plau, prova de nou", + "generic_error": "Hi ha hagut un error", + "loading": "Carregant…", + "more": "Més" }, "login": { "login": "Inicia sessió", @@ -25,7 +40,12 @@ "password": "Contrasenya", "placeholder": "p.ex.: Maria", "register": "Registra't", - "username": "Nom d'usuari/a" + "username": "Nom d'usuari/a", + "recovery_code": "Codi de recuperació", + "enter_recovery_code": "Posa un codi de recuperació", + "authentication_code": "Codi d'autenticació", + "hint": "Entra per participar a la conversa", + "description": "Entra amb OAuth" }, "nav": { "chat": "Xat local públic", @@ -33,7 +53,16 @@ "mentions": "Mencions", "public_tl": "Flux públic del node", "timeline": "Flux personal", - "twkn": "Flux de la xarxa coneguda" + "twkn": "Flux de la xarxa coneguda", + "chats": "Xats", + "timelines": "Línies de temps", + "preferences": "Preferències", + "who_to_follow": "A qui seguir", + "search": "Cerca", + "dms": "Missatges directes", + "interactions": "Interaccions", + "back": "Enrere", + "administration": "Administració" }, "notifications": { "broken_favorite": "No es coneix aquest estat. S'està cercant.", @@ -42,14 +71,19 @@ "load_older": "Carrega més notificacions", "notifications": "Notificacions", "read": "Read!", - "repeated_you": "ha repetit el teu estat" + "repeated_you": "ha repetit el teu estat", + "migrated_to": "migrat a", + "no_more_notifications": "No més notificacions", + "follow_request": "et vol seguir" }, "post_status": { "account_not_locked_warning": "El teu compte no està {0}. Qualsevol persona pot seguir-te per llegir les teves entrades reservades només a seguidores.", "account_not_locked_warning_link": "bloquejat", "attachments_sensitive": "Marca l'adjunt com a delicat", "content_type": { - "text/plain": "Text pla" + "text/plain": "Text pla", + "text/markdown": "Markdown", + "text/html": "HTML" }, "content_warning": "Assumpte (opcional)", "default": "Em sento…", @@ -60,7 +94,13 @@ "private": "Només seguidors/es - Publica només per comptes que et segueixin", "public": "Pública - Publica als fluxos públics", "unlisted": "Silenciosa - No la mostris en fluxos públics" - } + }, + "scope_notice": { + "private": "Aquesta entrada serà visible només per a qui et segueixi", + "public": "Aquesta entrada serà visible per a tothom" + }, + "preview_empty": "Buida", + "preview": "Vista prèvia" }, "registration": { "bio": "Presentació", @@ -68,7 +108,17 @@ "fullname": "Nom per mostrar", "password_confirm": "Confirma la contrasenya", "registration": "Registra't", - "token": "Codi d'invitació" + "token": "Codi d'invitació", + "validations": { + "password_confirmation_match": "hauria de ser la mateixa que la contrasenya", + "password_confirmation_required": "no es pot deixar en blanc", + "password_required": "no es pot deixar en blanc", + "email_required": "no es pot deixar en blanc", + "fullname_required": "no es pot deixar en blanc", + "username_required": "no es pot deixar en blanc" + }, + "fullname_placeholder": "p. ex. Lain Iwakura", + "username_placeholder": "p. ex. lain" }, "settings": { "attachmentRadius": "Adjunts", @@ -94,7 +144,7 @@ "data_import_export_tab": "Importa o exporta dades", "default_vis": "Abast per defecte de les entrades", "delete_account": "Esborra el compte", - "delete_account_description": "Esborra permanentment el teu compte i tots els missatges", + "delete_account_description": "Esborra permanentment les teves dades i desactiva el teu compte.", "delete_account_error": "No s'ha pogut esborrar el compte. Si continua el problema, contacta amb l'administració del node.", "delete_account_instructions": "Confirma que vols esborrar el compte escrivint la teva contrasenya aquí sota.", "export_theme": "Desa el tema", @@ -164,7 +214,57 @@ "values": { "false": "no", "true": "sí" - } + }, + "show_moderator_badge": "Mostra una insígnia de Moderació en el meu perfil", + "show_admin_badge": "Mostra una insígnia d'Administració en el meu perfil", + "hide_followers_description": "No mostris qui m'està seguint", + "hide_follows_description": "No mostris a qui segueixo", + "notification_visibility_emoji_reactions": "Reaccions", + "new_email": "Nou correu electrònic", + "profile_fields": { + "value": "Contingut", + "name": "Etiqueta", + "add_field": "Afegeix un camp", + "label": "Metadades del perfil" + }, + "mutes_tab": "Silenciaments", + "interface": "Interfície", + "instance_default_simple": "(per defecte)", + "checkboxRadius": "Caselles", + "import_blocks_from_a_csv_file": "Importa bloquejos des d'un arxiu csv", + "hide_post_stats": "Amaga les estadístiques de les entrades (p. ex. el nombre de favorits)", + "use_one_click_nsfw": "Obre els adjunts NSFW amb només un clic", + "hide_muted_posts": "Amaga les entrades de comptes silenciats", + "avatar_size_instruction": "La mida mínima recomanada per la imatge de l'avatar és de 150x150 píxels.", + "domain_mutes": "Dominis", + "discoverable": "Permet la descoberta d'aquest compte en resultats de cerques i altres serveis", + "mutes_and_blocks": "Silenciaments i bloquejos", + "composing": "Composant", + "chatMessageRadius": "Missatge de xat", + "changed_email": "Correu electrònic canviat amb èxit!", + "change_email_error": "Hi ha hagut un problema al canviar el teu correu electrònic.", + "change_email": "Canvia el correu electrònic", + "bot": "Aquest és un compte automatitzat", + "blocks_tab": "Bloquejos", + "blocks_imported": "Bloquejos importats! Processar-los pot trigar una mica.", + "block_import_error": "Error al importar bloquejos", + "block_import": "Importa bloquejos", + "block_export_button": "Exporta els teus bloquejos a un arxiu csv", + "block_export": "Exporta bloquejos", + "allow_following_move": "Permet el seguiment automàtic quan un compte a qui seguim es mou", + "mfa": { + "scan": { + "secret_code": "Clau" + }, + "authentication_methods": "Mètodes d'autenticació", + "waiting_a_recovery_codes": "Rebent còpies de seguretat dels codis…", + "recovery_codes": "Codis de recuperació.", + "warning_of_generate_new_codes": "Quan generes nous codis de recuperació, els antics ja no funcionaran més.", + "generate_new_recovery_codes": "Genera nous codis de recuperació" + }, + "enter_current_password_to_confirm": "Posar la contrasenya actual per confirmar la teva identitat", + "security": "Seguretat", + "app_name": "Nom de l'aplicació" }, "time": { "day": "{0} dia", @@ -232,5 +332,75 @@ "who_to_follow": { "more": "More", "who_to_follow": "A qui seguir" + }, + "selectable_list": { + "select_all": "Selecciona-ho tot" + }, + "remote_user_resolver": { + "error": "No trobat.", + "searching_for": "Cercant per" + }, + "interactions": { + "load_older": "Carrega antigues interaccions", + "favs_repeats": "Repeticions i favorits" + }, + "emoji": { + "stickers": "Adhesius" + }, + "polls": { + "expired": "L'enquesta va acabar fa {0}", + "expires_in": "L'enquesta acaba en {0}", + "multiple_choices": "Múltiples opcions", + "single_choice": "Una sola opció", + "type": "Tipus d'enquesta", + "vote": "Vota", + "votes": "vots", + "option": "Opció", + "add_option": "Afegeix opció", + "add_poll": "Afegeix enquesta" + }, + "media_modal": { + "next": "Següent", + "previous": "Anterior" + }, + "importer": { + "error": "Ha succeït un error mentre s'importava aquest arxiu.", + "success": "Importat amb èxit." + }, + "image_cropper": { + "cancel": "Cancel·la", + "save_without_cropping": "Desa sense retallar", + "save": "Desa", + "crop_picture": "Retalla la imatge" + }, + "exporter": { + "processing": "Processant, aviat se't preguntarà per descarregar el teu arxiu", + "export": "Exporta" + }, + "domain_mute_card": { + "mute_progress": "Silenciant…", + "mute": "Silencia" + }, + "about": { + "staff": "Equip responsable", + "mrf": { + "simple": { + "quarantine_desc": "Aquesta instància només enviarà entrades públiques a les següents instàncies:", + "quarantine": "Quarantena", + "reject_desc": "Aquesta instància no acceptarà missatges de les següents instàncies:", + "reject": "Rebutja", + "accept_desc": "Aquesta instància només accepta missatges de les següents instàncies:", + "accept": "Accepta", + "simple_policies": "Polítiques específiques de la instància" + }, + "mrf_policies_desc": "Les polítiques MRF controlen el comportament federat de la instància. Les següents polítiques estan habilitades:", + "mrf_policies": "Polítiques MRF habilitades", + "keyword": { + "replace": "Reemplaça", + "reject": "Rebutja", + "keyword_policies": "Polítiques de paraules clau" + }, + "federation": "Federació" + } } } diff --git a/src/i18n/de.json b/src/i18n/de.json index 3014b870..6fe6ab2c 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -478,7 +478,6 @@ "placeholder": "Dein Benutzername oder die zugehörige E-Mail-Adresse", "check_email": "Im E-Mail-Posteingang des angebenen Kontos müsste sich jetzt (oder zumindest in Kürze) die E-Mail mit dem Link zum Passwortzurücksetzen befinden.", "return_home": "Zurück zur Heimseite", - "not_found": "Benutzername/E-Mail-Adresse nicht gefunden. Vertippt?", "too_many_requests": "Kurze Pause. Zu viele Versuche. Bitte, später nochmal probieren.", "password_reset_disabled": "Passwortzurücksetzen deaktiviert. Bitte Administrator kontaktieren.", "password_reset_required": "Passwortzurücksetzen erforderlich.", diff --git a/src/i18n/en.json b/src/i18n/en.json index e05ac907..d3d57562 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -113,7 +113,6 @@ "about": "About", "administration": "Administration", "back": "Back", - "chat": "Local Chat", "friend_requests": "Follow Requests", "mentions": "Mentions", "interactions": "Interactions", @@ -276,6 +275,12 @@ "block_import": "Block import", "block_import_error": "Error importing blocks", "blocks_imported": "Blocks imported! Processing them will take a while.", + "mute_export": "Mute export", + "mute_export_button": "Export your mutes to a csv file", + "mute_import": "Mute import", + "mute_import_error": "Error importing mutes", + "mutes_imported": "Mutes imported! Processing them will take a while.", + "import_mutes_from_a_csv_file": "Import mutes from a csv file", "blocks_tab": "Blocks", "bot": "This is a bot account", "btnRadius": "Buttons", @@ -430,6 +435,7 @@ "false": "no", "true": "yes" }, + "virtual_scrolling": "Optimize timeline rendering", "fun": "Fun", "greentext": "Meme arrows", "notifications": "Notifications", @@ -659,7 +665,8 @@ "show_full_subject": "Show full subject", "hide_full_subject": "Hide full subject", "show_content": "Show content", - "hide_content": "Hide content" + "hide_content": "Hide content", + "status_deleted": "This post was deleted" }, "user_card": { "approve": "Approve", @@ -774,7 +781,6 @@ "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.", "password_reset_required": "You must reset your password to log in.", diff --git a/src/i18n/eo.json b/src/i18n/eo.json index b66f557a..e73ac2f8 100644 --- a/src/i18n/eo.json +++ b/src/i18n/eo.json @@ -776,7 +776,6 @@ "password_reset_required": "Vi devas restarigi vian pasvorton por saluti.", "password_reset_disabled": "Restarigado de pasvortoj estas malŝaltita. Bonvolu kontakti la administranton de via nodo.", "too_many_requests": "Vi atingis la limon de provoj, reprovu pli poste.", - "not_found": "Ni ne trovis tiun retpoŝtadreson aŭ uzantonomon.", "return_home": "Reiri al la hejmpaĝo", "check_email": "Kontrolu vian retpoŝton pro ligilo por restarigi vian pasvorton.", "placeholder": "Via retpoŝtadreso aŭ uzantonomo", diff --git a/src/i18n/es.json b/src/i18n/es.json index 3f313eb3..6889df9a 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -13,7 +13,8 @@ "scope_options": "Opciones del alcance de la visibilidad", "text_limit": "Límite de caracteres", "title": "Características", - "who_to_follow": "A quién seguir" + "who_to_follow": "A quién seguir", + "pleroma_chat_messages": "Chat de Pleroma" }, "finder": { "error_fetching_user": "Error al buscar usuario", @@ -31,7 +32,13 @@ "disable": "Inhabilitar", "enable": "Habilitar", "confirm": "Confirmar", - "verify": "Verificar" + "verify": "Verificar", + "peek": "Ojear", + "close": "Cerrar", + "dismiss": "Descartar", + "retry": "Inténtalo de nuevo", + "error_retry": "Por favor, inténtalo de nuevo", + "loading": "Cargando…" }, "image_cropper": { "crop_picture": "Recortar la foto", @@ -41,7 +48,7 @@ }, "importer": { "submit": "Enviar", - "success": "Importado con éxito", + "success": "Importado con éxito.", "error": "Se ha producido un error al importar el archivo." }, "login": { @@ -77,21 +84,27 @@ "dms": "Mensajes Directos", "public_tl": "Línea Temporal Pública", "timeline": "Línea Temporal", - "twkn": "Toda La Red Conocida", + "twkn": "Red Conocida", "user_search": "Búsqueda de Usuarios", "search": "Buscar", "who_to_follow": "A quién seguir", - "preferences": "Preferencias" + "preferences": "Preferencias", + "chats": "Chats", + "timelines": "Líneas de Tiempo", + "bookmarks": "Marcadores" }, "notifications": { - "broken_favorite": "Estado desconocido, buscándolo...", + "broken_favorite": "Estado desconocido, buscándolo…", "favorited_you": "le gusta tu estado", "followed_you": "empezó a seguirte", "load_older": "Cargar notificaciones antiguas", "notifications": "Notificaciones", "read": "¡Leído!", "repeated_you": "repitió tu estado", - "no_more_notifications": "No hay más notificaciones" + "no_more_notifications": "No hay más notificaciones", + "reacted_with": "reaccionó con {0}", + "migrated_to": "migrado a", + "follow_request": "quiere seguirte" }, "polls": { "add_poll": "Añadir encuesta", @@ -114,7 +127,9 @@ "search_emoji": "Buscar un emoji", "add_emoji": "Insertar un emoji", "custom": "Emojis personalizados", - "unicode": "Emojis unicode" + "unicode": "Emojis unicode", + "load_all": "Cargando todos los {emojiAmount} emoji", + "load_all_hint": "Cargado el primer emoji {saneAmount}, cargar todos los emoji puede causar problemas de rendimiento." }, "stickers": { "add_sticker": "Añadir Pegatina" @@ -122,7 +137,8 @@ "interactions": { "favs_repeats": "Favoritos y Repetidos", "follows": "Nuevos seguidores", - "load_older": "Cargar interacciones más antiguas" + "load_older": "Cargar interacciones más antiguas", + "moves": "Usuario Migrado" }, "post_status": { "new_status": "Publicar un nuevo estado", @@ -142,7 +158,7 @@ "posting": "Publicando", "scope_notice": { "public": "Esta publicación será visible para todo el mundo", - "private": "Esta publicación solo será visible para tus seguidores.", + "private": "Esta publicación solo será visible para tus seguidores", "unlisted": "Esta publicación no será visible en la Línea Temporal Pública ni en Toda La Red Conocida" }, "scope": { @@ -150,7 +166,12 @@ "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" - } + }, + "media_description_error": "Error al actualizar el archivo, inténtalo de nuevo", + "empty_status_error": "No se puede publicar un estado vacío y sin archivos adjuntos", + "preview_empty": "Vacío", + "preview": "Vista previa", + "media_description": "Descripción multimedia" }, "registration": { "bio": "Biografía", @@ -189,7 +210,7 @@ "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", + "waiting_a_recovery_codes": "Recibiendo códigos de respaldo…", "recovery_codes_warning": "Anote los códigos o guárdelos en un lugar seguro, de lo contrario no los volverá a ver. Si pierde el acceso a su aplicación 2FA y los códigos de recuperación, su cuenta quedará bloqueada.", "authentication_methods": "Métodos de autentificación", "scan": { @@ -232,7 +253,7 @@ "default_vis": "Alcance de visibilidad por defecto", "delete_account": "Eliminar la cuenta", "discoverable": "Permitir la aparición de esta cuenta en los resultados de búsqueda y otros servicios", - "delete_account_description": "Eliminar para siempre la cuenta y todos los mensajes.", + "delete_account_description": "Eliminar para siempre los datos y desactivar la cuenta.", "pad_emoji": "Rellenar con espacios al agregar emojis desde el selector", "delete_account_error": "Hubo un error al eliminar tu cuenta. Si el fallo persiste, ponte en contacto con el administrador de tu instancia.", "delete_account_instructions": "Escribe tu contraseña para confirmar la eliminación de tu cuenta.", @@ -253,7 +274,7 @@ "max_thumbnails": "Cantidad máxima de miniaturas por publicación", "hide_isp": "Ocultar el panel específico de la instancia", "preload_images": "Precargar las imágenes", - "use_one_click_nsfw": "Abrir los adjuntos NSFW con un solo click.", + "use_one_click_nsfw": "Abrir los adjuntos NSFW con un solo click", "hide_post_stats": "Ocultar las estadísticas de las entradas (p.ej. el número de favoritos)", "hide_user_stats": "Ocultar las estadísticas del usuario (p.ej. el número de seguidores)", "hide_filtered_statuses": "Ocultar estados filtrados", @@ -299,7 +320,7 @@ "valid_until": "Válido hasta", "revoke_token": "Revocar", "panelRadius": "Paneles", - "pause_on_unfocused": "Parar la transmisión cuando no estés en foco.", + "pause_on_unfocused": "Parar la transmisión cuando no estés en foco", "presets": "Por defecto", "profile_background": "Fondo del Perfil", "profile_banner": "Cabecera del Perfil", @@ -355,7 +376,24 @@ "save_load_hint": "Las opciones \"Mantener\" conservan las opciones configuradas actualmente al seleccionar o cargar temas, también almacena dichas opciones al exportar un tema. Cuando se desactiven todas las casillas de verificación, el tema de exportación lo guardará todo.", "reset": "Reiniciar", "clear_all": "Limpiar todo", - "clear_opacity": "Limpiar opacidad" + "clear_opacity": "Limpiar opacidad", + "help": { + "snapshot_source_mismatch": "Conflicto de versiones: lo más probable es que el frontend se haya revertido y actualizado nuevamente, si cambió el tema con una versión anterior del frontend, lo más probable es que desee usar la versión anterior; de lo contrario, use la nueva versión.", + "migration_napshot_gone": "Por alguna razón, faltaba la instantánea, algunas cosas podrían verse diferentes de lo que recuerdas.", + "migration_snapshot_ok": "Solo para estar seguro, se cargó la instantánea del tema. Puede intentar cargar los datos del tema.", + "fe_downgraded": "Versión de PleromaFE revertida.", + "fe_upgraded": "El creador de temas de PleromaFE se actualizó después de la actualización de la versión.", + "snapshot_missing": "No había ninguna instantánea del tema en el archivo, por lo que podría verse diferente de lo previsto originalmente.", + "snapshot_present": "Se ha cargado una instantánea del tema, por lo que todos los valores se sobrescriben. De lo contrario, puede cargar el tema por completo.", + "older_version_imported": "El archivo que ha importado se creó en una versión anterior del frontend actual.", + "v2_imported": "El archivo que ha importado fue creado para un frontend más antiguo. Intentamos maximizar la compatibilidad, pero aún podría haber inconsistencias.", + "future_version_imported": "El archivo que ha importado se creó para una versión más reciente del frontend.", + "upgraded_from_v2": "PleromaFE se ha actualizado, el tema podría verse un poco diferente de lo que recuerdas." + }, + "use_source": "Nueva versión", + "use_snapshot": "Versión antigua", + "keep_as_is": "Mantener como está", + "load_theme": "Cargar tema" }, "common": { "color": "Color", @@ -390,7 +428,26 @@ "borders": "Bordes", "buttons": "Botones", "inputs": "Campos de entrada", - "faint_text": "Texto desvanecido" + "faint_text": "Texto desvanecido", + "alert_neutral": "Neutral", + "chat": { + "border": "Borde", + "outgoing": "Salientes", + "incoming": "Entrantes" + }, + "tabs": "Pestañas", + "toggled": "Intercambiado", + "disabled": "Deshabilitado", + "selectedMenu": "Elemento del menú seleccionado", + "selectedPost": "Publicación seleccionada", + "pressed": "Presionado", + "highlight": "Elementos destacados", + "icons": "Iconos", + "poll": "Gráfico de la encuesta", + "underlay": "Subrayado", + "popover": "Sugerencias, menús, superposiciones", + "post": "Publicaciones/Biografías de Usuarios", + "alert_warning": "Precaución" }, "radii": { "_tab_label": "Redondez" @@ -423,7 +480,8 @@ "buttonPressed": "Botón (presionado)", "buttonPressedHover": "Botón (presionado+encima)", "input": "Campo de entrada" - } + }, + "hintV3": "Para las sombras, también puede usar la notación {0} para usar otro espacio de color." }, "fonts": { "_tab_label": "Fuentes", @@ -458,7 +516,49 @@ "title": "Versión", "backend_version": "Versión del Backend", "frontend_version": "Versión del Frontend" - } + }, + "notification_visibility_moves": "Usuario Migrado", + "greentext": "Texto verde (meme arrows)", + "notification_setting_hide_notification_contents": "Ocultar el remitente y el contenido de las notificaciones push", + "notification_setting_privacy": "Privacidad", + "notification_setting_block_from_strangers": "Bloquea las notificaciones de los usuarios que no sigues", + "notification_setting_filters": "Filtros", + "fun": "Divertido", + "type_domains_to_mute": "Buscar dominios para silenciar", + "useStreamingApiWarning": "(no recomendado, experimental, puede omitir publicaciones)", + "useStreamingApi": "Recibir entradas y notificaciones en tiempo real", + "user_mutes": "Usuarios", + "reset_profile_background": "Restablecer el fondo de pantalla", + "reset_background_confirm": "¿Estás seguro de restablecer el fondo de pantalla?", + "reset_banner_confirm": "¿Estás seguro de restablecer la imagen del banner?", + "reset_avatar_confirm": "¿Estás seguro de restablecer la imagen de avatar?", + "reset_profile_banner": "Restabler imagen del banner del perfil", + "reset_avatar": "Restablecer avatar", + "notification_visibility_emoji_reactions": "Reacciones", + "new_email": "Nuevo correo electrónico", + "profile_fields": { + "value": "Contenido", + "name": "Etiqueta", + "add_field": "Añadir un campo", + "label": "Metadatos del perfil" + }, + "accent": "Acento", + "emoji_reactions_on_timeline": "Mostrar las reacciones de emoji en la línea de tiempo", + "domain_mutes": "Dominios", + "mutes_and_blocks": "Silenciado y Bloqueados", + "chatMessageRadius": "Mensaje de chat", + "changed_email": "¡Correo electrónico modificado correctamente!", + "change_email_error": "Ha ocurrido un error al intentar modificar tu correo electrónico.", + "change_email": "Modificar el correo electrónico", + "bot": "Esta cuenta es un bot", + "allow_following_move": "Permitir el seguimiento automático, cuando la cuenta que sigues se traslada a otra instancia", + "virtual_scrolling": "Optimizar la representación de la linea temporal", + "import_mutes_from_a_csv_file": "Importar silenciados desde un archivo csv", + "mutes_imported": "¡Silenciados importados! Procesarlos llevará un tiempo.", + "mute_import_error": "Error al importar los silenciados", + "mute_import": "Importar silenciados", + "mute_export_button": "Exportar los silenciados a un archivo csv", + "mute_export": "Exportar silenciados" }, "time": { "day": "{0} día", @@ -504,7 +604,8 @@ "show_new": "Mostrar lo nuevo", "up_to_date": "Actualizado", "no_more_statuses": "No hay más estados", - "no_statuses": "Sin estados" + "no_statuses": "Sin estados", + "reload": "Recargar" }, "status": { "favorites": "Favoritos", @@ -517,7 +618,17 @@ "reply_to": "Respondiendo a", "replies_list": "Respuestas:", "mute_conversation": "Silenciar la conversación", - "unmute_conversation": "Mostrar la conversación" + "unmute_conversation": "Mostrar la conversación", + "hide_content": "Ocultar el contenido", + "show_content": "Mostrar el contenido", + "hide_full_subject": "Ocultar el tema completo", + "show_full_subject": "Mostrar el tema completo", + "thread_muted_and_words": ", contiene:", + "thread_muted": "Conversación silenciada", + "copy_link": "Copiar el enlace al estado", + "status_unavailable": "Estado no disponible", + "bookmark": "Marcar", + "unbookmark": "Desmarcar" }, "user_card": { "approve": "Aprobar", @@ -546,11 +657,11 @@ "subscribe": "Suscribirse", "unsubscribe": "Desuscribirse", "unblock": "Desbloquear", - "unblock_progress": "Desbloqueando...", - "block_progress": "Bloqueando...", - "unmute": "Quitar silencio", - "unmute_progress": "Quitando silencio...", - "mute_progress": "Silenciando...", + "unblock_progress": "Desbloqueando…", + "block_progress": "Bloqueando…", + "unmute": "Dejar de silenciar", + "unmute_progress": "Quitando silencio…", + "mute_progress": "Silenciando…", "admin_menu": { "moderation": "Moderación", "grant_admin": "Conceder permisos de Administrador", @@ -564,12 +675,16 @@ "strip_media": "Eliminar archivos multimedia de las publicaciones", "force_unlisted": "Forzar que se publique en el modo -Sin Listar-", "sandbox": "Forzar que se publique solo para tus seguidores", - "disable_remote_subscription": "No permitir que usuarios de instancias remotas te siga.", + "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": "Eliminar usuario", "delete_user_confirmation": "¿Estás completamente seguro? Esta acción no se puede deshacer." - } + }, + "show_repeats": "Mostrar repetidos", + "hide_repeats": "Ocultar repetidos", + "message": "Mensaje", + "hidden": "Oculto" }, "user_profile": { "timeline_title": "Linea Temporal del Usuario", @@ -594,7 +709,11 @@ "repeat": "Repetir", "reply": "Contestar", "favorite": "Favorito", - "user_settings": "Ajustes de usuario" + "user_settings": "Ajustes de usuario", + "bookmark": "Marcador", + "reject_follow_request": "Rechazar la solicitud de seguimiento", + "accept_follow_request": "Aceptar la solicitud de seguimiento", + "add_reaction": "Añadir Reacción" }, "upload": { "error": { @@ -624,8 +743,78 @@ "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." + "password_reset_disabled": "El restablecimiento de contraseñas está deshabilitado. Póngase en contacto con el administrador de su instancia.", + "password_reset_required_but_mailer_is_disabled": "Debes restablecer la contraseña, pero el restablecimiento de contraseñas está deshabilitado. Por favor contacta con el administrador de la instancia.", + "password_reset_required": "Debes restablecer la contraseña para iniciar sesión." + }, + "errors": { + "storage_unavailable": "Pleroma no pudo acceder al almacenamiento del navegador. Su inicio de sesión o su configuración local no se guardarán y puede encontrar problemas inesperados. Intente habilitar las cookies." + }, + "domain_mute_card": { + "unmute_progress": "Quitando silencio…", + "unmute": "Dejar de silenciar", + "mute_progress": "Silenciando…", + "mute": "Silenciar" + }, + "about": { + "mrf": { + "simple": { + "accept_desc": "Esta instancia solo acepta mensajes de las siguientes instancias:", + "media_nsfw_desc": "Esta instancia obliga a que los archivos multimedia se establezcan como sensibles en las publicaciones de las siguientes instancias:", + "media_nsfw": "Forzar Multimedia Como Sensible", + "media_removal_desc": "Esta instancia elimina los archivos multimedia de las publicaciones de las siguientes instancias:", + "media_removal": "Eliminar Multimedia", + "quarantine": "Cuarentena", + "ftl_removal_desc": "Esta instancia elimina las siguientes instancias de la línea de tiempo \"Toda la red conocida\":", + "ftl_removal": "Eliminar de la línea de tiempo \"Toda La Red Conocida\"", + "quarantine_desc": "Esta instancia enviará solo publicaciones públicas a las siguientes instancias:", + "simple_policies": "Políticas específicas de la instancia", + "reject_desc": "Esta instancia no aceptará mensajes de las siguientes instancias:", + "reject": "Rechazar", + "accept": "Aceptar" + }, + "mrf_policies_desc": "Las políticas MRF manipulan la federación de esta instancia con el resto del fediverso. Las siguientes políticas están habilitadas:", + "mrf_policies": "Habilitar políticas MRF", + "keyword": { + "ftl_removal": "Eliminar de la línea de tiempo \"Toda La Red Conocida\"", + "keyword_policies": "Política de Palabras Clave", + "is_replaced_by": "→", + "replace": "Reemplazar", + "reject": "Rechazar" + }, + "federation": "Federación" + }, + "staff": "Equipo" + }, + "shoutbox": { + "title": "Jaula de Grillos" + }, + "remote_user_resolver": { + "remote_user_resolver": "Resolución de usuario remoto", + "error": "No encontrado.", + "searching_for": "Buscando" + }, + "chats": { + "chats": "Chats", + "empty_chat_list_placeholder": "Aún no tienes ninguna conversación. ¡Inicia una nueva conversación!", + "error_sending_message": "Algo salió mal al enviar el mensaje.", + "error_loading_chat": "Algo salió mal al cargar el chat.", + "delete_confirm": "¿Realmente quieres borrar este mensaje?", + "more": "Más", + "empty_message_error": "No puedes publicar un mensaje vacío", + "new": "Nueva conversación", + "delete": "Borrar", + "message_user": "Mensaje de {nickname}", + "you": "Tú:" + }, + "display_date": { + "today": "Hoy" + }, + "file_type": { + "file": "Archivo", + "image": "Imagen", + "video": "Vídeo", + "audio": "Audio" } } diff --git a/src/i18n/eu.json b/src/i18n/eu.json index f04203f0..a45b7cfd 100644 --- a/src/i18n/eu.json +++ b/src/i18n/eu.json @@ -13,7 +13,8 @@ "scope_options": "Ikusgaitasun aukerak", "text_limit": "Testu limitea", "title": "Ezaugarriak", - "who_to_follow": "Nori jarraitu" + "who_to_follow": "Nori jarraitu", + "pleroma_chat_messages": "Pleroma Txata" }, "finder": { "error_fetching_user": "Errorea erabiltzailea eskuratzen", @@ -31,7 +32,13 @@ "disable": "Ezgaitu", "enable": "Gaitu", "confirm": "Baieztatu", - "verify": "Egiaztatu" + "verify": "Egiaztatu", + "peek": "Begiratu", + "close": "Itxi", + "dismiss": "Baztertu", + "retry": "Saiatu berriro", + "error_retry": "Saiatu berriro mesedez", + "loading": "Kargatzen…" }, "image_cropper": { "crop_picture": "Moztu argazkia", @@ -81,7 +88,10 @@ "user_search": "Erabiltzailea Bilatu", "search": "Bilatu", "who_to_follow": "Nori jarraitu", - "preferences": "Hobespenak" + "preferences": "Hobespenak", + "chats": "Txatak", + "timelines": "Denbora-lerroak", + "bookmarks": "Laster-markak" }, "notifications": { "broken_favorite": "Egoera ezezaguna, bilatzen…", @@ -91,7 +101,10 @@ "notifications": "Jakinarazpenak", "read": "Irakurrita!", "repeated_you": "zure mezua errepikatu du", - "no_more_notifications": "Ez dago jakinarazpen gehiago" + "no_more_notifications": "Ez dago jakinarazpen gehiago", + "reacted_with": "{0}kin erreakzionatu zuen", + "migrated_to": "hona migratua:", + "follow_request": "jarraitu nahi zaitu" }, "polls": { "add_poll": "Inkesta gehitu", @@ -114,7 +127,8 @@ "search_emoji": "Bilatu emoji bat", "add_emoji": "Emoji bat gehitu", "custom": "Ohiko emojiak", - "unicode": "Unicode emojiak" + "unicode": "Unicode emojiak", + "load_all": "{emojiAmount} emoji guztiak kargatzen" }, "stickers": { "add_sticker": "Pegatina gehitu" @@ -226,13 +240,13 @@ "composing": "Idazten", "confirm_new_password": "Baieztatu pasahitz berria", "current_avatar": "Zure uneko avatarra", - "current_password": "Indarrean den pasahitza", + "current_password": "Indarrean dagoen pasahitza", "current_profile_banner": "Zure profilaren banner-a", "data_import_export_tab": "Datuak Inportatu / Esportatu", "default_vis": "Lehenetsitako ikusgaitasunak", "delete_account": "Ezabatu kontua", "discoverable": "Baimendu zure kontua kanpo bilaketa-emaitzetan eta bestelako zerbitzuetan agertzea", - "delete_account_description": "Betirako ezabatu zure kontua eta zure mezu guztiak", + "delete_account_description": "Betirako ezabatu zure datuak eta desaktibatu kontua.", "pad_emoji": "Zuriuneak gehitu emoji bat aukeratzen denean", "delete_account_error": "Arazo bat gertatu da zure kontua ezabatzerakoan. Arazoa jarraitu eskero, administratzailearekin harremanetan jarri.", "delete_account_instructions": "Idatzi zure pasahitza kontua ezabatzeko.", @@ -626,10 +640,48 @@ "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.", "password_reset_required": "Pasahitza berrezarri behar duzu saioa hasteko.", "password_reset_required_but_mailer_is_disabled": "Pasahitza berrezarri behar duzu, baina pasahitza berrezartzeko aukera desgaituta dago. Mesedez, jarri harremanetan instantziaren administratzailearekin." + }, + "about": { + "mrf": { + "keyword": { + "keyword_policies": "Gako-hitz politika", + "ftl_removal": "\"Ezagutzen den Sarea\" denbora-lerrotik ezabatu", + "is_replaced_by": "→", + "replace": "Ordezkatuak", + "reject": "Ukatuak" + }, + "federation": "Federazioa", + "simple": { + "media_nsfw_desc": "Instantzia honek hurrengo instantzien multimediak sentikorrak izatera behartzen ditu:", + "media_nsfw": "Behartu Multimedia Sentikor", + "media_removal_desc": "Instantzia honek atxikitutako multimedia hurrengo instantzietatik ezabatzen ditu:", + "media_removal": "Multimedia Ezabatu", + "ftl_removal_desc": "Instantzia honek hurrengo instantziak ezabatzen ditu \"Ezagutzen den Sarea\" denbora-lerrotik:", + "ftl_removal": "\"Ezagutzen den Sarea\" denbora-lerrotik ezabatu", + "quarantine_desc": "Instantzia honek soilik mezu publikoak bidaliko ditu instantzia hauetara:", + "quarantine": "Koarentena", + "reject_desc": "Instantzia honek ez ditu hurrengo instantzien mezuak onartuko:", + "reject": "Ukatuak", + "accept_desc": "Instantzia honek hurrengo instantzietako mezuak soilik onartzen ditu:", + "accept": "Onartu", + "simple_policies": "Gure instantziaren politika zehatzak" + }, + "mrf_policies_desc": "MRF politikek instantzia honen federazioa manipulatzen dute gainerako instantziekin. Honako politika hauek daude gaituta:", + "mrf_policies": "Gaitutako MRF politikak" + }, + "staff": "Arduradunak" + }, + "domain_mute_card": { + "unmute_progress": "Isiltasuna kentzen…", + "unmute": "Isiltasuna kendu", + "mute_progress": "Isiltzen…", + "mute": "Isilarazi" + }, + "shoutbox": { + "title": "Oihu-kutxa" } } diff --git a/src/i18n/fa.json b/src/i18n/fa.json new file mode 100644 index 00000000..0e8bda4b --- /dev/null +++ b/src/i18n/fa.json @@ -0,0 +1,155 @@ +{ + "about": { + "mrf": { + "simple": { + "media_removal_desc": "این نمونه رسانهی پیغامهای نمونههای ذکر شده را حذف میکند:", + "ftl_removal_desc": "این نمونه، نمونههای ذکر شده را از تایملاین «تمام شبکه شناخته شده» حذف میکند:", + "media_removal": "حذف رسانه", + "ftl_removal": "حذف از تایملاین «تمام شبکه شناخته شده»", + "quarantine_desc": "این نمونه تنها پیغامهای عمومی را به نمونههای ذکر شده پیغام ارسال میکند:", + "quarantine": "قرنطینه شده", + "reject_desc": "این نمونه از نمونههای ذکر شده پیغامی دریافت نمیکند:", + "reject": "رد کننده", + "accept_desc": "این نمونه تنها از نمونههای ذکر شده پیغام دریافت میکند:", + "simple_policies": "سیاستهای مخصوص نمونه", + "accept": "دریافت کننده", + "media_nsfw_desc": "این نمونه، رسانه نمونههای ذکر شده را به اجبار حساس میکند:", + "media_nsfw": "به اجبار حساس کردن رسانه" + }, + "federation": "فدراسیون", + "mrf_policies_desc": "سیاستهای MRF رفتار فدراسیون این نمونه را تغییر میدهد. سیاستهایی که در ادامه آمده اعمال شده است:", + "keyword": { + "reject": "رد کننده", + "replace": "جایگزین کننده", + "keyword_policies": "سیاستهای واژگان کلیدی", + "is_replaced_by": "→", + "ftl_removal": "حذف از تایملاین «تمام شبکه شناخته شده»" + }, + "mrf_policies": "سیاستهای MRF(وسیله بازنویسی پیغام) فعال شده" + }, + "staff": "کارکنان" + }, + "image_cropper": { + "crop_picture": "برش تصویر", + "cancel": "لغو", + "save_without_cropping": "ذخیره بدون برش", + "save": "ذخیره" + }, + "notifications": { + "followed_you": "پیگیر شما شد", + "favorited_you": "پیغام شما را پسندید", + "broken_favorite": "پیغام ناشناخته، در حال جستجو…" + }, + "nav": { + "chats": "گپها", + "timelines": "تایملاینها", + "preferences": "ترجیحات", + "who_to_follow": "چه کسانی را پیگیری کنیم", + "search": "جستجو", + "user_search": "جستجوی کاربر", + "bookmarks": "نشانکها", + "twkn": "شبکه شناخته شده", + "timeline": "تایملاین", + "public_tl": "تایملاین عمومی", + "dms": "پیغامهای مستقیم", + "interactions": "تعاملات", + "mentions": "نام بردنها", + "friend_requests": "درخواست پیگیری", + "back": "قبلی", + "administration": "مدیریت", + "about": "درباره" + }, + "features_panel": { + "who_to_follow": "چه کسانی را پیگیری کنیم", + "title": "ویژگیها", + "text_limit": "محدودیت متن", + "scope_options": "تنظیمات حوزه", + "media_proxy": "پروکسی رسانه", + "gopher": "گوفر", + "pleroma_chat_messages": "گپ پلروما", + "chat": "گپ" + }, + "media_modal": { + "next": "بعدی", + "previous": "قبلی" + }, + "login": { + "heading": { + "recovery": "بازیابی دو مرحلهای", + "totp": "احراز هویت دو مرحلهای" + }, + "enter_two_factor_code": "کد احراز هویت دو مرحلهای را وارد کنید", + "recovery_code": "کد بازیابی", + "enter_recovery_code": "کد بازیابی را وارد کنید", + "authentication_code": "کد احراز هویت", + "hint": "برای شرکت در گفتگو، وارد سامانه شوید", + "username": "نام کاربری", + "register": "ثبت نام", + "description": "ورود به سامانه از طریق OAuth", + "placeholder": "به عنوان مثال: lain", + "password": "رمز عبور", + "logout": "خروج از سامانه", + "login": "ورود به سامانه" + }, + "importer": { + "error": "در حین بارگذاری فایل خطایی رخ داد.", + "success": "با موفقیت بارگذاری شد.", + "submit": "ارسال" + }, + "general": { + "peek": "نگاه سریع", + "close": "بستن", + "verify": "تأیید", + "confirm": "تأیید", + "enable": "فعال", + "disable": "غیر فعال", + "cancel": "لغو", + "show_less": "کمتر نشان بده", + "show_more": "بیشتر نشان بده", + "optional": "اختیاری", + "retry": "دوباره امتحان کنید", + "error_retry": "لطفاً دوباره امتحان کنید", + "generic_error": "خطایی رخ داد", + "loading": "در حال بارگذاری…", + "more": "بیشتر", + "submit": "ارسال", + "apply": "اعمال" + }, + "finder": { + "find_user": "جستجوی کاربر", + "error_fetching_user": "دریافت کاربر با خطا مواجه شد" + }, + "exporter": { + "processing": "در حال پردازش، شما به زودی قادر به دانلود فایل خواهید بود", + "export": "صادر کردن" + }, + "domain_mute_card": { + "unmute": "صدا دار", + "unmute_progress": "در حال صدا دار کردن …", + "mute_progress": "در حال بی صدا کردن…", + "mute": "بی صدا" + }, + "shoutbox": { + "title": "چت باکس" + }, + "display_date": { + "today": "امروز" + }, + "file_type": { + "file": "فایل", + "image": "تصویر", + "video": "ویدئو", + "audio": "صدا" + }, + "chats": { + "empty_chat_list_placeholder": "شما هنوز هیچ گپی ندارید، گپ جدیدی را آغاز کنید!", + "delete": "حذف", + "error_sending_message": "در حین ارسال پیغام خطایی رخ داد.", + "error_loading_chat": "در هنگام بارگذاری گپ خطایی رخ داد.", + "delete_confirm": "آیا از حذف این پیغام اطمینان دارید؟", + "more": "بیشتر", + "empty_message_error": "نمیتوان پیغام خالی فرستاد", + "new": "گپ جدید", + "chats": "گپها" + } +} diff --git a/src/i18n/fi.json b/src/i18n/fi.json index 510b2234..2524f278 100644 --- a/src/i18n/fi.json +++ b/src/i18n/fi.json @@ -578,7 +578,8 @@ "show_full_subject": "Näytä koko otsikko", "hide_full_subject": "Piilota koko otsikko", "show_content": "Näytä sisältö", - "hide_content": "Piilota sisältö" + "hide_content": "Piilota sisältö", + "status_deleted": "Poistettu viesti" }, "user_card": { "approve": "Hyväksy", @@ -752,7 +753,6 @@ "password_reset": "Salasanan nollaus", "placeholder": "Sähköpostiosoite tai käyttäjänimi", "return_home": "Palaa etusivulle", - "not_found": "Sähköpostiosoitetta tai käyttäjänimeä ei löytynyt.", "too_many_requests": "Olet käyttänyt kaikki yritykset, yritä uudelleen myöhemmin.", "password_reset_required": "Sinun täytyy vaihtaa salasana kirjautuaksesi." }, diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 794ed812..63ad46d2 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -13,7 +13,8 @@ "scope_options": "Options de visibilité", "text_limit": "Limite de texte", "title": "Caractéristiques", - "who_to_follow": "Personnes à suivre" + "who_to_follow": "Personnes à suivre", + "pleroma_chat_messages": "Chat Pleroma" }, "finder": { "error_fetching_user": "Erreur lors de la recherche de l'utilisateur·ice", @@ -32,7 +33,12 @@ "enable": "Activer", "confirm": "Confirmer", "verify": "Vérifier", - "dismiss": "Rejeter" + "dismiss": "Rejeter", + "peek": "Jeter un coup d'œil", + "close": "Fermer", + "retry": "Réessayez", + "error_retry": "Veuillez réessayer", + "loading": "Chargement…" }, "image_cropper": { "crop_picture": "Rogner l'image", @@ -77,15 +83,17 @@ "dms": "Messages directs", "public_tl": "Fil d'actualité public", "timeline": "Fil d'actualité", - "twkn": "Ensemble du réseau connu", + "twkn": "Réseau connu", "user_search": "Recherche d'utilisateur·ice", "who_to_follow": "Qui suivre", "preferences": "Préférences", "search": "Recherche", - "administration": "Administration" + "administration": "Administration", + "chats": "Chats", + "bookmarks": "Marques-Pages" }, "notifications": { - "broken_favorite": "Chargement d'un message inconnu…", + "broken_favorite": "Message inconnu, chargement…", "favorited_you": "a aimé votre statut", "followed_you": "a commencé à vous suivre", "load_older": "Charger les notifications précédentes", @@ -115,7 +123,7 @@ "text/bbcode": "BBCode" }, "content_warning": "Sujet (optionnel)", - "default": "Écrivez ici votre prochain statut.", + "default": "Je viens d'atterrir en Tchéquie.", "direct_warning_to_all": "Ce message sera visible pour toutes les personnes mentionnées.", "direct_warning_to_first_only": "Ce message sera visible uniquement pour personnes mentionnées au début du message.", "posting": "Envoi en cours", @@ -129,7 +137,12 @@ "private": "Abonné·e·s uniquement - Seul·e·s vos abonné·e·s verront vos billets", "public": "Publique - Afficher dans les fils publics", "unlisted": "Non-Listé - Ne pas afficher dans les fils publics" - } + }, + "media_description_error": "Échec de téléversement du media, essayez encore", + "empty_status_error": "Impossible de poster un statut vide sans attachements", + "preview_empty": "Vide", + "preview": "Prévisualisation", + "media_description": "Description de l'attachement" }, "registration": { "bio": "Biographie", @@ -488,7 +501,15 @@ "notification_setting_privacy_option": "Masquer l'expéditeur et le contenu des notifications push", "notification_setting_privacy": "Intimité", "hide_followers_count_description": "Masquer le nombre d'abonnés", - "accent": "Accent" + "accent": "Accent", + "chatMessageRadius": "Message de chat", + "bot": "Ce compte est un robot", + "import_mutes_from_a_csv_file": "Importer les masquages depuis un fichier CSV", + "mutes_imported": "Masquages importés ! Leur application peut prendre du temps.", + "mute_import_error": "Erreur à l'import des masquages", + "mute_import": "Import des masquages", + "mute_export_button": "Exporter vos masquages dans un fichier CSV", + "mute_export": "Export des masquages" }, "timeline": { "collapse": "Fermer", @@ -730,8 +751,13 @@ "instruction": "Entrer votre address de courriel ou votre nom utilisateur. Nous enverrons un lien pour changer votre mot de passe.", "placeholder": "Votre email ou nom d'utilisateur", "return_home": "Retourner à la page d'accueil", - "not_found": "Email ou nom d'utilisateur inconnu.", "too_many_requests": "Vos avez atteint la limite d'essais, essayez plus tard.", "password_reset_required": "Vous devez changer votre mot de passe pour vous authentifier." + }, + "errors": { + "storage_unavailable": "Pleroma n'a pas pu accéder au stockage du navigateur. Votre identifiant ou vos mots de passes ne seront sauvegardés et des problèmes pourront être rencontrés. Essayez d'activer les cookies." + }, + "shoutbox": { + "title": "Shoutbox" } } diff --git a/src/i18n/it.json b/src/i18n/it.json index b88fdd29..67e92b32 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -407,7 +407,14 @@ "reset_background_confirm": "Vuoi veramente azzerare lo sfondo?", "chatMessageRadius": "Messaggi istantanei", "notification_setting_hide_notification_contents": "Nascondi mittente e contenuti delle notifiche push", - "notification_setting_block_from_strangers": "Blocca notifiche da utenti che non segui" + "notification_setting_block_from_strangers": "Blocca notifiche da utenti che non segui", + "virtual_scrolling": "Velocizza l'elaborazione delle sequenze", + "import_mutes_from_a_csv_file": "Importa silenziati da un file CSV", + "mutes_imported": "Silenziati importati! Saranno elaborati a breve.", + "mute_import_error": "Errore nell'importazione", + "mute_import": "Importa silenziati", + "mute_export_button": "Esporta la tua lista di silenziati in un file CSV", + "mute_export": "Esporta silenziati" }, "timeline": { "error_fetching": "Errore nell'aggiornamento", @@ -591,12 +598,12 @@ "reject": "Rifiuta", "accept": "Accetta", "simple_policies": "Regole specifiche alla stanza", - "accept_desc": "Questa stanza accetta messaggi solo dalle seguenti stanze:", - "reject_desc": "Questa stanza non accetterà messaggi dalle stanze seguenti:", + "accept_desc": "Questa stanza accetta messaggi solo dalle seguenti altre:", + "reject_desc": "Questa stanza rifiuterà i messaggi provenienti dalle seguenti:", "quarantine": "Quarantena", - "quarantine_desc": "Questa stanza inoltrerà solo messaggi pubblici alle seguenti stanze:", + "quarantine_desc": "Questa stanza inoltrerà solo messaggi pubblici alle seguenti:", "ftl_removal": "Rimozione dalla sequenza globale", - "ftl_removal_desc": "Questa stanza rimuove le seguenti stanze dalla sequenza globale:", + "ftl_removal_desc": "Questa stanza rimuove le seguenti dalla sequenza globale:", "media_removal": "Rimozione multimedia", "media_removal_desc": "Questa istanza rimuove gli allegati dalle seguenti stanze:", "media_nsfw": "Allegati oscurati forzatamente", @@ -695,7 +702,8 @@ "reply_to": "Rispondi a", "delete_confirm": "Vuoi veramente eliminare questo messaggio?", "unbookmark": "Rimuovi segnalibro", - "bookmark": "Aggiungi segnalibro" + "bookmark": "Aggiungi segnalibro", + "status_deleted": "Questo messagio è stato cancellato" }, "time": { "years_short": "{0}a", @@ -745,7 +753,6 @@ "password_reset_required": "Devi reimpostare la tua password per poter continuare.", "password_reset_disabled": "Non puoi azzerare la tua password. Contatta il tuo amministratore.", "too_many_requests": "Hai raggiunto il numero massimo di tentativi, riprova più tardi.", - "not_found": "Non ho trovato questa email o nome utente.", "return_home": "Torna alla pagina principale", "check_email": "Controlla la tua posta elettronica.", "placeholder": "La tua email o nome utente", diff --git a/src/i18n/ja_easy.json b/src/i18n/ja_easy.json index 255648e7..991f3762 100644 --- a/src/i18n/ja_easy.json +++ b/src/i18n/ja_easy.json @@ -666,7 +666,6 @@ "placeholder": "あなたのメールアドレスかユーザーめい", "check_email": "パスワードをリセットするためのリンクがかかれたメールが、とどいているかどうか、みてください。", "return_home": "ホームページにもどる", - "not_found": "そのメールアドレスまたはユーザーめいを、みつけることができませんでした。", "too_many_requests": "パスワードリセットを、ためすことが、おおすぎます。しばらくしてから、ためしてください。", "password_reset_disabled": "このインスタンスでは、パスワードリセットは、できません。インスタンスのアドミニストレーターに、おといあわせください。", "password_reset_required": "ログインするには、パスワードをリセットしてください。", diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json index 07fea45d..e2de1066 100644 --- a/src/i18n/ja_pedantic.json +++ b/src/i18n/ja_pedantic.json @@ -625,7 +625,6 @@ "placeholder": "メールアドレスまたはユーザー名", "check_email": "パスワードをリセットするためのリンクが記載されたメールが届いているか確認してください。", "return_home": "ホームページに戻る", - "not_found": "メールアドレスまたはユーザー名が見つかりませんでした。", "too_many_requests": "試行回数の制限に達しました。しばらく時間を置いてから再試行してください。", "password_reset_disabled": "このインスタンスではパスワードリセットは無効になっています。インスタンスの管理者に連絡してください。" } diff --git a/src/i18n/nl.json b/src/i18n/nl.json index e7509f12..a01e57a0 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -677,7 +677,6 @@ "password_reset_required": "Je dient je wachtwoord opnieuw in te stellen om in te kunnen loggen.", "password_reset_disabled": "Wachtwoord reset is uitgeschakeld. Neem contact op met de beheerder van deze instantie.", "too_many_requests": "Je hebt het maximaal aantal pogingen bereikt, probeer het later opnieuw.", - "not_found": "We kunnen die email of gebruikersnaam niet vinden.", "return_home": "Terugkeren naar de home pagina", "check_email": "Controleer je email inbox voor een link om je wachtwoord opnieuw in te stellen.", "placeholder": "Je email of gebruikersnaam", diff --git a/src/i18n/pl.json b/src/i18n/pl.json index 5863ba8e..dfa0729d 100644 --- a/src/i18n/pl.json +++ b/src/i18n/pl.json @@ -49,7 +49,8 @@ "scope_options": "Ustawienia zakresu", "text_limit": "Limit tekstu", "title": "Funkcje", - "who_to_follow": "Propozycje obserwacji" + "who_to_follow": "Propozycje obserwacji", + "pleroma_chat_messages": "Czat Pleromy" }, "finder": { "error_fetching_user": "Błąd przy pobieraniu profilu", @@ -71,7 +72,9 @@ "verify": "Zweryfikuj", "close": "Zamknij", "loading": "Ładowanie…", - "retry": "Spróbuj ponownie" + "retry": "Spróbuj ponownie", + "peek": "Spójrz", + "error_retry": "Spróbuj ponownie" }, "image_cropper": { "crop_picture": "Przytnij obrazek", @@ -117,12 +120,14 @@ "dms": "Wiadomości prywatne", "public_tl": "Publiczna oś czasu", "timeline": "Oś czasu", - "twkn": "Cała znana sieć", + "twkn": "Znana sieć", "user_search": "Wyszukiwanie użytkowników", "search": "Wyszukiwanie", "who_to_follow": "Sugestie obserwacji", "preferences": "Preferencje", - "bookmarks": "Zakładki" + "bookmarks": "Zakładki", + "chats": "Czaty", + "timelines": "Osie czasu" }, "notifications": { "broken_favorite": "Nieznany status, szukam go…", @@ -197,7 +202,9 @@ }, "preview_empty": "Pusty", "preview": "Podgląd", - "empty_status_error": "Nie można wysłać pustego wpisu bez plików" + "empty_status_error": "Nie można wysłać pustego wpisu bez plików", + "media_description_error": "Nie udało się zaktualizować mediów, spróbuj ponownie", + "media_description": "Opis mediów" }, "registration": { "bio": "Bio", @@ -400,7 +407,7 @@ "theme_help_v2_1": "Możesz też zastąpić kolory i widoczność poszczególnych komponentów przełączając pola wyboru, użyj „Wyczyść wszystko” aby usunąć wszystkie zastąpienia.", "theme_help_v2_2": "Ikony pod niektórych wpisami są wskaźnikami kontrastu pomiędzy tłem a tekstem, po najechaniu na nie otrzymasz szczegółowe informacje. Zapamiętaj, że jeżeli używasz przezroczystości, wskaźniki pokazują najgorszy możliwy przypadek.", "tooltipRadius": "Etykiety/alerty", - "type_domains_to_mute": "Wpisz domeny, które chcesz wyciszyć", + "type_domains_to_mute": "Wyszukaj domeny, które chcesz wyciszyć", "upload_a_photo": "Wyślij zdjęcie", "user_settings": "Ustawienia użytkownika", "values": { @@ -492,7 +499,8 @@ "tabs": "Karty", "chat": { "outgoing": "Wiadomości wychodzące", - "incoming": "Wiadomości przychodzące" + "incoming": "Wiadomości przychodzące", + "border": "Granica" } }, "radii": { @@ -573,7 +581,22 @@ "add_field": "Dodaj pole" }, "bot": "To konto jest prowadzone przez bota", - "notification_setting_hide_notification_contents": "Ukryj nadawcę i zawartość powiadomień push" + "notification_setting_hide_notification_contents": "Ukryj nadawcę i zawartość powiadomień push", + "notification_setting_block_from_strangers": "Zablokuj powiadomienia od użytkowników których nie obserwujesz", + "virtual_scrolling": "Optymalizuj renderowanie osi czasu", + "reset_background_confirm": "Czy naprawdę chcesz zresetować tło?", + "reset_banner_confirm": "Czy naprawdę chcesz zresetować banner?", + "reset_avatar_confirm": "Czy naprawdę chcesz zresetować awatar?", + "reset_profile_banner": "Zresetuj banner profilowy", + "reset_profile_background": "Zresetuj tło profilowe", + "mutes_and_blocks": "Wyciszenia i blokady", + "chatMessageRadius": "Wiadomość czatu", + "import_mutes_from_a_csv_file": "Zaimportuj wyciszenia z pliku .csv", + "mutes_imported": "Zaimportowano wyciszenia! Przetwarzanie zajmie chwilę.", + "mute_import_error": "Wystąpił błąd podczas importowania wyciszeń", + "mute_import": "Import wyciszeń", + "mute_export_button": "Wyeksportuj swoje wyciszenia do pliku .csv", + "mute_export": "Eksport wyciszeń" }, "time": { "day": "{0} dzień", @@ -639,7 +662,11 @@ "unbookmark": "Usuń z zakładek", "bookmark": "Dodaj do zakładek", "hide_content": "Ukryj zawartość", - "show_content": "Pokaż zawartość" + "show_content": "Pokaż zawartość", + "hide_full_subject": "Ukryj cały temat", + "show_full_subject": "Pokaż cały temat", + "thread_muted_and_words": ", ma słowa:", + "thread_muted": "Wątek wyciszony" }, "user_card": { "approve": "Przyjmij", @@ -723,7 +750,8 @@ "add_reaction": "Dodaj reakcję", "user_settings": "Ustawienia użytkownika", "accept_follow_request": "Akceptuj prośbę o możliwość obserwacji", - "reject_follow_request": "Odrzuć prośbę o możliwość obserwacji" + "reject_follow_request": "Odrzuć prośbę o możliwość obserwacji", + "bookmark": "Zakładka" }, "upload": { "error": { @@ -753,7 +781,6 @@ "placeholder": "Twój email lub nazwa użytkownika", "check_email": "Sprawdź pocztę, aby uzyskać link do zresetowania hasła.", "return_home": "Wróć do strony głównej", - "not_found": "Nie mogliśmy znaleźć tego emaila lub nazwy użytkownika.", "too_many_requests": "Przekroczyłeś(-aś) limit prób, spróbuj ponownie później.", "password_reset_disabled": "Resetowanie hasła jest wyłączone. Proszę skontaktuj się z administratorem tej instancji.", "password_reset_required": "Musisz zresetować hasło, by się zalogować.", @@ -774,9 +801,17 @@ "error_sending_message": "Coś poszło nie tak podczas wysyłania wiadomości.", "error_loading_chat": "Coś poszło nie tak podczas ładowania czatu.", "empty_message_error": "Nie można wysłać pustej wiadomości", - "new": "Nowy czat" + "new": "Nowy czat", + "empty_chat_list_placeholder": "Nie masz jeszcze żadnych czatów. Zacznij nowy czat!", + "chats": "Czaty" }, "display_date": { "today": "Dzisiaj" + }, + "shoutbox": { + "title": "Shoutbox" + }, + "errors": { + "storage_unavailable": "Pleroma nie mogła uzyskać dostępu do pamięci masowej przeglądarki. Twój login lub lokalne ustawienia nie zostaną zapisane i możesz napotkać problemy. Spróbuj włączyć ciasteczka." } } diff --git a/src/i18n/ru.json b/src/i18n/ru.json index df172935..8f421b50 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -420,7 +420,6 @@ "placeholder": "Ваш email или имя пользователя", "check_email": "Проверьте ваш email и перейдите по ссылке для сброса пароля.", "return_home": "Вернуться на главную страницу", - "not_found": "Мы не смогли найти аккаунт с таким email-ом или именем пользователя.", "too_many_requests": "Вы исчерпали допустимое количество попыток, попробуйте позже.", "password_reset_disabled": "Сброс пароля отключен. Cвяжитесь с администратором вашего сервера." }, @@ -474,5 +473,10 @@ "tool_tip": { "accept_follow_request": "Принять запрос на чтение", "reject_follow_request": "Отклонить запрос на чтение" + }, + "image_cropper": { + "save_without_cropping": "Сохранить не обрезая", + "save": "Сохранить", + "crop_picture": "Обрезать картинку" } } diff --git a/src/i18n/zh.json b/src/i18n/zh.json index 24b799df..fa15991b 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -11,9 +11,10 @@ "gopher": "Gopher", "media_proxy": "媒体代理", "scope_options": "可见范围设置", - "text_limit": "文本长度限制", + "text_limit": "文字数量限制", "title": "功能", - "who_to_follow": "推荐关注" + "who_to_follow": "推荐关注", + "pleroma_chat_messages": "Pleroma 聊天" }, "finder": { "error_fetching_user": "获取用户时发生错误", @@ -23,8 +24,8 @@ "apply": "应用", "submit": "提交", "more": "更多", - "generic_error": "发生一个错误", - "optional": "可选项", + "generic_error": "发生了一个错误", + "optional": "可选", "show_more": "展开", "show_less": "收起", "cancel": "取消", @@ -32,7 +33,12 @@ "enable": "启用", "confirm": "确认", "verify": "验证", - "dismiss": "忽略" + "dismiss": "忽略", + "peek": "窥探", + "close": "关闭", + "retry": "重试", + "error_retry": "请重试", + "loading": "载入中…" }, "image_cropper": { "crop_picture": "裁剪图片", @@ -77,37 +83,40 @@ "dms": "私信", "public_tl": "公共时间线", "timeline": "时间线", - "twkn": "所有已知网络", + "twkn": "已知网络", "user_search": "用户搜索", "search": "搜索", "who_to_follow": "推荐关注", "preferences": "偏好设置", - "administration": "管理员" + "administration": "管理员", + "chats": "聊天", + "timelines": "时间线", + "bookmarks": "书签" }, "notifications": { "broken_favorite": "未知的状态,正在搜索中…", - "favorited_you": "收藏了你的状态", + "favorited_you": "喜欢了你的状态", "followed_you": "关注了你", "load_older": "加载更早的通知", "notifications": "通知", - "read": "阅读!", + "read": "已阅!", "repeated_you": "转发了你的状态", "no_more_notifications": "没有更多的通知", - "reacted_with": "和 {0} 互动过", + "reacted_with": "作出了 {0} 的反应", "migrated_to": "迁移到", "follow_request": "想要关注你" }, "polls": { - "add_poll": "增加问卷调查", + "add_poll": "增加投票", "add_option": "增加选项", "option": "选项", "votes": "投票", "vote": "投票", - "type": "问卷类型", - "single_choice": "单选项", - "multiple_choices": "多选项", - "expiry": "问卷的时间", - "expires_in": "投票于 {0} 内结束", + "type": "投票类型", + "single_choice": "单选", + "multiple_choices": "多选", + "expiry": "投票期限", + "expires_in": "投票于 {0} 后结束", "expired": "投票 {0} 前已结束", "not_enough_options": "投票的选项太少" }, @@ -132,7 +141,7 @@ "text/bbcode": "BBCode" }, "content_warning": "主题(可选)", - "default": "刚刚抵达上海", + "default": "刚刚抵达洛杉矶", "direct_warning_to_all": "本条内容只有被提及的用户能够看到。", "direct_warning_to_first_only": "本条内容只有被在消息开始处提及的用户能够看到。", "posting": "发送", @@ -146,7 +155,12 @@ "private": "仅关注者 - 只有关注了你的人能看到", "public": "公共 - 发送到公共时间轴", "unlisted": "不公开 - 不会发送到公共时间轴" - } + }, + "preview_empty": "空的", + "preview": "预览", + "media_description": "媒体描述", + "media_description_error": "更新媒体失败,请重试", + "empty_status_error": "不能发布没有内容、没有附件的发文" }, "registration": { "bio": "简介", @@ -175,7 +189,7 @@ "settings": { "app_name": "App 名称", "security": "安全", - "enter_current_password_to_confirm": "输入你当前密码来确认你的身份", + "enter_current_password_to_confirm": "输入您当前的密码来确认您的身份", "mfa": { "otp": "OTP", "setup_otp": "设置 OTP", @@ -183,18 +197,18 @@ "confirm_and_enable": "确认并启用 OTP", "title": "双因素验证", "generate_new_recovery_codes": "生成新的恢复码", - "warning_of_generate_new_codes": "当你生成新的恢复码时,你的旧恢复码就失效了。", + "warning_of_generate_new_codes": "当您生成新的恢复码时,您旧的恢复码将会失效。", "recovery_codes": "恢复码。", "waiting_a_recovery_codes": "正在接收备份码…", - "recovery_codes_warning": "抄写这些号码,或者保存在安全的地方。这些号码不会再次显示。如果你无法访问你的 2FA app,也丢失了你的恢复码,你的账号就再也无法登录了。", + "recovery_codes_warning": "抄写这些号码,或者将其保存在安全的地方。这些号码不会再次显示。如果您无法访问您的 2FA app,也丢失了您的恢复码,您就再也无法登录您的账号了。", "authentication_methods": "身份验证方法", "scan": { "title": "扫一下", - "desc": "使用你的双因素验证 app,扫描这个二维码,或者输入这些文字密钥:", + "desc": "使用您的双因素验证 app,扫描这个二维码,或者输入这些文字密钥:", "secret_code": "密钥" }, "verify": { - "desc": "要启用双因素验证,请把你的双因素验证 app 里的数字输入:" + "desc": "要启用双因素验证,请输入您的双因素验证 app 里的数字:" } }, "attachmentRadius": "附件", @@ -204,12 +218,12 @@ "avatarRadius": "头像", "background": "背景", "bio": "简介", - "block_export": "拉黑名单导出", - "block_export_button": "导出你的拉黑名单到一个 csv 文件", - "block_import": "拉黑名单导入", - "block_import_error": "导入拉黑名单出错", - "blocks_imported": "拉黑名单导入成功!需要一点时间来处理。", - "blocks_tab": "块", + "block_export": "屏蔽名单导出", + "block_export_button": "导出你的屏蔽名单到一个 csv 文件", + "block_import": "屏蔽名单导入", + "block_import_error": "导入屏蔽名单出错", + "blocks_imported": "屏蔽名单导入成功!需要一点时间来处理。", + "blocks_tab": "屏蔽", "btnRadius": "按钮", "cBlue": "蓝色(回复,关注)", "cGreen": "绿色(转发)", @@ -229,7 +243,7 @@ "delete_account": "删除账户", "delete_account_description": "永久删除你的帐号和所有数据。", "delete_account_error": "删除账户时发生错误,如果一直删除不了,请联系实例管理员。", - "delete_account_instructions": "在下面输入你的密码来确认删除账户", + "delete_account_instructions": "在下面输入您的密码来确认删除账户。", "avatar_size_instruction": "推荐的头像图片最小的尺寸是 150x150 像素。", "export_theme": "导出预置主题", "filtering": "过滤器", @@ -263,7 +277,7 @@ "invalid_theme_imported": "您所选择的主题文件不被 Pleroma 支持,因此主题未被修改。", "limited_availability": "在您的浏览器中无法使用", "links": "链接", - "lock_account_description": "你需要手动审核关注请求", + "lock_account_description": "您需要手动审核关注请求", "loop_video": "循环视频", "loop_video_silent_only": "只循环没有声音的视频(例如:Mastodon 里的“GIF”)", "mutes_tab": "隐藏", @@ -278,7 +292,7 @@ "notification_visibility_mentions": "提及", "notification_visibility_repeats": "转发", "no_rich_text_description": "不显示富文本格式", - "no_blocks": "没有拉黑的", + "no_blocks": "没有屏蔽", "no_mutes": "没有隐藏", "hide_follows_description": "不要显示我所关注的人", "hide_followers_description": "不要显示关注我的人", @@ -324,7 +338,7 @@ "text": "文本", "theme": "主题", "theme_help": "使用十六进制代码(#rrggbb)来设置主题颜色。", - "theme_help_v2_1": "你也可以通过切换复选框来覆盖某些组件的颜色和透明。使用“清除所有”来清楚所有覆盖设置。", + "theme_help_v2_1": "您也可以通过选中复选框来覆盖某些组件的颜色和透明度。使用“清除所有”按钮来清除所有覆盖设置。", "theme_help_v2_2": "某些条目下的图标是背景或文本对比指示器,鼠标悬停可以获取详细信息。请记住,使用透明度来显示最差的情况。", "tooltipRadius": "提醒", "upload_a_photo": "上传照片", @@ -335,7 +349,7 @@ }, "notifications": "通知", "notification_mutes": "要停止收到某个指定的用户的通知,请使用隐藏功能。", - "notification_blocks": "拉黑一个用户会停掉所有他的通知,等同于取消关注。", + "notification_blocks": "屏蔽一个用户会停止接收来自该用户的所有通知,并且会取消对该用户的关注。", "enable_web_push_notifications": "启用 web 推送通知", "style": { "switcher": { @@ -350,7 +364,17 @@ "clear_opacity": "清除透明度", "load_theme": "加载主题", "help": { - "upgraded_from_v2": "PleromaFE 已升级,主题会和你记忆中的不太一样。" + "upgraded_from_v2": "PleromaFE 已升级,主题会与您记忆中的不太一样。", + "older_version_imported": "您导入的文件来自旧版本的 FE。", + "future_version_imported": "您导入的文件来自更高版本的 FE。", + "v2_imported": "您导入的文件是旧版 FE 的。我们尽可能保持兼容性,但还是可能出现不一致的情况。", + "snapshot_source_mismatch": "版本冲突:很有可能是 FE 版本回滚后再次升级了,如果您使用旧版本的 FE 更改了主题那么您可能需要使用旧版本,否则请使用新版本。", + "migration_napshot_gone": "不知出于何种原因,主题快照缺失了,一些地方可能与您印象中的不符。", + "migration_snapshot_ok": "为保万无一失,加载了主题快照。您可以试着加载主题数据。", + "fe_downgraded": "PleromaFE 的版本回滚了。", + "fe_upgraded": "PleromaFE 的主题引擎随着版本更新升级了。", + "snapshot_missing": "在文件中没有主题快照,所以网站外观可能会与原来预想的不同。", + "snapshot_present": "主题快照已加载,因此所有的值均被覆盖。您可以改为加载主题的实际数据。" }, "use_source": "新版本", "use_snapshot": "老版本", @@ -389,7 +413,25 @@ "borders": "边框", "buttons": "按钮", "inputs": "输入框", - "faint_text": "灰度文字" + "faint_text": "灰度文字", + "chat": { + "border": "边框", + "outgoing": "发出的", + "incoming": "收到的" + }, + "disabled": "禁用的", + "pressed": "按下的", + "highlight": "强调元素", + "selectedMenu": "选中的菜单项", + "selectedPost": "选中的发布内容", + "icons": "图标", + "poll": "投票统计图", + "popover": "提示框,菜单,弹出框", + "post": "发布内容/用户简介", + "alert_neutral": "中性", + "alert_warning": "警告", + "tabs": "标签页", + "underlay": "底衬" }, "radii": { "_tab_label": "圆角" @@ -426,7 +468,7 @@ }, "fonts": { "_tab_label": "字体", - "help": "给用户界面的元素选择字体。选择 “自选”的你必须输入确切的字体名称。", + "help": "为用户界面的元素选择字体。若选择 “自选”,您必须输入与系统显示完全一致的字体名称。", "components": { "interface": "界面", "input": "输入框", @@ -461,7 +503,7 @@ "notification_setting_filters": "过滤器", "domain_mutes": "域名", "changed_email": "邮箱修改成功!", - "change_email_error": "修改你的电子邮箱时发生错误", + "change_email_error": "修改您的电子邮箱时发生错误。", "change_email": "修改电子邮箱", "allow_following_move": "正在关注的账号迁移时自动重新关注", "notification_setting_privacy_option": "在通知推送中隐藏发送者和内容", @@ -470,7 +512,41 @@ "notification_visibility_emoji_reactions": "互动", "notification_visibility_moves": "用户迁移", "new_email": "新邮箱", - "emoji_reactions_on_timeline": "在时间线上显示表情符号互动" + "emoji_reactions_on_timeline": "在时间线上显示表情符号互动", + "notification_setting_hide_notification_contents": "隐藏推送通知中的发送者与内容信息", + "notification_setting_block_from_strangers": "屏蔽来自你没有关注的用户的通知", + "type_domains_to_mute": "搜索需要隐藏的域名", + "useStreamingApi": "实时接收发布以及通知", + "user_mutes": "用户", + "reset_background_confirm": "您确定要重置个人资料背景图吗?", + "reset_banner_confirm": "您确定要重置横幅图片吗?", + "reset_avatar_confirm": "您确定要重置头像吗?", + "reset_profile_banner": "重置横幅图片", + "reset_profile_background": "重置个人资料背景图", + "reset_avatar": "重置头像", + "hide_followers_count_description": "不显示关注者数量", + "profile_fields": { + "value": "内容", + "name": "标签", + "add_field": "添加字段", + "label": "个人资料元数据" + }, + "accent": "强调色", + "pad_emoji": "从表情符号选择器插入表情符号时,在表情两侧插入空格", + "discoverable": "允许通过搜索检索等服务找到此账号", + "mutes_and_blocks": "隐藏与屏蔽", + "bot": "这是一个机器人账号", + "fun": "趣味", + "useStreamingApiWarning": "(不推荐使用,试验性,已知会跳过一些消息)", + "chatMessageRadius": "聊天消息", + "greentext": "Meme 箭头", + "virtual_scrolling": "优化时间线渲染", + "import_mutes_from_a_csv_file": "从 csv 文件导入隐藏名单", + "mutes_imported": "隐藏名单导入成功!处理它们将需要一段时间。", + "mute_import_error": "导入隐藏名单出错", + "mute_import": "隐藏名单导入", + "mute_export_button": "导出你的隐藏名单到一个 csv 文件", + "mute_export": "隐藏名单导出" }, "time": { "day": "{0} 天", @@ -516,7 +592,8 @@ "show_new": "显示新内容", "up_to_date": "已是最新", "no_more_statuses": "没有更多的状态", - "no_statuses": "没有状态更新" + "no_statuses": "没有状态更新", + "reload": "重新载入" }, "status": { "favorites": "收藏", @@ -529,7 +606,18 @@ "reply_to": "回复", "replies_list": "回复:", "mute_conversation": "隐藏对话", - "unmute_conversation": "对话取消隐藏" + "unmute_conversation": "对话取消隐藏", + "hide_content": "隐藏内容", + "show_content": "显示内容", + "hide_full_subject": "隐藏此部分标题", + "show_full_subject": "显示全部标题", + "thread_muted": "此系列消息已被隐藏", + "copy_link": "复制状态链接", + "status_unavailable": "状态不可取得", + "unbookmark": "取消书签", + "bookmark": "书签", + "thread_muted_and_words": ",含有过滤词:", + "status_deleted": "该状态已被删除" }, "user_card": { "approve": "允许", @@ -556,9 +644,9 @@ "statuses": "状态", "subscribe": "订阅", "unsubscribe": "退订", - "unblock": "取消拉黑", - "unblock_progress": "取消拉黑中…", - "block_progress": "拉黑中…", + "unblock": "取消屏蔽", + "unblock_progress": "正在取消屏蔽…", + "block_progress": "正在屏蔽…", "unmute": "取消隐藏", "unmute_progress": "取消隐藏中…", "mute_progress": "隐藏中…", @@ -579,11 +667,13 @@ "disable_any_subscription": "完全禁止关注用户", "quarantine": "从联合实例中禁止用户帖子", "delete_user": "删除用户", - "delete_user_confirmation": "你确认吗?此操作无法撤销。" + "delete_user_confirmation": "你确定吗?此操作无法撤销。" }, "hidden": "已隐藏", "show_repeats": "显示转发", - "hide_repeats": "隐藏转发" + "hide_repeats": "隐藏转发", + "message": "消息", + "mention": "提及" }, "user_profile": { "timeline_title": "用户时间线", @@ -592,12 +682,12 @@ }, "user_reporting": { "title": "报告 {0}", - "add_comment_description": "此报告会发送给你的实例管理员。你可以在下面提供更多详细信息解释报告的缘由:", + "add_comment_description": "此报告会发送给您的实例管理员。您可以在下面提供更多详细信息解释报告的缘由:", "additional_comments": "其它信息", "forward_description": "这个账号是从另外一个服务器。同时发送一个副本到那里?", "forward_to": "转发 {0}", "submit": "提交", - "generic_error": "当处理你的请求时,发生了一个错误。" + "generic_error": "当处理您的请求时,发生了一个错误。" }, "who_to_follow": { "more": "更多", @@ -610,7 +700,9 @@ "favorite": "收藏", "user_settings": "用户设置", "reject_follow_request": "拒绝关注请求", - "add_reaction": "添加互动" + "add_reaction": "添加互动", + "bookmark": "书签", + "accept_follow_request": "接受关注请求" }, "upload": { "error": { @@ -628,7 +720,7 @@ }, "search": { "people": "人", - "hashtags": "Hashtags", + "hashtags": "话题标签", "person_talking": "{count} 人正在讨论", "people_talking": "{count} 人正在讨论", "no_results": "没有搜索结果" @@ -636,13 +728,14 @@ "password_reset": { "forgot_password": "忘记密码了?", "password_reset": "重置密码", - "instruction": "输入你的电邮地址或者用户名,我们将发送一个链接到你的邮箱,用于重置密码。", - "placeholder": "你的电邮地址或者用户名", - "check_email": "检查你的邮箱,会有一个链接用于重置密码。", + "instruction": "输入您的电邮地址或者用户名,我们将发送一个链接到您的邮箱,用于重置密码。", + "placeholder": "您的电邮地址或者用户名", + "check_email": "检查您的邮箱,会有一个链接用于重置密码。", "return_home": "回到首页", - "not_found": "我们无法找到匹配的邮箱地址或者用户名。", - "too_many_requests": "你触发了尝试的限制,请稍后再试。", - "password_reset_disabled": "密码重置已经被禁用。请联系你的实例管理员。" + "too_many_requests": "您达到了尝试次数的上限,请稍后再试。", + "password_reset_disabled": "密码重置已被禁用。请联系您的实例管理员。", + "password_reset_required_but_mailer_is_disabled": "您必须重置密码,但是密码重置被禁用了。请联系您所在实例的管理员。", + "password_reset_required": "您必须重置密码才能登陆。" }, "remote_user_resolver": { "error": "未找到。", @@ -651,27 +744,34 @@ }, "emoji": { "keep_open": "选择器保持打开", - "stickers": "贴图", + "stickers": "贴纸", "unicode": "Unicode 表情符号", "custom": "自定义表情符号", "add_emoji": "插入表情符号", "search_emoji": "搜索表情符号", - "emoji": "表情符号" + "emoji": "表情符号", + "load_all": "加载所有表情符号(共 {emojiAmount} 个)", + "load_all_hint": "最先加载的 {saneAmount} 表情符号,加载全部表情符号可能会带来性能问题。" }, "about": { "mrf": { "simple": { - "quarantine_desc": "本实例只会把公开状态发送非下列实例:", + "quarantine_desc": "对于下列实例,本实例只发送公开的状态,不发送其它状态:", "quarantine": "隔离", "reject_desc": "本实例不会接收来自下列实例的消息:", "reject": "拒绝", "accept_desc": "本实例只接收来自下列实例的消息:", - "simple_policies": "站规", + "simple_policies": "对于特定实例的策略", "accept": "接受", - "media_removal": "移除媒体" + "media_removal": "移除媒体", + "media_nsfw_desc": "本实例将来自以下实例的媒体强制设置为敏感内容:", + "media_nsfw": "强制设置媒体为敏感内容", + "media_removal_desc": "本实例移除了来自以下实例的媒体内容:", + "ftl_removal_desc": "该实例在从“全部已知网络”时间线上移除了下列实例:", + "ftl_removal": "从“全部已知网络”时间线上移除" }, "mrf_policies_desc": "MRF 策略会影响本实例的互通行为。以下策略已启用:", - "mrf_policies": "已启动 MRF 策略", + "mrf_policies": "已启动的 MRF 策略", "keyword": { "ftl_removal": "从“全部已知网络”时间线上移除", "keyword_policies": "关键词策略", @@ -679,13 +779,42 @@ "replace": "替换", "reject": "拒绝" }, - "federation": "联邦" - } + "federation": "联邦互通" + }, + "staff": "管理人员" }, "domain_mute_card": { "unmute_progress": "正在取消隐藏…", "unmute": "取消隐藏", "mute_progress": "隐藏中…", "mute": "隐藏" + }, + "errors": { + "storage_unavailable": "Pleroma 无法访问浏览器储存。您的登陆名以及本地设置将不会被保存,您可能遇到意外问题。请尝试启用 cookies。" + }, + "shoutbox": { + "title": "留言板" + }, + "display_date": { + "today": "今天" + }, + "file_type": { + "file": "文件", + "image": "图片", + "video": "视频", + "audio": "音频" + }, + "chats": { + "empty_chat_list_placeholder": "您还没有任何聊天记录。开始聊天吧!", + "error_sending_message": "发送消息时出了点问题。", + "error_loading_chat": "加载聊天时出了点问题。", + "delete_confirm": "您确实要删除此消息吗?", + "more": "更多", + "empty_message_error": "无法发布空消息", + "new": "新聊天", + "chats": "聊天", + "delete": "删除", + "message_user": "发消息给 {nickname}", + "you": "你:" } } diff --git a/src/i18n/zh_Hant.json b/src/i18n/zh_Hant.json new file mode 100644 index 00000000..79a992fc --- /dev/null +++ b/src/i18n/zh_Hant.json @@ -0,0 +1,810 @@ +{ + "emoji": { + "unicode": "統一碼繪文字", + "custom": "自定義繪文字", + "add_emoji": "插入繪文字", + "search_emoji": "搜索繪文字", + "keep_open": "選擇器保持打開", + "emoji": "繪文字", + "stickers": "貼紙", + "load_all": "加載所有繪文字(共 {emojiAmount} 個)", + "load_all_hint": "最先加載的 {saneAmount} ,加載全部繪文字可能會帶來性能問題。" + }, + "polls": { + "not_enough_options": "投票的選項太少", + "expired": "投票 {0} 前已結束", + "expires_in": "投票於 {0} 內結束", + "expiry": "投票期限", + "multiple_choices": "多選", + "single_choice": "單選", + "type": "問卷類型", + "vote": "投票", + "votes": "票", + "option": "選項", + "add_option": "增加選項", + "add_poll": "增加投票" + }, + "notifications": { + "reacted_with": "和 {0} 互動過", + "migrated_to": "遷移到", + "no_more_notifications": "沒有更多的通知", + "repeated_you": "轉發了你的發文", + "read": "已閱!", + "notifications": "通知", + "load_older": "載入更早的通知", + "follow_request": "想要關注你", + "followed_you": "關注了你", + "favorited_you": "喜歡了你的發文", + "broken_favorite": "未知的狀態,正在搜索中…" + }, + "nav": { + "chats": "聊天", + "timelines": "時間線", + "preferences": "偏好設置", + "who_to_follow": "推薦關注", + "search": "搜索", + "user_search": "用戶搜索", + "bookmarks": "書籤", + "twkn": "已知網絡", + "timeline": "時間線", + "public_tl": "公共時間線", + "dms": "私信", + "interactions": "互動", + "mentions": "提及", + "friend_requests": "關注請求", + "back": "後退", + "administration": "管理", + "about": "關於" + }, + "media_modal": { + "next": "往後", + "previous": "往前" + }, + "login": { + "heading": { + "recovery": "雙重因素恢復", + "totp": "雙重因素驗證" + }, + "recovery_code": "恢復碼", + "enter_two_factor_code": "輸入一個雙重因素驗證碼", + "enter_recovery_code": "輸入一個恢復碼", + "authentication_code": "驗證碼", + "hint": "登錄後加入討論", + "username": "用戶名", + "register": "註冊", + "placeholder": "例:鈴音", + "password": "密碼", + "logout": "登出", + "description": "用 OAuth 登入", + "login": "登入" + }, + "importer": { + "error": "導入此文件時出現一個錯誤。", + "success": "導入成功。", + "submit": "提交" + }, + "image_cropper": { + "cancel": "取消", + "save_without_cropping": "保存不裁剪", + "save": "保存", + "crop_picture": "裁剪圖片" + }, + "general": { + "peek": "窺視", + "close": "關閉", + "verify": "驗證", + "confirm": "確認", + "enable": "啟用", + "disable": "禁用", + "cancel": "取消", + "dismiss": "忽略", + "show_less": "收起", + "show_more": "展開", + "optional": "可選", + "retry": "再試", + "error_retry": "請再試", + "generic_error": "發生一個錯誤", + "loading": "載入中…", + "more": "更多", + "submit": "提交", + "apply": "應用" + }, + "finder": { + "find_user": "尋找用戶", + "error_fetching_user": "獲取用戶時發生錯誤" + }, + "features_panel": { + "who_to_follow": "推薦關注", + "title": "特色", + "text_limit": "文字數量限制", + "scope_options": "可見範圍設置", + "media_proxy": "媒體代理", + "pleroma_chat_messages": "Pleroma 聊天", + "chat": "聊天", + "gopher": "Gopher" + }, + "exporter": { + "processing": "正在處理,稍後會提示您下載文件", + "export": "導出" + }, + "domain_mute_card": { + "unmute_progress": "取消靜音中…", + "unmute": "取消靜音", + "mute_progress": "靜音中…", + "mute": "靜音" + }, + "shoutbox": { + "title": "留言板" + }, + "about": { + "staff": "職員", + "mrf": { + "simple": { + "media_nsfw_desc": "這個實例強迫以下實例的帖子媒體設定為敏感:", + "media_nsfw": "媒體強制設定為敏感", + "media_removal_desc": "這個實例移除以下實例的帖子媒體:", + "media_removal": "移除媒體", + "ftl_removal_desc": "這個實例在所有已知網絡中移除下列實例:", + "ftl_removal": "從所有已知網路中移除", + "quarantine_desc": "本實例只會把公開發文發送到下列實例:", + "quarantine": "隔離", + "reject_desc": "本實例不會接收來自下列實例的消息:", + "reject": "拒絕", + "accept_desc": "本實例只接收來自下列實例的消息:", + "simple_policies": "站規", + "accept": "接受" + }, + "mrf_policies_desc": "MRF 策略會影響本實例的互通行為。以下策略已啟用:", + "keyword": { + "ftl_removal": "從“全部已知網絡”時間線上移除", + "replace": "取代", + "reject": "拒絕", + "is_replaced_by": "→", + "keyword_policies": "關鍵字政策" + }, + "mrf_policies": "已啟用的MRF政策", + "federation": "聯邦" + } + }, + "settings": { + "style": { + "common": { + "color": "顏色", + "contrast": { + "context": { + "18pt": "大字文本 (18pt+)", + "text": "文本" + }, + "level": { + "aaa": "符合 AAA 等級準則(推薦)", + "aa": "符合 AA 等級準則(最低)", + "bad": "不符合任何輔助功能指南" + }, + "hint": "對比度是 {ratio}, 它 {level} {context}" + }, + "opacity": "透明度" + }, + "advanced_colors": { + "faint_text": "灰度文字", + "alert_error": "錯誤", + "badge_notification": "通知", + "alert": "提醒或警告背景色", + "_tab_label": "高级", + "alert_warning": "警告", + "alert_neutral": "中性", + "post": "帖子/用戶簡介", + "badge": "徽章背景", + "popover": "提示框,菜單,彈出框", + "panel_header": "面板標題", + "top_bar": "頂欄", + "borders": "邊框", + "buttons": "按鈕", + "inputs": "輸入框", + "underlay": "底襯", + "poll": "投票統計圖", + "icons": "圖標", + "highlight": "強調元素", + "pressed": "按下", + "selectedPost": "選中的帖子", + "selectedMenu": "選中的菜單項", + "disabled": "關閉", + "toggled": "切換", + "tabs": "標籤", + "chat": { + "incoming": "收到", + "outgoing": "發出", + "border": "邊框" + } + }, + "preview": { + "header_faint": "這很正常", + "header": "預覽", + "content": "內容", + "error": "例子錯誤", + "button": "按鈕", + "text": "有堆 {0} 和 {1}", + "mono": "內容", + "input": "剛剛抵達洛杉磯.", + "faint_link": "有用的手冊", + "fine_print": "閱讀我們的 {0} ,然而什麼有用的也學不到!", + "checkbox": "我已經瀏覽了條款及細則", + "link": "一個很好的小鏈接" + }, + "shadows": { + "override": "覆寫", + "_tab_label": "陰影和燈光", + "component": "組件", + "shadow_id": "陰影 #{value}", + "blur": "模糊", + "spread": "擴散", + "inset": "插圖", + "hintV3": "對於陰影,您還可以使用{0}表示法來使用其他顏色插槽。", + "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": "輸入框" + } + }, + "switcher": { + "use_snapshot": "舊版", + "load_theme": "載入主題", + "keep_color": "保留顏色", + "keep_shadows": "保留陰影", + "keep_opacity": "保留透明度", + "keep_roundness": "保留圓角", + "help": { + "migration_napshot_gone": "不知出於何種原因,主題快照缺失了,一些地方可能與您印象中的不符。", + "snapshot_source_mismatch": "版本衝突:很有可能是 FE 版本回滾後再次升級了,如果您使用舊版本的 FE 更改了主題那麼您可能需要使用舊版本,否則請使用新版本。", + "future_version_imported": "您導入的文件來自更高版本的 FE。", + "older_version_imported": "您導入的文件來自舊版本的 FE。", + "snapshot_missing": "在文件中沒有主題快照,所以網站外觀可能會與原來預想的不同。", + "fe_upgraded": "PleromaFE 的主題引擎隨著版本更新升級了。", + "fe_downgraded": "PleromaFE 的版本回滾了。", + "upgraded_from_v2": "PleromaFE 已升級,主題會和你記憶中的不太一樣。", + "v2_imported": "您導入的文件是舊版 FE 的。我們儘可能保持兼容性,但還是可能出現不一致的情況。", + "snapshot_present": "載入快照已加載,因此所有值均被覆蓋。 您可以改為載入主題實際數據。", + "migration_snapshot_ok": "為保萬無一失,載入了主題快照。您可以試著載入主題數據。" + }, + "use_source": "新版本", + "keep_as_is": "保持原狀", + "clear_opacity": "清除透明度", + "clear_all": "清除全部", + "reset": "重置", + "keep_fonts": "保留字體", + "save_load_hint": "\"保留\" 選項在選擇或載入主題時保留當前設置的選項,在導出主題時還會存儲上述選項。當所有複選框未設置時,導出主題將保存所有內容。" + }, + "fonts": { + "components": { + "interface": "界面", + "input": "輸入框", + "post": "發帖文字", + "postCode": "帖子中使用等間距文字(富文本)" + }, + "_tab_label": "字體", + "help": "給用戶界面的元素選擇字體。選擇 “自選”的你必須輸入確切的字體名稱。", + "family": "字體名稱", + "size": "大小 (像素)", + "weight": "字重 (粗體))", + "custom": "自選" + }, + "common_colors": { + "foreground_hint": "點擊”高級“ 標籤進行細緻的控制", + "main": "常用顏色", + "_tab_label": "共同", + "rgbo": "圖標,強調,徽章" + }, + "radii": { + "_tab_label": "圓角" + } + }, + "notification_setting_block_from_strangers": "屏蔽來自你沒有關注的用戶的通知", + "user_mutes": "用户", + "hide_followers_count_description": "不顯示關注者數量", + "no_rich_text_description": "不顯示富文本格式", + "notification_visibility_moves": "用戶遷移", + "notification_visibility_repeats": "轉發", + "notification_visibility_mentions": "提及", + "notification_visibility_likes": "點贊", + "interfaceLanguage": "界面語言", + "instance_default": "(默認:{value})", + "inputRadius": "輸入框", + "import_theme": "導入預置主題", + "import_followers_from_a_csv_file": "從 csv 文件中導入關注", + "import_blocks_from_a_csv_file": "從 csv 文件中導入封鎖黑名單名單", + "hide_filtered_statuses": "隱藏過濾的發文", + "lock_account_description": "你需要手動審核關注請求", + "loop_video": "循環視頻", + "loop_video_silent_only": "只循環沒有聲音的視頻(例如:Mastodon 裡的“GIF”)", + "mutes_tab": "靜音", + "play_videos_in_modal": "在彈出框內播放視頻", + "profile_fields": { + "add_field": "添加字段", + "name": "標籤", + "value": "內容", + "label": "個人資料元數據" + }, + "use_contain_fit": "生成縮略圖時不要裁剪附件", + "notification_visibility": "要顯示的通知類型", + "notification_visibility_follows": "關注", + "new_email": "新電郵", + "subject_line_mastodon": "比如mastodon: copy as is", + "reset_background_confirm": "您確定要重置個人資料背景圖嗎?", + "reset_banner_confirm": "您確定要重置橫幅圖片嗎?", + "reset_avatar_confirm": "您確定要重置頭像嗎?", + "reset_profile_banner": "重置橫幅圖片", + "reset_profile_background": "重置個人資料背景圖", + "reset_avatar": "重置頭像", + "discoverable": "允許通過搜索檢索等服務找到此賬號", + "delete_account_error": "刪除賬戶時發生錯誤,如果一直刪除不了,請聯繫實例管理員。", + "composing": "正在書寫", + "chatMessageRadius": "聊天訊息", + "mfa": { + "confirm_and_enable": "確認並啟用OTP", + "setup_otp": "設置OTP", + "otp": "OTP", + "wait_pre_setup_otp": "預設OTP", + "verify": { + "desc": "要啟用雙因素驗證,請把你的雙因素驗證 app 裡的數字輸入:" + }, + "scan": { + "secret_code": "密鑰", + "desc": "使用你的雙因素驗證 app,掃瞄這個二維碼,或者輸入這些文字密鑰:", + "title": "掃瞄" + }, + "authentication_methods": "身份驗證方法", + "recovery_codes_warning": "抄寫這些號碼,或者保存在安全的地方。這些號碼不會再次顯示。如果你無法訪問你的 2FA app,也丟失了你的恢復碼,你的賬號就再也無法登錄了。", + "waiting_a_recovery_codes": "正在接收備份碼…", + "recovery_codes": "恢復碼。", + "warning_of_generate_new_codes": "當你生成新的恢復碼時,你的舊恢復碼就失效了。", + "generate_new_recovery_codes": "生成新的恢復碼", + "title": "雙因素驗證" + }, + "new_password": "新密碼", + "name_bio": "名字及簡介", + "name": "名字", + "domain_mutes": "域名", + "delete_account_instructions": "在下面輸入密碼,以確認刪除帳戶。", + "delete_account_description": "永久刪除你的帳號和所有數據。", + "delete_account": "刪除帳戶", + "default_vis": "默認可見性範圍", + "data_import_export_tab": "數據導入/導出", + "mutes_and_blocks": "靜音與封鎖", + "current_password": "當前密碼", + "confirm_new_password": "確認新密碼", + "collapse_subject": "摺疊帶標題的內容", + "checkboxRadius": "複選框", + "instance_default_simple": "(默認)", + "interface": "界面", + "invalid_theme_imported": "您所選擇的主題文件不被 Pleroma 支持,因此主題未被修改。", + "limited_availability": "在您的瀏覽器中無法使用", + "links": "鏈接", + "changed_password": "成功修改了密碼!", + "change_password_error": "修改密碼的時候出了點問題。", + "change_password": "修改密碼", + "changed_email": "郵箱修改成功!", + "bot": "這是一個機器人賬號", + "change_email": "修改電子郵箱", + "cRed": "紅色(取消)", + "cOrange": "橙色(收藏)", + "cGreen": "綠色(轉發)", + "cBlue": "藍色(回覆,關注)", + "btnRadius": "按鈕", + "notification_visibility_emoji_reactions": "互動", + "no_blocks": "沒有封鎖", + "no_mutes": "沒有靜音", + "hide_follows_description": "不要顯示我所關注的人", + "hide_followers_description": "不要顯示關注我的人", + "hide_follows_count_description": "不顯示關注數", + "nsfw_clickthrough": "將敏感附件隱藏,點擊才能打開", + "valid_until": "有效期至", + "panelRadius": "面板", + "pause_on_unfocused": "在離開頁面時暫停時間線推送", + "notifications": "通知", + "notification_setting_filters": "過濾器", + "notification_setting_privacy": "隱私", + "notification_mutes": "要停止收到某個指定的用戶的通知,請使用靜音功能。", + "notification_blocks": "封鎖一個用戶會停掉所有他的通知,等同於取消關注。", + "enable_web_push_notifications": "啟用 web 推送通知", + "presets": "預置", + "profile_background": "個人背景圖", + "profile_banner": "橫幅圖片", + "profile_tab": "個人資料", + "radii_help": "設置界面邊緣的圓角 (單位:像素)", + "reply_visibility_all": "顯示所有回覆", + "autohide_floating_post_button": "自動隱藏新帖子的按鈕(移動設備)", + "saving_err": "保存設置時發生錯誤", + "saving_ok": "設置已保存", + "search_user_to_block": "搜索你想屏蔽的用戶", + "search_user_to_mute": "搜索你想要隱藏的用戶", + "security_tab": "安全", + "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_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": "是" + }, + "avatar_size_instruction": "推薦的頭像圖片最小的尺寸是 150x150 像素。", + "emoji_reactions_on_timeline": "在時間線上顯示繪文字互動", + "export_theme": "導出預置主題", + "filtering": "過濾", + "filtering_explanation": "所有包含以下詞彙的內容都會被隱藏,一行一個", + "follow_export": "導出關注", + "follow_export_button": "將關注導出成 csv 文件", + "follow_import": "導入關注", + "follow_import_error": "導入關注時錯誤", + "follows_imported": "關注已導入!尚需要一些時間來處理。", + "hide_attachments_in_convo": "在對話中隱藏附件", + "hide_attachments_in_tl": "在時間線上隱藏附件", + "hide_muted_posts": "不顯示被靜音的用戶的帖子", + "max_thumbnails": "最多每個帖子所能顯示的縮略圖數量", + "hide_isp": "隱藏指定實例的面板", + "preload_images": "預載圖片", + "use_one_click_nsfw": "點擊一次以打開工作場所不適宜的附件", + "hide_post_stats": "隱藏帖子的統計數據(例如:收藏的次數)", + "hide_user_stats": "隱藏用戶的統計數據(例如:關注者的數量)", + "general": "通用", + "foreground": "前景", + "blocks_tab": "封鎖", + "blocks_imported": "封鎖黑名單導入成功!需要一點時間來處理。", + "block_import_error": "導入封鎖黑名單出錯", + "block_import": "封鎖黑名單導入", + "block_export_button": "導出你的封鎖黑名單到一個 csv 文件", + "block_export": "封鎖黑名單導出", + "bio": "簡介", + "background": "背景", + "avatarRadius": "頭像", + "avatarAltRadius": "頭像(通知)", + "avatar": "頭像", + "attachments": "附件", + "attachmentRadius": "附件", + "allow_following_move": "正在關注的賬號遷移時自動重新關注", + "enter_current_password_to_confirm": "輸入你當前密碼來確認你的身份", + "security": "安全", + "app_name": "App 名稱", + "change_email_error": "修改你的電子郵箱時發生錯誤。", + "type_domains_to_mute": "搜索需要隱藏的域名", + "pad_emoji": "從繪文字選擇器插入繪文字時,在繪文字兩側插入空格", + "useStreamingApi": "實時接收發佈以及通知", + "minimal_scopes_mode": "最小發文範圍", + "scope_copy": "回覆時的複製範圍(私信是總是複製的)", + "reply_visibility_self": "只顯示發送給我的回覆", + "reply_visibility_following": "只顯示發送給我的回覆/發送給我關注的用戶的回覆", + "replies_in_timeline": "時間線中的回覆", + "revoke_token": "撤消", + "show_admin_badge": "顯示管理徽章", + "accent": "強調色", + "greentext": "前文箭頭", + "show_moderator_badge": "顯示主持人徽章", + "oauth_tokens": "OAuth代幣", + "token": "代幣", + "refresh_token": "刷新代幣", + "useStreamingApiWarning": "(不推薦使用,實驗性的,已知跳過文章)", + "fun": "有趣", + "notification_setting_hide_notification_contents": "隱藏推送通知中的發送者與內容信息", + "version": { + "title": "版本", + "backend_version": "後端版本", + "frontend_version": "前端版本" + }, + "virtual_scrolling": "優化時間線渲染", + "import_mutes_from_a_csv_file": "從CSV文件導入靜音", + "mutes_imported": "靜音導入了!處理它們將需要一段時間。", + "mute_import": "靜音導入", + "mute_import_error": "導入靜音時出錯", + "mute_export_button": "將靜音導出到csv文件", + "mute_export": "靜音導出" + }, + "chats": { + "more": "更多", + "delete_confirm": "您確實要刪除此消息嗎?", + "error_loading_chat": "加載聊天時出了點問題。", + "error_sending_message": "發送消息時出了點問題。", + "empty_chat_list_placeholder": "您還沒有任何聊天記錄。 開始新的聊天!", + "new": "新聊天", + "empty_message_error": "無法發布空消息", + "you": "你:", + "message_user": "發消息給 {nickname}", + "delete": "刪除", + "chats": "聊天" + }, + "file_type": { + "audio": "音頻", + "video": "視頻", + "image": "图片", + "file": "檔案" + }, + "display_date": { + "today": "今天" + }, + "status": { + "mute_conversation": "靜音對話", + "replies_list": "回覆:", + "reply_to": "回覆", + "pin": "在個人資料置頂", + "unpin": "取消在個人資料置頂", + "favorites": "喜歡", + "repeats": "轉發", + "delete": "刪除發文", + "pinned": "置頂", + "bookmark": "書籤", + "unbookmark": "取消書籤", + "delete_confirm": "你真的想要刪除這條發文嗎?", + "unmute_conversation": "對話取消靜音", + "status_unavailable": "發文不可取得", + "copy_link": "複製發文鏈接", + "thread_muted": "静音線程", + "show_full_subject": "顯示完整標題", + "thread_muted_and_words": ",有这些字:", + "hide_full_subject": "隱藏完整標題", + "show_content": "顯示內容", + "hide_content": "隱藏內容" + }, + "time": { + "hours": "{0} 小時", + "days_short": "{0}天", + "day_short": "{0}天", + "days": "{0} 天", + "hour": "{0} 小时", + "hour_short": "{0}h", + "hours_short": "{0}h", + "years_short": "{0} y", + "now": "剛剛", + "day": "{0} 天", + "in_future": "還有 {0}", + "in_past": "{0} 之前", + "minute": "{0} 分鐘", + "minute_short": "{0} 分", + "minutes_short": "{0} 分", + "minutes": "{0} 分鐘", + "month": "{0} 月", + "months": "{0} 月", + "month_short": "{0} 月", + "months_short": "{0} 月", + "now_short": "剛剛", + "second": "{0} 秒", + "seconds": "{0} 秒", + "second_short": "{0} 秒", + "seconds_short": "{0} 秒", + "week": "{0}周", + "weeks": "{0}周", + "week_short": "{0}周", + "weeks_short": "{0}周", + "year": "{0} 年", + "years": "{0} 年", + "year_short": "{0}年" + }, + "post_status": { + "media_description_error": "無法更新媒體,請重試", + "media_description": "媒體描述", + "scope": { + "unlisted": "不公開 - 不會發送到公共時間軸", + "public": "公共 - 發送到公共時間軸", + "private": "僅關注者 - 只有關注了你的人能看到", + "direct": "私信 - 只發送給被提及的用戶" + }, + "scope_notice": { + "unlisted": "本條內容既不在公共時間線,也不會在所有已知網絡上可見", + "private": "關注你的人才能看到本條內容", + "public": "本條帖子可以被所有人看到" + }, + "preview_empty": "空的", + "preview": "預覽", + "posting": "正在發送", + "direct_warning_to_first_only": "本條內容只有被在消息開始處提及的用戶能夠看到。", + "direct_warning_to_all": "本條內容只有被提及的用戶能夠看到。", + "account_not_locked_warning": "你的帳號沒有 {0}。任何人都可以關注你並瀏覽你的上鎖內容。", + "new_status": "發佈新發文", + "content_warning": "主題(可選)", + "content_type": { + "text/bbcode": "BBCode", + "text/markdown": "Markdown", + "text/html": "HTML", + "text/plain": "純文本" + }, + "attachments_sensitive": "標記附件為敏感內容", + "account_not_locked_warning_link": "上鎖", + "default": "剛剛抵達洛杉磯。", + "empty_status_error": "無法發佈沒有附件的空發文" + }, + "errors": { + "storage_unavailable": "Pleroma無法訪問瀏覽器存儲。您的登錄名或本地設置將不會保存,您可能會遇到意外問題。嘗試啟用Cookie。" + }, + "timeline": { + "error_fetching": "獲取更新時發生錯誤", + "conversation": "對話", + "no_retweet_hint": "這條內容僅關注者可見,或者是私信,因此不能轉發", + "collapse": "摺疊", + "load_older": "載入更早的發文", + "repeated": "已轉發", + "show_new": "顯示新內容", + "reload": "重新載入", + "up_to_date": "已是最新", + "no_more_statuses": "没有更多發文", + "no_statuses": "没有發文" + }, + "interactions": { + "load_older": "載入更早的互動", + "moves": "用戶遷移", + "follows": "新的關注者", + "favs_repeats": "轉發和收藏" + }, + "selectable_list": { + "select_all": "選擇全部" + }, + "remote_user_resolver": { + "error": "未找到。", + "searching_for": "搜索", + "remote_user_resolver": "遠程用戶解析器" + }, + "registration": { + "validations": { + "password_confirmation_match": "不能和密碼一樣", + "password_confirmation_required": "不能留空", + "password_required": "不能留空", + "email_required": "不能留空", + "fullname_required": "不能留空", + "username_required": "不能留空" + }, + "fullname": "顯示名稱", + "bio_placeholder": "例如:\n你好,我是玲音。\n我是一個住在日本郊區的動畫少女。你可能在 Wired 見過我。", + "fullname_placeholder": "例如:岩倉玲音", + "username_placeholder": "例如:玲音", + "new_captcha": "點擊圖片獲取新的驗證碼", + "captcha": "CAPTCHA", + "token": "邀請碼", + "registration": "註冊", + "password_confirm": "確認密碼", + "email": "電子郵箱", + "bio": "簡介" + }, + "user_card": { + "its_you": "就是你!!", + "media": "媒體", + "per_day": "每天", + "remote_follow": "跨站關注", + "subscribe": "訂閱", + "mute_progress": "靜音中…", + "admin_menu": { + "delete_account": "刪除賬號", + "delete_user": "刪除用戶", + "delete_user_confirmation": "你確認嗎?此操作無法撤銷。", + "moderation": "調停", + "grant_admin": "賦予管理權限", + "revoke_admin": "撤銷管理權限", + "grant_moderator": "賦予主持人權限", + "revoke_moderator": "撤銷主持人權限", + "activate_account": "啟用賬號", + "deactivate_account": "關閉賬號", + "force_nsfw": "標記所有的帖子都是工作場合不適", + "strip_media": "從帖子裡刪除媒體文件", + "force_unlisted": "強制帖子為不公開", + "sandbox": "強制帖子為只有關注者可看", + "disable_remote_subscription": "禁止從遠程實例關注用戶", + "disable_any_subscription": "完全禁止關注用戶", + "quarantine": "從聯合實例中禁止用戶帖子" + }, + "approve": "批准", + "block": "封鎖", + "blocked": "已封鎖!", + "deny": "拒絕", + "favorites": "喜歡", + "follow": "關注", + "follow_sent": "請求已發送!", + "follow_progress": "請求中…", + "follow_again": "再次發送請求?", + "follow_unfollow": "取消關注", + "followees": "正在關注", + "followers": "關注者", + "following": "正在關注!", + "follows_you": "關注了你!", + "hidden": "已隱藏", + "mention": "提及", + "message": "消息", + "mute": "靜音", + "muted": "已靜音", + "report": "報告", + "statuses": "發文", + "unsubscribe": "退訂", + "unblock": "取消封鎖", + "unblock_progress": "取消封鎖中…", + "block_progress": "封鎖中…", + "unmute": "取消靜音", + "unmute_progress": "取消靜音中…", + "hide_repeats": "隱藏轉發", + "show_repeats": "顯示轉發" + }, + "user_profile": { + "timeline_title": "用戶時間線", + "profile_does_not_exist": "抱歉,此個人資料不存在。", + "profile_loading_error": "抱歉,載入個人資料時出錯。" + }, + "user_reporting": { + "title": "報告 {0}", + "add_comment_description": "此報告會發送給你的實例管理員。你可以在下面提供更多詳細信息解釋報告的緣由:", + "forward_to": "轉發 {0}", + "submit": "提交", + "generic_error": "當處理你的請求時,發生了一個錯誤。", + "additional_comments": "其它評論", + "forward_description": "這個賬號是從另外一個服務器。同時發送一個報告到那裡?" + }, + "who_to_follow": { + "more": "更多", + "who_to_follow": "推薦關注" + }, + "tool_tip": { + "media_upload": "上傳多媒體", + "repeat": "轉發", + "favorite": "喜歡", + "add_reaction": "添加互動", + "reply": "回覆", + "user_settings": "用戶設置", + "accept_follow_request": "接受關注請求", + "reject_follow_request": "拒絕關注請求", + "bookmark": "書籤" + }, + "upload": { + "file_size_units": { + "B": "B", + "KiB": "KiB", + "TiB": "TiB", + "MiB": "MiB", + "GiB": "GiB" + }, + "error": { + "base": "上傳失敗。", + "file_too_big": "文件太大[{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]", + "default": "稍後再試" + } + }, + "search": { + "people": "人", + "hashtags": "標籤", + "person_talking": "{count} 人正在討論", + "people_talking": "{count} 人正在討論", + "no_results": "沒有搜索結果" + }, + "password_reset": { + "forgot_password": "忘記密碼了?", + "password_reset": "重置密碼", + "instruction": "輸入你的電郵地址或者用戶名,我們將發送一個鏈接到你的郵箱,用於重置密碼。", + "placeholder": "你的電郵地址或者用戶名", + "check_email": "檢查你的郵箱,會有一個鏈接用於重置密碼。", + "return_home": "回到首頁", + "too_many_requests": "你觸發了嘗試的限制,請稍後再試。", + "password_reset_disabled": "密碼重置已經被禁用。請聯繫你的實例管理員。", + "password_reset_required": "您必須重置密碼才能登陸。", + "password_reset_required_but_mailer_is_disabled": "您必須重置密碼,但是密碼重置被禁用了。請聯繫您所在實例的管理員。" + } +} diff --git a/src/modules/api.js b/src/modules/api.js index 5e213f0d..0a354c3f 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -20,7 +20,7 @@ const api = { state.fetchers[fetcherName] = fetcher }, removeFetcher (state, { fetcherName, fetcher }) { - window.clearInterval(fetcher) + state.fetchers[fetcherName].stop() delete state.fetchers[fetcherName] }, setWsToken (state, token) { @@ -72,6 +72,8 @@ const api = { showImmediately: timelineData.visibleStatuses.length === 0, timeline: 'friends' }) + } else if (message.event === 'delete') { + dispatch('deleteStatusById', message.id) } else if (message.event === 'pleroma:chat_update') { dispatch('addChatMessages', { chatId: message.chatUpdate.id, diff --git a/src/modules/chats.js b/src/modules/chats.js index c7609018..21e30933 100644 --- a/src/modules/chats.js +++ b/src/modules/chats.js @@ -3,6 +3,7 @@ import { find, omitBy, orderBy, sumBy } from 'lodash' import chatService from '../services/chat_service/chat_service.js' import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js' import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js' +import { promiseInterval } from '../services/promise_interval/promise_interval.js' const emptyChatList = () => ({ data: [], @@ -42,12 +43,10 @@ const chats = { actions: { // Chat list startFetchingChats ({ dispatch, commit }) { - const fetcher = () => { - dispatch('fetchChats', { latest: true }) - } + const fetcher = () => dispatch('fetchChats', { latest: true }) fetcher() commit('setChatListFetcher', { - fetcher: () => setInterval(() => { fetcher() }, 5000) + fetcher: () => promiseInterval(fetcher, 5000) }) }, stopFetchingChats ({ commit }) { @@ -113,14 +112,14 @@ const chats = { setChatListFetcher (state, { commit, fetcher }) { const prevFetcher = state.chatListFetcher if (prevFetcher) { - clearInterval(prevFetcher) + prevFetcher.stop() } state.chatListFetcher = fetcher && fetcher() }, setCurrentChatFetcher (state, { fetcher }) { const prevFetcher = state.fetcher if (prevFetcher) { - clearInterval(prevFetcher) + prevFetcher.stop() } state.fetcher = fetcher && fetcher() }, @@ -143,6 +142,7 @@ const chats = { const isNewMessage = (chat.lastMessage && chat.lastMessage.id) !== (updatedChat.lastMessage && updatedChat.lastMessage.id) chat.lastMessage = updatedChat.lastMessage chat.unread = updatedChat.unread + chat.updated_at = updatedChat.updated_at if (isNewMessage && chat.unread) { newChatMessageSideEffects(updatedChat) } @@ -181,30 +181,16 @@ const chats = { setChatsLoading (state, { value }) { state.chats.loading = value }, - addChatMessages (state, { commit, chatId, messages }) { - const chatMessageService = state.openedChatMessageServices[chatId] - if (chatMessageService) { - chatService.add(chatMessageService, { messages: messages.map(parseChatMessage) }) - commit('refreshLastMessage', { chatId }) - } - }, - refreshLastMessage (state, { chatId }) { + addChatMessages (state, { chatId, messages, updateMaxId }) { const chatMessageService = state.openedChatMessageServices[chatId] if (chatMessageService) { - const chat = getChatById(state, chatId) - if (chat) { - chat.lastMessage = chatMessageService.lastMessage - if (chatMessageService.lastMessage) { - chat.updated_at = chatMessageService.lastMessage.created_at - } - } + chatService.add(chatMessageService, { messages: messages.map(parseChatMessage), updateMaxId }) } }, - deleteChatMessage (state, { commit, chatId, messageId }) { + deleteChatMessage (state, { chatId, messageId }) { const chatMessageService = state.openedChatMessageServices[chatId] if (chatMessageService) { chatService.deleteMessage(chatMessageService, messageId) - commit('refreshLastMessage', { chatId }) } }, resetChatNewMessageCount (state, _value) { diff --git a/src/modules/config.js b/src/modules/config.js index 409d77a4..444b8ec7 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -65,7 +65,8 @@ export const defaultState = { useContainFit: false, greentext: undefined, // instance default hidePostStats: undefined, // instance default - hideUserStats: undefined // instance default + hideUserStats: undefined, // instance default + virtualScrolling: undefined // instance default } // caching the instance default properties diff --git a/src/modules/instance.js b/src/modules/instance.js index 3fe3bbf3..b3cbffc6 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -41,6 +41,7 @@ const defaultState = { sidebarRight: false, subjectLineBehavior: 'email', theme: 'pleroma-dark', + virtualScrolling: true, // Nasty stuff customEmoji: [], diff --git a/src/modules/interface.js b/src/modules/interface.js index 748d3025..d6db32fd 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -3,6 +3,7 @@ import { set, delete as del } from 'vue' const defaultState = { settingsModalState: 'hidden', settingsModalLoaded: false, + settingsModalTargetTab: null, settings: { currentSaveStateNotice: null, noticeClearTimeout: null, @@ -62,6 +63,9 @@ const interfaceMod = { state.settingsModalLoaded = true } }, + setSettingsModalTargetTab (state, value) { + state.settingsModalTargetTab = value + }, pushGlobalNotice (state, notice) { state.globalNotices.push(notice) }, @@ -97,6 +101,13 @@ const interfaceMod = { togglePeekSettingsModal ({ commit }) { commit('togglePeekSettingsModal') }, + clearSettingsModalTargetTab ({ commit }) { + commit('setSettingsModalTargetTab', null) + }, + openSettingsModalTab ({ commit }, value) { + commit('setSettingsModalTargetTab', value) + commit('openSettingsModal') + }, pushGlobalNotice ( { commit, dispatch }, { diff --git a/src/modules/statuses.js b/src/modules/statuses.js index e108b2a7..e673141d 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -568,6 +568,9 @@ export const mutations = { updateStatusWithPoll (state, { id, poll }) { const status = state.allStatusesObject[id] status.poll = poll + }, + setVirtualHeight (state, { statusId, height }) { + state.allStatusesObject[statusId].virtualHeight = height } } @@ -608,6 +611,10 @@ const statuses = { commit('setDeleted', { status }) apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials }) }, + deleteStatusById ({ rootState, commit }, id) { + const status = rootState.statuses.allStatusesObject[id] + commit('setDeleted', { status }) + }, markStatusesAsDeleted ({ commit }, condition) { commit('setManyDeleted', condition) }, @@ -753,6 +760,9 @@ const statuses = { store.commit('addNewStatuses', { statuses: data.statuses }) return data }) + }, + setVirtualHeight ({ commit }, { statusId, height }) { + commit('setVirtualHeight', { statusId, height }) } }, mutations diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index da519001..1a3495d4 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -3,6 +3,7 @@ import { parseStatus, parseUser, parseNotification, parseAttachment, parseChat, import { RegistrationError, StatusCodeError } from '../errors/errors' /* eslint-env browser */ +const MUTES_IMPORT_URL = '/api/pleroma/mutes_import' const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import' const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' @@ -539,8 +540,10 @@ const fetchTimeline = ({ const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&') url += `?${queryString}` + let status = '' let statusText = '' + let pagination = {} return fetch(url, { headers: authHeaders(credentials) }) .then((data) => { @@ -710,6 +713,17 @@ const setMediaDescription = ({ id, description, credentials }) => { }).then((data) => parseAttachment(data)) } +const importMutes = ({ file, credentials }) => { + const formData = new FormData() + formData.append('list', file) + return fetch(MUTES_IMPORT_URL, { + body: formData, + method: 'POST', + headers: authHeaders(credentials) + }) + .then((response) => response.ok) +} + const importBlocks = ({ file, credentials }) => { const formData = new FormData() formData.append('list', file) @@ -1280,6 +1294,7 @@ const apiService = { getCaptcha, updateProfileImages, updateProfile, + importMutes, importBlocks, importFollows, deleteAccount, diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js index b60a889b..95c69482 100644 --- a/src/services/chat_service/chat_service.js +++ b/src/services/chat_service/chat_service.js @@ -8,7 +8,7 @@ const empty = (chatId) => { lastSeenTimestamp: 0, chatId: chatId, minId: undefined, - lastMessage: undefined + maxId: undefined } } @@ -18,7 +18,7 @@ const clear = (storage) => { storage.newMessageCount = 0 storage.lastSeenTimestamp = 0 storage.minId = undefined - storage.lastMessage = undefined + storage.maxId = undefined } const deleteMessage = (storage, messageId) => { @@ -26,8 +26,9 @@ const deleteMessage = (storage, messageId) => { storage.messages = storage.messages.filter(m => m.id !== messageId) delete storage.idIndex[messageId] - if (storage.lastMessage && (storage.lastMessage.id === messageId)) { - storage.lastMessage = _.maxBy(storage.messages, 'id') + if (storage.maxId === messageId) { + const lastMessage = _.maxBy(storage.messages, 'id') + storage.maxId = lastMessage.id } if (storage.minId === messageId) { @@ -36,7 +37,7 @@ const deleteMessage = (storage, messageId) => { } } -const add = (storage, { messages: newMessages }) => { +const add = (storage, { messages: newMessages, updateMaxId = true }) => { if (!storage) { return } for (let i = 0; i < newMessages.length; i++) { const message = newMessages[i] @@ -48,8 +49,10 @@ const add = (storage, { messages: newMessages }) => { storage.minId = message.id } - if (!storage.lastMessage || message.id > storage.lastMessage.id) { - storage.lastMessage = message + if (!storage.maxId || message.id > storage.maxId) { + if (updateMaxId) { + storage.maxId = message.id + } } if (!storage.idIndex[message.id]) { diff --git a/src/services/chat_utils/chat_utils.js b/src/services/chat_utils/chat_utils.js index ab898ced..583438f7 100644 --- a/src/services/chat_utils/chat_utils.js +++ b/src/services/chat_utils/chat_utils.js @@ -3,6 +3,7 @@ import { showDesktopNotification } from '../desktop_notification_utils/desktop_n export const maybeShowChatNotification = (store, chat) => { if (!chat.lastMessage) return if (store.rootState.chats.currentChatId === chat.id && !document.hidden) return + if (store.rootState.users.currentUser.id === chat.lastMessage.account.id) return const opts = { tag: chat.lastMessage.id, diff --git a/src/services/completion/completion.js b/src/services/completion/completion.js index df83d03d..8a6eba7e 100644 --- a/src/services/completion/completion.js +++ b/src/services/completion/completion.js @@ -5,7 +5,7 @@ export const replaceWord = (str, toReplace, replacement) => { } export const wordAtPosition = (str, pos) => { - const words = splitIntoWords(str) + const words = splitByWhitespaceBoundary(str) const wordsWithPosition = addPositionToWords(words) return find(wordsWithPosition, ({ start, end }) => start <= pos && end > pos) @@ -34,36 +34,36 @@ export const addPositionToWords = (words) => { }, []) } -export const splitIntoWords = (str) => { - // Split at word boundaries - const regex = /\b/ - const triggers = /[@#:]+$/ - - let split = str.split(regex) - - // Add trailing @ and # to the following word. - const words = reduce(split, (result, word) => { - if (result.length > 0) { - let previous = result.pop() - const matches = previous.match(triggers) - if (matches) { - previous = previous.replace(triggers, '') - word = matches[0] + word - } - result.push(previous) +export const splitByWhitespaceBoundary = (str) => { + let result = [] + let currentWord = '' + for (let i = 0; i < str.length; i++) { + const currentChar = str[i] + // Starting a new word + if (!currentWord) { + currentWord = currentChar + continue } - result.push(word) - - return result - }, []) - - return words + // current character is whitespace while word isn't, or vice versa: + // add our current word to results, start over the current word. + if (!!currentChar.trim() !== !!currentWord.trim()) { + result.push(currentWord) + currentWord = currentChar + continue + } + currentWord += currentChar + } + // Add the last word we were working on + if (currentWord) { + result.push(currentWord) + } + return result } const completion = { wordAtPosition, addPositionToWords, - splitIntoWords, + splitByWhitespaceBoundary, replaceWord } diff --git a/src/services/follow_request_fetcher/follow_request_fetcher.service.js b/src/services/follow_request_fetcher/follow_request_fetcher.service.js index 93fac9bc..74af4081 100644 --- a/src/services/follow_request_fetcher/follow_request_fetcher.service.js +++ b/src/services/follow_request_fetcher/follow_request_fetcher.service.js @@ -1,4 +1,5 @@ import apiService from '../api/api.service.js' +import { promiseInterval } from '../promise_interval/promise_interval.js' const fetchAndUpdate = ({ store, credentials }) => { return apiService.fetchFollowRequests({ credentials }) @@ -10,9 +11,9 @@ const fetchAndUpdate = ({ store, credentials }) => { } const startFetching = ({ credentials, store }) => { - fetchAndUpdate({ credentials, store }) const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) - return setInterval(boundFetchAndUpdate, 10000) + boundFetchAndUpdate() + return promiseInterval(boundFetchAndUpdate, 10000) } const followRequestFetcher = { diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index 80be02ca..c908b644 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -1,4 +1,5 @@ import apiService from '../api/api.service.js' +import { promiseInterval } from '../promise_interval/promise_interval.js' const update = ({ store, notifications, older }) => { store.dispatch('setNotificationsError', { value: false }) @@ -39,6 +40,7 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => { args['since'] = Math.max(...readNotifsIds) fetchNotifications({ store, args, older }) } + return result } } @@ -53,13 +55,13 @@ const fetchNotifications = ({ store, args, older }) => { } const startFetching = ({ credentials, store }) => { - fetchAndUpdate({ credentials, store }) - const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) // Initially there's set flag to silence all desktop notifications so // that there won't spam of them when user just opened up the FE we // reset that flag after a while to show new notifications once again. setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000) - return setInterval(boundFetchAndUpdate, 10000) + const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) + boundFetchAndUpdate() + return promiseInterval(boundFetchAndUpdate, 10000) } const notificationsFetcher = { diff --git a/src/services/promise_interval/promise_interval.js b/src/services/promise_interval/promise_interval.js new file mode 100644 index 00000000..cf17970d --- /dev/null +++ b/src/services/promise_interval/promise_interval.js @@ -0,0 +1,27 @@ + +// promiseInterval - replacement for setInterval for promises, starts counting +// the interval only after a promise is done instead of immediately. +// - promiseCall is a function that returns a promise, it's called the first +// time after the first interval. +// - interval is the interval delay in ms. + +export const promiseInterval = (promiseCall, interval) => { + let stopped = false + let timeout = null + + const func = () => { + promiseCall().finally(() => { + if (stopped) return + timeout = window.setTimeout(func, interval) + }) + } + + const stopFetcher = () => { + stopped = true + window.clearTimeout(timeout) + } + + timeout = window.setTimeout(func, interval) + + return { stop: stopFetcher } +} diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index d0cddf84..72ea4890 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -1,6 +1,7 @@ import { camelCase } from 'lodash' import apiService from '../api/api.service.js' +import { promiseInterval } from '../promise_interval/promise_interval.js' const update = ({ store, statuses, timeline, showImmediately, userId, pagination }) => { const ccTimeline = camelCase(timeline) @@ -71,8 +72,9 @@ const startFetching = ({ timeline = 'friends', credentials, store, userId = fals const showImmediately = timelineData.visibleStatuses.length === 0 timelineData.userId = userId fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, tag }) - const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store, userId, tag }) - return setInterval(boundFetchAndUpdate, 10000) + const boundFetchAndUpdate = () => + fetchAndUpdate({ timeline, credentials, store, userId, tag }) + return promiseInterval(boundFetchAndUpdate, 10000) } const timelineFetcher = { fetchAndUpdate, |
