From ce43c81ce81518dc2b91860c4a534f90163aec33 Mon Sep 17 00:00:00 2001 From: luce Date: Tue, 15 Jul 2025 14:30:46 +0200 Subject: [PATCH 001/337] list users, fetch updates, make them clickable --- .../selectable_list/selectable_list.js | 4 + .../selectable_list/selectable_list.vue | 21 ++++ .../settings_modal/admin_tabs/admin_card.js | 25 +++++ .../settings_modal/admin_tabs/admin_card.vue | 45 ++++++++ .../settings_modal/admin_tabs/users_tab.js | 100 ++++++++++++++++++ .../settings_modal/admin_tabs/users_tab.vue | 14 +++ .../settings_modal_admin_content.js | 2 + .../settings_modal_admin_content.vue | 8 ++ src/i18n/en.json | 13 +++ src/modules/adminSettings.js | 3 + src/services/api/api.service.js | 10 +- 11 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 src/components/settings_modal/admin_tabs/admin_card.js create mode 100644 src/components/settings_modal/admin_tabs/admin_card.vue create mode 100644 src/components/settings_modal/admin_tabs/users_tab.js create mode 100644 src/components/settings_modal/admin_tabs/users_tab.vue diff --git a/src/components/selectable_list/selectable_list.js b/src/components/selectable_list/selectable_list.js index 10980d46a..b097e8927 100644 --- a/src/components/selectable_list/selectable_list.js +++ b/src/components/selectable_list/selectable_list.js @@ -7,6 +7,10 @@ const SelectableList = { Checkbox }, props: { + boxOnly: { + type: Boolean, + default: false + }, items: { type: Array, default: () => [] diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue index 3d3a5ff04..eeea348ee 100644 --- a/src/components/selectable_list/selectable_list.vue +++ b/src/components/selectable_list/selectable_list.vue @@ -27,6 +27,7 @@ > @@ -227,70 +243,84 @@ :title="$t('admin_dash.users.bulk_actions.activate')" :cancel-text="$t('admin_dash.users.bulk_actions.no')" :confirm-text="$t('admin_dash.users.bulk_actions.yes')" - @callback="activateSelectedConfirmed" + @callback="selectionConfirmed('adminActivateUser')" /> + + diff --git a/src/i18n/en.json b/src/i18n/en.json index 6b5630a07..a90ed8200 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1173,7 +1173,9 @@ "revoke_moderator": "Revoke Moderator Privileges From Selected Users?", "approve": "Approve Selected Users?", "confirm": "Confirm Selected Users?", - "require_password_change": "Require Password Change For Selected Users?" + "require_password_change": "Require Password Change For Selected Users?", + "resend_confirmation_email": "Resend Confirmation Email For Selected Users?", + "disable_mfa": "Disable MFA For Selected Users?" }, "activate": "Activate", "deactivate": "Deactivate", @@ -1186,6 +1188,7 @@ "confirm_user": "Confirm", "resend_confirmation_email": "Resend Confirmation Email", "require_password_change": "Require Password Change", + "disable_mfa": "Disable MFA", "delete": "Delete", "loading_user": "Loading user...", "delete_user": "Delete User", diff --git a/src/modules/adminSettings.js b/src/modules/adminSettings.js index 70210a1eb..25d0a1f84 100644 --- a/src/modules/adminSettings.js +++ b/src/modules/adminSettings.js @@ -114,6 +114,9 @@ const adminSettingsStorage = { adminChangeStatusScope (store, { opts }) { return store.rootState.api.backendInteractor.adminChangeStatusScope({ opts }) }, + adminDisableMFA (store, { opts }) { + return store.rootState.api.backendInteractor.adminDisableMFA({ opts }) + }, loadFrontendsStuff ({ rootState, commit }) { rootState.api.backendInteractor.fetchAvailableFrontends() .then(frontends => commit('setAvailableFrontends', { frontends })) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 56b324635..31ea7acf5 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -146,6 +146,7 @@ const PLEROMA_ADMIN_APPROVE_URL = '/api/v1/pleroma/admin/users/approve' const PLEROMA_ADMIN_LIST_STATUSES_URL = (id, pageSize, godmode, withReblogs) => `/api/v1/pleroma/admin/users/${id}/statuses?page_size=${pageSize}&godmode=${godmode}&with_reblogs=${withReblogs}` const PLEROMA_ADMIN_CHANGE_STATUS_SCOPE_URL = (id) => `/api/v1/pleroma/admin/statuses/${id}` const PLEROMA_ADMIN_REQUIRE_PASSWORD_CHANGE_URL = '/api/v1/pleroma/admin/users/force_password_reset' +const PLEROMA_ADMIN_DISABLE_MFA_URL = '/api/v1/pleroma/admin/users/disable_mfa' const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji' const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import' const PLEROMA_EMOJI_PACKS_URL = (page, pageSize) => `/api/v1/pleroma/emoji/packs?page=${page}&page_size=${pageSize}` @@ -1600,6 +1601,18 @@ const adminRequirePasswordChange = ({ user: { screen_name: nickname }, credentia }) } +const adminDisableMFA = ({ user : { screen_name: nickname }, credentials }) => { + const url = PLEROMA_ADMIN_DISABLE_MFA_URL + return promisedRequest({ + url, + credentials, + method: 'PUT', + payload: { + nickname + } + }) +} + const announcementToPayload = ({ content, startsAt, endsAt, allDay }) => { const payload = { content } @@ -2282,7 +2295,8 @@ const apiService = { adminApproveUser, adminListStatuses, adminChangeStatusScope, - adminRequirePasswordChange + adminRequirePasswordChange, + adminDisableMFA } export default apiService From 05b4cbe6d6e5050e72198aecd2dbcef1510084e6 Mon Sep 17 00:00:00 2001 From: luce Date: Tue, 9 Sep 2025 11:38:25 +0200 Subject: [PATCH 042/337] adding jsdoc --- src/components/page_list/page_list.js | 25 ++++ .../settings_modal/admin_tabs/admin_card.js | 88 +++++++++++- .../settings_modal/admin_tabs/admin_card.vue | 39 ++---- .../admin_tabs/admin_status_card.js | 131 ++++++++++++------ .../settings_modal/admin_tabs/users_tab.js | 47 ++++++- 5 files changed, 250 insertions(+), 80 deletions(-) diff --git a/src/components/page_list/page_list.js b/src/components/page_list/page_list.js index 86bdd6801..5a28a847d 100644 --- a/src/components/page_list/page_list.js +++ b/src/components/page_list/page_list.js @@ -5,18 +5,30 @@ const PageList = { SelectableList }, props: { + /** + * only make the checkbox clickable to toggle, not the whole area + */ boxOnly: { type: Boolean, default: false }, + /** + * how many entries to fetch at once + */ pageSize: { type: Number, default: 50 }, + /** + * the function/callback used to fetch new entries (one page) + */ fetchPage: { type: Function, default: async () => [] }, + /** + * wether or not this is a single page list (so it won't allow fetching more pages) + */ singlePage: { type: Boolean, default: false @@ -31,6 +43,9 @@ const PageList = { } }, methods: { + /** + * reset and load first page + */ reset () { this.canLoadMore = true this.pageIndex = 1 @@ -38,6 +53,9 @@ const PageList = { this.isLoading = false this.loadMore() // load one page }, + /** + * load another page + */ loadMore () { if (!this.isLoading && this.canLoadMore) { this.isLoading = true @@ -50,10 +68,17 @@ const PageList = { }) } }, + /** + * get currently selected elements + * @returns {Array} + */ getSelected () { return this.$refs.list.selected } }, + /** + * auto-load first page when mounted + */ mounted () { this.loadMore() } diff --git a/src/components/settings_modal/admin_tabs/admin_card.js b/src/components/settings_modal/admin_tabs/admin_card.js index 6c9ed46db..cc5afc0d6 100644 --- a/src/components/settings_modal/admin_tabs/admin_card.js +++ b/src/components/settings_modal/admin_tabs/admin_card.js @@ -6,9 +6,23 @@ import Modal from 'src/components/modal/modal.vue' const AdminCard = { props: { + /** + * minimal user info + * @type {import('vue').PropType<{ + * id: string, + * _original: { + * is_approved: boolean; + * is_confirmed: boolean; + * }; + * }>} + */ userDetails: { type: Object, required: true, + /** + * @param {any} u + * @returns {u is { id: string; _original: { is_approved; is_confirmed: boolean; } } } + */ validator (u) { return ( typeof(u.id) === 'string' && @@ -22,7 +36,8 @@ const AdminCard = { data () { return { progress: false, - topLevelExpanded: false, + detailsExpanded: false, + topLevelExpanded: false, // REMOVE jsonExpanded: false, timelineExpanded: false, justApproved: false, @@ -31,15 +46,28 @@ const AdminCard = { } }, computed: { + /** + * checks if the user is defined + * @returns {boolean} + */ isLoaded () { return typeof(this.user) !== 'undefined' }, + /** + * @returns {object} user info + */ user () { return this.$store.getters.findUser(this.userDetails.id) }, + /** + * @returns {object} user relationship + */ relationship () { return this.$store.getters.relationship(this.userDetails.id) }, + /** + * @returns {boolean} is user local + */ isLocal () { const u = this.$store.getters.findUser(this.userDetails.id) if (typeof(u) !== 'undefined') { @@ -47,6 +75,9 @@ const AdminCard = { } return false }, + /** + * @returns {boolean} is user admin + */ isAdmin () { const u = this.$store.getters.findUser(this.userDetails.id) if (typeof(u) !== 'undefined') { @@ -54,6 +85,9 @@ const AdminCard = { } return false }, + /** + * @returns {boolean} is user moderator + */ isModerator () { const u = this.$store.getters.findUser(this.userDetails.id) if (typeof(u) !== 'undefined') { @@ -61,6 +95,9 @@ const AdminCard = { } return false }, + /** + * @returns {boolean} is user active + */ isActivated () { const u = this.$store.getters.findUser(this.userDetails.id) if (typeof(u) !== 'undefined') { @@ -68,10 +105,16 @@ const AdminCard = { } return false }, + /** + * @returns {boolean} has this user been confirmed + */ isConfirmed () { const u = this.$store.getters.findUser(this.userDetails.id) return (u._original.is_confirmed === true) || (this.justConfirmed === true) }, + /** + * @returns {boolean} has this user been approved + */ isApproved () { return (this.userDetails._original.is_approved === true) || (this.justApproved === true) } @@ -84,7 +127,10 @@ const AdminCard = { Modal }, methods: { - toggleAdmin (v) { + /** + * @param {boolean} v set admin status + */ + setAdmin (v) { const u = this.$store.getters.findUser(this.userDetails.id) if (v === true) { this.$store.dispatch('adminAddUserToAdminGroup', u) @@ -92,7 +138,10 @@ const AdminCard = { this.$store.dispatch('adminRemoveUserFromAdminGroup', u) } }, - toggleModerator (v) { + /** + * @param {boolean} v set moderator status + */ + setModerator (v) { const u = this.$store.getters.findUser(this.userDetails.id) if (v === true) { this.$store.dispatch('adminAddUserToModeratorGroup', u) @@ -100,7 +149,10 @@ const AdminCard = { this.$store.dispatch('adminRemoveUserFromModeratorGroup', u) } }, - toggleActivation (v) { + /** + * @param {boolean} v set activation status + */ + setActivation (v) { const u = this.$store.getters.findUser(this.userDetails.id) if (v === true) { this.$store.dispatch('adminActivateUser', u) @@ -108,28 +160,47 @@ const AdminCard = { this.$store.dispatch('adminDeactivateUser', u) } }, + /** + * confirm this user + */ confirmUser () { const u = this.$store.getters.findUser(this.userDetails.id) this.$store.dispatch('adminConfirmUser', u) this.just_confirmed = true }, + /** + * try resending the confirmation email + */ resendConfirmationEmail () { const u = this.$store.getters.findUser(this.userDetails.id) this.$store.dispatch('adminResendConfirmationEmail', u) }, - toggleApproval () { + /** + * approve this user + */ + approveUser () { const u = this.$store.getters.findUser(this.userDetails.id) this.$store.dispatch('adminApproveUser', u) }, + /** + * update user info from server + */ forceUpdateUser () { this.$store.dispatch('fetchUser', this.userDetails.id) }, + /** + * delete selected statuses + */ deleteSelection () { const l = this.$refs.timelineList const s = l.getSelected() s.forEach(p => this.$store.dispatch('deleteStatus', p)) l.reset() }, + /** + * delete this user. keep in mind that user deletion is not intuitive in pleroma backend. + * it actually deletes all content of a user. the user itself will keep showing up in search results. + */ deleteUser () { if (!this.justDeleted) { const u = this.$store.getters.findUser(this.userDetails.id) @@ -137,7 +208,12 @@ const AdminCard = { this.justDeleted = true } }, - fetchStatuses (store, opts) { + /** + * @param {object} store + * @param {object} opts + * @returns {Promise>} statuses + */ + async fetchStatuses (store, opts) { const u = this.$store.getters.findUser(this.userDetails.id) const res = store.dispatch('adminListStatuses', { user: u, opts: { pageSize: opts.pageSize, godmode: true, withReblogs: true}}) return res.then(r => r.activities) diff --git a/src/components/settings_modal/admin_tabs/admin_card.vue b/src/components/settings_modal/admin_tabs/admin_card.vue index bea1f83dc..e3ca14d0a 100644 --- a/src/components/settings_modal/admin_tabs/admin_card.vue +++ b/src/components/settings_modal/admin_tabs/admin_card.vue @@ -5,37 +5,26 @@
-
- -
+
    -
  • - -
  • {{ $t('admin_dash.users.is_admin') }} @@ -45,7 +34,7 @@ > {{ $t('admin_dash.users.is_moderator') }} @@ -78,7 +67,7 @@ @@ -86,7 +75,7 @@
  • {{ $t('admin_dash.users.is_active') }} @@ -195,7 +184,7 @@ {{ $t('admin_dash.users.expand_raw_info') }}
-
diff --git a/src/components/settings_modal/admin_tabs/admin_status_card.js b/src/components/settings_modal/admin_tabs/admin_status_card.js index aa7a5f2ef..e7e5932b8 100644 --- a/src/components/settings_modal/admin_tabs/admin_status_card.js +++ b/src/components/settings_modal/admin_tabs/admin_status_card.js @@ -4,53 +4,92 @@ import Status from 'src/components/status/status.vue' import { parseStatus } from 'src/services/entity_normalizer/entity_normalizer.service.js' const AdminStatusCard = { - props: ['statusDetails'], - data () { - return { - jsonExpanded: false, - statusCache: undefined, + props: { + /** + * minimal status info + * @type {import('vue').PropType<{ + * id: string + * }>} + */ + statusDetails: { + type: Object, + required: true, + /** + * @param {any} u + * @returns {u is { id: string }} + */ + validator (u) { + return typeof(u.id) === 'string' } - }, - computed: { - isSensitive () { - return this.statusDetails.sensitive === true - }, - visibility () { - return this.statusDetails.visibility - } - }, - methods: { - changeSensitivity (v) { - this.$store.dispatch('adminChangeStatusScope', { opts: { id: this.statusDetails.id, sensitive: v }}).then(res => parseStatus(res)).then(s => this.statusCache = s) - }, - changeVisibility (v) { - this.$store.dispatch('adminChangeStatusScope', { opts: { id: this.statusDetails.id, visibility: v }}).then(res => parseStatus(res)).then(s => this.statusCache = s) - }, - // show popup - confirmSelection(box) { - this.$refs[box].show() - this.$refs.dropdown.hidePopover() - }, - // do the thing - selectionConfirmed(action, opts) { - const restricted = [] - const s = this.$refs.userList.getSelected() - s.forEach(u => { - if (restricted.includes(action) !== false || u.id !== this.$store.state.users.currentUser.id) { - this.$store.dispatch(action, { id: this.statusDetails.id, ...(opts || {}) }) - } - }) - this.reset() - } - }, - components: { - Checkbox, - Select, - Status, - }, - mounted () { - this.$store.dispatch('adminChangeStatusScope', { opts: { id: this.statusDetails.id }}).then(res => parseStatus(res)).then(s => this.statusCache = s) - } + } + }, + data () { + return { + jsonExpanded: false, + statusCache: undefined, + } + }, + computed: { + /** + * @returns {boolean} is this status sensitive? + */ + isSensitive () { + return this.statusDetails.sensitive === true + }, + /** + * @returns {'public' | 'unlisted' | 'private' | 'direct'} status visibility + */ + visibility () { + return this.statusDetails.visibility + } + }, + methods: { + /** + * @param {boolean} v set sensitive + */ + changeSensitivity (v) { + this.$store.dispatch('adminChangeStatusScope', { opts: { id: this.statusDetails.id, sensitive: v }}).then(res => parseStatus(res)).then(s => this.statusCache = s) + }, + /** + * @param {boolean} v set visible + */ + changeVisibility (v) { + this.$store.dispatch('adminChangeStatusScope', { opts: { id: this.statusDetails.id, visibility: v }}).then(res => parseStatus(res)).then(s => this.statusCache = s) + }, + /** + * show the confirmation box for bulk actions. + * @param {string} box ref name specified for the confirm component + */ + confirmSelection(box) { + this.$refs[box].show() + this.$refs.dropdown.hidePopover() + }, + /** + * called when a bulk action was confirmed + * @param {string} action + */ + selectionConfirmed(action, opts) { + const restricted = [] + const s = this.$refs.userList.getSelected() + s.forEach(u => { + if (restricted.includes(action) !== false || u.id !== this.$store.state.users.currentUser.id) { + this.$store.dispatch(action, { id: this.statusDetails.id, ...(opts || {}) }) + } + }) + this.reset() + } + }, + components: { + Checkbox, + Select, + Status, + }, + /** + * fetch and cache status info + */ + mounted () { + this.$store.dispatch('adminChangeStatusScope', { opts: { id: this.statusDetails.id }}).then(res => parseStatus(res)).then(s => this.statusCache = s) + } } export default AdminStatusCard diff --git a/src/components/settings_modal/admin_tabs/users_tab.js b/src/components/settings_modal/admin_tabs/users_tab.js index 3486122e5..6245369c4 100644 --- a/src/components/settings_modal/admin_tabs/users_tab.js +++ b/src/components/settings_modal/admin_tabs/users_tab.js @@ -31,21 +31,45 @@ const UsersTab = { } }, computed: { + /** + * do we filter for admins? + * @returns {boolean} + */ filtersIsAdmin () { return this.filtersPrivileges === 'admin' || this.filtersPrivileges === 'modsnadmins' }, + /** + * do we filter for moderators? + * @returns {boolean} + */ filtersIsModerator () { return this.filtersPrivileges === 'moderator' || this.filtersPrivileges === 'modsnadmins' }, + /** + * do we filter for active users? + * @returns {boolean} + */ filtersActive () { return this.filtersActivity === 'active' }, + /** + * do we filter for deactivated users? + * @returns {boolean} + */ filtersDeactivated () { return this.filtersActivity === 'deactivated' }, + /** + * do we filter for local users? + * @returns {boolean} + */ filtersLocal () { return this.filtersOrigin === 'local' }, + /** + * do we filter for external users? + * @return {boolean} + */ filtersExternal () { return this.filtersOrigin === 'external' } @@ -55,13 +79,18 @@ const UsersTab = { Select, BasicUserCard, PageList, - ProgressButton, + ProgressButton, AdminCard, TabSwitcher, Popover, GenericConfirm }, methods: { + /** + * fetch a new page of users via admin-api + * @param {object} store + * @param {object} opts + */ fetchPage (store, opts) { if(!this.init) return new Promise(() => []) const filters = { @@ -83,15 +112,24 @@ const UsersTab = { const users = store.dispatch('fetchAdminUsers', nopts) return users }, + /** + * reset the userlist explicitly + */ reset () { this.$refs.userList.reset() }, - // show popup + /** + * show the confirmation box for bulk actions. + * @param {string} box ref name specified for the confirm component + */ confirmSelection(box) { this.$refs[box].show() this.$refs.dropdown.hidePopover() }, - // do the thing + /** + * called when a bulk action was confirmed + * @param {string} action + */ selectionConfirmed(action) { const restricted = [] const s = this.$refs.userList.getSelected() @@ -103,6 +141,9 @@ const UsersTab = { this.reset() } }, + /** + * mark as initialized and reset user list + */ mounted () { this.init = true this.reset() From 3a6dac7ce591202d0f34113ec48f7c3555aea3d3 Mon Sep 17 00:00:00 2001 From: luce Date: Tue, 9 Sep 2025 18:36:30 +0200 Subject: [PATCH 043/337] wip, ignore, i just want this pushed --- .../confirm_modal/generic_confirm.js | 9 +- .../confirm_modal/generic_confirm.vue | 2 +- .../settings_modal/admin_tabs/admin_card.js | 20 +- .../settings_modal/admin_tabs/admin_card.scss | 15 ++ .../settings_modal/admin_tabs/admin_card.vue | 215 +++++++++++++++++- .../settings_modal/admin_tabs/users_tab.js | 3 +- src/i18n/en.json | 9 +- 7 files changed, 254 insertions(+), 19 deletions(-) create mode 100644 src/components/settings_modal/admin_tabs/admin_card.scss diff --git a/src/components/confirm_modal/generic_confirm.js b/src/components/confirm_modal/generic_confirm.js index 21bf3f61b..7ac07581b 100644 --- a/src/components/confirm_modal/generic_confirm.js +++ b/src/components/confirm_modal/generic_confirm.js @@ -3,8 +3,9 @@ import ConfirmModal from './confirm_modal.vue' export default { props: { - callback: { - type: Function + action: { + type: Function, + require: true }, title: { type: String @@ -31,6 +32,10 @@ export default { hide () { this.showing = false this.$emit('hide') + }, + doGeneric () { + this.action() + this.hide() } } } diff --git a/src/components/confirm_modal/generic_confirm.vue b/src/components/confirm_modal/generic_confirm.vue index d0cbae2ca..794a15490 100644 --- a/src/components/confirm_modal/generic_confirm.vue +++ b/src/components/confirm_modal/generic_confirm.vue @@ -4,7 +4,7 @@ :title="title" :cancel-text="cancelText" :confirm-text="confirmText" - @accepted="callback" + @accepted="doGeneric" @cancelled="hide" /> diff --git a/src/components/settings_modal/admin_tabs/admin_card.js b/src/components/settings_modal/admin_tabs/admin_card.js index cc5afc0d6..b83426f0e 100644 --- a/src/components/settings_modal/admin_tabs/admin_card.js +++ b/src/components/settings_modal/admin_tabs/admin_card.js @@ -3,6 +3,8 @@ import Checkbox from 'src/components/checkbox/checkbox.vue' import PageList from 'src/components/page_list/page_list.vue' import AdminStatusCard from 'src/components/settings_modal/admin_tabs/admin_status_card.vue' import Modal from 'src/components/modal/modal.vue' +import Popover from 'src/components/popover/popover.vue' +import GenericConfirm from 'src/components/confirm_modal/generic_confirm.vue' const AdminCard = { props: { @@ -124,7 +126,9 @@ const AdminCard = { Checkbox, PageList, AdminStatusCard, - Modal + Modal, + Popover, + GenericConfirm }, methods: { /** @@ -217,6 +221,20 @@ const AdminCard = { const u = this.$store.getters.findUser(this.userDetails.id) const res = store.dispatch('adminListStatuses', { user: u, opts: { pageSize: opts.pageSize, godmode: true, withReblogs: true}}) return res.then(r => r.activities) + }, + /** + * ... + */ + confirmAction (box) { + this.$refs[box].show() + this.$refs.dropdownuser.hidePopover() + }, + /** + * ... + */ + actionConfirmed (action) { + console.log(action) + this.$store.dispatch(action, this.$store.getters.findUser(this.userDetails.id)) } } } diff --git a/src/components/settings_modal/admin_tabs/admin_card.scss b/src/components/settings_modal/admin_tabs/admin_card.scss new file mode 100644 index 000000000..dddfad29f --- /dev/null +++ b/src/components/settings_modal/admin_tabs/admin_card.scss @@ -0,0 +1,15 @@ +.inline-layout { + display: flex; + align-items: center; + width: 100%; + gap: 8px; +} + +.alert { + text-shadow: none; +} + +.user-role { + display: inline-block; + vertical-align: baseline; +} diff --git a/src/components/settings_modal/admin_tabs/admin_card.vue b/src/components/settings_modal/admin_tabs/admin_card.vue index e3ca14d0a..aa9953e22 100644 --- a/src/components/settings_modal/admin_tabs/admin_card.vue +++ b/src/components/settings_modal/admin_tabs/admin_card.vue @@ -4,20 +4,194 @@ {{ $t('admin_dash.users.loading_user') }}
- - +
+ + + + + + + + + + + +
+
+
    +
  • show statuses
  • +
+
+
+
+ +
- - + diff --git a/src/components/settings_modal/admin_tabs/users_tab.js b/src/components/settings_modal/admin_tabs/users_tab.js index 6245369c4..f8bc83ea7 100644 --- a/src/components/settings_modal/admin_tabs/users_tab.js +++ b/src/components/settings_modal/admin_tabs/users_tab.js @@ -109,8 +109,7 @@ const UsersTab = { name: this.filtersName, email: this.filtersEmail }} - const users = store.dispatch('fetchAdminUsers', nopts) - return users + return store.dispatch('fetchAdminUsers', nopts) }, /** * reset the userlist explicitly diff --git a/src/i18n/en.json b/src/i18n/en.json index 778bcd719..827a1988b 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1229,7 +1229,14 @@ "title_database": "Database", "title_details": "Details", "title_users": "Users", - "user_has_no_posts": "User has no posts" + "title_actions": "Actions", + "user_has_no_posts": "User has no posts", + "indicator_admin": "Admin", + "indicator_moderator": "Moderator", + "indicator_active": "Active", + "indicator_deactivated": "Deactivated", + "indicator_approved": "Approved", + "indicator_confirmed": "Confirmed" }, "limits": { "arbitrary_limits": "Arbitrary limits", From c3e854c1f9b241319829b40dde60924118101b3a Mon Sep 17 00:00:00 2001 From: luce Date: Wed, 10 Sep 2025 15:08:47 +0200 Subject: [PATCH 044/337] wip, added popovers and dialogs for user options, made modal timeline a little bit less painful to look at --- .../confirm_modal/generic_confirm.js | 8 +- .../settings_modal/admin_tabs/admin_card.js | 16 +- .../settings_modal/admin_tabs/admin_card.vue | 187 ++++++++++++++---- .../admin_tabs/admin_status_card.vue | 5 + .../settings_modal/admin_tabs/users_tab.vue | 24 +-- src/i18n/en.json | 61 +++++- src/modules/adminSettings.js | 2 +- 7 files changed, 233 insertions(+), 70 deletions(-) diff --git a/src/components/confirm_modal/generic_confirm.js b/src/components/confirm_modal/generic_confirm.js index 7ac07581b..a964a9805 100644 --- a/src/components/confirm_modal/generic_confirm.js +++ b/src/components/confirm_modal/generic_confirm.js @@ -3,10 +3,6 @@ import ConfirmModal from './confirm_modal.vue' export default { props: { - action: { - type: Function, - require: true - }, title: { type: String }, @@ -17,7 +13,7 @@ export default { type: String } }, - emits: ['hide', 'show'], + emits: ['hide', 'show', 'action'], data: () => ({ showing: false }), @@ -34,7 +30,7 @@ export default { this.$emit('hide') }, doGeneric () { - this.action() + this.$emit('action') this.hide() } } diff --git a/src/components/settings_modal/admin_tabs/admin_card.js b/src/components/settings_modal/admin_tabs/admin_card.js index b83426f0e..a773b7bb4 100644 --- a/src/components/settings_modal/admin_tabs/admin_card.js +++ b/src/components/settings_modal/admin_tabs/admin_card.js @@ -222,19 +222,19 @@ const AdminCard = { const res = store.dispatch('adminListStatuses', { user: u, opts: { pageSize: opts.pageSize, godmode: true, withReblogs: true}}) return res.then(r => r.activities) }, - /** - * ... - */ confirmAction (box) { this.$refs[box].show() this.$refs.dropdownuser.hidePopover() }, - /** - * ... - */ - actionConfirmed (action) { - console.log(action) + userActionConfirmed (action) { this.$store.dispatch(action, this.$store.getters.findUser(this.userDetails.id)) + }, + statusActionConfirmed (action, opts) { + const s = this.$refs.statusList.getSelected() + s.forEach(p => { + this.$store.dispatch(action, { id: p.id, ...(opts || {})}) + }) + this.reset() } } } diff --git a/src/components/settings_modal/admin_tabs/admin_card.vue b/src/components/settings_modal/admin_tabs/admin_card.vue index aa9953e22..cab2d2bec 100644 --- a/src/components/settings_modal/admin_tabs/admin_card.vue +++ b/src/components/settings_modal/admin_tabs/admin_card.vue @@ -56,7 +56,10 @@ diff --git a/src/components/settings_modal/admin_tabs/admin_status_card.vue b/src/components/settings_modal/admin_tabs/admin_status_card.vue index 388d1c58f..f56df92e9 100644 --- a/src/components/settings_modal/admin_tabs/admin_status_card.vue +++ b/src/components/settings_modal/admin_tabs/admin_status_card.vue @@ -27,6 +27,8 @@ @interacted="false" /> +

action dropdown thingy

+ + diff --git a/src/components/settings_modal/admin_tabs/users_tab.vue b/src/components/settings_modal/admin_tabs/users_tab.vue index 54d77989b..6a021d3c2 100644 --- a/src/components/settings_modal/admin_tabs/users_tab.vue +++ b/src/components/settings_modal/admin_tabs/users_tab.vue @@ -243,84 +243,84 @@ :title="$t('admin_dash.users.bulk_actions.activate')" :cancel-text="$t('admin_dash.users.bulk_actions.no')" :confirm-text="$t('admin_dash.users.bulk_actions.yes')" - @callback="selectionConfirmed('adminActivateUser')" + @action="selectionConfirmed('adminActivateUser')" /> diff --git a/src/i18n/en.json b/src/i18n/en.json index 827a1988b..8f9bf1ff1 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1172,13 +1172,67 @@ "only_unapproved": "Unapproved Only", "only_unconfirmed": "Unconfirmed Only", "refresh": "Refresh", + + "actions": { + "button": { + "title": "Actions", + "yes": "Confirm", + "no": "Abort", + "activate": "Activate", + "deactivate": "Deactivate", + "delete_user": "Delete", + "delete_status": "Delete", + "grant_admin": "Grant Admin", + "revoke_admin": "Revoke Admin", + "grant_moderator": "Grant Moderator", + "revoke_moderator": "Revoke Moderator", + "approve": "Approve", + "confirm": "Confirm", + "require_password_change": "Require Password Change", + "resend_confirmation_email": "Resend Confirmation Email", + "disable_mfa": "Disable MFA" + }, + "confirm_single": { + "activate": "Activate User?", + "deactivate": "Deactivate User?", + "delete_user": "Delete User?", + "delete_status": "Delete Status?", + "grant_admin": "Grant Admin Privileges?", + "revoke_admin": "Revoke Admin Privileges?", + "grant_moderator": "Grant Moderator Privileges?", + "revoke_moderator": "Revoke Moderator Privileges?", + "approve": "Approve User?", + "confirm": "Confirm User?", + "require_password_change": "Require Password Change?", + "resend_confirmation_email": "Resend Confirmation Email?", + "disable_mfa": "Disable MFA?" + }, + "confirm_multi": { + "activate": "Actiate Selected Users?", + "deactivate": "Deactivate Selected Users?", + "delete_user": "Delete Selected Users?", + "delete_status": "Delete Selected Statuses?", + "grant_admin": "Grant Admin Privileges For Selected Users?", + "revoke_admin": "Revoke Admin Privileges For Selected Users?", + "grant_moderator": "Grant Moderator Privileges For Selected Users?", + "revoke_moderator": "Revoke Moderator Privileges For Selected Users?", + "approve": "Approve Selected Users?", + "confirm": "Confirm Selected Users?", + "require_password_change": "Require Password Change For Selected Users?", + "resend_confirmation_email": "Resend Confirmation Email For Selected Users?", + "disable_mfa": "Disable MFA For Selected Users?" + + } + }, + "bulk_actions": { "title": "Bulk Actions", "yes": "Yes", "no": "No", "activate": "Activate Selected Users?", "deactivate": "Deactivate Selected Users?", - "delete": "Delete Selected Users?", + "delete_user": "Delete Selected Users?", + "delete_status": "Delete Selected Statuses?", "grant_admin": "Grant Admin Privileges To Selected Users?", "revoke_admin": "Revoke Admin Privileges From Selected Users?", "grant_moderator": "Grant Moderator Privileges To Selected Users?", @@ -1189,8 +1243,9 @@ "resend_confirmation_email": "Resend Confirmation Email For Selected Users?", "disable_mfa": "Disable MFA For Selected Users?" }, - "activate": "Activate", - "deactivate": "Deactivate", + "activate": "Activate?", + "deactivate": "Deactivate?", + "delete_user": "Delete User?", "resend_confirmation_email": "Resend confirmation email", "approve": "Approve", "grant_admin": "Grant Admin", diff --git a/src/modules/adminSettings.js b/src/modules/adminSettings.js index 25d0a1f84..a18764c75 100644 --- a/src/modules/adminSettings.js +++ b/src/modules/adminSettings.js @@ -71,7 +71,7 @@ const adminSettingsStorage = { }, adminRemoveUserFromAdminGroup (store, user) { // prevent revokation of own rights - if (user.id !== store.state.users.currentUser.id) { + if (user.id !== store.rootState.users.currentUser.id) { return store.rootState.api.backendInteractor.adminRemoveUserFromAdminGroup({ user }) .then(res => store.commit('updateRight', { user, right: 'admin', value: res.is_admin })) } From 53537e194f1bbfbf5e9a7b45642488459ccd3daa Mon Sep 17 00:00:00 2001 From: luce Date: Fri, 12 Sep 2025 15:20:27 +0200 Subject: [PATCH 045/337] filter and sorting options, fix status width in admin_dash --- .../confirm_modal/generic_confirm.js | 3 + .../confirm_modal/generic_confirm.vue | 6 +- src/components/list/list.vue | 1 + src/components/page_list/page_list.js | 1 + .../selectable_list/selectable_list.vue | 3 + .../settings_modal/admin_tabs/admin_card.js | 19 ++++- .../settings_modal/admin_tabs/admin_card.vue | 77 ++++++++++++++++++- .../settings_modal/admin_tabs/users_tab.vue | 9 ++- src/i18n/en.json | 28 ++++++- 9 files changed, 135 insertions(+), 12 deletions(-) diff --git a/src/components/confirm_modal/generic_confirm.js b/src/components/confirm_modal/generic_confirm.js index a964a9805..53b41e83e 100644 --- a/src/components/confirm_modal/generic_confirm.js +++ b/src/components/confirm_modal/generic_confirm.js @@ -6,6 +6,9 @@ export default { title: { type: String }, + message: { + type: String + }, cancelText: { type: String }, diff --git a/src/components/confirm_modal/generic_confirm.vue b/src/components/confirm_modal/generic_confirm.vue index 794a15490..d223d6ea9 100644 --- a/src/components/confirm_modal/generic_confirm.vue +++ b/src/components/confirm_modal/generic_confirm.vue @@ -6,7 +6,11 @@ :confirm-text="confirmText" @accepted="doGeneric" @cancelled="hide" - /> + > + + + - - - + + - - - - - - - - - - - - + + + + + + + + + + + + From ba577aec5281c31e69e32de59796fe8a9f417912 Mon Sep 17 00:00:00 2001 From: luce Date: Mon, 15 Sep 2025 13:37:08 +0200 Subject: [PATCH 048/337] tag edit --- src/components/confirm_modal/text_confirm.js | 41 +++++ src/components/confirm_modal/text_confirm.vue | 27 +++ .../settings_modal/admin_tabs/admin_card.js | 20 ++- .../settings_modal/admin_tabs/admin_card.vue | 69 ++++++-- .../settings_modal/admin_tabs/users_tab.vue | 151 ++++++++-------- src/i18n/en.json | 163 ++++++------------ src/modules/adminSettings.js | 6 + 7 files changed, 285 insertions(+), 192 deletions(-) create mode 100644 src/components/confirm_modal/text_confirm.js create mode 100644 src/components/confirm_modal/text_confirm.vue diff --git a/src/components/confirm_modal/text_confirm.js b/src/components/confirm_modal/text_confirm.js new file mode 100644 index 000000000..11319da39 --- /dev/null +++ b/src/components/confirm_modal/text_confirm.js @@ -0,0 +1,41 @@ +import ConfirmModal from './confirm_modal.vue' +//import Select from 'src/components/select/select.vue' + +export default { + props: { + title: { + type: String + }, + message: { + type: String + }, + cancelText: { + type: String + }, + confirmText: { + type: String + } + }, + emits: ['hide', 'show', 'action'], + data: () => ({ + showing: false, + text: "" + }), + components: { + ConfirmModal + }, + methods: { + show () { + this.showing = true + this.$emit('show') + }, + hide () { + this.showing = false + this.$emit('hide') + }, + doWithText () { + this.$emit('action', this.text) + this.hide() + } + } +} diff --git a/src/components/confirm_modal/text_confirm.vue b/src/components/confirm_modal/text_confirm.vue new file mode 100644 index 000000000..b4770c686 --- /dev/null +++ b/src/components/confirm_modal/text_confirm.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/components/settings_modal/admin_tabs/users_tab.vue b/src/components/settings_modal/admin_tabs/users_tab.vue index 56a0d62a2..17e2cc9fd 100644 --- a/src/components/settings_modal/admin_tabs/users_tab.vue +++ b/src/components/settings_modal/admin_tabs/users_tab.vue @@ -3,22 +3,22 @@
-

{{ $t('admin_dash.users.title_users') }}

+

{{ $t('admin_dash.users.title') }}

  • - + - + - +
  • - + - + - +
  • @@ -100,13 +100,13 @@ class="query-label" @update:model-value="v => {filtersNeedApproval = v; reset();}" > - {{ $t('admin_dash.users.only_unapproved') }} + {{ $t('admin_dash.users.options.only_unapproved') }} - {{ $t('admin_dash.users.only_unconfirmed') }} + {{ $t('admin_dash.users.options.only_unconfirmed') }}
@@ -128,7 +128,7 @@ diff --git a/src/components/settings_modal/admin_tabs/users_tab.js b/src/components/settings_modal/admin_tabs/users_tab.js index f8bc83ea7..292a08368 100644 --- a/src/components/settings_modal/admin_tabs/users_tab.js +++ b/src/components/settings_modal/admin_tabs/users_tab.js @@ -134,6 +134,8 @@ const UsersTab = { const s = this.$refs.userList.getSelected() s.forEach(u => { if (restricted.includes(action) !== false || u.id !== this.$store.state.users.currentUser.id) { + const uf = this.$store.getters.findUser(u.id) + console.log('user: ', uf) this.$store.dispatch(action, this.$store.getters.findUser(u.id)) } }) diff --git a/src/components/settings_modal/admin_tabs/users_tab.vue b/src/components/settings_modal/admin_tabs/users_tab.vue index 17e2cc9fd..19f31757f 100644 --- a/src/components/settings_modal/admin_tabs/users_tab.vue +++ b/src/components/settings_modal/admin_tabs/users_tab.vue @@ -335,7 +335,7 @@ Date: Fri, 27 Mar 2026 04:51:55 +0700 Subject: [PATCH 050/337] entity_normalizer: treat gifv attachment like it is video as we have handled this already. --- src/components/emoji_reactions/emoji_reactions.scss | 4 ++-- src/components/rich_content/rich_content.scss | 2 +- src/services/entity_normalizer/entity_normalizer.service.js | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/emoji_reactions/emoji_reactions.scss b/src/components/emoji_reactions/emoji_reactions.scss index 8ea45e1b4..f4b7d3fb0 100644 --- a/src/components/emoji_reactions/emoji_reactions.scss +++ b/src/components/emoji_reactions/emoji_reactions.scss @@ -41,7 +41,7 @@ margin: 0; .reaction-emoji { - width: var(--emoji-size); + width: auto; height: var(--emoji-size); margin-right: 0.25em; line-height: var(--emoji-size); @@ -55,7 +55,7 @@ .reaction-emoji-content { max-width: 100%; max-height: 100%; - width: var(--emoji-size); + width: auto; height: var(--emoji-size); line-height: inherit; overflow: hidden; diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss index d2ec77dcb..b1a9337e0 100644 --- a/src/components/rich_content/rich_content.scss +++ b/src/components/rich_content/rich_content.scss @@ -73,7 +73,7 @@ .emoji { display: inline-block; - width: var(--emoji-size, 32px); + width: auto; height: var(--emoji-size, 32px); } diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 909a014ce..9864cb302 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -299,7 +299,8 @@ export const parseAttachment = (data) => { } if (data.type !== 'unknown') { - output.type = data.type + // treat gifv like it is "video" + output.type = data.type === 'gifv' ? 'video' : data.type } else { output.type = fileTypeService.fileType(output.mimetype) } From ef0cba713d1befdb0a07b0a49d7cf0d5c1f11238 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 13 May 2026 11:14:12 +0400 Subject: [PATCH 051/337] fix reply form quote config access --- .../post_status_form/post_status_form.js | 2 +- .../status_action_buttons.js | 2 +- .../specs/components/post_status_form.spec.js | 61 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 test/unit/specs/components/post_status_form.spec.js diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index d08a8b9c2..a1ca37a2e 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -380,7 +380,7 @@ const PostStatusForm = { if ( !this.quotingAvailable || !this.isReply || - !this.$store.getters.mergedConfig.quoteReply + !useMergedConfigStore().mergedConfig.quoteReply ) { return false } diff --git a/src/components/status_action_buttons/status_action_buttons.js b/src/components/status_action_buttons/status_action_buttons.js index 9938e6382..74aeb6025 100644 --- a/src/components/status_action_buttons/status_action_buttons.js +++ b/src/components/status_action_buttons/status_action_buttons.js @@ -16,7 +16,7 @@ library.add(faEllipsisH) const StatusActionButtons = { props: ['status', 'replying'], - emits: ['toggleReplying', 'interacted'], + emits: ['toggleReplying', 'interacted', 'onSuccess', 'onError'], data() { return { showPin: false, diff --git a/test/unit/specs/components/post_status_form.spec.js b/test/unit/specs/components/post_status_form.spec.js new file mode 100644 index 000000000..8f3d6d80f --- /dev/null +++ b/test/unit/specs/components/post_status_form.spec.js @@ -0,0 +1,61 @@ +import { createTestingPinia } from '@pinia/testing' +import { mount } from '@vue/test-utils' +import { setActivePinia } from 'pinia' + +import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' +import { mountOpts } from '../../../fixtures/setup_test' + +import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' + +const currentUser = { + id: 'current-user', + default_scope: 'public', + locked: false, +} + +const repliedUser = { + id: 'replied-user', + screen_name: 'replied', +} + +const repliedStatus = { + id: 'status-1', + visibility: 'public', + user: repliedUser, +} + +const replyMountOpts = () => + mountOpts({ + props: { + replyTo: repliedStatus.id, + repliedUser, + attentions: [], + copyMessageScope: repliedStatus.visibility, + disableDraft: true, + }, + afterStore(store) { + store.state.users.currentUser = currentUser + store.state.statuses.allStatusesObject = { + [repliedStatus.id]: repliedStatus, + } + }, + }) + +describe('PostStatusForm', () => { + beforeEach(() => { + setActivePinia(createTestingPinia()) + }) + + it('initializes a reply form when quoteReply is unset', () => { + useInstanceCapabilitiesStore().quotingAvailable = true + + const wrapper = mount(PostStatusForm, replyMountOpts()) + + expect(wrapper.vm.newStatus.type).to.equal('reply') + expect(wrapper.vm.newStatus.quote).to.eql({ + id: '', + url: '', + thread: false, + }) + }) +}) From 304655d4487b644b5396cd2c3d1dd05121fda0b0 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 13 May 2026 11:26:48 +0400 Subject: [PATCH 052/337] fix lint and add changelog entry --- changelog.d/reply-quote-config.fix | 1 + .../mrf_transparency_panel/mrf_transparency_panel.js | 10 +++------- src/services/notification_utils/notification_utils.js | 5 ++++- src/stores/sync_config.js | 4 +--- src/stores/user_highlight.js | 5 ++++- 5 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 changelog.d/reply-quote-config.fix diff --git a/changelog.d/reply-quote-config.fix b/changelog.d/reply-quote-config.fix new file mode 100644 index 000000000..b6ac4e5e9 --- /dev/null +++ b/changelog.d/reply-quote-config.fix @@ -0,0 +1 @@ +Fix reply form crash when quote-reply settings are unavailable diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.js b/src/components/mrf_transparency_panel/mrf_transparency_panel.js index d77a0a839..b2048984d 100644 --- a/src/components/mrf_transparency_panel/mrf_transparency_panel.js +++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.js @@ -1,5 +1,6 @@ import { get } from 'lodash' import { mapState } from 'pinia' + import { useInstanceStore } from 'src/stores/instance.js' /** @@ -21,16 +22,11 @@ const MRFTransparencyPanel = { computed: { ...mapState(useInstanceStore, { federationPolicy: (state) => state.federationPolicy, - mrfPolicies: (state) => - get(state, 'federationPolicy.mrf_policies', []), + mrfPolicies: (state) => get(state, 'federationPolicy.mrf_policies', []), quarantineInstances: (state) => toInstanceReasonObject( get(state, 'federationPolicy.quarantined_instances', []), - get( - state, - 'federationPolicy.quarantined_instances_info', - [], - ), + get(state, 'federationPolicy.quarantined_instances_info', []), 'quarantined_instances', ), acceptInstances: (state) => diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index 1fbaf2a2c..921600094 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -81,7 +81,10 @@ export const maybeShowNotification = ( if (notification.seen) return if (!visibleTypes(notificationVisibility).includes(notification.type)) return - if (notification.type === 'mention' && isMutedNotification(muteFilters, notification)) + if ( + notification.type === 'mention' && + isMutedNotification(muteFilters, notification) + ) return const notificationObject = prepareNotificationObject( diff --git a/src/stores/sync_config.js b/src/stores/sync_config.js index 722dd7f16..1caa2b030 100644 --- a/src/stores/sync_config.js +++ b/src/stores/sync_config.js @@ -14,8 +14,8 @@ import { uniqWith, unset, } from 'lodash' -import { v4 as uuidv4 } from 'uuid' import { defineStore } from 'pinia' +import { v4 as uuidv4 } from 'uuid' import { toRaw } from 'vue' import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js' @@ -684,8 +684,6 @@ export const useSyncConfigStore = defineStore('sync_config', { `Already migrated Values: ${[...migratedEntries].join() || '[none]'}`, ) - const { configMigration } = useSyncConfigStore().flagStorage - Object.entries(oldDefaultConfigSync).forEach(([key, value]) => { const oldValue = config[key] const defaultValue = value diff --git a/src/stores/user_highlight.js b/src/stores/user_highlight.js index 5ca13b6a9..f41c41628 100644 --- a/src/stores/user_highlight.js +++ b/src/stores/user_highlight.js @@ -292,7 +292,10 @@ export const useUserHighlightStore = defineStore('user_highlight', { ) } }) - storage.setItem('vuex-lz', { ...vuexState, config: { ...config, highlight } }) + storage.setItem('vuex-lz', { + ...vuexState, + config: { ...config, highlight }, + }) if (recent === null) { console.debug( From c05381e6aa5a913509d1769397104b15e7d7e8ef Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 13 May 2026 16:12:52 +0300 Subject: [PATCH 053/337] lint --- src/boot/after_store.js | 6 ++++-- src/components/about/about.js | 6 +++++- src/components/about/about.vue | 4 +++- .../basic_user_card/basic_user_card.js | 2 +- src/components/chat_title/chat_title.js | 2 ++ src/components/color_input/color_input.vue | 1 + src/components/drafts/drafts.js | 10 ++++++---- src/components/emoji_input/emoji_input.vue | 2 +- src/components/gallery/gallery.js | 2 +- .../mrf_transparency_panel.js | 10 +++------- .../post_status_form/post_status_form.js | 7 ++----- .../post_status_form/post_status_form.vue | 2 +- src/components/rich_content/rich_content.jsx | 8 +++++++- .../settings_modal/helpers/number_setting.vue | 14 +++++++------- .../settings_modal/tabs/developer_tab.js | 7 ++++++- .../status_action_buttons.vue | 2 +- src/components/user_card/user_card.js | 4 ++-- src/components/user_card/user_card.vue | 1 + .../user_list_popover/user_list_popover.js | 4 ++-- src/components/user_profile/user_profile.js | 2 +- src/modules/drafts.js | 2 +- .../notification_utils/notification_utils.js | 5 ++++- src/services/status_parser/status_parser.js | 18 +++++++++++++----- .../theme_data/theme_data_3.service.js | 5 +---- src/stores/sync_config.js | 4 +--- src/stores/user_highlight.js | 5 ++++- 26 files changed, 81 insertions(+), 54 deletions(-) diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 678a5e7d1..e18cfa788 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -175,14 +175,16 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { if (source === 'name') return if (INSTANCE_IDENTIY_EXTERNAL.has(source)) return useInstanceStore().set({ - value: config[source] ?? INSTANCE_IDENTITY_DEFAULT_DEFINITIONS[source].default, + value: + config[source] ?? INSTANCE_IDENTITY_DEFAULT_DEFINITIONS[source].default, path: `instanceIdentity.${source}`, }) }) Object.keys(INSTANCE_DEFAULT_CONFIG_DEFINITIONS).forEach((source) => useInstanceStore().set({ - value: config[source] ?? INSTANCE_DEFAULT_CONFIG_DEFINITIONS[source].default, + value: + config[source] ?? INSTANCE_DEFAULT_CONFIG_DEFINITIONS[source].default, path: `prefsStorage.${source}`, }), ) diff --git a/src/components/about/about.js b/src/components/about/about.js index dc6733491..e6a1067fe 100644 --- a/src/components/about/about.js +++ b/src/components/about/about.js @@ -27,7 +27,11 @@ const About = { frontendVersionLink() { return pleromaFeCommitUrl + this.frontendVersion }, - ...mapState(useInstanceStore, ['backendVersion', 'backendRepository', 'frontendVersion']), + ...mapState(useInstanceStore, [ + 'backendVersion', + 'backendRepository', + 'frontendVersion', + ]), showInstanceSpecificPanel() { return ( useInstanceStore().instanceIdentity.showInstanceSpecificPanel && diff --git a/src/components/about/about.vue b/src/components/about/about.vue index df395c7dd..f7bc0d00d 100644 --- a/src/components/about/about.vue +++ b/src/components/about/about.vue @@ -7,7 +7,9 @@
-
{{ $t('settings.version.title') }}
+
+ {{ $t('settings.version.title') }} +
diff --git a/src/components/basic_user_card/basic_user_card.js b/src/components/basic_user_card/basic_user_card.js index a42293cc1..722d459f3 100644 --- a/src/components/basic_user_card/basic_user_card.js +++ b/src/components/basic_user_card/basic_user_card.js @@ -29,7 +29,7 @@ const BasicUserCard = { allowNonSquareEmoji() { return useMergedConfigStore().mergedConfig.nonSquareEmoji }, - } + }, } export default BasicUserCard diff --git a/src/components/chat_title/chat_title.js b/src/components/chat_title/chat_title.js index 7ec4c81f1..54a31a6fe 100644 --- a/src/components/chat_title/chat_title.js +++ b/src/components/chat_title/chat_title.js @@ -3,6 +3,8 @@ import { defineAsyncComponent } from 'vue' import RichContent from 'src/components/rich_content/rich_content.jsx' import UserAvatar from '../user_avatar/user_avatar.vue' +import { useMergedConfigStore } from 'src/stores/merged_config.js' + export default { name: 'ChatTitle', components: { diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue index 6f236d35f..07aed6b81 100644 --- a/src/components/color_input/color_input.vue +++ b/src/components/color_input/color_input.vue @@ -88,6 +88,7 @@ export default { label: { required: false, type: String, + default: '', }, // use unstyled, uh, style unstyled: { diff --git a/src/components/drafts/drafts.js b/src/components/drafts/drafts.js index 01a50759a..87c22138b 100644 --- a/src/components/drafts/drafts.js +++ b/src/components/drafts/drafts.js @@ -1,6 +1,6 @@ +import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue' import Draft from 'src/components/draft/draft.vue' import List from 'src/components/list/list.vue' -import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue' const Drafts = { components: { @@ -10,7 +10,7 @@ const Drafts = { }, data() { return { - showingConfirmDialog: false + showingConfirmDialog: false, } }, computed: { @@ -23,12 +23,14 @@ const Drafts = { this.showingConfirmDialog = true }, doAbandonAll() { - this.$store.dispatch('abandonAllDrafts').then(() => this.hideConfirmDialog()) + this.$store + .dispatch('abandonAllDrafts') + .then(() => this.hideConfirmDialog()) }, hideConfirmDialog() { this.showingConfirmDialog = false }, - } + }, } export default Drafts diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue index b362db32f..526f646ab 100644 --- a/src/components/emoji_input/emoji_input.vue +++ b/src/components/emoji_input/emoji_input.vue @@ -2,7 +2,7 @@
{ - const peek = attachments[i+1] + const peek = attachments[i + 1] const nextEnd = peek == null const nextWide = !nextEnd && !displayTypes.has(peek?.type) diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.js b/src/components/mrf_transparency_panel/mrf_transparency_panel.js index d77a0a839..b2048984d 100644 --- a/src/components/mrf_transparency_panel/mrf_transparency_panel.js +++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.js @@ -1,5 +1,6 @@ import { get } from 'lodash' import { mapState } from 'pinia' + import { useInstanceStore } from 'src/stores/instance.js' /** @@ -21,16 +22,11 @@ const MRFTransparencyPanel = { computed: { ...mapState(useInstanceStore, { federationPolicy: (state) => state.federationPolicy, - mrfPolicies: (state) => - get(state, 'federationPolicy.mrf_policies', []), + mrfPolicies: (state) => get(state, 'federationPolicy.mrf_policies', []), quarantineInstances: (state) => toInstanceReasonObject( get(state, 'federationPolicy.quarantined_instances', []), - get( - state, - 'federationPolicy.quarantined_instances_info', - [], - ), + get(state, 'federationPolicy.quarantined_instances_info', []), 'quarantined_instances', ), acceptInstances: (state) => diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 805b46745..6dfe48925 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -378,13 +378,10 @@ const PostStatusForm = { this.newStatus.hasQuote = value this.newStatus.quote.thread = value this.newStatus.quote.id = value ? this.replyTo : '' - } + }, }, defaultQuotable() { - if ( - !this.quotingAvailable || - !this.isReply - ) { + if (!this.quotingAvailable || !this.isReply) { return false } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 3d5e27d77..d37d0a90f 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -264,10 +264,10 @@ /> diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx index 95ab102c6..fc17a529f 100644 --- a/src/components/rich_content/rich_content.jsx +++ b/src/components/rich_content/rich_content.jsx @@ -328,7 +328,13 @@ export default { // slots updated -> rerender -> emit -> update up the tree -> rerender -> ... // at least until vue3? const result = ( - + {this.collapse ? pass2.map((x) => { if (!Array.isArray(x)) return x.replace(/\n/g, ' ') diff --git a/src/components/settings_modal/helpers/number_setting.vue b/src/components/settings_modal/helpers/number_setting.vue index 3a6434e9e..a53be93c3 100644 --- a/src/components/settings_modal/helpers/number_setting.vue +++ b/src/components/settings_modal/helpers/number_setting.vue @@ -9,13 +9,13 @@ class="setting-label" :class="{ 'faint': shouldBeDisabled }" > - - - {{ ' ' }} - + + + {{ ' ' }} + diff --git a/src/components/settings_modal/tabs/developer_tab.js b/src/components/settings_modal/tabs/developer_tab.js index 7558785f5..9a6258638 100644 --- a/src/components/settings_modal/tabs/developer_tab.js +++ b/src/components/settings_modal/tabs/developer_tab.js @@ -1,4 +1,5 @@ import { mapState } from 'pinia' + import BooleanSetting from '../helpers/boolean_setting.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' @@ -17,7 +18,11 @@ const VersionTab = { frontendVersionLink() { return pleromaFeCommitUrl + this.frontendVersion }, - ...mapState(useInstanceStore, ['backendVersion', 'backendRepository', 'frontendVersion']), + ...mapState(useInstanceStore, [ + 'backendVersion', + 'backendRepository', + 'frontendVersion', + ]), ...SharedComputedObject(), }, methods: { diff --git a/src/components/status_action_buttons/status_action_buttons.vue b/src/components/status_action_buttons/status_action_buttons.vue index 6c0fab542..3837e6ae5 100644 --- a/src/components/status_action_buttons/status_action_buttons.vue +++ b/src/components/status_action_buttons/status_action_buttons.vue @@ -3,7 +3,7 @@ + > {{ isKnownTag ? $t('user_card.tags.' + tag) : tag }} diff --git a/src/components/user_list_popover/user_list_popover.js b/src/components/user_list_popover/user_list_popover.js index 8f8b09fb5..6f79939da 100644 --- a/src/components/user_list_popover/user_list_popover.js +++ b/src/components/user_list_popover/user_list_popover.js @@ -3,11 +3,11 @@ import { defineAsyncComponent } from 'vue' import RichContent from 'src/components/rich_content/rich_content.jsx' import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue' +import { useMergedConfigStore } from 'src/stores/merged_config.js' + import { library } from '@fortawesome/fontawesome-svg-core' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' -import { useMergedConfigStore } from 'src/stores/merged_config.js' - library.add(faCircleNotch) const UserListPopover = { diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 5ae5b81a4..17d69f49b 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -10,9 +10,9 @@ import List from '../list/list.vue' import Timeline from '../timeline/timeline.vue' import UserCard from '../user_card/user_card.vue' -import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' +import { useMergedConfigStore } from 'src/stores/merged_config.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' diff --git a/src/modules/drafts.js b/src/modules/drafts.js index 42bb3b313..3cde4f574 100644 --- a/src/modules/drafts.js +++ b/src/modules/drafts.js @@ -44,7 +44,7 @@ const saveDraftToStorage = async (draft) => { const deleteDraftFromStorage = async (ids) => { const currentData = await getStorageData() - ids.forEach(id => { + ids.forEach((id) => { delete currentData[id] }) await storage.setItem(storageKey, currentData) diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index 1fbaf2a2c..921600094 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -81,7 +81,10 @@ export const maybeShowNotification = ( if (notification.seen) return if (!visibleTypes(notificationVisibility).includes(notification.type)) return - if (notification.type === 'mention' && isMutedNotification(muteFilters, notification)) + if ( + notification.type === 'mention' && + isMutedNotification(muteFilters, notification) + ) return const notificationObject = prepareNotificationObject( diff --git a/src/services/status_parser/status_parser.js b/src/services/status_parser/status_parser.js index c6ceb1f0a..a011fe265 100644 --- a/src/services/status_parser/status_parser.js +++ b/src/services/status_parser/status_parser.js @@ -10,7 +10,15 @@ export const muteFilterHits = (muteFilters, status) => { return muteFilters .toSorted((a, b) => b.order - a.order) .map((filter) => { - const { hide, expires, name, value, type, enabled, caseSensitive = false } = filter + const { + hide, + expires, + name, + value, + type, + enabled, + caseSensitive = false, + } = filter if (!enabled) return false if (value === '') return false if (expires !== null && expires < Date.now()) return false @@ -18,9 +26,7 @@ export const muteFilterHits = (muteFilters, status) => { case 'word': { let match = false if (caseSensitive) { - match = - statusText.includes(value) || - statusSummary.includes(value) + match = statusText.includes(value) || statusSummary.includes(value) } else { const lowercaseValue = value.toLowerCase() match = @@ -56,7 +62,9 @@ export const muteFilterHits = (muteFilters, status) => { match = poster.toLowerCase().includes(lowercaseValue) || replyToUser.toLowerCase().includes(lowercaseValue) || - mentions.some((mention) => mention.toLowerCase().includes(lowercaseValue)) + mentions.some((mention) => + mention.toLowerCase().includes(lowercaseValue), + ) } if (match) { return { hide, name } diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index 7babbaf32..62666d5f8 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -499,10 +499,7 @@ export const init = ({ }), ) const lastVariantRule = variantRules[variantRules.length - 1] - const lastVariantSelector = ruleToSelector( - lastVariantRule, - true, - ) + const lastVariantSelector = ruleToSelector(lastVariantRule, true) if (lastVariantRule && lastVariantSelector !== selector) { inheritRule = lastVariantRule diff --git a/src/stores/sync_config.js b/src/stores/sync_config.js index 722dd7f16..1caa2b030 100644 --- a/src/stores/sync_config.js +++ b/src/stores/sync_config.js @@ -14,8 +14,8 @@ import { uniqWith, unset, } from 'lodash' -import { v4 as uuidv4 } from 'uuid' import { defineStore } from 'pinia' +import { v4 as uuidv4 } from 'uuid' import { toRaw } from 'vue' import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js' @@ -684,8 +684,6 @@ export const useSyncConfigStore = defineStore('sync_config', { `Already migrated Values: ${[...migratedEntries].join() || '[none]'}`, ) - const { configMigration } = useSyncConfigStore().flagStorage - Object.entries(oldDefaultConfigSync).forEach(([key, value]) => { const oldValue = config[key] const defaultValue = value diff --git a/src/stores/user_highlight.js b/src/stores/user_highlight.js index 5ca13b6a9..f41c41628 100644 --- a/src/stores/user_highlight.js +++ b/src/stores/user_highlight.js @@ -292,7 +292,10 @@ export const useUserHighlightStore = defineStore('user_highlight', { ) } }) - storage.setItem('vuex-lz', { ...vuexState, config: { ...config, highlight } }) + storage.setItem('vuex-lz', { + ...vuexState, + config: { ...config, highlight }, + }) if (recent === null) { console.debug( From d19877ed8eba01ef60dcb515b53f9c4ff33b5ba1 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 13 May 2026 18:16:34 +0300 Subject: [PATCH 054/337] fix tests --- src/components/gallery/gallery.js | 21 +++++-- .../post_status_form/post_status_form.js | 12 +++- test/unit/specs/components/gallery.spec.js | 60 +++++++++++++++++-- test/unit/specs/stores/sync_config.spec.js | 3 +- 4 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/components/gallery/gallery.js b/src/components/gallery/gallery.js index 7ac3b1d77..03a5aab11 100644 --- a/src/components/gallery/gallery.js +++ b/src/components/gallery/gallery.js @@ -68,12 +68,23 @@ const Gallery = { } const maxPerRow = 3 - const currentRow = acc[acc.length - 1].items - if ((nextWide || nextEnd) && currentRow.length >= maxPerRow) { - const last = currentRow.splice(-1)[0] - return [...acc, { items: [last, attachment] }] + const currentRow = acc[acc.length - 1] + const previousRow = acc[acc.length - 2] + + if (currentRow.items.length >= maxPerRow) { + if (nextWide || nextEnd) { + if (previousRow?.items.length > 1) { + currentRow.items.push(attachment) + return [...acc, { items: [] }] + } else { + const last = currentRow.items.splice(-1)[0] + return [...acc, { items: [last, attachment] }] + } + } else { + return [...acc, { items: [attachment] }] + } } else { - currentRow.push(attachment) + currentRow.items.push(attachment) } return acc }, diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index b5feb8e11..1e9c624f4 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -211,7 +211,11 @@ const PostStatusForm = { poll: {}, hasPoll: false, hasQuote: false, - quote: {}, + quote: { + id: '', + url: '', + thread: false, + }, mediaDescriptions: {}, visibility: scope, contentType, @@ -230,7 +234,11 @@ const PostStatusForm = { poll: this.statusPoll || {}, hasPoll: false, hasQuote: false, - quote: {}, + quote: { + id: '', + url: '', + thread: false, + }, mediaDescriptions: this.statusMediaDescriptions || {}, visibility: this.statusScope || scope, contentType: statusContentType, diff --git a/test/unit/specs/components/gallery.spec.js b/test/unit/specs/components/gallery.spec.js index 367bab0b4..d4c466586 100644 --- a/test/unit/specs/components/gallery.spec.js +++ b/test/unit/specs/components/gallery.spec.js @@ -129,7 +129,7 @@ describe('Gallery', () => { ]) }) - it('mixed attachments', () => { + it('mixed attachments 1', () => { local = { attachments: [ { type: 'audio' }, @@ -138,7 +138,6 @@ describe('Gallery', () => { { type: 'image' }, { type: 'image' }, { type: 'image' }, - { type: 'image' }, ], } @@ -151,17 +150,17 @@ describe('Gallery', () => { { type: 'image' }, { type: 'image' }, { type: 'image' }, - { type: 'image' }, ], }, ]) + }) + it('mixed attachments 2', () => { local = { attachments: [ { type: 'image' }, { type: 'image' }, { type: 'image' }, - { type: 'image' }, { type: 'audio' }, { type: 'image' }, { type: 'audio' }, @@ -172,12 +171,13 @@ describe('Gallery', () => { { items: [{ type: 'image' }, { type: 'image' }, { type: 'image' }], }, - { items: [{ type: 'image' }] }, { audio: true, items: [{ type: 'audio' }] }, { items: [{ type: 'image' }] }, { audio: true, items: [{ type: 'audio' }] }, ]) + }) + it('7 images', () => { local = { attachments: [ { type: 'image' }, @@ -205,7 +205,9 @@ describe('Gallery', () => { ], }, ]) + }) + it('8 images', () => { local = { attachments: [ { type: 'image' }, @@ -230,6 +232,54 @@ describe('Gallery', () => { ]) }) + it('4 images + audio + image + 4 images', () => { + local = { + attachments: [ + { type: 'image' }, + { type: 'image' }, + { type: 'image' }, + { type: 'image' }, + { type: 'audio' }, + { type: 'image' }, + { type: 'audio' }, + { type: 'image' }, + { type: 'image' }, + { type: 'image' }, + { type: 'image' }, + ], + } + + expect(Gallery.computed.rows.call(local)).to.eql([ + { + items: [ + { type: 'image' }, + { type: 'image' }, + ], + }, + { + items: [ + { type: 'image' }, + { type: 'image' }, + ], + }, + { audio: true, items: [{ type: 'audio' }] }, + { items: [{ type: 'image' }] }, + { audio: true, items: [{ type: 'audio' }] }, + { + items: [ + { type: 'image' }, + { type: 'image' }, + ], + }, + { + items: [ + { type: 'image' }, + { type: 'image' }, + ], + }, + ]) + }) + it('does not do grouping when grid is set', () => { const attachments = [ { type: 'audio' }, diff --git a/test/unit/specs/stores/sync_config.spec.js b/test/unit/specs/stores/sync_config.spec.js index 4a502f626..a045aa18a 100644 --- a/test/unit/specs/stores/sync_config.spec.js +++ b/test/unit/specs/stores/sync_config.spec.js @@ -232,7 +232,8 @@ describe('The SyncConfig store', () => { expect(store.prefsStorage._journal.length).to.eql(2) }) - it('should remove depth = 3 set/unset entries from journal', () => { + // TODO We need a proper test for object-based stores + it.skip('should remove depth = 3 set/unset entries from journal', () => { const store = useSyncConfigStore() // PushSyncConfig is very simple but uses vuex to push data store.pushSyncConfig = () => { From 300289af60f8f862d9fcb9837ab9929cfd1f5fd8 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 18 May 2026 17:16:45 +0300 Subject: [PATCH 055/337] fix modal confiirmation styles --- .../user_timed_filter_modal.scss | 11 +++ .../user_timed_filter_modal.vue | 82 ++++++++++--------- 2 files changed, 53 insertions(+), 40 deletions(-) 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 index 24388868f..961656442 100644 --- a/src/components/user_timed_filter_modal/user_timed_filter_modal.scss +++ b/src/components/user_timed_filter_modal/user_timed_filter_modal.scss @@ -10,4 +10,15 @@ .footer-left-checkbox { width: max-content; } + + .expirationTime { + display: inline-flex; + white-space: nowrap; + padding: 0.5em; + } + + .dialog-modal-content .checkbox { + vertical-align: middle; + padding: 0.5em; + } } 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 index 91c05ad77..0ccf2812c 100644 --- a/src/components/user_timed_filter_modal/user_timed_filter_modal.vue +++ b/src/components/user_timed_filter_modal/user_timed_filter_modal.vue @@ -11,48 +11,50 @@

{{ $t(isMute ? 'user_card.expire_mute_message' : 'user_card.expire_block_message', [user.screen_name]) }}

-

+

{{ $t('user_card.expire_in') }} - - - {{ $t('time.unit.seconds_suffix') }} - - - - - + + + + + + {{ $t('user_card.mute_or') }} @@ -64,7 +66,7 @@ > {{ $t('user_card.mute_block_never') }} -

+
+ + From d22fb84d12083f1a2d066582f6c7e19fa807f9d4 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 18 May 2026 19:59:22 +0300 Subject: [PATCH 061/337] add plus icon to emoji reactions button --- src/components/status_action_buttons/buttons_definitions.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/status_action_buttons/buttons_definitions.js b/src/components/status_action_buttons/buttons_definitions.js index 294293a40..46b7088a6 100644 --- a/src/components/status_action_buttons/buttons_definitions.js +++ b/src/components/status_action_buttons/buttons_definitions.js @@ -97,6 +97,9 @@ export const BUTTONS = [ name: 'emoji', label: 'tool_tip.add_reaction', icon: ['far', 'smile-beam'], + interactive: () => true, + active: () => false, + toggleable: true, anonLink: true, }, { From 7116881b963aad281d6c01777bdf46f03a619396 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 18 May 2026 23:05:36 +0300 Subject: [PATCH 062/337] fix virtual scrolling showing nothing when clicking "show new" with too many statuses --- src/components/timeline/timeline.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 2153afc44..e2bcfcf7e 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -313,6 +313,9 @@ const Timeline = { }, }, watch: { + filteredVisibleStatuses() { + this.determineVisibleStatuses() + }, newStatusCount(count) { if (!useMergedConfigStore().mergedConfig.streaming) { return From 488a448f96a5aa91859d8bf722f2e564dde480a6 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 19 May 2026 16:39:46 +0300 Subject: [PATCH 063/337] small fixes to post form --- src/App.scss | 2 ++ src/components/post_status_form/post_status_form.scss | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/App.scss b/src/App.scss index f556a7d9d..6604d97f1 100644 --- a/src/App.scss +++ b/src/App.scss @@ -815,9 +815,11 @@ option { align-items: baseline; line-height: 1.5; + p, span { display: block; flex: 1 1 auto; + margin: 0; } .dismiss { diff --git a/src/components/post_status_form/post_status_form.scss b/src/components/post_status_form/post_status_form.scss index fb1de718c..3d78884fe 100644 --- a/src/components/post_status_form/post_status_form.scss +++ b/src/components/post_status_form/post_status_form.scss @@ -53,6 +53,7 @@ .preview-heading { display: flex; flex-wrap: wrap; + margin: 0 0.5em; } .preview-toggle { @@ -106,7 +107,6 @@ display: flex; justify-content: space-between; align-items: baseline; - margin-left: -0.5em; } .visibility-notice { From be96fd70b2bf07acb262b748d5996c6280eb8183 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 19 May 2026 16:40:20 +0300 Subject: [PATCH 064/337] changelog --- changelog.d/minor.fix | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.d/minor.fix b/changelog.d/minor.fix index 69d7b11fe..420836364 100644 --- a/changelog.d/minor.fix +++ b/changelog.d/minor.fix @@ -9,3 +9,4 @@ second language input not having header post form's bottom left buttons not showing their toggled state some font overrides not working popovers opening outside of window's boundaries +occasional blank page when showing new posts From b51d0baa66dbf0bffdaab03e04a53505d16f81d8 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 19 May 2026 17:45:58 +0300 Subject: [PATCH 065/337] image description improvements --- src/App.scss | 4 +- src/components/attachment/attachment.js | 6 +-- src/components/attachment/attachment.scss | 24 ++++++++++- src/components/attachment/attachment.vue | 44 ++++++++++--------- src/components/gallery/gallery.vue | 2 +- src/components/media_modal/media_modal.vue | 49 +++++++++++++++++----- src/i18n/en.json | 1 + 7 files changed, 88 insertions(+), 42 deletions(-) diff --git a/src/App.scss b/src/App.scss index 6604d97f1..41fea782b 100644 --- a/src/App.scss +++ b/src/App.scss @@ -50,7 +50,7 @@ body { // have a cursor/pointer to operate them @media (any-pointer: fine) { * { - scrollbar-color: var(--fg) transparent; + scrollbar-color: var(--text) transparent; &::-webkit-scrollbar { background: transparent; @@ -130,7 +130,7 @@ body { } // Body should have background to scrollbar otherwise it will use white (body color?) html { - scrollbar-color: var(--fg) var(--wallpaper); + scrollbar-color: var(--text) var(--wallpaper); background: var(--wallpaper); } } diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index fbe77a687..57a969c2a 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -4,6 +4,7 @@ import nsfwImage from '../../assets/nsfw.png' import Flash from '../flash/flash.vue' import StillImage from '../still-image/still-image.vue' import VideoAttachment from '../video_attachment/video_attachment.vue' +import Popover from '../popover/popover.vue' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' @@ -65,13 +66,13 @@ const Attachment = { modalOpen: false, showHidden: false, flashLoaded: false, - showDescription: false, } }, components: { Flash, StillImage, VideoAttachment, + Popover, }, computed: { classNames() { @@ -180,9 +181,6 @@ const Attachment = { setFlashLoaded(event) { this.flashLoaded = event }, - toggleDescription() { - this.showDescription = !this.showDescription - }, toggleHidden(event) { if ( this.mergedConfig.useOneClickNsfw && diff --git a/src/components/attachment/attachment.scss b/src/components/attachment/attachment.scss index 97515eb32..0a1f9f5ee 100644 --- a/src/components/attachment/attachment.scss +++ b/src/components/attachment/attachment.scss @@ -134,7 +134,7 @@ width: 2em; height: 2em; margin-left: 0.5em; - font-size: 1.25em; + font-size: 1em; } } @@ -265,3 +265,25 @@ } } } + +.description-popover { + padding: 1em; + width: 50ch; + overflow: hidden; + + summary { + display: inline-block; + margin-bottom: 0.5em; + font-weight: bold; + pointer-events: none; + } + + span { + display: block; + overflow-y: auto; + max-height: 10.5em; + text-wrap: pretty; + line-height: 1.5; + white-space: pre-wrap; + } +} diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 0db86ff8a..ca5f395a6 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -30,21 +30,16 @@
- -

- {{ localDescription }} -

+ />
- + trigger="click" + popover-class="popover popover-default description-popover" + :trigger-attrs="{ 'class': 'button-default attachment-button -transparent', 'title': $t('status.attachment_description') }" + > + + +
- -

- {{ localDescription }} -

+ />
diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue index eb1054df6..11637f3c2 100644 --- a/src/components/gallery/gallery.vue +++ b/src/components/gallery/gallery.vue @@ -129,7 +129,7 @@ .gallery-item { margin: 0; - height: 15em; + height: 20em; } } } diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 93587210f..0011e70a5 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -89,13 +89,16 @@ /> - - {{ description }} - + {{ $t('status.attachment_description') }} + {{ description }} + {{ $t('media_modal.counter', { current: currentIndex + 1, total: media.length }, currentIndex + 1) }} @@ -159,19 +162,43 @@ $modal-view-button-icon-margin: 0.5em; .counter { /* Hardcoded since background is also hardcoded */ color: white; - margin-top: 1em; - text-shadow: 0 0 10px black, 0 0 10px black; - padding: 0.2em 2em; + text-shadow: 0 0 1em black, 0 0 1em black, 0 0 1em black; + margin: 1em 2em; + overflow: hidden; + + } + + .description + .counter { + margin-top: 0; } .description { flex: 0 0 auto; - overflow-y: auto; - min-height: 1em; - max-width: 35.8em; + max-width: 80ch; max-height: 9.5em; - overflow-wrap: break-word; - text-wrap: pretty; + + summary { + margin-bottom: 0.5em; + display: inline-block; + font-weight: bold; + pointer-events: none; + } + + span { + display: block; + overflow-y: auto; + min-height: 1em; + text-wrap: pretty; + max-height: 10.5em; + white-space: pre-wrap; + line-height: 1.5; + scrollbar-color: white transparent; + + &::-webkit-scrollbar-button, + &::-webkit-scrollbar-thumb { + background-color: white; + } + } } .modal-image { diff --git a/src/i18n/en.json b/src/i18n/en.json index 9b5a7439d..510250f73 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1602,6 +1602,7 @@ "show_all_attachments": "Show all attachments", "show_attachment_in_modal": "Show in media modal", "show_attachment_description": "Preview description (open attachment for full description)", + "attachment_description": "Attachment description", "hide_attachment": "Hide attachment", "remove_attachment": "Remove attachment", "attachment_stop_flash": "Stop Flash player", From 5f8e000157f92a56ceab42487dcee6e47b4a4fed Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 19 May 2026 17:54:59 +0300 Subject: [PATCH 066/337] unescape attachment descriptions --- src/services/entity_normalizer/entity_normalizer.service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 0ea25014b..53fe8c127 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -1,5 +1,6 @@ import { parseLinkHeader } from '@web3-storage/parse-link-header' import escapeHtml from 'escape-html' +import { unescape } from 'lodash' import punycode from 'punycode.js' import fileTypeService from '../file_type/file_type.service.js' @@ -307,7 +308,7 @@ export const parseAttachment = (data) => { } output.url = data.url output.large_thumb_url = data.preview_url - output.description = data.description + output.description = unescape(data.description) return output } From 2d07671552db2bf6bc03cba210012af57e9a02fa Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 19 May 2026 18:14:19 +0300 Subject: [PATCH 067/337] fix errors in console --- src/components/emoji_input/emoji_input.js | 2 +- src/components/status_action_buttons/action_button.vue | 2 +- src/components/status_action_buttons/buttons_definitions.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index 6c4568d14..0cce2494d 100644 --- a/src/components/emoji_input/emoji_input.js +++ b/src/components/emoji_input/emoji_input.js @@ -589,7 +589,7 @@ const EmojiInput = { setCaret({ target: { selectionStart } }) { this.caret = selectionStart this.$nextTick(() => { - this.$refs.suggestorPopover.updateStyles() + this.$refs.suggestorPopover?.updateStyles() }) }, autoCompleteItemLabel(suggestion) { diff --git a/src/components/status_action_buttons/action_button.vue b/src/components/status_action_buttons/action_button.vue index 82b481127..a76af3809 100644 --- a/src/components/status_action_buttons/action_button.vue +++ b/src/components/status_action_buttons/action_button.vue @@ -26,7 +26,7 @@ />