feat: various formatting and type improvements
This commit is contained in:
parent
f9a825b577
commit
af464668b3
|
@ -21,7 +21,7 @@
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
|
||||||
<list-settings-dropdown
|
<list-settings-dropdown
|
||||||
v-if="canWriteCurrentList && currentList.id !== -1"
|
v-if="canWriteCurrentList && currentList.id !== LIST_ID.FAVORITES"
|
||||||
class="list-title-dropdown"
|
class="list-title-dropdown"
|
||||||
:list="currentList"
|
:list="currentList"
|
||||||
>
|
>
|
||||||
|
@ -88,6 +88,7 @@
|
||||||
import {computed} from 'vue'
|
import {computed} from 'vue'
|
||||||
|
|
||||||
import {RIGHTS as Rights} from '@/constants/rights'
|
import {RIGHTS as Rights} from '@/constants/rights'
|
||||||
|
import {LIST_ID} from '@/constants/lists'
|
||||||
|
|
||||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
||||||
import Dropdown from '@/components/misc/dropdown.vue'
|
import Dropdown from '@/components/misc/dropdown.vue'
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
<div
|
<div
|
||||||
:class="{'is-visible': background}"
|
:class="{'is-visible': background}"
|
||||||
class="app-container-background background-fade-in d-print-none"
|
class="app-container-background background-fade-in d-print-none"
|
||||||
:style="{'background-image': background && `url(${background})`}"></div>
|
:style="{'background-image': background && `url(${background})`}">
|
||||||
|
</div>
|
||||||
<navigation class="d-print-none"/>
|
<navigation class="d-print-none"/>
|
||||||
<main
|
<main
|
||||||
class="app-content"
|
class="app-content"
|
||||||
|
|
3
src/constants/lists.ts
Normal file
3
src/constants/lists.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const LIST_ID = {
|
||||||
|
FAVORITES: -1
|
||||||
|
} as const
|
5
src/constants/namespaces.ts
Normal file
5
src/constants/namespaces.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export const NAMESPACE_ID = {
|
||||||
|
SHARED_LIST: -1,
|
||||||
|
FAVORITES: -2,
|
||||||
|
FILTERS: -3,
|
||||||
|
} as const;
|
|
@ -1,8 +1,10 @@
|
||||||
import {i18n} from '@/i18n'
|
import {i18n} from '@/i18n'
|
||||||
import type {IList} from '@/modelTypes/IList'
|
import type {IList} from '@/modelTypes/IList'
|
||||||
|
|
||||||
|
import {LIST_ID} from '@/constants/lists'
|
||||||
|
|
||||||
export function getListTitle(l: IList) {
|
export function getListTitle(l: IList) {
|
||||||
if (l.id === -1) {
|
if (l.id === LIST_ID.FAVORITES) {
|
||||||
return i18n.global.t('list.pseudo.favorites.title')
|
return i18n.global.t('list.pseudo.favorites.title')
|
||||||
}
|
}
|
||||||
return l.title
|
return l.title
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import {i18n} from '@/i18n'
|
import {i18n} from '@/i18n'
|
||||||
import type {INamespace} from '@/modelTypes/INamespace'
|
import type {INamespace} from '@/modelTypes/INamespace'
|
||||||
|
|
||||||
|
import {NAMESPACE_ID} from "@/constants/namespaces"
|
||||||
|
|
||||||
export const getNamespaceTitle = (n: INamespace) => {
|
export const getNamespaceTitle = (n: INamespace) => {
|
||||||
if (n.id === -1) {
|
if (n.id === NAMESPACE_ID.SHARED_LIST) {
|
||||||
return i18n.global.t('namespace.pseudo.sharedLists.title')
|
return i18n.global.t('namespace.pseudo.sharedLists.title')
|
||||||
}
|
}
|
||||||
if (n.id === -2) {
|
if (n.id === NAMESPACE_ID.FAVORITES) {
|
||||||
return i18n.global.t('namespace.pseudo.favorites.title')
|
return i18n.global.t('namespace.pseudo.favorites.title')
|
||||||
}
|
}
|
||||||
if (n.id === -3) {
|
if (n.id === NAMESPACE_ID.FILTERS) {
|
||||||
return i18n.global.t('namespace.pseudo.savedFilters.title')
|
return i18n.global.t('namespace.pseudo.savedFilters.title')
|
||||||
}
|
}
|
||||||
return n.title
|
return n.title
|
||||||
|
|
|
@ -16,10 +16,10 @@ export interface IList extends IAbstract {
|
||||||
hexColor: string
|
hexColor: string
|
||||||
identifier: string
|
identifier: string
|
||||||
backgroundInformation: unknown | null // FIXME: improve type
|
backgroundInformation: unknown | null // FIXME: improve type
|
||||||
|
backgroundBlurHash: string
|
||||||
isFavorite: boolean
|
isFavorite: boolean
|
||||||
subscription: ISubscription
|
subscription: ISubscription
|
||||||
position: number
|
position: number
|
||||||
backgroundBlurHash: string
|
|
||||||
|
|
||||||
created: Date
|
created: Date
|
||||||
updated: Date
|
updated: Date
|
||||||
|
|
|
@ -300,7 +300,6 @@ const router = createRouter({
|
||||||
meta: {
|
meta: {
|
||||||
showAsModal: true,
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/share',
|
path: '/lists/:listId/settings/share',
|
||||||
|
|
|
@ -2,7 +2,6 @@ import {AuthenticatedHTTPFactory} from '@/helpers/fetcher'
|
||||||
import type {Method} from 'axios'
|
import type {Method} from 'axios'
|
||||||
|
|
||||||
import {objectToSnakeCase} from '@/helpers/case'
|
import {objectToSnakeCase} from '@/helpers/case'
|
||||||
import AbstractModel from '@/models/abstractModel'
|
|
||||||
import type {IAbstract} from '@/modelTypes/IAbstract'
|
import type {IAbstract} from '@/modelTypes/IAbstract'
|
||||||
import type {Right} from '@/constants/rights'
|
import type {Right} from '@/constants/rights'
|
||||||
|
|
||||||
|
@ -185,14 +184,14 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
* The modelFactory returns a model from an object.
|
* The modelFactory returns a model from an object.
|
||||||
* This one here is the default one, usually the service definitions for a model will override this.
|
* This one here is the default one, usually the service definitions for a model will override this.
|
||||||
*/
|
*/
|
||||||
modelFactory(data : Partial<Model>) {
|
modelFactory(data : Partial<Model> = {}) {
|
||||||
return data as Model
|
return {...data} as Model
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the model factory for get requests.
|
* This is the model factory for get requests.
|
||||||
*/
|
*/
|
||||||
modelGetFactory(data : Partial<Model>) {
|
modelGetFactory(data : Partial<Model> = {}) {
|
||||||
return this.modelFactory(data)
|
return this.modelFactory(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,7 +269,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
* This is a more abstract implementation which only does a get request.
|
* This is a more abstract implementation which only does a get request.
|
||||||
* Services which need more flexibility can use this.
|
* Services which need more flexibility can use this.
|
||||||
*/
|
*/
|
||||||
async getM(url : string, model : Model = new AbstractModel({}), params: Record<string, unknown> = {}) {
|
async getM(url : string, model : Model = this.modelGetFactory(), params: Record<string, unknown> = {}) {
|
||||||
const cancel = this.setLoading()
|
const cancel = this.setLoading()
|
||||||
|
|
||||||
model = this.beforeGet(model)
|
model = this.beforeGet(model)
|
||||||
|
@ -293,7 +292,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
return window.URL.createObjectURL(new Blob([response.data]))
|
return URL.createObjectURL(new Blob([response.data]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -303,7 +302,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
* @param params Optional query parameters
|
* @param params Optional query parameters
|
||||||
* @param page The page to get
|
* @param page The page to get
|
||||||
*/
|
*/
|
||||||
async getAll(model : Model = new AbstractModel({}), params = {}, page = 1) {
|
async getAll(model : Model = this.modelFactory(), params = {}, page = 1) {
|
||||||
if (this.paths.getAll === '') {
|
if (this.paths.getAll === '') {
|
||||||
throw new Error('This model is not able to get data.')
|
throw new Error('This model is not able to get data.')
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,6 @@ export default class BackgroundUnsplashService extends AbstractService<IBackgrou
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
})
|
})
|
||||||
return window.URL.createObjectURL(new Blob([response.data]))
|
return URL.createObjectURL(new Blob([response.data]))
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,20 +3,21 @@ import {useRouter} from 'vue-router'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import type {MaybeRef} from '@vueuse/core'
|
import type {MaybeRef} from '@vueuse/core'
|
||||||
|
|
||||||
import {success} from '@/message'
|
import type {IList} from '@/modelTypes/IList'
|
||||||
|
import type {INamespace} from '@/modelTypes/INamespace'
|
||||||
import AbstractService from './abstractService'
|
|
||||||
import ListModel from '@/models/list'
|
|
||||||
import TaskService from './task'
|
|
||||||
import {colorFromHex} from '@/helpers/color/colorFromHex'
|
|
||||||
import ListDuplicateModel from '@/models/listDuplicateModel'
|
|
||||||
import ListDuplicateService from './listDuplicateService'
|
|
||||||
|
|
||||||
import {useListStore} from '@/stores/lists'
|
import {useListStore} from '@/stores/lists'
|
||||||
import {useNamespaceStore} from '@/stores/namespaces'
|
import {useNamespaceStore} from '@/stores/namespaces'
|
||||||
|
|
||||||
import type {IList} from '@/modelTypes/IList'
|
import AbstractService from '@/services/abstractService'
|
||||||
import type {INamespace} from '@/modelTypes/INamespace'
|
import TaskService from '@/services/task'
|
||||||
|
import ListDuplicateService from '@/services/listDuplicateService'
|
||||||
|
|
||||||
|
import ListModel from '@/models/list'
|
||||||
|
import ListDuplicateModel from '@/models/listDuplicateModel'
|
||||||
|
|
||||||
|
import {colorFromHex} from '@/helpers/color/colorFromHex'
|
||||||
|
import {success} from '@/message'
|
||||||
|
|
||||||
export default class ListService extends AbstractService<IList> {
|
export default class ListService extends AbstractService<IList> {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -48,7 +49,7 @@ export default class ListService extends AbstractService<IList> {
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeCreate(list) {
|
beforeCreate(list: IList) {
|
||||||
list.hexColor = colorFromHex(list.hexColor)
|
list.hexColor = colorFromHex(list.hexColor)
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
@ -70,7 +71,7 @@ export default class ListService extends AbstractService<IList> {
|
||||||
const cancel = this.setLoading()
|
const cancel = this.setLoading()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.http.delete(`/lists/${list.id}/background`, list)
|
const response = await this.http.delete<IList>(`/lists/${list.id}/background`)
|
||||||
return response.data
|
return response.data
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||||
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
||||||
|
|
||||||
import ListModel from '@/models/list'
|
import ListModel from '@/models/list'
|
||||||
import ListService from '../services/list'
|
import ListService from '@/services/list'
|
||||||
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
||||||
|
|
||||||
import {useMenuActive} from '@/composables/useMenuActive'
|
import {useMenuActive} from '@/composables/useMenuActive'
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
import {watch, reactive, shallowReactive, unref, toRefs, readonly, ref, computed} from 'vue'
|
import {watch, reactive, shallowReactive, unref, toRefs, readonly, ref, computed} from 'vue'
|
||||||
|
import type {MaybeRef} from '@vueuse/core'
|
||||||
import {acceptHMRUpdate, defineStore} from 'pinia'
|
import {acceptHMRUpdate, defineStore} from 'pinia'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
import ListService from '@/services/list'
|
|
||||||
import {setModuleLoading} from '@/stores/helper'
|
|
||||||
import {removeListFromHistory} from '@/modules/listHistory'
|
|
||||||
import {createNewIndexer} from '@/indexes'
|
|
||||||
import {useNamespaceStore} from './namespaces'
|
|
||||||
|
|
||||||
import type {IList} from '@/modelTypes/IList'
|
import type {IList} from '@/modelTypes/IList'
|
||||||
|
|
||||||
import type {MaybeRef} from '@vueuse/core'
|
import {LIST_ID} from '@/constants/lists'
|
||||||
|
import {NAMESPACE_ID} from "@/constants/namespaces"
|
||||||
|
|
||||||
import ListModel from '@/models/list'
|
import {useNamespaceStore} from '@/stores/namespaces'
|
||||||
import {success} from '@/message'
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
|
|
||||||
const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description'])
|
import ListModel from '@/models/list'
|
||||||
|
import {removeListFromHistory} from '@/modules/listHistory'
|
||||||
|
import ListService from '@/services/list'
|
||||||
|
import {setModuleLoading} from '@/stores/helper'
|
||||||
|
import {createNewIndexer} from '@/indexes'
|
||||||
|
import {success} from '@/message'
|
||||||
|
|
||||||
const FavoriteListsNamespace = -2
|
const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description'])
|
||||||
|
|
||||||
export interface ListState {
|
export interface ListState {
|
||||||
[id: IList['id']]: IList
|
[id: IList['id']]: IList
|
||||||
|
@ -35,7 +35,9 @@ export const useListStore = defineStore('list', () => {
|
||||||
|
|
||||||
|
|
||||||
const getListById = computed(() => {
|
const getListById = computed(() => {
|
||||||
return (id: IList['id']) => typeof lists.value[id] !== 'undefined' ? lists.value[id] : null
|
return (id: IList['id']) => typeof lists.value[id] !== 'undefined'
|
||||||
|
? lists.value[id]
|
||||||
|
: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const findListByExactname = computed(() => {
|
const findListByExactname = computed(() => {
|
||||||
|
@ -85,7 +87,7 @@ export const useListStore = defineStore('list', () => {
|
||||||
function toggleListFavorite(list: IList) {
|
function toggleListFavorite(list: IList) {
|
||||||
// The favorites pseudo list is always favorite
|
// The favorites pseudo list is always favorite
|
||||||
// Archived lists cannot be marked favorite
|
// Archived lists cannot be marked favorite
|
||||||
if (list.id === -1 || list.isArchived) {
|
if (list.id === LIST_ID.FAVORITES || list.isArchived) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return updateList({
|
return updateList({
|
||||||
|
@ -122,7 +124,7 @@ export const useListStore = defineStore('list', () => {
|
||||||
// in order to not create a manipulation in pinia store we have to create a new copy
|
// in order to not create a manipulation in pinia store we have to create a new copy
|
||||||
const newList = {
|
const newList = {
|
||||||
...list,
|
...list,
|
||||||
namespaceId: FavoriteListsNamespace,
|
namespaceId: NAMESPACE_ID.FAVORITES,
|
||||||
}
|
}
|
||||||
|
|
||||||
namespaceStore.removeListFromNamespaceById(newList)
|
namespaceStore.removeListFromNamespaceById(newList)
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import {computed, readonly, ref} from 'vue'
|
import {computed, readonly, ref} from 'vue'
|
||||||
import {defineStore, acceptHMRUpdate} from 'pinia'
|
import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||||
|
|
||||||
import NamespaceService from '../services/namespace'
|
|
||||||
import {setModuleLoading} from '@/stores/helper'
|
|
||||||
import {createNewIndexer} from '@/indexes'
|
|
||||||
import type {INamespace} from '@/modelTypes/INamespace'
|
import type {INamespace} from '@/modelTypes/INamespace'
|
||||||
import type {IList} from '@/modelTypes/IList'
|
import type {IList} from '@/modelTypes/IList'
|
||||||
|
|
||||||
|
import {NAMESPACE_ID} from "@/constants/namespaces"
|
||||||
|
|
||||||
|
import {setModuleLoading} from '@/stores/helper'
|
||||||
import {useListStore} from '@/stores/lists'
|
import {useListStore} from '@/stores/lists'
|
||||||
|
|
||||||
|
import {createNewIndexer} from '@/indexes'
|
||||||
|
import NamespaceService from '@/services/namespace'
|
||||||
|
|
||||||
const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description'])
|
const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description'])
|
||||||
|
|
||||||
export const useNamespaceStore = defineStore('namespace', () => {
|
export const useNamespaceStore = defineStore('namespace', () => {
|
||||||
|
@ -169,14 +173,20 @@ export const useNamespaceStore = defineStore('namespace', () => {
|
||||||
|
|
||||||
function loadNamespacesIfFavoritesDontExist() {
|
function loadNamespacesIfFavoritesDontExist() {
|
||||||
// The first or second namespace should be the one holding all favorites
|
// The first or second namespace should be the one holding all favorites
|
||||||
if (namespaces.value[0].id === -2 || namespaces.value[1]?.id === -2) {
|
if (
|
||||||
|
namespaces.value[0].id === NAMESPACE_ID.FAVORITES ||
|
||||||
|
namespaces.value[1]?.id === NAMESPACE_ID.FAVORITES
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return loadNamespaces()
|
return loadNamespaces()
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFavoritesNamespaceIfEmpty() {
|
function removeFavoritesNamespaceIfEmpty() {
|
||||||
if (namespaces.value[0].id === -2 && namespaces.value[0].lists.length === 0) {
|
if (
|
||||||
|
namespaces.value[0].id === NAMESPACE_ID.FAVORITES &&
|
||||||
|
namespaces.value[0].lists.length === 0
|
||||||
|
) {
|
||||||
namespaces.value.splice(0, 1)
|
namespaces.value.splice(0, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,20 @@
|
||||||
@close="$router.back()"
|
@close="$router.back()"
|
||||||
@submit="archiveList()"
|
@submit="archiveList()"
|
||||||
>
|
>
|
||||||
<template #header><span>{{ list.isArchived ? $t('list.archive.unarchive') : $t('list.archive.archive') }}</span></template>
|
<template #header>
|
||||||
|
<span>{{
|
||||||
|
list.isArchived
|
||||||
|
? $t('list.archive.unarchive')
|
||||||
|
: $t('list.archive.archive')
|
||||||
|
}}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #text>
|
<template #text>
|
||||||
<p>{{ list.isArchived ? $t('list.archive.unarchiveText') : $t('list.archive.archiveText') }}</p>
|
<p>{{
|
||||||
|
list.isArchived
|
||||||
|
? $t('list.archive.unarchiveText')
|
||||||
|
: $t('list.archive.archiveText')
|
||||||
|
}}</p>
|
||||||
</template>
|
</template>
|
||||||
</modal>
|
</modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -183,8 +183,7 @@ async function searchBackgrounds(page = 1) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setBackground(backgroundId: IBackgroundImage['id']) {
|
||||||
async function setBackground(backgroundId: string) {
|
|
||||||
// Don't set a background if we're in the process of setting one
|
// Don't set a background if we're in the process of setting one
|
||||||
if (backgroundService.loading) {
|
if (backgroundService.loading) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -70,7 +70,7 @@ export default { name: 'list-setting-edit' }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {toRefs, type PropType} from 'vue'
|
import type {PropType} from 'vue'
|
||||||
import {useRouter} from 'vue-router'
|
import {useRouter} from 'vue-router'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
|
@ -85,21 +85,18 @@ import {useList} from '@/stores/lists'
|
||||||
|
|
||||||
import {useTitle} from '@/composables/useTitle'
|
import {useTitle} from '@/composables/useTitle'
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
listId: IList['id']
|
|
||||||
}>()
|
|
||||||
const {listId} = toRefs(props)
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const baseStore = useBaseStore()
|
||||||
|
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
|
|
||||||
const {list, save: saveList, isLoading} = useList(listId)
|
|
||||||
|
|
||||||
useTitle(() => list?.title ? t('list.edit.title', {list: list.title}) : '')
|
useTitle(() => list?.title ? t('list.edit.title', {list: list.title}) : '')
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
await saveList()
|
await saveList()
|
||||||
await useBaseStore().handleSetCurrentList({list: list.value})
|
await baseStore.handleSetCurrentList({list})
|
||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
Reference in New Issue
Block a user