diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4c2fd1..d08da09e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] ### Added +- Ability to hide/show repeats from user +- User profile button clutter organized into a menu - Emoji picker - Started changelog anew ### Changed diff --git a/src/App.js b/src/App.js index fe63b54c..04a40e30 100644 --- a/src/App.js +++ b/src/App.js @@ -45,7 +45,7 @@ export default { }), created () { // Load the locale from the storage - this.$i18n.locale = this.$store.state.config.interfaceLanguage + this.$i18n.locale = this.$store.getters.mergedConfig.interfaceLanguage window.addEventListener('resize', this.updateMobileState) }, destroyed () { @@ -93,7 +93,7 @@ export default { suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel && - !this.$store.state.config.hideISP && + !this.$store.getters.mergedConfig.hideISP && this.$store.state.instance.instanceSpecificPanelContent }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, diff --git a/src/App.scss b/src/App.scss index 2190f91a..2d10f1e7 100644 --- a/src/App.scss +++ b/src/App.scss @@ -39,10 +39,13 @@ h4 { text-align: center; } +html { + font-size: 14px; +} + body { font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); - font-size: 14px; margin: 0; color: $fallback--text; color: var(--text, $fallback--text); @@ -705,31 +708,6 @@ nav { } } -@keyframes modal-background-fadein { - from { - background-color: rgba(0, 0, 0, 0); - } - to { - background-color: rgba(0, 0, 0, 0.5); - } -} - -.modal-view { - z-index: 1000; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - justify-content: center; - align-items: center; - overflow: auto; - animation-duration: 0.2s; - background-color: rgba(0, 0, 0, 0.5); - animation-name: modal-background-fadein; -} - .button-icon { font-size: 1.2em; } diff --git a/src/boot/routes.js b/src/boot/routes.js index cd02711c..5670236c 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -47,7 +47,7 @@ export default (store) => { { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, { name: 'settings', path: '/settings', component: Settings }, { name: 'registration', path: '/registration', component: Registration }, - { name: 'password-reset', path: '/password-reset', component: PasswordReset }, + { name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true }, { name: 'registration-token', path: '/registration/:token', component: Registration }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute }, diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js new file mode 100644 index 00000000..204d506a --- /dev/null +++ b/src/components/account_actions/account_actions.js @@ -0,0 +1,35 @@ +import ProgressButton from '../progress_button/progress_button.vue' + +const AccountActions = { + props: [ + 'user' + ], + data () { + return { } + }, + components: { + ProgressButton + }, + methods: { + showRepeats () { + this.$store.dispatch('showReblogs', this.user.id) + }, + hideRepeats () { + this.$store.dispatch('hideReblogs', this.user.id) + }, + blockUser () { + this.$store.dispatch('blockUser', this.user.id) + }, + unblockUser () { + this.$store.dispatch('unblockUser', this.user.id) + }, + reportUser () { + this.$store.dispatch('openUserReportingModal', this.user.id) + }, + mentionUser () { + this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user }) + } + } +} + +export default AccountActions diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue new file mode 100644 index 00000000..046cba93 --- /dev/null +++ b/src/components/account_actions/account_actions.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index e93921fe..06b496b0 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -10,13 +10,14 @@ const Attachment = { 'statusId', 'size', 'allowPlay', - 'setMedia' + 'setMedia', + 'naturalSizeLoad' ], data () { return { nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage, - hideNsfwLocal: this.$store.state.config.hideNsfw, - preloadImage: this.$store.state.config.preloadImage, + hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw, + preloadImage: this.$store.getters.mergedConfig.preloadImage, loading: false, img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'), modalOpen: false, @@ -57,7 +58,7 @@ const Attachment = { } }, openModal (event) { - const modalTypes = this.$store.state.config.playVideosInModal + const modalTypes = this.$store.getters.mergedConfig.playVideosInModal ? ['image', 'video'] : ['image'] if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) || @@ -70,7 +71,7 @@ const Attachment = { } }, toggleHidden (event) { - if (this.$store.state.config.useOneClickNsfw && !this.showHidden) { + if (this.$store.getters.mergedConfig.useOneClickNsfw && !this.showHidden) { this.openModal(event) return } @@ -88,6 +89,11 @@ const Attachment = { } else { this.showHidden = !this.showHidden } + }, + onImageLoad (image) { + const width = image.naturalWidth + const height = image.naturalHeight + this.naturalSizeLoad && this.naturalSizeLoad({ width, height }) } } } diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index af16e302..0748b2f0 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -58,6 +58,7 @@ :referrerpolicy="referrerpolicy" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url" + :image-load-handler="onImageLoad" /> diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue index 2b822ec3..5917598a 100644 --- a/src/components/checkbox/checkbox.vue +++ b/src/components/checkbox/checkbox.vue @@ -1,13 +1,22 @@ @@ -17,7 +26,11 @@ export default { prop: 'checked', event: 'change' }, - props: ['checked', 'indeterminate'] + props: [ + 'checked', + 'indeterminate', + 'disabled' + ] } @@ -27,12 +40,16 @@ export default { .checkbox { position: relative; display: inline-block; - padding-left: 1.2em; min-height: 1.2em; + &-indicator { + position: relative; + padding-left: 1.2em; + } + &-indicator::before { position: absolute; - left: 0; + right: 0; top: 0; display: block; content: '✔'; @@ -54,6 +71,17 @@ export default { box-sizing: border-box; } + &.disabled { + .checkbox-indicator::before, + .label { + opacity: .5; + } + .label { + color: $fallback--faint; + color: var(--faint, $fallback--faint); + } + } + input[type=checkbox] { display: none; @@ -68,9 +96,6 @@ export default { color: var(--text, $fallback--text); } - &:disabled + .checkbox-indicator::before { - opacity: .5; - } } & > span { diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index a586b819..366951c0 100644 --- a/src/components/emoji_input/emoji_input.js +++ b/src/components/emoji_input/emoji_input.js @@ -99,7 +99,7 @@ const EmojiInput = { }, computed: { padEmoji () { - return this.$store.state.config.padEmoji + return this.$store.getters.mergedConfig.padEmoji }, suggestions () { const firstchar = this.textAtCaret.charAt(0) diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js index 824412dd..b1d70176 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -1,3 +1,4 @@ +import Checkbox from '../checkbox/checkbox.vue' const filterByKeyword = (list, keyword = '') => { return list.filter(x => x.displayText.includes(keyword)) @@ -13,7 +14,6 @@ const EmojiPicker = { }, data () { return { - labelKey: String(Math.random() * 100000), keyword: '', activeGroup: 'custom', showingStickers: false, @@ -22,7 +22,8 @@ const EmojiPicker = { } }, components: { - StickerPicker: () => import('../sticker_picker/sticker_picker.vue') + StickerPicker: () => import('../sticker_picker/sticker_picker.vue'), + Checkbox }, methods: { onEmoji (emoji) { diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss index b0ed00e9..d99539b0 100644 --- a/src/components/emoji_picker/emoji_picker.scss +++ b/src/components/emoji_picker/emoji_picker.scss @@ -14,10 +14,6 @@ padding: 7px; line-height: normal; } - .keep-open-label { - padding: 0 7px; - display: flex; - } .heading { display: flex; diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue index 42f20130..b974fce9 100644 --- a/src/components/emoji_picker/emoji_picker.vue +++ b/src/components/emoji_picker/emoji_picker.vue @@ -75,22 +75,10 @@ -
- - +
+ + {{ $t('emoji.keep_open') }} +
- {{ status.fave_num }} + {{ status.fave_num }}
diff --git a/src/components/follow_button/follow_button.js b/src/components/follow_button/follow_button.js new file mode 100644 index 00000000..12da2645 --- /dev/null +++ b/src/components/follow_button/follow_button.js @@ -0,0 +1,53 @@ +import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' +export default { + props: ['user', 'labelFollowing', 'buttonClass'], + data () { + return { + inProgress: false + } + }, + computed: { + isPressed () { + return this.inProgress || this.user.following + }, + title () { + if (this.inProgress || this.user.following) { + return this.$t('user_card.follow_unfollow') + } else if (this.user.requested) { + return this.$t('user_card.follow_again') + } else { + return this.$t('user_card.follow') + } + }, + label () { + if (this.inProgress) { + return this.$t('user_card.follow_progress') + } else if (this.user.following) { + return this.labelFollowing || this.$t('user_card.following') + } else if (this.user.requested) { + return this.$t('user_card.follow_sent') + } else { + return this.$t('user_card.follow') + } + } + }, + methods: { + onClick () { + this.user.following ? this.unfollow() : this.follow() + }, + follow () { + this.inProgress = true + requestFollow(this.user, this.$store).then(() => { + this.inProgress = false + }) + }, + unfollow () { + const store = this.$store + this.inProgress = true + requestUnfollow(this.user, store).then(() => { + this.inProgress = false + store.commit('removeStatus', { timeline: 'friends', userId: this.user.id }) + }) + } + } +} diff --git a/src/components/follow_button/follow_button.vue b/src/components/follow_button/follow_button.vue new file mode 100644 index 00000000..f0cbb94b --- /dev/null +++ b/src/components/follow_button/follow_button.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/components/follow_card/follow_card.js b/src/components/follow_card/follow_card.js index 118d0c7c..aefd609e 100644 --- a/src/components/follow_card/follow_card.js +++ b/src/components/follow_card/follow_card.js @@ -1,20 +1,16 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue' import RemoteFollow from '../remote_follow/remote_follow.vue' -import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' +import FollowButton from '../follow_button/follow_button.vue' const FollowCard = { props: [ 'user', 'noFollowsYou' ], - data () { - return { - inProgress: false - } - }, components: { BasicUserCard, - RemoteFollow + RemoteFollow, + FollowButton }, computed: { isMe () { @@ -23,20 +19,6 @@ const FollowCard = { loggedIn () { return this.$store.state.users.currentUser } - }, - methods: { - followUser () { - this.inProgress = true - requestFollow(this.user, this.$store).then(() => { - this.inProgress = false - }) - }, - unfollowUser () { - this.inProgress = true - requestUnfollow(this.user, this.$store).then(() => { - this.inProgress = false - }) - } } } diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue index a9d237bb..81e6e6dc 100644 --- a/src/components/follow_card/follow_card.vue +++ b/src/components/follow_card/follow_card.vue @@ -16,36 +16,11 @@
diff --git a/src/components/gallery/gallery.js b/src/components/gallery/gallery.js index 7f33a81b..f856fd0a 100644 --- a/src/components/gallery/gallery.js +++ b/src/components/gallery/gallery.js @@ -1,23 +1,18 @@ import Attachment from '../attachment/attachment.vue' -import { chunk, last, dropRight } from 'lodash' +import { chunk, last, dropRight, sumBy } from 'lodash' const Gallery = { - data: () => ({ - width: 500 - }), props: [ 'attachments', 'nsfw', 'setMedia' ], + data () { + return { + sizes: {} + } + }, components: { Attachment }, - mounted () { - this.resize() - window.addEventListener('resize', this.resize) - }, - destroyed () { - window.removeEventListener('resize', this.resize) - }, computed: { rows () { if (!this.attachments) { @@ -33,21 +28,24 @@ const Gallery = { } return rows }, - rowHeight () { - return itemsPerRow => ({ 'height': `${(this.width / (itemsPerRow + 0.6))}px` }) - }, useContainFit () { - return this.$store.state.config.useContainFit + return this.$store.getters.mergedConfig.useContainFit } }, methods: { - resize () { - // Quick optimization to make resizing not always trigger state change, - // only update attachment size in 10px steps - const width = Math.floor(this.$el.getBoundingClientRect().width / 10) * 10 - if (this.width !== width) { - this.width = width - } + onNaturalSizeLoad (id, size) { + this.$set(this.sizes, id, size) + }, + rowStyle (itemsPerRow) { + return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` } + }, + itemStyle (id, row) { + const total = sumBy(row, item => this.getAspectRatio(item.id)) + return { flex: `${this.getAspectRatio(id) / total} 1 0%` } + }, + getAspectRatio (id) { + const size = this.sizes[id] + return size ? size.width / size.height : 1 } } } diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue index 6169d294..7abc2161 100644 --- a/src/components/gallery/gallery.vue +++ b/src/components/gallery/gallery.vue @@ -7,17 +7,21 @@ v-for="(row, index) in rows" :key="index" class="gallery-row" - :style="rowHeight(row.length)" + :style="rowStyle(row.length)" :class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }" > - + @@ -28,15 +32,24 @@ @import '../../_variables.scss'; .gallery-row { - height: 200px; + position: relative; + height: 0; width: 100%; - display: flex; - flex-direction: row; - flex-wrap: nowrap; - align-content: stretch; flex-grow: 1; margin-top: 0.5em; + .gallery-row-inner { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-content: stretch; + } + // FIXME: specificity problem with this and .attachments.attachment // we shouldn't have the need for .image here .attachment.image { diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue index 83df9a0b..1ca22001 100644 --- a/src/components/interface_language_switcher/interface_language_switcher.vue +++ b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -40,7 +40,7 @@ export default { }, language: { - get: function () { return this.$store.state.config.interfaceLanguage }, + get: function () { return this.$store.getters.mergedConfig.interfaceLanguage }, set: function (val) { this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) this.$i18n.locale = val diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js index 10f52fe2..0b574a04 100644 --- a/src/components/login_form/login_form.js +++ b/src/components/login_form/login_form.js @@ -59,6 +59,8 @@ const LoginForm = { if (result.error) { if (result.error === 'mfa_required') { this.requireMFA({ app: app, settings: result }) + } else if (result.identifier === 'password_reset_required') { + this.$router.push({ name: 'password-reset', params: { passwordResetRequested: true } }) } else { this.error = result.error this.focusOnPasswordInput() diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 992d7129..4832abda 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -1,11 +1,13 @@ import StillImage from '../still-image/still-image.vue' import VideoAttachment from '../video_attachment/video_attachment.vue' +import Modal from '../modal/modal.vue' import fileTypeService from '../../services/file_type/file_type.service.js' const MediaModal = { components: { StillImage, - VideoAttachment + VideoAttachment, + Modal }, computed: { showing () { diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 06ced5a1..2597f4e3 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -1,9 +1,8 @@ diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js index c2bb76ee..5a90c31f 100644 --- a/src/components/mobile_nav/mobile_nav.js +++ b/src/components/mobile_nav/mobile_nav.js @@ -63,7 +63,7 @@ const MobileNav = { this.$refs.notifications.markAsSeen() }, onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) { - if (this.$store.state.config.autoLoad && scrollTop + clientHeight >= scrollHeight) { + if (this.$store.getters.mergedConfig.autoLoad && scrollTop + clientHeight >= scrollHeight) { this.$refs.notifications.fetchOlderNotifications() } } diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js index 3e77148a..0ad12bb1 100644 --- a/src/components/mobile_post_status_button/mobile_post_status_button.js +++ b/src/components/mobile_post_status_button/mobile_post_status_button.js @@ -30,7 +30,7 @@ const MobilePostStatusButton = { return this.autohideFloatingPostButton && (this.hidden || this.inputActive) }, autohideFloatingPostButton () { - return !!this.$store.state.config.autohideFloatingPostButton + return !!this.$store.getters.mergedConfig.autohideFloatingPostButton } }, watch: { diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue new file mode 100644 index 00000000..cee24241 --- /dev/null +++ b/src/components/modal/modal.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue index d97ca3aa..006d6373 100644 --- a/src/components/moderation_tools/moderation_tools.vue +++ b/src/components/moderation_tools/moderation_tools.vue @@ -3,9 +3,7 @@ diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index 8e817f3b..7d46eb5a 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -39,7 +39,7 @@ const Notification = { return highlightClass(this.notification.from_profile) }, userStyle () { - const highlight = this.$store.state.config.highlight + const highlight = this.$store.getters.mergedConfig.highlight const user = this.notification.from_profile return highlightStyle(highlight[user.screen_name]) }, diff --git a/src/components/password_reset/password_reset.js b/src/components/password_reset/password_reset.js index fa71e07a..62e74e30 100644 --- a/src/components/password_reset/password_reset.js +++ b/src/components/password_reset/password_reset.js @@ -25,6 +25,12 @@ const passwordReset = { this.$router.push({ name: 'root' }) } }, + props: { + passwordResetRequested: { + default: false, + type: Boolean + } + }, methods: { dismissError () { this.error = null diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue index 00474e95..713c9dce 100644 --- a/src/components/password_reset/password_reset.vue +++ b/src/components/password_reset/password_reset.vue @@ -10,7 +10,10 @@ >
-

+

+ {{ $t('password_reset.password_reset_required_but_mailer_is_disabled') }} +

+

{{ $t('password_reset.password_reset_disabled') }}

@@ -25,6 +28,12 @@
+

+ {{ $t('password_reset.password_reset_required') }} +

{{ $t('password_reset.instruction') }}

@@ -104,6 +113,11 @@ margin: 0.3em 0.0em 1em; } + .password-reset-required { + background-color: var(--alertError, $fallback--alertError); + padding: 10px 0; + } + .notice-dismissible { padding-right: 2rem; } diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss index 279b01be..06daa871 100644 --- a/src/components/popper/popper.scss +++ b/src/components/popper/popper.scss @@ -20,7 +20,6 @@ margin: 5px; border-color: $fallback--bg; border-color: var(--bg, $fallback--bg); - z-index: 1; } &[x-placement^="top"] { @@ -31,7 +30,7 @@ border-left-color: transparent !important; border-right-color: transparent !important; border-bottom-color: transparent !important; - bottom: -5px; + bottom: -4px; left: calc(50% - 5px); margin-top: 0; margin-bottom: 0; @@ -46,7 +45,7 @@ border-left-color: transparent !important; border-right-color: transparent !important; border-top-color: transparent !important; - top: -5px; + top: -4px; left: calc(50% - 5px); margin-top: 0; margin-bottom: 0; @@ -61,7 +60,7 @@ border-left-color: transparent !important; border-top-color: transparent !important; border-bottom-color: transparent !important; - left: -5px; + left: -4px; top: calc(50% - 5px); margin-left: 0; margin-right: 0; @@ -76,7 +75,7 @@ border-top-color: transparent !important; border-right-color: transparent !important; border-bottom-color: transparent !important; - right: -5px; + right: -4px; top: calc(50% - 5px); margin-left: 0; margin-right: 0; diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 9b2a9c90..c1d7209e 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -7,6 +7,8 @@ import fileTypeService from '../../services/file_type/file_type.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js' import { reject, map, uniqBy } from 'lodash' import suggestor from '../emoji_input/suggestor.js' +import { mapGetters } from 'vuex' +import Checkbox from '../checkbox/checkbox.vue' const buildMentionsString = ({ user, attentions = [] }, currentUser) => { let allAttentions = [...attentions] @@ -35,7 +37,8 @@ const PostStatusForm = { MediaUpload, EmojiInput, PollForm, - ScopeSelector + ScopeSelector, + Checkbox }, mounted () { this.resize(this.$refs.textarea) @@ -50,9 +53,7 @@ const PostStatusForm = { const preset = this.$route.query.message let statusText = preset || '' - const scopeCopy = typeof this.$store.state.config.scopeCopy === 'undefined' - ? this.$store.state.instance.scopeCopy - : this.$store.state.config.scopeCopy + const { scopeCopy } = this.$store.getters.mergedConfig if (this.replyTo) { const currentUser = this.$store.state.users.currentUser @@ -63,9 +64,7 @@ const PostStatusForm = { ? this.copyMessageScope : this.$store.state.users.currentUser.default_scope - const contentType = typeof this.$store.state.config.postContentType === 'undefined' - ? this.$store.state.instance.postContentType - : this.$store.state.config.postContentType + const { postContentType: contentType } = this.$store.getters.mergedConfig return { dropFiles: [], @@ -94,10 +93,7 @@ const PostStatusForm = { return this.$store.state.users.currentUser.default_scope }, showAllScopes () { - const minimalScopesMode = typeof this.$store.state.config.minimalScopesMode === 'undefined' - ? this.$store.state.instance.minimalScopesMode - : this.$store.state.config.minimalScopesMode - return !minimalScopesMode + return !this.mergedConfig.minimalScopesMode }, emojiUserSuggestor () { return suggestor({ @@ -145,13 +141,7 @@ const PostStatusForm = { return this.$store.state.instance.minimalScopesMode }, alwaysShowSubject () { - if (typeof this.$store.state.config.alwaysShowSubjectInput !== 'undefined') { - return this.$store.state.config.alwaysShowSubjectInput - } else if (typeof this.$store.state.instance.alwaysShowSubjectInput !== 'undefined') { - return this.$store.state.instance.alwaysShowSubjectInput - } else { - return true - } + return this.mergedConfig.alwaysShowSubjectInput }, postFormats () { return this.$store.state.instance.postFormats || [] @@ -164,13 +154,14 @@ const PostStatusForm = { this.$store.state.instance.pollLimits.max_options >= 2 }, hideScopeNotice () { - return this.$store.state.config.hideScopeNotice + return this.$store.getters.mergedConfig.hideScopeNotice }, pollContentError () { return this.pollFormVisible && this.newStatus.poll && this.newStatus.poll.error - } + }, + ...mapGetters(['mergedConfig']) }, methods: { postStatus (newStatus) { diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 4916d988..237ed725 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -261,12 +261,9 @@ v-if="newStatus.files.length > 0" class="upload_settings" > - - + + {{ $t('post_status.attachments_sensitive') }} +
diff --git a/src/components/post_status_modal/post_status_modal.js b/src/components/post_status_modal/post_status_modal.js index 38258296..b44354db 100644 --- a/src/components/post_status_modal/post_status_modal.js +++ b/src/components/post_status_modal/post_status_modal.js @@ -1,9 +1,11 @@ import PostStatusForm from '../post_status_form/post_status_form.vue' +import Modal from '../modal/modal.vue' import get from 'lodash/get' const PostStatusModal = { components: { - PostStatusForm + PostStatusForm, + Modal }, data () { return { diff --git a/src/components/post_status_modal/post_status_modal.vue b/src/components/post_status_modal/post_status_modal.vue index d3a82389..dbcd321e 100644 --- a/src/components/post_status_modal/post_status_modal.vue +++ b/src/components/post_status_modal/post_status_modal.vue @@ -1,14 +1,11 @@ diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js index 02e98f19..e48fef47 100644 --- a/src/components/still-image/still-image.js +++ b/src/components/still-image/still-image.js @@ -3,11 +3,12 @@ const StillImage = { 'src', 'referrerpolicy', 'mimetype', - 'imageLoadError' + 'imageLoadError', + 'imageLoadHandler' ], data () { return { - stopGifs: this.$store.state.config.stopGifs + stopGifs: this.$store.getters.mergedConfig.stopGifs } }, computed: { @@ -17,6 +18,7 @@ const StillImage = { }, methods: { onLoad () { + this.imageLoadHandler && this.imageLoadHandler(this.$refs.src) const canvas = this.$refs.canvas if (!canvas) return const width = this.$refs.src.naturalWidth diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index 8c3d4861..6e2a1d7b 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -10,6 +10,7 @@ import ContrastRatio from '../contrast_ratio/contrast_ratio.vue' import TabSwitcher from '../tab_switcher/tab_switcher.js' import Preview from './preview.vue' import ExportImport from '../export_import/export_import.vue' +import Checkbox from '../checkbox/checkbox.vue' // List of color values used in v1 const v1OnlyNames = [ @@ -27,7 +28,7 @@ export default { data () { return { availableStyles: [], - selected: this.$store.state.config.theme, + selected: this.$store.getters.mergedConfig.theme, previewShadows: {}, previewColors: {}, @@ -111,7 +112,7 @@ export default { }) }, mounted () { - this.normalizeLocalState(this.$store.state.config.customTheme) + this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme) if (typeof this.shadowSelected === 'undefined') { this.shadowSelected = this.shadowsAvailable[0] } @@ -338,7 +339,8 @@ export default { FontControl, TabSwitcher, Preview, - ExportImport + ExportImport, + Checkbox }, methods: { setCustomTheme () { @@ -365,9 +367,9 @@ export default { return version >= 1 || version <= 2 }, clearAll () { - const state = this.$store.state.config.customTheme + const state = this.$store.getters.mergedConfig.customTheme const version = state.colors ? 2 : 'l1' - this.normalizeLocalState(this.$store.state.config.customTheme, version) + this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme, version) }, // Clears all the extra stuff when loading V1 theme diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue index d24394a4..944debab 100644 --- a/src/components/style_switcher/style_switcher.vue +++ b/src/components/style_switcher/style_switcher.vue @@ -42,44 +42,29 @@
- - + + {{ $t('settings.style.switcher.keep_color') }} + - - + + {{ $t('settings.style.switcher.keep_shadows') }} + - - + + {{ $t('settings.style.switcher.keep_opacity') }} + - - + + {{ $t('settings.style.switcher.keep_roundness') }} + - - + + {{ $t('settings.style.switcher.keep_fonts') }} +

{{ $t('settings.style.switcher.save_load_hint') }}

diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 0594576c..27a9a55e 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -141,7 +141,7 @@ const Timeline = { const bodyBRect = document.body.getBoundingClientRect() const height = Math.max(bodyBRect.height, -(bodyBRect.y)) if (this.timeline.loading === false && - this.$store.state.config.autoLoad && + this.$store.getters.mergedConfig.autoLoad && this.$el.offsetHeight > 0 && (window.innerHeight + window.pageYOffset) >= (height - 750)) { this.fetchOlderStatuses() @@ -153,7 +153,7 @@ const Timeline = { }, watch: { newStatusCount (count) { - if (!this.$store.state.config.streaming) { + if (!this.$store.getters.mergedConfig.streaming) { return } if (count > 0) { @@ -162,7 +162,7 @@ const Timeline = { const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0) if (top < 15 && !this.paused && - !(this.unfocused && this.$store.state.config.pauseOnUnfocused) + !(this.unfocused && this.$store.getters.mergedConfig.pauseOnUnfocused) ) { this.showNewStatuses() } else { diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 9c931c01..cc8a1ed6 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -1,19 +1,20 @@ import UserAvatar from '../user_avatar/user_avatar.vue' import RemoteFollow from '../remote_follow/remote_follow.vue' import ProgressButton from '../progress_button/progress_button.vue' +import FollowButton from '../follow_button/follow_button.vue' import ModerationTools from '../moderation_tools/moderation_tools.vue' +import AccountActions from '../account_actions/account_actions.vue' import { hex2rgb } from '../../services/color_convert/color_convert.js' -import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import { mapGetters } from 'vuex' export default { - props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar' ], + props: [ + 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar' + ], data () { return { followRequestInProgress: false, - hideUserStatsLocal: typeof this.$store.state.config.hideUserStats === 'undefined' - ? this.$store.state.instance.hideUserStats - : this.$store.state.config.hideUserStats, betterShadow: this.$store.state.interface.browserSupport.cssFilter } }, @@ -29,9 +30,9 @@ export default { }] }, style () { - const color = this.$store.state.config.customTheme.colors - ? this.$store.state.config.customTheme.colors.bg // v2 - : this.$store.state.config.colors.bg // v1 + const color = this.$store.getters.mergedConfig.customTheme.colors + ? this.$store.getters.mergedConfig.customTheme.colors.bg // v2 + : this.$store.getters.mergedConfig.colors.bg // v1 if (color) { const rgb = (typeof color === 'string') ? hex2rgb(color) : color @@ -63,21 +64,22 @@ export default { }, userHighlightType: { get () { - const data = this.$store.state.config.highlight[this.user.screen_name] + const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name] return (data && data.type) || 'disabled' }, set (type) { - const data = this.$store.state.config.highlight[this.user.screen_name] + const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name] if (type !== 'disabled') { this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: (data && data.color) || '#FFFFFF', type }) } else { this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: undefined }) } - } + }, + ...mapGetters(['mergedConfig']) }, userHighlightColor: { get () { - const data = this.$store.state.config.highlight[this.user.screen_name] + const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name] return data && data.color }, set (color) { @@ -90,36 +92,18 @@ export default { const validRole = rights.admin || rights.moderator const roleTitle = rights.admin ? 'admin' : 'moderator' return validRole && roleTitle - } + }, + ...mapGetters(['mergedConfig']) }, components: { UserAvatar, RemoteFollow, ModerationTools, - ProgressButton + AccountActions, + ProgressButton, + FollowButton }, methods: { - followUser () { - const store = this.$store - this.followRequestInProgress = true - requestFollow(this.user, store).then(() => { - this.followRequestInProgress = false - }) - }, - unfollowUser () { - const store = this.$store - this.followRequestInProgress = true - requestUnfollow(this.user, store).then(() => { - this.followRequestInProgress = false - store.commit('removeStatus', { timeline: 'friends', userId: this.user.id }) - }) - }, - blockUser () { - this.$store.dispatch('blockUser', this.user.id) - }, - unblockUser () { - this.$store.dispatch('unblockUser', this.user.id) - }, muteUser () { this.$store.dispatch('muteUser', this.user.id) }, @@ -147,10 +131,10 @@ export default { } }, userProfileLink (user) { - return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) - }, - reportUser () { - this.$store.dispatch('openUserReportingModal', this.user.id) + return generateProfileLink( + user.id, user.screen_name, + this.$store.state.instance.restrictedNicknames + ) }, zoomAvatar () { const attachment = { @@ -159,9 +143,6 @@ export default { } this.$store.dispatch('setMedia', [attachment]) this.$store.dispatch('setCurrent', attachment) - }, - mentionUser () { - this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user }) } } } diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 5b6f66e7..6f3c958e 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -66,8 +66,11 @@ > + -
{{ visibleRole }} {{ dailyAvg }} {{ $t('user_card.per_day') }}
@@ -135,72 +138,27 @@ v-if="loggedIn && isOtherUser" class="user-interactions" > -
- +
+ +
-
- -
-
- - - - - - - -
- -
- -
-
- -
- - -
- -
- -
-
* { - flex: 1 0 0; margin: 0 .75em .6em 0; white-space: nowrap; + min-width: 95px; } button { diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js index 7c6ea409..833fa98a 100644 --- a/src/components/user_reporting_modal/user_reporting_modal.js +++ b/src/components/user_reporting_modal/user_reporting_modal.js @@ -2,12 +2,14 @@ import Status from '../status/status.vue' import List from '../list/list.vue' import Checkbox from '../checkbox/checkbox.vue' +import Modal from '../modal/modal.vue' const UserReportingModal = { components: { Status, List, - Checkbox + Checkbox, + Modal }, data () { return { diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue index c79a3707..6ee53461 100644 --- a/src/components/user_reporting_modal/user_reporting_modal.vue +++ b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -1,13 +1,9 @@ diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index f12cccae..32eb802e 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -17,6 +17,7 @@ import Autosuggest from '../autosuggest/autosuggest.vue' import Importer from '../importer/importer.vue' import Exporter from '../exporter/exporter.vue' import withSubscription from '../../hocs/with_subscription/with_subscription' +import Checkbox from '../checkbox/checkbox.vue' import Mfa from './mfa.vue' const BlockList = withSubscription({ @@ -82,7 +83,8 @@ const UserSettings = { ProgressButton, Importer, Exporter, - Mfa + Mfa, + Checkbox }, computed: { user () { diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index ef75ac52..adf11907 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -53,12 +53,9 @@ />

- - + + {{ $t('settings.lock_account_description') }} +

@@ -75,69 +72,52 @@

- - + + {{ $t('settings.no_rich_text_description') }} +

- - + + {{ $t('settings.hide_follows_description') }} +

- - + > + {{ $t('settings.hide_follows_count_description') }} +

- - + {{ $t('settings.hide_followers_description') }} +

- - + {{ $t('settings.hide_followers_count_description') }} +

- - - + + + +

- - + + {{ $t('settings.discoverable') }} +

diff --git a/src/components/video_attachment/video_attachment.js b/src/components/video_attachment/video_attachment.js index 76b19a02..f0ca7e89 100644 --- a/src/components/video_attachment/video_attachment.js +++ b/src/components/video_attachment/video_attachment.js @@ -3,7 +3,7 @@ const VideoAttachment = { props: ['attachment', 'controls'], data () { return { - loopVideo: this.$store.state.config.loopVideo + loopVideo: this.$store.getters.mergedConfig.loopVideo } }, methods: { @@ -12,16 +12,16 @@ const VideoAttachment = { if (typeof target.webkitAudioDecodedByteCount !== 'undefined') { // non-zero if video has audio track if (target.webkitAudioDecodedByteCount > 0) { - this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly + this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly } } else if (typeof target.mozHasAudio !== 'undefined') { // true if video has audio track if (target.mozHasAudio) { - this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly + this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly } } else if (typeof target.audioTracks !== 'undefined') { if (target.audioTracks.length > 0) { - this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly + this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly } } } diff --git a/src/directives/body_scroll_lock.js b/src/directives/body_scroll_lock.js index 6ab20c3f..13a6de1c 100644 --- a/src/directives/body_scroll_lock.js +++ b/src/directives/body_scroll_lock.js @@ -2,42 +2,49 @@ import * as bodyScrollLock from 'body-scroll-lock' let previousNavPaddingRight let previousAppBgWrapperRight +const lockerEls = new Set([]) const disableBodyScroll = (el) => { const scrollBarGap = window.innerWidth - document.documentElement.clientWidth bodyScrollLock.disableBodyScroll(el, { reserveScrollBarGap: true }) + lockerEls.add(el) setTimeout(() => { - // If previousNavPaddingRight is already set, don't set it again. - if (previousNavPaddingRight === undefined) { - const navEl = document.getElementById('nav') - previousNavPaddingRight = window.getComputedStyle(navEl).getPropertyValue('padding-right') - navEl.style.paddingRight = previousNavPaddingRight ? `calc(${previousNavPaddingRight} + ${scrollBarGap}px)` : `${scrollBarGap}px` + if (lockerEls.size <= 1) { + // If previousNavPaddingRight is already set, don't set it again. + if (previousNavPaddingRight === undefined) { + const navEl = document.getElementById('nav') + previousNavPaddingRight = window.getComputedStyle(navEl).getPropertyValue('padding-right') + navEl.style.paddingRight = previousNavPaddingRight ? `calc(${previousNavPaddingRight} + ${scrollBarGap}px)` : `${scrollBarGap}px` + } + // If previousAppBgWrapeprRight is already set, don't set it again. + if (previousAppBgWrapperRight === undefined) { + const appBgWrapperEl = document.getElementById('app_bg_wrapper') + previousAppBgWrapperRight = window.getComputedStyle(appBgWrapperEl).getPropertyValue('right') + appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px` + } + document.body.classList.add('scroll-locked') } - // If previousAppBgWrapeprRight is already set, don't set it again. - if (previousAppBgWrapperRight === undefined) { - const appBgWrapperEl = document.getElementById('app_bg_wrapper') - previousAppBgWrapperRight = window.getComputedStyle(appBgWrapperEl).getPropertyValue('right') - appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px` - } - document.body.classList.add('scroll-locked') }) } const enableBodyScroll = (el) => { + lockerEls.delete(el) setTimeout(() => { - if (previousNavPaddingRight !== undefined) { - document.getElementById('nav').style.paddingRight = previousNavPaddingRight - // Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again. - previousNavPaddingRight = undefined + if (lockerEls.size === 0) { + if (previousNavPaddingRight !== undefined) { + document.getElementById('nav').style.paddingRight = previousNavPaddingRight + // Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again. + previousNavPaddingRight = undefined + } + if (previousAppBgWrapperRight !== undefined) { + document.getElementById('app_bg_wrapper').style.right = previousAppBgWrapperRight + // Restore previousAppBgWrapperRight to undefined so disableBodyScroll knows it can be set again. + previousAppBgWrapperRight = undefined + } + document.body.classList.remove('scroll-locked') } - if (previousAppBgWrapperRight !== undefined) { - document.getElementById('app_bg_wrapper').style.right = previousAppBgWrapperRight - // Restore previousAppBgWrapperRight to undefined so disableBodyScroll knows it can be set again. - previousAppBgWrapperRight = undefined - } - document.body.classList.remove('scroll-locked') }) bodyScrollLock.enableBodyScroll(el) } diff --git a/src/i18n/en.json b/src/i18n/en.json index 32c25e3e..d11b2d14 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -555,6 +555,8 @@ "unmute": "Unmute", "unmute_progress": "Unmuting...", "mute_progress": "Muting...", + "hide_repeats": "Hide repeats", + "show_repeats": "Show repeats", "admin_menu": { "moderation": "Moderation", "grant_admin": "Grant Admin", @@ -630,6 +632,8 @@ "return_home": "Return to the home page", "not_found": "We couldn't find that email or username.", "too_many_requests": "You have reached the limit of attempts, try again later.", - "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator." + "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator.", + "password_reset_required": "You must reset your password to log in.", + "password_reset_required_but_mailer_is_disabled": "You must reset your password, but password reset is disabled. Please contact your instance administrator." } } diff --git a/src/i18n/es.json b/src/i18n/es.json index 53a3b9dd..163eb707 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -45,8 +45,8 @@ "error": "Se ha producido un error al importar el archivo." }, "login": { - "login": "Identificación", - "description": "Identificación con OAuth", + "login": "Identificarse", + "description": "Identificarse con OAuth", "logout": "Cerrar sesión", "password": "Contraseña", "placeholder": "p.ej. lain", diff --git a/src/i18n/eu.json b/src/i18n/eu.json index ad8f4c05..90bc13a4 100644 --- a/src/i18n/eu.json +++ b/src/i18n/eu.json @@ -68,6 +68,7 @@ }, "nav": { "about": "Honi buruz", + "administration": "Administrazioa", "back": "Atzera", "chat": "Txat lokala", "friend_requests": "Jarraitzeko eskaerak", @@ -106,6 +107,15 @@ "expired": "Inkesta {0} bukatu zen", "not_enough_options": "Aukera gutxiegi inkestan" }, + "emoji": { + "stickers": "Pegatinak", + "emoji": "Emoji", + "keep_open": "Mantendu hautatzailea zabalik", + "search_emoji": "Bilatu emoji bat", + "add_emoji": "Emoji bat gehitu", + "custom": "Ohiko emojiak", + "unicode": "Unicode emojiak" + }, "stickers": { "add_sticker": "Pegatina gehitu" }, @@ -199,12 +209,12 @@ "avatarRadius": "Avatarrak", "background": "Atzeko planoa", "bio": "Biografia", - "block_export": "Bloke esportatzea", - "block_export_button": "Esportatu zure blokeak csv fitxategi batera", - "block_import": "Bloke inportazioa", - "block_import_error": "Errorea blokeak inportatzen", - "blocks_imported": "Blokeak inportaturik! Hauek prozesatzeak denbora hartuko du.", - "blocks_tab": "Blokeak", + "block_export": "Blokeatu dituzunak esportatu", + "block_export_button": "Esportatu blokeatutakoak csv fitxategi batera", + "block_import": "Blokeatu dituzunak inportatu", + "block_import_error": "Errorea blokeatutakoak inportatzen", + "blocks_imported": "Blokeatutakoak inportaturik! Hauek prozesatzeak denbora hartuko du.", + "blocks_tab": "Blokeatutakoak", "btnRadius": "Botoiak", "cBlue": "Urdina (erantzun, jarraitu)", "cGreen": "Berdea (Bertxiotu)", @@ -222,7 +232,9 @@ "data_import_export_tab": "Datuak Inportatu / Esportatu", "default_vis": "Lehenetsitako ikusgaitasunak", "delete_account": "Ezabatu kontua", + "discoverable": "Baimendu zure kontua kanpo bilaketa-emaitzetan eta bestelako zerbitzuetan agertzea", "delete_account_description": "Betirako ezabatu zure kontua eta zure mezu guztiak", + "pad_emoji": "Zuriuneak gehitu emoji bat aukeratzen denean", "delete_account_error": "Arazo bat gertatu da zure kontua ezabatzerakoan. Arazoa jarraitu eskero, administratzailearekin harremanetan jarri.", "delete_account_instructions": "Idatzi zure pasahitza kontua ezabatzeko.", "avatar_size_instruction": "Avatar irudien gomendatutako gutxieneko tamaina 150x150 pixel dira.", @@ -254,7 +266,7 @@ "instance_default": "(lehenetsia: {value})", "instance_default_simple": "(lehenetsia)", "interface": "Interfazea", - "interfaceLanguage": "Interfaze hizkuntza", + "interfaceLanguage": "Interfazearen hizkuntza", "invalid_theme_imported": "Hautatutako fitxategia ez da onartutako Pleroma gaia. Ez da zure gaian aldaketarik burutu.", "limited_availability": "Ez dago erabilgarri zure nabigatzailean", "links": "Estekak", @@ -277,6 +289,8 @@ "no_mutes": "Ez daude erabiltzaile mututuak", "hide_follows_description": "Ez erakutsi nor jarraitzen ari naizen", "hide_followers_description": "Ez erakutsi nor ari den ni jarraitzen", + "hide_follows_count_description": "Ez erakutsi jarraitzen ari naizen kontuen kopurua", + "hide_followers_count_description": "Ez erakutsi nire jarraitzaileen kontuen kopurua", "show_admin_badge": "Erakutsi Administratzaile etiketa nire profilan", "show_moderator_badge": "Erakutsi Moderatzaile etiketa nire profilan", "nsfw_clickthrough": "Gaitu klika hunkigarri eranskinak ezkutatzeko", @@ -449,7 +463,7 @@ }, "version": { "title": "Bertsioa", - "backend_version": "Backend Bertsio", + "backend_version": "Backend Bertsioa", "frontend_version": "Frontend Bertsioa" } }, @@ -529,6 +543,7 @@ "follows_you": "Jarraitzen dizu!", "its_you": "Zu zara!", "media": "Multimedia", + "mention": "Aipatu", "mute": "Isilarazi", "muted": "Isilduta", "per_day": "eguneko", diff --git a/src/main.js b/src/main.js index 7923ffe8..a9db1cff 100644 --- a/src/main.js +++ b/src/main.js @@ -41,7 +41,13 @@ Vue.use(VueChatScroll) Vue.use(VueClickOutside) Vue.use(PortalVue) Vue.use(VBodyScrollLock) -Vue.use(VTooltip) +Vue.use(VTooltip, { + popover: { + defaultTrigger: 'hover click', + defaultContainer: false, + defaultOffset: 5 + } +}) const i18n = new VueI18n({ // By default, use the browser locale, we will update it if neccessary diff --git a/src/modules/config.js b/src/modules/config.js index cf04d14f..78314118 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -3,8 +3,9 @@ import { setPreset, applyTheme } from '../services/style_setter/style_setter.js' const browserLocale = (window.navigator.language || 'en').split('-')[0] -const defaultState = { +export const defaultState = { colors: {}, + // bad name: actually hides posts of muted USERS hideMutedPosts: undefined, // instance default collapseMessageWithSubject: undefined, // instance default padEmoji: true, @@ -37,11 +38,37 @@ const defaultState = { subjectLineBehavior: undefined, // instance default alwaysShowSubjectInput: undefined, // instance default postContentType: undefined, // instance default - minimalScopesMode: undefined // instance default + minimalScopesMode: undefined, // instance default + // This hides statuses filtered via a word filter + hideFilteredStatuses: undefined, // instance default + playVideosInModal: false, + useOneClickNsfw: false, + useContainFit: false, + hidePostStats: undefined, // instance default + hideUserStats: undefined // instance default } +// caching the instance default properties +export const instanceDefaultProperties = Object.entries(defaultState) + .filter(([key, value]) => value === undefined) + .map(([key, value]) => key) + const config = { state: defaultState, + getters: { + mergedConfig (state, getters, rootState, rootGetters) { + const { instance } = rootState + return { + ...state, + ...instanceDefaultProperties + .map(key => [key, state[key] === undefined + ? instance[key] + : state[key] + ]) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) + } + } + }, mutations: { setOption (state, { name, value }) { set(state, name, value) diff --git a/src/modules/instance.js b/src/modules/instance.js index 7d602aa1..0c1235ca 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -1,5 +1,6 @@ import { set } from 'vue' import { setPreset } from '../services/style_setter/style_setter.js' +import { instanceDefaultProperties } from './config.js' const defaultState = { // Stuff from static/config.json and apiConfig @@ -72,6 +73,13 @@ const instance = { } } }, + getters: { + instanceDefaultConfig (state) { + return instanceDefaultProperties + .map(key => [key, state[key]]) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) + } + }, actions: { setInstanceOption ({ commit, dispatch }, { name, value }) { commit('setInstanceOption', { name, value }) diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 918065d2..f11ffdcd 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -537,6 +537,10 @@ const statuses = { setNotificationsSilence ({ rootState, commit }, { value }) { commit('setNotificationsSilence', { value }) }, + fetchStatus ({ rootState, dispatch }, id) { + rootState.api.backendInteractor.fetchStatus({ id }) + .then((status) => dispatch('addNewStatuses', { statuses: [status] })) + }, deleteStatus ({ rootState, commit }, status) { commit('setDeleted', { status }) apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials }) diff --git a/src/modules/users.js b/src/modules/users.js index 4d02f8d7..6d259dc2 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -60,6 +60,18 @@ const unmuteUser = (store, id) => { .then((relationship) => store.commit('updateUserRelationship', [relationship])) } +const hideReblogs = (store, userId) => { + return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: false }) + .then((relationship) => { + store.commit('updateUserRelationship', [relationship]) + }) +} + +const showReblogs = (store, userId) => { + return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: true }) + .then((relationship) => store.commit('updateUserRelationship', [relationship])) +} + export const mutations = { setMuted (state, { user: { id }, muted }) { const user = state.usersObject[id] @@ -135,6 +147,7 @@ export const mutations = { user.muted = relationship.muting user.statusnet_blocking = relationship.blocking user.subscribed = relationship.subscribing + user.showing_reblogs = relationship.showing_reblogs } }) }, @@ -272,6 +285,12 @@ const users = { unmuteUser (store, id) { return unmuteUser(store, id) }, + hideReblogs (store, id) { + return hideReblogs(store, id) + }, + showReblogs (store, id) { + return showReblogs(store, id) + }, muteUsers (store, ids = []) { return Promise.all(ids.map(id => muteUser(store, id))) }, diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 887d7d7a..61cd4f16 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -219,10 +219,16 @@ const authHeaders = (accessToken) => { } } -const followUser = ({ id, credentials }) => { +const followUser = ({ id, credentials, ...options }) => { let url = MASTODON_FOLLOW_URL(id) + const form = {} + if (options.reblogs !== undefined) { form['reblogs'] = options.reblogs } return fetch(url, { - headers: authHeaders(credentials), + body: JSON.stringify(form), + headers: { + ...authHeaders(credentials), + 'Content-Type': 'application/json' + }, method: 'POST' }).then((data) => data.json()) } diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 3c44a10c..cbf48ee4 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -31,8 +31,8 @@ const backendInteractorService = credentials => { return apiService.fetchUserRelationship({ id, credentials }) } - const followUser = (id) => { - return apiService.followUser({ credentials, id }) + const followUser = ({ id, reblogs }) => { + return apiService.followUser({ credentials, id, reblogs }) } const unfollowUser = (id) => { diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 67664af8..5f45660d 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -69,6 +69,7 @@ export const parseUser = (data) => { output.following = relationship.following output.statusnet_blocking = relationship.blocking output.muted = relationship.muting + output.showing_reblogs = relationship.showing_reblogs output.subscribed = relationship.subscribing } diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js index d82ce593..598cb5f7 100644 --- a/src/services/follow_manipulate/follow_manipulate.js +++ b/src/services/follow_manipulate/follow_manipulate.js @@ -14,7 +14,7 @@ const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => { }) export const requestFollow = (user, store) => new Promise((resolve, reject) => { - store.state.api.backendInteractor.followUser(user.id) + store.state.api.backendInteractor.followUser({ id: user.id }) .then((updated) => { store.commit('updateUserRelationship', [updated]) diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index b6c4cf80..47008026 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -8,11 +8,10 @@ const update = ({ store, notifications, older }) => { const fetchAndUpdate = ({ store, credentials, older = false }) => { const args = { credentials } + const { getters } = store const rootState = store.rootState || store.state const timelineData = rootState.statuses.notifications - const hideMutedPosts = typeof rootState.config.hideMutedPosts === 'undefined' - ? rootState.instance.hideMutedPosts - : rootState.config.hideMutedPosts + const hideMutedPosts = getters.mergedConfig.hideMutedPosts args['withMuted'] = !hideMutedPosts diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index f72688f8..9eb30c2d 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -15,13 +15,21 @@ const update = ({ store, statuses, timeline, showImmediately, userId }) => { }) } -const fetchAndUpdate = ({ store, credentials, timeline = 'friends', older = false, showImmediately = false, userId = false, tag = false, until }) => { +const fetchAndUpdate = ({ + store, + credentials, + timeline = 'friends', + older = false, + showImmediately = false, + userId = false, + tag = false, + until +}) => { const args = { timeline, credentials } const rootState = store.rootState || store.state + const { getters } = store const timelineData = rootState.statuses.timelines[camelCase(timeline)] - const hideMutedPosts = typeof rootState.config.hideMutedPosts === 'undefined' - ? rootState.instance.hideMutedPosts - : rootState.config.hideMutedPosts + const hideMutedPosts = getters.mergedConfig.hideMutedPosts if (older) { args['until'] = until || timelineData.minId diff --git a/test/unit/specs/components/emoji_input.spec.js b/test/unit/specs/components/emoji_input.spec.js index 368d623d..b1b98802 100644 --- a/test/unit/specs/components/emoji_input.spec.js +++ b/test/unit/specs/components/emoji_input.spec.js @@ -12,8 +12,8 @@ const generateInput = (value, padEmoji = true) => { }, mocks: { $store: { - state: { - config: { + getters: { + mergedConfig: { padEmoji } } diff --git a/test/unit/specs/components/user_profile.spec.js b/test/unit/specs/components/user_profile.spec.js index 6de9491a..1b6a47d7 100644 --- a/test/unit/specs/components/user_profile.spec.js +++ b/test/unit/specs/components/user_profile.spec.js @@ -18,7 +18,14 @@ const actions = { } const testGetters = { - findUser: state => getters.findUser(state.users) + findUser: state => getters.findUser(state.users), + mergedConfig: state => ({ + colors: '', + highlight: {}, + customTheme: { + colors: [] + } + }) } const localUser = { @@ -45,13 +52,6 @@ const externalProfileStore = new Vuex.Store({ interface: { browserSupport: '' }, - config: { - colors: '', - highlight: {}, - customTheme: { - colors: [] - } - }, instance: { hideUserStats: true },