some basic expiration modal. "don't as again" doesn't work yet

This commit is contained in:
Henry Jameson 2025-06-12 20:04:39 +03:00
commit b9161ef697
17 changed files with 117 additions and 124 deletions

View file

@ -675,11 +675,6 @@ option {
} }
} }
.btn-block {
display: block;
width: 100%;
}
.btn-group { .btn-group {
position: relative; position: relative;
display: inline-flex; display: inline-flex;

View file

@ -247,6 +247,7 @@ const getNodeInfo = async ({ store }) => {
const data = await res.json() const data = await res.json()
const metadata = data.metadata const metadata = data.metadata
const features = metadata.features const features = metadata.features
console.log(features)
store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName }) store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName })
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations }) store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
@ -262,6 +263,7 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') }) store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') })
store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') }) store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') })
store.dispatch('setInstanceOption', { name: 'blockExpiration', value: features.includes('pleroma:block_expiration') })
const uploadLimits = metadata.uploadLimits const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })

View file

@ -3,6 +3,7 @@ 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 ConfirmModal from '../confirm_modal/confirm_modal.vue'
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faEllipsisV faEllipsisV
@ -27,15 +28,10 @@ const AccountActions = {
ProgressButton, ProgressButton,
Popover, Popover,
UserListMenu, UserListMenu,
ConfirmModal ConfirmModal,
UserTimedFilterModal
}, },
methods: { methods: {
showConfirmBlock () {
this.showingConfirmBlock = true
},
hideConfirmBlock () {
this.showingConfirmBlock = false
},
showConfirmRemoveUserFromFollowers () { showConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = true this.showingConfirmRemoveFollower = true
}, },
@ -49,10 +45,14 @@ const AccountActions = {
this.$store.dispatch('hideReblogs', this.user.id) this.$store.dispatch('hideReblogs', this.user.id)
}, },
blockUser () { blockUser () {
if (!this.shouldConfirmBlock) { if (this.$refs.timedBlockDialog) {
this.doBlockUser() this.$refs.timedBlockDialog.optionallyPrompt()
} else { } else {
this.showConfirmBlock() if (!this.shouldConfirmBlock) {
this.doBlockUser()
} else {
this.showingConfirmBlock = true
}
} }
}, },
doBlockUser () { doBlockUser () {
@ -91,6 +91,7 @@ const AccountActions = {
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
}, },
...mapState({ ...mapState({
blockExpirationSupported: state => state.instance.blockExpiration,
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
}) })
} }

View file

@ -96,7 +96,8 @@
</Popover> </Popover>
<teleport to="#modal"> <teleport to="#modal">
<confirm-modal <confirm-modal
v-if="showingConfirmBlock" v-if="showingConfirmBlock && !blockExpirationSupported"
ref="blockDialog"
:title="$t('user_card.block_confirm_title')" :title="$t('user_card.block_confirm_title')"
:confirm-text="$t('user_card.block_confirm_accept_button')" :confirm-text="$t('user_card.block_confirm_accept_button')"
:cancel-text="$t('user_card.block_confirm_cancel_button')" :cancel-text="$t('user_card.block_confirm_cancel_button')"
@ -137,6 +138,12 @@
</template> </template>
</i18n-t> </i18n-t>
</confirm-modal> </confirm-modal>
<UserTimedFilterModal
v-if="blockExpirationSupported"
:is-mute="false"
:user="user"
ref="timedBlockDialog"
/>
</teleport> </teleport>
</div> </div>
</template> </template>

View file

@ -1,4 +1,3 @@
import { unitToSeconds } from 'src/services/date_utils/date_utils.js'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import ConfirmModal from './confirm_modal.vue' import ConfirmModal from './confirm_modal.vue'
@ -8,21 +7,13 @@ export default {
props: ['type', 'user', 'status'], props: ['type', 'user', 'status'],
emits: ['hide', 'show', 'muted'], emits: ['hide', 'show', 'muted'],
data: () => ({ data: () => ({
showing: false, showing: false
muteExpiryAmount: 2,
muteExpiryUnit: 'hours'
}), }),
components: { components: {
ConfirmModal, ConfirmModal,
Select Select
}, },
computed: { computed: {
muteExpiryValue () {
unitToSeconds(this.muteExpiryUnit, this.muteExpiryAmount)
},
muteExpiryUnits () {
return ['minutes', 'hours', 'days']
},
domain () { domain () {
return this.user.fqn.split('@')[1] return this.user.fqn.split('@')[1]
}, },
@ -31,13 +22,8 @@ export default {
return 'status.mute_domain_confirm' return 'status.mute_domain_confirm'
} else if (this.type === 'conversation') { } else if (this.type === 'conversation') {
return 'status.mute_conversation_confirm' return 'status.mute_conversation_confirm'
} else {
return 'user_card.mute_confirm'
} }
}, },
userIsMuted () {
return this.$store.getters.relationship(this.user.id).muting
},
conversationIsMuted () { conversationIsMuted () {
return this.status.conversation_muted return this.status.conversation_muted
}, },
@ -49,12 +35,9 @@ export default {
case 'domain': { case 'domain': {
return this.mergedConfig.modalOnMuteDomain return this.mergedConfig.modalOnMuteDomain
} }
case 'conversation': { default: { // conversation
return this.mergedConfig.modalOnMuteConversation return this.mergedConfig.modalOnMuteConversation
} }
default: {
return this.mergedConfig.modalOnMute
}
} }
}, },
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
@ -79,7 +62,7 @@ export default {
switch (this.type) { switch (this.type) {
case 'domain': { case 'domain': {
if (!this.domainIsMuted) { if (!this.domainIsMuted) {
this.$store.dispatch('muteDomain', { id: this.domain, expiresIn: this.muteExpiryValue }) this.$store.dispatch('muteDomain', { id: this.domain })
} else { } else {
this.$store.dispatch('unmuteDomain', { id: this.domain }) this.$store.dispatch('unmuteDomain', { id: this.domain })
} }
@ -87,20 +70,12 @@ export default {
} }
case 'conversation': { case 'conversation': {
if (!this.conversationIsMuted) { if (!this.conversationIsMuted) {
this.$store.dispatch('muteConversation', { id: this.status.id, expiresIn: this.muteExpiryValue }) this.$store.dispatch('muteConversation', { id: this.status.id })
} else { } else {
this.$store.dispatch('unmuteConversation', { id: this.status.id }) this.$store.dispatch('unmuteConversation', { id: this.status.id })
} }
break 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.$emit('muted')
this.hide() this.hide()

View file

@ -18,36 +18,6 @@
<span v-text="user.screen_name_ui" /> <span v-text="user.screen_name_ui" />
</template> </template>
</i18n-t> </i18n-t>
<div
v-if="type !== 'domain'"
class="mute-expiry"
>
<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> </confirm-modal>
</template> </template>

View file

@ -107,7 +107,7 @@ const FilteringTab = {
...mapActions(useServerSideStorageStore, ['setPreference', 'unsetPreference', 'pushServerSideStorage']), ...mapActions(useServerSideStorageStore, ['setPreference', 'unsetPreference', 'pushServerSideStorage']),
getDatetimeLocal (timestamp) { getDatetimeLocal (timestamp) {
const date = new Date(timestamp) const date = new Date(timestamp)
let fmt = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 2}) const fmt = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 2})
const datetime = [ const datetime = [
date.getFullYear(), date.getFullYear(),
'-', '-',

View file

@ -1,6 +1,7 @@
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 MuteConfirm from 'src/components/confirm_modal/mute_confirm.vue'
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -19,7 +20,8 @@ export default {
components: { components: {
ActionButton, ActionButton,
Popover, Popover,
MuteConfirm MuteConfirm,
UserTimedFilterModal
}, },
props: ['button', 'status'], props: ['button', 'status'],
emits: ['interacted'], emits: ['interacted'],

View file

@ -94,11 +94,10 @@
:status="status" :status="status"
:user="user" :user="user"
/> />
<MuteConfirm <UserTimedFilterModal
ref="confirmUser" :is-mute="true"
type="user"
:status="status"
:user="user" :user="user"
ref="confirmUser"
/> />
</teleport> </teleport>
</div> </div>

View file

@ -8,7 +8,8 @@ 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 MuteConfirm from '../confirm_modal/mute_confirm.vue' import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_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 { usePostStatusStore } from 'src/stores/post_status' import { usePostStatusStore } from 'src/stores/post_status'
@ -48,6 +49,19 @@ export default {
'onClose', 'onClose',
'hasNoteEditor' 'hasNoteEditor'
], ],
components: {
UserAvatar,
RemoteFollow,
ModerationTools,
AccountActions,
ProgressButton,
FollowButton,
Select,
RichContent,
UserLink,
UserNote,
UserTimedFilterModal
},
data () { data () {
return { return {
followRequestInProgress: false, followRequestInProgress: false,
@ -63,6 +77,7 @@ export default {
return this.$store.getters.findUser(this.userId) return this.$store.getters.findUser(this.userId)
}, },
relationship () { relationship () {
console.log(this.$store.getters.relationship(this.userId))
return this.$store.getters.relationship(this.userId) return this.$store.getters.relationship(this.userId)
}, },
classes () { classes () {
@ -144,22 +159,9 @@ export default {
}, },
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
components: {
UserAvatar,
RemoteFollow,
ModerationTools,
AccountActions,
ProgressButton,
FollowButton,
Select,
RichContent,
UserLink,
UserNote,
MuteConfirm
},
methods: { methods: {
muteUser () { muteUser () {
this.$refs.confirmation.optionallyPrompt() this.$refs.timedMuteDialog.optionallyPrompt()
}, },
unmuteUser () { unmuteUser () {
this.$store.dispatch('unmuteUser', this.user.id) this.$store.dispatch('unmuteUser', this.user.id)

View file

@ -8,6 +8,11 @@
--_still-image-label-visibility: hidden; --_still-image-label-visibility: hidden;
} }
.btn-mute, .btn-mention {
display: block;
width: 100%;
}
.panel-heading { .panel-heading {
padding: 0.5em 0; padding: 0.5em 0;
text-align: center; text-align: center;

View file

@ -232,7 +232,7 @@
<div> <div>
<button <button
v-if="relationship.muting" v-if="relationship.muting"
class="btn button-default btn-block toggled" class="btn button-default btn-mute toggled"
:disabled="user.deactivated" :disabled="user.deactivated"
@click="unmuteUser" @click="unmuteUser"
> >
@ -240,7 +240,7 @@
</button> </button>
<button <button
v-else v-else
class="btn button-default btn-block" class="btn button-default btn-mute"
:disabled="user.deactivated" :disabled="user.deactivated"
@click="muteUser" @click="muteUser"
> >
@ -249,7 +249,7 @@
</div> </div>
<div> <div>
<button <button
class="btn button-default btn-block" class="btn button-default btn-mention"
:disabled="user.deactivated" :disabled="user.deactivated"
@click="mentionUser" @click="mentionUser"
> >
@ -314,10 +314,10 @@
/> />
</div> </div>
<teleport to="#modal"> <teleport to="#modal">
<MuteConfirm <UserTimedFilterModal
ref="confirmation"
type="user"
:user="user" :user="user"
:is-mute="true"
ref="timedMuteDialog"
/> />
</teleport> </teleport>
</div> </div>

View file

@ -1392,6 +1392,10 @@
"mute_confirm": "Do you really want to mute {user}?", "mute_confirm": "Do you really want to mute {user}?",
"mute_confirm_accept_button": "Mute", "mute_confirm_accept_button": "Mute",
"mute_confirm_cancel_button": "Do not mute", "mute_confirm_cancel_button": "Do not mute",
"expire_at": "Expire at",
"dont_ask_again": "Do not ask again",
"mute_block_temporarily": "Temporarily",
"mute_block_forever": "Forever",
"mute_duration_prompt": "Mute this user for (0 for indefinite time):", "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",

View file

@ -97,8 +97,11 @@ export const defaultState = {
alwaysShowSubjectInput: undefined, // instance default alwaysShowSubjectInput: undefined, // instance default
postContentType: undefined, // instance default postContentType: undefined, // instance default
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
// Confirmations
modalOnRepeat: undefined, // instance default modalOnRepeat: undefined, // instance default
modalOnUnfollow: undefined, // instance default modalOnUnfollow: undefined, // instance default
modalOnBlock: undefined, // instance default modalOnBlock: undefined, // instance default
@ -110,6 +113,11 @@ export const defaultState = {
modalOnApproveFollow: undefined, // instance default modalOnApproveFollow: undefined, // instance default
modalOnDenyFollow: undefined, // instance default modalOnDenyFollow: undefined, // instance default
modalOnRemoveUserFromFollowers: undefined, // instance default modalOnRemoveUserFromFollowers: undefined, // instance default
// Expiry confirmations/default actions
onMuteDefaultAction: 'ask',
onBlockDefaultAction: 'ask',
modalMobileCenter: undefined, modalMobileCenter: undefined,
playVideosInModal: false, playVideosInModal: false,
useOneClickNsfw: false, useOneClickNsfw: false,

View file

@ -162,6 +162,7 @@ const defaultState = {
suggestionsWeb: '', suggestionsWeb: '',
quotingAvailable: false, quotingAvailable: false,
groupActorAvailable: false, groupActorAvailable: false,
blockExpiration: false,
// Html stuff // Html stuff
instanceSpecificPanelContent: '', instanceSpecificPanelContent: '',

View file

@ -43,11 +43,20 @@ const getNotificationPermission = () => {
return Promise.resolve(Notification.permission) return Promise.resolve(Notification.permission)
} }
const blockUser = (store, id) => { const blockUser = (store, args) => {
return store.rootState.api.backendInteractor.blockUser({ id }) const id = args.id
const expiresIn = typeof args === 'object' ? args.expiresIn : 0
const predictedRelationship = store.state.relationships[id] || { id }
store.commit('updateUserRelationship', [predictedRelationship])
store.commit('addBlockId', id)
return store.rootState.api.backendInteractor.blockUser({ id, expiresIn })
.then((relationship) => { .then((relationship) => {
console.log(relationship)
store.commit('updateUserRelationship', [relationship]) store.commit('updateUserRelationship', [relationship])
store.commit('addBlockId', id) store.commit('addBlockId', id)
store.commit('removeStatus', { timeline: 'friends', userId: id }) store.commit('removeStatus', { timeline: 'friends', userId: id })
store.commit('removeStatus', { timeline: 'public', userId: id }) store.commit('removeStatus', { timeline: 'public', userId: id })
store.commit('removeStatus', { timeline: 'publicAndExternal', userId: id }) store.commit('removeStatus', { timeline: 'publicAndExternal', userId: id })
@ -74,7 +83,6 @@ const muteUser = (store, args) => {
const expiresIn = typeof args === 'object' ? args.expiresIn : 0 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
store.commit('updateUserRelationship', [predictedRelationship]) store.commit('updateUserRelationship', [predictedRelationship])
store.commit('addMuteId', id) store.commit('addMuteId', id)
@ -360,20 +368,20 @@ const users = {
return blocks return blocks
}) })
}, },
blockUser (store, id) { blockUser (store, data) {
return blockUser(store, id) return blockUser(store, data)
}, },
unblockUser (store, id) { unblockUser (store, data) {
return unblockUser(store, id) return unblockUser(store, data)
}, },
removeUserFromFollowers (store, id) { removeUserFromFollowers (store, id) {
return removeUserFromFollowers(store, id) return removeUserFromFollowers(store, id)
}, },
blockUsers (store, ids = []) { blockUsers (store, data = []) {
return Promise.all(ids.map(id => blockUser(store, id))) return Promise.all(data.map(d => blockUser(store, d)))
}, },
unblockUsers (store, ids = []) { unblockUsers (store, data = []) {
return Promise.all(ids.map(id => unblockUser(store, id))) return Promise.all(data.map(d => unblockUser(store, d)))
}, },
editUserNote (store, args) { editUserNote (store, args) {
return editUserNote(store, args) return editUserNote(store, args)
@ -396,8 +404,8 @@ const users = {
return mutes return mutes
}) })
}, },
muteUser (store, id) { muteUser (store, data) {
return muteUser(store, id) return muteUser(store, data)
}, },
unmuteUser (store, id) { unmuteUser (store, id) {
return unmuteUser(store, id) return unmuteUser(store, id)
@ -408,11 +416,11 @@ const users = {
showReblogs (store, id) { showReblogs (store, id) {
return showReblogs(store, id) return showReblogs(store, id)
}, },
muteUsers (store, ids = []) { muteUsers (store, data = []) {
return Promise.all(ids.map(id => muteUser(store, id))) return Promise.all(data.map(d => muteUser(store, d)))
}, },
unmuteUsers (store, ids = []) { unmuteUsers (store, ids = []) {
return Promise.all(ids.map(id => unmuteUser(store, id))) return Promise.all(ids.map(d => unmuteUser(store, d)))
}, },
fetchDomainMutes (store) { fetchDomainMutes (store) {
return store.rootState.api.backendInteractor.fetchDomainMutes() return store.rootState.api.backendInteractor.fetchDomainMutes()

View file

@ -319,11 +319,19 @@ const unmuteConversation = ({ id, credentials }) => {
.then((data) => parseStatus(data)) .then((data) => parseStatus(data))
} }
const blockUser = ({ id, credentials }) => { const blockUser = ({ id, expiresIn, credentials }) => {
return fetch(MASTODON_BLOCK_USER_URL(id), { const payload = {}
headers: authHeaders(credentials), if (expiresIn) {
method: 'POST' payload.expires_in = expiresIn
}).then((data) => data.json()) }
console.log(payload)
return promisedRequest({
url: MASTODON_BLOCK_USER_URL(id),
credentials,
method: 'POST',
payload
})
} }
const unblockUser = ({ id, credentials }) => { const unblockUser = ({ id, credentials }) => {
@ -1172,7 +1180,13 @@ const muteUser = ({ id, expiresIn, credentials }) => {
if (expiresIn) { if (expiresIn) {
payload.expires_in = expiresIn payload.expires_in = expiresIn
} }
return promisedRequest({ url: MASTODON_MUTE_USER_URL(id), credentials, method: 'POST', payload })
return promisedRequest({
url: MASTODON_MUTE_USER_URL(id),
credentials,
method: 'POST',
payload
})
} }
const unmuteUser = ({ id, credentials }) => { const unmuteUser = ({ id, credentials }) => {