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) { button:not(.button-default) {
color: var(--text); color: var(--text);
font-size: 100%; font-size: 100%;
text-align: initial;
padding: 0;
background: none;
border: none;
outline: none;
display: inline;
font-family: inherit;
line-height: unset;
} }
&.disabled { &.disabled {
@ -436,22 +444,6 @@ nav {
--__line-height: 1.5em; --__line-height: 1.5em;
--__horizontal-gap: 0.75em; --__horizontal-gap: 0.75em;
--__vertical-gap: 0.5em; --__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 { .button-unstyled {

View file

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

View file

@ -18,7 +18,9 @@
<slot /> <slot />
</div> </div>
</div> </div>
<div class="below">
<slot name="below" />
</div>
<template #footer> <template #footer>
<slot name="footerLeft" /> <slot name="footerLeft" />
<button <button
@ -61,6 +63,10 @@
} }
} }
.below:not(:empty) {
margin-top: 1em;
}
.text { .text {
max-width: 50ch; 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: allowFollowingMove:
this.$store.state.users.currentUser.allow_following_move, this.$store.state.users.currentUser.allow_following_move,
filterMode: tabModeDict.mentions, filterMode: tabModeDict.mentions,
canSeeReports: this.$store.state.users.currentUser.has.has( canSeeReports: this.$store.state.users.currentUser.privileges.has(
'reports_manage_reports', 'reports_manage_reports',
), ),
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -43,7 +43,7 @@
> >
<td>{{ entry.instance }}</td> <td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''"> <td v-if="entry.reason === ''">
{{ $t("general.not_applicable") }} {{ $t("about.mrf.simple.not_applicable") }}
</td> </td>
<td v-else> <td v-else>
{{ entry.reason }} {{ entry.reason }}
@ -70,7 +70,7 @@
> >
<td>{{ entry.instance }}</td> <td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''"> <td v-if="entry.reason === ''">
{{ $t("general.not_applicable") }} {{ $t("about.mrf.simple.not_applicable") }}
</td> </td>
<td v-else> <td v-else>
{{ entry.reason }} {{ entry.reason }}
@ -97,7 +97,7 @@
> >
<td>{{ entry.instance }}</td> <td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''"> <td v-if="entry.reason === ''">
{{ $t("general.not_applicable") }} {{ $t("about.mrf.simple.not_applicable") }}
</td> </td>
<td v-else> <td v-else>
{{ entry.reason }} {{ entry.reason }}
@ -124,7 +124,7 @@
> >
<td>{{ entry.instance }}</td> <td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''"> <td v-if="entry.reason === ''">
{{ $t("general.not_applicable") }} {{ $t("about.mrf.simple.not_applicable") }}
</td> </td>
<td v-else> <td v-else>
{{ entry.reason }} {{ entry.reason }}
@ -151,7 +151,7 @@
> >
<td>{{ entry.instance }}</td> <td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''"> <td v-if="entry.reason === ''">
{{ $t("general.not_applicable") }} {{ $t("about.mrf.simple.not_applicable") }}
</td> </td>
<td v-else> <td v-else>
{{ entry.reason }} {{ entry.reason }}
@ -178,7 +178,7 @@
> >
<td>{{ entry.instance }}</td> <td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''"> <td v-if="entry.reason === ''">
{{ $t("general.not_applicable") }} {{ $t("about.mrf.simple.not_applicable") }}
</td> </td>
<td v-else> <td v-else>
{{ entry.reason }} {{ 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 BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue' import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue'
const AdminCard = { const AdminUserCard = {
props: { props: {
userId: { userId: {
type: String, type: String,
@ -17,9 +17,6 @@ const AdminCard = {
user() { user() {
return this.$store.getters.findUser(this.userId) return this.$store.getters.findUser(this.userId)
}, },
relationship() {
return this.$store.getters.relationship(this.userId)
},
isAdmin() { isAdmin() {
return this.user.rights.admin 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>
<template v-else> <template v-else>
<BasicUserCard <BasicUserCard
class="AdminCard" class="AdminUserCard"
:user="user" :user="user"
show-line-labels show-line-labels
> >
@ -18,8 +18,8 @@
</strong> </strong>
{{ ' ' }} {{ ' ' }}
<span <span
class="faint"
v-if="user.adminData.email == null" v-if="user.adminData.email == null"
class="faint"
> >
{{ $t('general.not_available') }} {{ $t('general.not_available') }}
</span> </span>
@ -35,7 +35,7 @@
{{ $t('user_card.admin_data.registration_reason') }} {{ $t('user_card.admin_data.registration_reason') }}
</summary> </summary>
<span> <span>
{{ user.adminData.registration_reason }} {{ user.adminData.registration_reason }}
</span> </span>
</details> </details>
<div class="right-side"> <div class="right-side">
@ -102,6 +102,6 @@
</template> </template>
</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 ref = suggestRef || this.getSuggestedRef(frontend)
const { name } = frontend const { name } = frontend
useAdminSettingsStore.updateAdminDraft({ useAdminSettingsStore().updateAdminDraft({
path: [':pleroma', ':frontends', ':primary'], path: [':pleroma', ':frontends', ':primary'],
value: { name, ref }, value: { name, ref },
}) })

View file

@ -112,19 +112,19 @@ const LinksTab = {
}, },
methods: { methods: {
checkRel(e) { checkRel(e) {
useAdminSettingsStore.updateAdminDraft({ useAdminSettingsStore().updateAdminDraft({
path: [':pleroma', 'Pleroma.Formatter', ':rel'], path: [':pleroma', 'Pleroma.Formatter', ':rel'],
value: e ? '' : false, value: e ? '' : false,
}) })
}, },
checkClass(e) { checkClass(e) {
useAdminSettingsStore.updateAdminDraft({ useAdminSettingsStore().updateAdminDraft({
path: [':pleroma', 'Pleroma.Formatter', ':class'], path: [':pleroma', 'Pleroma.Formatter', ':class'],
value: e ? '' : false, value: e ? '' : false,
}) })
}, },
checkTruncate(e) { checkTruncate(e) {
useAdminSettingsStore.updateAdminDraft({ useAdminSettingsStore().updateAdminDraft({
path: [':pleroma', 'Pleroma.Formatter', ':truncate'], path: [':pleroma', 'Pleroma.Formatter', ':truncate'],
value: e ? 20 : false, 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 BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import Checkbox from 'src/components/checkbox/checkbox.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 List from 'src/components/list/list.vue'
import ModerationTools from 'src/components/moderation_tools/moderation_tools.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 Select from 'src/components/select/select.vue'
import AdminCard from 'src/components/settings_modal/admin_tabs/admin_card.vue' import AdminUserCard from 'src/components/settings_modal/admin_tabs/admin_user_card.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js' import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
@ -18,17 +15,8 @@ const UsersTab = {
Select, Select,
BasicUserCard, BasicUserCard,
List, List,
ProgressButton, AdminUserCard,
AdminCard,
TabSwitcher,
ModerationTools, ModerationTools,
GenericConfirm,
},
provide() {
return {
defaultDraftMode: true,
defaultSource: 'admin',
}
}, },
data() { data() {
return { return {
@ -120,12 +108,11 @@ const UsersTab = {
...this.fetchOptions, ...this.fetchOptions,
page, page,
}) })
.then(({ count, users }) => ({ count, items: users }))
}, },
}, },
watch: { watch: {
fetchOptions() { fetchOptions() {
this.$refs.usersList.reset() this.$refs.usersList?.reset()
}, },
}, },
} }

View file

@ -6,7 +6,9 @@
.filters-section { .filters-section {
display: grid; 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; gap: 0.5em 1em;
> div { > div {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -51,19 +51,18 @@
</div> </div>
</div> </div>
<div class="user-reporting-panel-right"> <div class="user-reporting-panel-right">
<List :items="statuses"> <List
:external-items="reportModal.statuses"
:pre-select="reportModal.preTickedIds"
selectable
@select="onListSelect"
>
<template #item="{item}"> <template #item="{item}">
<div class="status-fadein user-reporting-panel-sitem"> <Status
<Status :in-conversation="false"
:in-conversation="false" :focused="false"
:focused="false" :statusoid="item"
:statusoid="item" />
/>
<Checkbox
:model-value="isChecked(item.id)"
@update:model-value="checked => toggleStatus(checked, item.id)"
/>
</div>
</template> </template>
</List> </List>
</div> </div>
@ -136,20 +135,6 @@
overflow-y: auto; 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) { @media all and (width >= 801px) {
.panel-body { .panel-body {
flex-direction: row; flex-direction: row;

View file

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

View file

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

View file

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

View file

@ -307,8 +307,12 @@ export const useAdminSettingsStore = defineStore('adminSettings', {
opts, opts,
}) })
const statuses = activities.map(parseStatus)
await window.vuex.dispatch('addNewStatuses', { statuses })
return { return {
items: activities.map(parseStatus), items: statuses,
count: total, count: total,
} }
}, },
@ -318,10 +322,7 @@ export const useAdminSettingsStore = defineStore('adminSettings', {
}) })
const status = parseStatus(raw) const status = parseStatus(raw)
await window.vuex.dispatch('addNewStatuses', { await window.vuex.dispatch('addNewStatuses', { statuses: [status] })
statuses: [status],
userId: false,
})
}, },
// Users stuff // 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() { togglePeekSettingsModal() {
switch (this.settingsModalState) { switch (this.settingsModalState) {
case 'minimized': case 'minimized':
@ -141,8 +158,10 @@ export const useInterfaceStore = defineStore('interface', {
case 'visible': case 'visible':
this.settingsModalState = 'minimized' this.settingsModalState = 'minimized'
return return
case 'hidden':
return
default: default:
throw new Error('Illegal minimization state of settings modal') throw new Error(`Illegal minimization state of settings modal: ${this.settingsModalState}`)
} }
}, },
clearSettingsModalTargetTab() { clearSettingsModalTargetTab() {

View file

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

File diff suppressed because it is too large Load diff

View file

@ -5,76 +5,6 @@ import {
parseUser, parseUser,
} from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
import mastoapidata from '../../../../fixtures/mastoapi.json' 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 = {}) => { const makeMockUserMasto = (overrides = {}) => {
return Object.assign( 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 = [{}]) => { const makeMockEmojiMasto = (overrides = [{}]) => {
return [ return [
Object.assign( Object.assign(
@ -189,78 +106,6 @@ const makeMockEmojiMasto = (overrides = [{}]) => {
describe('API Entities normalizer', () => { describe('API Entities normalizer', () => {
describe('parseStatus', () => { 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', () => { describe('Mastoapi preprocessing and converting', () => {
it("doesn't blow up", () => { it("doesn't blow up", () => {
const parsed = mastoapidata.map(parseStatus) 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', () => { describe('Link header pagination', () => {
it('Parses min and max ids as integers', () => { it('Parses min and max ids as integers', () => {
const linkHeader = const linkHeader =