abstracted mute confirmation dialog into its own component. mutes in status actions work now

This commit is contained in:
Henry Jameson 2025-01-16 20:14:05 +02:00
parent 41f54b687b
commit 68093b6276
13 changed files with 356 additions and 139 deletions

View file

@ -0,0 +1,111 @@
import { unitToSeconds } from 'src/services/date_utils/date_utils.js'
import { mapGetters } from 'vuex'
import ConfirmModal from './confirm_modal.vue'
import Select from 'src/components/select/select.vue'
export default {
props: ['type', 'user'],
emits: ['hide', 'show', 'muted'],
data: () => ({
showing: false,
muteExpiryAmount: 2,
muteExpiryUnit: 'hours'
}),
components: {
ConfirmModal,
Select
},
computed: {
muteExpiryValue () {
unitToSeconds(this.muteExpiryUnit, this.muteExpiryAmount)
},
muteExpiryUnits () {
return ['minutes', 'hours', 'days']
},
domain () {
return this.user.fqn.split('@')[1]
},
keypath () {
if (this.type === 'domain') {
return 'status.mute_domain_confirm'
} else if (this.type === 'conversation') {
return 'status.mute_conversation_confirm'
} else {
return 'user_card.mute_confirm'
}
},
userIsMuted () {
return this.$store.getters.relationship(this.user.id).muting
},
conversationIsMuted () {
return this.status.conversation_muted
},
domainIsMuted () {
return new Set(this.$store.state.users.currentUser.domainMutes).has(this.domain)
},
shouldConfirm () {
switch (this.type) {
case 'domain': {
return this.mergedConfig.modalOnMuteDomain
}
case 'conversation': {
return this.mergedConfig.modalOnMuteConversation
}
default: {
return this.mergedConfig.modalOnMute
}
}
},
...mapGetters(['mergedConfig'])
},
methods: {
optionallyPrompt () {
console.log('Triggered')
if (this.shouldConfirm) {
console.log('SHAWN!!')
this.show()
} else {
this.doMute()
}
},
show () {
this.showing = true
this.$emit('show')
},
hide () {
this.showing = false
this.$emit('hide')
},
doMute () {
switch (this.type) {
case 'domain': {
if (!this.domainIsMuted) {
this.$store.dispatch('muteDomain', { id: this.domain, expiresIn: this.muteExpiryValue })
} else {
this.$store.dispatch('unmuteDomain', { id: this.domain })
}
break
}
case 'conversation': {
if (!this.conversationIsMuted) {
this.$store.dispatch('muteConversation', { id: this.status.id, expiresIn: this.muteExpiryValue })
} else {
this.$store.dispatch('unmuteConversation', { id: this.status.id })
}
break
}
default: {
if (!this.userIsMuted) {
this.$store.dispatch('muteUser', { id: this.user.id, expiresIn: this.muteExpiryValue })
} else {
this.$store.dispatch('unmuteUser', { id: this.user.id })
}
break
}
}
this.$emit('muted')
this.hide()
}
}
}

View file

@ -0,0 +1,58 @@
<template>
<confirm-modal
v-if="showing"
: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="doMute"
@cancelled="hide"
>
<i18n-t
:keypath="keypath"
tag="div"
>
<template #domain>
<span v-text="domain" />
</template>
<template #user>
<span v-text="user.screen_name_ui" />
</template>
</i18n-t>
<div class="mute-expiry" v-if="type !== 'domain'">
<p>
<label>
{{ $t('user_card.mute_duration_prompt') }}
</label>
<input
v-model="muteExpiryAmount"
type="number"
class="input 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.${unit}_short`, ['']) }}
</option>
</Select>
</p>
</div>
</confirm-modal>
</template>
<script src="./mute_confirm.js" />
<style lang="scss">
.expiry-amount {
width: 4em;
text-align: right;
}
</style>

View file

@ -116,6 +116,16 @@
{{ $t('settings.confirm_dialogs_mute') }} {{ $t('settings.confirm_dialogs_mute') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<BooleanSetting path="modalOnMuteConversation">
{{ $t('settings.confirm_dialogs_mute_conversation') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnMuteDomain">
{{ $t('settings.confirm_dialogs_mute_domain') }}
</BooleanSetting>
</li>
<li> <li>
<BooleanSetting path="modalOnDelete"> <BooleanSetting path="modalOnDelete">
{{ $t('settings.confirm_dialogs_delete') }} {{ $t('settings.confirm_dialogs_delete') }}

View file

@ -84,8 +84,13 @@ export default {
} }
] ]
}, },
userIsMuted () {
return this.$store.getters.relationship(this.status.user.id).muting
},
threadIsMuted () {
return this.status.thread_muted
},
buttonInnerClass () { buttonInnerClass () {
if (!this.extra) console.log(this.button.name)
return [ return [
this.button.name + '-button', this.button.name + '-button',
{ {

View file

@ -1,5 +1,6 @@
import ActionButton from './action_button.vue' import ActionButton from './action_button.vue'
import Popover from 'src/components/popover/popover.vue' import Popover from 'src/components/popover/popover.vue'
import MuteConfirm from 'src/components/confirm_modal/mute_confirm.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -17,7 +18,72 @@ library.add(
export default { export default {
components: { components: {
ActionButton, ActionButton,
Popover Popover,
MuteConfirm
}, },
props: ['button'] props: ['button', 'status'],
mounted () {
if (this.button.name === 'mute') {
this.$store.dispatch('fetchDomainMutes')
}
},
computed: {
buttonClass () {
return [
this.button.name + '-button',
{
'-with-extra': this.button.name === 'bookmark',
'-extra': this.extra,
'-quick': !this.extra
}
]
},
user () {
return this.status.user
},
userIsMuted () {
return this.$store.getters.relationship(this.user.id).muting
},
conversationIsMuted () {
return this.status.thread_muted
},
domain () {
return this.user.fqn.split('@')[1]
},
domainIsMuted () {
return new Set(this.$store.state.users.currentUser.domainMutes).has(this.domain)
}
},
methods: {
unmuteUser () {
return this.$store.dispatch('unmuteUser', this.user.id)
},
unmuteThread () {
return this.$store.dispatch('unmuteConversation', this.user.id)
},
unmuteDomain () {
return this.$store.dispatch('unmuteDomain', this.user.id)
},
toggleUserMute () {
if (this.userIsMuted) {
this.unmuteUser()
} else {
this.$refs.confirmUser.optionallyPrompt()
}
},
toggleConversationMute () {
if (this.conversationIsMuted) {
this.unmuteConversation()
} else {
this.$refs.confirmConversation.optionallyPrompt()
}
},
toggleDomainMute () {
if (this.domainIsMuted) {
this.unmuteDomain()
} else {
this.$refs.confirmDomain.optionallyPrompt()
}
}
}
} }

View file

@ -1,58 +1,94 @@
<template> <template>
<Popover <div>
trigger="hover" <Popover
:placement="$attrs.extra ? 'right' : 'top'" trigger="hover"
v-if="button.dropdown?.()" :placement="$attrs.extra ? 'right' : 'top'"
> v-if="button.dropdown?.()"
<template #trigger> >
{{ props }} <template #trigger>
<ActionButton {{ props }}
:button="button" <ActionButton
v-bind.prop="$attrs" :button="button"
:status="status"
v-bind.prop="$attrs"
/>
</template>
<template #content>
<div
v-if="button.name === 'mute'"
class="dropdown-menu"
:id="`popup-menu-${randomSeed}`"
role="menu"
>
<div class="menu-item dropdown-item extra-action -icon">
<button
class="main-button"
@click="toggleUserMute"
>
<FAIcon icon="user" fixed-width />
<template v-if="userIsMuted">
{{ $t('status.unmute_user') }}
</template>
<template v-else>
{{ $t('status.mute_user') }}
</template>
</button>
</div>
<div class="menu-item dropdown-item extra-action -icon">
<button
class="main-button"
@click="toggleUserMute"
>
<FAIcon icon="folder-tree" fixed-width />
<template v-if="threadIsMuted">
{{ $t('status.unmute_conversation') }}
</template>
<template v-else>
{{ $t('status.mute_conversation') }}
</template>
</button>
</div>
<div class="menu-item dropdown-item extra-action -icon">
<button
class="main-button"
@click="toggleDomainMute"
>
<FAIcon icon="globe" fixed-width />
<template v-if="domainIsMuted">
{{ $t('status.unmute_domain') }}
</template>
<template v-else>
{{ $t('status.mute_domain') }}
</template>
</button>
</div>
</div>
</template>
</Popover>
<ActionButton
v-else
:button="button"
:status="status"
v-bind="$attrs"
/>
<teleport to="#modal">
<mute-confirm
type="conversation"
:status="this.status"
ref="confirmConversation"
/> />
</template> <mute-confirm
<template #content> type="domain"
<div :user="this.user"
v-if="button.name === 'mute'" ref="confirmDomain"
class="dropdown-menu" />
:id="`popup-menu-${randomSeed}`" <mute-confirm
role="menu" type="user"
> :user="this.user"
<div class="menu-item dropdown-item extra-action -icon"> ref="confirmUser"
<button />
class="main-button" </teleport>
@click="() => {}" </div>
>
<FAIcon icon="user" fixed-width />
{{ $t('status.mute_user') }}
</button>
</div>
<div class="menu-item dropdown-item extra-action -icon">
<button
class="main-button"
@click="() => {}"
>
<FAIcon icon="folder-tree" fixed-width />
{{ $t('status.mute_conversation') }}
</button>
</div>
<div class="menu-item dropdown-item extra-action -icon">
<button
class="main-button"
@click="() => {}"
>
<FAIcon icon="globe" fixed-width />
{{ $t('status.mute_domain') }}
</button>
</div>
</div>
</template>
</Popover>
<ActionButton
v-else
:button="button"
v-bind="$attrs"
/>
</template> </template>
<script src="./action_button_container.js"/> <script src="./action_button_container.js"/>

View file

@ -94,11 +94,6 @@ export const BUTTONS = [{
toggleable: true, toggleable: true,
dropdown: true dropdown: true
// action ({ status, dispatch, emit }) { // action ({ status, dispatch, emit }) {
// if (status.thread_muted) {
// return dispatch('unmuteConversation', { id: status.id })
// } else {
// return dispatch('muteConversation', { id: status.id })
// }
// } // }
}, { }, {
// ========= // =========

View file

@ -1,4 +1,3 @@
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'
@ -9,7 +8,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 MuteConfirm from '../confirm_modal/mute_confirm.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'
@ -48,7 +47,6 @@ export default {
data () { data () {
return { return {
followRequestInProgress: false, followRequestInProgress: false,
showingConfirmMute: false,
muteExpiryAmount: 0, muteExpiryAmount: 0,
muteExpiryUnit: 'minutes' muteExpiryUnit: 'minutes'
} }
@ -141,12 +139,6 @@ 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: {
@ -160,28 +152,11 @@ export default {
RichContent, RichContent,
UserLink, UserLink,
UserNote, UserNote,
ConfirmModal MuteConfirm
}, },
methods: { methods: {
showConfirmMute () {
this.showingConfirmMute = true
},
hideConfirmMute () {
this.showingConfirmMute = false
},
muteUser () { muteUser () {
if (!this.shouldConfirmMute) { this.$refs.confirmation.optionallyPrompt()
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

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

View file

@ -311,51 +311,11 @@
/> />
</div> </div>
<teleport to="#modal"> <teleport to="#modal">
<confirm-modal <mute-confirm
v-if="showingConfirmMute" type="user"
:title="$t('user_card.mute_confirm_title')" :user="this.user"
:confirm-text="$t('user_card.mute_confirm_accept_button')" ref="confirmation"
: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> </teleport>
</div> </div>
</template> </template>

View file

@ -490,6 +490,8 @@
"confirm_dialogs_unfollow": "unfollowing a user", "confirm_dialogs_unfollow": "unfollowing a user",
"confirm_dialogs_block": "blocking a user", "confirm_dialogs_block": "blocking a user",
"confirm_dialogs_mute": "muting a user", "confirm_dialogs_mute": "muting a user",
"confirm_dialogs_mute_domain": "muting domains",
"confirm_dialogs_mute_conversation": "muting conversations",
"confirm_dialogs_delete": "deleting a status", "confirm_dialogs_delete": "deleting a status",
"confirm_dialogs_logout": "logging out", "confirm_dialogs_logout": "logging out",
"confirm_dialogs_approve_follow": "approving a follower", "confirm_dialogs_approve_follow": "approving a follower",

View file

@ -137,6 +137,8 @@ export const defaultState = {
modalOnUnfollow: undefined, // instance default modalOnUnfollow: undefined, // instance default
modalOnBlock: undefined, // instance default modalOnBlock: undefined, // instance default
modalOnMute: undefined, // instance default modalOnMute: undefined, // instance default
modalOnMuteConversation: undefined, // instance default
modalOnMuteDomain: undefined, // instance default
modalOnDelete: undefined, // instance default modalOnDelete: undefined, // instance default
modalOnLogout: undefined, // instance default modalOnLogout: undefined, // instance default
modalOnApproveFollow: undefined, // instance default modalOnApproveFollow: undefined, // instance default

View file

@ -77,6 +77,8 @@ const defaultState = {
modalOnUnfollow: false, modalOnUnfollow: false,
modalOnBlock: true, modalOnBlock: true,
modalOnMute: false, modalOnMute: false,
modalOnMuteConversation: false,
modalOnMuteDomain: true,
modalOnDelete: true, modalOnDelete: true,
modalOnLogout: true, modalOnLogout: true,
modalOnApproveFollow: false, modalOnApproveFollow: false,