fix(advanced checklist): style and layout improvements (#1182)
* fix: reorder icon color * fix: change reorder icon design * feat: useResize hook * feat: responsive task item * fix: adjust padding for mobile devices * fix: checkbox size should be proportional to text size Co-authored-by: Johnny Almonte <johnny243@users.noreply.github.com>
This commit is contained in:
BIN
.yarn/cache/@juggle-resize-observer-npm-3.3.1-f36d80a4f0-ddabc40442.zip
vendored
Normal file
BIN
.yarn/cache/@juggle-resize-observer-npm-3.3.1-f36d80a4f0-ddabc40442.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@react-hook-latest-npm-1.0.3-3d70c2d1bb-2408c9cd35.zip
vendored
Normal file
BIN
.yarn/cache/@react-hook-latest-npm-1.0.3-3d70c2d1bb-2408c9cd35.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@react-hook-passive-layout-effect-npm-1.2.1-ef94afc326-217cb8aa90.zip
vendored
Normal file
BIN
.yarn/cache/@react-hook-passive-layout-effect-npm-1.2.1-ef94afc326-217cb8aa90.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@react-hook-resize-observer-npm-1.2.5-31c198b62d-a2f3b333e3.zip
vendored
Normal file
BIN
.yarn/cache/@react-hook-resize-observer-npm-1.2.5-31c198b62d-a2f3b333e3.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@types-raf-schd-npm-4.0.1-f381dfea40-0babaa8554.zip
vendored
Normal file
BIN
.yarn/cache/@types-raf-schd-npm-4.0.1-f381dfea40-0babaa8554.zip
vendored
Normal file
Binary file not shown.
@@ -83,6 +83,7 @@
|
|||||||
"@reach/alert-dialog": "0.16.2",
|
"@reach/alert-dialog": "0.16.2",
|
||||||
"@reach/menu-button": "0.16.2",
|
"@reach/menu-button": "0.16.2",
|
||||||
"@reach/visually-hidden": "0.16.0",
|
"@reach/visually-hidden": "0.16.0",
|
||||||
|
"@react-hook/resize-observer": "^1.2.5",
|
||||||
"@reduxjs/toolkit": "1.8.0",
|
"@reduxjs/toolkit": "1.8.0",
|
||||||
"@standardnotes/editor-kit": "2.2.5",
|
"@standardnotes/editor-kit": "2.2.5",
|
||||||
"@standardnotes/stylekit": "5.23.0",
|
"@standardnotes/stylekit": "5.23.0",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import useResizeObserver from '@react-hook/resize-observer'
|
||||||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
|
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
|
||||||
import type { AppDispatch, RootState } from './store'
|
import type { AppDispatch, RootState } from './store'
|
||||||
|
|
||||||
@@ -16,3 +17,18 @@ export const useDidMount = (effect: React.EffectCallback, deps?: React.Dependenc
|
|||||||
}
|
}
|
||||||
}, [deps, didMount, effect])
|
}, [deps, didMount, effect])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useResize = (ref: React.RefObject<HTMLElement>, effect: (target: HTMLElement) => void) => {
|
||||||
|
const [size, setSize] = useState<DOMRect>()
|
||||||
|
|
||||||
|
function isDeepEqual(prevSize?: DOMRect, nextSize?: DOMRect) {
|
||||||
|
return JSON.stringify(prevSize) === JSON.stringify(nextSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
useResizeObserver(ref, ({ contentRect, target }) => {
|
||||||
|
if (!isDeepEqual(size, contentRect)) {
|
||||||
|
setSize(contentRect)
|
||||||
|
effect(target as HTMLElement)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const StyledTextArea = styled.textarea`
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-size: var(--sn-stylekit-font-size-h3);
|
font-size: 1rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ type ReorderIconProps = {
|
|||||||
export const ReorderIcon: React.FC<ReorderIconProps> = ({ highlight = false }) => {
|
export const ReorderIcon: React.FC<ReorderIconProps> = ({ highlight = false }) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 20 20"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
className={`sn-icon block ${highlight ? 'info' : ''}`}
|
className={`sn-icon block ${highlight ? 'info' : 'neutral'}`}
|
||||||
data-testid="reorder-icon"
|
data-testid="reorder-icon"
|
||||||
>
|
>
|
||||||
<path d="M3 15H21V13H3V15ZM3 19H21V17H3V19ZM3 11H21V9H3V11ZM3 5V7H21V5H3Z" />
|
<path d="M17 5V6.66667H3V5H17ZM3 15H17V13.3333H3V15ZM3 10.8333H17V9.16667H3V10.8333Z" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ $transition-duration: 750ms;
|
|||||||
width: 22px;
|
width: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.align-baseline {
|
||||||
|
.checkbox-button {
|
||||||
|
top: -10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.checkbox-square,
|
.checkbox-square,
|
||||||
.checkbox-mark {
|
.checkbox-mark {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ const TaskGroupContainer = styled.div<{ isLast?: boolean }>`
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: ${({ isLast }) => (!isLast ? '9px' : '0px')};
|
margin-bottom: ${({ isLast }) => (!isLast ? '9px' : '0px')};
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
type CollapsableContainerProps = {
|
type CollapsableContainerProps = {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import './TaskItem.scss'
|
import './TaskItem.scss'
|
||||||
|
|
||||||
import { ChangeEvent, createRef, KeyboardEvent, useEffect, useState } from 'react'
|
import { ChangeEvent, createRef, KeyboardEvent, useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector, useDidMount } from '../../app/hooks'
|
import { useAppDispatch, useAppSelector, useDidMount, useResize } from '../../app/hooks'
|
||||||
import { taskDeleted, TaskModel, taskModified, taskToggled } from './tasks-slice'
|
import { taskDeleted, TaskModel, taskModified, taskToggled } from './tasks-slice'
|
||||||
|
|
||||||
import { CheckBoxInput, TextAreaInput } from '../../common/components'
|
import { CheckBoxInput, TextAreaInput } from '../../common/components'
|
||||||
@@ -33,7 +33,7 @@ const Container = styled.div<{ completed?: boolean }>`
|
|||||||
`}
|
`}
|
||||||
|
|
||||||
min-width: 10%;
|
min-width: 10%;
|
||||||
max-width: 85%;
|
max-width: 90%;
|
||||||
`
|
`
|
||||||
|
|
||||||
export type TaskItemProps = {
|
export type TaskItemProps = {
|
||||||
@@ -53,22 +53,29 @@ const TaskItem: React.FC<TaskItemProps> = ({ task, groupName, innerRef, ...props
|
|||||||
const [completed, setCompleted] = useState(!!task.completed)
|
const [completed, setCompleted] = useState(!!task.completed)
|
||||||
const [description, setDescription] = useState(task.description)
|
const [description, setDescription] = useState(task.description)
|
||||||
|
|
||||||
function resizeTextArea(textarea: HTMLTextAreaElement | null): void {
|
function resizeTextArea(textarea: HTMLElement): void {
|
||||||
if (!textarea) {
|
if (!textarea) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const heightOffset = 4
|
||||||
/**
|
/**
|
||||||
* Set to 1px first to reset scroll height in case it shrunk.
|
* Set to 1px first to reset scroll height in case it shrunk.
|
||||||
*/
|
*/
|
||||||
const heightOffset = 4
|
|
||||||
textarea.style.height = '1px'
|
textarea.style.height = '1px'
|
||||||
textarea.style.height = textarea.scrollHeight - heightOffset + 'px'
|
textarea.style.height = textarea.scrollHeight - heightOffset + 'px'
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
const singleLineHeight = 20
|
||||||
resizeTextArea(textAreaRef.current)
|
const currentHeight = parseFloat(textarea.style.height)
|
||||||
})
|
|
||||||
|
if (currentHeight > singleLineHeight) {
|
||||||
|
textarea.parentElement?.classList.add('align-baseline')
|
||||||
|
textarea.parentElement?.classList.remove('align-center')
|
||||||
|
} else {
|
||||||
|
textarea.parentElement?.classList.add('align-center')
|
||||||
|
textarea.parentElement?.classList.remove('align-baseline')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onCheckBoxToggle() {
|
function onCheckBoxToggle() {
|
||||||
const newCompletedState = !completed
|
const newCompletedState = !completed
|
||||||
@@ -122,6 +129,8 @@ const TaskItem: React.FC<TaskItemProps> = ({ task, groupName, innerRef, ...props
|
|||||||
return () => clearTimeout(timeoutId)
|
return () => clearTimeout(timeoutId)
|
||||||
}, [description, groupName])
|
}, [description, groupName])
|
||||||
|
|
||||||
|
useResize(textAreaRef, resizeTextArea)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container data-testid="task-item" completed={completed} ref={innerRef} {...props}>
|
<Container data-testid="task-item" completed={completed} ref={innerRef} {...props}>
|
||||||
<CheckBoxInput testId="check-box-input" checked={completed} disabled={!canEdit} onChange={onCheckBoxToggle} />
|
<CheckBoxInput testId="check-box-input" checked={completed} disabled={!canEdit} onChange={onCheckBoxToggle} />
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const InnerTasksContainer = styled.div<{ collapsed: boolean }>`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
& > *:not(:last-child) {
|
& > *:not(:last-child) {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 7px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -94,72 +94,70 @@ const TasksSection: React.FC<TasksSectionProps> = ({ groupName, tasks, section,
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
<TransitionGroup component={null} childFactory={(child) => React.cloneElement(child)}>
|
<TransitionGroup component={null} childFactory={(child) => React.cloneElement(child)}>
|
||||||
{tasks.map((task, index) => {
|
{tasks.map((task, index) => (
|
||||||
return (
|
<CSSTransition
|
||||||
<CSSTransition
|
key={`${task.id}-${!!task.completed}`}
|
||||||
key={`${task.id}-${!!task.completed}`}
|
classNames={{
|
||||||
classNames={{
|
enter: 'fade-in',
|
||||||
enter: 'fade-in',
|
enterActive: 'fade-in',
|
||||||
enterActive: 'fade-in',
|
enterDone: 'fade-in',
|
||||||
enterDone: 'fade-in',
|
exit: 'fade-out',
|
||||||
exit: 'fade-out',
|
exitActive: 'fade-out',
|
||||||
exitActive: 'fade-out',
|
exitDone: 'fade-out',
|
||||||
exitDone: 'fade-out',
|
}}
|
||||||
}}
|
timeout={{
|
||||||
timeout={{
|
enter: 1_500,
|
||||||
enter: 1_500,
|
exit: 1_250,
|
||||||
exit: 1_250,
|
}}
|
||||||
}}
|
onEnter={(node: HTMLElement) => {
|
||||||
onEnter={(node: HTMLElement) => {
|
node.classList.remove('explode')
|
||||||
node.classList.remove('explode')
|
}}
|
||||||
}}
|
onEntered={(node: HTMLElement) => {
|
||||||
onEntered={(node: HTMLElement) => {
|
node.classList.remove('fade-in')
|
||||||
node.classList.remove('fade-in')
|
|
||||||
|
|
||||||
const completed = !!task.completed
|
const completed = !!task.completed
|
||||||
completed && node.classList.add('explode')
|
completed && node.classList.add('explode')
|
||||||
|
|
||||||
node.addEventListener(
|
node.addEventListener(
|
||||||
'animationend',
|
'animationend',
|
||||||
() => {
|
() => {
|
||||||
node.classList.remove('explode')
|
node.classList.remove('explode')
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
onExited={(node: HTMLElement) => {
|
||||||
|
node.classList.remove('fade-out')
|
||||||
|
}}
|
||||||
|
addEndListener={(node, done) => {
|
||||||
|
done()
|
||||||
|
}}
|
||||||
|
mountOnEnter
|
||||||
|
unmountOnExit
|
||||||
|
>
|
||||||
|
<Draggable
|
||||||
|
key={`draggable-${task.id}`}
|
||||||
|
draggableId={`draggable-${task.id}`}
|
||||||
|
index={index}
|
||||||
|
isDragDisabled={!canEdit}
|
||||||
|
>
|
||||||
|
{({ innerRef, draggableProps, dragHandleProps }, { isDragging }) => {
|
||||||
|
const { style, ...restDraggableProps } = draggableProps
|
||||||
|
return (
|
||||||
|
<div className="task-item" style={getItemStyle(isDragging, style)} {...restDraggableProps}>
|
||||||
|
<TaskItem
|
||||||
|
key={`task-item-${task.id}`}
|
||||||
|
task={task}
|
||||||
|
groupName={groupName}
|
||||||
|
innerRef={innerRef}
|
||||||
|
{...dragHandleProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
onExited={(node: HTMLElement) => {
|
</Draggable>
|
||||||
node.classList.remove('fade-out')
|
</CSSTransition>
|
||||||
}}
|
))}
|
||||||
addEndListener={(node, done) => {
|
|
||||||
done()
|
|
||||||
}}
|
|
||||||
mountOnEnter
|
|
||||||
unmountOnExit
|
|
||||||
>
|
|
||||||
<Draggable
|
|
||||||
key={`draggable-${task.id}`}
|
|
||||||
draggableId={`draggable-${task.id}`}
|
|
||||||
index={index}
|
|
||||||
isDragDisabled={!canEdit}
|
|
||||||
>
|
|
||||||
{({ innerRef, draggableProps, dragHandleProps }, { isDragging }) => {
|
|
||||||
const { style, ...restDraggableProps } = draggableProps
|
|
||||||
return (
|
|
||||||
<div className="task-item" style={getItemStyle(isDragging, style)} {...restDraggableProps}>
|
|
||||||
<TaskItem
|
|
||||||
key={`task-item-${task.id}`}
|
|
||||||
task={task}
|
|
||||||
groupName={groupName}
|
|
||||||
innerRef={innerRef}
|
|
||||||
{...dragHandleProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Draggable>
|
|
||||||
</CSSTransition>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</InnerTasksContainer>
|
</InnerTasksContainer>
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ import { getPlainPreview } from './common/utils'
|
|||||||
const MainContainer = styled.div`
|
const MainContainer = styled.div`
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
padding-bottom: 60px;
|
padding-bottom: 60px;
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const FloatingContainer = styled.div`
|
const FloatingContainer = styled.div`
|
||||||
|
|||||||
@@ -67,6 +67,14 @@ html {
|
|||||||
.pt-1px {
|
.pt-1px {
|
||||||
padding-top: 1px;
|
padding-top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.align-baseline {
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
48
yarn.lock
48
yarn.lock
@@ -2995,6 +2995,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@juggle/resize-observer@npm:^3.3.1":
|
||||||
|
version: 3.3.1
|
||||||
|
resolution: "@juggle/resize-observer@npm:3.3.1"
|
||||||
|
checksum: ddabc4044276a2cb57d469c4917206c7e39f2463aa8e3430e33e4eda554412afe29c22afa40e6708b49dad5d56768dc83acd68a704b1dcd49a0906bb96b991b2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@leichtgewicht/ip-codec@npm:^2.0.1":
|
"@leichtgewicht/ip-codec@npm:^2.0.1":
|
||||||
version: 2.0.4
|
version: 2.0.4
|
||||||
resolution: "@leichtgewicht/ip-codec@npm:2.0.4"
|
resolution: "@leichtgewicht/ip-codec@npm:2.0.4"
|
||||||
@@ -4425,6 +4432,39 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@react-hook/latest@npm:^1.0.2":
|
||||||
|
version: 1.0.3
|
||||||
|
resolution: "@react-hook/latest@npm:1.0.3"
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=16.8"
|
||||||
|
checksum: 2408c9cd35c5cfa7697b6da3bc5eebef254a932ade70955074c474f23be7dd3e2f81bbba12edcc9208bd0f89c6ed366d6b11d4f6d7b1052877a0bac8f74afad4
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@react-hook/passive-layout-effect@npm:^1.2.0":
|
||||||
|
version: 1.2.1
|
||||||
|
resolution: "@react-hook/passive-layout-effect@npm:1.2.1"
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=16.8"
|
||||||
|
checksum: 217cb8aa90fb8e677672319a9a466d7752890cf4357c76df000b207696e9cc717cf2ee88080671cc9dae238a82f92093ab4f61ab2f6032d2a8db958fc7d99b5d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@react-hook/resize-observer@npm:^1.2.5":
|
||||||
|
version: 1.2.5
|
||||||
|
resolution: "@react-hook/resize-observer@npm:1.2.5"
|
||||||
|
dependencies:
|
||||||
|
"@juggle/resize-observer": ^3.3.1
|
||||||
|
"@react-hook/latest": ^1.0.2
|
||||||
|
"@react-hook/passive-layout-effect": ^1.2.0
|
||||||
|
"@types/raf-schd": ^4.0.0
|
||||||
|
raf-schd: ^4.0.2
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=16.8"
|
||||||
|
checksum: a2f3b333e344adb3d7a4cfd2bc77fd75054b07063fba706d57bef0c497650c3e82038eafb64fa4148c08527e8b0a34f2f70392da1700a7a0d9676ee8ba795c16
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@react-native-community/async-storage@npm:1.12.1":
|
"@react-native-community/async-storage@npm:1.12.1":
|
||||||
version: 1.12.1
|
version: 1.12.1
|
||||||
resolution: "@react-native-community/async-storage@npm:1.12.1"
|
resolution: "@react-native-community/async-storage@npm:1.12.1"
|
||||||
@@ -4884,6 +4924,7 @@ __metadata:
|
|||||||
"@reach/alert-dialog": 0.16.2
|
"@reach/alert-dialog": 0.16.2
|
||||||
"@reach/menu-button": 0.16.2
|
"@reach/menu-button": 0.16.2
|
||||||
"@reach/visually-hidden": 0.16.0
|
"@reach/visually-hidden": 0.16.0
|
||||||
|
"@react-hook/resize-observer": ^1.2.5
|
||||||
"@reduxjs/toolkit": 1.8.0
|
"@reduxjs/toolkit": 1.8.0
|
||||||
"@standardnotes/editor-kit": 2.2.5
|
"@standardnotes/editor-kit": 2.2.5
|
||||||
"@standardnotes/stylekit": 5.23.0
|
"@standardnotes/stylekit": 5.23.0
|
||||||
@@ -7342,6 +7383,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/raf-schd@npm:^4.0.0":
|
||||||
|
version: 4.0.1
|
||||||
|
resolution: "@types/raf-schd@npm:4.0.1"
|
||||||
|
checksum: 0babaa85541aadc6e5f8aa64ec79cd68bf67ea56abb7610c8daf3ca5f4b1a75d12e4e147a0b5434938a4031650ebddc733021ec4e7db4f11f7955390ec32c917
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/range-parser@npm:*":
|
"@types/range-parser@npm:*":
|
||||||
version: 1.2.4
|
version: 1.2.4
|
||||||
resolution: "@types/range-parser@npm:1.2.4"
|
resolution: "@types/range-parser@npm:1.2.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user