Merge branch 'admin-users' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2026-06-11 12:31:43 +03:00
commit 9f68d151c1
50 changed files with 283 additions and 2454 deletions

View file

@ -411,6 +411,14 @@ nav {
button:not(.button-default) {
color: var(--text);
font-size: 100%;
text-align: initial;
padding: 0;
background: none;
border: none;
outline: none;
display: inline;
font-family: inherit;
line-height: unset;
}
&.disabled {
@ -436,22 +444,6 @@ nav {
--__line-height: 1.5em;
--__horizontal-gap: 0.75em;
--__vertical-gap: 0.5em;
&.-non-interactive {
cursor: auto;
}
> a,
> button:not(.button-default) {
text-align: initial;
padding: 0;
background: none;
border: none;
outline: none;
display: inline;
font-family: inherit;
line-height: unset;
}
}
.button-unstyled {

View file

@ -1,5 +1,5 @@
<template>
<basic-user-card :user="user">
<BasicUserCard :user="user">
<div class="block-card-content-container">
<span
v-if="blocked && blockExpiryAvailable"
@ -30,7 +30,7 @@
:is-mute="false"
/>
</teleport>
</basic-user-card>
</BasicUserCard>
</template>
<script src="./block_card.js"></script>

View file

@ -18,7 +18,9 @@
<slot />
</div>
</div>
<div class="below">
<slot name="below" />
</div>
<template #footer>
<slot name="footerLeft" />
<button
@ -61,6 +63,10 @@
}
}
.below:not(:empty) {
margin-top: 1em;
}
.text {
max-width: 50ch;
}

View file

@ -1,40 +0,0 @@
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,
}),
components: {
ConfirmModal,
},
methods: {
show() {
this.showing = true
this.$emit('show')
},
hide() {
this.showing = false
this.$emit('hide')
},
doGeneric() {
this.$emit('action')
this.hide()
},
},
}

View file

@ -1,23 +0,0 @@
<template>
<ConfirmModal
v-if="showing"
:title="title"
:cancel-text="cancelText"
:confirm-text="confirmText"
@accepted="doGeneric"
@cancelled="hide"
>
<template #default>
<span v-text="message" />
</template>
</ConfirmModal>
</template>
<script src="./generic_confirm.js" />
<style lang="scss">
.expiry-amount {
width: 4em;
text-align: right;
}
</style>

View file

@ -1,41 +0,0 @@
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()
},
},
}

View file

@ -1,27 +0,0 @@
<template>
<ConfirmModal
v-if="showing"
:title="title"
:cancel-text="cancelText"
:confirm-text="confirmText"
@accepted="doWithText"
@cancelled="hide"
>
<template #default>
<span v-text="message" />
<input
v-model="text"
class="input string-input filter-input"
>
</template>
</ConfirmModal>
</template>
<script src="./text_confirm.js" />
<style lang="scss">
.expiry-amount {
width: 4em;
text-align: right;
}
</style>

View file

@ -17,7 +17,7 @@ const Interactions = {
allowFollowingMove:
this.$store.state.users.currentUser.allow_following_move,
filterMode: tabModeDict.mentions,
canSeeReports: this.$store.state.users.currentUser.has.has(
canSeeReports: this.$store.state.users.currentUser.privileges.has(
'reports_manage_reports',
),
}

View file

@ -20,6 +20,10 @@ const List = {
type: Function,
default: () => '',
},
preSelect: {
type: Array,
default: [],
},
nonInteractive: {
type: Boolean,
default: false,
@ -44,7 +48,7 @@ const List = {
data() {
return {
items: [],
selected: new Set([]),
selected: new Set(this.preSelect),
loading: false,
bottomedOut: true,
error: null,

View file

@ -1,6 +1,7 @@
<template>
<div
class="List"
role="list"
:class="{ '-scrollable': scrollable }"
>
<div
@ -72,9 +73,9 @@
/>
<a
v-else-if="!bottomedOut"
@click="fetchEntries"
role="button"
tabindex="0"
@click="fetchEntries"
>
{{ $t('general.more') }}
</a>

View file

@ -50,7 +50,7 @@
</div>
<tab-switcher
class="list-member-management"
:scrollable-tabs="true"
:scrollable-tabs
>
<div
v-if="id || addedUserIds.size > 0"

View file

@ -80,6 +80,7 @@ const ENTRIES = [
{
check: 'action:statuses',
label: 'user_card.admin_menu.show_statuses',
conditions: ['count:1'],
},
{
separator: true,
@ -215,22 +216,17 @@ const ModerationTools = {
return () => this.setTag(`${group}:${name}`, noTag)
case 'action': {
switch (name) {
case 'delete': {
case 'delete':
return () => this.deleteUsers()
}
case 'resend_confirmation': {
case 'resend_confirmation':
return () => this.resendConfirmationEmail()
}
case 'disable_mfa': {
case 'disable_mfa':
return () => this.disableMFA()
}
case 'statuses': {
case 'statuses':
return () =>
this.$router.push(`/users/\$${this.users[0].id}/admin_view`)
}
case 'require_password_change': {
case 'require_password_change':
return () => this.requirePasswordChange()
}
default:
throw new Error(`Unknown action group: ${name}`)
}
@ -277,24 +273,23 @@ const ModerationTools = {
}
switch (group) {
case 'action': {
return true
}
case 'rights': {
case 'action':
if (name === 'statuses') return this.privileged('users_read')
else return true
case 'rights':
return this.canGrantRole(name, value)
}
case 'state': {
case 'state':
return this.canChangeState(name, value)
}
case 'mrf_tag': {
case 'mrf_tag':
return this.canUseTagPolicy
}
default: {
throw new Error(`Unknown moderation group: ${group}`)
}
}
})
.reduce((acc, entry, index) => {
// Removing any double separators as well
// as separators at very end and bery beginning
if (entry === 'separator') {
if (
acc.length === 0 ||
@ -363,12 +358,16 @@ const ModerationTools = {
const result = new Set()
// Each tag can have three states for given group of users
TAGS.forEach((tag) => {
if (present.has(tag) && missing.has(tag)) {
// Some users have tag, some don't: "~tag"
result.add(`~${tag}`)
} else if (missing.has(tag)) {
// No users have tag: "!tag"
result.add(`!${tag}`)
} else {
// All users have tag: "tag"
result.add(tag)
}
})
@ -394,6 +393,7 @@ const ModerationTools = {
...this.stateSet,
...this.tagsSet,
...this.propertySet,
`count:${this.users.length}`,
])
},
canDeleteAccount() {
@ -405,27 +405,33 @@ const ModerationTools = {
this.privileged('users_manage_tags')
)
},
isAdmin() {
this.$store.state.users.currentUser.role === 'admin'
}
},
methods: {
canGrantRole(name, value) {
const setEntry = `${value ? '!' : ''}rights:${name}`
return (
this.$store.state.users.currentUser.role === 'admin' &&
this.isAdmin &&
this.totalSet.has(setEntry)
)
},
canChangeState(name, value) {
let privilege
switch (name) {
// TODO detailed privileges
default: {
privilege = 'users_manage_activation_state'
}
}
const setEntry = `${value ? '!' : ''}state:${name}`
const privilege = (() => {
switch (name) {
case 'activated':
return 'users_manage_activation_state'
case 'approved':
return 'users_manage_invites'
case 'confirmed':
return 'users_manage_credentials'
default:
return null
}
})()
return this.privileged(privilege) && this.totalSet.has(setEntry)
},
@ -449,6 +455,7 @@ const ModerationTools = {
this.confirmDialogName = null
},
privileged(privilege) {
if (this.isAdmin) return true
return this.$store.state.users.currentUser.privileges.has(privilege)
},
setTag(tag, value) {

View file

@ -46,13 +46,19 @@
:disabled="disabled"
>
{{ $t('user_card.admin_menu.moderation') }}
<FAIcon v-if="ready" icon="chevron-down" />
<span v-else class="loading-spinner">
<FAIcon
v-if="ready"
icon="chevron-down"
/>
<span
v-else
class="loading-spinner"
>
<FAIcon
class="fa-old-padding"
spin
icon="circle-notch"
/>
/>
</span>
</button>
</template>
@ -91,7 +97,10 @@
{{ $t(confirmDialogContent2) }}
</p>
<ul v-if="users.length > 1">
<li v-for="user in users">
<li
v-for="user in users"
:key="user.screen_name"
>
{{ user.screen_name }}
</li>
</ul>

View file

@ -43,7 +43,7 @@
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("general.not_applicable") }}
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
@ -70,7 +70,7 @@
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("general.not_applicable") }}
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
@ -97,7 +97,7 @@
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("general.not_applicable") }}
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
@ -124,7 +124,7 @@
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("general.not_applicable") }}
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
@ -151,7 +151,7 @@
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("general.not_applicable") }}
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
@ -178,7 +178,7 @@
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("general.not_applicable") }}
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}

View file

@ -1,9 +0,0 @@
.AdminCard {
width: 100%;
.right-side {
align-items: baseline;
justify-content: end;
display: flex;
}
}

View file

@ -1,95 +0,0 @@
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Select from 'src/components/select/select.vue'
import Status from 'src/components/status/status.vue'
import { parseStatus } from 'src/services/entity_normalizer/entity_normalizer.service.js'
const AdminStatusCard = {
props: {
statusDetails: {
type: Object,
required: true,
validator(u) {
return typeof u.id === 'string'
},
},
},
data() {
return {
jsonExpanded: false,
statusCache: undefined,
}
},
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 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

View file

@ -1,106 +0,0 @@
<template>
<div class="setting-item">
<h2> {{ $t('admin_dash.users.title_info') }}: </h2>
<ul
class="setting-list"
>
<li>
<span> {{ $t('admin_dash.users.status_id') }}: {{ statusDetails.id }} </span>
</li>
<li>
<span> {{ $t('admin_dash.users.created_at') }}: {{ new Date(statusDetails.created_at).toLocaleString() }} </span>
</li>
<li>
<span v-if="statusDetails.edited_at !== null"> {{ $t('admin_dash.users.edited_at') }}: {{ new Date(statusDetails.edited_at).toLocaleString() }} </span>
</li>
</ul>
<h2> {{ $t('admin_dash.users.title_content') }}: </h2>
<ul
class="setting-list"
>
<li>
<Status
v-if="typeof(statusCache) !== 'undefined'"
class="Notification"
:compact="true"
:statusoid="statusCache"
@interacted="false"
/>
</li>
<p> action dropdown thingy </p>
<!--
<li>
<button
class="button button-default btn"
type="button"
@click="deleteStatus(status.id)"
>
{{ $t('admin_dash.users.delete_status') }}
</button>
</li>
<li>
<Checkbox
:model-value="isSensitive"
@update:model-value="v => changeSensitivity(v)"
>
{{ $t('admin_dash.users.content_nsfw') }}
</Checkbox>
</li>
<li>
<Select
:model-value="visibility"
@update:model-value="v => changeVisibility(v)"
>
<option
value="public"
>
{{ $t('admin_dash.users.scope_public') }}
</option>
<option
value="unlisted"
>
{{ $t('admin_dash.users.scope_unlisted') }}
</option>
<option
value="private"
>
{{ $t('admin_dash.users.scope_private') }}
</option>
<option
value="direct"
>
{{ $t('admin_dash.users.scope_direct') }}
</option>
</Select>
</li>
<li>
<a :href="statusDetails.url"> {{ $t('admin_dash.users.link_source') }} </a>
</li>
-->
</ul>
<!--
<div v-if="!jsonExpanded">
<button
class="button button-default btn"
type="button"
@click="jsonExpanded = !jsonExpanded"
>
{{ $t('admin_dash.users.expand_raw_info') }}
</button>
</div>
<div v-else>
<button
class="button button-default btn"
type="button"
@click="jsonExpanded = !jsonExpanded"
>
{{ $t('admin_dash.users.collapse_raw_info') }}
</button>
<h2> {{ $t('admin_dash.users.title_details') }} </h2>
<pre> {{ JSON.stringify(statusDetails, null, 2) }} </pre>
</div>
-->
</div>
</template>
<script src="./admin_status_card.js"></script>

View file

@ -3,7 +3,7 @@ import { defineAsyncComponent } from 'vue'
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue'
const AdminCard = {
const AdminUserCard = {
props: {
userId: {
type: String,
@ -17,9 +17,6 @@ const AdminCard = {
user() {
return this.$store.getters.findUser(this.userId)
},
relationship() {
return this.$store.getters.relationship(this.userId)
},
isAdmin() {
return this.user.rights.admin
},
@ -32,4 +29,4 @@ const AdminCard = {
},
}
export default AdminCard
export default AdminUserCard

View file

@ -0,0 +1,14 @@
.AdminUserCard {
.right-side {
align-items: baseline;
justify-content: end;
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.5em;
.alert {
margin: 0;
}
}
}

View file

@ -8,7 +8,7 @@
</template>
<template v-else>
<BasicUserCard
class="AdminCard"
class="AdminUserCard"
:user="user"
show-line-labels
>
@ -18,8 +18,8 @@
</strong>
{{ ' ' }}
<span
class="faint"
v-if="user.adminData.email == null"
class="faint"
>
{{ $t('general.not_available') }}
</span>
@ -35,7 +35,7 @@
{{ $t('user_card.admin_data.registration_reason') }}
</summary>
<span>
{{ user.adminData.registration_reason }}
{{ user.adminData.registration_reason }}
</span>
</details>
<div class="right-side">
@ -102,6 +102,6 @@
</template>
</template>
<script src="./admin_card.js"></script>
<script src="./admin_user_card.js"></script>
<style lang="scss" src="./admin_card.scss"></style>
<style lang="scss" src="./admin_user_card.scss"></style>

View file

@ -105,7 +105,7 @@ const FrontendsTab = {
const ref = suggestRef || this.getSuggestedRef(frontend)
const { name } = frontend
useAdminSettingsStore.updateAdminDraft({
useAdminSettingsStore().updateAdminDraft({
path: [':pleroma', ':frontends', ':primary'],
value: { name, ref },
})

View file

@ -112,19 +112,19 @@ const LinksTab = {
},
methods: {
checkRel(e) {
useAdminSettingsStore.updateAdminDraft({
useAdminSettingsStore().updateAdminDraft({
path: [':pleroma', 'Pleroma.Formatter', ':rel'],
value: e ? '' : false,
})
},
checkClass(e) {
useAdminSettingsStore.updateAdminDraft({
useAdminSettingsStore().updateAdminDraft({
path: [':pleroma', 'Pleroma.Formatter', ':class'],
value: e ? '' : false,
})
},
checkTruncate(e) {
useAdminSettingsStore.updateAdminDraft({
useAdminSettingsStore().updateAdminDraft({
path: [':pleroma', 'Pleroma.Formatter', ':truncate'],
value: e ? 20 : false,
})

View file

@ -2,13 +2,10 @@ import { isEmpty } from 'lodash'
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import GenericConfirm from 'src/components/confirm_modal/generic_confirm.vue'
import List from 'src/components/list/list.vue'
import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue'
import ProgressButton from 'src/components/progress_button/progress_button.vue'
import Select from 'src/components/select/select.vue'
import AdminCard from 'src/components/settings_modal/admin_tabs/admin_card.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import AdminUserCard from 'src/components/settings_modal/admin_tabs/admin_user_card.vue'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
@ -18,17 +15,8 @@ const UsersTab = {
Select,
BasicUserCard,
List,
ProgressButton,
AdminCard,
TabSwitcher,
AdminUserCard,
ModerationTools,
GenericConfirm,
},
provide() {
return {
defaultDraftMode: true,
defaultSource: 'admin',
}
},
data() {
return {
@ -120,12 +108,11 @@ const UsersTab = {
...this.fetchOptions,
page,
})
.then(({ count, users }) => ({ count, items: users }))
},
},
watch: {
fetchOptions() {
this.$refs.usersList.reset()
this.$refs.usersList?.reset()
},
},
}

View file

@ -6,7 +6,9 @@
.filters-section {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-columns: repeat(auto-fit, minmax(15em, 1fr));
grid-auto-columns: 1fr;
grid-auto-flow: row;
gap: 0.5em 1em;
> div {

View file

@ -123,15 +123,15 @@
<List
ref="usersList"
:fetch-function="fetchUsers"
@select="onSelect"
selectable
scrollable
@select="onSelect"
>
<template #header="{selected}">
<ModerationTools :users="selected" />
</template>
<template #item="{item}">
<AdminCard :user-id="item.id" />
<AdminUserCard :user-id="item.id" />
</template>
<template #load>
<span> loading </span>

View file

@ -133,7 +133,7 @@ export default {
},
set(value) {
if (this.realSource === 'admin' || this.path == null) {
useAdminSettingsStore.updateAdminDraft({
useAdminSettingsStore().updateAdminDraft({
path: this.canonPath,
value,
})
@ -254,7 +254,7 @@ export default {
this.$store.dispatch('setProfileOption', { name: k, value: v })
case 'admin':
return (k, v) =>
useAdminSettingsStore.pushAdminSetting({ path: k, value: v })
useAdminSettingsStore().pushAdminSetting({ path: k, value: v })
default:
return (readPath, value) => {
const writePath = `${readPath}`

View file

@ -58,6 +58,7 @@
&.-full-height {
height: 100%;
> * {
height: 100%;
}

View file

@ -93,8 +93,11 @@ const SettingsModal = {
closeModal() {
useInterfaceStore().closeSettingsModal()
},
peekModal() {
useInterfaceStore().togglePeekSettingsModal()
toggleMinimizeModal(state) {
useInterfaceStore().toggleMinimizeSettingsModal()
},
minimizeModal() {
useInterfaceStore().setSettingsModalState('minimized')
},
importValidator(data) {
if (!Array.isArray(data._pleroma_settings_version)) {
@ -233,10 +236,10 @@ const SettingsModal = {
return clone
},
resetAdminDraft() {
useAdminSettingsStore.resetAdminDraft()
useAdminSettingsStore().resetAdminDraft()
},
pushAdminDraft() {
useAdminSettingsStore.pushAdminDraft()
useAdminSettingsStore().pushAdminDraft()
},
...mapActions(useInterfaceStore, [
'temporaryChangesRevert',
@ -251,7 +254,7 @@ const SettingsModal = {
modalMode: (store) => store.settingsModalMode,
modalOpenedOnceUser: (store) => store.settingsModalLoadedUser,
modalOpenedOnceAdmin: (store) => store.settingsModalLoadedAdmin,
modalPeeked: (store) => store.settingsModalState === 'minimized',
modalMinimized: (store) => store.settingsModalState === 'minimized',
}),
expertLevel: {
get() {
@ -271,6 +274,11 @@ const SettingsModal = {
)
},
},
watch: {
$route(r) {
this.minimizeModal()
}
},
}
export default SettingsModal

View file

@ -300,7 +300,7 @@
}
}
&.peek {
&.minimize {
.settings-modal-panel {
/* Explanation:
* Modal is positioned vertically centered.

View file

@ -2,8 +2,8 @@
<Modal
:is-open="modalActivated"
class="settings-modal"
:class="{ peek: modalPeeked }"
:no-background="modalPeeked"
:class="{ minimize: modalMinimized }"
:no-background="modalMinimized"
>
<div class="settings-modal-panel panel">
<div class="panel-heading">
@ -22,8 +22,8 @@
</transition>
<button
class="btn button-default"
:title="$t('general.peek')"
@click="peekModal"
:title="$t('general.minimize')"
@click="toggleMinimizeModal"
>
<FAIcon
:icon="['far', 'window-minimize']"

View file

@ -4,7 +4,7 @@
ref="tabSwitcher"
class="settings-admin-content settings_tab-switcher"
:side-tab-bar="true"
:scrollable-tabs="true"
:scrollable-tabs
:render-only-focused="true"
:body-scroll-lock="bodyLock"
>
@ -50,7 +50,6 @@
</div>
<div
v-if="adminDbLoaded"
:label="$t('admin_dash.tabs.users')"
icon="user"
data-tab-name="users"

View file

@ -2,7 +2,7 @@
<vertical-tab-switcher
ref="tabSwitcher"
class="settings_tab-switcher"
:scrollable-tabs="true"
:scrollable-tabs
:body-scroll-lock="bodyLock"
:hide-header="navHideHeader"
>

View file

@ -16,15 +16,6 @@ const MutesAndBlocks = {
data() {
return {
activeTab: 'profile',
mutesLoading: false,
mutesError: null,
mutesBottomedOut: false,
blocksLoading: false,
blocksError: null,
blocksBottomedOut: false,
domainsLoading: false,
domainsError: null,
domainsBottomedOut: false,
}
},
created() {

View file

@ -1,8 +1,8 @@
<template>
<tab-switcher
class="mutes-and-blocks-tab"
:scrollable-tabs="true"
>
:scrollable-tabs
>
<div
class="blocks"
:label="$t('settings.user_blocks')"
@ -60,7 +60,10 @@
</List>
</div>
<div class="mutes" :label="$t('settings.user_mutes2')">
<div
class="mutes"
:label="$t('settings.user_mutes2')"
>
<div class="usersearch-wrapper">
<Autosuggest
:filter="filterUnMutedUsers"

View file

@ -22,6 +22,7 @@
>
<div
v-for="visibility in availableScopes"
:key="visibility"
class="menu-item dropdown-item extra-action -icon"
>
<button
@ -32,7 +33,7 @@
:icon="visibilityIcon(visibility)"
fixed-width
/>
{{ $t('general.scope_in_timeline.' + visibility) }}
{{ $t('general.scope_in_timeline.' + visibility) }}
</button>
</div>
<div
@ -47,7 +48,7 @@
icon="eye"
fixed-width
/>
{{ $t('status.mark_as_non-sensitive') }}
{{ $t('status.mark_as_non-sensitive') }}
</button>
</div>
<div
@ -62,7 +63,7 @@
icon="eye-slash"
fixed-width
/>
{{ $t('status.mark_as_sensitive') }}
{{ $t('status.mark_as_sensitive') }}
</button>
</div>
</div>

View file

@ -299,10 +299,12 @@ export const BUTTONS = [
label: 'user_card.report',
if: ({ loggedIn }) => loggedIn,
action({ status }) {
return useReportsStore().openUserReportingModal({
useReportsStore().openUserReportingModal({
userId: status.user.id,
statusIds: [status.id],
})
return Promise.resolve()
},
},
].map((button) => {

View file

@ -348,8 +348,8 @@
</div>
</div>
<div
class="admin-data"
v-if="user.adminData && !hideBio"
class="admin-data"
>
<details>
<summary>
@ -454,7 +454,7 @@
:key="tag"
>
<code>
{{ tag }}
{{ tag }}
</code>
{{ ' ' }}
</li>

View file

@ -26,7 +26,7 @@
}
&.-admin-view {
.godmode {
.filter {
padding: 1em;
}

View file

@ -17,12 +17,11 @@ library.add(faCircleNotch)
const UserProfileAdminView = {
data() {
return {
userId: null,
godmode: false,
showReblogs: false,
}
},
created() {
this.userId = this.$route.params.id
this.$store.dispatch('fetchUserIfMissing', this.userId)
useInterfaceStore().setForeignProfileBackground(this.user?.background_image)
},
@ -38,12 +37,15 @@ const UserProfileAdminView = {
pageSize: 20,
godmode: this.godmode,
id: this.userId,
withReblogs: false,
withReblogs: this.showReblogs,
}
},
user() {
return this.$store.getters.findUser(this.userId)
},
userId() {
return this.$route.params.id
}
},
methods: {
fetchStatuses(page) {
@ -60,7 +62,7 @@ const UserProfileAdminView = {
Checkbox,
},
watch: {
godmode() {
fetchOptions() {
this.$refs.list.reset()
},
},

View file

@ -10,18 +10,30 @@
hide-bio
hide-buttons
/>
<Checkbox class="godmode" v-model="godmode">
{{ $t('admin_dash.users.godmode') }}
<Checkbox
v-model="godmode"
class="filter"
>
{{ $t('admin_dash.users.filters.show_direct') }}
</Checkbox>
<Checkbox
v-model="showReblogs"
class="filter"
>
{{ $t('admin_dash.users.filters.show_reblogs') }}
</Checkbox>
</div>
<List
ref="list"
:fetch-function="fetchStatuses"
@select="onSelect"
scrollable
>
<template #item="{item}">
<Status :statusoid="item" />
<Status
:statusoid="item"
:in-conversation="false"
:focused="false"
/>
</template>
</List>
</div>

View file

@ -1,3 +1,5 @@
import { mapState } from 'pinia'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import List from 'src/components/list/list.vue'
import Modal from 'src/components/modal/modal.vue'
@ -16,19 +18,17 @@ const UserReportingModal = {
return {
comment: '',
forward: false,
statusIdsToReport: [],
statusIdsToReport: new Set(),
processing: false,
error: false,
}
},
computed: {
reportModal() {
return useReportsStore().reportModal
},
isLoggedIn() {
return !!this.$store.state.users.currentUser
},
isOpen() {
console.log(this.reportModal)
return this.isLoggedIn && this.reportModal.activated
},
userId() {
@ -43,31 +43,26 @@ const UserReportingModal = {
this.user.screen_name.substr(this.user.screen_name.indexOf('@') + 1)
)
},
statuses() {
return this.reportModal.statuses
},
preTickedIds() {
return this.reportModal.preTickedIds
},
...mapState(useReportsStore, ['reportModal']),
},
watch: {
userId: 'resetState',
preTickedIds(newValue) {
this.statusIdsToReport = newValue
},
},
methods: {
resetState() {
// Reset state
this.comment = ''
this.forward = false
this.statusIdsToReport = this.preTickedIds
this.statusIdsToReport = new Set(this.reportModal.preTickedIds)
this.processing = false
this.error = false
},
closeModal() {
useReportsStore().closeUserReportingModal()
},
onListSelect(selected) {
this.statusIdsToReport = selected
},
reportUser() {
this.processing = true
this.error = false
@ -75,7 +70,7 @@ const UserReportingModal = {
userId: this.userId,
comment: this.comment,
forward: this.forward,
statusIds: this.statusIdsToReport,
statusIds: [...this.statusIdsToReport],
}
this.$store.state.api.backendInteractor
.reportUser({ ...params })
@ -92,23 +87,6 @@ const UserReportingModal = {
clearError() {
this.error = false
},
isChecked(statusId) {
return this.statusIdsToReport.indexOf(statusId) !== -1
},
toggleStatus(checked, statusId) {
if (checked === this.isChecked(statusId)) {
return
}
if (checked) {
this.statusIdsToReport.push(statusId)
} else {
this.statusIdsToReport.splice(
this.statusIdsToReport.indexOf(statusId),
1,
)
}
},
resize(e) {
const target = e.target || e
if (!(target instanceof window.Element)) {

View file

@ -51,19 +51,18 @@
</div>
</div>
<div class="user-reporting-panel-right">
<List :items="statuses">
<List
:external-items="reportModal.statuses"
:pre-select="reportModal.preTickedIds"
selectable
@select="onListSelect"
>
<template #item="{item}">
<div class="status-fadein user-reporting-panel-sitem">
<Status
:in-conversation="false"
:focused="false"
:statusoid="item"
/>
<Checkbox
:model-value="isChecked(item.id)"
@update:model-value="checked => toggleStatus(checked, item.id)"
/>
</div>
<Status
:in-conversation="false"
:focused="false"
:statusoid="item"
/>
</template>
</List>
</div>
@ -136,20 +135,6 @@
overflow-y: auto;
}
&-sitem {
display: flex;
justify-content: space-between;
/* TODO cleanup this */
> .Status {
flex: 1;
}
> .checkbox {
margin: 0.75em;
}
}
@media all and (width >= 801px) {
.panel-body {
flex-direction: row;

View file

@ -11,62 +11,64 @@
<p>
{{ $t(isMute ? 'user_card.expire_mute_message' : 'user_card.expire_block_message', [user.screen_name]) }}
</p>
<div>
{{ $t('user_card.expire_in') }}
<span class="expirationTime">
<input
id="userFilterExpires"
v-model="expiration"
class="input input-expire-in"
:class="{ disabled: forever }"
:disabled="forever"
min="1"
type="number"
>
<Select
id="userFilterExpiresUnit"
v-model="expirationUnit"
class="input unit-input unstyled"
:disabled="forever"
>
<option
key="s"
value="s"
<template #below>
<div>
{{ $t('user_card.expire_in') }}
<span class="expirationTime">
<input
id="userFilterExpires"
v-model="expiration"
class="input input-expire-in"
:class="{ disabled: forever }"
:disabled="forever"
min="1"
type="number"
>
{{ $t('time.unit.seconds_suffix') }}
</option>
<option
key="m"
value="m"
<Select
id="userFilterExpiresUnit"
v-model="expirationUnit"
class="input unit-input unstyled"
:disabled="forever"
>
{{ $t('time.unit.minutes_suffix') }}
</option>
<option
key="h"
value="h"
>
{{ $t('time.unit.hours_suffix') }}
</option>
<option
key="d"
value="d"
>
{{ $t('time.unit.days_suffix') }}
</option>
</Select>
</span>
<option
key="s"
value="s"
>
{{ $t('time.unit.seconds_suffix') }}
</option>
<option
key="m"
value="m"
>
{{ $t('time.unit.minutes_suffix') }}
</option>
<option
key="h"
value="h"
>
{{ $t('time.unit.hours_suffix') }}
</option>
<option
key="d"
value="d"
>
{{ $t('time.unit.days_suffix') }}
</option>
</Select>
</span>
{{ $t('user_card.mute_or') }}
{{ $t('user_card.mute_or') }}
<Checkbox
id="forever"
v-model="forever"
name="forever"
class="input-forever"
>
{{ $t('user_card.mute_block_never') }}
</Checkbox>
</div>
<Checkbox
id="forever"
v-model="forever"
name="forever"
class="input-forever"
>
{{ $t('user_card.mute_block_never') }}
</Checkbox>
</div>
</template>
<template #footerLeft>
<div class="footer-left-checkbox">

View file

@ -385,9 +385,6 @@
"selectable_list": {
"select_all": "Select all"
},
"page_list": {
"load_more": "Load more"
},
"settings": {
"invalid_settings_imported": "Error importing settings",
"add_language": "Add fallback language",
@ -1298,7 +1295,6 @@
"users": {
"title": "Users",
"local_id": "Local ID",
"godmode": "Show direct messages",
"labels": {
"query": "Search",
"name": "Name",
@ -1329,10 +1325,8 @@
"only_unconfirmed": "Exclude Confirmed"
},
"filters": {
"show_direct": "Show Direct Posts",
"show_reblogs": "Show Reblogs",
"ascending": "Oldest First",
"descending": "Newest First"
"show_direct": "Show Direct Messages",
"show_reblogs": "Show Reblogs"
},
"indicator": {
"admin": "Admin",

View file

@ -591,7 +591,7 @@ const statuses = {
pagination,
},
) {
commit('addNewStatuses', {
return commit('addNewStatuses', {
statuses,
showImmediately,
timeline,
@ -683,7 +683,7 @@ const statuses = {
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
unpinStatus({ rootState, dispatch }, statusId) {
rootState.api.backendInteractor
return rootState.api.backendInteractor
.unpinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
@ -822,7 +822,7 @@ const statuses = {
'addNewUsers',
data.statuses.map((s) => s.user).filter((u) => u),
)
store.commit('addNewStatuses', { statuses: data.statuses })
data.statuses = store.commit('addNewStatuses', { statuses: data.statuses })
return data
})
},

View file

@ -307,8 +307,12 @@ export const useAdminSettingsStore = defineStore('adminSettings', {
opts,
})
const statuses = activities.map(parseStatus)
await window.vuex.dispatch('addNewStatuses', { statuses })
return {
items: activities.map(parseStatus),
items: statuses,
count: total,
}
},
@ -318,10 +322,7 @@ export const useAdminSettingsStore = defineStore('adminSettings', {
})
const status = parseStatus(raw)
await window.vuex.dispatch('addNewStatuses', {
statuses: [status],
userId: false,
})
await window.vuex.dispatch('addNewStatuses', { statuses: [status] })
},
// Users stuff

View file

@ -133,6 +133,23 @@ export const useInterfaceStore = defineStore('interface', {
}
}
},
setSettingsModalState(newState) {
const oldState = this.settingsModalState
const legal = (() => {
switch (oldState) {
case 'minimized':
return true
case 'visible':
return true
case 'hidden':
return newState === 'visible'
}
})()
if (legal) {
this.settingsModalState = newState
}
},
togglePeekSettingsModal() {
switch (this.settingsModalState) {
case 'minimized':
@ -141,8 +158,10 @@ export const useInterfaceStore = defineStore('interface', {
case 'visible':
this.settingsModalState = 'minimized'
return
case 'hidden':
return
default:
throw new Error('Illegal minimization state of settings modal')
throw new Error(`Illegal minimization state of settings modal: ${this.settingsModalState}`)
}
},
clearSettingsModalTargetTab() {

View file

@ -15,10 +15,12 @@ export const useReportsStore = defineStore('reports', {
}),
actions: {
openUserReportingModal({ userId, statusIds = [] }) {
console.log('ASS')
const preTickedStatuses = statusIds.map(
(id) => window.vuex.state.statuses.allStatusesObject[id],
)
const preTickedIds = statusIds
console.log(preTickedStatuses)
const statuses = preTickedStatuses.concat(
filter(
window.vuex.state.statuses.allStatuses,

File diff suppressed because it is too large Load diff

View file

@ -5,76 +5,6 @@ import {
parseUser,
} from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
import mastoapidata from '../../../../fixtures/mastoapi.json'
import qvitterapidata from '../../../../fixtures/statuses.json'
const makeMockStatusQvitter = (overrides = {}) => {
return Object.assign(
{
activity_type: 'post',
attachments: [],
attentions: [],
created_at: 'Tue Jan 15 13:57:56 +0000 2019',
external_url: 'https://ap.example/whatever',
fave_num: 1,
favorited: false,
id: '10335970',
in_reply_to_ostatus_uri: null,
in_reply_to_profileurl: null,
in_reply_to_screen_name: null,
in_reply_to_status_id: null,
in_reply_to_user_id: null,
is_local: false,
is_post_verb: true,
possibly_sensitive: false,
repeat_num: 0,
repeated: false,
statusnet_conversation_id: '16300488',
summary: null,
tags: [],
text: 'haha benis',
uri: 'https://ap.example/whatever',
user: makeMockUserQvitter(),
visibility: 'public',
},
overrides,
)
}
const makeMockUserQvitter = (overrides = {}) => {
return Object.assign(
{
background_image: null,
cover_photo: '',
created_at: 'Mon Jan 14 13:56:51 +0000 2019',
default_scope: 'public',
description: 'ebin',
description_html: '<p>ebin</p>',
favourites_count: 0,
fields: [],
followers_count: 1,
following: true,
follows_you: true,
friends_count: 1,
id: '60717',
is_local: false,
locked: false,
name: 'Spurdo :ebin:',
name_html: 'Spurdo <img src="whatever">',
no_rich_text: false,
pleroma: { confirmation_pending: false, tags: [] },
profile_image_url: 'https://ap.example/whatever',
profile_image_url_https: 'https://ap.example/whatever',
profile_image_url_original: 'https://ap.example/whatever',
profile_image_url_profile_size: 'https://ap.example/whatever',
rights: { delete_others_notice: false },
screen_name: 'spurdo@ap.example',
statuses_count: 46,
statusnet_blocking: false,
statusnet_profile_url: '',
},
overrides,
)
}
const makeMockUserMasto = (overrides = {}) => {
return Object.assign(
@ -151,19 +81,6 @@ const makeMockStatusMasto = (overrides = {}) => {
)
}
const makeMockNotificationQvitter = (overrides = {}) => {
return Object.assign(
{
notice: makeMockStatusQvitter(),
ntype: 'follow',
from_profile: makeMockUserQvitter(),
is_seen: 0,
id: 123,
},
overrides,
)
}
const makeMockEmojiMasto = (overrides = [{}]) => {
return [
Object.assign(
@ -189,78 +106,6 @@ const makeMockEmojiMasto = (overrides = [{}]) => {
describe('API Entities normalizer', () => {
describe('parseStatus', () => {
describe('QVitter preprocessing', () => {
it("doesn't blow up", () => {
const parsed = qvitterapidata.map(parseStatus)
expect(parsed.length).to.eq(qvitterapidata.length)
})
it('identifies favorites', () => {
const fav = {
uri: 'tag:soykaf.com,2016-08-21:fave:2558:note:339495:2016-08-21T16:54:04+00:00',
is_post_verb: false,
}
const mastoFav = {
uri: 'tag:mastodon.social,2016-11-27:objectId=73903:objectType=Favourite',
is_post_verb: false,
}
expect(parseStatus(makeMockStatusQvitter(fav))).to.have.property(
'type',
'favorite',
)
expect(parseStatus(makeMockStatusQvitter(mastoFav))).to.have.property(
'type',
'favorite',
)
})
it('processes repeats correctly', () => {
const post = makeMockStatusQvitter({
retweeted_status: null,
id: 'deadbeef',
})
const repeat = makeMockStatusQvitter({
retweeted_status: post,
is_post_verb: false,
id: 'foobar',
})
const parsedPost = parseStatus(post)
const parsedRepeat = parseStatus(repeat)
expect(parsedPost).to.have.property('type', 'status')
expect(parsedRepeat).to.have.property('type', 'retweet')
expect(parsedRepeat).to.have.property('retweeted_status')
expect(parsedRepeat).to.have.nested.property(
'retweeted_status.id',
'deadbeef',
)
})
it('sets nsfw for statuses with the #nsfw tag', () => {
const safe = makeMockStatusQvitter({ id: '1', text: 'Hello oniichan' })
const nsfw = makeMockStatusQvitter({
id: '1',
text: 'Hello oniichan #nsfw',
})
expect(parseStatus(safe).nsfw).to.eq(false)
expect(parseStatus(nsfw).nsfw).to.eq(true)
})
it('leaves existing nsfw settings alone', () => {
const nsfw = makeMockStatusQvitter({
id: '1',
text: 'Hello oniichan #nsfw',
nsfw: false,
})
expect(parseStatus(nsfw).nsfw).to.eq(false)
})
})
describe('Mastoapi preprocessing and converting', () => {
it("doesn't blow up", () => {
const parsed = mastoapidata.map(parseStatus)
@ -344,60 +189,6 @@ describe('API Entities normalizer', () => {
})
})
// We currently use QvitterAPI notifications only, and especially due to MastoAPI lacking is_seen, support for MastoAPI
// is more of an afterthought
describe('parseNotifications (QvitterAPI)', () => {
it("correctly normalizes data to FE's format", () => {
const notif = makeMockNotificationQvitter({
id: 123,
notice: makeMockStatusQvitter({ id: 444 }),
from_profile: makeMockUserQvitter({ id: 'spurdo' }),
})
expect(parseNotification(notif)).to.have.property('id', 123)
expect(parseNotification(notif)).to.have.property('seen', false)
expect(parseNotification(notif)).to.have.nested.property(
'status.id',
'444',
)
expect(parseNotification(notif)).to.have.nested.property(
'action.id',
'444',
)
expect(parseNotification(notif)).to.have.nested.property(
'from_profile.id',
'spurdo',
)
})
it('correctly normalizes favorite notifications', () => {
const notif = makeMockNotificationQvitter({
id: 123,
ntype: 'like',
notice: makeMockStatusQvitter({
id: 444,
favorited_status: makeMockStatusQvitter({ id: 4412 }),
}),
is_seen: 1,
from_profile: makeMockUserQvitter({ id: 'spurdo' }),
})
expect(parseNotification(notif)).to.have.property('id', 123)
expect(parseNotification(notif)).to.have.property('type', 'like')
expect(parseNotification(notif)).to.have.property('seen', true)
expect(parseNotification(notif)).to.have.nested.property(
'status.id',
'4412',
)
expect(parseNotification(notif)).to.have.nested.property(
'action.id',
'444',
)
expect(parseNotification(notif)).to.have.nested.property(
'from_profile.id',
'spurdo',
)
})
})
describe('Link header pagination', () => {
it('Parses min and max ids as integers', () => {
const linkHeader =