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: {
type: Boolean,
default: false,
}
},
},
components: {
UserPopover,

View file

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

View file

@ -6,19 +6,9 @@ import { parseStatus } from 'src/services/entity_normalizer/entity_normalizer.se
const AdminStatusCard = {
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'
},
@ -31,23 +21,14 @@ const AdminStatusCard = {
}
},
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', {
@ -56,9 +37,6 @@ const AdminStatusCard = {
.then((res) => parseStatus(res))
.then((s) => (this.statusCache = s))
},
/**
* @param {boolean} v set visible
*/
changeVisibility(v) {
this.$store
.dispatch('adminChangeStatusScope', {

View file

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

View file

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

View file

@ -14,6 +14,58 @@
/>
</template>
<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
v-if="button.name === 'mute'"
:id="`popup-menu-${randomSeed}`"

View file

@ -243,6 +243,26 @@ export const BUTTONS = [
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

View file

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

View file

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

View file

@ -24,6 +24,21 @@
.user-info {
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 {

View file

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

View file

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

View file

@ -1689,7 +1689,10 @@
"invisible_quote": "Quoted status unavailable: {link}",
"more_actions": "More actions on this status",
"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": {
"approve": "Approve",

View file

@ -186,7 +186,12 @@ const PLEROMA_ADMIN_CONFIRM_USERS_URL =
'/api/v1/pleroma/admin/users/confirm_email'
const PLEROMA_ADMIN_RESEND_CONFIRMATION_EMAIL_URL =
'/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}`
const PLEROMA_ADMIN_CHANGE_STATUS_SCOPE_URL = (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,
// so make sure to fetchIfMissing right afterward using this call
const url = PLEROMA_ADMIN_USERS_URL_LIST(opts)
return promisedRequest({
url: url,
url,
credentials,
method: 'GET',
}).then((data) => ({
...data,
users: data.users,
}))
})
}
const adminResendConfirmationEmail = ({
@ -1752,20 +1755,14 @@ const adminDisableMFA = ({ screen_name: nickname, credentials }) => {
})
}
const adminListStatuses = ({
userId,
pageSize = 20,
godmode,
withReblogs,
credentials,
}) => {
const url = PLEROMA_ADMIN_LIST_STATUSES_URL(
userId,
pageSize,
godmode,
withReblogs,
)
return promisedRequest({ url, credentials, method: 'GET' })
const adminListStatuses = ({ opts, credentials }) => {
const url = PLEROMA_ADMIN_LIST_STATUSES_URL(opts)
return promisedRequest({
url,
credentials,
method: 'GET',
})
}
const adminChangeStatusScope = ({
@ -2260,7 +2257,6 @@ const listEmojiPacks = ({ page, pageSize }) => {
}
const listRemoteEmojiPacks = ({ instance, page, pageSize }) => {
console.log(instance)
if (!instance.startsWith('http')) {
instance = 'https://' + instance
}

View file

@ -150,7 +150,10 @@ export const parseUser = (data) => {
output.fields = data.source.fields
if (data.source.pleroma) {
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.show_birthday = data.pleroma.show_birthday
output.actor_type = data.source.pleroma.actor_type

View file

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