diff options
| author | Henry Jameson <me@hjkos.com> | 2019-10-29 09:36:16 +0200 |
|---|---|---|
| committer | Henry Jameson <me@hjkos.com> | 2019-10-29 09:36:16 +0200 |
| commit | b66564a30dfadfdd2b216072931576c6c4dcbd61 (patch) | |
| tree | de26f9a87bcaa6d8e576cf33165247f5c66b61af /src | |
| parent | c79b2501d275d8d022c6a9a2810211f606f14cde (diff) | |
| parent | 42f428d90e87315107dd20975548a5bd63dc2d53 (diff) | |
Merge remote-tracking branch 'upstream/develop' into settings-refactor
* upstream/develop: (89 commits)
remove needless ref
show preview popover when hover numbered replies
refactor conditions
do not make too many nested div
add fetchStatus action
refactor status loading logic
split status preview popover into a separate component
uninstall mobile-detect library
listen both events
minor css fix
restrict distance at top side only
set different trigger event in desktop and mobile by default
fix eslint warnings
fix popper go behind the top bar
migrate Popper to v-popover
fix popper go behind the top bar
fix eslint warnings
reset font-size to normal text size using rem
use top placement by default
hide status preview popper when hover popper content
...
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]) |
