diff options
Diffstat (limited to 'src')
47 files changed, 786 insertions, 465 deletions
diff --git a/src/App.scss b/src/App.scss index 2190f91a..2d10f1e7 100644 --- a/src/App.scss +++ b/src/App.scss @@ -39,10 +39,13 @@ h4 { text-align: center; } +html { + font-size: 14px; +} + body { font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); - font-size: 14px; margin: 0; color: $fallback--text; color: var(--text, $fallback--text); @@ -705,31 +708,6 @@ nav { } } -@keyframes modal-background-fadein { - from { - background-color: rgba(0, 0, 0, 0); - } - to { - background-color: rgba(0, 0, 0, 0.5); - } -} - -.modal-view { - z-index: 1000; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - justify-content: center; - align-items: center; - overflow: auto; - animation-duration: 0.2s; - background-color: rgba(0, 0, 0, 0.5); - animation-name: modal-background-fadein; -} - .button-icon { font-size: 1.2em; } diff --git a/src/App.vue b/src/App.vue index 8d7f6c79..dbe842ec 100644 --- a/src/App.vue +++ b/src/App.vue @@ -54,6 +54,15 @@ /> </router-link> <a + v-if="currentUser && currentUser.role === 'admin'" + href="/pleroma/admin/#/login-pleroma" + class="mobile-hidden" + target="_blank" + ><i + class="button-icon icon-gauge nav-icon" + :title="$t('nav.administration')" + /></a> + <a v-if="currentUser" href="#" class="mobile-hidden" 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 21cd0351..06b496b0 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/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/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 96ac1b93..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.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/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/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/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_modal/post_status_modal.js b/src/components/post_status_modal/post_status_modal.js index 1033ba11..b44354db 100644 --- a/src/components/post_status_modal/post_status_modal.js +++ b/src/components/post_status_modal/post_status_modal.js @@ -1,24 +1,43 @@ 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 { + resettingForm: false + } }, computed: { isLoggedIn () { return !!this.$store.state.users.currentUser }, - isOpen () { - return this.isLoggedIn && this.$store.state.postStatus.modalActivated + modalActivated () { + return this.$store.state.postStatus.modalActivated + }, + isFormVisible () { + return this.isLoggedIn && !this.resettingForm && this.modalActivated }, params () { return this.$store.state.postStatus.params || {} } }, watch: { - isOpen (val) { + params (newVal, oldVal) { + if (get(newVal, 'repliedUser.id') !== get(oldVal, 'repliedUser.id')) { + this.resettingForm = true + this.$nextTick(() => { + this.resettingForm = false + }) + } + }, + isFormVisible (val) { if (val) { - this.$nextTick(() => this.$el.querySelector('textarea').focus()) + this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus()) } } }, diff --git a/src/components/post_status_modal/post_status_modal.vue b/src/components/post_status_modal/post_status_modal.vue index 3f8eec69..dbcd321e 100644 --- a/src/components/post_status_modal/post_status_modal.vue +++ b/src/components/post_status_modal/post_status_modal.vue @@ -1,13 +1,11 @@ <template> - <div - v-if="isOpen" - class="post-form-modal-view modal-view" - @click="closeModal" + <Modal + v-if="isLoggedIn && !resettingForm" + :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> @@ -17,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/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 5b2d4473..214b8e0c 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -123,6 +123,17 @@ </router-link> </li> <li + v-if="currentUser && currentUser.role === 'admin'" + @click="toggleDrawer" + > + <a + href="/pleroma/admin/#/login-pleroma" + target="_blank" + > + {{ $t("nav.administration") }} + </a> + </li> + <li v-if="currentUser" @click="toggleDrawer" > diff --git a/src/components/status/status.js b/src/components/status/status.js index fa46debf..4fbd5ac3 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -10,11 +10,12 @@ 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 = { @@ -38,8 +39,6 @@ const Status = { replying: false, unmuted: false, userExpanded: false, - preview: null, - showPreview: false, showingTall: this.inConversation && this.focused, showingLongSubject: false, error: null, @@ -293,7 +292,8 @@ const Status = { Gallery, LinkPreview, AvatarList, - Timeago + Timeago, + StatusPopover }, methods: { visibilityIcon (visibility) { @@ -368,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 a5559d5d..e48fef47 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 c84afe77..cc8a1ed6 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -1,14 +1,17 @@ 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, @@ -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 6bcc3aac..2755d89b 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 52cf0f36..d11b2d14 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -68,6 +68,7 @@ }, "nav": { "about": "About", + "administration": "Administration", "back": "Back", "chat": "Local Chat", "friend_requests": "Follow Requests", @@ -554,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", @@ -629,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 91c7f383..163eb707 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -45,8 +45,8 @@ "error": "Se ha producido un error al importar el archivo." }, "login": { - "login": "Identificación", - "description": "Identificación con OAuth", + "login": "Identificarse", + "description": "Identificarse con OAuth", "logout": "Cerrar sesión", "password": "Contraseña", "placeholder": "p.ej. lain", @@ -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/i18n/eu.json b/src/i18n/eu.json index ad8f4c05..90bc13a4 100644 --- a/src/i18n/eu.json +++ b/src/i18n/eu.json @@ -68,6 +68,7 @@ }, "nav": { "about": "Honi buruz", + "administration": "Administrazioa", "back": "Atzera", "chat": "Txat lokala", "friend_requests": "Jarraitzeko eskaerak", @@ -106,6 +107,15 @@ "expired": "Inkesta {0} bukatu zen", "not_enough_options": "Aukera gutxiegi inkestan" }, + "emoji": { + "stickers": "Pegatinak", + "emoji": "Emoji", + "keep_open": "Mantendu hautatzailea zabalik", + "search_emoji": "Bilatu emoji bat", + "add_emoji": "Emoji bat gehitu", + "custom": "Ohiko emojiak", + "unicode": "Unicode emojiak" + }, "stickers": { "add_sticker": "Pegatina gehitu" }, @@ -199,12 +209,12 @@ "avatarRadius": "Avatarrak", "background": "Atzeko planoa", "bio": "Biografia", - "block_export": "Bloke esportatzea", - "block_export_button": "Esportatu zure blokeak csv fitxategi batera", - "block_import": "Bloke inportazioa", - "block_import_error": "Errorea blokeak inportatzen", - "blocks_imported": "Blokeak inportaturik! Hauek prozesatzeak denbora hartuko du.", - "blocks_tab": "Blokeak", + "block_export": "Blokeatu dituzunak esportatu", + "block_export_button": "Esportatu blokeatutakoak csv fitxategi batera", + "block_import": "Blokeatu dituzunak inportatu", + "block_import_error": "Errorea blokeatutakoak inportatzen", + "blocks_imported": "Blokeatutakoak inportaturik! Hauek prozesatzeak denbora hartuko du.", + "blocks_tab": "Blokeatutakoak", "btnRadius": "Botoiak", "cBlue": "Urdina (erantzun, jarraitu)", "cGreen": "Berdea (Bertxiotu)", @@ -222,7 +232,9 @@ "data_import_export_tab": "Datuak Inportatu / Esportatu", "default_vis": "Lehenetsitako ikusgaitasunak", "delete_account": "Ezabatu kontua", + "discoverable": "Baimendu zure kontua kanpo bilaketa-emaitzetan eta bestelako zerbitzuetan agertzea", "delete_account_description": "Betirako ezabatu zure kontua eta zure mezu guztiak", + "pad_emoji": "Zuriuneak gehitu emoji bat aukeratzen denean", "delete_account_error": "Arazo bat gertatu da zure kontua ezabatzerakoan. Arazoa jarraitu eskero, administratzailearekin harremanetan jarri.", "delete_account_instructions": "Idatzi zure pasahitza kontua ezabatzeko.", "avatar_size_instruction": "Avatar irudien gomendatutako gutxieneko tamaina 150x150 pixel dira.", @@ -254,7 +266,7 @@ "instance_default": "(lehenetsia: {value})", "instance_default_simple": "(lehenetsia)", "interface": "Interfazea", - "interfaceLanguage": "Interfaze hizkuntza", + "interfaceLanguage": "Interfazearen hizkuntza", "invalid_theme_imported": "Hautatutako fitxategia ez da onartutako Pleroma gaia. Ez da zure gaian aldaketarik burutu.", "limited_availability": "Ez dago erabilgarri zure nabigatzailean", "links": "Estekak", @@ -277,6 +289,8 @@ "no_mutes": "Ez daude erabiltzaile mututuak", "hide_follows_description": "Ez erakutsi nor jarraitzen ari naizen", "hide_followers_description": "Ez erakutsi nor ari den ni jarraitzen", + "hide_follows_count_description": "Ez erakutsi jarraitzen ari naizen kontuen kopurua", + "hide_followers_count_description": "Ez erakutsi nire jarraitzaileen kontuen kopurua", "show_admin_badge": "Erakutsi Administratzaile etiketa nire profilan", "show_moderator_badge": "Erakutsi Moderatzaile etiketa nire profilan", "nsfw_clickthrough": "Gaitu klika hunkigarri eranskinak ezkutatzeko", @@ -449,7 +463,7 @@ }, "version": { "title": "Bertsioa", - "backend_version": "Backend Bertsio", + "backend_version": "Backend Bertsioa", "frontend_version": "Frontend Bertsioa" } }, @@ -529,6 +543,7 @@ "follows_you": "Jarraitzen dizu!", "its_you": "Zu zara!", "media": "Multimedia", + "mention": "Aipatu", "mute": "Isilarazi", "muted": "Isilduta", "per_day": "eguneko", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index f0ec88eb..592a7257 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -106,6 +106,15 @@ "expired": "いれふだは {0} まえに、おわりました", "not_enough_options": "ユニークなオプションが、たりません" }, + "emoji": { + "stickers": "ステッカー", + "emoji": "えもじ", + "keep_open": "ピッカーをあけたままにする", + "search_emoji": "えもじをさがす", + "add_emoji": "えもじをうちこむ", + "custom": "カスタムえもじ", + "unicode": "ユニコードえもじ" + }, "stickers": { "add_sticker": "ステッカーをふやす" }, @@ -224,10 +233,11 @@ "default_vis": "デフォルトのこうかいはんい", "delete_account": "アカウントをけす", "delete_account_description": "あなたのアカウントとメッセージが、きえます。", - "delete_account_error": "アカウントをけすことが、できなかったかもしれません。インスタンスのかんりしゃに、れんらくしてください。", + "delete_account_error": "アカウントをけすことが、できなかったかもしれません。インスタンスのアドミニストレーターに、おといあわせください。", "delete_account_instructions": "ほんとうにアカウントをけしてもいいなら、パスワードをかいてください。", "discoverable": "けんさくなどのサービスで、このアカウントをみつけてもよい", "avatar_size_instruction": "アバターのおおきさは、150×150ピクセルか、それよりもおおきくするといいです。", + "pad_emoji": "えもじをピッカーでえらんだとき、えもじのまわりにスペースをいれる", "export_theme": "セーブ", "filtering": "フィルタリング", "filtering_explanation": "これらのことばをふくむすべてのものがミュートされます。1ぎょうに1つのことばをかいてください。", @@ -280,6 +290,8 @@ "no_mutes": "ミュートしていません", "hide_follows_description": "フォローしているひとをみせない", "hide_followers_description": "フォロワーをみせない", + "hide_follows_count_description": "フォローしているひとのかずをみせない", + "hide_followers_count_description": "フォロワーのかずをみせない", "show_admin_badge": "アドミンのしるしをみせる", "show_moderator_badge": "モデレーターのしるしをみせる", "nsfw_clickthrough": "NSFWなファイルをかくす", @@ -532,6 +544,7 @@ "follows_you": "フォローされました!", "its_you": "これはあなたです!", "media": "メディア", + "mention": "メンション", "mute": "ミュート", "muted": "ミュートしています!", "per_day": "/日", @@ -611,5 +624,16 @@ "person_talking": "{count} にんが、はなしています", "people_talking": "{count} にんが、はなしています", "no_results": "みつかりませんでした" + }, + "password_reset": { + "forgot_password": "パスワードを、わすれましたか?", + "password_reset": "パスワードリセット", + "instruction": "あなたのメールアドレスかユーザーめいをいれてください。パスワードをリセットするためのリンクをおくります。", + "placeholder": "あなたのメールアドレスかユーザーめい", + "check_email": "パスワードをリセットするためのリンクがかかれたメールが、とどいているかどうか、みてください。", + "return_home": "ホームページにもどる", + "not_found": "そのメールアドレスまたはユーザーめいを、みつけることができませんでした。", + "too_many_requests": "パスワードリセットを、ためすことが、おおすぎます。しばらくしてから、ためしてください。", + "password_reset_disabled": "このインスタンスでは、パスワードリセットは、できません。インスタンスのアドミニストレーターに、おといあわせください。" } } diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json index c25f6f1d..2ca7dca8 100644 --- a/src/i18n/ja_pedantic.json +++ b/src/i18n/ja_pedantic.json @@ -106,6 +106,15 @@ "expired": "投票は {0} 前に終了しました", "not_enough_options": "相異なる選択肢が不足しています" }, + "emoji": { + "stickers": "ステッカー", + "emoji": "絵文字", + "keep_open": "ピッカーを開いたままにする", + "search_emoji": "絵文字を検索", + "add_emoji": "絵文字を挿入", + "custom": "カスタム絵文字", + "unicode": "Unicode絵文字" + }, "stickers": { "add_sticker": "ステッカーを追加" }, @@ -228,6 +237,7 @@ "delete_account_instructions": "本当にアカウントを消してもいいなら、パスワードを入力してください。", "discoverable": "検索などのサービスでこのアカウントを見つけることを許可する", "avatar_size_instruction": "アバターの大きさは、150×150ピクセルか、それよりも大きくするといいです。", + "pad_emoji": "ピッカーから絵文字を挿入するとき、絵文字の両側にスペースを入れる", "export_theme": "保存", "filtering": "フィルタリング", "filtering_explanation": "これらの言葉を含むすべてのものがミュートされます。1行に1つの言葉を書いてください。", @@ -280,6 +290,8 @@ "no_mutes": "ミュートはありません", "hide_follows_description": "フォローしている人を見せない", "hide_followers_description": "フォロワーを見せない", + "hide_follows_count_description": "フォローしている人の数を見せない", + "hide_followers_count_description": "フォロワーの数を見せない", "show_admin_badge": "管理者のバッジを見せる", "show_moderator_badge": "モデレーターのバッジを見せる", "nsfw_clickthrough": "NSFWなファイルを隠す", @@ -532,6 +544,7 @@ "follows_you": "フォローされました!", "its_you": "これはあなたです!", "media": "メディア", + "mention": "メンション", "mute": "ミュート", "muted": "ミュートしています!", "per_day": "/日", @@ -611,5 +624,16 @@ "person_talking": "{count} 人が話しています", "people_talking": "{count} 人が話しています", "no_results": "見つかりませんでした" + }, + "password_reset": { + "forgot_password": "パスワードを忘れましたか?", + "password_reset": "パスワードリセット", + "instruction": "メールアドレスまたはユーザー名を入力してください。パスワードをリセットするためのリンクを送信します。", + "placeholder": "メールアドレスまたはユーザー名", + "check_email": "パスワードをリセットするためのリンクが記載されたメールが届いているか確認してください。", + "return_home": "ホームページに戻る", + "not_found": "メールアドレスまたはユーザー名が見つかりませんでした。", + "too_many_requests": "試行回数の制限に達しました。しばらく時間を置いてから再試行してください。", + "password_reset_disabled": "このインスタンスではパスワードリセットは無効になっています。インスタンスの管理者に連絡してください。" } } diff --git a/src/main.js b/src/main.js index 7923ffe8..a9db1cff 100644 --- a/src/main.js +++ b/src/main.js @@ -41,7 +41,13 @@ Vue.use(VueChatScroll) Vue.use(VueClickOutside) Vue.use(PortalVue) Vue.use(VBodyScrollLock) -Vue.use(VTooltip) +Vue.use(VTooltip, { + popover: { + defaultTrigger: 'hover click', + defaultContainer: false, + defaultOffset: 5 + } +}) const i18n = new VueI18n({ // By default, use the browser locale, we will update it if neccessary diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 918065d2..f11ffdcd 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -537,6 +537,10 @@ const statuses = { setNotificationsSilence ({ rootState, commit }, { value }) { commit('setNotificationsSilence', { value }) }, + fetchStatus ({ rootState, dispatch }, id) { + rootState.api.backendInteractor.fetchStatus({ id }) + .then((status) => dispatch('addNewStatuses', { statuses: [status] })) + }, deleteStatus ({ rootState, commit }, status) { commit('setDeleted', { status }) apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials }) diff --git a/src/modules/users.js b/src/modules/users.js index 4d02f8d7..6d259dc2 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -60,6 +60,18 @@ const unmuteUser = (store, id) => { .then((relationship) => store.commit('updateUserRelationship', [relationship])) } +const hideReblogs = (store, userId) => { + return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: false }) + .then((relationship) => { + store.commit('updateUserRelationship', [relationship]) + }) +} + +const showReblogs = (store, userId) => { + return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: true }) + .then((relationship) => store.commit('updateUserRelationship', [relationship])) +} + export const mutations = { setMuted (state, { user: { id }, muted }) { const user = state.usersObject[id] @@ -135,6 +147,7 @@ export const mutations = { user.muted = relationship.muting user.statusnet_blocking = relationship.blocking user.subscribed = relationship.subscribing + user.showing_reblogs = relationship.showing_reblogs } }) }, @@ -272,6 +285,12 @@ const users = { unmuteUser (store, id) { return unmuteUser(store, id) }, + hideReblogs (store, id) { + return hideReblogs(store, id) + }, + showReblogs (store, id) { + return showReblogs(store, id) + }, muteUsers (store, ids = []) { return Promise.all(ids.map(id => muteUser(store, id))) }, diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 887d7d7a..61cd4f16 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -219,10 +219,16 @@ const authHeaders = (accessToken) => { } } -const followUser = ({ id, credentials }) => { +const followUser = ({ id, credentials, ...options }) => { let url = MASTODON_FOLLOW_URL(id) + const form = {} + if (options.reblogs !== undefined) { form['reblogs'] = options.reblogs } return fetch(url, { - headers: authHeaders(credentials), + body: JSON.stringify(form), + headers: { + ...authHeaders(credentials), + 'Content-Type': 'application/json' + }, method: 'POST' }).then((data) => data.json()) } diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 3c44a10c..cbf48ee4 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -31,8 +31,8 @@ const backendInteractorService = credentials => { return apiService.fetchUserRelationship({ id, credentials }) } - const followUser = (id) => { - return apiService.followUser({ credentials, id }) + const followUser = ({ id, reblogs }) => { + return apiService.followUser({ credentials, id, reblogs }) } const unfollowUser = (id) => { diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index fc326ad1..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 } @@ -196,9 +197,11 @@ export const parseAttachment = (data) => { return output } export const addEmojis = (string, emojis) => { + const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g return emojis.reduce((acc, emoji) => { + const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&') return acc.replace( - new RegExp(`:${emoji.shortcode}:`, 'g'), + new RegExp(`:${regexSafeShortCode}:`, 'g'), `<img src='${emoji.url}' alt='${emoji.shortcode}' title='${emoji.shortcode}' class='emoji' />` ) }, string) 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]) |
