From b9161ef6971b1392bb65b1de77ea64aab7a61ac6 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 12 Jun 2025 20:04:39 +0300 Subject: [PATCH 01/12] some basic expiration modal. "don't as again" doesn't work yet --- src/App.scss | 5 --- src/boot/after_store.js | 2 + .../account_actions/account_actions.js | 21 +++++----- .../account_actions/account_actions.vue | 9 ++++- src/components/confirm_modal/mute_confirm.js | 33 ++------------- src/components/confirm_modal/mute_confirm.vue | 30 -------------- .../settings_modal/tabs/filtering_tab.js | 2 +- .../action_button_container.js | 4 +- .../action_button_container.vue | 7 ++-- src/components/user_card/user_card.js | 32 ++++++++------- src/components/user_card/user_card.scss | 5 +++ src/components/user_card/user_card.vue | 12 +++--- src/i18n/en.json | 4 ++ src/modules/default_config_state.js | 8 ++++ src/modules/instance.js | 1 + src/modules/users.js | 40 +++++++++++-------- src/services/api/api.service.js | 26 +++++++++--- 17 files changed, 117 insertions(+), 124 deletions(-) diff --git a/src/App.scss b/src/App.scss index 704d51cea..24afac8ab 100644 --- a/src/App.scss +++ b/src/App.scss @@ -675,11 +675,6 @@ option { } } -.btn-block { - display: block; - width: 100%; -} - .btn-group { position: relative; display: inline-flex; diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 1b133a089..28ff1e530 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -247,6 +247,7 @@ const getNodeInfo = async ({ store }) => { const data = await res.json() const metadata = data.metadata const features = metadata.features + console.log(features) store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName }) store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations }) 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: 'quotingAvailable', value: features.includes('quote_posting') }) 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 store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js index 9a63f57eb..f8ad0e11b 100644 --- a/src/components/account_actions/account_actions.js +++ b/src/components/account_actions/account_actions.js @@ -3,6 +3,7 @@ import ProgressButton from '../progress_button/progress_button.vue' import Popover from '../popover/popover.vue' import UserListMenu from 'src/components/user_list_menu/user_list_menu.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 { faEllipsisV @@ -27,15 +28,10 @@ const AccountActions = { ProgressButton, Popover, UserListMenu, - ConfirmModal + ConfirmModal, + UserTimedFilterModal }, methods: { - showConfirmBlock () { - this.showingConfirmBlock = true - }, - hideConfirmBlock () { - this.showingConfirmBlock = false - }, showConfirmRemoveUserFromFollowers () { this.showingConfirmRemoveFollower = true }, @@ -49,10 +45,14 @@ const AccountActions = { this.$store.dispatch('hideReblogs', this.user.id) }, blockUser () { - if (!this.shouldConfirmBlock) { - this.doBlockUser() + if (this.$refs.timedBlockDialog) { + this.$refs.timedBlockDialog.optionallyPrompt() } else { - this.showConfirmBlock() + if (!this.shouldConfirmBlock) { + this.doBlockUser() + } else { + this.showingConfirmBlock = true + } } }, doBlockUser () { @@ -91,6 +91,7 @@ const AccountActions = { return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers }, ...mapState({ + blockExpirationSupported: state => state.instance.blockExpiration, pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable }) } diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index fd4837ee4..f3cca45d0 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -96,7 +96,8 @@ + diff --git a/src/components/confirm_modal/mute_confirm.js b/src/components/confirm_modal/mute_confirm.js index 1bef9f620..a279dc716 100644 --- a/src/components/confirm_modal/mute_confirm.js +++ b/src/components/confirm_modal/mute_confirm.js @@ -1,4 +1,3 @@ -import { unitToSeconds } from 'src/services/date_utils/date_utils.js' import { mapGetters } from 'vuex' import ConfirmModal from './confirm_modal.vue' @@ -8,21 +7,13 @@ export default { props: ['type', 'user', 'status'], emits: ['hide', 'show', 'muted'], data: () => ({ - showing: false, - muteExpiryAmount: 2, - muteExpiryUnit: 'hours' + showing: false }), components: { ConfirmModal, Select }, computed: { - muteExpiryValue () { - unitToSeconds(this.muteExpiryUnit, this.muteExpiryAmount) - }, - muteExpiryUnits () { - return ['minutes', 'hours', 'days'] - }, domain () { return this.user.fqn.split('@')[1] }, @@ -31,13 +22,8 @@ export default { 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 }, @@ -49,12 +35,9 @@ export default { case 'domain': { return this.mergedConfig.modalOnMuteDomain } - case 'conversation': { + default: { // conversation return this.mergedConfig.modalOnMuteConversation } - default: { - return this.mergedConfig.modalOnMute - } } }, ...mapGetters(['mergedConfig']) @@ -79,7 +62,7 @@ export default { switch (this.type) { case 'domain': { if (!this.domainIsMuted) { - this.$store.dispatch('muteDomain', { id: this.domain, expiresIn: this.muteExpiryValue }) + this.$store.dispatch('muteDomain', { id: this.domain }) } else { this.$store.dispatch('unmuteDomain', { id: this.domain }) } @@ -87,20 +70,12 @@ export default { } case 'conversation': { if (!this.conversationIsMuted) { - this.$store.dispatch('muteConversation', { id: this.status.id, expiresIn: this.muteExpiryValue }) + this.$store.dispatch('muteConversation', { id: this.status.id }) } 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() diff --git a/src/components/confirm_modal/mute_confirm.vue b/src/components/confirm_modal/mute_confirm.vue index bf7ab338f..7c754b006 100644 --- a/src/components/confirm_modal/mute_confirm.vue +++ b/src/components/confirm_modal/mute_confirm.vue @@ -18,36 +18,6 @@ -
-

- - - {{ ' ' }} - -

-
diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js index 5ff137062..f78f6815c 100644 --- a/src/components/settings_modal/tabs/filtering_tab.js +++ b/src/components/settings_modal/tabs/filtering_tab.js @@ -107,7 +107,7 @@ const FilteringTab = { ...mapActions(useServerSideStorageStore, ['setPreference', 'unsetPreference', 'pushServerSideStorage']), getDatetimeLocal (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 = [ date.getFullYear(), '-', diff --git a/src/components/status_action_buttons/action_button_container.js b/src/components/status_action_buttons/action_button_container.js index 313e3022f..a8f20800b 100644 --- a/src/components/status_action_buttons/action_button_container.js +++ b/src/components/status_action_buttons/action_button_container.js @@ -1,6 +1,7 @@ import ActionButton from './action_button.vue' import Popover from 'src/components/popover/popover.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 { @@ -19,7 +20,8 @@ export default { components: { ActionButton, Popover, - MuteConfirm + MuteConfirm, + UserTimedFilterModal }, props: ['button', 'status'], emits: ['interacted'], diff --git a/src/components/status_action_buttons/action_button_container.vue b/src/components/status_action_buttons/action_button_container.vue index 931a40349..75f5010ee 100644 --- a/src/components/status_action_buttons/action_button_container.vue +++ b/src/components/status_action_buttons/action_button_container.vue @@ -94,11 +94,10 @@ :status="status" :user="user" /> - diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index be81b8ad5..addb49afb 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -8,7 +8,8 @@ import UserNote from '../user_note/user_note.vue' import Select from '../select/select.vue' import UserLink from '../user_link/user_link.vue' 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 { mapGetters } from 'vuex' import { usePostStatusStore } from 'src/stores/post_status' @@ -48,6 +49,19 @@ export default { 'onClose', 'hasNoteEditor' ], + components: { + UserAvatar, + RemoteFollow, + ModerationTools, + AccountActions, + ProgressButton, + FollowButton, + Select, + RichContent, + UserLink, + UserNote, + UserTimedFilterModal + }, data () { return { followRequestInProgress: false, @@ -63,6 +77,7 @@ export default { return this.$store.getters.findUser(this.userId) }, relationship () { + console.log(this.$store.getters.relationship(this.userId)) return this.$store.getters.relationship(this.userId) }, classes () { @@ -144,22 +159,9 @@ export default { }, ...mapGetters(['mergedConfig']) }, - components: { - UserAvatar, - RemoteFollow, - ModerationTools, - AccountActions, - ProgressButton, - FollowButton, - Select, - RichContent, - UserLink, - UserNote, - MuteConfirm - }, methods: { muteUser () { - this.$refs.confirmation.optionallyPrompt() + this.$refs.timedMuteDialog.optionallyPrompt() }, unmuteUser () { this.$store.dispatch('unmuteUser', this.user.id) diff --git a/src/components/user_card/user_card.scss b/src/components/user_card/user_card.scss index d263be1c0..418bef08b 100644 --- a/src/components/user_card/user_card.scss +++ b/src/components/user_card/user_card.scss @@ -8,6 +8,11 @@ --_still-image-label-visibility: hidden; } + .btn-mute, .btn-mention { + display: block; + width: 100%; + } + .panel-heading { padding: 0.5em 0; text-align: center; diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 36935424d..d05f9a6e4 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -232,7 +232,7 @@
- diff --git a/src/i18n/en.json b/src/i18n/en.json index 019beba1c..64d11b07c 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1392,6 +1392,10 @@ "mute_confirm": "Do you really want to mute {user}?", "mute_confirm_accept_button": "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):", "per_day": "per day", "remote_follow": "Remote follow", diff --git a/src/modules/default_config_state.js b/src/modules/default_config_state.js index eb12d2df1..8ccf79e70 100644 --- a/src/modules/default_config_state.js +++ b/src/modules/default_config_state.js @@ -97,8 +97,11 @@ export const defaultState = { alwaysShowSubjectInput: undefined, // instance default postContentType: undefined, // instance default minimalScopesMode: undefined, // instance default + // This hides statuses filtered via a word filter hideFilteredStatuses: undefined, // instance default + + // Confirmations modalOnRepeat: undefined, // instance default modalOnUnfollow: undefined, // instance default modalOnBlock: undefined, // instance default @@ -110,6 +113,11 @@ export const defaultState = { modalOnApproveFollow: undefined, // instance default modalOnDenyFollow: undefined, // instance default modalOnRemoveUserFromFollowers: undefined, // instance default + + // Expiry confirmations/default actions + onMuteDefaultAction: 'ask', + onBlockDefaultAction: 'ask', + modalMobileCenter: undefined, playVideosInModal: false, useOneClickNsfw: false, diff --git a/src/modules/instance.js b/src/modules/instance.js index 83671a881..cee039ccc 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -162,6 +162,7 @@ const defaultState = { suggestionsWeb: '', quotingAvailable: false, groupActorAvailable: false, + blockExpiration: false, // Html stuff instanceSpecificPanelContent: '', diff --git a/src/modules/users.js b/src/modules/users.js index 01936c716..8779376b0 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -43,11 +43,20 @@ const getNotificationPermission = () => { return Promise.resolve(Notification.permission) } -const blockUser = (store, id) => { - return store.rootState.api.backendInteractor.blockUser({ id }) +const blockUser = (store, args) => { + 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) => { + console.log(relationship) store.commit('updateUserRelationship', [relationship]) store.commit('addBlockId', id) + store.commit('removeStatus', { timeline: 'friends', userId: id }) store.commit('removeStatus', { timeline: 'public', 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 predictedRelationship = store.state.relationships[id] || { id } - predictedRelationship.muting = true store.commit('updateUserRelationship', [predictedRelationship]) store.commit('addMuteId', id) @@ -360,20 +368,20 @@ const users = { return blocks }) }, - blockUser (store, id) { - return blockUser(store, id) + blockUser (store, data) { + return blockUser(store, data) }, - unblockUser (store, id) { - return unblockUser(store, id) + unblockUser (store, data) { + return unblockUser(store, data) }, removeUserFromFollowers (store, id) { return removeUserFromFollowers(store, id) }, - blockUsers (store, ids = []) { - return Promise.all(ids.map(id => blockUser(store, id))) + blockUsers (store, data = []) { + return Promise.all(data.map(d => blockUser(store, d))) }, - unblockUsers (store, ids = []) { - return Promise.all(ids.map(id => unblockUser(store, id))) + unblockUsers (store, data = []) { + return Promise.all(data.map(d => unblockUser(store, d))) }, editUserNote (store, args) { return editUserNote(store, args) @@ -396,8 +404,8 @@ const users = { return mutes }) }, - muteUser (store, id) { - return muteUser(store, id) + muteUser (store, data) { + return muteUser(store, data) }, unmuteUser (store, id) { return unmuteUser(store, id) @@ -408,11 +416,11 @@ const users = { showReblogs (store, id) { return showReblogs(store, id) }, - muteUsers (store, ids = []) { - return Promise.all(ids.map(id => muteUser(store, id))) + muteUsers (store, data = []) { + return Promise.all(data.map(d => muteUser(store, d))) }, unmuteUsers (store, ids = []) { - return Promise.all(ids.map(id => unmuteUser(store, id))) + return Promise.all(ids.map(d => unmuteUser(store, d))) }, fetchDomainMutes (store) { return store.rootState.api.backendInteractor.fetchDomainMutes() diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 117c621d9..9328c8edc 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -319,11 +319,19 @@ const unmuteConversation = ({ id, credentials }) => { .then((data) => parseStatus(data)) } -const blockUser = ({ id, credentials }) => { - return fetch(MASTODON_BLOCK_USER_URL(id), { - headers: authHeaders(credentials), - method: 'POST' - }).then((data) => data.json()) +const blockUser = ({ id, expiresIn, credentials }) => { + const payload = {} + if (expiresIn) { + payload.expires_in = expiresIn + } + + console.log(payload) + return promisedRequest({ + url: MASTODON_BLOCK_USER_URL(id), + credentials, + method: 'POST', + payload + }) } const unblockUser = ({ id, credentials }) => { @@ -1172,7 +1180,13 @@ const muteUser = ({ id, expiresIn, credentials }) => { if (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 }) => { From c9a4aee9547e3bc3ea8caf714d0dcc3c0b87d79a Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 12 Jun 2025 22:03:15 +0300 Subject: [PATCH 02/12] forgotten files --- .../user_timed_filter_modal.js | 93 +++++++++++++++++++ .../user_timed_filter_modal.scss | 9 ++ .../user_timed_filter_modal.vue | 61 ++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 src/components/user_timed_filter_modal/user_timed_filter_modal.js create mode 100644 src/components/user_timed_filter_modal/user_timed_filter_modal.scss create mode 100644 src/components/user_timed_filter_modal/user_timed_filter_modal.vue diff --git a/src/components/user_timed_filter_modal/user_timed_filter_modal.js b/src/components/user_timed_filter_modal/user_timed_filter_modal.js new file mode 100644 index 000000000..ff920dcca --- /dev/null +++ b/src/components/user_timed_filter_modal/user_timed_filter_modal.js @@ -0,0 +1,93 @@ +import DialogModal from 'src/components/dialog_modal/dialog_modal.vue' +import Checkbox from 'src/components/checkbox/checkbox.vue' + +const UserTimedFilterModal = { + data () { + return { + showing: false, + dontAskAgain: false, + expiration: (() => { + const date = new Date() + const fmt = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 2}) + return [ + date.getFullYear(), + '-', + fmt.format(date.getMonth() + 1), + '-', + fmt.format(date.getDate()), + 'T', + fmt.format(date.getHours()), + ':', + fmt.format(date.getMinutes()) + ].join('') + })() + } + }, + components: { + DialogModal, + Checkbox + }, + props: { + isMute: Boolean, + user: Object + }, + emits: [ + 'timed', + 'forever', + 'user' + ], + computed: { + dateValid () { + return (new Date(this.expiration).toJSON() != null) && + new Date(this.expiration) > new Date() + }, + expiryTime () { + return Math.floor((new Date(this.expiration).valueOf() - Date.now()) / 1000) + }, + shouldConfirm () { + if (this.isMute) { + return this.mergedConfig.onMuteDefaultAction === 'ask' + } else { + return this.mergedConfig.onBlockDefaultAction === 'ask' + } + } + }, + methods: { + optionallyPrompt () { + this.showing = true + }, + temporarily () { + if (this.isMute) { + this.muteUserTemporarily() + } else { + this.blockUserTemporarily() + } + this.showing = false + }, + forever () { + if (this.isMute) { + this.muteUserForever() + } else { + this.blockUserForever() + } + this.showing = false + }, + blockUserForever () { + this.$store.dispatch('blockUser', { id: this.user.id }) + }, + blockUserTemporarily () { + this.$store.dispatch('blockUser', { id: this.user.id, expiresIn: this.expiryTime }) + }, + muteUserForever () { + this.$store.dispatch('muteUser', { id: this.user.id }) + }, + muteUserTemporarily () { + this.$store.dispatch('muteUser', { id: this.user.id, expiresIn: this.expiryTime }) + }, + cancel () { + this.showing = false + } + } +} + +export default UserTimedFilterModal diff --git a/src/components/user_timed_filter_modal/user_timed_filter_modal.scss b/src/components/user_timed_filter_modal/user_timed_filter_modal.scss new file mode 100644 index 000000000..0fdb66eea --- /dev/null +++ b/src/components/user_timed_filter_modal/user_timed_filter_modal.scss @@ -0,0 +1,9 @@ +.UserTimedFilterModal { + .input-dont-ask-again { + margin-left: 1em; + } + + .input-expire-at { + margin-left: 0.25em; + } +} diff --git a/src/components/user_timed_filter_modal/user_timed_filter_modal.vue b/src/components/user_timed_filter_modal/user_timed_filter_modal.vue new file mode 100644 index 000000000..b3f6fbe27 --- /dev/null +++ b/src/components/user_timed_filter_modal/user_timed_filter_modal.vue @@ -0,0 +1,61 @@ + + + + +