Merge branch 'from/develop/tusooa/confirm-dialogs' into 'develop'

Confirmation dialogs

See merge request pleroma/pleroma-fe!1431
This commit is contained in:
HJ 2023-01-25 23:49:16 +00:00
commit 65e10f07de
35 changed files with 759 additions and 42 deletions

View File

@ -9,6 +9,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 -->
<div id="popovers" /> <div id="popovers" />
</body> </body>

View File

@ -71,7 +71,6 @@
<StatusHistoryModal v-if="editingAvailable" /> <StatusHistoryModal v-if="editingAvailable" />
<SettingsModal /> <SettingsModal />
<UpdateNotification /> <UpdateNotification />
<div id="modal" />
<GlobalNoticeList /> <GlobalNoticeList />
</div> </div>
</template> </template>

View File

@ -2,6 +2,7 @@ import { mapState } from 'vuex'
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 UserListMenu from 'src/components/user_list_menu/user_list_menu.vue' import UserListMenu from 'src/components/user_list_menu/user_list_menu.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 {
faEllipsisV faEllipsisV
@ -16,14 +17,30 @@ const AccountActions = {
'user', 'relationship' 'user', 'relationship'
], ],
data () { data () {
return { } return {
showingConfirmBlock: false,
showingConfirmRemoveFollower: false
}
}, },
components: { components: {
ProgressButton, ProgressButton,
Popover, Popover,
UserListMenu UserListMenu,
ConfirmModal
}, },
methods: { methods: {
showConfirmBlock () {
this.showingConfirmBlock = true
},
hideConfirmBlock () {
this.showingConfirmBlock = false
},
showConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = true
},
hideConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = false
},
showRepeats () { showRepeats () {
this.$store.dispatch('showReblogs', this.user.id) this.$store.dispatch('showReblogs', this.user.id)
}, },
@ -31,13 +48,29 @@ 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)
}, },
removeUserFromFollowers () { removeUserFromFollowers () {
if (!this.shouldConfirmRemoveUserFromFollowers) {
this.doRemoveUserFromFollowers()
} else {
this.showConfirmRemoveUserFromFollowers()
}
},
doRemoveUserFromFollowers () {
this.$store.dispatch('removeUserFromFollowers', this.user.id) this.$store.dispatch('removeUserFromFollowers', this.user.id)
this.hideConfirmRemoveUserFromFollowers()
}, },
reportUser () { reportUser () {
this.$store.dispatch('openUserReportingModal', { userId: this.user.id }) this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
@ -50,6 +83,12 @@ const AccountActions = {
} }
}, },
computed: { computed: {
shouldConfirmBlock () {
return this.$store.getters.mergedConfig.modalOnBlock
},
shouldConfirmRemoveUserFromFollowers () {
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
},
...mapState({ ...mapState({
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
}) })

View File

@ -74,6 +74,48 @@
</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 #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmRemoveFollower"
:title="$t('user_card.remove_follower_confirm_title')"
:confirm-text="$t('user_card.remove_follower_confirm_accept_button')"
:cancel-text="$t('user_card.remove_follower_confirm_cancel_button')"
@accepted="doRemoveUserFromFollowers"
@cancelled="hideConfirmRemoveUserFromFollowers"
>
<i18n-t
keypath="user_card.remove_follower_confirm"
tag="span"
>
<template #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,29 @@
<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="onAccept"
v-text="confirmText"
/>
<button
class="btn button-default"
@click.prevent="onCancel"
v-text="cancelText"
/>
</template>
</dialog-modal>
</template>
<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,
@ -30,7 +31,8 @@ library.add(
export default { export default {
components: { components: {
SearchBar SearchBar,
ConfirmModal
}, },
data: () => ({ data: () => ({
searchBarHidden: true, searchBarHidden: true,
@ -40,7 +42,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 },
@ -73,15 +76,32 @@ 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 () {
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()
}, },
onSearchBarToggled (hidden) { onSearchBarToggled (hidden) {
this.searchBarHidden = hidden this.searchBarHidden = hidden

View File

@ -76,6 +76,18 @@
</button> </button>
</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: rgb(27 31 35 / 50%); background: rgb(27 31 35 / 50%);
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,
@ -32,10 +33,14 @@ library.add(
const ExtraButtons = { const ExtraButtons = {
props: ['status'], props: ['status'],
components: { Popover }, components: {
Popover,
ConfirmModal
},
data () { data () {
return { return {
expanded: false expanded: false,
showingDeleteDialog: false
} }
}, },
methods: { methods: {
@ -46,11 +51,22 @@ const ExtraButtons = {
this.expanded = false this.expanded = false
}, },
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'))
@ -133,7 +149,10 @@ const ExtraButtons = {
isEdited () { isEdited () {
return this.status.edited_at !== null return this.status.edited_at !== null
}, },
editingAvailable () { return this.$store.state.instance.editingAvailable } editingAvailable () { return this.$store.state.instance.editingAvailable },
shouldConfirmDelete () {
return this.$store.getters.mergedConfig.modalOnDelete
}
} }
} }

View File

@ -165,6 +165,18 @@
/> />
</FALayers> </FALayers>
</span> </span>
<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

@ -24,6 +24,7 @@
/> />
<RemoveFollowerButton <RemoveFollowerButton
v-if="noFollowsYou && relationship.followed_by" v-if="noFollowsYou && relationship.followed_by"
:user="user"
:relationship="relationship" :relationship="relationship"
class="follow-card-button" class="follow-card-button"
/> />

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,5 +1,6 @@
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 NavigationPins from 'src/components/navigation/navigation_pins.vue' import NavigationPins from 'src/components/navigation/navigation_pins.vue'
@ -25,12 +26,14 @@ const MobileNav = {
components: { components: {
SideDrawer, SideDrawer,
Notifications, Notifications,
NavigationPins NavigationPins,
ConfirmModal
}, },
data: () => ({ data: () => ({
notificationsCloseGesture: undefined, notificationsCloseGesture: undefined,
notificationsOpen: false, notificationsOpen: false,
notificationsAtTop: true notificationsAtTop: true,
showingConfirmLogout: false
}), }),
created () { created () {
this.notificationsCloseGesture = GestureService.swipeGesture( this.notificationsCloseGesture = GestureService.swipeGesture(
@ -57,7 +60,11 @@ const MobileNav = {
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']), ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']),
chatsPinned () { chatsPinned () {
return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats') return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats')
} },
shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout
},
...mapGetters(['unreadChatCount'])
}, },
methods: { methods: {
toggleMobileSidebar () { toggleMobileSidebar () {
@ -88,9 +95,23 @@ const MobileNav = {
scrollMobileNotificationsToTop () { scrollMobileNotificationsToTop () {
this.$refs.mobileNotifications.scrollTo(0, 0) this.$refs.mobileNotifications.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

@ -88,6 +88,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>
@ -235,6 +247,16 @@
} }
} }
} }
.confirm-modal.dark-overlay {
&::before {
z-index: 3000;
}
.dialog-modal.panel {
z-index: 3001;
}
}
} }
</style> </style>

View File

@ -8,6 +8,7 @@ import Report from '../report/report.vue'
import UserLink from '../user_link/user_link.vue' import UserLink from '../user_link/user_link.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import UserPopover from '../user_popover/user_popover.vue' import UserPopover from '../user_popover/user_popover.vue'
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'
@ -43,7 +44,9 @@ const Notification = {
return { return {
statusExpanded: false, statusExpanded: 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'],
@ -56,7 +59,8 @@ const Notification = {
Report, Report,
RichContent, RichContent,
UserPopover, UserPopover,
UserLink UserLink,
ConfirmModal
}, },
methods: { methods: {
toggleStatusExpanded () { toggleStatusExpanded () {
@ -71,7 +75,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 })
@ -81,13 +104,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: {
@ -117,6 +149,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

@ -243,6 +243,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>
</article> </article>
</template> </template>

View File

@ -94,19 +94,10 @@ export default {
}, },
convertExpiryToUnit (unit, amount) { convertExpiryToUnit (unit, amount) {
// Note: we want seconds and not milliseconds // Note: we want seconds and not milliseconds
switch (unit) { return DateUtils.secondsToUnit(unit, amount)
case 'minutes': return (1000 * amount) / DateUtils.MINUTE
case 'hours': return (1000 * amount) / DateUtils.HOUR
case 'days': return (1000 * amount) / DateUtils.DAY
}
}, },
convertExpiryFromUnit (unit, amount) { convertExpiryFromUnit (unit, amount) {
// Note: we want seconds and not milliseconds return DateUtils.unitToSeconds(unit, amount)
switch (unit) {
case 'minutes': return 0.001 * amount * DateUtils.MINUTE
case 'hours': return 0.001 * amount * DateUtils.HOUR
case 'days': return 0.001 * amount * DateUtils.DAY
}
}, },
expiryAmountChange () { expiryAmountChange () {
this.expiryAmount = this.expiryAmount =

View File

@ -1,10 +1,16 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
export default { export default {
props: ['relationship'], props: ['user', 'relationship'],
data () { data () {
return { return {
inProgress: false inProgress: false,
showingConfirmRemoveFollower: false
} }
}, },
components: {
ConfirmModal
},
computed: { computed: {
label () { label () {
if (this.inProgress) { if (this.inProgress) {
@ -12,14 +18,31 @@ export default {
} else { } else {
return this.$t('user_card.remove_follower') return this.$t('user_card.remove_follower')
} }
},
shouldConfirmRemoveUserFromFollowers () {
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
} }
}, },
methods: { methods: {
showConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = true
},
hideConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = false
},
onClick () { onClick () {
if (!this.shouldConfirmRemoveUserFromFollowers) {
this.doRemoveUserFromFollowers()
} else {
this.showConfirmRemoveUserFromFollowers()
}
},
doRemoveUserFromFollowers () {
this.inProgress = true this.inProgress = true
this.$store.dispatch('removeUserFromFollowers', this.relationship.id).then(() => { this.$store.dispatch('removeUserFromFollowers', this.relationship.id).then(() => {
this.inProgress = false this.inProgress = false
}) })
this.hideConfirmRemoveUserFromFollowers()
} }
} }
} }

View File

@ -7,6 +7,27 @@
@click="onClick" @click="onClick"
> >
{{ label }} {{ label }}
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmRemoveFollower"
:title="$t('user_card.remove_follower_confirm_title')"
:confirm-text="$t('user_card.remove_follower_confirm_accept_button')"
:cancel-text="$t('user_card.remove_follower_confirm_cancel_button')"
@accepted="doRemoveUserFromFollowers"
@cancelled="hideConfirmRemoveUserFromFollowers"
>
<i18n-t
keypath="user_card.remove_follower_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,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 { import {
faRetweet, faRetweet,
@ -15,13 +16,24 @@ library.add(
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 {
@ -31,6 +43,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: {
@ -39,6 +58,9 @@ const RetweetButton = {
}, },
remoteInteractionLink () { remoteInteractionLink () {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id }) return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
},
shouldConfirmRepeat () {
return this.mergedConfig.modalOnRepeat
} }
} }
} }

View File

@ -59,6 +59,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

@ -148,6 +148,56 @@
</SizeSetting> </SizeSetting>
</div> </div>
</li> </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="modalOnLogout">
{{ $t('settings.confirm_dialogs_logout') }}
</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>
<li>
<BooleanSetting path="modalOnRemoveUserFromFollowers">
{{ $t('settings.confirm_dialogs_remove_follower') }}
</BooleanSetting>
</li>
</ul>
</li>
</ul> </ul>
</div> </div>
<div class="setting-item"> <div class="setting-item">

View File

@ -1,3 +1,4 @@
import { unitToSeconds } from 'src/services/date_utils/date_utils.js'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue' import RemoteFollow from '../remote_follow/remote_follow.vue'
import ProgressButton from '../progress_button/progress_button.vue' import ProgressButton from '../progress_button/progress_button.vue'
@ -8,6 +9,7 @@ import UserNote from '../user_note/user_note.vue'
import Select from '../select/select.vue' import Select from '../select/select.vue'
import UserLink from '../user_link/user_link.vue' import UserLink from '../user_link/user_link.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'
@ -46,7 +48,10 @@ 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,
muteExpiryAmount: 0,
muteExpiryUnit: 'minutes'
} }
}, },
created () { created () {
@ -137,6 +142,12 @@ export default {
supportsNote () { supportsNote () {
return 'note' in this.relationship return 'note' in this.relationship
}, },
shouldConfirmMute () {
return this.mergedConfig.modalOnMute
},
muteExpiryUnits () {
return ['minutes', 'hours', 'days']
},
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
components: { components: {
@ -149,11 +160,29 @@ export default {
Select, Select,
RichContent, RichContent,
UserLink, UserLink,
UserNote UserNote,
ConfirmModal
}, },
methods: { methods: {
showConfirmMute () {
this.showingConfirmMute = true
},
hideConfirmMute () {
this.showingConfirmMute = false
},
muteUser () { muteUser () {
this.$store.dispatch('muteUser', this.user.id) if (!this.shouldConfirmMute) {
this.doMuteUser()
} else {
this.showConfirmMute()
}
},
doMuteUser () {
this.$store.dispatch('muteUser', {
id: this.user.id,
expiresIn: this.shouldConfirmMute ? unitToSeconds(this.muteExpiryUnit, this.muteExpiryAmount) : 0
})
this.hideConfirmMute()
}, },
unmuteUser () { unmuteUser () {
this.$store.dispatch('unmuteUser', this.user.id) this.$store.dispatch('unmuteUser', this.user.id)

View File

@ -355,3 +355,8 @@
text-decoration: none; text-decoration: none;
} }
} }
.mute-expiry {
display: flex;
flex-direction: row;
}

View File

@ -314,6 +314,53 @@
: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="div"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
<div
class="mute-expiry"
>
<label>
{{ $t('user_card.mute_duration_prompt') }}
</label>
<input
v-model="muteExpiryAmount"
type="number"
class="expiry-amount hide-number-spinner"
:min="0"
>
<Select
v-model="muteExpiryUnit"
unstyled="true"
class="expiry-unit"
>
<option
v-for="unit in muteExpiryUnits"
:key="unit"
:value="unit"
>
{{ $t(`time.${unit}_short`, ['']) }}
</option>
</Select>
</div>
</confirm-modal>
</teleport>
</div> </div>
</template> </template>

View File

@ -137,6 +137,10 @@
"login": "Log in", "login": "Log in",
"description": "Log in with OAuth", "description": "Log in with OAuth",
"logout": "Log out", "logout": "Log out",
"logout_confirm_title": "Logout confirmation",
"logout_confirm": "Do you really want to logout?",
"logout_confirm_accept_button": "Logout",
"logout_confirm_cancel_button": "Do not logout",
"password": "Password", "password": "Password",
"placeholder": "e.g. lain", "placeholder": "e.g. lain",
"register": "Register", "register": "Register",
@ -420,6 +424,16 @@
"composing": "Composing", "composing": "Composing",
"confirm_new_password": "Confirm new password", "confirm_new_password": "Confirm new password",
"current_password": "Current password", "current_password": "Current password",
"confirm_dialogs": "Ask for confirmation when",
"confirm_dialogs_repeat": "repeating a status",
"confirm_dialogs_unfollow": "unfollowing a user",
"confirm_dialogs_block": "blocking a user",
"confirm_dialogs_mute": "muting a user",
"confirm_dialogs_delete": "deleting a status",
"confirm_dialogs_logout": "logging out",
"confirm_dialogs_approve_follow": "approving a follower",
"confirm_dialogs_deny_follow": "denying a follower",
"confirm_dialogs_remove_follower": "removing a follower",
"mutes_and_blocks": "Mutes and Blocks", "mutes_and_blocks": "Mutes and Blocks",
"data_import_export_tab": "Data import / export", "data_import_export_tab": "Data import / export",
"default_vis": "Default visibility scope", "default_vis": "Default visibility scope",
@ -847,6 +861,10 @@
"status": { "status": {
"favorites": "Favorites", "favorites": "Favorites",
"repeats": "Repeats", "repeats": "Repeats",
"repeat_confirm": "Do you really want to repeat this status?",
"repeat_confirm_title": "Repeat confirmation",
"repeat_confirm_accept_button": "Repeat",
"repeat_confirm_cancel_button": "Do not repeat",
"delete": "Delete status", "delete": "Delete status",
"edit": "Edit status", "edit": "Edit status",
"edited_at": "(last edited {time})", "edited_at": "(last edited {time})",
@ -856,6 +874,9 @@
"bookmark": "Bookmark", "bookmark": "Bookmark",
"unbookmark": "Unbookmark", "unbookmark": "Unbookmark",
"delete_confirm": "Do you really want to delete this status?", "delete_confirm": "Do you really want to delete this status?",
"delete_confirm_title": "Delete confirmation",
"delete_confirm_accept_button": "Delete",
"delete_confirm_cancel_button": "Keep",
"reply_to": "Reply to", "reply_to": "Reply to",
"mentions": "Mentions", "mentions": "Mentions",
"replies_list": "Replies:", "replies_list": "Replies:",
@ -902,10 +923,22 @@
}, },
"user_card": { "user_card": {
"approve": "Approve", "approve": "Approve",
"approve_confirm_title": "Approve confirmation",
"approve_confirm_accept_button": "Approve",
"approve_confirm_cancel_button": "Do not approve",
"approve_confirm": "Do you want to approve {user}'s follow request?",
"block": "Block", "block": "Block",
"blocked": "Blocked!", "blocked": "Blocked!",
"block_confirm_title": "Block confirmation",
"block_confirm": "Do you really want to block {user}?",
"block_confirm_accept_button": "Block",
"block_confirm_cancel_button": "Do not block",
"deactivated": "Deactivated", "deactivated": "Deactivated",
"deny": "Deny", "deny": "Deny",
"deny_confirm_title": "Deny confirmation",
"deny_confirm_accept_button": "Deny",
"deny_confirm_cancel_button": "Do not deny",
"deny_confirm": "Do you want to deny {user}'s follow request?",
"edit_profile": "Edit profile", "edit_profile": "Edit profile",
"favorites": "Favorites", "favorites": "Favorites",
"follow": "Follow", "follow": "Follow",
@ -913,6 +946,10 @@
"follow_sent": "Request sent!", "follow_sent": "Request sent!",
"follow_progress": "Requesting…", "follow_progress": "Requesting…",
"follow_unfollow": "Unfollow", "follow_unfollow": "Unfollow",
"unfollow_confirm_title": "Unfollow confirmation",
"unfollow_confirm": "Do you really want to unfollow {user}?",
"unfollow_confirm_accept_button": "Unfollow",
"unfollow_confirm_cancel_button": "Do not unfollow",
"followees": "Following", "followees": "Following",
"followers": "Followers", "followers": "Followers",
"following": "Following!", "following": "Following!",
@ -924,9 +961,18 @@
"message": "Message", "message": "Message",
"mute": "Mute", "mute": "Mute",
"muted": "Muted", "muted": "Muted",
"mute_confirm_title": "Mute confirmation",
"mute_confirm": "Do you really want to mute {user}?",
"mute_confirm_accept_button": "Mute",
"mute_confirm_cancel_button": "Do not mute",
"mute_duration_prompt": "Mute this user for (0 for indefinite time):",
"per_day": "per day", "per_day": "per day",
"remote_follow": "Remote follow", "remote_follow": "Remote follow",
"remove_follower": "Remove follower", "remove_follower": "Remove follower",
"remove_follower_confirm_title": "Remove follower confirmation",
"remove_follower_confirm_accept_button": "Remove",
"remove_follower_confirm_cancel_button": "Keep",
"remove_follower_confirm": "Do you really want to remove {user} from your followers?",
"report": "Report", "report": "Report",
"statuses": "Statuses", "statuses": "Statuses",
"subscribe": "Subscribe", "subscribe": "Subscribe",

View File

@ -78,6 +78,15 @@ 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
modalOnRemoveUserFromFollowers: undefined, // instance default
playVideosInModal: false, playVideosInModal: false,
useOneClickNsfw: false, useOneClickNsfw: false,
useContainFit: true, useContainFit: true,

View File

@ -71,6 +71,15 @@ 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,
modalOnRemoveUserFromFollowers: false,
loginMethod: 'password', loginMethod: 'password',
logo: '/static/logo.svg', logo: '/static/logo.svg',
logoMargin: '.2em', logoMargin: '.2em',

View File

@ -61,13 +61,16 @@ const editUserNote = (store, { id, comment }) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship])) .then((relationship) => store.commit('updateUserRelationship', [relationship]))
} }
const muteUser = (store, id) => { const muteUser = (store, args) => {
const id = typeof args === 'object' ? args.id : args
const expiresIn = typeof args === 'object' ? args.expiresIn : 0
const predictedRelationship = store.state.relationships[id] || { id } const predictedRelationship = store.state.relationships[id] || { id }
predictedRelationship.muting = true predictedRelationship.muting = true
store.commit('updateUserRelationship', [predictedRelationship]) store.commit('updateUserRelationship', [predictedRelationship])
store.commit('addMuteId', id) store.commit('addMuteId', id)
return store.rootState.api.backendInteractor.muteUser({ id }) return store.rootState.api.backendInteractor.muteUser({ id, expiresIn })
.then((relationship) => { .then((relationship) => {
store.commit('updateUserRelationship', [relationship]) store.commit('updateUserRelationship', [relationship])
store.commit('addMuteId', id) store.commit('addMuteId', id)

View File

@ -1118,8 +1118,12 @@ const fetchMutes = ({ credentials }) => {
.then((users) => users.map(parseUser)) .then((users) => users.map(parseUser))
} }
const muteUser = ({ id, credentials }) => { const muteUser = ({ id, expiresIn, credentials }) => {
return promisedRequest({ url: MASTODON_MUTE_USER_URL(id), credentials, method: 'POST' }) const payload = {}
if (expiresIn) {
payload.expires_in = expiresIn
}
return promisedRequest({ url: MASTODON_MUTE_USER_URL(id), credentials, method: 'POST', payload })
} }
const unmuteUser = ({ id, credentials }) => { const unmuteUser = ({ id, credentials }) => {

View File

@ -41,3 +41,19 @@ export const relativeTimeShort = (date, nowThreshold = 1) => {
r.key += '_short' r.key += '_short'
return r return r
} }
export const unitToSeconds = (unit, amount) => {
switch (unit) {
case 'minutes': return 0.001 * amount * MINUTE
case 'hours': return 0.001 * amount * HOUR
case 'days': return 0.001 * amount * DAY
}
}
export const secondsToUnit = (unit, amount) => {
switch (unit) {
case 'minutes': return (1000 * amount) / MINUTE
case 'hours': return (1000 * amount) / HOUR
case 'days': return (1000 * amount) / DAY
}
}