implemented status visibility change

This commit is contained in:
Henry Jameson 2026-06-10 17:19:45 +03:00
commit 19d8875196
16 changed files with 225 additions and 98 deletions

View file

@ -15,7 +15,7 @@ const BasicUserCard = {
showLineLabels: { showLineLabels: {
type: Boolean, type: Boolean,
default: false, default: false,
} },
}, },
components: { components: {
UserPopover, UserPopover,

View file

@ -185,16 +185,15 @@ const ModerationTools = {
entries() { entries() {
return ENTRIES.map(({ check, label, separator, conditions }) => { return ENTRIES.map(({ check, label, separator, conditions }) => {
if (separator) return 'separator' if (separator) return 'separator'
const [, negateToken, group, name] = /^([!~]?)([a-z-_]+):([a-z-_]+)$/.exec( const [, negateToken, group, name] =
check, /^([!~]?)([a-z-_]+):([a-z-_]+)$/.exec(check)
)
const hasTag = this.tagsSet.has(`${group}:${name}`) const hasTag = this.tagsSet.has(`${group}:${name}`)
const noTag = this.tagsSet.has(`!${group}:${name}`) const noTag = this.tagsSet.has(`!${group}:${name}`)
const maybeTag = this.tagsSet.has(`~${group}:${name}`) const maybeTag = this.tagsSet.has(`~${group}:${name}`)
// We are checking for condition to show element, i.e. only show "activate" if user is "deactivated" // We are checking for condition to show element, i.e. only show "activate" if user is "deactivated"
const checkNegated = (negateToken === '!' || negateToken === '~') const checkNegated = negateToken === '!' || negateToken === '~'
// Naturally, new value should also be the same // Naturally, new value should also be the same
const value = checkNegated const value = checkNegated
@ -442,7 +441,11 @@ const ModerationTools = {
return this.$store.state.users.currentUser.privileges.has(privilege) return this.$store.state.users.currentUser.privileges.has(privilege)
}, },
setTag(tag, value) { setTag(tag, value) {
useAdminSettingsStore().setUsersTags({ users: this.users, value, tags: [tag] }) useAdminSettingsStore().setUsersTags({
users: this.users,
value,
tags: [tag],
})
}, },
setRight(right, value) { setRight(right, value) {
useAdminSettingsStore().setUsersRight({ users: this.users, value, right }) useAdminSettingsStore().setUsersRight({ users: this.users, value, right })
@ -542,9 +545,7 @@ const ModerationTools = {
) )
this.confirmDialogContent = this.confirmDialogContent =
'user_card.admin_menu.confirm_modal.disable_mfa_content' 'user_card.admin_menu.confirm_modal.disable_mfa_content'
this.confirmDialogConfirm = this.$t( this.confirmDialogConfirm = this.$t('settings.confirm')
'settings.confirm'
)
break break
} }
case 'require_password_change': { case 'require_password_change': {
@ -554,12 +555,11 @@ const ModerationTools = {
) )
this.confirmDialogContent = this.confirmDialogContent =
'user_card.admin_menu.confirm_modal.require_password_change_content' 'user_card.admin_menu.confirm_modal.require_password_change_content'
this.confirmDialogConfirm = this.$t( this.confirmDialogConfirm = this.$t('settings.confirm')
'settings.confirm'
)
break break
} }
} }
break
} }
case 'state': { case 'state': {
switch (name) { switch (name) {

View file

@ -6,19 +6,9 @@ import { parseStatus } from 'src/services/entity_normalizer/entity_normalizer.se
const AdminStatusCard = { const AdminStatusCard = {
props: { props: {
/**
* minimal status info
* @type {import('vue').PropType<{
* id: string
* }>}
*/
statusDetails: { statusDetails: {
type: Object, type: Object,
required: true, required: true,
/**
* @param {any} u
* @returns {u is { id: string }}
*/
validator(u) { validator(u) {
return typeof u.id === 'string' return typeof u.id === 'string'
}, },
@ -31,23 +21,14 @@ const AdminStatusCard = {
} }
}, },
computed: { computed: {
/**
* @returns {boolean} is this status sensitive?
*/
isSensitive() { isSensitive() {
return this.statusDetails.sensitive === true return this.statusDetails.sensitive === true
}, },
/**
* @returns {'public' | 'unlisted' | 'private' | 'direct'} status visibility
*/
visibility() { visibility() {
return this.statusDetails.visibility return this.statusDetails.visibility
}, },
}, },
methods: { methods: {
/**
* @param {boolean} v set sensitive
*/
changeSensitivity(v) { changeSensitivity(v) {
this.$store this.$store
.dispatch('adminChangeStatusScope', { .dispatch('adminChangeStatusScope', {
@ -56,9 +37,6 @@ const AdminStatusCard = {
.then((res) => parseStatus(res)) .then((res) => parseStatus(res))
.then((s) => (this.statusCache = s)) .then((s) => (this.statusCache = s))
}, },
/**
* @param {boolean} v set visible
*/
changeVisibility(v) { changeVisibility(v) {
this.$store this.$store
.dispatch('adminChangeStatusScope', { .dispatch('adminChangeStatusScope', {

View file

@ -19,6 +19,7 @@ import {
faChevronDown, faChevronDown,
faChevronRight, faChevronRight,
faExternalLinkAlt, faExternalLinkAlt,
faEye,
faEyeSlash, faEyeSlash,
faHistory, faHistory,
faMinus, faMinus,
@ -51,6 +52,7 @@ library.add(
faBookmark, faBookmark,
faBookmarkRegular, faBookmarkRegular,
faEyeSlash, faEyeSlash,
faEye,
faThumbtack, faThumbtack,
faShareAlt, faShareAlt,
faExternalLinkAlt, faExternalLinkAlt,

View file

@ -3,14 +3,30 @@ import { defineAsyncComponent } from 'vue'
import Popover from 'src/components/popover/popover.vue' import Popover from 'src/components/popover/popover.vue'
import ActionButton from './action_button.vue' import ActionButton from './action_button.vue'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faEnvelope,
faEye,
faEyeSlash,
faFolderTree, faFolderTree,
faGlobe, faGlobe,
faLock,
faLockOpen,
faUser, faUser,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add(faUser, faGlobe, faFolderTree) library.add(
faUser,
faGlobe,
faFolderTree,
faEye,
faEyeSlash,
faLock,
faLockOpen,
faEnvelope,
)
export default { export default {
components: { components: {
@ -61,8 +77,27 @@ export default {
this.domain, this.domain,
) )
}, },
availableScopes() {
return ['private', 'unlisted', 'direct', 'public'].filter((scope) => {
return scope !== this.status.visibility
})
},
}, },
methods: { methods: {
visibilityIcon(visibility) {
switch (visibility) {
case 'private':
return 'lock'
case 'unlisted':
return 'lock-open'
case 'direct':
return 'envelope'
case 'local':
return 'igloo'
default:
return 'globe'
}
},
unmuteUser() { unmuteUser() {
return this.$store.dispatch('unmuteUser', this.user.id) return this.$store.dispatch('unmuteUser', this.user.id)
}, },
@ -79,6 +114,18 @@ export default {
this.$refs.confirmUser.optionallyPrompt() this.$refs.confirmUser.optionallyPrompt()
} }
}, },
setScope(visibility) {
return useAdminSettingsStore().changeStatusScope({
id: this.status.id,
visibility,
})
},
setSensitive(sensitive) {
useAdminSettingsStore().changeStatusScope({
id: this.status.id,
sensitive,
})
},
toggleConversationMute() { toggleConversationMute() {
if (this.conversationIsMuted) { if (this.conversationIsMuted) {
this.unmuteConversation() this.unmuteConversation()

View file

@ -14,6 +14,58 @@
/> />
</template> </template>
<template #content> <template #content>
<div
v-if="button.name === 'changeScope'"
:id="`popup-menu-scope-${randomSeed}`"
class="dropdown-menu"
role="menu"
>
<div
v-for="visibility in availableScopes"
class="menu-item dropdown-item extra-action -icon"
>
<button
class="main-button"
@click="() => setScope(visibility)"
>
<FAIcon
:icon="visibilityIcon(visibility)"
fixed-width
/>
{{ $t('general.scope_in_timeline.' + visibility) }}
</button>
</div>
<div
v-if="status.nsfw"
class="menu-item dropdown-item extra-action -icon"
>
<button
class="main-button"
@click="() => setSensitive(false)"
>
<FAIcon
icon="eye"
fixed-width
/>
{{ $t('status.mark_as_non-sensitive') }}
</button>
</div>
<div
v-else
class="menu-item dropdown-item extra-action -icon"
>
<button
class="main-button"
@click="() => setSensitive(true)"
>
<FAIcon
icon="eye-slash"
fixed-width
/>
{{ $t('status.mark_as_sensitive') }}
</button>
</div>
</div>
<div <div
v-if="button.name === 'mute'" v-if="button.name === 'mute'"
:id="`popup-menu-${randomSeed}`" :id="`popup-menu-${randomSeed}`"

View file

@ -243,6 +243,26 @@ export const BUTTONS = [
return dispatch('deleteStatus', { id: status.id }) return dispatch('deleteStatus', { id: status.id })
}, },
}, },
{
// =========
// CHANGE SCOPE
// =========
name: 'changeScope',
icon: 'eye',
label: 'status.admin_change_scope',
if({ status, loggedIn, currentUser }) {
return (
loggedIn &&
(status.user.id === currentUser.id ||
currentUser.privileges.has('messages_delete'))
)
},
toggleable: false,
dropdown: true,
action({ status, dispatch, emit }) {
/* prevent hiding */
},
},
{ {
// ========= // =========
// SHARE/COPY // SHARE/COPY

View file

@ -84,6 +84,12 @@ export default {
default: false, default: false,
type: Boolean, type: Boolean,
}, },
// Hide action buttons
hideButtons: {
required: false,
default: false,
type: Boolean,
},
// default - open profile, 'zoom' - zoom, function - call function // default - open profile, 'zoom' - zoom, function - call function
avatarAction: { avatarAction: {
required: false, required: false,

View file

@ -104,7 +104,7 @@
/> />
</a> </a>
<AccountActions <AccountActions
v-if="isOtherUser && loggedIn" v-if="isOtherUser && loggedIn && !hideButtons"
:user="user" :user="user"
:relationship="relationship" :relationship="relationship"
/> />
@ -228,7 +228,7 @@
</div> </div>
</div> </div>
<div <div
v-if="loggedIn && isOtherUser" v-if="loggedIn && isOtherUser && !hideButtons"
class="user-interactions" class="user-interactions"
> >
<div class="btn-group"> <div class="btn-group">

View file

@ -24,6 +24,21 @@
.user-info { .user-info {
margin: 1.2em; margin: 1.2em;
} }
&.-admin-view {
.list-item {
padding: 0;
border-bottom: 1px solid var(--border);
.admin-actions {
background: var(--background);
}
.Status{
width: 100%
}
}
}
} }
.user-profile-placeholder { .user-profile-placeholder {

View file

@ -1,20 +1,18 @@
import { get } from 'lodash' import { get } from 'lodash'
import { mapState } from 'pinia' import { mapState } from 'pinia'
import Conversation from 'src/components/conversation/conversation.vue'
import List from 'src/components/list/list.vue' import List from 'src/components/list/list.vue'
import Status from 'src/components/status/status.vue'
import UserCard from 'src/components/user_card/user_card.vue' import UserCard from 'src/components/user_card/user_card.vue'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js' import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
library.add(faCircleNotch) library.add(faCircleNotch)
const defaultTabKey = 'statuses'
const UserProfileAdminView = { const UserProfileAdminView = {
data() { data() {
return { return {
@ -24,7 +22,6 @@ const UserProfileAdminView = {
}, },
created() { created() {
this.userId = this.$route.params.id this.userId = this.$route.params.id
console.log(this.userId)
useInterfaceStore().setForeignProfileBackground(this.user?.background_image) useInterfaceStore().setForeignProfileBackground(this.user?.background_image)
}, },
updated() { updated() {
@ -38,8 +35,8 @@ const UserProfileAdminView = {
return { return {
pageSize: 20, pageSize: 20,
godmode: this.godmode, godmode: this.godmode,
userId: this.userId, id: this.userId,
withReblogs: false withReblogs: false,
} }
}, },
user() { user() {
@ -48,18 +45,16 @@ const UserProfileAdminView = {
}, },
methods: { methods: {
fetchStatuses(page) { fetchStatuses(page) {
return useAdminSettingsStore() return useAdminSettingsStore().fetchStatuses({
.fetchStatuses({ ...this.fetchOptions,
...this.fetchOptions, page,
page, })
})
.then(({ count, users }) => ({ count, items: users }))
}, },
}, },
components: { components: {
UserCard, UserCard,
List, List,
Conversation, Status,
}, },
} }

View file

@ -1,14 +1,15 @@
<template> <template>
<div <div
v-if="user" v-if="user"
class="user-profile panel panel-default" class="user-profile -admin-view panel panel-default"
> >
<div class="panel-body card-wrapper"> <div class="panel-body card-wrapper">
<UserCard <UserCard
:user-id="userId" :user-id="userId"
:compact="compactProfiles" :compact="true"
avatar-action="zoom" avatar-action="zoom"
:hide-bio="true" hide-bio
hide-buttons
/> />
</div> </div>
<List <List
@ -17,8 +18,8 @@
scrollable scrollable
> >
<template #item="{item}"> <template #item="{item}">
<Conversation <Status
:user="item" :statusoid="item"
/> />
</template> </template>
</List> </List>

View file

@ -1689,7 +1689,10 @@
"invisible_quote": "Quoted status unavailable: {link}", "invisible_quote": "Quoted status unavailable: {link}",
"more_actions": "More actions on this status", "more_actions": "More actions on this status",
"loading": "Loading...", "loading": "Loading...",
"load_error": "Unable to load status: {error}" "load_error": "Unable to load status: {error}",
"admin_change_scope": "Change visibility",
"mark_as_sensitive": "Sensitive",
"mark_as_non-sensitive": "Non-sensitive"
}, },
"user_card": { "user_card": {
"approve": "Approve", "approve": "Approve",

View file

@ -186,7 +186,12 @@ const PLEROMA_ADMIN_CONFIRM_USERS_URL =
'/api/v1/pleroma/admin/users/confirm_email' '/api/v1/pleroma/admin/users/confirm_email'
const PLEROMA_ADMIN_RESEND_CONFIRMATION_EMAIL_URL = const PLEROMA_ADMIN_RESEND_CONFIRMATION_EMAIL_URL =
'/api/v1/pleroma/admin/users/resend_confirmation_email' '/api/v1/pleroma/admin/users/resend_confirmation_email'
const PLEROMA_ADMIN_LIST_STATUSES_URL = (id, pageSize, godmode, withReblogs) => 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}` `/api/v1/pleroma/admin/users/${id}/statuses?page_size=${pageSize}&godmode=${godmode}&with_reblogs=${withReblogs}`
const PLEROMA_ADMIN_CHANGE_STATUS_SCOPE_URL = (id) => const PLEROMA_ADMIN_CHANGE_STATUS_SCOPE_URL = (id) =>
`/api/v1/pleroma/admin/statuses/${id}` `/api/v1/pleroma/admin/statuses/${id}`
@ -1700,14 +1705,12 @@ const adminListUsers = ({ opts, credentials }) => {
// the reported list is hardly useful because standards are for dating i guess, // the reported list is hardly useful because standards are for dating i guess,
// so make sure to fetchIfMissing right afterward using this call // so make sure to fetchIfMissing right afterward using this call
const url = PLEROMA_ADMIN_USERS_URL_LIST(opts) const url = PLEROMA_ADMIN_USERS_URL_LIST(opts)
return promisedRequest({ return promisedRequest({
url: url, url,
credentials, credentials,
method: 'GET', method: 'GET',
}).then((data) => ({ })
...data,
users: data.users,
}))
} }
const adminResendConfirmationEmail = ({ const adminResendConfirmationEmail = ({
@ -1752,20 +1755,14 @@ const adminDisableMFA = ({ screen_name: nickname, credentials }) => {
}) })
} }
const adminListStatuses = ({ const adminListStatuses = ({ opts, credentials }) => {
userId, const url = PLEROMA_ADMIN_LIST_STATUSES_URL(opts)
pageSize = 20,
godmode, return promisedRequest({
withReblogs, url,
credentials, credentials,
}) => { method: 'GET',
const url = PLEROMA_ADMIN_LIST_STATUSES_URL( })
userId,
pageSize,
godmode,
withReblogs,
)
return promisedRequest({ url, credentials, method: 'GET' })
} }
const adminChangeStatusScope = ({ const adminChangeStatusScope = ({
@ -2260,7 +2257,6 @@ const listEmojiPacks = ({ page, pageSize }) => {
} }
const listRemoteEmojiPacks = ({ instance, page, pageSize }) => { const listRemoteEmojiPacks = ({ instance, page, pageSize }) => {
console.log(instance)
if (!instance.startsWith('http')) { if (!instance.startsWith('http')) {
instance = 'https://' + instance instance = 'https://' + instance
} }

View file

@ -150,7 +150,10 @@ export const parseUser = (data) => {
output.fields = data.source.fields output.fields = data.source.fields
if (data.source.pleroma) { if (data.source.pleroma) {
output.no_rich_text = data.source.pleroma.no_rich_text output.no_rich_text = data.source.pleroma.no_rich_text
output.show_role = typeof data.source.pleroma.show_role === 'boolean' ? data.source.pleroma.show_role : true output.show_role =
typeof data.source.pleroma.show_role === 'boolean'
? data.source.pleroma.show_role
: true
output.discoverable = data.source.pleroma.discoverable output.discoverable = data.source.pleroma.discoverable
output.show_birthday = data.pleroma.show_birthday output.show_birthday = data.pleroma.show_birthday
output.actor_type = data.source.pleroma.actor_type output.actor_type = data.source.pleroma.actor_type

View file

@ -301,37 +301,46 @@ export const useAdminSettingsStore = defineStore('adminSettings', {
}, },
// Statuses stuff // Statuses stuff
fetchStatuses(opts) { async fetchStatuses(opts) {
return this const { total, activities } =
.backendInteractor await this.backendInteractor.adminListStatuses({
.adminListStatuses(opts) opts,
.then(({ total, activities }) => ({
count: total,
items: activities.map(parseStatus)
}) })
return {
items: activities.map(parseStatus),
count: total,
}
}, },
changeStatusScope({ opts }) { async changeStatusScope(opts) {
return this.backendInteractor.adminChangeStatusScope({ const raw = await this.backendInteractor.adminChangeStatusScope({
opts, opts,
}) })
const status = parseStatus(raw)
await window.vuex.dispatch('addNewStatuses', {
statuses: [status],
userId: false,
})
}, },
// Users stuff // Users stuff
async fetchUsers(opts) { async fetchUsers(opts) {
const adminData = await this.backendInteractor.adminListUsers({ const { users, count } = await this.backendInteractor.adminListUsers({
opts, opts,
}) })
adminData.users = await Promise.all( return {
adminData.users.map( items: await Promise.all(
async (userAdminData) => users.map(
await window.vuex.dispatch('updateUserAdminData', { async (userAdminData) =>
userAdminData, await window.vuex.dispatch('updateUserAdminData', {
}), userAdminData,
}),
),
), ),
) count,
}
return adminData
}, },
async getUserData({ user }) { async getUserData({ user }) {
const api = this.backendInteractor.adminGetUserData const api = this.backendInteractor.adminGetUserData