diff options
35 files changed, 484 insertions, 307 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4c2fd1..d08da09e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] ### Added +- Ability to hide/show repeats from user +- User profile button clutter organized into a menu - Emoji picker - Started changelog anew ### Changed diff --git a/src/App.scss b/src/App.scss index 2190f91a..f20eb44c 100644 --- a/src/App.scss +++ b/src/App.scss @@ -705,31 +705,6 @@ nav { } } -@keyframes modal-background-fadein { - from { - background-color: rgba(0, 0, 0, 0); - } - to { - background-color: rgba(0, 0, 0, 0.5); - } -} - -.modal-view { - z-index: 1000; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - justify-content: center; - align-items: center; - overflow: auto; - animation-duration: 0.2s; - background-color: rgba(0, 0, 0, 0.5); - animation-name: modal-background-fadein; -} - .button-icon { font-size: 1.2em; } diff --git a/src/boot/routes.js b/src/boot/routes.js index cd02711c..5670236c 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -47,7 +47,7 @@ export default (store) => { { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, { name: 'settings', path: '/settings', component: Settings }, { name: 'registration', path: '/registration', component: Registration }, - { name: 'password-reset', path: '/password-reset', component: PasswordReset }, + { name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true }, { name: 'registration-token', path: '/registration/:token', component: Registration }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute }, diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js new file mode 100644 index 00000000..204d506a --- /dev/null +++ b/src/components/account_actions/account_actions.js @@ -0,0 +1,35 @@ +import ProgressButton from '../progress_button/progress_button.vue' + +const AccountActions = { + props: [ + 'user' + ], + data () { + return { } + }, + components: { + ProgressButton + }, + methods: { + showRepeats () { + this.$store.dispatch('showReblogs', this.user.id) + }, + hideRepeats () { + this.$store.dispatch('hideReblogs', this.user.id) + }, + blockUser () { + this.$store.dispatch('blockUser', this.user.id) + }, + unblockUser () { + this.$store.dispatch('unblockUser', this.user.id) + }, + reportUser () { + this.$store.dispatch('openUserReportingModal', this.user.id) + }, + mentionUser () { + this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user }) + } + } +} + +export default AccountActions diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue new file mode 100644 index 00000000..046cba93 --- /dev/null +++ b/src/components/account_actions/account_actions.vue @@ -0,0 +1,93 @@ +<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..0880bd7f 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -10,7 +10,8 @@ const Attachment = { 'statusId', 'size', 'allowPlay', - 'setMedia' + 'setMedia', + 'naturalSizeLoad' ], data () { 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/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..bd0de608 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 } }, 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/login_form/login_form.js b/src/components/login_form/login_form.js index 10f52fe2..0b574a04 100644 --- a/src/components/login_form/login_form.js +++ b/src/components/login_form/login_form.js @@ -59,6 +59,8 @@ const LoginForm = { if (result.error) { if (result.error === 'mfa_required') { this.requireMFA({ app: app, settings: result }) + } else if (result.identifier === 'password_reset_required') { + this.$router.push({ name: 'password-reset', params: { passwordResetRequested: true } }) } else { this.error = result.error this.focusOnPasswordInput() diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 992d7129..4832abda 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -1,11 +1,13 @@ import StillImage from '../still-image/still-image.vue' import VideoAttachment from '../video_attachment/video_attachment.vue' +import Modal from '../modal/modal.vue' import fileTypeService from '../../services/file_type/file_type.service.js' const MediaModal = { components: { StillImage, - VideoAttachment + VideoAttachment, + Modal }, computed: { showing () { diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 06ced5a1..2597f4e3 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -1,9 +1,8 @@ <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'" @@ -33,21 +32,15 @@ > <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; @@ -114,5 +107,4 @@ } } } - </style> 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/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/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/status/status.vue b/src/components/status/status.vue index 93f37a49..912f77d2 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -840,6 +840,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/still-image/still-image.js b/src/components/still-image/still-image.js index 02e98f19..9c2d446b 100644 --- a/src/components/still-image/still-image.js +++ b/src/components/still-image/still-image.js @@ -3,7 +3,8 @@ const StillImage = { 'src', 'referrerpolicy', 'mimetype', - 'imageLoadError' + 'imageLoadError', + 'imageLoadHandler' ], data () { return { @@ -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/user_card/user_card.js b/src/components/user_card/user_card.js index 9c931c01..0107cfa6 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -1,13 +1,16 @@ 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' export default { - props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar' ], + props: [ + 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar' + ], data () { return { followRequestInProgress: false, @@ -96,30 +99,11 @@ export default { 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..f5cba09a 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" @@ -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" @@ -587,13 +518,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/directives/body_scroll_lock.js b/src/directives/body_scroll_lock.js index 6ab20c3f..13a6de1c 100644 --- a/src/directives/body_scroll_lock.js +++ b/src/directives/body_scroll_lock.js @@ -2,42 +2,49 @@ import * as bodyScrollLock from 'body-scroll-lock' let previousNavPaddingRight let previousAppBgWrapperRight +const lockerEls = new Set([]) const disableBodyScroll = (el) => { const scrollBarGap = window.innerWidth - document.documentElement.clientWidth bodyScrollLock.disableBodyScroll(el, { reserveScrollBarGap: true }) + lockerEls.add(el) setTimeout(() => { - // If previousNavPaddingRight is already set, don't set it again. - if (previousNavPaddingRight === undefined) { - const navEl = document.getElementById('nav') - previousNavPaddingRight = window.getComputedStyle(navEl).getPropertyValue('padding-right') - navEl.style.paddingRight = previousNavPaddingRight ? `calc(${previousNavPaddingRight} + ${scrollBarGap}px)` : `${scrollBarGap}px` + if (lockerEls.size <= 1) { + // If previousNavPaddingRight is already set, don't set it again. + if (previousNavPaddingRight === undefined) { + const navEl = document.getElementById('nav') + previousNavPaddingRight = window.getComputedStyle(navEl).getPropertyValue('padding-right') + navEl.style.paddingRight = previousNavPaddingRight ? `calc(${previousNavPaddingRight} + ${scrollBarGap}px)` : `${scrollBarGap}px` + } + // If previousAppBgWrapeprRight is already set, don't set it again. + if (previousAppBgWrapperRight === undefined) { + const appBgWrapperEl = document.getElementById('app_bg_wrapper') + previousAppBgWrapperRight = window.getComputedStyle(appBgWrapperEl).getPropertyValue('right') + appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px` + } + document.body.classList.add('scroll-locked') } - // If previousAppBgWrapeprRight is already set, don't set it again. - if (previousAppBgWrapperRight === undefined) { - const appBgWrapperEl = document.getElementById('app_bg_wrapper') - previousAppBgWrapperRight = window.getComputedStyle(appBgWrapperEl).getPropertyValue('right') - appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px` - } - document.body.classList.add('scroll-locked') }) } const enableBodyScroll = (el) => { + lockerEls.delete(el) setTimeout(() => { - if (previousNavPaddingRight !== undefined) { - document.getElementById('nav').style.paddingRight = previousNavPaddingRight - // Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again. - previousNavPaddingRight = undefined - } - if (previousAppBgWrapperRight !== undefined) { - document.getElementById('app_bg_wrapper').style.right = previousAppBgWrapperRight - // Restore previousAppBgWrapperRight to undefined so disableBodyScroll knows it can be set again. - previousAppBgWrapperRight = undefined + if (lockerEls.size === 0) { + if (previousNavPaddingRight !== undefined) { + document.getElementById('nav').style.paddingRight = previousNavPaddingRight + // Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again. + previousNavPaddingRight = undefined + } + if (previousAppBgWrapperRight !== undefined) { + document.getElementById('app_bg_wrapper').style.right = previousAppBgWrapperRight + // Restore previousAppBgWrapperRight to undefined so disableBodyScroll knows it can be set again. + previousAppBgWrapperRight = undefined + } + document.body.classList.remove('scroll-locked') } - document.body.classList.remove('scroll-locked') }) bodyScrollLock.enableBodyScroll(el) } diff --git a/src/i18n/en.json b/src/i18n/en.json index 32c25e3e..d11b2d14 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -555,6 +555,8 @@ "unmute": "Unmute", "unmute_progress": "Unmuting...", "mute_progress": "Muting...", + "hide_repeats": "Hide repeats", + "show_repeats": "Show repeats", "admin_menu": { "moderation": "Moderation", "grant_admin": "Grant Admin", @@ -630,6 +632,8 @@ "return_home": "Return to the home page", "not_found": "We couldn't find that email or username.", "too_many_requests": "You have reached the limit of attempts, try again later.", - "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator." + "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator.", + "password_reset_required": "You must reset your password to log in.", + "password_reset_required_but_mailer_is_disabled": "You must reset your password, but password reset is disabled. Please contact your instance administrator." } } diff --git a/src/i18n/es.json b/src/i18n/es.json index 92a93cdd..163eb707 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -68,6 +68,7 @@ }, "nav": { "about": "Acerca de", + "administration": "Administración", "back": "Volver", "chat": "Chat Local", "friend_requests": "Solicitudes de seguimiento", @@ -106,6 +107,15 @@ "expired": "La encuesta terminó hace {0}", "not_enough_options": "Muy pocas opciones únicas en la encuesta" }, + "emoji": { + "stickers": "Pegatinas", + "emoji": "Emoji", + "keep_open": "Mantener el selector abierto", + "search_emoji": "Buscar un emoji", + "add_emoji": "Insertar un emoji", + "custom": "Emojis personalizados", + "unicode": "Emojis unicode" + }, "stickers": { "add_sticker": "Añadir Pegatina" }, @@ -222,7 +232,9 @@ "data_import_export_tab": "Importar / Exportar Datos", "default_vis": "Alcance de visibilidad por defecto", "delete_account": "Eliminar la cuenta", + "discoverable": "Permitir la aparición de esta cuenta en los resultados de búsqueda y otros servicios", "delete_account_description": "Eliminar para siempre la cuenta y todos los mensajes.", + "pad_emoji": "Rellenar con espacios al agregar emojis desde el selector", "delete_account_error": "Hubo un error al eliminar tu cuenta. Si el fallo persiste, ponte en contacto con el administrador de tu instancia.", "delete_account_instructions": "Escribe tu contraseña para confirmar la eliminación de tu cuenta.", "avatar_size_instruction": "El tamaño mínimo recomendado para el avatar es de 150X150 píxeles.", @@ -277,6 +289,8 @@ "no_mutes": "No hay usuarios sinlenciados", "hide_follows_description": "No mostrar a quién sigo", "hide_followers_description": "No mostrar quién me sigue", + "hide_follows_count_description": "No mostrar el número de cuentas que sigo", + "hide_followers_count_description": "No mostrar el número de cuentas que me siguen", "show_admin_badge": "Mostrar la insignia de Administrador en mi perfil", "show_moderator_badge": "Mostrar la insignia de Moderador en mi perfil", "nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW", @@ -529,6 +543,7 @@ "follows_you": "¡Te sigue!", "its_you": "¡Eres tú!", "media": "Media", + "mention": "Mencionar", "mute": "Silenciar", "muted": "Silenciado", "per_day": "por día", diff --git a/src/modules/users.js b/src/modules/users.js index 4d02f8d7..6d259dc2 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -60,6 +60,18 @@ const unmuteUser = (store, id) => { .then((relationship) => store.commit('updateUserRelationship', [relationship])) } +const hideReblogs = (store, userId) => { + return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: false }) + .then((relationship) => { + store.commit('updateUserRelationship', [relationship]) + }) +} + +const showReblogs = (store, userId) => { + return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: true }) + .then((relationship) => store.commit('updateUserRelationship', [relationship])) +} + export const mutations = { setMuted (state, { user: { id }, muted }) { const user = state.usersObject[id] @@ -135,6 +147,7 @@ export const mutations = { user.muted = relationship.muting user.statusnet_blocking = relationship.blocking user.subscribed = relationship.subscribing + user.showing_reblogs = relationship.showing_reblogs } }) }, @@ -272,6 +285,12 @@ const users = { unmuteUser (store, id) { return unmuteUser(store, id) }, + hideReblogs (store, id) { + return hideReblogs(store, id) + }, + showReblogs (store, id) { + return showReblogs(store, id) + }, muteUsers (store, ids = []) { return Promise.all(ids.map(id => muteUser(store, id))) }, diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 887d7d7a..61cd4f16 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -219,10 +219,16 @@ const authHeaders = (accessToken) => { } } -const followUser = ({ id, credentials }) => { +const followUser = ({ id, credentials, ...options }) => { let url = MASTODON_FOLLOW_URL(id) + const form = {} + if (options.reblogs !== undefined) { form['reblogs'] = options.reblogs } return fetch(url, { - headers: authHeaders(credentials), + body: JSON.stringify(form), + headers: { + ...authHeaders(credentials), + 'Content-Type': 'application/json' + }, method: 'POST' }).then((data) => data.json()) } diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 3c44a10c..cbf48ee4 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -31,8 +31,8 @@ const backendInteractorService = credentials => { return apiService.fetchUserRelationship({ id, credentials }) } - const followUser = (id) => { - return apiService.followUser({ credentials, id }) + const followUser = ({ id, reblogs }) => { + return apiService.followUser({ credentials, id, reblogs }) } const unfollowUser = (id) => { diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 67664af8..5f45660d 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -69,6 +69,7 @@ export const parseUser = (data) => { output.following = relationship.following output.statusnet_blocking = relationship.blocking output.muted = relationship.muting + output.showing_reblogs = relationship.showing_reblogs output.subscribed = relationship.subscribing } diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js index d82ce593..598cb5f7 100644 --- a/src/services/follow_manipulate/follow_manipulate.js +++ b/src/services/follow_manipulate/follow_manipulate.js @@ -14,7 +14,7 @@ const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => { }) export const requestFollow = (user, store) => new Promise((resolve, reject) => { - store.state.api.backendInteractor.followUser(user.id) + store.state.api.backendInteractor.followUser({ id: user.id }) .then((updated) => { store.commit('updateUserRelationship', [updated]) |
