mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
Refactor Layer and its subcomponents, replacing cloneElement by context
This commit is contained in:
parent
9e6bcf1846
commit
d739977ca1
@ -1,38 +0,0 @@
|
||||
import { Children, cloneElement, isValidElement, useEffect } from 'react'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
|
||||
/**
|
||||
* @category Map
|
||||
*/
|
||||
export const ItemForm = ({
|
||||
children,
|
||||
item,
|
||||
title,
|
||||
setPopupTitle,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
item?: Item
|
||||
title?: string
|
||||
setPopupTitle?: React.Dispatch<React.SetStateAction<string>>
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
setPopupTitle && title && setPopupTitle(title)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [title])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{children
|
||||
? Children.toArray(children).map((child) =>
|
||||
isValidElement<{ item: Item; test: string }>(child)
|
||||
? cloneElement(child, { item, test: 'test' })
|
||||
: '',
|
||||
)
|
||||
: ''}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ItemForm.__TYPE = 'ItemForm'
|
||||
@ -1,20 +0,0 @@
|
||||
import { Children, cloneElement, isValidElement } from 'react'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
|
||||
/**
|
||||
* @category Map
|
||||
*/
|
||||
export const ItemView = ({ children, item }: { children?: React.ReactNode; item?: Item }) => {
|
||||
return (
|
||||
<div>
|
||||
{children
|
||||
? Children.toArray(children).map((child) =>
|
||||
isValidElement<{ item: Item }>(child) ? cloneElement(child, { item }) : null,
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ItemView.__TYPE = 'ItemView'
|
||||
@ -1,31 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
/* eslint-disable @typescript-eslint/prefer-optional-chain */
|
||||
import { Children, isValidElement, useEffect, useState } from 'react'
|
||||
import { Marker, Tooltip } from 'react-leaflet'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { encodeTag } from '#utils/FormatTags'
|
||||
import { hashTagRegex } from '#utils/HashTagRegex'
|
||||
import MarkerIconFactory from '#utils/MarkerIconFactory'
|
||||
import { randomColor } from '#utils/RandomColor'
|
||||
import LayerContext from '#components/Profile/templateComponents/LayerContext'
|
||||
|
||||
import {
|
||||
useFilterTags,
|
||||
useIsGroupTypeVisible,
|
||||
useIsLayerVisible,
|
||||
useVisibleGroupType,
|
||||
} from './hooks/useFilter'
|
||||
import { useAllItemsLoaded, useItems, useSetItemsApi, useSetItemsData } from './hooks/useItems'
|
||||
import { useAddMarker, useAddPopup, useLeafletRefs } from './hooks/useLeafletRefs'
|
||||
import { useSelectPosition, useSetMarkerClicked } from './hooks/useSelectPosition'
|
||||
import { useAddTag, useAllTagsLoaded, useGetItemTags, useTags } from './hooks/useTags'
|
||||
import { ItemFormPopup } from './Subcomponents/ItemFormPopup'
|
||||
import { ItemViewPopup } from './Subcomponents/ItemViewPopup'
|
||||
import { useSetItemsApi, useSetItemsData } from './hooks/useItems'
|
||||
import { useAddTag } from './hooks/useTags'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
import type { LayerProps } from '#types/LayerProps'
|
||||
import type { Tag } from '#types/Tag'
|
||||
import type { Popup } from 'leaflet'
|
||||
import type { ReactElement, ReactNode } from 'react'
|
||||
|
||||
export type { Point } from 'geojson'
|
||||
export type { Item } from '#types/Item'
|
||||
@ -59,32 +40,12 @@ export const Layer = ({
|
||||
itemFormPopup,
|
||||
clusterRef,
|
||||
}: LayerProps) => {
|
||||
const filterTags = useFilterTags()
|
||||
|
||||
const items = useItems()
|
||||
const setItemsApi = useSetItemsApi()
|
||||
const setItemsData = useSetItemsData()
|
||||
const getItemTags = useGetItemTags()
|
||||
const addMarker = useAddMarker()
|
||||
const addPopup = useAddPopup()
|
||||
const leafletRefs = useLeafletRefs()
|
||||
|
||||
const allTagsLoaded = useAllTagsLoaded()
|
||||
const allItemsLoaded = useAllItemsLoaded()
|
||||
|
||||
const setMarkerClicked = useSetMarkerClicked()
|
||||
const selectPosition = useSelectPosition()
|
||||
|
||||
const tags = useTags()
|
||||
const addTag = useAddTag()
|
||||
const [newTagsToAdd, setNewTagsToAdd] = useState<Tag[]>([])
|
||||
const [tagsReady, setTagsReady] = useState<boolean>(false)
|
||||
|
||||
const isLayerVisible = useIsLayerVisible()
|
||||
|
||||
const isGroupTypeVisible = useIsGroupTypeVisible()
|
||||
|
||||
const visibleGroupTypes = useVisibleGroupType()
|
||||
const [newTagsToAdd] = useState<Tag[]>([])
|
||||
const [tagsReady] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
data &&
|
||||
@ -156,178 +117,18 @@ export const Layer = ({
|
||||
}, [tagsReady])
|
||||
|
||||
return (
|
||||
<>
|
||||
{items &&
|
||||
items
|
||||
.filter((item) => item.layer?.name === name)
|
||||
.filter((item) =>
|
||||
filterTags.length === 0
|
||||
? item
|
||||
: filterTags.some((tag) =>
|
||||
getItemTags(item).some(
|
||||
(filterTag) =>
|
||||
filterTag.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.filter((item) => item.layer && isLayerVisible(item.layer))
|
||||
.filter(
|
||||
(item) =>
|
||||
(item.group_type && isGroupTypeVisible(item.group_type)) ||
|
||||
visibleGroupTypes.length === 0,
|
||||
)
|
||||
.map((item: Item) => {
|
||||
if (item.position?.coordinates[0] && item.position?.coordinates[1]) {
|
||||
if (item.tags) {
|
||||
item.text += '\n\n'
|
||||
item.tags.map((tag) => {
|
||||
if (!item.text?.includes(`#${encodeTag(tag)}`)) {
|
||||
item.text += `#${encodeTag(tag)}`
|
||||
}
|
||||
return item.text
|
||||
})
|
||||
}
|
||||
|
||||
if (allTagsLoaded && allItemsLoaded) {
|
||||
item.text?.match(hashTagRegex)?.map((tag) => {
|
||||
if (
|
||||
!tags.find(
|
||||
(t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase(),
|
||||
) &&
|
||||
!newTagsToAdd.find(
|
||||
(t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase(),
|
||||
)
|
||||
) {
|
||||
const newTag = {
|
||||
id: crypto.randomUUID(),
|
||||
name: tag.slice(1),
|
||||
color: randomColor(),
|
||||
}
|
||||
setNewTagsToAdd((current) => [...current, newTag])
|
||||
}
|
||||
return null
|
||||
})
|
||||
!tagsReady && setTagsReady(true)
|
||||
}
|
||||
|
||||
const itemTags = getItemTags(item)
|
||||
|
||||
const latitude = item.position.coordinates[1]
|
||||
const longitude = item.position.coordinates[0]
|
||||
|
||||
let color1 = markerDefaultColor
|
||||
let color2 = markerDefaultColor2
|
||||
if (item.color) {
|
||||
color1 = item.color
|
||||
} else if (itemTags[0]) {
|
||||
color1 = itemTags[0].color
|
||||
}
|
||||
if (itemTags[0] && item.color) {
|
||||
color2 = itemTags[0].color
|
||||
} else if (itemTags[1]) {
|
||||
color2 = itemTags[1].color
|
||||
}
|
||||
return (
|
||||
<Marker
|
||||
ref={(r) => {
|
||||
if (!(item.id in leafletRefs && leafletRefs[item.id].marker === r)) {
|
||||
r && addMarker(item, r)
|
||||
}
|
||||
}}
|
||||
eventHandlers={{
|
||||
click: () => {
|
||||
selectPosition && setMarkerClicked(item)
|
||||
},
|
||||
}}
|
||||
icon={MarkerIconFactory(
|
||||
markerShape,
|
||||
color1,
|
||||
color2,
|
||||
item.markerIcon ? item.markerIcon : markerIcon,
|
||||
)}
|
||||
key={item.id}
|
||||
position={[latitude, longitude]}
|
||||
>
|
||||
{children &&
|
||||
Children.toArray(children).some(
|
||||
(child) => isComponentWithType(child) && child.type.__TYPE === 'ItemView',
|
||||
) ? (
|
||||
Children.toArray(children).map((child) =>
|
||||
isComponentWithType(child) && child.type.__TYPE === 'ItemView' ? (
|
||||
<ItemViewPopup
|
||||
ref={(r) => {
|
||||
if (!(item.id in leafletRefs && leafletRefs[item.id].popup === r)) {
|
||||
r && addPopup(item, r as Popup)
|
||||
}
|
||||
}}
|
||||
key={item.id + item.name}
|
||||
item={item}
|
||||
setItemFormPopup={setItemFormPopup}
|
||||
>
|
||||
{child}
|
||||
</ItemViewPopup>
|
||||
) : null,
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<ItemViewPopup
|
||||
key={item.id + item.name}
|
||||
ref={(r) => {
|
||||
if (!(item.id in leafletRefs && leafletRefs[item.id].popup === r)) {
|
||||
r && addPopup(item, r as Popup)
|
||||
}
|
||||
}}
|
||||
item={item}
|
||||
setItemFormPopup={setItemFormPopup}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Tooltip offset={[0, -38]} direction='top'>
|
||||
{item.name}
|
||||
</Tooltip>
|
||||
</Marker>
|
||||
)
|
||||
} else return null
|
||||
})}
|
||||
{
|
||||
// {children}}
|
||||
}
|
||||
{itemFormPopup &&
|
||||
itemFormPopup.layer.name === name &&
|
||||
(children &&
|
||||
Children.toArray(children).some(
|
||||
(child) => isComponentWithType(child) && child.type.__TYPE === 'ItemForm',
|
||||
) ? (
|
||||
Children.toArray(children).map((child) =>
|
||||
isComponentWithType(child) && child.type.__TYPE === 'ItemForm' ? (
|
||||
<ItemFormPopup
|
||||
key={setItemFormPopup?.name}
|
||||
position={itemFormPopup.position}
|
||||
layer={itemFormPopup.layer}
|
||||
setItemFormPopup={setItemFormPopup}
|
||||
item={itemFormPopup.item}
|
||||
>
|
||||
{child}
|
||||
</ItemFormPopup>
|
||||
) : (
|
||||
''
|
||||
),
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<ItemFormPopup
|
||||
position={itemFormPopup.position}
|
||||
layer={itemFormPopup.layer}
|
||||
setItemFormPopup={setItemFormPopup}
|
||||
item={itemFormPopup.item}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
<LayerContext.Provider
|
||||
value={{
|
||||
name,
|
||||
markerDefaultColor,
|
||||
markerDefaultColor2,
|
||||
markerShape,
|
||||
markerIcon,
|
||||
itemFormPopup,
|
||||
setItemFormPopup,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LayerContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function isComponentWithType(node: ReactNode): node is ReactElement & { type: { __TYPE: string } } {
|
||||
return isValidElement(node) && typeof node.type !== 'string' && '__TYPE' in node.type
|
||||
}
|
||||
|
||||
0
src/Components/Map/ProfileView.tsx
Normal file
0
src/Components/Map/ProfileView.tsx
Normal file
@ -2,8 +2,6 @@ export { UtopiaMap } from './UtopiaMap'
|
||||
export * from './Layer'
|
||||
export { Tags } from './Tags'
|
||||
export * from './Permissions'
|
||||
export { ItemForm } from './ItemForm'
|
||||
export { ItemView } from './ItemView'
|
||||
export { PopupTextAreaInput } from './Subcomponents/ItemPopupComponents/PopupTextAreaInput'
|
||||
export { PopupStartEndInput } from './Subcomponents/ItemPopupComponents/PopupStartEndInput'
|
||||
export { PopupTextInput } from './Subcomponents/ItemPopupComponents/PopupTextInput'
|
||||
|
||||
@ -2,3 +2,5 @@ export { UserSettings } from './UserSettings'
|
||||
// export { PlusButton } from './Subcomponents/PlusButton'
|
||||
export { ProfileView } from './ProfileView'
|
||||
export { ProfileForm } from './ProfileForm'
|
||||
export { CardForm } from './templateComponents/CardForm'
|
||||
export { CardView } from './templateComponents/CardView'
|
||||
|
||||
29
src/Components/Profile/templateComponents/CardForm.tsx
Normal file
29
src/Components/Profile/templateComponents/CardForm.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { useContext } from 'react'
|
||||
|
||||
import { ItemFormPopup } from '#components/Map/Subcomponents/ItemFormPopup'
|
||||
|
||||
import LayerContext from './LayerContext'
|
||||
import TemplateItemContext from './TemplateItemContext'
|
||||
|
||||
/**
|
||||
* @category Map
|
||||
*/
|
||||
export const CardForm = ({ children }: { children?: React.ReactNode }) => {
|
||||
const { itemFormPopup, setItemFormPopup } = useContext(LayerContext)
|
||||
|
||||
return (
|
||||
itemFormPopup && (
|
||||
<ItemFormPopup
|
||||
key={setItemFormPopup?.name}
|
||||
position={itemFormPopup.position}
|
||||
layer={itemFormPopup.layer}
|
||||
setItemFormPopup={setItemFormPopup}
|
||||
item={itemFormPopup.item}
|
||||
>
|
||||
<TemplateItemContext.Provider value={itemFormPopup.item}>
|
||||
{children}
|
||||
</TemplateItemContext.Provider>
|
||||
</ItemFormPopup>
|
||||
)
|
||||
)
|
||||
}
|
||||
186
src/Components/Profile/templateComponents/CardView.tsx
Normal file
186
src/Components/Profile/templateComponents/CardView.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import { useContext, useMemo, useState } from 'react'
|
||||
import { Marker, Tooltip } from 'react-leaflet'
|
||||
|
||||
import {
|
||||
useFilterTags,
|
||||
useIsLayerVisible,
|
||||
useIsGroupTypeVisible,
|
||||
useVisibleGroupType,
|
||||
} from '#components/Map/hooks/useFilter'
|
||||
import { useItems, useAllItemsLoaded } from '#components/Map/hooks/useItems'
|
||||
import { useAddMarker, useAddPopup, useLeafletRefs } from '#components/Map/hooks/useLeafletRefs'
|
||||
import { useSetMarkerClicked, useSelectPosition } from '#components/Map/hooks/useSelectPosition'
|
||||
import { useGetItemTags, useAllTagsLoaded, useTags } from '#components/Map/hooks/useTags'
|
||||
import { ItemViewPopup } from '#components/Map/Subcomponents/ItemViewPopup'
|
||||
import { encodeTag } from '#utils/FormatTags'
|
||||
import { hashTagRegex } from '#utils/HashTagRegex'
|
||||
import MarkerIconFactory from '#utils/MarkerIconFactory'
|
||||
import { randomColor } from '#utils/RandomColor'
|
||||
|
||||
import LayerContext from './LayerContext'
|
||||
import TemplateItemContext from './TemplateItemContext'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
import type { Tag } from '#types/Tag'
|
||||
import type { Popup } from 'leaflet'
|
||||
|
||||
// TODO Think about folder structure. This is not for profile, but for card / popup. Both can use the same template components.
|
||||
|
||||
/**
|
||||
* @category Profile
|
||||
*/
|
||||
export const CardView = ({ children }: { children?: React.ReactNode }) => {
|
||||
const cardViewContext = useContext(LayerContext)
|
||||
const {
|
||||
name,
|
||||
markerDefaultColor,
|
||||
markerDefaultColor2,
|
||||
markerShape,
|
||||
markerIcon,
|
||||
setItemFormPopup,
|
||||
} = cardViewContext
|
||||
|
||||
const filterTags = useFilterTags()
|
||||
|
||||
const items = useItems()
|
||||
const getItemTags = useGetItemTags()
|
||||
const addMarker = useAddMarker()
|
||||
const addPopup = useAddPopup()
|
||||
const leafletRefs = useLeafletRefs()
|
||||
|
||||
const allTagsLoaded = useAllTagsLoaded()
|
||||
const allItemsLoaded = useAllItemsLoaded()
|
||||
|
||||
const setMarkerClicked = useSetMarkerClicked()
|
||||
const selectPosition = useSelectPosition()
|
||||
|
||||
const tags = useTags()
|
||||
const [newTagsToAdd, setNewTagsToAdd] = useState<Tag[]>([])
|
||||
const [tagsReady, setTagsReady] = useState<boolean>(false)
|
||||
|
||||
const isLayerVisible = useIsLayerVisible()
|
||||
|
||||
const isGroupTypeVisible = useIsGroupTypeVisible()
|
||||
|
||||
const visibleGroupTypes = useVisibleGroupType()
|
||||
|
||||
const visibleItems = useMemo(
|
||||
() =>
|
||||
items
|
||||
.filter((item) => item.layer?.name === name)
|
||||
.filter((item) =>
|
||||
filterTags.length === 0
|
||||
? item
|
||||
: filterTags.some((tag) =>
|
||||
getItemTags(item).some(
|
||||
(filterTag) =>
|
||||
filterTag.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.filter((item) => item.layer && isLayerVisible(item.layer))
|
||||
.filter(
|
||||
(item) =>
|
||||
(item.group_type && isGroupTypeVisible(item.group_type)) ||
|
||||
visibleGroupTypes.length === 0,
|
||||
),
|
||||
[
|
||||
filterTags,
|
||||
getItemTags,
|
||||
isGroupTypeVisible,
|
||||
isLayerVisible,
|
||||
items,
|
||||
name,
|
||||
visibleGroupTypes.length,
|
||||
],
|
||||
)
|
||||
|
||||
if (!setItemFormPopup) {
|
||||
throw new Error('setItemFormPopup is not defined')
|
||||
}
|
||||
|
||||
return visibleItems.map((item: Item) => {
|
||||
if (!(item.position?.coordinates[0] && item.position.coordinates[1])) return null
|
||||
|
||||
if (item.tags) {
|
||||
item.text += '\n\n'
|
||||
item.tags.map((tag) => {
|
||||
if (!item.text?.includes(`#${encodeTag(tag)}`)) {
|
||||
item.text += `#${encodeTag(tag)}`
|
||||
}
|
||||
return item.text
|
||||
})
|
||||
}
|
||||
|
||||
if (allTagsLoaded && allItemsLoaded) {
|
||||
item.text?.match(hashTagRegex)?.map((tag) => {
|
||||
if (
|
||||
!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase()) &&
|
||||
!newTagsToAdd.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())
|
||||
) {
|
||||
const newTag = {
|
||||
id: crypto.randomUUID(),
|
||||
name: tag.slice(1),
|
||||
color: randomColor(),
|
||||
}
|
||||
setNewTagsToAdd((current) => [...current, newTag])
|
||||
}
|
||||
return null
|
||||
})
|
||||
!tagsReady && setTagsReady(true)
|
||||
}
|
||||
|
||||
const itemTags = getItemTags(item)
|
||||
|
||||
const latitude = item.position.coordinates[1]
|
||||
const longitude = item.position.coordinates[0]
|
||||
|
||||
let color1 = markerDefaultColor
|
||||
let color2 = markerDefaultColor2
|
||||
if (item.color) {
|
||||
color1 = item.color
|
||||
} else if (itemTags[0]) {
|
||||
color1 = itemTags[0].color
|
||||
}
|
||||
if (itemTags[0] && item.color) {
|
||||
color2 = itemTags[0].color
|
||||
} else if (itemTags[1]) {
|
||||
color2 = itemTags[1].color
|
||||
}
|
||||
|
||||
return (
|
||||
<TemplateItemContext.Provider value={item} key={item.id}>
|
||||
<Marker
|
||||
ref={(r) => {
|
||||
if (!(item.id in leafletRefs && leafletRefs[item.id].marker === r)) {
|
||||
r && addMarker(item, r)
|
||||
}
|
||||
}}
|
||||
eventHandlers={{
|
||||
click: () => {
|
||||
selectPosition && setMarkerClicked(item)
|
||||
},
|
||||
}}
|
||||
icon={MarkerIconFactory(markerShape, color1, color2, item.markerIcon ?? markerIcon)}
|
||||
position={[latitude, longitude]}
|
||||
>
|
||||
<ItemViewPopup
|
||||
ref={(r: Popup | null) => {
|
||||
if (!(item.id in leafletRefs && leafletRefs[item.id].popup === r)) {
|
||||
r && addPopup(item, r)
|
||||
}
|
||||
}}
|
||||
item={item}
|
||||
setItemFormPopup={setItemFormPopup}
|
||||
>
|
||||
{children}
|
||||
</ItemViewPopup>
|
||||
|
||||
<Tooltip offset={[0, -38]} direction='top'>
|
||||
{item.name}
|
||||
</Tooltip>
|
||||
</Marker>
|
||||
</TemplateItemContext.Provider>
|
||||
)
|
||||
})
|
||||
}
|
||||
27
src/Components/Profile/templateComponents/LayerContext.ts
Normal file
27
src/Components/Profile/templateComponents/LayerContext.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { createContext } from 'react'
|
||||
|
||||
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
|
||||
|
||||
// Where should we define defaults, here or in Layer.tsx?
|
||||
|
||||
interface LayerContextType {
|
||||
name: string
|
||||
markerDefaultColor: string
|
||||
markerDefaultColor2: string
|
||||
markerShape: string
|
||||
markerIcon: string
|
||||
itemFormPopup: ItemFormPopupProps | null | undefined
|
||||
setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>> | undefined
|
||||
}
|
||||
|
||||
const LayerContext = createContext<LayerContextType>({
|
||||
name: '',
|
||||
markerDefaultColor: '#777',
|
||||
markerDefaultColor2: 'RGBA(35, 31, 32, 0.2)',
|
||||
markerShape: 'circle',
|
||||
markerIcon: '',
|
||||
itemFormPopup: undefined,
|
||||
setItemFormPopup: undefined,
|
||||
})
|
||||
|
||||
export default LayerContext
|
||||
@ -0,0 +1,7 @@
|
||||
import { createContext } from 'react'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
|
||||
const ItemContext = createContext<Item | undefined>(undefined)
|
||||
|
||||
export default ItemContext
|
||||
Loading…
x
Reference in New Issue
Block a user