Confirmation dialogs (#140)

supercedes #135

adapted from https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1431

Co-authored-by: Tusooa Zhu <tusooa@kazv.moe>
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/pleroma-fe#140
This commit is contained in:
floatingghost 2022-08-26 11:58:33 +00:00
parent f925fa6265
commit 776bee889e
27 changed files with 619 additions and 23 deletions

View file

@ -15,6 +15,7 @@
<body class="hidden"> <body class="hidden">
<noscript>To use Pleroma, please enable JavaScript.</noscript> <noscript>To use Pleroma, please enable JavaScript.</noscript>
<div id="app"></div> <div id="app"></div>
<div id="modal"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>
</html> </html>

View file

@ -59,7 +59,7 @@
<UserReportingModal /> <UserReportingModal />
<PostStatusModal /> <PostStatusModal />
<SettingsModal /> <SettingsModal />
<div id="modal" /> <UpdateNotification />
<GlobalNoticeList /> <GlobalNoticeList />
</div> </div>
</template> </template>

View file

@ -1,6 +1,8 @@
import ProgressButton from '../progress_button/progress_button.vue' import ProgressButton from '../progress_button/progress_button.vue'
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { mapState } from 'vuex'
import { import {
faEllipsisV faEllipsisV
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
@ -14,13 +16,22 @@ const AccountActions = {
'user', 'relationship' 'user', 'relationship'
], ],
data () { data () {
return { } return {
showingConfirmBlock: false
}
}, },
components: { components: {
ProgressButton, ProgressButton,
Popover Popover,
ConfirmModal
}, },
methods: { methods: {
showConfirmBlock () {
this.showingConfirmBlock = true
},
hideConfirmBlock () {
this.showingConfirmBlock = false
},
showRepeats () { showRepeats () {
this.$store.dispatch('showReblogs', this.user.id) this.$store.dispatch('showReblogs', this.user.id)
}, },
@ -28,7 +39,15 @@ const AccountActions = {
this.$store.dispatch('hideReblogs', this.user.id) this.$store.dispatch('hideReblogs', this.user.id)
}, },
blockUser () { blockUser () {
if (!this.shouldConfirmBlock) {
this.doBlockUser()
} else {
this.showConfirmBlock()
}
},
doBlockUser () {
this.$store.dispatch('blockUser', this.user.id) this.$store.dispatch('blockUser', this.user.id)
this.hideConfirmBlock()
}, },
unblockUser () { unblockUser () {
this.$store.dispatch('unblockUser', this.user.id) this.$store.dispatch('unblockUser', this.user.id)
@ -36,6 +55,14 @@ const AccountActions = {
reportUser () { reportUser () {
this.$store.dispatch('openUserReportingModal', { userId: this.user.id }) this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
} }
},
computed: {
shouldConfirmBlock () {
return this.$store.getters.mergedConfig.modalOnBlock
},
...mapState({
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
})
} }
} }

View file

@ -59,6 +59,27 @@
</button> </button>
</template> </template>
</Popover> </Popover>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmBlock"
:title="$t('user_card.block_confirm_title')"
:confirm-text="$t('user_card.block_confirm_accept_button')"
:cancel-text="$t('user_card.block_confirm_cancel_button')"
@accepted="doBlockUser"
@cancelled="hideConfirmBlock"
>
<i18n-t
keypath="user_card.block_confirm"
tag="span"
>
<template v-slot:user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
</div> </div>
</template> </template>

View file

@ -0,0 +1,37 @@
import DialogModal from '../dialog_modal/dialog_modal.vue'
/**
* This component emits the following events:
* cancelled, emitted when the action should not be performed;
* accepted, emitted when the action should be performed;
*
* The caller should close this dialog after receiving any of the two events.
*/
const ConfirmModal = {
components: {
DialogModal
},
props: {
title: {
type: String
},
cancelText: {
type: String
},
confirmText: {
type: String
}
},
computed: {
},
methods: {
onCancel () {
this.$emit('cancelled')
},
onAccept () {
this.$emit('accepted')
}
}
}
export default ConfirmModal

View file

@ -0,0 +1,39 @@
<template>
<dialog-modal
v-body-scroll-lock="true"
class="confirm-modal"
:on-cancel="onCancel"
>
<template #header>
<span v-text="title" />
</template>
<slot />
<template #footer>
<button
class="btn button-default"
@click.prevent="onCancel"
v-text="cancelText"
/>
<button
class="btn button-default button-positive"
@click.prevent="onAccept"
v-text="confirmText"
/>
</template>
</dialog-modal>
</template>
<style lang="scss" scoped>
@import '../../_variables';
.confirm-modal {
.button-positive {
border: 3px solid var(--accent, $fallback--link);
border-radius: var(--btnRadius, $fallback--btnRadius);
}
}
</style>
<script src="./confirm_modal.js"></script>

View file

@ -1,4 +1,5 @@
import SearchBar from 'components/search_bar/search_bar.vue' import SearchBar from 'components/search_bar/search_bar.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faSignInAlt, faSignInAlt,
@ -38,7 +39,8 @@ library.add(
export default { export default {
components: { components: {
SearchBar SearchBar,
ConfirmModal
}, },
data: () => ({ data: () => ({
searchBarHidden: true, searchBarHidden: true,
@ -48,7 +50,8 @@ export default {
window.CSS.supports('-moz-mask-size', 'contain') || window.CSS.supports('-moz-mask-size', 'contain') ||
window.CSS.supports('-ms-mask-size', 'contain') || window.CSS.supports('-ms-mask-size', 'contain') ||
window.CSS.supports('-o-mask-size', 'contain') window.CSS.supports('-o-mask-size', 'contain')
) ),
showingConfirmLogout: false
}), }),
computed: { computed: {
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
@ -92,7 +95,10 @@ export default {
hideSitename () { return this.$store.state.instance.hideSitename }, hideSitename () { return this.$store.state.instance.hideSitename },
logoLeft () { return this.$store.state.instance.logoLeft }, logoLeft () { return this.$store.state.instance.logoLeft },
currentUser () { return this.$store.state.users.currentUser }, currentUser () { return this.$store.state.users.currentUser },
privateMode () { return this.$store.state.instance.private } privateMode () { return this.$store.state.instance.private },
shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout
}
}, },
methods: { methods: {
scrollToTop () { scrollToTop () {

View file

@ -167,6 +167,18 @@
</a> </a>
</div> </div>
</div> </div>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmLogout"
:title="$t('login.logout_confirm_title')"
:confirm-text="$t('login.logout_confirm_accept_button')"
:cancel-text="$t('login.logout_confirm_cancel_button')"
@accepted="doLogout"
@cancelled="hideConfirmLogout"
>
{{ $t('login.logout_confirm') }}
</confirm-modal>
</teleport>
</nav> </nav>
</template> </template>
<script src="./desktop_nav.js"></script> <script src="./desktop_nav.js"></script>

View file

@ -39,7 +39,7 @@
right: 0; right: 0;
top: 0; top: 0;
background: rgba(27,31,35,.5); background: rgba(27,31,35,.5);
z-index: 99; z-index: 2000;
} }
} }
@ -51,7 +51,7 @@
margin: 15vh auto; margin: 15vh auto;
position: fixed; position: fixed;
transform: translateX(-50%); transform: translateX(-50%);
z-index: 999; z-index: 2001;
cursor: default; cursor: default;
display: block; display: block;
background-color: $fallback--bg; background-color: $fallback--bg;

View file

@ -1,4 +1,5 @@
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faEllipsisH, faEllipsisH,
@ -26,14 +27,34 @@ library.add(
const ExtraButtons = { const ExtraButtons = {
props: ['status'], props: ['status'],
components: { Popover }, components: {
Popover,
ConfirmModal
},
data () {
return {
expanded: false,
showingDeleteDialog: false
}
},
methods: { methods: {
deleteStatus () { deleteStatus () {
const confirmed = window.confirm(this.$t('status.delete_confirm')) if (this.shouldConfirmDelete) {
if (confirmed) { this.showDeleteStatusConfirmDialog()
this.$store.dispatch('deleteStatus', { id: this.status.id }) } else {
this.doDeleteStatus()
} }
}, },
doDeleteStatus () {
this.$store.dispatch('deleteStatus', { id: this.status.id })
this.hideDeleteStatusConfirmDialog()
},
showDeleteStatusConfirmDialog () {
this.showingDeleteDialog = true
},
hideDeleteStatusConfirmDialog () {
this.showingDeleteDialog = false
},
pinStatus () { pinStatus () {
this.$store.dispatch('pinStatus', this.status.id) this.$store.dispatch('pinStatus', this.status.id)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
@ -91,6 +112,9 @@ const ExtraButtons = {
}, },
statusLink () { statusLink () {
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}` return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
},
shouldConfirmDelete () {
return this.$store.getters.mergedConfig.modalOnDelete
} }
} }
} }

View file

@ -125,6 +125,18 @@
icon="ellipsis-h" icon="ellipsis-h"
/> />
</button> </button>
<teleport to="#modal">
<ConfirmModal
v-if="showingDeleteDialog"
:title="$t('status.delete_confirm_title')"
:cancel-text="$t('status.delete_confirm_cancel_button')"
:confirm-text="$t('status.delete_confirm_accept_button')"
@cancelled="hideDeleteStatusConfirmDialog"
@accepted="doDeleteStatus"
>
{{ $t('status.delete_confirm') }}
</ConfirmModal>
</teleport>
</template> </template>
</Popover> </Popover>
</template> </template>

View file

@ -1,12 +1,20 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
export default { export default {
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'], props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
components: {
ConfirmModal
},
data () { data () {
return { return {
inProgress: false inProgress: false,
showingConfirmUnfollow: false
} }
}, },
computed: { computed: {
shouldConfirmUnfollow () {
return this.$store.getters.mergedConfig.modalOnUnfollow
},
isPressed () { isPressed () {
return this.inProgress || this.relationship.following return this.inProgress || this.relationship.following
}, },
@ -35,6 +43,12 @@ export default {
} }
}, },
methods: { methods: {
showConfirmUnfollow () {
this.showingConfirmUnfollow = true
},
hideConfirmUnfollow () {
this.showingConfirmUnfollow = false
},
onClick () { onClick () {
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow() this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow()
}, },
@ -45,12 +59,21 @@ export default {
}) })
}, },
unfollow () { unfollow () {
if (this.shouldConfirmUnfollow) {
this.showConfirmUnfollow()
} else {
this.doUnfollow()
}
},
doUnfollow () {
const store = this.$store const store = this.$store
this.inProgress = true this.inProgress = true
requestUnfollow(this.relationship.id, store).then(() => { requestUnfollow(this.relationship.id, store).then(() => {
this.inProgress = false this.inProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id }) store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id })
}) })
this.hideConfirmUnfollow()
} }
} }
} }

View file

@ -7,6 +7,27 @@
@click="onClick" @click="onClick"
> >
{{ label }} {{ label }}
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmUnfollow"
:title="$t('user_card.unfollow_confirm_title')"
:confirm-text="$t('user_card.unfollow_confirm_accept_button')"
:cancel-text="$t('user_card.unfollow_confirm_cancel_button')"
@accepted="doUnfollow"
@cancelled="hideConfirmUnfollow"
>
<i18n-t
keypath="user_card.unfollow_confirm"
tag="span"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
</button> </button>
</template> </template>

View file

@ -1,10 +1,18 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js' import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
const FollowRequestCard = { const FollowRequestCard = {
props: ['user'], props: ['user'],
components: { components: {
BasicUserCard BasicUserCard,
ConfirmModal
},
data () {
return {
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false
}
}, },
methods: { methods: {
findFollowRequestNotificationId () { findFollowRequestNotificationId () {
@ -13,7 +21,26 @@ const FollowRequestCard = {
) )
return notif && notif.id return notif && notif.id
}, },
showApproveConfirmDialog () {
this.showingApproveConfirmDialog = true
},
hideApproveConfirmDialog () {
this.showingApproveConfirmDialog = false
},
showDenyConfirmDialog () {
this.showingDenyConfirmDialog = true
},
hideDenyConfirmDialog () {
this.showingDenyConfirmDialog = false
},
approveUser () { approveUser () {
if (this.shouldConfirmApprove) {
this.showApproveConfirmDialog()
} else {
this.doApprove()
}
},
doApprove () {
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
@ -25,14 +52,34 @@ const FollowRequestCard = {
notification.type = 'follow' notification.type = 'follow'
} }
}) })
this.hideApproveConfirmDialog()
}, },
denyUser () { denyUser () {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
doDeny () {
const notifId = this.findFollowRequestNotificationId() const notifId = this.findFollowRequestNotificationId()
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
.then(() => { .then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: notifId }) this.$store.dispatch('dismissNotificationLocal', { id: notifId })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
}) })
this.hideDenyConfirmDialog()
}
},
computed: {
mergedConfig () {
return this.$store.getters.mergedConfig
},
shouldConfirmApprove () {
return this.mergedConfig.modalOnApproveFollow
},
shouldConfirmDeny () {
return this.mergedConfig.modalOnDenyFollow
} }
} }
} }

View file

@ -14,6 +14,28 @@
{{ $t('user_card.deny') }} {{ $t('user_card.deny') }}
</button> </button>
</div> </div>
<teleport to="#modal">
<confirm-modal
v-if="showingApproveConfirmDialog"
:title="$t('user_card.approve_confirm_title')"
:confirm-text="$t('user_card.approve_confirm_accept_button')"
:cancel-text="$t('user_card.approve_confirm_cancel_button')"
@accepted="doApprove"
@cancelled="hideApproveConfirmDialog"
>
{{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
<confirm-modal
v-if="showingDenyConfirmDialog"
:title="$t('user_card.deny_confirm_title')"
:confirm-text="$t('user_card.deny_confirm_accept_button')"
:cancel-text="$t('user_card.deny_confirm_cancel_button')"
@accepted="doDeny"
@cancelled="hideDenyConfirmDialog"
>
{{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
</teleport>
</basic-user-card> </basic-user-card>
</template> </template>

View file

@ -1,7 +1,9 @@
import SideDrawer from '../side_drawer/side_drawer.vue' import SideDrawer from '../side_drawer/side_drawer.vue'
import Notifications from '../notifications/notifications.vue' import Notifications from '../notifications/notifications.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
import GestureService from '../../services/gesture_service/gesture_service' import GestureService from '../../services/gesture_service/gesture_service'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faTimes, faTimes,
@ -18,11 +20,13 @@ library.add(
const MobileNav = { const MobileNav = {
components: { components: {
SideDrawer, SideDrawer,
Notifications Notifications,
ConfirmModal
}, },
data: () => ({ data: () => ({
notificationsCloseGesture: undefined, notificationsCloseGesture: undefined,
notificationsOpen: false notificationsOpen: false,
showingConfirmLogout: false
}), }),
created () { created () {
this.notificationsCloseGesture = GestureService.swipeGesture( this.notificationsCloseGesture = GestureService.swipeGesture(
@ -47,7 +51,11 @@ const MobileNav = {
hideSiteName () { hideSiteName () {
return this.mergedConfig.hideSiteName return this.mergedConfig.hideSiteName
}, },
sitename () { return this.$store.state.instance.name } sitename () { return this.$store.state.instance.name },
shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout
},
...mapGetters(['unreadChatCount'])
}, },
methods: { methods: {
toggleMobileSidebar () { toggleMobileSidebar () {
@ -73,9 +81,23 @@ const MobileNav = {
scrollToTop () { scrollToTop () {
window.scrollTo(0, 0) window.scrollTo(0, 0)
}, },
showConfirmLogout () {
this.showingConfirmLogout = true
},
hideConfirmLogout () {
this.showingConfirmLogout = false
},
logout () { logout () {
if (!this.shouldConfirmLogout) {
this.doLogout()
} else {
this.showConfirmLogout()
}
},
doLogout () {
this.$router.replace('/main/public') this.$router.replace('/main/public')
this.$store.dispatch('logout') this.$store.dispatch('logout')
this.hideConfirmLogout()
}, },
markNotificationsAsSeen () { markNotificationsAsSeen () {
// this.$refs.notifications.markAsSeen() // this.$refs.notifications.markAsSeen()

View file

@ -76,6 +76,18 @@
ref="sideDrawer" ref="sideDrawer"
:logout="logout" :logout="logout"
/> />
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmLogout"
:title="$t('login.logout_confirm_title')"
:confirm-text="$t('login.logout_confirm_accept_button')"
:cancel-text="$t('login.logout_confirm_cancel_button')"
@accepted="doLogout"
@cancelled="hideConfirmLogout"
>
{{ $t('login.logout_confirm') }}
</confirm-modal>
</teleport>
</div> </div>
</template> </template>
@ -206,6 +218,14 @@
} }
} }
} }
.confirm-modal.dark-overlay {
&::before {
z-index: 3000;
}
.dialog-modal.panel {
z-index: 3001;
}
}
} }
</style> </style>

View file

@ -5,6 +5,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue' import Timeago from '../timeago/timeago.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -36,7 +37,9 @@ const Notification = {
return { return {
userExpanded: false, userExpanded: false,
betterShadow: this.$store.state.interface.browserSupport.cssFilter, betterShadow: this.$store.state.interface.browserSupport.cssFilter,
unmuted: false unmuted: false,
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false
} }
}, },
props: [ 'notification' ], props: [ 'notification' ],
@ -46,7 +49,8 @@ const Notification = {
UserCard, UserCard,
Timeago, Timeago,
Status, Status,
RichContent RichContent,
ConfirmModal
}, },
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded () {
@ -61,7 +65,26 @@ const Notification = {
toggleMute () { toggleMute () {
this.unmuted = !this.unmuted this.unmuted = !this.unmuted
}, },
showApproveConfirmDialog () {
this.showingApproveConfirmDialog = true
},
hideApproveConfirmDialog () {
this.showingApproveConfirmDialog = false
},
showDenyConfirmDialog () {
this.showingDenyConfirmDialog = true
},
hideDenyConfirmDialog () {
this.showingDenyConfirmDialog = false
},
approveUser () { approveUser () {
if (this.shouldConfirmApprove) {
this.showApproveConfirmDialog()
} else {
this.doApprove()
}
},
doApprove () {
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id }) this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id })
@ -71,13 +94,22 @@ const Notification = {
notification.type = 'follow' notification.type = 'follow'
} }
}) })
this.hideApproveConfirmDialog()
}, },
denyUser () { denyUser () {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
doDeny () {
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
.then(() => { .then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id }) this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
}) })
this.hideDenyConfirmDialog()
} }
}, },
computed: { computed: {
@ -107,6 +139,15 @@ const Notification = {
isStatusNotification () { isStatusNotification () {
return isStatusNotification(this.notification.type) return isStatusNotification(this.notification.type)
}, },
mergedConfig () {
return this.$store.getters.mergedConfig
},
shouldConfirmApprove () {
return this.mergedConfig.modalOnApproveFollow
},
shouldConfirmDeny () {
return this.mergedConfig.modalOnDenyFollow
},
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: state => state.users.currentUser
}) })

View file

@ -231,6 +231,28 @@
</template> </template>
</div> </div>
</div> </div>
<teleport to="#modal">
<confirm-modal
v-if="showingApproveConfirmDialog"
:title="$t('user_card.approve_confirm_title')"
:confirm-text="$t('user_card.approve_confirm_accept_button')"
:cancel-text="$t('user_card.approve_confirm_cancel_button')"
@accepted="doApprove"
@cancelled="hideApproveConfirmDialog"
>
{{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
<confirm-modal
v-if="showingDenyConfirmDialog"
:title="$t('user_card.deny_confirm_title')"
:confirm-text="$t('user_card.deny_confirm_accept_button')"
:cancel-text="$t('user_card.deny_confirm_cancel_button')"
@accepted="doDeny"
@cancelled="hideDenyConfirmDialog"
>
{{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
</teleport>
</div> </div>
</template> </template>

View file

@ -1,3 +1,4 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faRetweet } from '@fortawesome/free-solid-svg-icons' import { faRetweet } from '@fortawesome/free-solid-svg-icons'
@ -5,13 +6,24 @@ library.add(faRetweet)
const RetweetButton = { const RetweetButton = {
props: ['status', 'loggedIn', 'visibility'], props: ['status', 'loggedIn', 'visibility'],
components: {
ConfirmModal
},
data () { data () {
return { return {
animated: false animated: false,
showingConfirmDialog: false
} }
}, },
methods: { methods: {
retweet () { retweet () {
if (!this.status.repeated && this.shouldConfirmRepeat) {
this.showConfirmDialog()
} else {
this.doRetweet()
}
},
doRetweet () {
if (!this.status.repeated) { if (!this.status.repeated) {
this.$store.dispatch('retweet', { id: this.status.id }) this.$store.dispatch('retweet', { id: this.status.id })
} else { } else {
@ -21,6 +33,13 @@ const RetweetButton = {
setTimeout(() => { setTimeout(() => {
this.animated = false this.animated = false
}, 500) }, 500)
this.hideConfirmDialog()
},
showConfirmDialog () {
this.showingConfirmDialog = true
},
hideConfirmDialog () {
this.showingConfirmDialog = false
} }
}, },
computed: { computed: {
@ -29,6 +48,9 @@ const RetweetButton = {
}, },
mergedConfig () { mergedConfig () {
return this.$store.getters.mergedConfig return this.$store.getters.mergedConfig
},
shouldConfirmRepeat () {
return this.mergedConfig.modalOnRepeat
} }
} }
} }

View file

@ -33,6 +33,18 @@
> >
{{ status.repeat_num }} {{ status.repeat_num }}
</span> </span>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmDialog"
:title="$t('status.repeat_confirm_title')"
:confirm-text="$t('status.repeat_confirm_accept_button')"
:cancel-text="$t('status.repeat_confirm_cancel_button')"
@accepted="doRetweet"
@cancelled="hideConfirmDialog"
>
{{ $t('status.repeat_confirm') }}
</confirm-modal>
</teleport>
</div> </div>
</template> </template>

View file

@ -172,6 +172,77 @@
{{ $t('settings.autohide_floating_post_button') }} {{ $t('settings.autohide_floating_post_button') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<h3>{{ $t('settings.columns') }}</h3>
</li>
<li>
<BooleanSetting path="disableStickyHeaders">
{{ $t('settings.disable_sticky_headers') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="showScrollbars">
{{ $t('settings.show_scrollbars') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="sidebarRight">
{{ $t('settings.right_sidebar') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
v-if="user"
id="thirdColumnMode"
path="thirdColumnMode"
:options="thirdColumnModeOptions"
>
{{ $t('settings.third_column_mode') }}
</ChoiceSetting>
</li>
<li>
<h3>{{ $t('settings.confirmation_dialogs') }}</h3>
</li>
<li class="select-multiple">
<span class="label">{{ $t('settings.confirm_dialogs') }}</span>
<ul class="option-list">
<li>
<BooleanSetting path="modalOnRepeat">
{{ $t('settings.confirm_dialogs_repeat') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnUnfollow">
{{ $t('settings.confirm_dialogs_unfollow') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnBlock">
{{ $t('settings.confirm_dialogs_block') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnMute">
{{ $t('settings.confirm_dialogs_mute') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnDelete">
{{ $t('settings.confirm_dialogs_delete') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnApproveFollow">
{{ $t('settings.confirm_dialogs_approve_follow') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnDenyFollow">
{{ $t('settings.confirm_dialogs_deny_follow') }}
</BooleanSetting>
</li>
</ul>
</li>
</ul> </ul>
</div> </div>
<div class="setting-item"> <div class="setting-item">

View file

@ -6,6 +6,7 @@ import ModerationTools from '../moderation_tools/moderation_tools.vue'
import AccountActions from '../account_actions/account_actions.vue' import AccountActions from '../account_actions/account_actions.vue'
import Select from '../select/select.vue' import Select from '../select/select.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@ -32,7 +33,8 @@ export default {
data () { data () {
return { return {
followRequestInProgress: false, followRequestInProgress: false,
betterShadow: this.$store.state.interface.browserSupport.cssFilter betterShadow: this.$store.state.interface.browserSupport.cssFilter,
showingConfirmMute: false
} }
}, },
created () { created () {
@ -113,6 +115,9 @@ export default {
hideFollowersCount () { hideFollowersCount () {
return this.isOtherUser && this.user.hide_followers_count return this.isOtherUser && this.user.hide_followers_count
}, },
shouldConfirmMute () {
return this.mergedConfig.modalOnMute
},
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
components: { components: {
@ -123,14 +128,29 @@ export default {
ProgressButton, ProgressButton,
FollowButton, FollowButton,
Select, Select,
RichContent RichContent,
ConfirmModal
}, },
methods: { methods: {
refetchRelationship () { refetchRelationship () {
return this.$store.dispatch('fetchUserRelationship', this.user.id) return this.$store.dispatch('fetchUserRelationship', this.user.id)
}, },
showConfirmMute () {
this.showingConfirmMute = true
},
hideConfirmMute () {
this.showingConfirmMute = false
},
muteUser () { muteUser () {
if (!this.shouldConfirmMute) {
this.doMuteUser()
} else {
this.showConfirmMute()
}
},
doMuteUser () {
this.$store.dispatch('muteUser', this.user.id) this.$store.dispatch('muteUser', this.user.id)
this.hideConfirmMute()
}, },
unmuteUser () { unmuteUser () {
this.$store.dispatch('unmuteUser', this.user.id) this.$store.dispatch('unmuteUser', this.user.id)

View file

@ -295,6 +295,27 @@
:handle-links="true" :handle-links="true"
/> />
</div> </div>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmMute"
:title="$t('user_card.mute_confirm_title')"
:confirm-text="$t('user_card.mute_confirm_accept_button')"
:cancel-text="$t('user_card.mute_confirm_cancel_button')"
@accepted="doMuteUser"
@cancelled="hideConfirmMute"
>
<i18n-t
keypath="user_card.mute_confirm"
tag="span"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
</div> </div>
</template> </template>

View file

@ -394,7 +394,17 @@
"chatMessageRadius": "Chat message", "chatMessageRadius": "Chat message",
"checkboxRadius": "Checkboxes", "checkboxRadius": "Checkboxes",
"collapse_subject": "Collapse posts with subjects", "collapse_subject": "Collapse posts with subjects",
"columns": "Columns",
"composing": "Composing", "composing": "Composing",
"confirmation_dialogs": "Confirmation options",
"confirm_dialogs": "Require confirmation for:",
"confirm_dialogs_repeat": "Repeating a post",
"confirm_dialogs_unfollow": "Unfollowing someone",
"confirm_dialogs_block": "Blocking someone",
"confirm_dialogs_mute": "Muting someone",
"confirm_dialogs_delete": "Deleting a post",
"confirm_dialogs_approve_follow": "Accepting a follow request",
"confirm_dialogs_deny_follow": "Rejecting a follow request",
"confirm_new_password": "Confirm new password", "confirm_new_password": "Confirm new password",
"conversation_display": "Conversation display style", "conversation_display": "Conversation display style",
"conversation_display_linear": "Linear-style", "conversation_display_linear": "Linear-style",
@ -819,6 +829,9 @@
"copy_link": "Copy link to post", "copy_link": "Copy link to post",
"delete": "Delete post", "delete": "Delete post",
"delete_confirm": "Do you really want to delete this post?", "delete_confirm": "Do you really want to delete this post?",
"delete_confirm_title": "Confirm deletion",
"delete_confirm_accept_button": "Yes, delete it",
"delete_confirm_cancel_button": "No, keep it",
"expand": "Expand", "expand": "Expand",
"external_source": "External source", "external_source": "External source",
"favorites": "Favorites", "favorites": "Favorites",
@ -840,6 +853,10 @@
"replies_list": "Replies:", "replies_list": "Replies:",
"replies_list_with_others": "Replies (+{numReplies} other): | Replies (+{numReplies} others):", "replies_list_with_others": "Replies (+{numReplies} other): | Replies (+{numReplies} others):",
"reply_to": "Reply to", "reply_to": "Reply to",
"repeat_confirm": "Do you really want to repeat this post?",
"repeat_confirm_title": "Confirm repeat",
"repeat_confirm_accept_button": "Yes, repeat it",
"repeat_confirm_cancel_button": "No, don't repeat",
"show_all_attachments": "Show all attachments", "show_all_attachments": "Show all attachments",
"show_all_conversation": "Show full conversation ({numStatus} other post) | Show full conversation ({numStatus} other posts)", "show_all_conversation": "Show full conversation ({numStatus} other post) | Show full conversation ({numStatus} other posts)",
"show_all_conversation_with_icon": "{icon} {text}", "show_all_conversation_with_icon": "{icon} {text}",
@ -948,12 +965,24 @@
"strip_media": "Remove media from posts" "strip_media": "Remove media from posts"
}, },
"approve": "Approve", "approve": "Approve",
"approve_confirm_title": "Approve follow request",
"approve_confirm": "Are you sure you want to let this user follow you?",
"approve_confirm_accept_button": "Yes, accept",
"approve_confirm_cancel_button": "No, cancel",
"block": "Block", "block": "Block",
"block_confirm": "Are you sure you want to block {user}?",
"block_confirm_title": "Block user",
"block_confirm_cancel_button": "No, don't block",
"block_confirm_accept_button": "Yes, block",
"block_progress": "Blocking…", "block_progress": "Blocking…",
"blocked": "Blocked!", "blocked": "Blocked!",
"bot": "Bot", "bot": "Bot",
"deactivated": "Deactivated", "deactivated": "Deactivated",
"deny": "Deny", "deny": "Deny",
"deny_confirm_title": "Deny follow request",
"deny_confirm": "Are you sure you want to deny this user's follow request?",
"deny_confirm_accept_button": "Yes, deny",
"deny_confirm_cancel_button": "No, cancel",
"domain_muted": "Unblock domain", "domain_muted": "Unblock domain",
"edit_profile": "Edit profile", "edit_profile": "Edit profile",
"favorites": "Favorites", "favorites": "Favorites",
@ -979,6 +1008,10 @@
"mention": "Mention", "mention": "Mention",
"message": "Message", "message": "Message",
"mute": "Mute", "mute": "Mute",
"mute_confirm": "Are you sure you want to mute {user}?",
"mute_confirm_title": "Mute user",
"mute_confirm_cancel_button": "No, don't mute",
"mute_confirm_accept_button": "Yes, mute",
"mute_domain": "Block domain", "mute_domain": "Block domain",
"mute_progress": "Muting…", "mute_progress": "Muting…",
"muted": "Muted", "muted": "Muted",
@ -991,6 +1024,10 @@
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unblock": "Unblock", "unblock": "Unblock",
"unblock_progress": "Unblocking…", "unblock_progress": "Unblocking…",
"unfollow_confirm": "Are you sure you want to unfollow {user}?",
"unfollow_confirm_title": "Unfollow user",
"unfollow_confirm_cancel_button": "No, don't unfollow",
"unfollow_confirm_accept_button": "Yes, unfollow",
"unmute": "Unmute", "unmute": "Unmute",
"unmute_progress": "Unmuting…", "unmute_progress": "Unmuting…",
"unsubscribe": "Unsubscribe" "unsubscribe": "Unsubscribe"

View file

@ -81,6 +81,14 @@ export const defaultState = {
minimalScopesMode: undefined, // instance default minimalScopesMode: undefined, // instance default
// This hides statuses filtered via a word filter // This hides statuses filtered via a word filter
hideFilteredStatuses: undefined, // instance default hideFilteredStatuses: undefined, // instance default
modalOnRepeat: undefined, // instance default
modalOnUnfollow: undefined, // instance default
modalOnBlock: undefined, // instance default
modalOnMute: undefined, // instance default
modalOnDelete: undefined, // instance default
modalOnLogout: undefined, // instance default
modalOnApproveFollow: undefined, // instance default
modalOnDenyFollow: undefined, // instance default
playVideosInModal: false, playVideosInModal: false,
useOneClickNsfw: false, useOneClickNsfw: false,
useContainFit: true, useContainFit: true,

View file

@ -38,6 +38,14 @@ const defaultState = {
hideSiteName: false, hideSiteName: false,
hideUserStats: false, hideUserStats: false,
muteBotStatuses: false, muteBotStatuses: false,
modalOnRepeat: false,
modalOnUnfollow: false,
modalOnBlock: true,
modalOnMute: false,
modalOnDelete: true,
modalOnLogout: true,
modalOnApproveFollow: false,
modalOnDenyFollow: false,
loginMethod: 'password', loginMethod: 'password',
logo: '/static/logo.svg', logo: '/static/logo.svg',
logoMargin: '.2em', logoMargin: '.2em',