diff options
| author | Henry Jameson <me@hjkos.com> | 2019-11-08 19:48:31 +0200 |
|---|---|---|
| committer | Henry Jameson <me@hjkos.com> | 2019-11-08 19:48:31 +0200 |
| commit | 0dcb696e26dfcf97ad46e533ed4797bc9364fc01 (patch) | |
| tree | 2d7b5e7d43b92eb36a9b170e9c5e32dda5d2b7b4 /src/components | |
| parent | 6ade3be5b40816f8765e657442aa2945e51ce7d4 (diff) | |
| parent | 632773ba91ef021dd589ab4d2037f5e0ed7ca5b2 (diff) | |
Merge remote-tracking branch 'upstream/develop' into emoji-optimizations
* upstream/develop: (95 commits)
Lightbox/modal multi image improvements - #381
'/api/pleroma/profile/mfa' -> '/api/pleroma/accounts/mfa'
Add ability to change user's email
translations-de-batch-1
eu-translate update
profile-banner rounding css, fixes #690
fix indentation
remove needless ref
show preview popover when hover numbered replies
refactor conditions
do not make too many nested div
add fetchStatus action
refactor status loading logic
split status preview popover into a separate component
uninstall mobile-detect library
listen both events
minor css fix
restrict distance at top side only
set different trigger event in desktop and mobile by default
fix eslint warnings
...
Diffstat (limited to 'src/components')
52 files changed, 994 insertions, 1027 deletions
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 @@ +<template> + <div class="account-actions"> + <v-popover + trigger="click" + class="account-tools-popover" + :container="false" + placement="bottom-end" + :offset="5" + > + <div slot="popover"> + <div class="dropdown-menu"> + <button + class="btn btn-default btn-block dropdown-item" + @click="mentionUser" + > + {{ $t('user_card.mention') }} + </button> + <template v-if="user.following"> + <div + role="separator" + class="dropdown-divider" + /> + <button + v-if="user.showing_reblogs" + class="btn btn-default dropdown-item" + @click="hideRepeats" + > + {{ $t('user_card.hide_repeats') }} + </button> + <button + v-if="!user.showing_reblogs" + class="btn btn-default dropdown-item" + @click="showRepeats" + > + {{ $t('user_card.show_repeats') }} + </button> + </template> + <div + role="separator" + class="dropdown-divider" + /> + <button + v-if="user.statusnet_blocking" + class="btn btn-default btn-block dropdown-item" + @click="unblockUser" + > + {{ $t('user_card.unblock') }} + </button> + <button + v-else + class="btn btn-default btn-block dropdown-item" + @click="blockUser" + > + {{ $t('user_card.block') }} + </button> + <button + class="btn btn-default btn-block dropdown-item" + @click="reportUser" + > + {{ $t('user_card.report') }} + </button> + </div> + </div> + <div class="btn btn-default ellipsis-button"> + <i class="icon-ellipsis trigger-button" /> + </div> + </v-popover> + </div> +</template> + +<script src="./account_actions.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; +@import '../popper/popper.scss'; +.account-actions { + margin: 0 .8em; +} + +.account-actions button.dropdown-item { + margin-left: 0; +} +.account-actions .trigger-button { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + opacity: .8; + cursor: pointer; + &:hover { + color: $fallback--text; + color: var(--text, $fallback--text); + } +} +</style> 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" /> </a> 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 @@ <template> - <label class="checkbox"> + <label + class="checkbox" + :class="{ disabled, indeterminate }" + > <input type="checkbox" + :disabled="disabled" :checked="checked" :indeterminate.prop="indeterminate" @change="$emit('change', $event.target.checked)" > <i class="checkbox-indicator" /> - <span v-if="!!$slots.default"><slot /></span> + <span + class="label" + v-if="!!$slots.default" + > + <slot /> + </span> </label> </template> @@ -17,7 +26,11 @@ export default { prop: 'checked', event: 'change' }, - props: ['checked', 'indeterminate'] + props: [ + 'checked', + 'indeterminate', + 'disabled' + ] } </script> @@ -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 2c73faa7..001a22e9 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 0d5ffc9b..76d1f26b 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -1,4 +1,5 @@ import { set } from 'vue' +import Checkbox from '../checkbox/checkbox.vue' const LOAD_EMOJI_BY = 50 const LOAD_EMOJI_INTERVAL = 100 @@ -18,7 +19,6 @@ const EmojiPicker = { }, data () { return { - labelKey: String(Math.random() * 100000), keyword: '', activeGroup: 'custom', showingStickers: false, @@ -32,7 +32,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.vue b/src/components/emoji_picker/emoji_picker.vue index e46a5ec1..43da6aa2 100644 --- a/src/components/emoji_picker/emoji_picker.vue +++ b/src/components/emoji_picker/emoji_picker.vue @@ -75,22 +75,10 @@ </span> </div> </div> - <div - class="keep-open" - > - <input - :id="labelKey + 'keep-open'" - v-model="keepOpen" - type="checkbox" - > - <label - class="keep-open-label" - :for="labelKey + 'keep-open'" - > - <div class="keep-open-label-text"> - {{ $t('emoji.keep_open') }} - </div> - </label> + <div class="keep-open"> + <Checkbox v-model="keepOpen"> + {{ $t('emoji.keep_open') }} + </Checkbox> </div> <div v-if="askForSanity" diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index 6781a4f8..746f1c91 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -4,8 +4,6 @@ trigger="click" placement="top" class="extra-button-popover" - :offset="5" - :container="false" > <div slot="popover"> <div class="dropdown-menu"> diff --git a/src/components/favorite_button/favorite_button.js b/src/components/favorite_button/favorite_button.js index a24eacbf..5014d84f 100644 --- a/src/components/favorite_button/favorite_button.js +++ b/src/components/favorite_button/favorite_button.js @@ -1,10 +1,9 @@ +import { mapGetters } from 'vuex' + const FavoriteButton = { props: ['status', 'loggedIn'], data () { return { - hidePostStatsLocal: typeof this.$store.state.config.hidePostStats === 'undefined' - ? this.$store.state.instance.hidePostStats - : this.$store.state.config.hidePostStats, animated: false } }, @@ -28,7 +27,8 @@ const FavoriteButton = { 'icon-star': this.status.favorited, 'animate-spin': this.animated } - } + }, + ...mapGetters(['mergedConfig']) } } diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue index 06ce9983..fbc90f84 100644 --- a/src/components/favorite_button/favorite_button.vue +++ b/src/components/favorite_button/favorite_button.vue @@ -6,7 +6,7 @@ :title="$t('tool_tip.favorite')" @click.prevent="favorite()" /> - <span v-if="!hidePostStatsLocal && status.fave_num > 0">{{ status.fave_num }}</span> + <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span> </div> <div v-else> <i @@ -14,7 +14,7 @@ class="button-icon favorite-button" :title="$t('tool_tip.favorite')" /> - <span v-if="!hidePostStatsLocal && status.fave_num > 0">{{ status.fave_num }}</span> + <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span> </div> </template> 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 @@ +<template> + <button + class="btn btn-default follow-button" + :class="{ pressed: isPressed }" + :disabled="inProgress" + :title="title" + @click="onClick" + > + {{ label }} + </button> +</template> + +<script src="./follow_button.js"></script> diff --git a/src/components/follow_card/follow_card.js b/src/components/follow_card/follow_card.js index dc4a0d41..aefd609e 100644 --- a/src/components/follow_card/follow_card.js +++ b/src/components/follow_card/follow_card.js @@ -1,21 +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, - requestSent: false - } - }, components: { BasicUserCard, - RemoteFollow + RemoteFollow, + FollowButton }, computed: { isMe () { @@ -24,21 +19,6 @@ const FollowCard = { loggedIn () { return this.$store.state.users.currentUser } - }, - methods: { - followUser () { - this.inProgress = true - requestFollow(this.user, this.$store).then(({ sent }) => { - this.inProgress = false - this.requestSent = sent - }) - }, - 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 310fe843..81e6e6dc 100644 --- a/src/components/follow_card/follow_card.vue +++ b/src/components/follow_card/follow_card.vue @@ -16,36 +16,11 @@ </div> </template> <template v-else> - <button - v-if="!user.following" - class="btn btn-default follow-card-follow-button" - :disabled="inProgress" - :title="requestSent ? $t('user_card.follow_again') : ''" - @click="followUser" - > - <template v-if="inProgress"> - {{ $t('user_card.follow_progress') }} - </template> - <template v-else-if="requestSent"> - {{ $t('user_card.follow_sent') }} - </template> - <template v-else> - {{ $t('user_card.follow') }} - </template> - </button> - <button - v-else - class="btn btn-default follow-card-follow-button pressed" - :disabled="inProgress" - @click="unfollowUser" - > - <template v-if="inProgress"> - {{ $t('user_card.follow_progress') }} - </template> - <template v-else> - {{ $t('user_card.follow_unfollow') }} - </template> - </button> + <FollowButton + :user="user" + class="follow-card-follow-button" + :label-following="$t('user_card.follow_unfollow')" + /> </template> </div> </basic-user-card> 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' ], - components: { Attachment }, - mounted () { - this.resize() - window.addEventListener('resize', this.resize) - }, - destroyed () { - window.removeEventListener('resize', this.resize) + data () { + return { + sizes: {} + } }, + components: { Attachment }, 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 }" > - <attachment - v-for="attachment in row" - :key="attachment.id" - :set-media="setMedia" - :nsfw="nsfw" - :attachment="attachment" - :allow-play="false" - /> + <div class="gallery-row-inner"> + <attachment + v-for="attachment in row" + :key="attachment.id" + :set-media="setMedia" + :nsfw="nsfw" + :attachment="attachment" + :allow-play="false" + :natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)" + :style="itemStyle(attachment.id, row)" + /> + </div> </div> </div> </template> @@ -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..abb18c7d 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -1,11 +1,14 @@ 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' +import GestureService from '../../services/gesture_service/gesture_service' const MediaModal = { components: { StillImage, - VideoAttachment + VideoAttachment, + Modal }, computed: { showing () { @@ -27,7 +30,27 @@ const MediaModal = { return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null } }, + created () { + this.mediaSwipeGestureRight = GestureService.swipeGesture( + GestureService.DIRECTION_RIGHT, + this.goPrev, + 50 + ) + this.mediaSwipeGestureLeft = GestureService.swipeGesture( + GestureService.DIRECTION_LEFT, + this.goNext, + 50 + ) + }, methods: { + mediaTouchStart (e) { + GestureService.beginSwipe(e, this.mediaSwipeGestureRight) + GestureService.beginSwipe(e, this.mediaSwipeGestureLeft) + }, + mediaTouchMove (e) { + GestureService.updateSwipe(e, this.mediaSwipeGestureRight) + GestureService.updateSwipe(e, this.mediaSwipeGestureLeft) + }, hide () { this.$store.dispatch('closeMediaViewer') }, diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 06ced5a1..49e3143e 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -1,14 +1,15 @@ <template> - <div + <Modal v-if="showing" - v-body-scroll-lock="showing" - class="modal-view media-modal-view" - @click.prevent="hide" + class="media-modal-view" + @backdropClicked="hide" > <img v-if="type === 'image'" class="modal-image" :src="currentMedia.url" + @touchstart.stop="mediaTouchStart" + @touchmove.stop="mediaTouchMove" > <VideoAttachment v-if="type === 'video'" @@ -33,33 +34,25 @@ > <i class="icon-right-open arrow-icon" /> </button> - </div> + </Modal> </template> <script src="./media_modal.js"></script> <style lang="scss"> -@import '../../_variables.scss'; - -.media-modal-view { +.modal-view.media-modal-view { z-index: 1001; - body:not(.scroll-locked) & { - display: none; - } - - &:hover { - .modal-view-button-arrow { - opacity: 0.75; + .modal-view-button-arrow { + opacity: 0.75; - &:focus, - &:hover { - outline: none; - box-shadow: none; - } - &:hover { - opacity: 1; - } + &:focus, + &:hover { + outline: none; + box-shadow: none; + } + &:hover { + opacity: 1; } } } @@ -114,5 +107,4 @@ } } } - </style> 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 @@ +<template> + <div + v-show="isOpen" + v-body-scroll-lock="isOpen" + class="modal-view" + @click.self="$emit('backdropClicked')" + > + <slot /> + </div> +</template> + +<script> +export default { + props: { + isOpen: { + type: Boolean, + default: true + } + } +} +</script> + +<style lang="scss"> +.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; + + body:not(.scroll-locked) & { + opacity: 0; + } +} + +@keyframes modal-background-fadein { + from { + background-color: rgba(0, 0, 0, 0); + } + to { + background-color: rgba(0, 0, 0, 0.5); + } +} +</style> 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 @@ <v-popover trigger="click" class="moderation-tools-popover" - :container="false" placement="bottom-end" - :offset="5" @show="showDropDown = true" @hide="showDropDown = false" > 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 @@ > <div class="container"> <div v-if="!mailerEnabled"> - <p> + <p v-if="passwordResetRequested"> + {{ $t('password_reset.password_reset_required_but_mailer_is_disabled') }} + </p> + <p v-else> {{ $t('password_reset.password_reset_disabled') }} </p> </div> @@ -25,6 +28,12 @@ </div> </div> <div v-else> + <p + v-if="passwordResetRequested" + class="password-reset-required error" + > + {{ $t('password_reset.password_reset_required') }} + </p> <p> {{ $t('password_reset.instruction') }} </p> @@ -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 483c395e..af6299e4 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 60dcf90c..13e8b0aa 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -264,12 +264,9 @@ v-if="newStatus.files.length > 0" class="upload_settings" > - <input - id="filesSensitive" - v-model="newStatus.nsfw" - type="checkbox" - > - <label for="filesSensitive">{{ $t('post_status.attachments_sensitive') }}</label> + <Checkbox v-model="newStatus.nsfw"> + {{ $t('post_status.attachments_sensitive') }} + </Checkbox> </div> </form> </div> 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 @@ <template> - <div + <Modal v-if="isLoggedIn && !resettingForm" - v-show="modalActivated" - class="post-form-modal-view modal-view" - @click="closeModal" + :is-open="modalActivated" + class="post-form-modal-view" + @backdropClicked="closeModal" > - <div - class="post-form-modal-panel panel" - @click.stop="" - > + <div class="post-form-modal-panel panel"> <div class="panel-heading"> {{ $t('post_status.new_status') }} </div> @@ -18,15 +15,13 @@ @posted="closeModal" /> </div> - </div> + </Modal> </template> <script src="./post_status_modal.js"></script> <style lang="scss"> -@import '../../_variables.scss'; - -.post-form-modal-view { +.modal-view.post-form-modal-view { align-items: flex-start; } diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js index fb543a9c..d9a0f92e 100644 --- a/src/components/retweet_button/retweet_button.js +++ b/src/components/retweet_button/retweet_button.js @@ -1,10 +1,9 @@ +import { mapGetters } from 'vuex' + const RetweetButton = { props: ['status', 'loggedIn', 'visibility'], data () { return { - hidePostStatsLocal: typeof this.$store.state.config.hidePostStats === 'undefined' - ? this.$store.state.instance.hidePostStats - : this.$store.state.config.hidePostStats, animated: false } }, @@ -28,7 +27,8 @@ const RetweetButton = { 'retweeted-empty': !this.status.repeated, 'animate-spin': this.animated } - } + }, + ...mapGetters(['mergedConfig']) } } diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue index d58a7f8c..074f7747 100644 --- a/src/components/retweet_button/retweet_button.vue +++ b/src/components/retweet_button/retweet_button.vue @@ -7,7 +7,7 @@ :title="$t('tool_tip.repeat')" @click.prevent="retweet()" /> - <span v-if="!hidePostStatsLocal && status.repeat_num > 0">{{ status.repeat_num }}</span> + <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span> </template> <template v-else> <i @@ -23,7 +23,7 @@ class="button-icon icon-retweet" :title="$t('tool_tip.repeat')" /> - <span v-if="!hidePostStatsLocal && status.repeat_num > 0">{{ status.repeat_num }}</span> + <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span> </div> </template> diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index b6540d7e..c49083f9 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -5,88 +5,22 @@ import TabSwitcher from '../tab_switcher/tab_switcher.js' import StyleSwitcher from '../style_switcher/style_switcher.vue' import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue' import { extractCommit } from '../../services/version/version.service' +import { instanceDefaultProperties, defaultState as configDefaultState } from '../../modules/config.js' +import Checkbox from '../checkbox/checkbox.vue' const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/' const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/' +const multiChoiceProperties = [ + 'postContentType', + 'subjectLineBehavior' +] + const settings = { data () { - const user = this.$store.state.config const instance = this.$store.state.instance return { - hideAttachmentsLocal: user.hideAttachments, - padEmojiLocal: user.padEmoji, - hideAttachmentsInConvLocal: user.hideAttachmentsInConv, - maxThumbnails: user.maxThumbnails, - hideNsfwLocal: user.hideNsfw, - useOneClickNsfw: user.useOneClickNsfw, - hideISPLocal: user.hideISP, - preloadImage: user.preloadImage, - - hidePostStatsLocal: typeof user.hidePostStats === 'undefined' - ? instance.hidePostStats - : user.hidePostStats, - hidePostStatsDefault: this.$t('settings.values.' + instance.hidePostStats), - - hideUserStatsLocal: typeof user.hideUserStats === 'undefined' - ? instance.hideUserStats - : user.hideUserStats, - hideUserStatsDefault: this.$t('settings.values.' + instance.hideUserStats), - - hideFilteredStatusesLocal: typeof user.hideFilteredStatuses === 'undefined' - ? instance.hideFilteredStatuses - : user.hideFilteredStatuses, - hideFilteredStatusesDefault: this.$t('settings.values.' + instance.hideFilteredStatuses), - - notificationVisibilityLocal: user.notificationVisibility, - replyVisibilityLocal: user.replyVisibility, - loopVideoLocal: user.loopVideo, - muteWordsString: user.muteWords.join('\n'), - autoLoadLocal: user.autoLoad, - streamingLocal: user.streaming, - pauseOnUnfocusedLocal: user.pauseOnUnfocused, - hoverPreviewLocal: user.hoverPreview, - autohideFloatingPostButtonLocal: user.autohideFloatingPostButton, - - hideMutedPostsLocal: typeof user.hideMutedPosts === 'undefined' - ? instance.hideMutedPosts - : user.hideMutedPosts, - hideMutedPostsDefault: this.$t('settings.values.' + instance.hideMutedPosts), - - collapseMessageWithSubjectLocal: typeof user.collapseMessageWithSubject === 'undefined' - ? instance.collapseMessageWithSubject - : user.collapseMessageWithSubject, - collapseMessageWithSubjectDefault: this.$t('settings.values.' + instance.collapseMessageWithSubject), - - subjectLineBehaviorLocal: typeof user.subjectLineBehavior === 'undefined' - ? instance.subjectLineBehavior - : user.subjectLineBehavior, - subjectLineBehaviorDefault: instance.subjectLineBehavior, - - postContentTypeLocal: typeof user.postContentType === 'undefined' - ? instance.postContentType - : user.postContentType, - postContentTypeDefault: instance.postContentType, - - alwaysShowSubjectInputLocal: typeof user.alwaysShowSubjectInput === 'undefined' - ? instance.alwaysShowSubjectInput - : user.alwaysShowSubjectInput, - alwaysShowSubjectInputDefault: this.$t('settings.values.' + instance.alwaysShowSubjectInput), - - scopeCopyLocal: typeof user.scopeCopy === 'undefined' - ? instance.scopeCopy - : user.scopeCopy, - scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy), - - minimalScopesModeLocal: typeof user.minimalScopesMode === 'undefined' - ? instance.minimalScopesMode - : user.minimalScopesMode, - minimalScopesModeDefault: this.$t('settings.values.' + instance.minimalScopesMode), - - stopGifs: user.stopGifs, - webPushNotificationsLocal: user.webPushNotifications, - loopVideoSilentOnlyLocal: user.loopVideosSilentOnly, loopSilentAvailable: // Firefox Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || @@ -94,8 +28,6 @@ const settings = { Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') || // Future spec, still not supported in Nightly 63 as of 08/2018 Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'), - playVideosInModal: user.playVideosInModal, - useContainFit: user.useContainFit, backendVersion: instance.backendVersion, frontendVersion: instance.frontendVersion @@ -104,7 +36,8 @@ const settings = { components: { TabSwitcher, StyleSwitcher, - InterfaceLanguageSwitcher + InterfaceLanguageSwitcher, + Checkbox }, computed: { user () { @@ -122,116 +55,56 @@ const settings = { }, backendVersionLink () { return pleromaBeCommitUrl + extractCommit(this.backendVersion) + }, + // Getting localized values for instance-default properties + ...instanceDefaultProperties + .filter(key => multiChoiceProperties.includes(key)) + .map(key => [ + key + 'DefaultValue', + function () { + return this.$store.getters.instanceDefaultConfig[key] + } + ]) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), + ...instanceDefaultProperties + .filter(key => !multiChoiceProperties.includes(key)) + .map(key => [ + key + 'LocalizedValue', + function () { + return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key]) + } + ]) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), + // Generating computed values for vuex properties + ...Object.keys(configDefaultState) + .map(key => [key, { + get () { return this.$store.getters.mergedConfig[key] }, + set (value) { + this.$store.dispatch('setOption', { name: key, value }) + } + }]) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), + // Special cases (need to transform values) + muteWordsString: { + get () { return this.$store.getters.mergedConfig.muteWords.join('\n') }, + set (value) { + this.$store.dispatch('setOption', { + name: 'muteWords', + value: filter(value.split('\n'), (word) => trim(word).length > 0) + }) + } } }, + // Updating nested properties watch: { - hideAttachmentsLocal (value) { - this.$store.dispatch('setOption', { name: 'hideAttachments', value }) - }, - padEmojiLocal (value) { - this.$store.dispatch('setOption', { name: 'padEmoji', value }) - }, - hideAttachmentsInConvLocal (value) { - this.$store.dispatch('setOption', { name: 'hideAttachmentsInConv', value }) - }, - hidePostStatsLocal (value) { - this.$store.dispatch('setOption', { name: 'hidePostStats', value }) - }, - hideUserStatsLocal (value) { - this.$store.dispatch('setOption', { name: 'hideUserStats', value }) - }, - hideFilteredStatusesLocal (value) { - this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value }) - }, - hideNsfwLocal (value) { - this.$store.dispatch('setOption', { name: 'hideNsfw', value }) - }, - useOneClickNsfw (value) { - this.$store.dispatch('setOption', { name: 'useOneClickNsfw', value }) - }, - preloadImage (value) { - this.$store.dispatch('setOption', { name: 'preloadImage', value }) - }, - hideISPLocal (value) { - this.$store.dispatch('setOption', { name: 'hideISP', value }) - }, - 'notificationVisibilityLocal.likes' (value) { - this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility }) - }, - 'notificationVisibilityLocal.follows' (value) { - this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility }) - }, - 'notificationVisibilityLocal.repeats' (value) { - this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility }) - }, - 'notificationVisibilityLocal.mentions' (value) { - this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility }) - }, - replyVisibilityLocal (value) { - this.$store.dispatch('setOption', { name: 'replyVisibility', value }) - }, - loopVideoLocal (value) { - this.$store.dispatch('setOption', { name: 'loopVideo', value }) - }, - loopVideoSilentOnlyLocal (value) { - this.$store.dispatch('setOption', { name: 'loopVideoSilentOnly', value }) - }, - autoLoadLocal (value) { - this.$store.dispatch('setOption', { name: 'autoLoad', value }) - }, - streamingLocal (value) { - this.$store.dispatch('setOption', { name: 'streaming', value }) - }, - pauseOnUnfocusedLocal (value) { - this.$store.dispatch('setOption', { name: 'pauseOnUnfocused', value }) - }, - hoverPreviewLocal (value) { - this.$store.dispatch('setOption', { name: 'hoverPreview', value }) - }, - autohideFloatingPostButtonLocal (value) { - this.$store.dispatch('setOption', { name: 'autohideFloatingPostButton', value }) - }, - muteWordsString (value) { - value = filter(value.split('\n'), (word) => trim(word).length > 0) - this.$store.dispatch('setOption', { name: 'muteWords', value }) - }, - hideMutedPostsLocal (value) { - this.$store.dispatch('setOption', { name: 'hideMutedPosts', value }) - }, - collapseMessageWithSubjectLocal (value) { - this.$store.dispatch('setOption', { name: 'collapseMessageWithSubject', value }) - }, - scopeCopyLocal (value) { - this.$store.dispatch('setOption', { name: 'scopeCopy', value }) - }, - alwaysShowSubjectInputLocal (value) { - this.$store.dispatch('setOption', { name: 'alwaysShowSubjectInput', value }) - }, - subjectLineBehaviorLocal (value) { - this.$store.dispatch('setOption', { name: 'subjectLineBehavior', value }) - }, - postContentTypeLocal (value) { - this.$store.dispatch('setOption', { name: 'postContentType', value }) - }, - minimalScopesModeLocal (value) { - this.$store.dispatch('setOption', { name: 'minimalScopesMode', value }) - }, - stopGifs (value) { - this.$store.dispatch('setOption', { name: 'stopGifs', value }) - }, - webPushNotificationsLocal (value) { - this.$store.dispatch('setOption', { name: 'webPushNotifications', value }) - if (value) this.$store.dispatch('registerPushNotifications') - }, - playVideosInModal (value) { - this.$store.dispatch('setOption', { name: 'playVideosInModal', value }) - }, - useContainFit (value) { - this.$store.dispatch('setOption', { name: 'useContainFit', value }) - }, - maxThumbnails (value) { - value = this.maxThumbnails = Math.floor(Math.max(value, 0)) - this.$store.dispatch('setOption', { name: 'maxThumbnails', value }) + notificationVisibility: { + handler (value) { + this.$store.dispatch('setOption', { + name: 'notificationVisibility', + value: this.$store.getters.mergedConfig.notificationVisibility + }) + }, + deep: true } } } diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index 6d87a060..a83489d2 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -36,12 +36,9 @@ <interface-language-switcher /> </li> <li v-if="instanceSpecificPanelPresent"> - <input - id="hideISP" - v-model="hideISPLocal" - type="checkbox" - > - <label for="hideISP">{{ $t('settings.hide_isp') }}</label> + <Checkbox v-model="hideISP"> + {{ $t('settings.hide_isp') }} + </Checkbox> </li> </ul> </div> @@ -49,58 +46,42 @@ <h2>{{ $t('nav.timeline') }}</h2> <ul class="setting-list"> <li> - <input - id="hideMutedPosts" - v-model="hideMutedPostsLocal" - type="checkbox" - > - <label for="hideMutedPosts">{{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsDefault }) }}</label> + <Checkbox v-model="hideMutedPosts"> + {{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }} + </Checkbox> </li> <li> - <input - id="collapseMessageWithSubject" - v-model="collapseMessageWithSubjectLocal" - type="checkbox" - > - <label for="collapseMessageWithSubject">{{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectDefault }) }}</label> + <Checkbox v-model="collapseMessageWithSubject"> + {{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }} + </Checkbox> </li> <li> - <input - id="streaming" - v-model="streamingLocal" - type="checkbox" - > - <label for="streaming">{{ $t('settings.streaming') }}</label> + <Checkbox v-model="streaming"> + {{ $t('settings.streaming') }} + </Checkbox> <ul class="setting-list suboptions" - :class="[{disabled: !streamingLocal}]" + :class="[{disabled: !streaming}]" > <li> - <input - id="pauseOnUnfocused" - v-model="pauseOnUnfocusedLocal" - :disabled="!streamingLocal" - type="checkbox" + <Checkbox + v-model="pauseOnUnfocused" + :disabled="!streaming" > - <label for="pauseOnUnfocused">{{ $t('settings.pause_on_unfocused') }}</label> + {{ $t('settings.pause_on_unfocused') }} + </Checkbox> </li> </ul> </li> <li> - <input - id="autoload" - v-model="autoLoadLocal" - type="checkbox" - > - <label for="autoload">{{ $t('settings.autoload') }}</label> + <Checkbox v-model="autoLoad"> + {{ $t('settings.autoload') }} + </Checkbox> </li> <li> - <input - id="hoverPreview" - v-model="hoverPreviewLocal" - type="checkbox" - > - <label for="hoverPreview">{{ $t('settings.reply_link_preview') }}</label> + <Checkbox v-model="hoverPreview"> + {{ $t('settings.reply_link_preview') }} + </Checkbox> </li> </ul> </div> @@ -109,24 +90,14 @@ <h2>{{ $t('settings.composing') }}</h2> <ul class="setting-list"> <li> - <input - id="scopeCopy" - v-model="scopeCopyLocal" - type="checkbox" - > - <label for="scopeCopy"> - {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyDefault }) }} - </label> + <Checkbox v-model="scopeCopy"> + {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }} + </Checkbox> </li> <li> - <input - id="subjectHide" - v-model="alwaysShowSubjectInputLocal" - type="checkbox" - > - <label for="subjectHide"> - {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputDefault }) }} - </label> + <Checkbox v-model="alwaysShowSubjectInput"> + {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }} + </Checkbox> </li> <li> <div> @@ -137,19 +108,19 @@ > <select id="subjectLineBehavior" - v-model="subjectLineBehaviorLocal" + v-model="subjectLineBehavior" > <option value="email"> {{ $t('settings.subject_line_email') }} - {{ subjectLineBehaviorDefault == 'email' ? $t('settings.instance_default_simple') : '' }} + {{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }} </option> <option value="masto"> {{ $t('settings.subject_line_mastodon') }} - {{ subjectLineBehaviorDefault == 'mastodon' ? $t('settings.instance_default_simple') : '' }} + {{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }} </option> <option value="noop"> {{ $t('settings.subject_line_noop') }} - {{ subjectLineBehaviorDefault == 'noop' ? $t('settings.instance_default_simple') : '' }} + {{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }} </option> </select> <i class="icon-down-open" /> @@ -165,7 +136,7 @@ > <select id="postContentType" - v-model="postContentTypeLocal" + v-model="postContentType" > <option v-for="postFormat in postFormats" @@ -173,7 +144,7 @@ :value="postFormat" > {{ $t(`post_status.content_type["${postFormat}"]`) }} - {{ postContentTypeDefault === postFormat ? $t('settings.instance_default_simple') : '' }} + {{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }} </option> </select> <i class="icon-down-open" /> @@ -181,30 +152,19 @@ </div> </li> <li> - <input - id="minimalScopesMode" - v-model="minimalScopesModeLocal" - type="checkbox" - > - <label for="minimalScopesMode"> - {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeDefault }) }} - </label> + <Checkbox v-model="minimalScopesMode"> + {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }} + </Checkbox> </li> <li> - <input - id="autohideFloatingPostButton" - v-model="autohideFloatingPostButtonLocal" - type="checkbox" - > - <label for="autohideFloatingPostButton">{{ $t('settings.autohide_floating_post_button') }}</label> + <Checkbox v-model="autohideFloatingPostButton"> + {{ $t('settings.autohide_floating_post_button') }} + </Checkbox> </li> <li> - <input - id="padEmoji" - v-model="padEmojiLocal" - type="checkbox" - > - <label for="padEmoji">{{ $t('settings.pad_emoji') }}</label> + <Checkbox v-model="padEmoji"> + {{ $t('settings.pad_emoji') }} + </Checkbox> </li> </ul> </div> @@ -213,23 +173,19 @@ <h2>{{ $t('settings.attachments') }}</h2> <ul class="setting-list"> <li> - <input - id="hideAttachments" - v-model="hideAttachmentsLocal" - type="checkbox" - > - <label for="hideAttachments">{{ $t('settings.hide_attachments_in_tl') }}</label> + <Checkbox v-model="hideAttachments"> + {{ $t('settings.hide_attachments_in_tl') }} + </Checkbox> </li> <li> - <input - id="hideAttachmentsInConv" - v-model="hideAttachmentsInConvLocal" - type="checkbox" - > - <label for="hideAttachmentsInConv">{{ $t('settings.hide_attachments_in_convo') }}</label> + <Checkbox v-model="hideAttachmentsInConv"> + {{ $t('settings.hide_attachments_in_convo') }} + </Checkbox> </li> <li> - <label for="maxThumbnails">{{ $t('settings.max_thumbnails') }}</label> + <label for="maxThumbnails"> + {{ $t('settings.max_thumbnails') }} + </label> <input id="maxThumbnails" v-model.number="maxThumbnails" @@ -240,60 +196,48 @@ > </li> <li> - <input - id="hideNsfw" - v-model="hideNsfwLocal" - type="checkbox" - > - <label for="hideNsfw">{{ $t('settings.nsfw_clickthrough') }}</label> + <Checkbox v-model="hideNsfw"> + {{ $t('settings.nsfw_clickthrough') }} + </Checkbox> </li> <ul class="setting-list suboptions"> <li> - <input - id="preloadImage" + <Checkbox v-model="preloadImage" - :disabled="!hideNsfwLocal" - type="checkbox" + :disabled="!hideNsfw" > - <label for="preloadImage">{{ $t('settings.preload_images') }}</label> + {{ $t('settings.preload_images') }} + </Checkbox> </li> <li> - <input - id="useOneClickNsfw" + <Checkbox v-model="useOneClickNsfw" - :disabled="!hideNsfwLocal" - type="checkbox" + :disabled="!hideNsfw" > - <label for="useOneClickNsfw">{{ $t('settings.use_one_click_nsfw') }}</label> + {{ $t('settings.use_one_click_nsfw') }} + </Checkbox> </li> </ul> <li> - <input - id="stopGifs" - v-model="stopGifs" - type="checkbox" - > - <label for="stopGifs">{{ $t('settings.stop_gifs') }}</label> + <Checkbox v-model="stopGifs"> + {{ $t('settings.stop_gifs') }} + </Checkbox> </li> <li> - <input - id="loopVideo" - v-model="loopVideoLocal" - type="checkbox" - > - <label for="loopVideo">{{ $t('settings.loop_video') }}</label> + <Checkbox v-model="loopVideo"> + {{ $t('settings.loop_video') }} + </Checkbox> <ul class="setting-list suboptions" - :class="[{disabled: !streamingLocal}]" + :class="[{disabled: !streaming}]" > <li> - <input - id="loopVideoSilentOnly" - v-model="loopVideoSilentOnlyLocal" - :disabled="!loopVideoLocal || !loopSilentAvailable" - type="checkbox" + <Checkbox + v-model="loopVideoSilentOnly" + :disabled="!loopVideo || !loopSilentAvailable" > - <label for="loopVideoSilentOnly">{{ $t('settings.loop_video_silent_only') }}</label> + {{ $t('settings.loop_video_silent_only') }} + </Checkbox> <div v-if="!loopSilentAvailable" class="unavailable" @@ -304,20 +248,14 @@ </ul> </li> <li> - <input - id="playVideosInModal" - v-model="playVideosInModal" - type="checkbox" - > - <label for="playVideosInModal">{{ $t('settings.play_videos_in_modal') }}</label> + <Checkbox v-model="playVideosInModal"> + {{ $t('settings.play_videos_in_modal') }} + </Checkbox> </li> <li> - <input - id="useContainFit" - v-model="useContainFit" - type="checkbox" - > - <label for="useContainFit">{{ $t('settings.use_contain_fit') }}</label> + <Checkbox v-model="useContainFit"> + {{ $t('settings.use_contain_fit') }} + </Checkbox> </li> </ul> </div> @@ -326,14 +264,9 @@ <h2>{{ $t('settings.notifications') }}</h2> <ul class="setting-list"> <li> - <input - id="webPushNotifications" - v-model="webPushNotificationsLocal" - type="checkbox" - > - <label for="webPushNotifications"> + <Checkbox v-model="webPushNotifications"> {{ $t('settings.enable_web_push_notifications') }} - </label> + </Checkbox> </li> </ul> </div> @@ -351,44 +284,24 @@ <span class="label">{{ $t('settings.notification_visibility') }}</span> <ul class="option-list"> <li> - <input - id="notification-visibility-likes" - v-model="notificationVisibilityLocal.likes" - type="checkbox" - > - <label for="notification-visibility-likes"> + <Checkbox v-model="notificationVisibility.likes"> {{ $t('settings.notification_visibility_likes') }} - </label> + </Checkbox> </li> <li> - <input - id="notification-visibility-repeats" - v-model="notificationVisibilityLocal.repeats" - type="checkbox" - > - <label for="notification-visibility-repeats"> + <Checkbox v-model="notificationVisibility.repeats"> {{ $t('settings.notification_visibility_repeats') }} - </label> + </Checkbox> </li> <li> - <input - id="notification-visibility-follows" - v-model="notificationVisibilityLocal.follows" - type="checkbox" - > - <label for="notification-visibility-follows"> + <Checkbox v-model="notificationVisibility.follows"> {{ $t('settings.notification_visibility_follows') }} - </label> + </Checkbox> </li> <li> - <input - id="notification-visibility-mentions" - v-model="notificationVisibilityLocal.mentions" - type="checkbox" - > - <label for="notification-visibility-mentions"> + <Checkbox v-model="notificationVisibility.mentions"> {{ $t('settings.notification_visibility_mentions') }} - </label> + </Checkbox> </li> </ul> </div> @@ -400,7 +313,7 @@ > <select id="replyVisibility" - v-model="replyVisibilityLocal" + v-model="replyVisibility" > <option value="all" @@ -413,24 +326,14 @@ </label> </div> <div> - <input - id="hidePostStats" - v-model="hidePostStatsLocal" - type="checkbox" - > - <label for="hidePostStats"> - {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsDefault }) }} - </label> + <Checkbox v-model="hidePostStats"> + {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }} + </Checkbox> </div> <div> - <input - id="hideUserStats" - v-model="hideUserStatsLocal" - type="checkbox" - > - <label for="hideUserStats"> - {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsDefault }) }} - </label> + <Checkbox v-model="hideUserStats"> + {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }} + </Checkbox> </div> </div> <div class="setting-item"> @@ -442,14 +345,9 @@ /> </div> <div> - <input - id="hideFilteredStatuses" - v-model="hideFilteredStatusesLocal" - type="checkbox" - > - <label for="hideFilteredStatuses"> - {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesDefault }) }} - </label> + <Checkbox v-model="hideFilteredStatuses"> + {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }} + </Checkbox> </div> </div> </div> diff --git a/src/components/status/status.js b/src/components/status/status.js index d17ba318..4fbd5ac3 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -10,11 +10,13 @@ import Gallery from '../gallery/gallery.vue' import LinkPreview from '../link-preview/link-preview.vue' import AvatarList from '../avatar_list/avatar_list.vue' import Timeago from '../timeago/timeago.vue' +import StatusPopover from '../status_popover/status_popover.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import fileType from 'src/services/file_type/file_type.service' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js' -import { filter, find, unescape, uniqBy } from 'lodash' +import { filter, unescape, uniqBy } from 'lodash' +import { mapGetters } from 'vuex' const Status = { name: 'Status', @@ -37,25 +39,19 @@ const Status = { replying: false, unmuted: false, userExpanded: false, - preview: null, - showPreview: false, showingTall: this.inConversation && this.focused, showingLongSubject: false, error: null, - expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined' - ? !this.$store.state.instance.collapseMessageWithSubject - : !this.$store.state.config.collapseMessageWithSubject, + expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject, betterShadow: this.$store.state.interface.browserSupport.cssFilter } }, computed: { localCollapseSubjectDefault () { - return typeof this.$store.state.config.collapseMessageWithSubject === 'undefined' - ? this.$store.state.instance.collapseMessageWithSubject - : this.$store.state.config.collapseMessageWithSubject + return this.mergedConfig.collapseMessageWithSubject }, muteWords () { - return this.$store.state.config.muteWords + return this.mergedConfig.muteWords }, repeaterClass () { const user = this.statusoid.user @@ -70,18 +66,18 @@ const Status = { }, repeaterStyle () { const user = this.statusoid.user - const highlight = this.$store.state.config.highlight + const highlight = this.mergedConfig.highlight return highlightStyle(highlight[user.screen_name]) }, userStyle () { if (this.noHeading) return const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user - const highlight = this.$store.state.config.highlight + const highlight = this.mergedConfig.highlight return highlightStyle(highlight[user.screen_name]) }, hideAttachments () { - return (this.$store.state.config.hideAttachments && !this.inConversation) || - (this.$store.state.config.hideAttachmentsInConv && this.inConversation) + return (this.mergedConfig.hideAttachments && !this.inConversation) || + (this.mergedConfig.hideAttachmentsInConv && this.inConversation) }, userProfileLink () { return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name) @@ -120,9 +116,7 @@ const Status = { }, muted () { return !this.unmuted && ((!this.inProfile && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) }, hideFilteredStatuses () { - return typeof this.$store.state.config.hideFilteredStatuses === 'undefined' - ? this.$store.state.instance.hideFilteredStatuses - : this.$store.state.config.hideFilteredStatuses + return this.mergedConfig.hideFilteredStatuses }, hideStatus () { return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses) @@ -163,7 +157,7 @@ const Status = { } }, hideReply () { - if (this.$store.state.config.replyVisibility === 'all') { + if (this.mergedConfig.replyVisibility === 'all') { return false } if (this.inConversation || !this.isReply) { @@ -175,7 +169,7 @@ const Status = { if (this.status.type === 'retweet') { return false } - const checkFollowing = this.$store.state.config.replyVisibility === 'following' + const checkFollowing = this.mergedConfig.replyVisibility === 'following' for (var i = 0; i < this.status.attentions.length; ++i) { if (this.status.user.id === this.status.attentions[i].id) { continue @@ -220,9 +214,7 @@ const Status = { replySubject () { if (!this.status.summary) return '' const decodedSummary = unescape(this.status.summary) - const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined' - ? this.$store.state.instance.subjectLineBehavior - : this.$store.state.config.subjectLineBehavior + const behavior = this.mergedConfig.subjectLineBehavior const startsWithRe = decodedSummary.match(/^re[: ]/i) if ((behavior !== 'noop' && startsWithRe) || behavior === 'masto') { return decodedSummary @@ -233,8 +225,8 @@ const Status = { } }, attachmentSize () { - if ((this.$store.state.config.hideAttachments && !this.inConversation) || - (this.$store.state.config.hideAttachmentsInConv && this.inConversation) || + if ((this.mergedConfig.hideAttachments && !this.inConversation) || + (this.mergedConfig.hideAttachmentsInConv && this.inConversation) || (this.status.attachments.length > this.maxThumbnails)) { return 'hide' } else if (this.compact) { @@ -246,7 +238,7 @@ const Status = { if (this.attachmentSize === 'hide') { return [] } - return this.$store.state.config.playVideosInModal + return this.mergedConfig.playVideosInModal ? ['image', 'video'] : ['image'] }, @@ -261,7 +253,7 @@ const Status = { ) }, maxThumbnails () { - return this.$store.state.config.maxThumbnails + return this.mergedConfig.maxThumbnails }, contentHtml () { if (!this.status.summary_html) { @@ -284,10 +276,9 @@ const Status = { return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ') }, hidePostStats () { - return typeof this.$store.state.config.hidePostStats === 'undefined' - ? this.$store.state.instance.hidePostStats - : this.$store.state.config.hidePostStats - } + return this.mergedConfig.hidePostStats + }, + ...mapGetters(['mergedConfig']) }, components: { Attachment, @@ -301,7 +292,8 @@ const Status = { Gallery, LinkPreview, AvatarList, - Timeago + Timeago, + StatusPopover }, methods: { visibilityIcon (visibility) { @@ -376,27 +368,6 @@ const Status = { this.expandingSubject = true } }, - replyEnter (id, event) { - this.showPreview = true - const targetId = id - const statuses = this.$store.state.statuses.allStatuses - - if (!this.preview) { - // if we have the status somewhere already - this.preview = find(statuses, { 'id': targetId }) - // or if we have to fetch it - if (!this.preview) { - this.$store.state.api.backendInteractor.fetchStatus({ id }).then((status) => { - this.preview = status - }) - } - } else if (this.preview.id !== targetId) { - this.preview = find(statuses, { 'id': targetId }) - } - }, - replyLeave () { - this.showPreview = false - }, generateUserProfileLink (id, name) { return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) }, diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 93f37a49..65778b2e 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -174,20 +174,26 @@ v-if="isReply" class="reply-to-and-accountname" > - <a + <StatusPopover + v-if="!isPreview" + :status-id="status.in_reply_to_status_id" + > + <a + class="reply-to" + href="#" + :aria-label="$t('tool_tip.reply')" + @click.prevent="gotoOriginal(status.in_reply_to_status_id)" + > + <i class="button-icon icon-reply" /> + <span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span> + </a> + </StatusPopover> + <span + v-else class="reply-to" - href="#" - :aria-label="$t('tool_tip.reply')" - @click.prevent="gotoOriginal(status.in_reply_to_status_id)" - @mouseenter.prevent.stop="replyEnter(status.in_reply_to_status_id, $event)" - @mouseleave.prevent.stop="replyLeave()" > - <i - v-if="!isPreview" - class="button-icon icon-reply" - /> - <span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span> - </a> + <span class="reply-to-text">{{ $t('status.reply_to') }}</span> + </span> <router-link :to="replyProfileLink"> {{ replyToName }} </router-link> @@ -199,51 +205,26 @@ </span> </div> <div - v-if="inConversation && !isPreview" + v-if="inConversation && !isPreview && replies && replies.length" class="replies" > - <span - v-if="replies && replies.length" - class="faint" - >{{ $t('status.replies_list') }}</span> - <template v-if="replies"> - <span - v-for="reply in replies" - :key="reply.id" - class="reply-link faint" - > - <a - href="#" - @click.prevent="gotoOriginal(reply.id)" - @mouseenter="replyEnter(reply.id, $event)" - @mouseout="replyLeave()" - >{{ reply.name }}</a> - </span> - </template> + <span class="faint">{{ $t('status.replies_list') }}</span> + <StatusPopover + v-for="reply in replies" + :key="reply.id" + :status-id="reply.id" + > + <a + href="#" + class="reply-link" + @click.prevent="gotoOriginal(reply.id)" + >{{ reply.name }}</a> + </StatusPopover> </div> </div> </div> <div - v-if="showPreview" - class="status-preview-container" - > - <status - v-if="preview" - class="status-preview" - :is-preview="true" - :statusoid="preview" - :compact="true" - /> - <div - v-else - class="status-preview status-preview-loading" - > - <i class="icon-spin4 animate-spin" /> - </div> - </div> - - <div v-if="longSubject" class="status-content-wrapper" :class="{ 'tall-status': !showingLongSubject }" @@ -439,18 +420,6 @@ $status-margin: 0.75em; min-width: 0; } -.status-preview.status-el { - border-style: solid; - border-width: 1px; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); -} - -.status-preview-container { - position: relative; - max-width: 100%; -} - .status-pin { padding: $status-margin $status-margin 0; display: flex; @@ -458,44 +427,6 @@ $status-margin: 0.75em; justify-content: flex-end; } -.status-preview { - position: absolute; - max-width: 95%; - display: flex; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - border-style: solid; - border-width: 1px; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5); - box-shadow: var(--popupShadow); - margin-top: 0.25em; - margin-left: 0.5em; - z-index: 50; - - .status { - flex: 1; - border: 0; - min-width: 15em; - } -} - -.status-preview-loading { - display: block; - min-width: 15em; - padding: 1em; - text-align: center; - border-width: 1px; - border-style: solid; - - i { - font-size: 2em; - } -} - .media-left { margin-right: $status-margin; } @@ -553,11 +484,6 @@ $status-margin: 0.75em; flex-basis: 100%; margin-bottom: 0.5em; - a { - display: inline-block; - word-break: break-all; - } - small { font-weight: lighter; } @@ -568,6 +494,11 @@ $status-margin: 0.75em; justify-content: space-between; line-height: 18px; + a { + display: inline-block; + word-break: break-all; + } + .name-and-account-name { display: flex; min-width: 0; @@ -600,6 +531,7 @@ $status-margin: 0.75em; } .heading-reply-row { + position: relative; align-content: baseline; font-size: 12px; line-height: 18px; @@ -608,11 +540,13 @@ $status-margin: 0.75em; flex-wrap: wrap; align-items: stretch; - a { + > .reply-to-and-accountname > a { max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; + display: inline-block; + word-break: break-all; } } @@ -639,6 +573,8 @@ $status-margin: 0.75em; overflow: hidden; text-overflow: ellipsis; margin: 0 0.4em 0 0.2em; + color: $fallback--faint; + color: var(--faint, $fallback--faint); } .replies-separator { @@ -840,6 +776,11 @@ $status-margin: 0.75em; &.button-icon-active { color: $fallback--cBlue; color: var(--cBlue, $fallback--cBlue); + } +} + +.button-icon.icon-reply { + &:not(.button-icon-disabled) { cursor: pointer; } } diff --git a/src/components/status_popover/status_popover.js b/src/components/status_popover/status_popover.js new file mode 100644 index 00000000..19f16bd9 --- /dev/null +++ b/src/components/status_popover/status_popover.js @@ -0,0 +1,34 @@ +import { find } from 'lodash' + +const StatusPopover = { + name: 'StatusPopover', + props: [ + 'statusId' + ], + data () { + return { + popperOptions: { + modifiers: { + preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' } + } + } + } + }, + computed: { + status () { + return find(this.$store.state.statuses.allStatuses, { id: this.statusId }) + } + }, + components: { + Status: () => import('../status/status.vue') + }, + methods: { + enter () { + if (!this.status) { + this.$store.dispatch('fetchStatus', this.statusId) + } + } + } +} + +export default StatusPopover diff --git a/src/components/status_popover/status_popover.vue b/src/components/status_popover/status_popover.vue new file mode 100644 index 00000000..eacf4c06 --- /dev/null +++ b/src/components/status_popover/status_popover.vue @@ -0,0 +1,85 @@ +<template> + <v-popover + popover-class="status-popover" + placement="top-start" + :popper-options="popperOptions" + @show="enter()" + > + <template slot="popover"> + <Status + v-if="status" + :is-preview="true" + :statusoid="status" + :compact="true" + /> + <div + v-else + class="status-preview-loading" + > + <i class="icon-spin4 animate-spin" /> + </div> + </template> + + <slot /> + </v-popover> +</template> + +<script src="./status_popover.js" ></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.tooltip.popover.status-popover { + font-size: 1rem; + min-width: 15em; + max-width: 95%; + margin-left: 0.5em; + + .popover-inner { + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + border-style: solid; + border-width: 1px; + border-radius: $fallback--tooltipRadius; + border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5); + box-shadow: var(--popupShadow); + } + + .popover-arrow::before { + position: absolute; + content: ''; + left: -7px; + border: solid 7px transparent; + z-index: -1; + } + + &[x-placement^="bottom-start"] .popover-arrow::before { + top: -2px; + border-top-width: 0; + border-bottom-color: $fallback--border; + border-bottom-color: var(--border, $fallback--border); + } + + &[x-placement^="top-start"] .popover-arrow::before { + bottom: -2px; + border-bottom-width: 0; + border-top-color: $fallback--border; + border-top-color: var(--border, $fallback--border); + } + + .status-el.status-el { + border: none; + } + + .status-preview-loading { + padding: 1em; + text-align: center; + + i { + font-size: 2em; + } + } +} + +</style> 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 ef5704c1..ebde4475 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: {}, @@ -112,7 +113,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] } @@ -341,7 +342,8 @@ export default { FontControl, TabSwitcher, Preview, - ExportImport + ExportImport, + Checkbox }, methods: { setCustomTheme () { @@ -368,9 +370,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 d32641c2..ad032041 100644 --- a/src/components/style_switcher/style_switcher.vue +++ b/src/components/style_switcher/style_switcher.vue @@ -42,44 +42,29 @@ </div> <div class="save-load-options"> <span class="keep-option"> - <input - id="keep-color" - v-model="keepColor" - type="checkbox" - > - <label for="keep-color">{{ $t('settings.style.switcher.keep_color') }}</label> + <Checkbox v-model="keepColor"> + {{ $t('settings.style.switcher.keep_color') }} + </Checkbox> </span> <span class="keep-option"> - <input - id="keep-shadows" - v-model="keepShadows" - type="checkbox" - > - <label for="keep-shadows">{{ $t('settings.style.switcher.keep_shadows') }}</label> + <Checkbox v-model="keepShadows"> + {{ $t('settings.style.switcher.keep_shadows') }} + </Checkbox> </span> <span class="keep-option"> - <input - id="keep-opacity" - v-model="keepOpacity" - type="checkbox" - > - <label for="keep-opacity">{{ $t('settings.style.switcher.keep_opacity') }}</label> + <Checkbox v-model="keepOpacity"> + {{ $t('settings.style.switcher.keep_opacity') }} + </Checkbox> </span> <span class="keep-option"> - <input - id="keep-roundness" - v-model="keepRoundness" - type="checkbox" - > - <label for="keep-roundness">{{ $t('settings.style.switcher.keep_roundness') }}</label> + <Checkbox v-model="keepRoundness"> + {{ $t('settings.style.switcher.keep_roundness') }} + </Checkbox> </span> <span class="keep-option"> - <input - id="keep-fonts" - v-model="keepFonts" - type="checkbox" - > - <label for="keep-fonts">{{ $t('settings.style.switcher.keep_fonts') }}</label> + <Checkbox v-model="keepFonts"> + {{ $t('settings.style.switcher.keep_fonts') }} + </Checkbox> </span> <p>{{ $t('settings.style.switcher.save_load_hint') }}</p> </div> 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 @@ > <i class="icon-link-ext usersettings" /> </a> + <AccountActions + v-if="isOtherUser && loggedIn" + :user="user" + /> </div> - <div class="bottom-line"> <router-link class="user-screen-name" @@ -81,7 +84,7 @@ >{{ visibleRole }}</span> <span v-if="user.locked"><i class="icon icon-lock" /></span> <span - v-if="!hideUserStatsLocal && !hideBio" + v-if="!mergedConfig.hideUserStats && !hideBio" class="dailyAvg" >{{ dailyAvg }} {{ $t('user_card.per_day') }}</span> </div> @@ -135,72 +138,27 @@ v-if="loggedIn && isOtherUser" class="user-interactions" > - <div v-if="!user.following"> - <button - class="btn btn-default btn-block" - :disabled="followRequestInProgress" - :title="user.requested ? $t('user_card.follow_again') : ''" - @click="followUser" - > - <template v-if="followRequestInProgress"> - {{ $t('user_card.follow_progress') }} - </template> - <template v-else-if="user.requested"> - {{ $t('user_card.follow_sent') }} - </template> - <template v-else> - {{ $t('user_card.follow') }} - </template> - </button> - </div> - <div v-else-if="followRequestInProgress"> - <button - class="btn btn-default btn-block pressed" - disabled - :title="$t('user_card.follow_unfollow')" - @click="unfollowUser" - > - {{ $t('user_card.follow_progress') }} - </button> - </div> - <div - v-else - class="btn-group" - > - <button - class="btn btn-default pressed" - :title="$t('user_card.follow_unfollow')" - @click="unfollowUser" - > - {{ $t('user_card.following') }} - </button> - <ProgressButton - v-if="!user.subscribed" - class="btn btn-default" - :click="subscribeUser" - :title="$t('user_card.subscribe')" - > - <i class="icon-bell-alt" /> - </ProgressButton> - <ProgressButton - v-else - class="btn btn-default pressed" - :click="unsubscribeUser" - :title="$t('user_card.unsubscribe')" - > - <i class="icon-bell-ringing-o" /> - </ProgressButton> - </div> - - <div> - <button - class="btn btn-default btn-block" - @click="mentionUser" - > - {{ $t('user_card.mention') }} - </button> + <div class="btn-group"> + <FollowButton :user="user" /> + <template v-if="user.following"> + <ProgressButton + v-if="!user.subscribed" + class="btn btn-default" + :click="subscribeUser" + :title="$t('user_card.subscribe')" + > + <i class="icon-bell-alt" /> + </ProgressButton> + <ProgressButton + v-else + class="btn btn-default pressed" + :click="unsubscribeUser" + :title="$t('user_card.unsubscribe')" + > + <i class="icon-bell-ringing-o" /> + </ProgressButton> + </template> </div> - <div> <button v-if="user.muted" @@ -217,33 +175,6 @@ {{ $t('user_card.mute') }} </button> </div> - - <div> - <button - v-if="user.statusnet_blocking" - class="btn btn-default btn-block pressed" - @click="unblockUser" - > - {{ $t('user_card.blocked') }} - </button> - <button - v-else - class="btn btn-default btn-block" - @click="blockUser" - > - {{ $t('user_card.block') }} - </button> - </div> - - <div> - <button - class="btn btn-default btn-block" - @click="reportUser" - > - {{ $t('user_card.report') }} - </button> - </div> - <ModerationTools v-if="loggedIn.role === "admin"" :user="user" @@ -262,7 +193,7 @@ class="panel-body" > <div - v-if="!hideUserStatsLocal && switcher" + v-if="!mergedConfig.hideUserStats && switcher" class="user-counts" > <div @@ -345,6 +276,8 @@ mask-composite: exclude; background-size: cover; mask-size: 100% 60%; + border-top-left-radius: calc(var(--panelRadius) - 1px); + border-top-right-radius: calc(var(--panelRadius) - 1px); &.hide-bio { mask-size: 100% 40px; @@ -587,13 +520,12 @@ position: relative; display: flex; flex-flow: row wrap; - justify-content: space-between; margin-right: -.75em; > * { - 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 @@ <template> - <div + <Modal v-if="isOpen" - class="modal-view" - @click="closeModal" + @backdropClicked="closeModal" > - <div - class="user-reporting-panel panel" - @click.stop="" - > + <div class="user-reporting-panel panel"> <div class="panel-heading"> <div class="title"> {{ $t('user_reporting.title', [user.screen_name]) }} @@ -69,7 +65,7 @@ </div> </div> </div> - </div> + </Modal> </template> <script src="./user_reporting_modal.js"></script> diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index f12cccae..3fdc5340 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({ @@ -34,6 +35,7 @@ const MuteList = withSubscription({ const UserSettings = { data () { return { + newEmail: '', newName: this.$store.state.users.currentUser.name, newBio: unescape(this.$store.state.users.currentUser.description), newLocked: this.$store.state.users.currentUser.locked, @@ -55,6 +57,9 @@ const UserSettings = { backgroundPreview: null, bannerUploadError: null, backgroundUploadError: null, + changeEmailError: false, + changeEmailPassword: '', + changedEmail: false, deletingAccount: false, deleteAccountConfirmPasswordInput: '', deleteAccountError: false, @@ -82,7 +87,8 @@ const UserSettings = { ProgressButton, Importer, Exporter, - Mfa + Mfa, + Checkbox }, computed: { user () { @@ -303,6 +309,22 @@ const UserSettings = { } }) }, + changeEmail () { + const params = { + email: this.newEmail, + password: this.changeEmailPassword + } + this.$store.state.api.backendInteractor.changeEmail(params) + .then((res) => { + if (res.status === 'success') { + this.changedEmail = true + this.changeEmailError = false + } else { + this.changedEmail = false + this.changeEmailError = res.error + } + }) + }, activateTab (tabName) { this.activeTab = tabName }, diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index ef75ac52..8c18cf49 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -53,12 +53,9 @@ /> </EmojiInput> <p> - <input - id="account-locked" - v-model="newLocked" - type="checkbox" - > - <label for="account-locked">{{ $t('settings.lock_account_description') }}</label> + <Checkbox v-model="newLocked"> + {{ $t('settings.lock_account_description') }} + </Checkbox> </p> <div> <label for="default-vis">{{ $t('settings.default_vis') }}</label> @@ -75,69 +72,52 @@ </div> </div> <p> - <input - id="account-no-rich-text" - v-model="newNoRichText" - type="checkbox" - > - <label for="account-no-rich-text">{{ $t('settings.no_rich_text_description') }}</label> + <Checkbox v-model="newNoRichText"> + {{ $t('settings.no_rich_text_description') }} + </Checkbox> </p> <p> - <input - id="account-hide-follows" - v-model="hideFollows" - type="checkbox" - > - <label for="account-hide-follows">{{ $t('settings.hide_follows_description') }}</label> + <Checkbox v-model="hideFollows"> + {{ $t('settings.hide_follows_description') }} + </Checkbox> </p> <p class="setting-subitem"> - <input - id="account-hide-follows-count" + <Checkbox v-model="hideFollowsCount" - type="checkbox" :disabled="!hideFollows" > - <label for="account-hide-follows-count">{{ $t('settings.hide_follows_count_description') }}</label> + {{ $t('settings.hide_follows_count_description') }} + </Checkbox> </p> <p> - <input - id="account-hide-followers" + <Checkbox v-model="hideFollowers" - type="checkbox" > - <label for="account-hide-followers">{{ $t('settings.hide_followers_description') }}</label> + {{ $t('settings.hide_followers_description') }} + </Checkbox> </p> <p class="setting-subitem"> - <input - id="account-hide-followers-count" + <Checkbox v-model="hideFollowersCount" - type="checkbox" :disabled="!hideFollowers" > - <label for="account-hide-followers-count">{{ $t('settings.hide_followers_count_description') }}</label> + {{ $t('settings.hide_followers_count_description') }} + </Checkbox> </p> <p> - <input - id="account-show-role" - v-model="showRole" - type="checkbox" - > - <label - v-if="role === 'admin'" - for="account-show-role" - >{{ $t('settings.show_admin_badge') }}</label> - <label - v-if="role === 'moderator'" - for="account-show-role" - >{{ $t('settings.show_moderator_badge') }}</label> + <Checkbox v-model="showRole"> + <template v-if="role === 'admin'"> + {{ $t('settings.show_admin_badge') }} + </template> + <template v-if="role === 'moderator'"> + {{ $t('settings.show_moderator_badge') }} + </template> + </Checkbox> </p> <p> - <input - id="discoverable" - v-model="discoverable" - type="checkbox" - > - <label for="discoverable">{{ $t('settings.discoverable') }}</label> + <Checkbox v-model="discoverable"> + {{ $t('settings.discoverable') }} + </Checkbox> </p> <button :disabled="newName && newName.length === 0" @@ -254,6 +234,39 @@ <div :label="$t('settings.security_tab')"> <div class="setting-item"> + <h2>{{ $t('settings.change_email') }}</h2> + <div> + <p>{{ $t('settings.new_email') }}</p> + <input + v-model="newEmail" + type="email" + autocomplete="email" + > + </div> + <div> + <p>{{ $t('settings.current_password') }}</p> + <input + v-model="changeEmailPassword" + type="password" + autocomplete="current-password" + > + </div> + <button + class="btn btn-default" + @click="changeEmail" + > + {{ $t('general.submit') }} + </button> + <p v-if="changedEmail"> + {{ $t('settings.changed_email') }} + </p> + <template v-if="changeEmailError !== false"> + <p>{{ $t('settings.change_email_error') }}</p> + <p>{{ changeEmailError }}</p> + </template> + </div> + + <div class="setting-item"> <h2>{{ $t('settings.change_password') }}</h2> <div> <p>{{ $t('settings.current_password') }}</p> @@ -367,44 +380,24 @@ <span class="label">{{ $t('settings.notification_setting') }}</span> <ul class="option-list"> <li> - <input - id="notification-setting-follows" - v-model="notificationSettings.follows" - type="checkbox" - > - <label for="notification-setting-follows"> + <Checkbox v-model="notificationSettings.follows"> {{ $t('settings.notification_setting_follows') }} - </label> + </Checkbox> </li> <li> - <input - id="notification-setting-followers" - v-model="notificationSettings.followers" - type="checkbox" - > - <label for="notification-setting-followers"> + <Checkbox v-model="notificationSettings.followers"> {{ $t('settings.notification_setting_followers') }} - </label> + </Checkbox> </li> <li> - <input - id="notification-setting-non-follows" - v-model="notificationSettings.non_follows" - type="checkbox" - > - <label for="notification-setting-non-follows"> + <Checkbox v-model="notificationSettings.non_follows"> {{ $t('settings.notification_setting_non_follows') }} - </label> + </Checkbox> </li> <li> - <input - id="notification-setting-non-followers" - v-model="notificationSettings.non_followers" - type="checkbox" - > - <label for="notification-setting-non-followers"> + <Checkbox v-model="notificationSettings.non_followers"> {{ $t('settings.notification_setting_non_followers') }} - </label> + </Checkbox> </li> </ul> </div> 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 } } } |
