diff options
Diffstat (limited to 'src')
113 files changed, 2012 insertions, 806 deletions
@@ -15,6 +15,7 @@ import UserReportingModal from './components/user_reporting_modal/user_reporting import PostStatusModal from './components/post_status_modal/post_status_modal.vue' import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' import { windowWidth, windowHeight } from './services/window_utils/window_utils' +import { mapGetters } from 'vuex' export default { name: 'app', @@ -50,17 +51,18 @@ export default { }, computed: { currentUser () { return this.$store.state.users.currentUser }, - background () { - return this.currentUser.background_image || this.$store.state.instance.background + userBackground () { return this.currentUser.background_image }, + instanceBackground () { + return this.mergedConfig.hideInstanceWallpaper + ? null + : this.$store.state.instance.background }, + background () { return this.userBackground || this.instanceBackground }, bgStyle () { - return { - 'background-image': `url(${this.background})` - } - }, - bgAppStyle () { - return { - '--body-background-image': `url(${this.background})` + if (this.background) { + return { + '--body-background-image': `url(${this.background})` + } } }, chat () { return this.$store.state.chat.channel.state === 'joined' }, @@ -77,7 +79,8 @@ export default { return { 'order': this.$store.state.instance.sidebarRight ? 99 : 0 } - } + }, + ...mapGetters(['mergedConfig']) }, methods: { updateMobileState () { diff --git a/src/App.scss b/src/App.scss index ca7d33cd..2a1d7b1b 100644 --- a/src/App.scss +++ b/src/App.scss @@ -14,7 +14,9 @@ right: -20px; background-size: cover; background-repeat: no-repeat; - background-position: 0 50%; + background-color: var(--wallpaper); + background-image: var(--body-background-image); + background-position: 50% 50px; } i[class^='icon-'] { @@ -33,6 +35,7 @@ h4 { max-width: 980px; align-content: flex-start; } + .underlay { background-color: rgba(0,0,0,0.15); background-color: var(--underlay, rgba(0,0,0,0.15)); @@ -69,7 +72,7 @@ a { color: var(--link, $fallback--link); } -button { +.button-default { user-select: none; color: $fallback--text; color: var(--btnText, $fallback--text); @@ -85,7 +88,8 @@ button { font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); - i[class*=icon-], .svg-inline--fa { + i[class*=icon-], + .svg-inline--fa { color: $fallback--text; color: var(--btnText, $fallback--text); } @@ -107,7 +111,8 @@ button { background-color: $fallback--fg; background-color: var(--btnPressed, $fallback--fg); - svg, i { + svg, + i { color: $fallback--text; color: var(--btnPressedText, $fallback--text); } @@ -120,7 +125,8 @@ button { background-color: $fallback--fg; background-color: var(--btnDisabled, $fallback--fg); - svg, i { + svg, + i { color: $fallback--text; color: var(--btnDisabledText, $fallback--text); } @@ -134,7 +140,8 @@ button { box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset; box-shadow: var(--buttonPressedShadow); - svg, i { + svg, + i { color: $fallback--text; color: var(--btnToggledText, $fallback--text); } @@ -149,6 +156,30 @@ button { } } +.button-unstyled { + background: none; + border: none; + outline: none; + display: inline; + text-align: initial; + font-size: 100%; + font-family: inherit; + padding: 0; + line-height: unset; + cursor: pointer; + box-sizing: content-box; + color: inherit; + + &.-link { + color: $fallback--link; + color: var(--link, $fallback--link); + } + + &.-fullwidth { + width: 100%; + } +} + input, textarea, .select, .input { &.unstyled { @@ -303,6 +334,10 @@ input, textarea, .select, .input { box-sizing: border-box; } } + + &.resize-height { + resize: vertical; + } } option { @@ -442,6 +477,7 @@ main-router { color: $fallback--faint; color: var(--panelFaint, $fallback--faint); } + .faint-link { color: $fallback--faint; color: var(--faintLink, $fallback--faint); @@ -453,11 +489,8 @@ main-router { overflow-x: hidden; } - button { - flex-shrink: 0; - } - - button, .alert { + .button-default, + .alert { // height: 100%; line-height: 21px; min-height: 0; @@ -468,8 +501,11 @@ main-router { align-self: stretch; } - button { - &, i[class*=icon-] { + .button-default { + flex-shrink: 0; + + &, + i[class*=icon-] { color: $fallback--text; color: var(--btnPanelText, $fallback--text); } @@ -492,7 +528,8 @@ main-router { } } - a { + a, + .-link { color: $fallback--link; color: var(--panelLink, $fallback--link) } @@ -507,15 +544,15 @@ main-router { border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); - .faint { color: $fallback--faint; color: var(--panelFaint, $fallback--faint); } - a { + a, + .-link { color: $fallback--link; - color: var(--panelLink, $fallback--link) + color: var(--panelLink, $fallback--link); } } @@ -797,7 +834,7 @@ nav { } } -.btn.btn-default { +.btn.button-default { min-height: 28px; } diff --git a/src/App.vue b/src/App.vue index b4eb0524..1a166778 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,12 +1,11 @@ <template> <div id="app" - :style="bgAppStyle" + :style="bgStyle" > <div id="app_bg_wrapper" class="app-bg-wrapper" - :style="bgStyle" /> <MobileNav v-if="isMobileLayout" /> <DesktopNav v-else /> diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 3cbbf020..b472fcf6 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -7,6 +7,7 @@ import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' import { applyTheme } from '../services/style_setter/style_setter.js' +import FaviconService from '../services/favicon_service/favicon_service.js' let staticInitialResults = null @@ -326,6 +327,8 @@ const afterStoreSetup = async ({ store, i18n }) => { const width = windowWidth() store.dispatch('setMobileLayout', width <= 800) + FaviconService.initFaviconService() + const overrides = window.___pleromafe_dev_overrides || {} const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin store.dispatch('setInstanceOption', { name: 'server', value: server }) diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index e3ae376e..ab5d1d29 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -4,6 +4,7 @@ trigger="click" placement="bottom" :bound-to="{ x: 'container' }" + remove-padding > <div slot="content" @@ -13,14 +14,14 @@ <template v-if="relationship.following"> <button v-if="relationship.showing_reblogs" - class="btn btn-default dropdown-item" + class="btn button-default dropdown-item" @click="hideRepeats" > {{ $t('user_card.hide_repeats') }} </button> <button v-if="!relationship.showing_reblogs" - class="btn btn-default dropdown-item" + class="btn button-default dropdown-item" @click="showRepeats" > {{ $t('user_card.show_repeats') }} @@ -32,27 +33,27 @@ </template> <button v-if="relationship.blocking" - class="btn btn-default btn-block dropdown-item" + class="btn button-default btn-block dropdown-item" @click="unblockUser" > {{ $t('user_card.unblock') }} </button> <button v-else - class="btn btn-default btn-block dropdown-item" + class="btn button-default btn-block dropdown-item" @click="blockUser" > {{ $t('user_card.block') }} </button> <button - class="btn btn-default btn-block dropdown-item" + class="btn button-default btn-block dropdown-item" @click="reportUser" > {{ $t('user_card.report') }} </button> <button v-if="pleromaChatMessagesAvailable" - class="btn btn-default btn-block dropdown-item" + class="btn button-default btn-block dropdown-item" @click="openChat" > {{ $t('user_card.message') }} @@ -61,7 +62,7 @@ </div> <div slot="trigger" - class="btn btn-default ellipsis-button" + class="ellipsis-button" > <FAIcon class="icon" diff --git a/src/components/async_component_error/async_component_error.vue b/src/components/async_component_error/async_component_error.vue index b68b98f9..b1b59638 100644 --- a/src/components/async_component_error/async_component_error.vue +++ b/src/components/async_component_error/async_component_error.vue @@ -8,7 +8,7 @@ {{ $t('general.error_retry') }} </p> <button - class="btn" + class="btn button-default" @click="retry" > {{ $t('general.retry') }} diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index e23fcb1b..5f5779a0 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -8,14 +8,18 @@ import { faFile, faMusic, faImage, - faVideo + faVideo, + faPlayCircle, + faTimes } from '@fortawesome/free-solid-svg-icons' library.add( faFile, faMusic, faImage, - faVideo + faVideo, + faPlayCircle, + faTimes ) const Attachment = { diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index f1fac2c8..2c1c1682 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -42,15 +42,13 @@ icon="play-circle" /> </a> - <div + <button v-if="nsfw && hideNsfwLocal && !hidden" - class="hider" + class="button-unstyled hider" + @click.prevent="toggleHidden" > - <a - href="#" - @click.prevent="toggleHidden" - >Hide</a> - </div> + <FAIcon icon="times" /> + </button> <a v-if="type === 'image' && (!hidden || preloadImage)" @@ -234,15 +232,23 @@ .hider { position: absolute; right: 0; - white-space: nowrap; margin: 10px; - padding: 5px; - background: rgba(230,230,230,0.6); - font-weight: bold; + padding: 0; z-index: 4; - line-height: 1; border-radius: $fallback--tooltipRadius; border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + text-align: center; + width: 2em; + height: 2em; + font-size: 1.25em; + // TODO: theming? hard to theme with unknown background image color + background: rgba(230, 230, 230, 0.7); + .svg-inline--fa { + color: rgba(0, 0, 0, 0.6); + } + &:hover .svg-inline--fa { + color: rgba(0, 0, 0, 0.9); + } } video { diff --git a/src/components/block_card/block_card.vue b/src/components/block_card/block_card.vue index 5b00b738..2fe66d4c 100644 --- a/src/components/block_card/block_card.vue +++ b/src/components/block_card/block_card.vue @@ -3,7 +3,7 @@ <div class="block-card-content-container"> <button v-if="blocked" - class="btn btn-default" + class="btn button-default" :disabled="progress" @click="unblockUser" > @@ -16,7 +16,7 @@ </button> <button v-else - class="btn btn-default" + class="btn button-default" :disabled="progress" @click="blockUser" > diff --git a/src/components/chat_list/chat_list.vue b/src/components/chat_list/chat_list.vue index 17e2f795..e23eec13 100644 --- a/src/components/chat_list/chat_list.vue +++ b/src/components/chat_list/chat_list.vue @@ -10,7 +10,10 @@ <span class="title"> {{ $t("chats.chats") }} </span> - <button @click="newChat"> + <button + class="button-default" + @click="newChat" + > {{ $t("chats.new") }} </button> </div> diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss index 5af744a3..e4351d3b 100644 --- a/src/components/chat_message/chat_message.scss +++ b/src/components/chat_message/chat_message.scss @@ -31,9 +31,6 @@ color: $fallback--text; color: var(--text, $fallback--text); } - - border-radius: $fallback--chatMessageRadius; - border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius); } .popover { diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue index 3849ab6e..0777f880 100644 --- a/src/components/chat_message/chat_message.vue +++ b/src/components/chat_message/chat_message.vue @@ -53,7 +53,7 @@ <div slot="content"> <div class="dropdown-menu"> <button - class="dropdown-item dropdown-item-icon" + class="button-default dropdown-item dropdown-item-icon" @click="deleteMessage" > <FAIcon icon="times" /> {{ $t("chats.delete") }} @@ -62,7 +62,7 @@ </div> <button slot="trigger" - class="menu-icon" + class="button-default menu-icon" :title="$t('chats.more')" > <FAIcon icon="ellipsis-h" /> diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index e0b9fcc5..353859b8 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -10,12 +10,13 @@ class="panel-heading conversation-heading" > <span class="title"> {{ $t('timeline.conversation') }} </span> - <span v-if="collapsable"> - <a - href="#" - @click.prevent="toggleExpanded" - >{{ $t('timeline.collapse') }}</a> - </span> + <button + v-if="collapsable" + class="button-unstyled -link" + @click.prevent="toggleExpanded" + > + {{ $t('timeline.collapse') }} + </button> </div> <status v-for="status in conversation" @@ -57,13 +58,6 @@ } &.-expanded { - .conversation-status { - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - border-left-color: $fallback--cRed; - border-left-color: var(--cRed, $fallback--cRed); - } - .conversation-status:last-child { border-bottom: none; border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss index 028692a9..2d468588 100644 --- a/src/components/desktop_nav/desktop_nav.scss +++ b/src/components/desktop_nav/desktop_nav.scss @@ -5,6 +5,10 @@ width: 100%; position: fixed; + a { + color: var(--topBarLink, $fallback--link); + } + .inner-nav { display: grid; grid-template-rows: 50px; @@ -21,7 +25,7 @@ grid-template-areas: "logo sitename actions"; } - button { + .button-default { &, svg { color: $fallback--text; color: var(--btnTopBarText, $fallback--text); @@ -80,12 +84,13 @@ .nav-icon { margin-left: 0.2em; width: 2em; + height: 100%; text-align: center; - } - a, a svg { - color: $fallback--link; - color: var(--topBarLink, $fallback--link); + .svg-inline--fa { + color: $fallback--link; + color: var(--topBarLink, $fallback--link); + } } .sitename { diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue index 3a6e4033..762aa610 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -36,9 +36,8 @@ @toggled="onSearchBarToggled" @click.stop.native /> - <a - href="#" - class="nav-icon" + <button + class="button-unstyled nav-icon" @click.stop="openSettingsModal" > <FAIcon @@ -47,29 +46,32 @@ icon="cog" :title="$t('nav.preferences')" /> - </a> + </button> <a v-if="currentUser && currentUser.role === 'admin'" href="/pleroma/admin/#/login-pleroma" class="nav-icon" target="_blank" - ><FAIcon - fixed-width - class="fa-scale-110 fa-old-padding" - icon="tachometer-alt" - :title="$t('nav.administration')" - /></a> - <a + > + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="tachometer-alt" + :title="$t('nav.administration')" + /> + </a> + <button v-if="currentUser" - href="#" - class="nav-icon" + class="button-unstyled nav-icon" @click.prevent="logout" - ><FAIcon - fixed-width - class="fa-scale-110 fa-old-padding" - icon="sign-out-alt" - :title="$t('login.logout')" - /></a> + > + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="sign-out-alt" + :title="$t('login.logout')" + /> + </button> </div> </div> </nav> diff --git a/src/components/domain_mute_card/domain_mute_card.vue b/src/components/domain_mute_card/domain_mute_card.vue index 97aee243..3b5aec14 100644 --- a/src/components/domain_mute_card/domain_mute_card.vue +++ b/src/components/domain_mute_card/domain_mute_card.vue @@ -6,7 +6,7 @@ <ProgressButton v-if="muted" :click="unmuteDomain" - class="btn btn-default" + class="btn button-default" > {{ $t('domain_mute_card.unmute') }} <template slot="progress"> @@ -16,7 +16,7 @@ <ProgressButton v-else :click="muteDomain" - class="btn btn-default" + class="btn button-default" > {{ $t('domain_mute_card.mute') }} <template slot="progress"> diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index 87303d08..2068a598 100644 --- a/src/components/emoji_input/emoji_input.js +++ b/src/components/emoji_input/emoji_input.js @@ -114,7 +114,8 @@ const EmojiInput = { showPicker: false, temporarilyHideSuggestions: false, keepOpen: false, - disableClickOutside: false + disableClickOutside: false, + suggestions: [] } }, components: { @@ -124,21 +125,6 @@ const EmojiInput = { padEmoji () { return this.$store.getters.mergedConfig.padEmoji }, - suggestions () { - const firstchar = this.textAtCaret.charAt(0) - if (this.textAtCaret === firstchar) { return [] } - const matchedSuggestions = this.suggest(this.textAtCaret) - if (matchedSuggestions.length <= 0) { - return [] - } - return take(matchedSuggestions, 5) - .map(({ imageUrl, ...rest }, index) => ({ - ...rest, - // eslint-disable-next-line camelcase - img: imageUrl || '', - highlighted: index === this.highlighted - })) - }, showSuggestions () { return this.focused && this.suggestions && @@ -188,6 +174,23 @@ const EmojiInput = { watch: { showSuggestions: function (newValue) { this.$emit('shown', newValue) + }, + textAtCaret: async function (newWord) { + const firstchar = newWord.charAt(0) + this.suggestions = [] + if (newWord === firstchar) return + const matchedSuggestions = await this.suggest(newWord) + // Async: cancel if textAtCaret has changed during wait + if (this.textAtCaret !== newWord) return + if (matchedSuggestions.length <= 0) return + this.suggestions = take(matchedSuggestions, 5) + .map(({ imageUrl, ...rest }) => ({ + ...rest, + img: imageUrl || '' + })) + }, + suggestions (newValue) { + this.$nextTick(this.resize) } }, methods: { diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue index 224e72cf..4becdc41 100644 --- a/src/components/emoji_input/emoji_input.vue +++ b/src/components/emoji_input/emoji_input.vue @@ -6,13 +6,13 @@ > <slot /> <template v-if="enableEmojiPicker"> - <div + <button v-if="!hideEmojiButton" - class="emoji-picker-icon" + class="button-unstyled emoji-picker-icon" @click.prevent="togglePicker" > <FAIcon :icon="['far', 'smile-beam']" /> - </div> + </button> <EmojiPicker v-if="enableEmojiPicker" ref="picker" @@ -37,7 +37,7 @@ v-for="(suggestion, index) in suggestions" :key="index" class="autocomplete-item" - :class="{ highlighted: suggestion.highlighted }" + :class="{ highlighted: index === highlighted }" @click.stop.prevent="onClick($event, suggestion)" > <span class="image"> diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js index 8330345b..14a2b41e 100644 --- a/src/components/emoji_input/suggestor.js +++ b/src/components/emoji_input/suggestor.js @@ -1,4 +1,3 @@ -import { debounce } from 'lodash' /** * suggest - generates a suggestor function to be used by emoji-input * data: object providing source information for specific types of suggestions: @@ -11,19 +10,19 @@ import { debounce } from 'lodash' * doesn't support user linking you can just provide only emoji. */ -const debounceUserSearch = debounce((data, input) => { - data.updateUsersList(input) -}, 500) - -export default data => input => { - const firstChar = input[0] - if (firstChar === ':' && data.emoji) { - return suggestEmoji(data.emoji)(input) - } - if (firstChar === '@' && data.users) { - return suggestUsers(data)(input) +export default data => { + const emojiCurry = suggestEmoji(data.emoji) + const usersCurry = data.store && suggestUsers(data.store) + return input => { + const firstChar = input[0] + if (firstChar === ':' && data.emoji) { + return emojiCurry(input) + } + if (firstChar === '@' && usersCurry) { + return usersCurry(input) + } + return [] } - return [] } export const suggestEmoji = emojis => input => { @@ -57,50 +56,75 @@ export const suggestEmoji = emojis => input => { }) } -export const suggestUsers = data => input => { - const noPrefix = input.toLowerCase().substr(1) - const users = data.users - - const newUsers = users.filter( - user => - user.screen_name.toLowerCase().startsWith(noPrefix) || - user.name.toLowerCase().startsWith(noPrefix) - - /* taking only 20 results so that sorting is a bit cheaper, we display - * only 5 anyway. could be inaccurate, but we ideally we should query - * backend anyway - */ - ).slice(0, 20).sort((a, b) => { - let aScore = 0 - let bScore = 0 - - // Matches on screen name (i.e. user@instance) makes a priority - aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0 - bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0 - - // Matches on name takes second priority - aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0 - bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0 - - const diff = (bScore - aScore) * 10 - - // Then sort alphabetically - const nameAlphabetically = a.name > b.name ? 1 : -1 - const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1 - - return diff + nameAlphabetically + screenNameAlphabetically - /* eslint-disable camelcase */ - }).map(({ screen_name, name, profile_image_url_original }) => ({ - displayText: screen_name, - detailText: name, - imageUrl: profile_image_url_original, - replacement: '@' + screen_name + ' ' - })) - - // BE search users to get more comprehensive results - if (data.updateUsersList) { - debounceUserSearch(data, noPrefix) +export const suggestUsers = ({ dispatch, state }) => { + // Keep some persistent values in closure, most importantly for the + // custom debounce to work. Lodash debounce does not return a promise. + let suggestions = [] + let previousQuery = '' + let timeout = null + let cancelUserSearch = null + + const userSearch = (query) => dispatch('searchUsers', { query }) + const debounceUserSearch = (query) => { + cancelUserSearch && cancelUserSearch() + return new Promise((resolve, reject) => { + timeout = setTimeout(() => { + userSearch(query).then(resolve).catch(reject) + }, 300) + cancelUserSearch = () => { + clearTimeout(timeout) + resolve([]) + } + }) + } + + return async input => { + const noPrefix = input.toLowerCase().substr(1) + if (previousQuery === noPrefix) return suggestions + + suggestions = [] + previousQuery = noPrefix + // Fetch more and wait, don't fetch if there's the 2nd @ because + // the backend user search can't deal with it. + // Reference semantics make it so that we get the updated data after + // the await. + if (!noPrefix.includes('@')) { + await debounceUserSearch(noPrefix) + } + + const newSuggestions = state.users.users.filter( + user => + user.screen_name.toLowerCase().startsWith(noPrefix) || + user.name.toLowerCase().startsWith(noPrefix) + ).slice(0, 20).sort((a, b) => { + let aScore = 0 + let bScore = 0 + + // Matches on screen name (i.e. user@instance) makes a priority + aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0 + bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0 + + // Matches on name takes second priority + aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0 + bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0 + + const diff = (bScore - aScore) * 10 + + // Then sort alphabetically + const nameAlphabetically = a.name > b.name ? 1 : -1 + const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1 + + return diff + nameAlphabetically + screenNameAlphabetically + /* eslint-disable camelcase */ + }).map(({ screen_name, name, profile_image_url_original }) => ({ + displayText: screen_name, + detailText: name, + imageUrl: profile_image_url_original, + replacement: '@' + screen_name + ' ' + })) + /* eslint-enable camelcase */ + + suggestions = newSuggestions || [] + return suggestions } - return newUsers - /* eslint-enable camelcase */ } diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue index 2f14b5b2..51d50359 100644 --- a/src/components/emoji_reactions/emoji_reactions.vue +++ b/src/components/emoji_reactions/emoji_reactions.vue @@ -6,7 +6,7 @@ :users="accountsForEmoji[reaction.name]" > <button - class="emoji-reaction btn btn-default" + class="emoji-reaction btn button-default" :class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }" @click="emojiOnClick(reaction.name, $event)" @mouseenter="fetchEmojiReactionsByIfMissing()" diff --git a/src/components/export_import/export_import.vue b/src/components/export_import/export_import.vue index ae00487f..8ffe34f8 100644 --- a/src/components/export_import/export_import.vue +++ b/src/components/export_import/export_import.vue @@ -2,13 +2,13 @@ <div class="import-export-container"> <slot name="before" /> <button - class="btn" + class="btn button-default" @click="exportData" > {{ exportLabel }} </button> <button - class="btn" + class="btn button-default" @click="importData" > {{ importLabel }} diff --git a/src/components/exporter/exporter.vue b/src/components/exporter/exporter.vue index ecd71bf1..d6a03088 100644 --- a/src/components/exporter/exporter.vue +++ b/src/components/exporter/exporter.vue @@ -11,7 +11,7 @@ </div> <button v-else - class="btn btn-default" + class="btn button-default" @click="process" > {{ exportButtonLabel }} diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js index 1a8eef72..b5b29e8a 100644 --- a/src/components/extra_buttons/extra_buttons.js +++ b/src/components/extra_buttons/extra_buttons.js @@ -5,7 +5,8 @@ import { faBookmark, faEyeSlash, faThumbtack, - faShareAlt + faShareAlt, + faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons' import { faBookmark as faBookmarkReg @@ -17,7 +18,8 @@ library.add( faBookmarkReg, faEyeSlash, faThumbtack, - faShareAlt + faShareAlt, + faExternalLinkAlt ) const ExtraButtons = { diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index a33f6e87..dc790cad 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -1,9 +1,11 @@ <template> <Popover + class="ExtraButtons" trigger="click" placement="top" - class="extra-button-popover" + :offset="{ y: 5 }" :bound-to="{ x: 'container' }" + remove-padding > <div slot="content" @@ -12,7 +14,7 @@ <div class="dropdown-menu"> <button v-if="canMute && !status.thread_muted" - class="dropdown-item dropdown-item-icon" + class="button-default dropdown-item dropdown-item-icon" @click.prevent="muteConversation" > <FAIcon @@ -22,7 +24,7 @@ </button> <button v-if="canMute && status.thread_muted" - class="dropdown-item dropdown-item-icon" + class="button-default dropdown-item dropdown-item-icon" @click.prevent="unmuteConversation" > <FAIcon @@ -32,7 +34,7 @@ </button> <button v-if="!status.pinned && canPin" - class="dropdown-item dropdown-item-icon" + class="button-default dropdown-item dropdown-item-icon" @click.prevent="pinStatus" @click="close" > @@ -43,7 +45,7 @@ </button> <button v-if="status.pinned && canPin" - class="dropdown-item dropdown-item-icon" + class="button-default dropdown-item dropdown-item-icon" @click.prevent="unpinStatus" @click="close" > @@ -54,7 +56,7 @@ </button> <button v-if="!status.bookmarked" - class="dropdown-item dropdown-item-icon" + class="button-default dropdown-item dropdown-item-icon" @click.prevent="bookmarkStatus" @click="close" > @@ -65,7 +67,7 @@ </button> <button v-if="status.bookmarked" - class="dropdown-item dropdown-item-icon" + class="button-default dropdown-item dropdown-item-icon" @click.prevent="unbookmarkStatus" @click="close" > @@ -76,7 +78,7 @@ </button> <button v-if="canDelete" - class="dropdown-item dropdown-item-icon" + class="button-default dropdown-item dropdown-item-icon" @click.prevent="deleteStatus" @click="close" > @@ -86,7 +88,7 @@ /><span>{{ $t("status.delete") }}</span> </button> <button - class="dropdown-item dropdown-item-icon" + class="button-default dropdown-item dropdown-item-icon" @click.prevent="copyLink" @click="close" > @@ -95,11 +97,26 @@ icon="share-alt" /><span>{{ $t("status.copy_link") }}</span> </button> + <a + v-if="!status.is_local" + class="button-default dropdown-item dropdown-item-icon" + title="Source" + :href="status.external_url" + target="_blank" + > + <FAIcon + fixed-width + icon="external-link-alt" + /><span>{{ $t("status.external_source") }}</span> + </a> </div> </div> - <span slot="trigger"> + <span + slot="trigger" + class="popover-trigger" + > <FAIcon - class="ExtraButtons fa-scale-110 fa-old-padding" + class="fa-scale-110 fa-old-padding" icon="ellipsis-h" /> </span> @@ -112,13 +129,15 @@ @import '../../_variables.scss'; .ExtraButtons { - cursor: pointer; - position: static; + .popover-trigger { + position: static; + padding: 10px; + margin: -10px; - &:hover, - .extra-button-popover.open & { - color: $fallback--text; - color: var(--text, $fallback--text); + &:hover .svg-inline--fa { + color: $fallback--text; + color: var(--text, $fallback--text); + } } } </style> diff --git a/src/components/favorite_button/favorite_button.js b/src/components/favorite_button/favorite_button.js index 2a2ee84a..5cd05f73 100644 --- a/src/components/favorite_button/favorite_button.js +++ b/src/components/favorite_button/favorite_button.js @@ -31,11 +31,6 @@ const FavoriteButton = { } }, computed: { - classes () { - return { - '-favorited': this.status.favorited - } - }, ...mapGetters(['mergedConfig']) } } diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue index dfe12f86..dce25e24 100644 --- a/src/components/favorite_button/favorite_button.vue +++ b/src/components/favorite_button/favorite_button.vue @@ -1,23 +1,31 @@ <template> - <div v-if="loggedIn"> - <FAIcon - :class="classes" - class="FavoriteButton fa-scale-110 fa-old-padding -interactive" + <div class="FavoriteButton"> + <button + v-if="loggedIn" + class="button-unstyled interactive" + :class="status.favorited && '-favorited'" :title="$t('tool_tip.favorite')" - :icon="[status.favorited ? 'fas' : 'far', 'star']" - :spin="animated" @click.prevent="favorite()" - /> - <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span> - </div> - <div v-else> - <FAIcon - :class="classes" - class="FavoriteButton fa-scale-110 fa-old-padding" - :title="$t('tool_tip.favorite')" - :icon="['far', 'star']" - /> - <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span> + > + <FAIcon + class="fa-scale-110 fa-old-padding" + :icon="[status.favorited ? 'fas' : 'far', 'star']" + :spin="animated" + /> + </button> + <span v-else> + <FAIcon + class="fa-scale-110 fa-old-padding" + :title="$t('tool_tip.favorite')" + :icon="['far', 'star']" + /> + </span> + <span + v-if="!mergedConfig.hidePostStats && status.fave_num > 0" + class="action-counter" + > + {{ status.fave_num }} + </span> </div> </template> @@ -27,19 +35,28 @@ @import '../../_variables.scss'; .FavoriteButton { - &.-interactive { - cursor: pointer; - animation-duration: 0.6s; + display: flex; + + > :first-child { + padding: 10px; + margin: -10px -8px -10px -10px; + } + + .action-counter { + pointer-events: none; + user-select: none; + } + + .interactive { + .svg-inline--fa { + animation-duration: 0.6s; + } - &:hover { + &:hover .svg-inline--fa, + &.-favorited .svg-inline--fa { color: $fallback--cOrange; color: var(--cOrange, $fallback--cOrange); } } - - &.-favorited { - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); - } } </style> diff --git a/src/components/features_panel/features_panel.js b/src/components/features_panel/features_panel.js index 620a85ea..8b142d08 100644 --- a/src/components/features_panel/features_panel.js +++ b/src/components/features_panel/features_panel.js @@ -1,3 +1,5 @@ +import fileSizeFormatService from '../../services/file_size_format/file_size_format.js' + const FeaturesPanel = { computed: { chat: function () { return this.$store.state.instance.chatAvailable }, @@ -6,7 +8,8 @@ const FeaturesPanel = { whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled }, mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable }, minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode }, - textlimit: function () { return this.$store.state.instance.textlimit } + textlimit: function () { return this.$store.state.instance.textlimit }, + uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) } } } diff --git a/src/components/features_panel/features_panel.vue b/src/components/features_panel/features_panel.vue index 608b11c8..9605d09d 100644 --- a/src/components/features_panel/features_panel.vue +++ b/src/components/features_panel/features_panel.vue @@ -25,6 +25,7 @@ </li> <li>{{ $t('features_panel.scope_options') }}</li> <li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</li> + <li>{{ $t('features_panel.upload_limit') }} = {{ uploadlimit.num }} {{ $t('upload.file_size_units.' + uploadlimit.unit) }}</li> </ul> </div> </div> diff --git a/src/components/follow_button/follow_button.vue b/src/components/follow_button/follow_button.vue index bfdc137b..7f85f1d7 100644 --- a/src/components/follow_button/follow_button.vue +++ b/src/components/follow_button/follow_button.vue @@ -1,6 +1,6 @@ <template> <button - class="btn btn-default follow-button" + class="btn button-default follow-button" :class="{ toggled: isPressed }" :disabled="inProgress" :title="title" diff --git a/src/components/follow_request_card/follow_request_card.vue b/src/components/follow_request_card/follow_request_card.vue index b217b8ed..1b12ba4b 100644 --- a/src/components/follow_request_card/follow_request_card.vue +++ b/src/components/follow_request_card/follow_request_card.vue @@ -2,13 +2,13 @@ <basic-user-card :user="user"> <div class="follow-request-card-content-container"> <button - class="btn btn-default" + class="btn button-default" @click="approveUser" > {{ $t('user_card.approve') }} </button> <button - class="btn btn-default" + class="btn button-default" @click="denyUser" > {{ $t('user_card.deny') }} diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue index 8a33b9eb..049e23db 100644 --- a/src/components/global_notice_list/global_notice_list.vue +++ b/src/components/global_notice_list/global_notice_list.vue @@ -9,11 +9,15 @@ <div class="notice-message"> {{ $t(notice.messageKey, notice.messageArgs) }} </div> - <FAIcon - class="fa-scale-110 fa-old-padding" - icon="times" + <button + class="button-unstyled close-notice" @click="closeNotice(notice)" - /> + > + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" + /> + </button> </div> </div> </template> @@ -54,7 +58,7 @@ .global-error { background-color: var(--alertPopupError, $fallback--cRed); color: var(--alertPopupErrorText, $fallback--text); - i { + .svg-inline--fa { color: var(--alertPopupErrorText, $fallback--text); } } @@ -62,7 +66,7 @@ .global-warning { background-color: var(--alertPopupWarning, $fallback--cOrange); color: var(--alertPopupWarningText, $fallback--text); - i { + .svg-inline--fa { color: var(--alertPopupWarningText, $fallback--text); } } @@ -70,9 +74,16 @@ .global-info { background-color: var(--alertPopupNeutral, $fallback--fg); color: var(--alertPopupNeutralText, $fallback--text); - i { + .svg-inline--fa { color: var(--alertPopupNeutralText, $fallback--text); } } + + .close-notice { + padding-right: 0.2em; + .svg-inline--fa:hover { + opacity: 0.6; + } + } } </style> diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js index 59e4d07e..e8d5ec6d 100644 --- a/src/components/image_cropper/image_cropper.js +++ b/src/components/image_cropper/image_cropper.js @@ -2,12 +2,10 @@ import Cropper from 'cropperjs' import 'cropperjs/dist/cropper.css' import { library } from '@fortawesome/fontawesome-svg-core' import { - faTimes, faCircleNotch } from '@fortawesome/free-solid-svg-icons' library.add( - faTimes, faCircleNotch ) @@ -53,8 +51,7 @@ const ImageCropper = { cropper: undefined, dataUrl: undefined, filename: undefined, - submitting: false, - submitError: null + submitting: false } }, computed: { @@ -66,9 +63,6 @@ const ImageCropper = { }, cancelText () { return this.cancelButtonLabel || this.$t('image_cropper.cancel') - }, - submitErrorMsg () { - return this.submitError && this.submitError instanceof Error ? this.submitError.toString() : this.submitError } }, methods: { @@ -82,12 +76,8 @@ const ImageCropper = { }, submit (cropping = true) { this.submitting = true - this.avatarUploadError = null this.submitHandler(cropping && this.cropper, this.file) .then(() => this.destroy()) - .catch((err) => { - this.submitError = err - }) .finally(() => { this.submitting = false }) @@ -113,9 +103,6 @@ const ImageCropper = { reader.readAsDataURL(this.file) this.$emit('changed', this.file, reader) } - }, - clearError () { - this.submitError = null } }, mounted () { diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue index 75def612..8c48a387 100644 --- a/src/components/image_cropper/image_cropper.vue +++ b/src/components/image_cropper/image_cropper.vue @@ -11,21 +11,21 @@ </div> <div class="image-cropper-buttons-wrapper"> <button - class="btn" + class="button-default btn" type="button" :disabled="submitting" @click="submit()" v-text="saveText" /> <button - class="btn" + class="button-default btn" type="button" :disabled="submitting" @click="destroy" v-text="cancelText" /> <button - class="btn" + class="button-default btn" type="button" :disabled="submitting" @click="submit(false)" @@ -37,17 +37,6 @@ icon="circle-notch" /> </div> - <div - v-if="submitError" - class="alert error" - > - {{ submitErrorMsg }} - <FAIcon - class="fa-scale-110 fa-old-padding" - icon="times" - @click="clearError" - /> - </div> </div> <input ref="input" diff --git a/src/components/importer/importer.vue b/src/components/importer/importer.vue index c4fe5b00..210823f5 100644 --- a/src/components/importer/importer.vue +++ b/src/components/importer/importer.vue @@ -15,7 +15,7 @@ /> <button v-else - class="btn btn-default" + class="btn button-default" @click="submit" > {{ submitButtonLabel }} diff --git a/src/components/link-preview/link-preview.js b/src/components/link-preview/link-preview.js index 444aafbe..add7c563 100644 --- a/src/components/link-preview/link-preview.js +++ b/src/components/link-preview/link-preview.js @@ -1,3 +1,5 @@ +import { mapGetters } from 'vuex' + const LinkPreview = { name: 'LinkPreview', props: [ @@ -15,11 +17,20 @@ const LinkPreview = { // Currently BE shoudn't give cards if tagged NSFW, this is a bit paranoid // as it makes sure to hide the image if somehow NSFW tagged preview can // exist. - return this.card.image && !this.nsfw && this.size !== 'hide' + return this.card.image && !this.censored && this.size !== 'hide' + }, + censored () { + return this.nsfw && this.hideNsfwConfig }, useDescription () { return this.card.description && /\S/.test(this.card.description) - } + }, + hideNsfwConfig () { + return this.mergedConfig.hideNsfw + }, + ...mapGetters([ + 'mergedConfig' + ]) }, created () { if (this.useImage) { diff --git a/src/components/link-preview/link-preview.vue b/src/components/link-preview/link-preview.vue index 69171977..d3ca39b8 100644 --- a/src/components/link-preview/link-preview.vue +++ b/src/components/link-preview/link-preview.vue @@ -9,12 +9,17 @@ <div v-if="useImage && imageLoaded" class="card-image" - :class="{ 'small-image': size === 'small' }" > <img :src="card.image"> </div> <div class="card-content"> - <span class="card-host faint">{{ card.provider_name }}</span> + <span class="card-host faint"> + <span + v-if="censored" + class="nsfw-alert alert warning" + >{{ $t('status.nsfw') }}</span> + {{ card.provider_name }} + </span> <h4 class="card-title">{{ card.title }}</h4> <p v-if="useDescription" @@ -50,10 +55,6 @@ } } - .small-image { - width: 80px; - } - .card-content { max-height: 100%; margin: 0.5em; @@ -76,6 +77,10 @@ max-height: calc(1.2em * 3 - 1px); } + .nsfw-alert { + margin: 2em 0; + } + color: $fallback--text; color: var(--text, $fallback--text); border-style: solid; diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue index a1f77210..bfabb946 100644 --- a/src/components/login_form/login_form.vue +++ b/src/components/login_form/login_form.vue @@ -61,7 +61,7 @@ <button :disabled="loggingIn" type="submit" - class="btn btn-default" + class="btn button-default" > {{ $t('login.login') }} </button> diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue index 88251a26..e955aa72 100644 --- a/src/components/media_upload/media_upload.vue +++ b/src/components/media_upload/media_upload.vue @@ -1,33 +1,29 @@ <template> - <div + <label class="media-upload" :class="{ disabled: disabled }" + :title="$t('tool_tip.media_upload')" > - <label - class="label" - :title="$t('tool_tip.media_upload')" + <FAIcon + v-if="uploading" + class="progress-icon" + icon="circle-notch" + spin + /> + <FAIcon + v-if="!uploading" + class="new-icon" + icon="upload" + /> + <input + v-if="uploadReady" + :disabled="disabled" + type="file" + style="position: fixed; top: -100em" + multiple="true" + @change="change" > - <FAIcon - v-if="uploading" - class="progress-icon" - icon="circle-notch" - spin - /> - <FAIcon - v-if="!uploading" - class="new-icon" - icon="upload" - /> - <input - v-if="uploadReady" - :disabled="disabled" - type="file" - style="position: fixed; top: -100em" - multiple="true" - @change="change" - > - </label> - </div> + </label> </template> <script src="./media_upload.js" ></script> @@ -36,12 +32,6 @@ @import '../../_variables.scss'; .media-upload { - .label { - display: inline-block; - } - - .new-icon { - cursor: pointer; - } + cursor: pointer; } </style> diff --git a/src/components/mfa_form/recovery_form.vue b/src/components/mfa_form/recovery_form.vue index 78953649..0bf68e27 100644 --- a/src/components/mfa_form/recovery_form.vue +++ b/src/components/mfa_form/recovery_form.vue @@ -23,23 +23,23 @@ <div class="form-group"> <div class="login-bottom"> <div> - <a - href="#" + <button + class="button-unstyled -link" @click.prevent="requireTOTP" > {{ $t('login.enter_two_factor_code') }} - </a> + </button> <br> - <a - href="#" + <button + class="button-unstyled -link" @click.prevent="abortMFA" > {{ $t('general.cancel') }} - </a> + </button> </div> <button type="submit" - class="btn btn-default" + class="btn button-default" > {{ $t('general.verify') }} </button> diff --git a/src/components/mfa_form/totp_form.vue b/src/components/mfa_form/totp_form.vue index 9401cad5..79230148 100644 --- a/src/components/mfa_form/totp_form.vue +++ b/src/components/mfa_form/totp_form.vue @@ -25,23 +25,23 @@ <div class="form-group"> <div class="login-bottom"> <div> - <a - href="#" + <button + class="button-unstyled -link" @click.prevent="requireRecovery" > {{ $t('login.enter_recovery_code') }} - </a> + </button> <br> - <a - href="#" + <button + class="button-unstyled -link" @click.prevent="abortMFA" > {{ $t('general.cancel') }} - </a> + </button> </div> <button type="submit" - class="btn btn-default" + class="btn button-default" > {{ $t('general.verify') }} </button> diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index 5304a500..0f0ea457 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -9,9 +9,8 @@ @click="scrollToTop()" > <div class="item"> - <a - href="#" - class="mobile-nav-button" + <button + class="button-unstyled mobile-nav-button" @click.stop.prevent="toggleMobileSidebar()" > <FAIcon @@ -22,7 +21,7 @@ v-if="unreadChatCount" class="alert-dot" /> - </a> + </button> <router-link v-if="!hideSitename" class="site-name" @@ -33,10 +32,9 @@ </router-link> </div> <div class="item right"> - <a + <button v-if="currentUser" - class="mobile-nav-button" - href="#" + class="button-unstyled mobile-nav-button" @click.stop.prevent="openMobileNotifications()" > <FAIcon @@ -47,7 +45,7 @@ v-if="unseenNotificationsCount" class="alert-dot" /> - </a> + </button> </div> </nav> <div diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.vue b/src/components/mobile_post_status_button/mobile_post_status_button.vue index 50529878..767f8244 100644 --- a/src/components/mobile_post_status_button/mobile_post_status_button.vue +++ b/src/components/mobile_post_status_button/mobile_post_status_button.vue @@ -1,7 +1,7 @@ <template> <div v-if="isLoggedIn"> <button - class="new-status-button" + class="button-default new-status-button" :class="{ 'hidden': isHidden }" @click="openPostForm" > diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue index 60fa6ceb..5c7b82ec 100644 --- a/src/components/moderation_tools/moderation_tools.vue +++ b/src/components/moderation_tools/moderation_tools.vue @@ -12,13 +12,13 @@ <div class="dropdown-menu"> <span v-if="user.is_local"> <button - class="dropdown-item" + class="button-default dropdown-item" @click="toggleRight("admin")" > {{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }} </button> <button - class="dropdown-item" + class="button-default dropdown-item" @click="toggleRight("moderator")" > {{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }} @@ -29,13 +29,13 @@ /> </span> <button - class="dropdown-item" + class="button-default dropdown-item" @click="toggleActivationStatus()" > {{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }} </button> <button - class="dropdown-item" + class="button-default dropdown-item" @click="deleteUserDialog(true)" > {{ $t('user_card.admin_menu.delete_account') }} @@ -47,7 +47,7 @@ /> <span v-if="hasTagPolicy"> <button - class="dropdown-item" + class="button-default dropdown-item" @click="toggleTag(tags.FORCE_NSFW)" > {{ $t('user_card.admin_menu.force_nsfw') }} @@ -57,7 +57,7 @@ /> </button> <button - class="dropdown-item" + class="button-default dropdown-item" @click="toggleTag(tags.STRIP_MEDIA)" > {{ $t('user_card.admin_menu.strip_media') }} @@ -67,7 +67,7 @@ /> </button> <button - class="dropdown-item" + class="button-default dropdown-item" @click="toggleTag(tags.FORCE_UNLISTED)" > {{ $t('user_card.admin_menu.force_unlisted') }} @@ -77,7 +77,7 @@ /> </button> <button - class="dropdown-item" + class="button-default dropdown-item" @click="toggleTag(tags.SANDBOX)" > {{ $t('user_card.admin_menu.sandbox') }} @@ -88,7 +88,7 @@ </button> <button v-if="user.is_local" - class="dropdown-item" + class="button-default dropdown-item" @click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)" > {{ $t('user_card.admin_menu.disable_remote_subscription') }} @@ -99,7 +99,7 @@ </button> <button v-if="user.is_local" - class="dropdown-item" + class="button-default dropdown-item" @click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)" > {{ $t('user_card.admin_menu.disable_any_subscription') }} @@ -110,7 +110,7 @@ </button> <button v-if="user.is_local" - class="dropdown-item" + class="button-default dropdown-item" @click="toggleTag(tags.QUARANTINE)" > {{ $t('user_card.admin_menu.quarantine') }} @@ -124,7 +124,7 @@ </div> <button slot="trigger" - class="btn btn-default btn-block" + class="btn button-default btn-block" :class="{ toggled }" > {{ $t('user_card.admin_menu.moderation') }} @@ -141,13 +141,13 @@ <p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p> <template slot="footer"> <button - class="btn btn-default" + class="btn button-default" @click="deleteUserDialog(false)" > {{ $t('general.cancel') }} </button> <button - class="btn btn-default danger" + class="btn button-default danger" @click="deleteUser()" > {{ $t('user_card.admin_menu.delete_user') }} diff --git a/src/components/mute_card/mute_card.vue b/src/components/mute_card/mute_card.vue index 9611fb82..ca33c6c5 100644 --- a/src/components/mute_card/mute_card.vue +++ b/src/components/mute_card/mute_card.vue @@ -3,7 +3,7 @@ <div class="mute-card-content-container"> <button v-if="muted" - class="btn btn-default" + class="btn button-default" :disabled="progress" @click="unmuteUser" > @@ -16,7 +16,7 @@ </button> <button v-else - class="btn btn-default" + class="btn button-default" :disabled="progress" @click="muteUser" > diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 2bbde108..f56aa977 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -14,14 +14,15 @@ {{ notification.from_profile.screen_name }} </router-link> </small> - <a - href="#" - class="unmute" + <button + class="button-unstyled unmute" @click.prevent="toggleMute" - ><FAIcon - class="fa-scale-110 fa-old-padding" - icon="eye-slash" - /></a> + > + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="eye-slash" + /> + </button> </div> <div v-else @@ -132,14 +133,16 @@ /> </span> </div> - <a + <button v-if="needMute" - href="#" + class="button-unstyled" @click.prevent="toggleMute" - ><FAIcon - class="fa-scale-110 fa-old-padding" - icon="eye-slash" - /></a> + > + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="eye-slash" + /> + </button> </span> <div v-if="notification.type === 'follow' || notification.type === 'follow_request'" diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index 4b479e13..49258563 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -6,6 +6,7 @@ import { filteredNotificationsFromStore, unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils.js' +import FaviconService from '../../services/favicon_service/favicon_service.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' @@ -75,8 +76,10 @@ const Notifications = { watch: { unseenCountTitle (count) { if (count > 0) { + FaviconService.drawFaviconBadge() this.$store.dispatch('setPageTitle', `(${count})`) } else { + FaviconService.clearFaviconBadge() this.$store.dispatch('setPageTitle', '') } } diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue index bd875cca..725d1ad4 100644 --- a/src/components/notifications/notifications.vue +++ b/src/components/notifications/notifications.vue @@ -15,16 +15,9 @@ class="badge badge-notification unseen-count" >{{ unseenCount }}</span> </div> - <div - v-if="error" - class="loadmore-error alert error" - @click.prevent - > - {{ $t('timeline.error_fetching') }} - </div> <button v-if="unseenCount" - class="read-button" + class="button-default read-button" @click.prevent="markAsSeen" > {{ $t('notifications.read') }} @@ -48,15 +41,15 @@ > {{ $t('notifications.no_more_notifications') }} </div> - <a + <button v-else-if="!loading" - href="#" + class="button-unstyled -link -fullwidth" @click.prevent="fetchOlderNotifications()" > <div class="new-status-notification text-center panel-footer"> {{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }} </div> - </a> + </button> <div v-else class="new-status-notification text-center panel-footer" diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue index 0deb9ccf..a931cb5a 100644 --- a/src/components/password_reset/password_reset.vue +++ b/src/components/password_reset/password_reset.vue @@ -51,7 +51,7 @@ <button :disabled="isPending" type="submit" - class="btn btn-default btn-block" + class="btn button-default btn-block" > {{ $t('general.submit') }} </button> diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue index 5f54b416..42819c19 100644 --- a/src/components/poll/poll.vue +++ b/src/components/poll/poll.vue @@ -42,14 +42,15 @@ :value="index" > <label class="option-vote"> - <div>{{ option.title }}</div> + <!-- eslint-disable-next-line vue/no-v-html --> + <div v-html="option.title_html" /> </label> </div> </div> <div class="footer faint"> <button v-if="!showResults" - class="btn btn-default poll-vote-button" + class="btn button-default poll-vote-button" type="button" :disabled="isDisabled" @click="vote" diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js index 695f73b9..5e417fa0 100644 --- a/src/components/popover/popover.js +++ b/src/components/popover/popover.js @@ -21,7 +21,10 @@ const Popover = { // Replaces the classes you may want for the popover container. // Use 'popover-default' in addition to get the default popover // styles with your custom class. - popoverClass: String + popoverClass: String, + // If true, subtract padding when calculating position for the popover, + // use it when popover offset looks to be different on top vs bottom. + removePadding: Boolean }, data () { return { @@ -96,9 +99,15 @@ const Popover = { if (origin.y + content.offsetHeight > yBounds.max) usingTop = true if (origin.y - content.offsetHeight < yBounds.min) usingTop = false + let vPadding = 0 + if (this.removePadding && usingTop) { + const anchorStyle = getComputedStyle(anchorEl) + vPadding = parseFloat(anchorStyle.paddingTop) + parseFloat(anchorStyle.paddingBottom) + } + const yOffset = (this.offset && this.offset.y) || 0 const translateY = usingTop - ? -anchorEl.offsetHeight - yOffset - content.offsetHeight + ? -anchorEl.offsetHeight + vPadding - yOffset - content.offsetHeight : yOffset const xOffset = (this.offset && this.offset.x) || 0 diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue index 9b8680e5..2252c68f 100644 --- a/src/components/popover/popover.vue +++ b/src/components/popover/popover.vue @@ -3,12 +3,13 @@ @mouseenter="onMouseenter" @mouseleave="onMouseleave" > - <div + <button ref="trigger" + class="button-unstyled -fullwidth popover-trigger-button" @click="onClick" > <slot name="trigger" /> - </div> + </button> <div v-if="!hidden" ref="content" @@ -30,6 +31,10 @@ <style lang="scss"> @import '../../_variables.scss'; +.popover-trigger-button { + display: block; +} + .popover { z-index: 8; position: absolute; @@ -90,6 +95,7 @@ box-shadow: none; width: 100%; height: 100%; + box-sizing: border-box; --btnText: var(--popoverText, $fallback--text); diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index de583269..4148381c 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -159,8 +159,7 @@ const PostStatusForm = { ...this.$store.state.instance.emoji, ...this.$store.state.instance.customEmoji ], - users: this.$store.state.users.users, - updateUsersList: (query) => this.$store.dispatch('searchUsers', { query }) + store: this.$store }) }, emojiSuggestor () { @@ -531,7 +530,7 @@ const PostStatusForm = { !(isFormBiggerThanScroller && this.$refs.textarea.selectionStart !== this.$refs.textarea.value.length) const totalDelta = shouldScrollToBottom ? bottomChangeDelta : 0 - const targetScroll = currentScroll + totalDelta + const targetScroll = Math.round(currentScroll + totalDelta) if (scrollerRef === window) { scrollerRef.scroll(0, targetScroll) diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 42d3152b..ed830f57 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -24,12 +24,12 @@ tag="p" class="visibility-notice" > - <a - href="#" + <button + class="button-unstyled -link" @click="openProfileTab" > {{ $t('post_status.account_not_locked_warning_link') }} - </a> + </button> </i18n> <p v-if="!hideScopeNotice && newStatus.visibility === 'public'" @@ -243,38 +243,34 @@ @upload-failed="uploadFailed" @all-uploaded="finishedUploadingFiles" /> - <div - class="emoji-icon" + <button + class="emoji-icon button-unstyled" + :title="$t('emoji.add_emoji')" + @click="showEmojiPicker" > - <div - :title="$t('emoji.add_emoji')" - class="btn btn-default" - @click="showEmojiPicker" - > - <FAIcon icon="smile-beam" /> - </div> - </div> - <div + <FAIcon icon="smile-beam" /> + </button> + <button v-if="pollsAvailable" - class="poll-icon" + class="poll-icon button-unstyled" :class="{ selected: pollFormVisible }" :title="$t('polls.add_poll')" @click="togglePollForm" > <FAIcon icon="poll-h" /> - </div> + </button> </div> <button v-if="posting" disabled - class="btn btn-default" + class="btn button-default" > {{ $t('post_status.posting') }} </button> <button v-else-if="isOverLengthLimit" disabled - class="btn btn-default" + class="btn button-default" > {{ $t('general.submit') }} </button> @@ -282,7 +278,7 @@ <button v-else :disabled="uploadingFiles || disableSubmit" - class="btn btn-default" + class="btn button-default" @touchstart.stop.prevent="postStatus($event, newStatus)" @click.stop.prevent="postStatus($event, newStatus)" > diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js index de0df70c..5e7b7580 100644 --- a/src/components/react_button/react_button.js +++ b/src/components/react_button/react_button.js @@ -27,13 +27,21 @@ const ReactButton = { }, computed: { commonEmojis () { - return ['👍', '😠', '👀', '😂', '🔥'] + return [ + { displayText: 'thumbsup', replacement: '👍' }, + { displayText: 'angry', replacement: '😠' }, + { displayText: 'eyes', replacement: '👀' }, + { displayText: 'joy', replacement: '😂' }, + { displayText: 'fire', replacement: '🔥' } + ] }, emojis () { if (this.filterWord !== '') { const filterWordLowercase = this.filterWord.toLowerCase() let orderedEmojiList = [] for (const emoji of this.$store.state.instance.emoji) { + if (emoji.replacement === this.filterWord) return [emoji] + const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase) if (indexOfFilterWord > -1) { if (!Array.isArray(orderedEmojiList[indexOfFilterWord])) { diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue index e508a3e9..ac940b98 100644 --- a/src/components/react_button/react_button.vue +++ b/src/components/react_button/react_button.vue @@ -3,8 +3,8 @@ trigger="click" placement="top" :offset="{ y: 5 }" - class="react-button-popover" :bound-to="{ x: 'container' }" + remove-padding > <div slot="content" @@ -20,17 +20,19 @@ <div class="reaction-picker"> <span v-for="emoji in commonEmojis" - :key="emoji" + :key="emoji.replacement" class="emoji-button" - @click="addReaction($event, emoji, close)" + :title="emoji.displayText" + @click="addReaction($event, emoji.replacement, close)" > - {{ emoji }} + {{ emoji.replacement }} </span> <div class="reaction-picker-divider" /> <span v-for="(emoji, key) in emojis" :key="key" class="emoji-button" + :title="emoji.displayText" @click="addReaction($event, emoji.replacement, close)" > {{ emoji.replacement }} @@ -38,11 +40,14 @@ <div class="reaction-bottom-fader" /> </div> </div> - <span slot="trigger"> + <span + slot="trigger" + class="ReactButton" + :title="$t('tool_tip.add_reaction')" + > <FAIcon - class="fa-scale-110 fa-old-padding add-reaction-button" + class="fa-scale-110 fa-old-padding" :icon="['far', 'smile-beam']" - :title="$t('tool_tip.add_reaction')" /> </span> </Popover> @@ -102,10 +107,11 @@ } } -.add-reaction-button { - cursor: pointer; +.ReactButton { + padding: 10px; + margin: -10px; - &:hover { + &:hover .svg-inline--fa { color: $fallback--text; color: var(--text, $fallback--text); } diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue index a83ca1e5..100df0d6 100644 --- a/src/components/registration/registration.vue +++ b/src/components/registration/registration.vue @@ -211,7 +211,7 @@ <button :disabled="isPending" type="submit" - class="btn btn-default" + class="btn button-default" > {{ $t('general.submit') }} </button> diff --git a/src/components/remote_follow/remote_follow.vue b/src/components/remote_follow/remote_follow.vue index cb1c2a1b..be827400 100644 --- a/src/components/remote_follow/remote_follow.vue +++ b/src/components/remote_follow/remote_follow.vue @@ -16,7 +16,7 @@ > <button click="submit" - class="remote-button" + class="button-default remote-button" > {{ $t('user_card.remote_follow') }} </button> diff --git a/src/components/reply_button/reply_button.vue b/src/components/reply_button/reply_button.vue index a0ac8941..c17041da 100644 --- a/src/components/reply_button/reply_button.vue +++ b/src/components/reply_button/reply_button.vue @@ -1,20 +1,28 @@ <template> - <div> - <FAIcon + <div class="ReplyButton"> + <button v-if="loggedIn" - class="ReplyButton fa-scale-110 fa-old-padding -interactive" - icon="reply" - :title="$t('tool_tip.reply')" + class="button-unstyled interactive" :class="{'-active': replying}" - @click.prevent="$emit('toggle')" - /> - <FAIcon - v-else - icon="reply" - class="ReplyButton fa-scale-110 fa-old-padding" :title="$t('tool_tip.reply')" - /> - <span v-if="status.replies_count > 0"> + @click.prevent="$emit('toggle')" + > + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="reply" + /> + </button> + <span v-else> + <FAIcon + icon="reply" + class="fa-scale-110 fa-old-padding" + :title="$t('tool_tip.reply')" + /> + </span> + <span + v-if="status.replies_count > 0" + class="action-counter" + > {{ status.replies_count }} </span> </div> @@ -26,14 +34,25 @@ @import '../../_variables.scss'; .ReplyButton { - &.-interactive { - cursor: pointer; + display: flex; - &:hover, - &.-active { + > :first-child { + padding: 10px; + margin: -10px -8px -10px -10px; + } + + .action-counter { + pointer-events: none; + user-select: none; + } + + .interactive { + &:hover .svg-inline--fa, + &.-active .svg-inline--fa { color: $fallback--cBlue; color: var(--cBlue, $fallback--cBlue); } } + } </style> diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js index 5ee4179a..2103fd0b 100644 --- a/src/components/retweet_button/retweet_button.js +++ b/src/components/retweet_button/retweet_button.js @@ -24,11 +24,6 @@ const RetweetButton = { } }, computed: { - classes () { - return { - '-repeated': this.status.repeated - } - }, mergedConfig () { return this.$store.getters.mergedConfig } diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue index b234f3d9..859ce499 100644 --- a/src/components/retweet_button/retweet_button.vue +++ b/src/components/retweet_button/retweet_button.vue @@ -1,33 +1,38 @@ <template> - <div v-if="loggedIn"> - <template v-if="visibility !== 'private' && visibility !== 'direct'"> + <div class="RetweetButton"> + <button + v-if="visibility !== 'private' && visibility !== 'direct' && loggedIn" + class="button-unstyled interactive" + :class="status.repeated && '-repeated'" + :title="$t('tool_tip.repeat')" + @click.prevent="retweet()" + > <FAIcon - :class="classes" - class="RetweetButton fa-scale-110 fa-old-padding -interactive" + class="fa-scale-110 fa-old-padding" icon="retweet" :spin="animated" - :title="$t('tool_tip.repeat')" - @click.prevent="retweet()" /> - <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span> - </template> - <template v-else> + </button> + <span v-else-if="loggedIn"> <FAIcon - :class="classes" - class="RetweetButton fa-scale-110 fa-old-padding" + class="fa-scale-110 fa-old-padding" icon="lock" :title="$t('timeline.no_retweet_hint')" /> - </template> - </div> - <div v-else-if="!loggedIn"> - <FAIcon - :class="classes" - class="fa-scale-110 fa-old-padding" - icon="retweet" - :title="$t('tool_tip.repeat')" - /> - <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span> + </span> + <span v-else> + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="retweet" + :title="$t('tool_tip.repeat')" + /> + </span> + <span + v-if="!mergedConfig.hidePostStats && status.repeat_num > 0" + class="no-event" + > + {{ status.repeat_num }} + </span> </div> </template> @@ -37,19 +42,28 @@ @import '../../_variables.scss'; .RetweetButton { - &.-interactive { - cursor: pointer; - animation-duration: 0.6s; + display: flex; - &:hover { + > :first-child { + padding: 10px; + margin: -10px -8px -10px -10px; + } + + .action-counter { + pointer-events: none; + user-select: none; + } + + .interactive { + .svg-inline--fa { + animation-duration: 0.6s; + } + + &:hover .svg-inline--fa, + &.-repeated .svg-inline--fa { color: $fallback--cGreen; color: var(--cGreen, $fallback--cGreen); } } - - &.-repeated { - color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); - } } </style> diff --git a/src/components/scope_selector/scope_selector.vue b/src/components/scope_selector/scope_selector.vue index a22a4fda..66ac612e 100644 --- a/src/components/scope_selector/scope_selector.vue +++ b/src/components/scope_selector/scope_selector.vue @@ -3,9 +3,9 @@ v-if="!showNothing" class="ScopeSelector" > - <span + <button v-if="showDirect" - class="scope" + class="button-unstyled scope" :class="css.direct" :title="$t('post_status.scope.direct')" @click="changeVis('direct')" @@ -14,10 +14,10 @@ icon="envelope" class="fa-scale-110 fa-old-padding" /> - </span> - <span + </button> + <button v-if="showPrivate" - class="scope" + class="button-unstyled scope" :class="css.private" :title="$t('post_status.scope.private')" @click="changeVis('private')" @@ -26,10 +26,10 @@ icon="lock" class="fa-scale-110 fa-old-padding" /> - </span> - <span + </button> + <button v-if="showUnlisted" - class="scope" + class="button-unstyled scope" :class="css.unlisted" :title="$t('post_status.scope.unlisted')" @click="changeVis('unlisted')" @@ -38,10 +38,10 @@ icon="lock-open" class="fa-scale-110 fa-old-padding" /> - </span> - <span + </button> + <button v-if="showPublic" - class="scope" + class="button-unstyled scope" :class="css.public" :title="$t('post_status.scope.public')" @click="changeVis('public')" @@ -50,7 +50,7 @@ icon="globe" class="fa-scale-110 fa-old-padding" /> - </span> + </button> </div> </template> diff --git a/src/components/search/search.vue b/src/components/search/search.vue index 665390f9..a6503c9f 100644 --- a/src/components/search/search.vue +++ b/src/components/search/search.vue @@ -14,7 +14,7 @@ @keyup.enter="newQuery(searchTerm)" > <button - class="btn search-button" + class="btn button-default search-button" @click="newQuery(searchTerm)" > <FAIcon icon="search" /> diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue index 89a601c8..6cf9179e 100644 --- a/src/components/search_bar/search_bar.vue +++ b/src/components/search_bar/search_bar.vue @@ -3,17 +3,18 @@ class="SearchBar" :class="{ '-expanded': !hidden }" > - <a + <button v-if="hidden" - href="#" - class="nav-icon" + class="button-unstyled nav-icon" :title="$t('nav.search')" - ><FAIcon - fixed-width - class="fa-scale-110 fa-old-padding" - icon="search" @click.prevent.stop="toggleHidden" - /></a> + > + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="search" + /> + </button> <template v-else> <input id="search-bar-input" @@ -25,7 +26,7 @@ @keyup.enter="find(searchTerm)" > <button - class="btn search-button" + class="button-default search-button" @click="find(searchTerm)" > <FAIcon @@ -33,14 +34,16 @@ icon="search" /> </button> - <span> + <button + class="button-unstyled cancel-search" + @click.prevent.stop="toggleHidden" + > <FAIcon fixed-width icon="times" class="cancel-icon fa-scale-110 fa-old-padding" - @click.prevent.stop="toggleHidden" /> - </span> + </button> </template> </div> </template> @@ -69,8 +72,11 @@ flex: 1 0 auto; } + .cancel-search { + height: 50px; + } + .cancel-icon { - cursor: pointer; color: $fallback--text; color: var(--btnTopBarText, $fallback--text); } diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue index 6bc64ed0..552ca41f 100644 --- a/src/components/settings_modal/settings_modal.vue +++ b/src/components/settings_modal/settings_modal.vue @@ -30,13 +30,13 @@ </template> </transition> <button - class="btn" + class="btn button-default" @click="peekModal" > {{ $t('general.peek') }} </button> <button - class="btn" + class="btn button-default" @click="closeModal" > {{ $t('general.close') }} diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue index 813dc4cd..8f850c8b 100644 --- a/src/components/settings_modal/tabs/filtering_tab.vue +++ b/src/components/settings_modal/tabs/filtering_tab.vue @@ -75,6 +75,7 @@ <p>{{ $t('settings.filtering_explanation') }}</p> <textarea id="muteWords" + class="resize-height" v-model="muteWordsString" /> </div> diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index df592a10..029ee7a1 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -34,6 +34,10 @@ const GeneralTab = { return this.$store.state.instance.postFormats || [] }, instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, + instanceWallpaperUsed () { + return this.$store.state.instance.background && + !this.$store.state.users.currentUser.background_image + }, ...SharedComputedObject() } } diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index c1d0d0ec..a9081793 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -11,6 +11,11 @@ {{ $t('settings.hide_isp') }} </Checkbox> </li> + <li v-if="instanceWallpaperUsed"> + <Checkbox v-model="hideInstanceWallpaper"> + {{ $t('settings.hide_wallpaper') }} + </Checkbox> + </li> </ul> </div> <div class="setting-item"> diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue index 5a1cf2c0..63d36bf9 100644 --- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue +++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue @@ -27,7 +27,7 @@ <div class="bulk-actions"> <ProgressButton v-if="selected.length > 0" - class="btn btn-default bulk-action-button" + class="btn button-default bulk-action-button" :click="() => blockUsers(selected)" > {{ $t('user_card.block') }} @@ -37,7 +37,7 @@ </ProgressButton> <ProgressButton v-if="selected.length > 0" - class="btn btn-default" + class="btn button-default" :click="() => unblockUsers(selected)" > {{ $t('user_card.unblock') }} @@ -85,7 +85,7 @@ <div class="bulk-actions"> <ProgressButton v-if="selected.length > 0" - class="btn btn-default" + class="btn button-default" :click="() => muteUsers(selected)" > {{ $t('user_card.mute') }} @@ -95,7 +95,7 @@ </ProgressButton> <ProgressButton v-if="selected.length > 0" - class="btn btn-default" + class="btn button-default" :click="() => unmuteUsers(selected)" > {{ $t('user_card.unmute') }} @@ -141,7 +141,7 @@ <div class="bulk-actions"> <ProgressButton v-if="selected.length > 0" - class="btn btn-default" + class="btn button-default" :click="() => unmuteDomains(selected)" > {{ $t('domain_mute_card.unmute') }} diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue index 86eed3f5..8f8fe48e 100644 --- a/src/components/settings_modal/tabs/notifications_tab.vue +++ b/src/components/settings_modal/tabs/notifications_tab.vue @@ -21,7 +21,7 @@ <p>{{ $t('settings.notification_mutes') }}</p> <p>{{ $t('settings.notification_blocks') }}</p> <button - class="btn btn-default" + class="btn button-default" @click="updateNotificationSettings" > {{ $t('general.submit') }} diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index a3e4feaf..9709424c 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -45,9 +45,7 @@ const ProfileTab = { banner: null, bannerPreview: null, background: null, - backgroundPreview: null, - bannerUploadError: null, - backgroundUploadError: null + backgroundPreview: null } }, components: { @@ -68,8 +66,7 @@ const ProfileTab = { ...this.$store.state.instance.emoji, ...this.$store.state.instance.customEmoji ], - users: this.$store.state.users.users, - updateUsersList: (query) => this.$store.dispatch('searchUsers', { query }) + store: this.$store }) }, emojiSuggestor () { @@ -79,10 +76,7 @@ const ProfileTab = { ] }) }, userSuggestor () { - return suggestor({ - users: this.$store.state.users.users, - updateUsersList: (query) => this.$store.dispatch('searchUsers', { query }) - }) + return suggestor({ store: this.$store }) }, fieldsLimits () { return this.$store.state.instance.fieldsLimits @@ -166,18 +160,18 @@ const ProfileTab = { if (file.size > this.$store.state.instance[slot + 'limit']) { const filesize = fileSizeFormatService.fileSizeFormat(file.size) const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit']) - this[slot + 'UploadError'] = [ - this.$t('upload.error.base'), - this.$t( - 'upload.error.file_too_big', - { + this.$store.dispatch('pushGlobalNotice', { + messageKey: 'upload.error.message', + messageArgs: [ + this.$t('upload.error.file_too_big', { filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit - } - ) - ].join(' ') + }) + ], + level: 'error' + }) return } // eslint-disable-next-line no-undef @@ -217,8 +211,9 @@ const ProfileTab = { that.$store.commit('setCurrentUser', user) resolve() }) - .catch((err) => { - reject(new Error(that.$t('upload.error.base') + ' ' + err.message)) + .catch((error) => { + that.displayUploadError(error) + reject(error) }) } @@ -239,24 +234,27 @@ const ProfileTab = { this.$store.commit('setCurrentUser', user) this.bannerPreview = null }) - .catch((err) => { - this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message - }) - .then(() => { this.bannerUploading = false }) + .catch(this.displayUploadError) + .finally(() => { this.bannerUploading = false }) }, submitBackground (background) { if (!this.backgroundPreview && background !== '') { return } this.backgroundUploading = true - this.$store.state.api.backendInteractor.updateProfileImages({ background }).then((data) => { - if (!data.error) { + this.$store.state.api.backendInteractor.updateProfileImages({ background }) + .then((data) => { this.$store.commit('addNewUsers', [data]) this.$store.commit('setCurrentUser', data) this.backgroundPreview = null - } else { - this.backgroundUploadError = this.$t('upload.error.base') + data.error - } - this.backgroundUploading = false + }) + .catch(this.displayUploadError) + .finally(() => { this.backgroundUploading = false }) + }, + displayUploadError (error) { + this.$store.dispatch('pushGlobalNotice', { + messageKey: 'upload.error.message', + messageArgs: [error.message], + level: 'error' }) } } diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue index d62bc392..b7ef21d7 100644 --- a/src/components/settings_modal/tabs/profile_tab.vue +++ b/src/components/settings_modal/tabs/profile_tab.vue @@ -11,7 +11,7 @@ <input id="username" v-model="newName" - classname="name-changer" + class="name-changer" > </EmojiInput> <p>{{ $t('settings.bio') }}</p> @@ -22,7 +22,7 @@ > <textarea v-model="newBio" - classname="bio" + class="bio resize-height" /> </EmojiInput> <p> @@ -150,7 +150,7 @@ </p> <button :disabled="newName && newName.length === 0" - class="btn btn-default" + class="btn button-default" @click="updateProfile" > {{ $t('general.submit') }} @@ -179,7 +179,7 @@ <button v-show="pickAvatarBtnVisible" id="pick-avatar" - class="btn" + class="button-default btn" type="button" > {{ $t('settings.upload_a_photo') }} @@ -224,22 +224,11 @@ /> <button v-else-if="bannerPreview" - class="btn btn-default" + class="btn button-default" @click="submitBanner(banner)" > {{ $t('general.submit') }} </button> - <div - v-if="bannerUploadError" - class="alert error" - > - Error: {{ bannerUploadError }} - <FAIcon - class="fa-scale-110 fa-old-padding" - icon="times" - @click="clearUploadError('banner')" - /> - </div> </div> <div class="setting-item"> <h2>{{ $t('settings.profile_background') }}</h2> @@ -274,23 +263,11 @@ /> <button v-else-if="backgroundPreview" - class="btn btn-default" + class="btn button-default" @click="submitBackground(background)" > {{ $t('general.submit') }} </button> - <div - v-if="backgroundUploadError" - class="alert error" - > - Error: {{ backgroundUploadError }} - <FAIcon - size="lg" - class="fa-scale-110 fa-old-padding" - icon="times" - @click="clearUploadError('background')" - /> - </div> </div> </div> </template> diff --git a/src/components/settings_modal/tabs/security_tab/confirm.vue b/src/components/settings_modal/tabs/security_tab/confirm.vue index 69b3811b..38c2a610 100644 --- a/src/components/settings_modal/tabs/security_tab/confirm.vue +++ b/src/components/settings_modal/tabs/security_tab/confirm.vue @@ -2,14 +2,14 @@ <div> <slot /> <button - class="btn btn-default" + class="btn button-default" :disabled="disabled" @click="confirm" > {{ $t('general.confirm') }} </button> <button - class="btn btn-default" + class="btn button-default" :disabled="disabled" @click="cancel" > diff --git a/src/components/settings_modal/tabs/security_tab/mfa.vue b/src/components/settings_modal/tabs/security_tab/mfa.vue index 7aca3c8d..455d17b6 100644 --- a/src/components/settings_modal/tabs/security_tab/mfa.vue +++ b/src/components/settings_modal/tabs/security_tab/mfa.vue @@ -29,7 +29,7 @@ /> <button v-if="!confirmNewBackupCodes" - class="btn btn-default" + class="btn button-default" @click="getBackupCodes" > {{ $t('settings.mfa.generate_new_recovery_codes') }} @@ -61,7 +61,7 @@ <button v-if="canSetupOTP" - class="btn btn-default" + class="btn button-default" @click="cancelSetup" > {{ $t('general.cancel') }} @@ -69,7 +69,7 @@ <button v-if="canSetupOTP" - class="btn btn-default" + class="btn button-default" @click="setupOTP" > {{ $t('settings.mfa.setup_otp') }} @@ -108,13 +108,13 @@ > <div class="confirm-otp-actions"> <button - class="btn btn-default" + class="btn button-default" @click="doConfirmOTP" > {{ $t('settings.mfa.confirm_and_enable') }} </button> <button - class="btn btn-default" + class="btn button-default" @click="cancelSetup" > {{ $t('general.cancel') }} diff --git a/src/components/settings_modal/tabs/security_tab/mfa_totp.vue b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue index c6f2cc7b..8e767bd0 100644 --- a/src/components/settings_modal/tabs/security_tab/mfa_totp.vue +++ b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue @@ -4,7 +4,7 @@ <strong>{{ $t('settings.mfa.otp') }}</strong> <button v-if="!isActivated" - class="btn btn-default" + class="btn button-default" @click="doActivate" > {{ $t('general.enable') }} @@ -12,7 +12,7 @@ <button v-if="isActivated" - class="btn btn-default" + class="btn button-default" :disabled="deactivate" @click="doDeactivate" > diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue index 3d32d73d..56bea1f4 100644 --- a/src/components/settings_modal/tabs/security_tab/security_tab.vue +++ b/src/components/settings_modal/tabs/security_tab/security_tab.vue @@ -19,7 +19,7 @@ > </div> <button - class="btn btn-default" + class="btn button-default" @click="changeEmail" > {{ $t('general.submit') }} @@ -57,7 +57,7 @@ > </div> <button - class="btn btn-default" + class="btn button-default" @click="changePassword" > {{ $t('general.submit') }} @@ -92,7 +92,7 @@ <td>{{ oauthToken.validUntil }}</td> <td class="actions"> <button - class="btn btn-default" + class="btn button-default" @click="revokeToken(oauthToken.id)" > {{ $t('settings.revoke_token') }} @@ -116,7 +116,7 @@ type="password" > <button - class="btn btn-default" + class="btn button-default" @click="deleteAccount" > {{ $t('settings.delete_account') }} @@ -130,7 +130,7 @@ </p> <button v-if="!deletingAccount" - class="btn btn-default" + class="btn button-default" @click="confirmDelete" > {{ $t('general.submit') }} diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue index 02fea0b6..7ac7b9d3 100644 --- a/src/components/settings_modal/tabs/theme_tab/preview.vue +++ b/src/components/settings_modal/tabs/theme_tab/preview.vue @@ -15,7 +15,7 @@ <span class="alert error"> {{ $t('settings.style.preview.error') }} </span> - <button class="btn"> + <button class="btn button-default"> {{ $t('settings.style.preview.button') }} </button> </div> @@ -102,7 +102,7 @@ > <label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label> </span> - <button class="btn"> + <button class="btn button-default"> {{ $t('settings.style.preview.button') }} </button> </div> diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss index 926eceff..1b7d9f31 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss @@ -165,7 +165,8 @@ border-color: var(--border, $fallback--border); margin: 1em 0; padding: 1em; - background: var(--body-background-image); + background-color: var(--wallpaper); + background-image: var(--body-background-image); background-size: cover; background-position: 50% 50%; 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 280e1955..b8add42f 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue @@ -12,13 +12,13 @@ <div class="buttons"> <template v-if="themeWarning.type === 'snapshot_source_mismatch'"> <button - class="btn" + class="btn button-default" @click="forceLoad" > {{ $t('settings.style.switcher.use_source') }} </button> <button - class="btn" + class="btn button-default" @click="forceSnapshot" > {{ $t('settings.style.switcher.use_snapshot') }} @@ -26,7 +26,7 @@ </template> <template v-else-if="themeWarning.noActionsPossible"> <button - class="btn" + class="btn button-default" @click="dismissWarning" > {{ $t('general.dismiss') }} @@ -34,13 +34,13 @@ </template> <template v-else> <button - class="btn" + class="btn button-default" @click="forceLoad" > {{ $t('settings.style.switcher.load_theme') }} </button> <button - class="btn" + class="btn button-default" @click="dismissWarning" > {{ $t('settings.style.switcher.keep_as_is') }} @@ -131,13 +131,13 @@ <p>{{ $t('settings.theme_help') }}</p> <div class="tab-header-buttons"> <button - class="btn" + class="btn button-default" @click="clearOpacity" > {{ $t('settings.style.switcher.clear_opacity') }} </button> <button - class="btn" + class="btn button-default" @click="clearV1" > {{ $t('settings.style.switcher.clear_all') }} @@ -238,13 +238,13 @@ <div class="tab-header"> <p>{{ $t('settings.theme_help') }}</p> <button - class="btn" + class="btn button-default" @click="clearOpacity" > {{ $t('settings.style.switcher.clear_opacity') }} </button> <button - class="btn" + class="btn button-default" @click="clearV1" > {{ $t('settings.style.switcher.clear_all') }} @@ -617,6 +617,15 @@ /> </div> <div class="color-item"> + <h4>{{ $t('settings.style.advanced_colors.wallpaper') }}</h4> + <ColorInput + v-model="wallpaperColorLocal" + name="wallpaper" + :label="$t('settings.style.advanced_colors.wallpaper')" + :fallback="previewTheme.colors.wallpaper" + /> + </div> + <div class="color-item"> <h4>{{ $t('settings.style.advanced_colors.poll') }}</h4> <ColorInput v-model="pollColorLocal" @@ -806,7 +815,7 @@ <div class="tab-header"> <p>{{ $t('settings.radii_help') }}</p> <button - class="btn" + class="btn button-default" @click="clearRoundness" > {{ $t('settings.style.switcher.clear_all') }} @@ -936,7 +945,7 @@ /> </div> <button - class="btn" + class="btn button-default" @click="clearShadows" > {{ $t('settings.style.switcher.clear_all') }} @@ -980,7 +989,7 @@ <div class="tab-header"> <p>{{ $t('settings.style.fonts.help') }}</p> <button - class="btn" + class="btn button-default" @click="clearFonts" > {{ $t('settings.style.switcher.clear_all') }} @@ -1017,14 +1026,14 @@ <div class="apply-container"> <button - class="btn submit" + class="btn button-default submit" :disabled="!themeValid" @click="setCustomTheme" > {{ $t('general.apply') }} </button> <button - class="btn" + class="btn button-default" @click="clearAll" > {{ $t('settings.style.switcher.reset') }} diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue index 78f0e544..37d491f0 100644 --- a/src/components/shadow_control/shadow_control.vue +++ b/src/components/shadow_control/shadow_control.vue @@ -84,7 +84,7 @@ /> </label> <button - class="btn btn-default" + class="btn button-default" :disabled="!ready || !present" @click="del" > @@ -94,7 +94,7 @@ /> </button> <button - class="btn btn-default" + class="btn button-default" :disabled="!moveUpValid" @click="moveUp" > @@ -104,7 +104,7 @@ /> </button> <button - class="btn btn-default" + class="btn button-default" :disabled="!moveDnValid" @click="moveDn" > @@ -114,7 +114,7 @@ /> </button> <button - class="btn btn-default" + class="btn button-default" :disabled="usingFallback" @click="add" > diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 28c888fe..695ae03b 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -144,8 +144,8 @@ </router-link> </li> <li @click="toggleDrawer"> - <a - href="#" + <button + class="button-unstyled -link -fullwidth" @click="openSettingsModal" > <FAIcon @@ -153,7 +153,7 @@ class="fa-scale-110 fa-old-padding" icon="cog" /> {{ $t("settings.settings") }} - </a> + </button> </li> <li @click="toggleDrawer"> <router-link :to="{ name: 'about'}"> @@ -183,8 +183,8 @@ v-if="currentUser" @click="toggleDrawer" > - <a - href="#" + <button + class="button-unstyled -link -fullwidth" @click="doLogout" > <FAIcon @@ -192,7 +192,7 @@ class="fa-scale-110 fa-old-padding" icon="sign-out-alt" /> {{ $t("login.logout") }} - </a> + </button> </li> </ul> </div> @@ -331,7 +331,7 @@ .side-drawer li { padding: 0; - a { + a, button { box-sizing: border-box; display: block; height: 3em; diff --git a/src/components/status/status.js b/src/components/status/status.js index 142e1fc6..f9c710ab 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -26,7 +26,6 @@ import { faTimes, faRetweet, faReply, - faExternalLinkSquareAlt, faPlusSquare, faSmileBeam, faEllipsisH, @@ -44,7 +43,6 @@ library.add( faTimes, faRetweet, faReply, - faExternalLinkSquareAlt, faPlusSquare, faStar, faSmileBeam, diff --git a/src/components/status/status.scss b/src/components/status/status.scss index 0a94de32..58b55bc8 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -29,6 +29,8 @@ $status-margin: 0.75em; &.-conversation { border-left-width: 4px; border-left-style: solid; + border-left-color: $fallback--cRed; + border-left-color: var(--cRed, $fallback--cRed); } .gravestone { @@ -137,6 +139,20 @@ $status-margin: 0.75em; .heading-right { display: flex; flex-shrink: 0; + + .button-unstyled { + padding: 5px; + margin: -5px; + + &:hover svg { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + } + } + + .svg-inline--fa { + margin-left: 0.25em; + } } .timeago { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 21412faa..6ee8117f 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -47,16 +47,15 @@ > {{ muteWordHits.join(', ') }} </small> - <a - href="#" - class="unmute fa-scale-110 fa-old-padding" + <button + class="unmute button-unstyled" @click.prevent="toggleMute" > <FAIcon icon="eye-slash" class="fa-scale-110 fa-old-padding" /> - </a> + </button> </div> </template> <template v-else> @@ -185,43 +184,34 @@ :title="status.visibility | capitalize" > <FAIcon - class="fa-scale-110 fa-old-padding" + fixed-width + class="fa-scale-110" :icon="visibilityIcon(status.visibility)" /> </span> - <a - v-if="!status.is_local && !isPreview" - :href="status.external_url" - target="_blank" - class="source_url" - title="Source" - > - <FAIcon - class="fa-scale-110 fa-old-padding" - icon="external-link-square-alt" - /> - </a> - <a + <button v-if="expandable && !isPreview" - href="#" - title="Expand" + class="button-unstyled" + :title="$t('status.expand')" @click.prevent="toggleExpanded" > <FAIcon - class="fa-scale-110 fa-old-padding" + fixed-width + class="fa-scale-110" icon="plus-square" /> - </a> - <a + </button> + <button v-if="unmuted" - href="#" + class="button-unstyled" @click.prevent="toggleMute" > <FAIcon + fixed-width icon="eye-slash" - class="fa-scale-110 fa-old-padding" + class="fa-scale-110" /> - </a> + </button> </span> </div> @@ -237,9 +227,8 @@ style="min-width: 0" :class="{ '-strikethrough': !status.parent_visible }" > - <a - class="reply-to" - href="#" + <button + class="button-unstyled reply-to" :aria-label="$t('tool_tip.reply')" @click.prevent="gotoOriginal(status.in_reply_to_status_id)" > @@ -253,7 +242,7 @@ > {{ $t('status.reply_to') }} </span> - </a> + </button> </StatusPopover> <span @@ -286,11 +275,12 @@ :key="reply.id" :status-id="reply.id" > - <a - href="#" - class="reply-link" + <button + class="button-unstyled -link reply-link" @click.prevent="gotoOriginal(reply.id)" - >{{ reply.name }}</a> + > + {{ reply.name }} + </button> </StatusPopover> </div> </div> diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue index 321cd477..90bfaf40 100644 --- a/src/components/status_content/status_content.vue +++ b/src/components/status_content/status_content.vue @@ -12,35 +12,34 @@ @click.prevent="linkClicked" v-html="status.summary_html" /> - <a + <button v-if="longSubject && showingLongSubject" - href="#" - class="tall-subject-hider" + class="button-unstyled -link tall-subject-hider" @click.prevent="showingLongSubject=false" - >{{ $t("status.hide_full_subject") }}</a> - <a + > + {{ $t("status.hide_full_subject") }} + </button> + <button v-else-if="longSubject" - class="tall-subject-hider" + class="button-unstyled -link tall-subject-hider" :class="{ 'tall-subject-hider_focused': focused }" - href="#" @click.prevent="showingLongSubject=true" > {{ $t("status.show_full_subject") }} - </a> + </button> </div> <div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper" > - <a + <button v-if="hideTallStatus" - class="tall-status-hider" + class="button-unstyled -link tall-status-hider" :class="{ 'tall-status-hider_focused': focused }" - href="#" @click.prevent="toggleShowMore" > {{ $t("general.show_more") }} - </a> + </button> <div v-if="!hideSubjectStatus" :class="{ 'single-line': singleLine }" @@ -48,10 +47,9 @@ @click.prevent="linkClicked" v-html="postBodyHtml" /> - <a + <button v-if="hideSubjectStatus" - href="#" - class="cw-status-hider" + class="button-unstyled -link cw-status-hider" @click.prevent="toggleShowMore" > {{ $t("status.show_content") }} @@ -79,15 +77,14 @@ v-if="status.card" icon="link" /> - </a> - <a + </button> + <button v-if="showingMore && !fullContent" - href="#" - class="status-unhider" + class="button-unstyled -link status-unhider" @click.prevent="toggleShowMore" > {{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }} - </a> + </button> </div> <div v-if="status.poll && status.poll.options && !hideSubjectStatus"> diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js index 6e6e8193..76e7ef03 100644 --- a/src/components/tab_switcher/tab_switcher.js +++ b/src/components/tab_switcher/tab_switcher.js @@ -81,7 +81,7 @@ export default Vue.component('tab-switcher', { const tabs = this.$slots.default .map((slot, index) => { if (!slot.tag) return - const classesTab = ['tab'] + const classesTab = ['tab', 'button-default'] const classesWrapper = ['tab-wrapper'] if (this.activeIndex === index) { classesTab.push('active') diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index cba46daf..665d195e 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -50,17 +50,10 @@ const Timeline = { TimelineMenu }, computed: { - timelineError () { - return this.$store.state.statuses.error - }, - errorData () { - return this.$store.state.statuses.errorData - }, newStatusCount () { return this.timeline.newStatusCount }, showLoadButton () { - if (this.timelineError || this.errorData) return false return this.timeline.newStatusCount > 0 || this.timeline.flushMarker !== 0 }, loadButtonString () { @@ -171,11 +164,12 @@ const Timeline = { userId: this.userId, tag: this.tag }).then(({ statuses }) => { - store.commit('setLoading', { timeline: this.timelineName, value: false }) if (statuses && statuses.length === 0) { this.bottomedOut = true } - }) + }).finally(() => + store.commit('setLoading', { timeline: this.timelineName, value: false }) + ) }, 1000, this), determineVisibleStatuses () { if (!this.$refs.timeline) return diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 04859852..4c43fe5c 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -2,23 +2,9 @@ <div :class="[classes.root, 'Timeline']"> <div :class="classes.header"> <TimelineMenu v-if="!embedded" /> - <div - v-if="timelineError" - class="loadmore-error alert error" - @click.prevent - > - {{ $t('timeline.error_fetching') }} - </div> - <div - v-else-if="errorData" - class="loadmore-error alert error" - @click.prevent - > - {{ errorData.statusText }} - </div> <button - v-else-if="showLoadButton" - class="loadmore-button" + v-if="showLoadButton" + class="button-default loadmore-button" @click.prevent="showNewStatuses" > {{ loadButtonString }} @@ -75,19 +61,15 @@ > {{ $t('timeline.no_more_statuses') }} </div> - <a - v-else-if="!timeline.loading && !errorData" - href="#" + <button + v-else-if="!timeline.loading" + class="button-unstyled -link -fullwidth" @click.prevent="fetchOlderStatuses()" > - <div class="new-status-notification text-center panel-footer">{{ $t('timeline.load_older') }}</div> - </a> - <a - v-else-if="errorData" - href="#" - > - <div class="new-status-notification text-center panel-footer">{{ errorData.error }}</div> - </a> + <div class="new-status-notification text-center panel-footer"> + {{ $t('timeline.load_older') }} + </div> + </button> <div v-else class="new-status-notification text-center panel-footer" @@ -120,6 +102,7 @@ .timeline-heading { max-width: 100%; flex-wrap: nowrap; + align-items: center; .loadmore-button { flex-shrink: 0; } diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js index 4ccd52b4..8d6a58b1 100644 --- a/src/components/timeline_menu/timeline_menu.js +++ b/src/components/timeline_menu/timeline_menu.js @@ -19,7 +19,7 @@ library.add( faChevronDown ) -// Route -> i18n key mapping, exported andnot in the computed +// Route -> i18n key mapping, exported and not in the computed // because nav panel benefits from the same information. export const timelineNames = () => { return { @@ -27,8 +27,7 @@ export const timelineNames = () => { 'bookmarks': 'nav.bookmarks', 'dms': 'nav.dms', 'public-timeline': 'nav.public_tl', - 'public-external-timeline': 'nav.twkn', - 'tag-timeline': 'tag' + 'public-external-timeline': 'nav.twkn' } } @@ -60,6 +59,14 @@ const TimelineMenu = { this.isOpen = true }, 25) }, + blockOpen (event) { + // For the blank area inside the button element. + // Just setting @click.stop="" makes unintuitive behavior when + // menu is open and clicking on the blank area doesn't close it. + if (!this.isOpen) { + event.stopPropagation() + } + }, timelineName () { const route = this.$route.name if (route === 'tag-timeline') { diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue index c46531be..3c86842b 100644 --- a/src/components/timeline_menu/timeline_menu.vue +++ b/src/components/timeline_menu/timeline_menu.vue @@ -65,10 +65,16 @@ slot="trigger" class="title timeline-menu-title" > - <span>{{ timelineName() }}</span> - <FAIcon - size="sm" - icon="chevron-down" + <span class="timeline-title">{{ timelineName() }}</span> + <span> + <FAIcon + size="sm" + icon="chevron-down" + /> + </span> + <span + class="click-blocker" + @click="blockOpen" /> </div> </Popover> @@ -117,8 +123,9 @@ cursor: pointer; user-select: none; width: 100%; + display: flex; - span { + .timeline-menu-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -128,6 +135,11 @@ margin-left: 0.6em; transition: transform 100ms; } + + .click-blocker { + cursor: default; + flex-grow: 1; + } } &.open .timeline-menu-title svg { diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index f916af9d..16dd5249 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -162,7 +162,7 @@ <template v-if="relationship.following"> <ProgressButton v-if="!relationship.subscribing" - class="btn btn-default" + class="btn button-default" :click="subscribeUser" :title="$t('user_card.subscribe')" > @@ -170,7 +170,7 @@ </ProgressButton> <ProgressButton v-else - class="btn btn-default toggled" + class="btn button-default toggled" :click="unsubscribeUser" :title="$t('user_card.unsubscribe')" > @@ -192,14 +192,14 @@ <div> <button v-if="relationship.muting" - class="btn btn-default btn-block toggled" + class="btn button-default btn-block toggled" @click="unmuteUser" > {{ $t('user_card.muted') }} </button> <button v-else - class="btn btn-default btn-block" + class="btn button-default btn-block" @click="muteUser" > {{ $t('user_card.mute') }} @@ -207,7 +207,7 @@ </div> <div> <button - class="btn btn-default btn-block" + class="btn button-default btn-block" @click="mentionUser" > {{ $t('user_card.mention') }} diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index f1f51840..745e795d 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -20,14 +20,13 @@ :key="index" class="user-profile-field" > + <!-- eslint-disable vue/no-v-html --> <dt :title="user.fields_text[index].name" class="user-profile-field-name" @click.prevent="linkClicked" - > - {{ field.name }} - </dt> - <!-- eslint-disable vue/no-v-html --> + v-html="field.name" + /> <dd :title="user.fields_text[index].value" class="user-profile-field-value" diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue index 2a8d8d48..fb43094f 100644 --- a/src/components/user_reporting_modal/user_reporting_modal.vue +++ b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -29,7 +29,7 @@ </div> <div> <button - class="btn btn-default" + class="btn button-default" :disabled="processing" @click="reportUser" > diff --git a/src/components/video_attachment/video_attachment.vue b/src/components/video_attachment/video_attachment.vue index a4bf01e8..8a3ea1e3 100644 --- a/src/components/video_attachment/video_attachment.vue +++ b/src/components/video_attachment/video_attachment.vue @@ -1,6 +1,7 @@ <template> <video class="video" + preload="metadata" :src="attachment.url" :loop="loopVideo" :controls="controls" diff --git a/src/hocs/with_load_more/with_load_more.js b/src/hocs/with_load_more/with_load_more.js index afb51a0f..7df9dbb2 100644 --- a/src/hocs/with_load_more/with_load_more.js +++ b/src/hocs/with_load_more/with_load_more.js @@ -91,7 +91,11 @@ const withLoadMore = ({ {children} </WrappedComponent> <div class="with-load-more-footer"> - {this.error && <a onClick={this.fetchEntries} class="alert error">{this.$t('general.generic_error')}</a>} + {this.error && + <button onClick={this.fetchEntries} class="button-unstyled -link -fullwidth alert error"> + {this.$t('general.generic_error')} + </button> + } {!this.error && this.loading && <FAIcon spin icon="circle-notch"/>} {!this.error && !this.loading && !this.bottomedOut && <a onClick={this.fetchEntries}>{this.$t('general.more')}</a>} </div> diff --git a/src/i18n/en.json b/src/i18n/en.json index d3d57562..26dd6144 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -50,7 +50,8 @@ "scope_options": "Scope options", "text_limit": "Text limit", "title": "Features", - "who_to_follow": "Who to follow" + "who_to_follow": "Who to follow", + "upload_limit": "Upload limit" }, "finder": { "error_fetching_user": "Error fetching user", @@ -130,6 +131,7 @@ }, "notifications": { "broken_favorite": "Unknown status, searching for it…", + "error": "Error fetching notifications: {0}", "favorited_you": "favorited your status", "followed_you": "followed you", "follow_request": "wants to follow you", @@ -327,6 +329,7 @@ "hide_muted_posts": "Hide posts of muted users", "max_thumbnails": "Maximum amount of thumbnails per post", "hide_isp": "Hide instance-specific panel", + "hide_wallpaper": "Hide instance wallpaper", "preload_images": "Preload images", "use_one_click_nsfw": "Open NSFW attachments with just one click", "hide_post_stats": "Hide post statistics (e.g. the number of favorites)", @@ -376,7 +379,7 @@ "hide_followers_count_description": "Don't show follower count", "show_admin_badge": "Show Admin badge in my profile", "show_moderator_badge": "Show Moderator badge in my profile", - "nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding", + "nsfw_clickthrough": "Enable clickthrough attachment and link preview image hiding for NSFW statuses", "oauth_tokens": "OAuth tokens", "token": "Token", "refresh_token": "Refresh Token", @@ -514,6 +517,7 @@ "inputs": "Input fields", "faint_text": "Faded text", "underlay": "Underlay", + "wallpaper": "Wallpaper", "poll": "Poll graph", "icons": "Icons", "highlight": "Highlighted elements", @@ -634,7 +638,7 @@ "timeline": { "collapse": "Collapse", "conversation": "Conversation", - "error_fetching": "Error fetching updates", + "error": "Error fetching timeline: {0}", "load_older": "Load older statuses", "no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated", "repeated": "repeated", @@ -660,13 +664,16 @@ "unmute_conversation": "Unmute conversation", "status_unavailable": "Status unavailable", "copy_link": "Copy link to status", + "external_source": "External source", "thread_muted": "Thread muted", "thread_muted_and_words": ", has words:", "show_full_subject": "Show full subject", "hide_full_subject": "Hide full subject", "show_content": "Show content", "hide_content": "Hide content", - "status_deleted": "This post was deleted" + "status_deleted": "This post was deleted", + "nsfw": "NSFW", + "expand": "Expand" }, "user_card": { "approve": "Approve", @@ -756,6 +763,7 @@ "upload": { "error": { "base": "Upload failed.", + "message": "Upload failed: {0}", "file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", "default": "Try again later" }, diff --git a/src/i18n/eo.json b/src/i18n/eo.json index 1247d50d..b0a15cfe 100644 --- a/src/i18n/eo.json +++ b/src/i18n/eo.json @@ -134,14 +134,14 @@ "registration": { "bio": "Priskribo", "email": "Retpoŝtadreso", - "fullname": "Vidiga nomo", + "fullname": "Prezenta nomo", "password_confirm": "Konfirmo de pasvorto", "registration": "Registriĝo", "token": "Invita ĵetono", "captcha": "TESTO DE HOMECO", "new_captcha": "Klaku la bildon por akiri novan teston", "username_placeholder": "ekz. lain", - "fullname_placeholder": "ekz. Lain Iwakura", + "fullname_placeholder": "ekz. Lain Ivakura", "bio_placeholder": "ekz.\nSaluton, mi estas Lain.\nMi estas animea knabino vivanta en Japanujo. Eble vi konas min pro la retejo « Wired ».", "validations": { "username_required": "ne povas resti malplena", @@ -164,7 +164,7 @@ "blocks_tab": "Blokitoj", "btnRadius": "Butonoj", "cBlue": "Blua (respondi, aboni)", - "cGreen": "Verda (kunhavigi)", + "cGreen": "Verda (diskonigi)", "cOrange": "Oranĝa (ŝati)", "cRed": "Ruĝa (nuligi)", "change_password": "Ŝanĝi pasvorton", @@ -207,8 +207,8 @@ "import_theme": "Enlegi antaŭagordojn", "inputRadius": "Enigaj kampoj", "checkboxRadius": "Markbutonoj", - "instance_default": "(implicita: {value})", - "instance_default_simple": "(implicita)", + "instance_default": "(originale: {value})", + "instance_default_simple": "(originale)", "interface": "Fasado", "interfaceLanguage": "Lingvo de fasado", "invalid_theme_imported": "La elektita dosiero ne estas subtenata haŭto de Pleromo. Neniuj ŝanĝoj al via haŭto okazis.", @@ -219,7 +219,7 @@ "loop_video_silent_only": "Ripetadi nur filmojn sen sono (ekz. la «GIF-ojn» de Mastodon)", "mutes_tab": "Silentigoj", "play_videos_in_modal": "Ludi filmojn en ŝpruca kadro", - "use_contain_fit": "Ne tondi la kunsendaĵon en bildetoj", + "use_contain_fit": "Ne pritondi bildetojn de kunsendaĵoj", "name": "Nomo", "name_bio": "Nomo kaj priskribo", "new_password": "Nova pasvorto", @@ -265,7 +265,7 @@ "subject_line_email": "Kiel retpoŝto: «re: temo»", "subject_line_mastodon": "Kiel Mastodon: kopii senŝanĝe", "subject_line_noop": "Ne kopii", - "post_status_content_type": "Afiŝi specon de la enhavo de la stato", + "post_status_content_type": "Speco de enhavo de afiŝo", "stop_gifs": "Movi GIF-bildojn dum ŝvebo de muso", "streaming": "Ŝalti memagan fluigon de novaj afiŝoj kiam vi vidas la supron de la paĝo", "text": "Teksto", @@ -379,7 +379,7 @@ "hint": "Por ombroj vi ankaŭ povas uzi --variable kiel koloran valoron, por uzi variantojn de CSS3. Bonvolu rimarki, ke tiuokaze agordoj de maltravidebleco ne funkcios.", "filter_hint": { "always_drop_shadow": "Averto: ĉi tiu ombro ĉiam uzas {0} kiam la foliumilo tion subtenas.", - "drop_shadow_syntax": "{0} ne subtenas parametron {1} kaj ŝlosilvorton {2}.", + "drop_shadow_syntax": "{0} ne subtenas parametron {1} kaj ĉefvorton {2}.", "avatar_inset": "Bonvolu rimarki, ke agordi ambaŭ internajn kaj eksterajn ombrojn por profilbildoj povas redoni neatenditajn rezultojn ĉe profilbildoj travideblaj.", "spread_zero": "Ombroj kun vastigo > 0 aperos kvazaŭ ĝi estus fakte nulo", "inset_classic": "Internaj ombroj uzos {0}" @@ -394,7 +394,7 @@ "button": "Butono", "buttonHover": "Butono (je ŝvebo)", "buttonPressed": "Butono (premita)", - "buttonPressedHover": "Butono (premita kaj je ŝvebo)", + "buttonPressedHover": "Butono (je premo kaj ŝvebo)", "input": "Eniga kampo" }, "hintV3": "Kolorojn de ombroj vi ankaŭ povas skribi per la sistemo {0}." @@ -683,7 +683,7 @@ "replace": "Anstataŭigi", "reject": "Rifuzi", "ftl_removal": "Forigo el la historio de «La tuta konata reto»", - "keyword_policies": "Politiko pri ŝlosilvortoj" + "keyword_policies": "Politiko pri ĉefvortoj" }, "federation": "Federado", "mrf_policies_desc": "Politikoj de Mesaĝa ŝanĝilaro (MRF) efikas sur federa konduto de la nodo. La sekvaj politikoj estas ŝaltitaj:" @@ -739,8 +739,8 @@ "week_short": "{0}s", "weeks": "{0} semajnoj", "week": "{0} semajno", - "seconds_short": "{0}s", - "second_short": "{0}s", + "seconds_short": "{0}sek", + "second_short": "{0}sek", "seconds": "{0} sekundoj", "second": "{0} sekundo", "now_short": "nun", @@ -749,14 +749,14 @@ "month_short": "{0}m", "months": "{0} monatoj", "month": "{0} monato", - "minutes_short": "{0}m", - "minute_short": "{0}m", + "minutes_short": "{0}min", + "minute_short": "{0}min", "minutes": "{0} minutoj", "minute": "{0} minuto", "in_past": "antaŭ {0}", "in_future": "post {0}", - "hours_short": "{0}h", - "hour_short": "{0}h", + "hours_short": "{0}hor", + "hour_short": "{0}hor", "hours": "{0} horoj", "hour": "{0} horo", "days_short": "{0}t", diff --git a/src/i18n/es.json b/src/i18n/es.json index 6889df9a..0c2cc3e9 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -104,7 +104,8 @@ "no_more_notifications": "No hay más notificaciones", "reacted_with": "reaccionó con {0}", "migrated_to": "migrado a", - "follow_request": "quiere seguirte" + "follow_request": "quiere seguirte", + "error": "Error obteniendo notificaciones:{0}" }, "polls": { "add_poll": "Añadir encuesta", @@ -313,7 +314,7 @@ "hide_followers_count_description": "No mostrar el número de cuentas que me siguen", "show_admin_badge": "Mostrar la insignia de Administrador en mi perfil", "show_moderator_badge": "Mostrar la insignia de Moderador en mi perfil", - "nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW", + "nsfw_clickthrough": "Habilitar la ocultación de la imagen de vista previa del enlace y el adjunto para los estados NSFW por defecto", "oauth_tokens": "Tokens de OAuth", "token": "Token", "refresh_token": "Actualizar el token", @@ -605,7 +606,8 @@ "up_to_date": "Actualizado", "no_more_statuses": "No hay más estados", "no_statuses": "Sin estados", - "reload": "Recargar" + "reload": "Recargar", + "error": "Error obteniendo la linea de tiempo:{0}" }, "status": { "favorites": "Favoritos", @@ -628,7 +630,9 @@ "copy_link": "Copiar el enlace al estado", "status_unavailable": "Estado no disponible", "bookmark": "Marcar", - "unbookmark": "Desmarcar" + "unbookmark": "Desmarcar", + "status_deleted": "Esta entrada ha sido eliminada", + "nsfw": "NSFW (No apropiado para el trabajo)" }, "user_card": { "approve": "Aprobar", diff --git a/src/i18n/he.json b/src/i18n/he.json index 7f2bf58f..4b920536 100644 --- a/src/i18n/he.json +++ b/src/i18n/he.json @@ -390,5 +390,13 @@ "GiB": "GiB", "TiB": "TiB" } + }, + "about": { + "mrf": { + "keyword": { + "keyword_policies": "פוליסת מילות מפתח" + }, + "federation": "פדרציה" + } } } diff --git a/src/i18n/it.json b/src/i18n/it.json index 67e92b32..58dafca5 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -50,7 +50,8 @@ "follow_request": "vuole seguirti", "no_more_notifications": "Fine delle notifiche", "migrated_to": "è migrato verso", - "reacted_with": "ha reagito con {0}" + "reacted_with": "ha reagito con {0}", + "error": "Errore nel caricare le notifiche: {0}" }, "settings": { "attachments": "Allegati", @@ -427,7 +428,8 @@ "repeated": "condiviso", "no_statuses": "Nessun messaggio", "no_more_statuses": "Fine dei messaggi", - "reload": "Ricarica" + "reload": "Ricarica", + "error": "Errore nel caricare la sequenza: {0}" }, "user_card": { "follow": "Segui", @@ -703,7 +705,8 @@ "delete_confirm": "Vuoi veramente eliminare questo messaggio?", "unbookmark": "Rimuovi segnalibro", "bookmark": "Aggiungi segnalibro", - "status_deleted": "Questo messagio è stato cancellato" + "status_deleted": "Questo messagio è stato cancellato", + "nsfw": "Pruriginoso" }, "time": { "years_short": "{0}a", diff --git a/src/i18n/messages.js b/src/i18n/messages.js index c3195f10..2624d1d0 100644 --- a/src/i18n/messages.js +++ b/src/i18n/messages.js @@ -33,6 +33,7 @@ const loaders = { ro: () => import('./ro.json'), ru: () => import('./ru.json'), te: () => import('./te.json'), + uk: () => import('./uk.json'), zh: () => import('./zh.json') } diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 8f421b50..3a7c61a9 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -18,7 +18,13 @@ "generic_error": "Произошла ошибка", "optional": "не обязательно", "show_less": "Показать меньше", - "show_more": "Показать больше" + "show_more": "Показать больше", + "peek": "Свернуть", + "dismiss": "Закрыть", + "retry": "Попробуйте еще раз", + "error_retry": "Пожалуйста попробуйте еще раз", + "close": "Закрыть", + "loading": "Загрузка…" }, "login": { "login": "Войти", @@ -33,8 +39,11 @@ "recovery_code": "Код восстановления", "heading": { "TotpForm": "Двухфакторная аутентификация", - "RecoveryForm": "Two-factor recovery" - } + "RecoveryForm": "Two-factor recovery", + "totp": "Двухфакторная аутентификация" + }, + "hint": "Войдите чтобы присоединиться к дискуссии", + "description": "Войти с помощью OAuth" }, "nav": { "back": "Назад", @@ -46,7 +55,14 @@ "twkn": "Федеративная лента", "search": "Поиск", "friend_requests": "Запросы на чтение", - "bookmarks": "Закладки" + "bookmarks": "Закладки", + "chats": "Беседы", + "timelines": "Ленты", + "preferences": "Настройки", + "who_to_follow": "Кого читать", + "dms": "Личные Сообщения", + "administration": "Панель администратора", + "about": "О сервере" }, "notifications": { "broken_favorite": "Неизвестный статус, ищем...", @@ -56,12 +72,17 @@ "notifications": "Уведомления", "read": "Прочесть", "repeated_you": "повторил(а) ваш статус", - "follow_request": "хочет читать вас" + "follow_request": "хочет читать вас", + "reacted_with": "добавил реакцию: {0}", + "migrated_to": "мигрировал на", + "no_more_notifications": "Нет дальнейших уведомлений", + "error": "Ошибка при обновлении уведомлений: {0}" }, "interactions": { "favs_repeats": "Повторы и фавориты", - "follows": "Новые подписки", - "load_older": "Загрузить старые взаимодействия" + "follows": "Новые читатели", + "load_older": "Загрузить старые взаимодействия", + "moves": "Миграции пользователей" }, "post_status": { "account_not_locked_warning": "Ваш аккаунт не {0}. Кто угодно может начать читать вас чтобы видеть посты только для подписчиков.", @@ -81,7 +102,21 @@ "private": "Для подписчиков - этот пост видят только подписчики", "public": "Публичный - этот пост виден всем", "unlisted": "Непубличный - этот пост не виден на публичных лентах" - } + }, + "preview_empty": "Пустой предпросмотр", + "media_description_error": "Не удалось обновить вложение, попробуйте еще раз", + "empty_status_error": "Нельзя отправить пустой статус без вложений", + "preview": "Предпросмотр", + "direct_warning_to_first_only": "Это сообщение увидят только пользователи упомянутые в его начале.", + "direct_warning_to_all": "Это сообщение увидят все упомянутые пользователи.", + "content_type": { + "text/bbcode": "BBCode", + "text/html": "HTML", + "text/markdown": "Markdown", + "text/plain": "Простой текст" + }, + "media_description": "Описание вложения", + "new_status": "Написать новый статус" }, "registration": { "bio": "Описание", @@ -97,7 +132,12 @@ "password_required": "не должен быть пустым", "password_confirmation_required": "не должно быть пустым", "password_confirmation_match": "должно совпадать с паролем" - } + }, + "bio_placeholder": "например:\nПривет, я Игорь Печкин.\nРаботаю почтальоном в деревне Простоквашино. С недавних пор велосипедист.", + "fullname_placeholder": "например: Почтальон Печкин", + "username_placeholder": "например: pechkin", + "captcha": "Код подтверждения", + "new_captcha": "Нажмите на изображение чтобы получить новый код" }, "settings": { "enter_current_password_to_confirm": "Введите свой текущий пароль", @@ -196,7 +236,7 @@ "hide_followers_count_description": "Не показывать число моих подписчиков", "show_admin_badge": "Показывать значок администратора в моем профиле", "show_moderator_badge": "Показывать значок модератора в моем профиле", - "nsfw_clickthrough": "Включить скрытие NSFW вложений", + "nsfw_clickthrough": "Включить скрытие NSFW вложений и не показывать изображения в предпросмотре ссылок для NSFW статусов", "oauth_tokens": "OAuth токены", "token": "Токен", "refresh_token": "Рефреш токен", @@ -349,7 +389,60 @@ } }, "allow_following_move": "Разрешить автоматически читать новый аккаунт при перемещении на другой сервер", - "hide_user_stats": "Не показывать статистику пользователей (например количество читателей)" + "hide_user_stats": "Не показывать статистику пользователей (например количество читателей)", + "discoverable": "Разрешить показ аккаунта в поисковиках и других сервисах", + "default_vis": "Видимость постов по умолчанию", + "mutes_and_blocks": "Блокировки и игнорируемые", + "composing": "Составление постов", + "chatMessageRadius": "Сообщения в беседе", + "blocks_tab": "Блокировки", + "import_mutes_from_a_csv_file": "Импортировать игнорируемых из CSV файла", + "mutes_imported": "Игнорируемые импортированы! Обработка может занять некоторое время.", + "mute_import_error": "Произошла ошибка при импорте игнорируемых", + "mute_import": "Импорт игнорируемых", + "block_export_button": "Экспортирует блокировки в CSV файл", + "mute_export_button": "Экспортирует игнорируемых пользователей в CSV файл", + "mute_export": "Экспорт игнорируемых", + "blocks_imported": "Блокировки импортированы! Обработка может занять некоторое время.", + "block_import_error": "Произошла ошибка при импорте блокировок", + "block_import": "Импорт блокировок", + "block_export": "Экспортировать блокировки", + "security": "Безопасность", + "app_name": "Приложение", + "user_mutes": "Пользователи", + "post_status_content_type": "Формат составляемых статусов по умолчанию", + "subject_line_noop": "Не копировать", + "subject_line_mastodon": "Как в Mastodon: скопировать как есть", + "subject_line_email": "Как в e-mail: \"re: тема\"", + "subject_line_behavior": "Копировать тему в ответах", + "no_mutes": "Нет игнорируемых", + "no_blocks": "Нет блокировок", + "notification_visibility_emoji_reactions": "Реакции", + "notification_visibility_moves": "Миграции пользователей", + "use_contain_fit": "Не обрезать вложения в миниатюрах", + "profile_fields": { + "value": "Значение", + "name": "Пункт", + "add_field": "Добавить поле", + "label": "Таблица метаданных профиля" + }, + "play_videos_in_modal": "Проигрывать видео во всплывающей рамке", + "mutes_tab": "Игнорируемые", + "invalid_theme_imported": "Выбраный файл не является темой Pleroma. Изменений в тему не было внесено.", + "import_blocks_from_a_csv_file": "Импортировать блокировки из CSV файла", + "hide_filtered_statuses": "Не показывать отфильтрованные статусы", + "hide_muted_posts": "Не показывать статусы игнорируемых пользователей", + "hide_post_stats": "Не показывать статистику статусов (например количество отметок «Нравится»)", + "use_one_click_nsfw": "Открывать NSFW вложения одним кликом", + "preload_images": "Предварительно загружать изображения", + "max_thumbnails": "Максимальное число миниатюр показываемых в статусе", + "emoji_reactions_on_timeline": "Показывать эмодзи реакции в ленте", + "domain_mutes": "Узлы", + "notification_setting_privacy": "Приватность", + "notification_setting_block_from_strangers": "Не показывать уведомления от пользователей которых вы не читаете", + "notification_setting_filters": "Фильтрация", + "notifications": "Уведомления", + "virtual_scrolling": "Оптимизировать рендеринг ленты" }, "timeline": { "collapse": "Свернуть", @@ -363,7 +456,11 @@ }, "status": { "bookmark": "В закладки", - "unbookmark": "Удалить из закладок" + "unbookmark": "Удалить из закладок", + "status_deleted": "Пост удален", + "reply_to": "Ответ", + "repeats": "Повторы", + "favorites": "Понравилось" }, "user_card": { "block": "Заблокировать", @@ -401,7 +498,11 @@ "quarantine": "Не федерировать посты пользователя", "delete_user": "Удалить пользователя", "delete_user_confirmation": "Вы уверены? Это действие нельзя отменить." - } + }, + "media": "С вложениями", + "mention": "Упомянуть", + "show_repeats": "Показывать повторы", + "hide_repeats": "Скрыть повторы" }, "user_profile": { "timeline_title": "Лента пользователя" @@ -468,15 +569,84 @@ "media_proxy": "Прокси для внешних вложений", "text_limit": "Лимит символов", "title": "Особенности", - "gopher": "Gopher" + "gopher": "Gopher", + "who_to_follow": "Предложения кого читать", + "pleroma_chat_messages": "Pleroma Чат" }, "tool_tip": { "accept_follow_request": "Принять запрос на чтение", - "reject_follow_request": "Отклонить запрос на чтение" + "reject_follow_request": "Отклонить запрос на чтение", + "media_upload": "Прикрепить вложение" }, "image_cropper": { "save_without_cropping": "Сохранить не обрезая", "save": "Сохранить", - "crop_picture": "Обрезать картинку" + "crop_picture": "Обрезать картинку", + "cancel": "Отменить" + }, + "errors": { + "storage_unavailable": "Pleroma не смогла получить доступ к хранилищу браузера. Ваша сессия и настройки не будут сохранены, и вы можете столкнуться с непредвиденными проблемами. Попробуйте включить файлы cookie." + }, + "polls": { + "not_enough_options": "Недостаточно уникальных вариантов в опросе", + "expired": "Опрос закончился {0} назад", + "expires_in": "Опрос заканчивается через {0}", + "expiry": "Срок опроса", + "multiple_choices": "Несколько вариантов", + "single_choice": "Один вариант", + "type": "Тип опроса", + "vote": "Проголосовать", + "votes": "голосов", + "option": "Вариант", + "add_option": "Добавить вариант", + "add_poll": "Прикрепить опрос" + }, + "media_modal": { + "next": "Следующая", + "previous": "Предыдущая" + }, + "importer": { + "error": "Произошла ошибка при импорте файла.", + "success": "Импорт прошел успешно.", + "submit": "Отправить" + }, + "selectable_list": { + "select_all": "Выбрать все" + }, + "emoji": { + "load_all": "Все {emojiAmount} эмодзи загружаются", + "load_all_hint": "Загружены первые {saneAmount} эмодзи, загрузка всех эмодзи может привести к проблемам с производительностью.", + "unicode": "Стандартные эмодзи", + "custom": "Пользовательские эмодзи", + "add_emoji": "Добавить эмодзи", + "search_emoji": "Поиск эмодзи", + "keep_open": "Оставить окно выбора открытым", + "emoji": "Эмодзи", + "stickers": "Стикеры" + }, + "shoutbox": { + "title": "Болтовня" + }, + "time": { + "days_short": "{0}дн", + "years_short": "{0}г", + "year_short": "{0}г", + "weeks_short": "{0}нед", + "week_short": "{0}нед", + "seconds_short": "{0}сек", + "second_short": "{0}с", + "now_short": "только что", + "now": "только что", + "months_short": "{0}мес", + "month_short": "{0}мес", + "minutes_short": "{0}мин", + "minute_short": "{0}мин", + "in_past": "{0} назад", + "in_future": "через {0}", + "hours_short": "{0}ч", + "hour_short": "{0}ч", + "hour": "{0} час", + "day_short": "{0}д", + "days": "{0} дней" } } diff --git a/src/i18n/uk.json b/src/i18n/uk.json new file mode 100644 index 00000000..f630b2dd --- /dev/null +++ b/src/i18n/uk.json @@ -0,0 +1,777 @@ +{ + "general": { + "dismiss": "Закрити", + "close": "Закрити", + "verify": "Перевірити", + "confirm": "Підтвердити", + "enable": "Увімкнути", + "disable": "Вимкнути", + "cancel": "Скасувати", + "show_less": "Показати менше", + "show_more": "Показати більше", + "optional": "необов'язково", + "retry": "Спробуйте ще раз", + "error_retry": "Будь ласка, спробуйте ще раз", + "generic_error": "Виникла помилка", + "loading": "Завантаження…", + "more": "Більше", + "submit": "Відправити", + "apply": "Застосувати", + "peek": "Глянути" + }, + "finder": { + "error_fetching_user": "Користувача не знайдено", + "find_user": "Знайти користувача" + }, + "features_panel": { + "gopher": "Gopher", + "pleroma_chat_messages": "Чат Pleroma", + "chat": "Чат", + "who_to_follow": "Кого відстежувати", + "title": "Особливості", + "scope_options": "Параметри осягу", + "media_proxy": "Посередник медіа-даних", + "text_limit": "Ліміт символів" + }, + "exporter": { + "processing": "Опрацьовую, скоро ви зможете завантажити файл", + "export": "Експорт" + }, + "domain_mute_card": { + "unmute_progress": "Вимикаю…", + "unmute": "Вимкнути ігнорування", + "mute_progress": "Вмикаю…", + "mute": "Ігнорувати" + }, + "shoutbox": { + "title": "Міні-чат" + }, + "about": { + "staff": "Адміністрація", + "mrf": { + "simple": { + "media_nsfw_desc": "Даний інстанс примусово позначає медіа в наступних інстансах як NSFW:", + "media_nsfw": "Примусове визначення медіа як дратівливого", + "media_removal_desc": "Поточний інстанс видаляє медіа з дописів на перелічених інстансах:", + "media_removal": "Видалення медіа", + "ftl_removal_desc": "Цей інстанс видаляє перелічені інстанси з \"Усієї відомої мережі\":", + "ftl_removal": "Видалення з \"Усієї відомої мережі\"", + "quarantine_desc": "Поточний інстанс буде надсилати тільки публічні дописи наступним інстансам:", + "quarantine": "Карантин", + "reject_desc": "Поточний інстанс не прийматиме повідомлення з перелічених інстансів:", + "accept": "Прийняти", + "reject": "Відхилити", + "accept_desc": "Поточний інстанс приймає повідомлення тільки з перелічених інстансів:", + "simple_policies": "Правила поточного інстансу" + }, + "mrf_policies_desc": "Правила MRF розповсюджуються на даний інстанс. Наступні правила активні:", + "mrf_policies": "Активні правила MRF (модуль переписування повідомлень)", + "keyword": { + "is_replaced_by": "→", + "replace": "Замінити", + "reject": "Відхилити", + "ftl_removal": "Прибрати з федеративної стрічки", + "keyword_policies": "Політика щодо ключових слів" + }, + "federation": "Федерація" + } + }, + "login": { + "hint": "Увійдіть, щоб доєднатися до дискусії", + "username": "Ім'я користувача", + "register": "Зареєструватись", + "password": "Пароль", + "logout": "Вийти", + "description": "Увійти за допомогою OAuth", + "login": "Увійти", + "recovery_code": "Код відновлення", + "enter_recovery_code": "Введіть код відновлення", + "authentication_code": "Код автентифікації", + "heading": { + "recovery": "Двофакторне відновлення", + "totp": "Двофакторна автентифікація" + }, + "enter_two_factor_code": "Введіть двофакторний код автентифікації", + "placeholder": "напр. stepan" + }, + "importer": { + "error": "Під час імпортування файлу сталася помилка.", + "success": "Імпортовано успішно.", + "submit": "Відправити" + }, + "image_cropper": { + "cancel": "Відмінити", + "save_without_cropping": "Зберегти не обрізаючи", + "crop_picture": "Обрізати малюнок", + "save": "Зберегти" + }, + "polls": { + "expired": "Опитування закінчилось {0} тому", + "expires_in": "Опитування закінчується через {0}", + "expiry": "Термін опитування", + "multiple_choices": "Декілька варіантів", + "single_choice": "Один варіант", + "add_option": "Додати опцію", + "type": "Тип опитування", + "vote": "Проголосувати", + "votes": "голосів", + "option": "Відповідь", + "add_poll": "Додати опитування", + "not_enough_options": "Замало унікальних варіантів в опитуванні" + }, + "notifications": { + "reacted_with": "додав реакцію: {0}", + "migrated_to": "мігрував на", + "no_more_notifications": "Немає більше сповіщень", + "repeated_you": "поширив(-ла) ваш допис", + "read": "Прочитано!", + "notifications": "Сповіщення", + "load_older": "Завантажити давніші сповіщення", + "follow_request": "хоче підписатись на вас", + "followed_you": "підписався(-лась) на вас", + "favorited_you": "вподобав(-ла) ваш допис", + "broken_favorite": "Невідомий допис, шукаю його…", + "error": "Помилка при оновленні сповіщень: {0}" + }, + "nav": { + "chats": "Локальні балачки", + "timelines": "Стрічки", + "twkn": "Уся відома мережа", + "about": "Інформація", + "preferences": "Налаштування", + "friend_requests": "Запити послідовників", + "who_to_follow": "Кого відстежувати", + "search": "Пошук", + "user_search": "Пошук користувача", + "bookmarks": "Закладки", + "timeline": "Домашня стрічка", + "public_tl": "Публічна стрічка", + "dms": "Приватні повідомлення", + "interactions": "Взаємодії", + "mentions": "Згадування", + "back": "Назад", + "administration": "Адміністрування" + }, + "media_modal": { + "next": "Наступна", + "previous": "Попередня" + }, + "password_reset": { + "instruction": "Введіть свою адресу електронної пошти або ім’я користувача. Ми надішлемо вам посилання для скидання пароля.", + "placeholder": "Ваша електронна адреса або ім'я користувача", + "check_email": "Перевірте електронну пошту на наявність посилання для скидання пароля.", + "return_home": "Повернутися на головну сторінку", + "too_many_requests": "Ви досягли ліміту спроб, спробуйте ще раз пізніше.", + "password_reset_required_but_mailer_is_disabled": "Ви повинні скинути свій пароль, але скидання пароля вимкнено. Зверніться до адміністратора інстансу.", + "password_reset_disabled": "Скидання пароля вимкнено. Зверніться до адміністратора інстансу.", + "password_reset_required": "Для входу потрібно скинути пароль.", + "password_reset": "Відновити пароль", + "forgot_password": "Забули пароль?" + }, + "chats": { + "you": "Ви:", + "message_user": "Повідомлення для {nickname}", + "delete": "Видалити", + "chats": "Чати", + "new": "Новий чат", + "empty_message_error": "Не вдається опублікувати порожнє повідомлення", + "more": "Більше", + "delete_confirm": "Ви дійсно хочете видалити це повідомлення?", + "error_loading_chat": "Під час завантаження чату сталася помилка.", + "error_sending_message": "Під час надсилання повідомлення сталася помилка.", + "empty_chat_list_placeholder": "У вас ще немає чатів. Почніть новий чат!" + }, + "file_type": { + "audio": "Аудіо", + "video": "Відео", + "image": "Зображення", + "file": "Файл" + }, + "display_date": { + "today": "Сьогодні" + }, + "interactions": { + "load_older": "Завантажити давніші взаємодії", + "follows": "Нові підписки", + "favs_repeats": "Повтори та вподобайки", + "moves": "Міграції користувачів" + }, + "errors": { + "storage_unavailable": "Pleroma не змогла отримати доступ до сховища браузеру. Ваша сесія та налаштування не будуть збережені, це може спричинити непередбачувані проблеми. Спробуйте увімкнути cookie." + }, + "emoji": { + "stickers": "Стікери", + "custom": "Користувацькі емодзі", + "search_emoji": "Пошук емодзі", + "keep_open": "Тримати панель відкритою", + "add_emoji": "Додати емодзі", + "emoji": "Емодзі", + "load_all": "Всі {emojiAmount} эмодзі завантажуються", + "load_all_hint": "Завантажені перші {saneAmount} емодзі, завантаження всіх емодзі може призвести до проблем з продуктивністю.", + "unicode": "Стандартні емодзі" + }, + "post_status": { + "content_type": { + "text/bbcode": "BBCode", + "text/markdown": "Markdown", + "text/html": "HTML", + "text/plain": "Простий текст" + }, + "attachments_sensitive": "Позначити вкладення як чутливі", + "account_not_locked_warning_link": "замкнена", + "account_not_locked_warning": "Ваша обліковка не {0}. Будь-хто може відстежувати вас для перегляду дописів тільки для відстежувачів.", + "new_status": "Створити допис", + "direct_warning_to_first_only": "Цей допис побачать лише користувачі, що були згадані на початку повідомлення.", + "direct_warning_to_all": "Цей допис побачать всі згадані користувачі.", + "default": "Що нового?", + "content_warning": "Тема (необов'язково)", + "preview": "Попередній перегляд", + "posting": "Відправляється", + "empty_status_error": "Не можу опублікувати пустий статус без вкладень", + "scope": { + "unlisted": "Непублічний - цей допис буде відсутній у публічних стрічках", + "public": "Піблічний - цей допис побачать усі", + "private": "Для читачів - цей допис побачать лише ваші читачі", + "direct": "Приватний - цей допис побачать лише згадані користувачі" + }, + "scope_notice": { + "unlisted": "Цей допис не буде видно в публічній стрічці та усій відомій мережі", + "private": "Цей допис побачать лише ваші підписники", + "public": "Цей допис бачитимуть усі" + }, + "preview_empty": "Пустий", + "media_description_error": "Не вдалось оновити медіа, спробуйте ще раз", + "media_description": "Опис медіа" + }, + "settings": { + "blocks_imported": "Блокування імпортовані! Їх обробка триватиме певний час.", + "block_import_error": "Помилка імпортування блокувань", + "block_import": "Імпорт блокувань", + "block_export_button": "Експорт блокувань у файл CSV", + "block_export": "Експорт блокувань", + "bio": "Про Вас", + "background": "Обкладинка", + "app_name": "Назва програми", + "follow_export": "Експортувати відстежуваних", + "filtering_explanation": "Усі статуси з цими словами будуть приховані, один на рядок", + "filtering": "Фільтрування", + "export_theme": "Зберегти переднабір", + "avatar_size_instruction": "Рекомендований мінімальний розмір для зображень аватара становить 150x150 пікселів.", + "delete_account_instructions": "Введіть ваш пароль в поле нижче, аби підтвердити видалення облікового запису.", + "delete_account_error": "Під час видалення вашого облікового запису виникла проблема. Якщо це трапляється постійно, будь ласка, зверніться до адміністратора вашого сервера.", + "delete_account_description": "Остаточно видалити ваш обліковий запис та усі ваші повідомлення.", + "delete_account": "Видалити обліковий запис", + "default_vis": "Обсяг видимості за замовчуванням", + "data_import_export_tab": "Імпорт/експорт даних", + "current_password": "Поточний пароль", + "confirm_new_password": "Підтвердіть новий пароль", + "composing": "Складання відповіді", + "collapse_subject": "Згорнути дописи з темами", + "changed_password": "Пароль успішно змінено!", + "change_password_error": "Не вдалося змінити пароль.", + "change_password": "Змінити пароль", + "cRed": "Червоний (Скасувати)", + "cGreen": "Зелений (Поширити)", + "cOrange": "Жовтогарячий (Вподобайки)", + "cBlue": "Блакитний (Відповісти, читати)", + "btnRadius": "Кнопки", + "blocks_tab": "Блокування", + "avatarRadius": "Аватарки", + "avatarAltRadius": "Аватарки у сповіщеннях", + "avatar": "Аватар", + "attachments": "Вкладення", + "attachmentRadius": "Вкладення", + "general": "Загальні", + "foreground": "Передній план", + "follows_imported": "Відстежуваних імпортовано! Їхня обробка потребує часу.", + "follow_import_error": "Помилка імпортування відстежуваних", + "follow_import": "Імпортувати відстежуваних", + "follow_export_button": "Експортувати відстежуваних до csv файлу", + "lock_account_description": "Обмежте свій обліковий запис лише схваленими читачами", + "links": "Посилання", + "limited_availability": "Недоступно у вашому браузері", + "invalid_theme_imported": "Вибраний файл не є темою Pleroma. У вашу тему не внесено жодних змін.", + "interfaceLanguage": "Мова оболонки", + "interface": "Оболонка", + "instance_default_simple": "(за замовчуванням)", + "instance_default": "(за замовчуванням: {value})", + "checkboxRadius": "Прапорці", + "inputRadius": "Поля вводу", + "import_theme": "Завантажити переднабір", + "import_followers_from_a_csv_file": "Імпортувати відстежуваних з csv файлу", + "import_blocks_from_a_csv_file": "Імпортувати заблокованих з csv файлу", + "hide_filtered_statuses": "Сховати відфільтровані статуси", + "hide_user_stats": "Приховувати статистику користувачів (напр. кількість відстежувачів)", + "hide_post_stats": "Приховувати статистику дописів (напр. кількість вподобаних)", + "use_one_click_nsfw": "Відкривати NSFW вкладення одним кліком миші", + "preload_images": "Передзавантажувати світлини", + "hide_isp": "Сховати панель з особливостями сервера", + "max_thumbnails": "Максимальна кількість мініатюр на повідомлення", + "hide_muted_posts": "Приховати повідомлення приглушених користувачів", + "hide_attachments_in_tl": "Приховувати вкладення у стрічці", + "hide_attachments_in_convo": "Приховувати вкладення у розмовах", + "mutes_tab": "Заглушені", + "loop_video_silent_only": "Зациклити відео без звуку (напр. Mastodon \"gifs\")", + "loop_video": "Зациклити відео", + "mfa": { + "verify": { + "desc": "Щоб увімкнути двофакторну автентифікацію, введіть код з вашого застосунку для двофакторної автентифікації:" + }, + "scan": { + "desc": "Відскануйте цей QR-код за допомогою програми двофакторної автентифікації або введіть текстовий ключ:", + "title": "Сканування", + "secret_code": "Ключ" + }, + "authentication_methods": "Методи автентифікації", + "recovery_codes_warning": "Запишіть ці коди і тримайте в безпечному місці - інакше ви їх ніколи не побачите. Якщо ви втратите доступ до OTP додатку - без резервних кодів ви не зможете отримати доступ до свого облікового запису.", + "waiting_a_recovery_codes": "Отримую резервні коди…", + "recovery_codes": "Резервні коди.", + "warning_of_generate_new_codes": "Після отримання нових резервних кодів, старі перестануть працювати.", + "generate_new_recovery_codes": "Згенерувати нові резервні коди", + "title": "Двофакторна автентифікація", + "confirm_and_enable": "Підтвердити та увімкнути OTP", + "wait_pre_setup_otp": "попереднє налаштування OTP", + "setup_otp": "Налаштування OTP", + "otp": "OTP" + }, + "enter_current_password_to_confirm": "Введіть свій поточний пароль", + "security": "Безпека", + "domain_mutes": "Домени", + "discoverable": "Дозволити виявлення цього облікового запису в результатах пошуку та інших службах", + "mutes_and_blocks": "Заглушені та блоковані", + "changed_email": "Email успішно змінено!", + "change_email_error": "Сталася помилка під час зміни email.", + "change_email": "Змінити email", + "bot": "Це обліковий запис бота", + "import_mutes_from_a_csv_file": "Імпорт заглушених з csv файлу", + "mutes_imported": "Заглушені імпортовані! Їх обробка триватиме певний час.", + "mute_export_button": "Експорт заглушених у csv файл", + "mute_import_error": "Під час імпорту заглушених сталася помилка", + "mute_import": "Імпорт ігнорувань", + "mute_export": "Експорт ігнорувань", + "new_password": "Новий пароль", + "new_email": "Нова ел. пошта", + "name_bio": "Особисті дані", + "set_new_profile_banner": "Встановити новий банер", + "set_new_avatar": "Встановити новий аватар", + "security_tab": "Безпека", + "saving_ok": "Налаштування збережені", + "saving_err": "Помилка при збереженні налаштувань", + "reply_visibility_self": "Показувати лише адресовані мені відповіді", + "reply_visibility_following": "Показувати відповіді адресовані лише мені або користувачам, яких я читаю", + "reply_visibility_all": "Показати всі відповіді", + "replies_in_timeline": "Відповіді в стрічці", + "profile_tab": "Профіль", + "profile_banner": "Банер профілю", + "profile_background": "Обкладинка профілю", + "revoke_token": "Відкликати", + "oauth_tokens": "OAuth ключі", + "token": "Ключ", + "refresh_token": "Оновити ключ", + "valid_until": "Діє до", + "use_contain_fit": "Не обрізати краї мініатюр", + "name": "Ім'я", + "profile_fields": { + "value": "Зміст", + "name": "Назва", + "add_field": "Додати поле", + "label": "Метадані профілю" + }, + "play_videos_in_modal": "Відтворювати відео у спливаючій рамці", + "accent": "Акцент", + "chatMessageRadius": "Повідомлення в бесіді", + "notification_mutes": "Щоб перестати отримувати сповіщення від певного користувача, заглушіть його.", + "user_mutes": "Користувачі", + "no_mutes": "Заглушені відсутні", + "emoji_reactions_on_timeline": "Показувати реакції емоджі на стрічці", + "pad_emoji": "Додавати простір з обидвох сторін емоджі, при додаванні з панелі", + "allow_following_move": "Дозволити автостеження при переміщенні на інший інстанс", + "set_new_profile_background": "Встановити нову обкладинку профілю", + "radii_help": "Радіус заокруглення кутів інтерфейсу (в пікселях)", + "presets": "Переднабір", + "show_moderator_badge": "Показувати значок модератора в моєму профілі", + "show_admin_badge": "Показувати значок адміністратора в моєму профілі", + "hide_followers_description": "Не показувати хто підписаний на мене", + "hide_follows_description": "Не показувати на кого я підписаний", + "no_rich_text_description": "Видалення всього форматування тексту з усіх дописів", + "notification_visibility_emoji_reactions": "Реакції", + "notification_visibility_moves": "Міграція користувача", + "notification_visibility_repeats": "Поширення допису", + "notification_visibility_mentions": "Згадування", + "notification_visibility_likes": "Вподобайки", + "notification_visibility_follows": "Нові підписки", + "notification_visibility": "Отримувати сповіщення про наступні події", + "settings": "Налаштування", + "panelRadius": "Панелі", + "text": "Текст", + "tooltipRadius": "Підказки/попередження", + "values": { + "true": "так", + "false": "ні" + }, + "user_settings": "Користувацькі налаштування", + "upload_a_photo": "Завантажити фото", + "theme": "Тема", + "style": { + "switcher": { + "keep_fonts": "Залишити шрифти", + "keep_roundness": "Залишити скруглення", + "keep_opacity": "Залишити прозорості", + "keep_shadows": "Залишити тіні", + "keep_color": "Залишити кольори", + "use_source": "Нова версія", + "use_snapshot": "Стара версія", + "load_theme": "Завантажити тему", + "reset": "Скинути", + "clear_all": "Очистити все", + "help": { + "older_version_imported": "Імпортований файл було створено в старішій версії FE.", + "future_version_imported": "Імпортований файл було створено в новішій версії FE.", + "v2_imported": "Файл, який ви імпортували, був створений для старішої версії інтерфейсу Pleroma. Ми намагаємось покращити сумісність, але все одно можуть бути розбіжності.", + "upgraded_from_v2": "PleromaFE було оновлено, тема може дещо відрізнятися від тієї, яку ви пам’ятаєте.", + "snapshot_source_mismatch": "Конфлікт версій: Швидше за все, FE повернуто до попередньої версії та оновлено знову, якщо ви змінили тему за допомогою старішої версії FE, швидше за все, ви хочете використовувати стару версію, інакше використовуйте нову версію.", + "migration_napshot_gone": "З якоїсь причини знімок зник, деякі речі можуть бути не такими, як ви пам’ятаєте.", + "migration_snapshot_ok": "Для безпеки, знімок теми завантажено. Ви можете спробувати завантажити дані теми.", + "fe_downgraded": "Версію PleromaFE змінено на старшу.", + "fe_upgraded": "Двигун теми PleromaFE оновлено.", + "snapshot_missing": "У файлі немає жодного знімка теми, тому він може виглядати інакше, ніж передбачалося спочатку.", + "snapshot_present": "Знімок теми завантажено, тому всі значення було перезаписано. Натомість ви можете завантажити правильні дані теми." + }, + "keep_as_is": "Залишити як є", + "clear_opacity": "Очистити прозорість", + "save_load_hint": "Параметри \"Зберегти\" зберігають встановлені на даний момент параметри під час вибору або завантаження тем, вони також зберігають зазначені параметри під час експорту теми. Коли всі прапорці знято, експортування теми збереже все." + }, + "common": { + "color": "Колір", + "contrast": { + "context": { + "text": "для тексту", + "18pt": "для великого (18pt+) тексту" + }, + "level": { + "bad": "Не відповідає жодним вимогам щодо доступності", + "aaa": "відповідає вимогам рівня ААA (рекомендований)", + "aa": "відповідає вимогам рівня АА (мінімальний)" + }, + "hint": "Рівень контрасту: {ratio}, {level} {context}" + }, + "opacity": "Прозорість" + }, + "preview": { + "mono": "змісту", + "text": "Трохи більше {0} та {1}", + "button": "Кнопка", + "error": "Приклад помилки", + "content": "Зміст", + "header": "Попередній перегляд", + "link": "невеличке посилання", + "header_faint": "Це нормально", + "input": "Що нового?", + "checkbox": "Я переглянув умови використання", + "fine_print": "Прочитайте наш {0} аби нічого нового не дізнатись!", + "faint_link": "корисний підручник" + }, + "shadows": { + "components": { + "button": "Кнопка", + "input": "Поле вводу", + "panel": "Панель", + "panelHeader": "Заголовок панелі", + "avatarStatus": "Аватар користувача (в стрічці)", + "avatar": "Аватар користувача (профіль)", + "buttonPressedHover": "Кнопка (натиснута + наведенний курсор)", + "buttonPressed": "Кнопка (натиснута)", + "buttonHover": "Кнопка (при наведенні)", + "popup": "Спливаючі вікна та підказки" + }, + "component": "Компонент", + "filter_hint": { + "inset_classic": "Тіні спрямовані всередину використовуватимуть {0}", + "spread_zero": "Тіні з поширенням > 0 відображатимуться так, ніби було встановлено нуль", + "avatar_inset": "Зауважте, що використання як вставних, так і невставних тіней на аватарах може привести до непередбачуваних результатів із прозорими аватарами.", + "drop_shadow_syntax": "{0} не підтримує параметр {1} та ключове слово {2}.", + "always_drop_shadow": "Увага! Ця тінь завжди використовує {0}, якщо підтримується браузером." + }, + "inset": "Всередину", + "blur": "Розмиття", + "shadow_id": "Тінь №{value}", + "override": "Перевизначити", + "_tab_label": "Тінь і підсвічування", + "hintV3": "Для тіней ви також можете використовувати позначення {0} для використання іншого кольорового слота." + }, + "fonts": { + "components": { + "input": "Поля вводу", + "interface": "Інтерфейс", + "postCode": "Моноширинний текст в дописі (форматований текст)", + "post": "Текст допису" + }, + "_tab_label": "Шрифти", + "size": "Розмір (в пікселях)", + "custom": "Нестандартний", + "weight": "Товщина", + "family": "Назва шрифту", + "help": "Виберіть шрифт для елементів інтерфейсу. Для \"нестандартного\" потрібно ввести точну назву шрифту, так як вона відображається в системі." + }, + "advanced_colors": { + "alert_warning": "Попередження", + "underlay": "Тло", + "inputs": "Поля входу", + "buttons": "Кнопки", + "borders": "Кордони", + "top_bar": "Верхня панель", + "panel_header": "Заголовок панелі", + "badge_notification": "Сповіщення", + "popover": "Підказки, меню, поповери", + "badge": "Тло значків", + "post": "Дописи/Дані користувачів", + "alert_neutral": "Нейтральний", + "alert_error": "Помилки", + "alert": "Фон сповіщень", + "_tab_label": "Додатково", + "selectedPost": "Вибраний допис", + "highlight": "Виділені елементи", + "poll": "Діаграма опитування", + "icons": "Іконки", + "faint_text": "Затемнений текст", + "chat": { + "border": "Кайма", + "outgoing": "Вихідні повідомлення", + "incoming": "Вхідні повідомлення" + }, + "toggled": "Переключено", + "disabled": "Вимкнено", + "selectedMenu": "Вибраний пункт меню", + "tabs": "Вкладки", + "pressed": "Натиснуто" + }, + "common_colors": { + "rgbo": "Піктограми, акценти, значки", + "foreground_hint": "Перегляньте вкладку \"Додатково\" для більшого контролю", + "main": "Загальні кольори", + "_tab_label": "Загальні" + }, + "radii": { + "_tab_label": "Округлість" + } + }, + "enable_web_push_notifications": "Увімкнути web push-сповіщення", + "notifications": "Сповіщення", + "fun": "Розваги", + "notification_setting_privacy": "Приватність", + "notification_setting_filters": "Фільтри", + "reset_avatar": "Скинути аватар", + "reset_profile_background": "Скинути обкладинку профілю", + "reset_avatar_confirm": "Ви дійсно хочете скинути аватар?", + "reset_profile_banner": "Скинули банер профілю", + "hide_follows_count_description": "Не показувати на кого я підписаний", + "reset_banner_confirm": "Ви дійсно хочете скинути банер?", + "reset_background_confirm": "Ви дійсно хочете скинути обкладинку?", + "subject_line_behavior": "Вигляд теми при відповіді", + "subject_input_always_show": "Завжди показувати поле для вводу теми", + "minimal_scopes_mode": "Мінімізувати набір варіантів осягу для допису", + "scope_copy": "Копіювати осяг при відповіді (завжди ввімкнено для особистих повідомлень)", + "search_user_to_mute": "Шукайте кого ви хочете заглушити", + "search_user_to_block": "Шукайте кого ви хочете заблокувати", + "autohide_floating_post_button": "Автоматично ховати кнопку \"Новий допис\" (в мобільній версії)", + "pause_on_unfocused": "Призупинити трансляцію, коли вкладка неактивна", + "hide_followers_count_description": "Не показувати кількість читачів", + "notification_blocks": "Блокування користувача зупиняє всі сповіщення від нього, а також скасовує його відстеження.", + "notification_setting_hide_notification_contents": "Ховати відправника та вміст push-сповіщень", + "notification_setting_block_from_strangers": "Блокувати сповіщення від користувачів за якими ви не слідкуєте", + "type_domains_to_mute": "Пошук доменів для заглушення", + "nsfw_clickthrough": "Увімкнути приховування NSFW медіа", + "greentext": "Мемний текст", + "virtual_scrolling": "Оптимізувати оновлення стрчки", + "theme_help_v2_2": "Піктограми під деякими записами є показниками контрасту між фоном та текстом. Коли ви наведете на них курсор, ви отримаєте детальну інформацію. Пам'ятайте, якщо ви використовуєте прозорість, індикатори показують найгірший варіант.", + "theme_help_v2_1": "Ви також можете замінити кольори та видимість окремих компонентів, перемикаючи прапорці, використовуйте \"Очистити все\", щоб видалити всі заміни.", + "theme_help": "Використовувати шістнадцяткові коди кольору (#rrggbb) щоб редагувати тему.", + "no_blocks": "Блокування відсутні", + "subject_line_mastodon": "Як в mastodon: просто скопіювати", + "subject_line_email": "Як в email: \"re: тема\"", + "useStreamingApiWarning": "(Не рекомендується, експериментально, повідомлення можуть зникати)", + "useStreamingApi": "Отримувати повідомлення та сповіщення в режимі реального часу", + "streaming": "Ввімкнути автоматичне завантаження нових повідомлень при прокручуванні вгору", + "stop_gifs": "Відтворювати GIF анімації тільки при наведенні", + "post_status_content_type": "Тип вмісту допису", + "subject_line_noop": "Не копіювати", + "version": { + "frontend_version": "Версія фронтенду", + "backend_version": "Версія бекенду", + "title": "Версія" + } + }, + "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": "не може бути порожнім" + }, + "bio_placeholder": "напр.\nНаш народ завжди прагне волі для себе і бажає її для інших народів. Він боровся і бореться за правду і справедливість. Ми хочемо жити у згоді і взаємному шануванні з усіми народами доброї волі. Такі самі права визнаємо за іншими народами, за які боремося для себе.", + "fullname_placeholder": "напр. Степан Бандера", + "username_placeholder": "напр. stepan", + "new_captcha": "Натисніть на зображення, щоб оновити код, якщо він нерозбірливий", + "token": "Ключ запрошення", + "registration": "Реєстрація", + "password_confirm": "Підтвердження паролю", + "fullname": "Відображене ім'я", + "email": "Ел. пошта", + "bio": "Про себе", + "captcha": "CAPTCHA" + }, + "who_to_follow": { + "who_to_follow": "На кого підписатися", + "more": "Більше" + }, + "tool_tip": { + "repeat": "Поширити", + "reply": "Відповісти", + "add_reaction": "Додати реакцію", + "user_settings": "Налаштування користувача", + "favorite": "Подобається", + "reject_follow_request": "Відхилити запит на підписку", + "accept_follow_request": "Прийняти запит на підписку", + "media_upload": "Завантажити медіа" + }, + "upload": { + "error": { + "base": "Збій при завантаженні.", + "file_too_big": "Файл завеликий [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "Спробуйте ще раз пізніше" + }, + "file_size_units": { + "TiB": "ТіБ", + "GiB": "ГіБ", + "MiB": "МіБ", + "KiB": "КіБ", + "B": "Б" + } + }, + "time": { + "weeks_short": "{0}тижд.", + "week_short": "{0}тижд.", + "years_short": "{0}р", + "year_short": "{0}р", + "years": "{0} роки", + "year": "{0} рік", + "weeks": "{0} тижднів", + "week": "{0} тиждень", + "second_short": "{0}с", + "second": "{0} секунда", + "now_short": "щойно", + "now": "щойно", + "months_short": "{0}міс.", + "month_short": "{0}міс.", + "months": "{0} місяці", + "month": "{0} місяць", + "minutes_short": "{0}хв", + "hours_short": "{0}год", + "hour_short": "{0}год", + "day_short": "{0}д", + "minute_short": "{0}хв", + "minutes": "{0} хвилини", + "minute": "{0} хвилина", + "in_past": "{0} тому", + "hours": "{0} години", + "hour": "{0} година", + "days_short": "{0}д", + "days": "{0} дні", + "day": "{0} день", + "seconds_short": "{0}с", + "seconds": "{0} секунди", + "in_future": "через {0}" + }, + "search": { + "no_results": "Немає результатів", + "hashtags": "Хештеги", + "people": "Люди" + }, + "user_card": { + "statuses": "Дописи", + "message": "Повідомлення", + "follow": "Підписатись", + "follow_unfollow": "Відписатись", + "follow_again": "Відправити запит знову?", + "follow_sent": "Запит відправлено!", + "blocked": "Заблоковано!", + "admin_menu": { + "activate_account": "Активувати обліковий запис", + "deactivate_account": "Деактивувати обліковий запис", + "delete_account": "Видалити аккаунт", + "moderation": "Модерація", + "delete_user_confirmation": "Ви абсолютно впевнені? Цю дію неможливо буде скасовувати.", + "delete_user": "Видалити обліковий запис", + "strip_media": "Вилучити медіа з дописів користувача", + "force_nsfw": "Позначити всі дописи як NSFW" + }, + "deny": "Відмовити", + "block": "Заблокувати", + "approve": "Схвалити", + "mention": "Згадати", + "unsubscribe": "Відписатись", + "subscribe": "Підписатись", + "report": "Поскаржитись", + "per_day": "на день", + "favorites": "Вподобання", + "media": "Медіа" + }, + "status": { + "copy_link": "Скопіювати посилання на допис", + "status_unavailable": "Допис недоступний", + "replies_list": "Відповіді:", + "delete_confirm": "Ви дійсно хочете видалити цей допис?", + "delete": "Видалити допис", + "pin": "Закріпити в профілі", + "status_deleted": "Цей допис був видалений", + "favorites": "Вподобане", + "hide_content": "Сховати вміст", + "show_content": "Показати вміст", + "hide_full_subject": "Сховати всю тему", + "show_full_subject": "Показати всю тему", + "thread_muted_and_words": ", має слова:", + "mute_conversation": "Заглушити розмову", + "reply_to": "Відповідь", + "unbookmark": "Видалити із закладок", + "bookmark": "Додати до закладок", + "pinned": "Закріплено", + "unpin": "Відкріпити від профілю", + "repeats": "Повтори" + }, + "timeline": { + "no_more_statuses": "Більше немає дописів", + "up_to_date": "Оновлено", + "reload": "Оновити", + "show_new": "Показати нові", + "load_older": "Завантажити давніші дописи", + "error": "Помилка завантаження стрічки: {0}", + "collapse": "Згорнути", + "conversation": "Розмова", + "no_statuses": "Ніяких статусів", + "repeated": "поширив(-ла)", + "no_retweet_hint": "Запис, позначено як \"тільки для відстежувачів\" або \"особисте\" і тому не може бути повторений" + }, + "user_reporting": { + "submit": "Відправити", + "forward_to": "Переслати до {0}", + "forward_description": "Цей обліковий запис належить іншому інстансу. Відправити їм копію скарги?", + "additional_comments": "Додаткове пояснення", + "add_comment_description": "Скарга буде надіслана модераторам вашого інстансу. Нижче Ви можете додати пояснення чому ви вирішили поскаржитись на цей обліковий запис:", + "title": "Поскаржитись на {0}", + "generic_error": "Виникла помилка під час обробки вашого запиту." + }, + "user_profile": { + "profile_loading_error": "Вибачте, під час завантаження цього профілю виникла помилка.", + "profile_does_not_exist": "Вибачте, цей профіль більше не існує." + } +} diff --git a/src/i18n/zh.json b/src/i18n/zh.json index 09e2ab0d..7f8e5593 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -22,7 +22,7 @@ }, "general": { "apply": "应用", - "submit": "提交", + "submit": "发送", "more": "更多", "generic_error": "发生了一个错误", "optional": "可选", @@ -297,7 +297,7 @@ "hide_follows_description": "不要显示我所关注的人", "hide_followers_description": "不要显示关注我的人", "show_admin_badge": "显示管理徽章", - "show_moderator_badge": "显示版主徽章", + "show_moderator_badge": "在我的个人资料中显示监察员标志", "nsfw_clickthrough": "将不和谐附件隐藏,点击才能打开", "oauth_tokens": "OAuth令牌", "token": "令牌", @@ -655,8 +655,8 @@ "moderation": "权限", "grant_admin": "赋予管理权限", "revoke_admin": "撤销管理权限", - "grant_moderator": "赋予版主权限", - "revoke_moderator": "撤销版主权限", + "grant_moderator": "赋予监察员权限", + "revoke_moderator": "撤销监察员权限", "activate_account": "激活账号", "deactivate_account": "关闭账号", "delete_account": "删除账号", @@ -683,7 +683,7 @@ }, "user_reporting": { "title": "报告 {0}", - "add_comment_description": "此报告会发送给您的实例管理员。您可以在下面提供更多详细信息解释报告的缘由:", + "add_comment_description": "此报告会发送给您的实例监察员。您可以在下面提供更多详细信息解释报告的缘由:", "additional_comments": "其它信息", "forward_description": "这个账号是从另外一个服务器。同时发送一个副本到那里?", "forward_to": "转发 {0}", diff --git a/src/modules/config.js b/src/modules/config.js index 444b8ec7..cd088737 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -20,6 +20,7 @@ export const defaultState = { customTheme: undefined, customThemeSource: undefined, hideISP: false, + hideInstanceWallpaper: false, // bad name: actually hides posts of muted USERS hideMutedPosts: undefined, // instance default collapseMessageWithSubject: undefined, // instance default diff --git a/src/modules/statuses.js b/src/modules/statuses.js index e673141d..33c68c57 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -39,8 +39,7 @@ const emptyNotifications = () => ({ minId: Number.POSITIVE_INFINITY, data: [], idStore: {}, - loading: false, - error: false + loading: false }) export const defaultState = () => ({ @@ -50,8 +49,6 @@ export const defaultState = () => ({ maxId: 0, notifications: emptyNotifications(), favorites: new Set(), - error: false, - errorData: null, timelines: { mentions: emptyTl(), public: emptyTl(), @@ -462,18 +459,9 @@ export const mutations = { const newStatus = state.allStatusesObject[id] newStatus.nsfw = nsfw }, - setError (state, { value }) { - state.error = value - }, - setErrorData (state, { value }) { - state.errorData = value - }, setNotificationsLoading (state, { value }) { state.notifications.loading = value }, - setNotificationsError (state, { value }) { - state.notifications.error = value - }, setNotificationsSilence (state, { value }) { state.notifications.desktopNotificationSilence = value }, @@ -588,18 +576,9 @@ const statuses = { } commit('addNewNotifications', { dispatch, notifications, older, rootGetters, newNotificationSideEffects }) }, - setError ({ rootState, commit }, { value }) { - commit('setError', { value }) - }, - setErrorData ({ rootState, commit }, { value }) { - commit('setErrorData', { value }) - }, setNotificationsLoading ({ rootState, commit }, { value }) { commit('setNotificationsLoading', { value }) }, - setNotificationsError ({ rootState, commit }, { value }) { - commit('setNotificationsError', { value }) - }, setNotificationsSilence ({ rootState, commit }, { value }) { commit('setNotificationsSilence', { value }) }, diff --git a/src/modules/users.js b/src/modules/users.js index 9245db5c..655db4c7 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -137,11 +137,11 @@ export const mutations = { }, saveFriendIds (state, { id, friendIds }) { const user = state.usersObject[id] - user.friendIds = uniq(concat(user.friendIds, friendIds)) + user.friendIds = uniq(concat(user.friendIds || [], friendIds)) }, saveFollowerIds (state, { id, followerIds }) { const user = state.usersObject[id] - user.followerIds = uniq(concat(user.followerIds, followerIds)) + user.followerIds = uniq(concat(user.followerIds || [], followerIds)) }, // Because frontend doesn't have a reason to keep these stuff in memory // outside of viewing someones user profile. @@ -202,7 +202,9 @@ export const mutations = { }, setPinnedToUser (state, status) { const user = state.usersObject[status.user.id] + user.pinnedStatusIds = user.pinnedStatusIds || [] const index = user.pinnedStatusIds.indexOf(status.id) + if (status.pinned && index === -1) { user.pinnedStatusIds.push(status.id) } else if (!status.pinned && index !== -1) { diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 22b5e8ba..f4483149 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -162,7 +162,12 @@ const updateProfileImages = ({ credentials, avatar = null, banner = null, backgr body: form }) .then((data) => data.json()) - .then((data) => parseUser(data)) + .then((data) => { + if (data.error) { + throw new Error(data.error) + } + return parseUser(data) + }) } const updateProfile = ({ credentials, params }) => { @@ -560,7 +565,7 @@ const fetchTimeline = ({ }) .then((data) => data.json()) .then((data) => { - if (!data.error) { + if (!data.errors) { return { data: data.map(isNotifications ? parseNotification : parseStatus), pagination } } else { data.status = status diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js index 1fc4e390..e653ebc1 100644 --- a/src/services/chat_service/chat_service.js +++ b/src/services/chat_service/chat_service.js @@ -21,7 +21,7 @@ const clear = (storage) => { failedMessageIds.push(message.id) } else { delete storage.idIndex[message.id] - delete storage.idempotencyKeyIndex[message.id] + delete storage.idempotencyKeyIndex[message.idempotency_key] } } diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 9d09b8d0..b1e68df5 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -2,6 +2,15 @@ import escape from 'escape-html' import parseLinkHeader from 'parse-link-header' import { isStatusNotification } from '../notification_utils/notification_utils.js' +/** NOTICE! ** + * Do not initialize UI-generated data here. + * It will override existing data. + * + * i.e. user.pinnedStatusIds was set to [] here + * UI code would update it with data but upon next user fetch + * it would be reverted back to [] + */ + const qvitterStatusType = (status) => { if (status.is_post_verb) { return 'status' @@ -53,7 +62,7 @@ export const parseUser = (data) => { output.fields = data.fields output.fields_html = data.fields.map(field => { return { - name: addEmojis(field.name, data.emojis), + name: addEmojis(escape(field.name), data.emojis), value: addEmojis(field.value, data.emojis) } }) @@ -173,9 +182,6 @@ export const parseUser = (data) => { output.locked = data.locked output.followers_count = data.followers_count output.statuses_count = data.statuses_count - output.friendIds = [] - output.followerIds = [] - output.pinnedStatusIds = [] if (data.pleroma) { output.follow_request_count = data.pleroma.follow_request_count @@ -274,7 +280,7 @@ export const parseStatus = (data) => { if (output.poll) { output.poll.options = (output.poll.options || []).map(field => ({ ...field, - title_html: addEmojis(field.title, data.emojis) + title_html: addEmojis(escape(field.title), data.emojis) })) } output.pinned = data.pinned diff --git a/src/services/favicon_service/favicon_service.js b/src/services/favicon_service/favicon_service.js new file mode 100644 index 00000000..d1ddee41 --- /dev/null +++ b/src/services/favicon_service/favicon_service.js @@ -0,0 +1,61 @@ +import { find } from 'lodash' + +const createFaviconService = () => { + let favimg, favcanvas, favcontext, favicon + const faviconWidth = 128 + const faviconHeight = 128 + const badgeRadius = 32 + + const initFaviconService = () => { + const nodes = document.getElementsByTagName('link') + favicon = find(nodes, node => node.rel === 'icon') + if (favicon) { + favcanvas = document.createElement('canvas') + favcanvas.width = faviconWidth + favcanvas.height = faviconHeight + favimg = new Image() + favimg.src = favicon.href + favcontext = favcanvas.getContext('2d') + } + } + + const isImageLoaded = (img) => img.complete && img.naturalHeight !== 0 + + const clearFaviconBadge = () => { + if (!favimg || !favcontext || !favicon) return + + favcontext.clearRect(0, 0, faviconWidth, faviconHeight) + if (isImageLoaded(favimg)) { + favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) + } + favicon.href = favcanvas.toDataURL('image/png') + } + + const drawFaviconBadge = () => { + if (!favimg || !favcontext || !favcontext) return + + clearFaviconBadge() + + const style = getComputedStyle(document.body) + const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}` + + if (isImageLoaded(favimg)) { + favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) + } + favcontext.fillStyle = badgeColor + favcontext.beginPath() + favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false) + favcontext.fill() + favicon.href = favcanvas.toDataURL('image/png') + } + + return { + initFaviconService, + clearFaviconBadge, + drawFaviconBadge + } +} + +const FaviconService = createFaviconService() + +export default FaviconService diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index 6ff7d9df..5f9c1e5e 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -2,7 +2,6 @@ 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 }) store.dispatch('addNewNotifications', { notifications, older }) } @@ -50,11 +49,22 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => { const fetchNotifications = ({ store, args, older }) => { return apiService.fetchTimeline(args) - .then(({ data: notifications }) => { + .then((response) => { + if (response.errors) { + throw new Error(`${response.status} ${response.statusText}`) + } + const notifications = response.data update({ store, notifications, older }) return notifications - }, () => store.dispatch('setNotificationsError', { value: true })) - .catch(() => store.dispatch('setNotificationsError', { value: true })) + }) + .catch((error) => { + store.dispatch('pushGlobalNotice', { + level: 'error', + messageKey: 'notifications.error', + messageArgs: [error.message], + timeout: 5000 + }) + }) } const startFetching = ({ credentials, store }) => { diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js index 7ed85797..bec1eebd 100644 --- a/src/services/theme_data/pleromafe.js +++ b/src/services/theme_data/pleromafe.js @@ -84,6 +84,10 @@ export const SLOT_INHERITANCE = { opacity: 'bg', priority: 1 }, + wallpaper: { + depends: ['bg'], + color: (mod, bg) => brightness(-2 * mod, bg).rgb + }, fg: { depends: [], priority: 1 diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 72ea4890..921df3ed 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -6,9 +6,6 @@ import { promiseInterval } from '../promise_interval/promise_interval.js' const update = ({ store, statuses, timeline, showImmediately, userId, pagination }) => { const ccTimeline = camelCase(timeline) - store.dispatch('setError', { value: false }) - store.dispatch('setErrorData', { value: null }) - store.dispatch('addNewStatuses', { timeline: ccTimeline, userId, @@ -52,9 +49,8 @@ const fetchAndUpdate = ({ return apiService.fetchTimeline(args) .then(response => { - if (response.error) { - store.dispatch('setErrorData', { value: response }) - return + if (response.errors) { + throw new Error(`${response.status} ${response.statusText}`) } const { data: statuses, pagination } = response @@ -63,7 +59,15 @@ const fetchAndUpdate = ({ } update({ store, statuses, timeline, showImmediately, userId, pagination }) return { statuses, pagination } - }, () => store.dispatch('setError', { value: true })) + }) + .catch((error) => { + store.dispatch('pushGlobalNotice', { + level: 'error', + messageKey: 'timeline.error', + messageArgs: [error.message], + timeout: 5000 + }) + }) } const startFetching = ({ timeline = 'friends', credentials, store, userId = false, tag = false }) => { |
