grid view
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { resolveDocumentAssetUrl } from '../asset_manager';
|
||||
import { getTagColorStyle } from '../utils/colors';
|
||||
import { DownloadIcon, FolderIcon, EditIcon } from '../ui/icons';
|
||||
import { DownloadIcon, FolderIcon, EditIcon, ViewListIcon, ViewGridIcon } from '../ui/icons';
|
||||
|
||||
const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
|
||||
|
||||
@@ -74,6 +74,8 @@ const DocumentsTable = ({
|
||||
onTagClick,
|
||||
isSearchLoading = false,
|
||||
onDocumentTagDrop,
|
||||
viewMode = 'list',
|
||||
onViewModeChange,
|
||||
}) => {
|
||||
const showingSearchResults = searchResults !== null;
|
||||
const rows = showingSearchResults ? searchResults : documents;
|
||||
@@ -91,6 +93,19 @@ const DocumentsTable = ({
|
||||
[draggingDocumentIds],
|
||||
);
|
||||
const scrollRef = useRef(null);
|
||||
const isGridView = viewMode === 'grid';
|
||||
const handleSetViewMode = useCallback(
|
||||
(nextMode) => {
|
||||
if (!onViewModeChange) {
|
||||
return;
|
||||
}
|
||||
onViewModeChange(nextMode);
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = 0;
|
||||
}
|
||||
},
|
||||
[onViewModeChange],
|
||||
);
|
||||
const isTagDragEvent = useCallback((event) => {
|
||||
const types = Array.from(event.dataTransfer?.types || []);
|
||||
return TAG_MIME_TYPES.some((type) => types.includes(type));
|
||||
@@ -199,7 +214,9 @@ const DocumentsTable = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="documents-panel column">
|
||||
<section
|
||||
className={`documents-panel column documents-panel--view-${isGridView ? 'grid' : 'list'}`}
|
||||
>
|
||||
<div className="column-header">
|
||||
<div className="column-header__titles">
|
||||
<nav className="breadcrumb" aria-label="Folder breadcrumbs">
|
||||
@@ -230,6 +247,26 @@ const DocumentsTable = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
<div className="view-toggle" role="group" aria-label="Change view">
|
||||
<button
|
||||
type="button"
|
||||
className={`view-toggle__button${isGridView ? '' : ' active'}`}
|
||||
onClick={() => handleSetViewMode('list')}
|
||||
aria-pressed={!isGridView}
|
||||
title="List view"
|
||||
>
|
||||
<ViewListIcon className="view-toggle__icon" size={18} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`view-toggle__button${isGridView ? ' active' : ''}`}
|
||||
onClick={() => handleSetViewMode('grid')}
|
||||
aria-pressed={isGridView}
|
||||
title="Icons view"
|
||||
>
|
||||
<ViewGridIcon className="view-toggle__icon" size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRequestCreateFolder}
|
||||
@@ -263,12 +300,176 @@ const DocumentsTable = ({
|
||||
onDocumentListKeyDown(event);
|
||||
}
|
||||
}}
|
||||
aria-activedescendant={activeDescendantId}
|
||||
aria-activedescendant={isGridView ? undefined : activeDescendantId}
|
||||
>
|
||||
{!showingSearchResults && !subfolders.length && rows.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
Drop files anywhere or onto a folder to upload documents.
|
||||
</div>
|
||||
) : isGridView ? (
|
||||
<div className="documents-grid" role="list">
|
||||
{!showingSearchResults &&
|
||||
subfolders.map((folder) => {
|
||||
const canDragFolder = folder.id !== 'root';
|
||||
const isDraggingFolder = draggedFolderId === folder.id;
|
||||
const isSelectedFolder = selectedFolderSet.has(folder.id);
|
||||
const classes = ['document-card', 'folder-card'];
|
||||
if (isDraggingFolder) classes.push('is-dragging');
|
||||
if (isSelectedFolder) classes.push('selected');
|
||||
return (
|
||||
<div
|
||||
key={folder.id}
|
||||
className={classes.join(' ')}
|
||||
role="listitem"
|
||||
id={`folder-card-${folder.id}`}
|
||||
draggable={canDragFolder}
|
||||
onClick={(event) => {
|
||||
onFolderRowClick?.(folder.id, event);
|
||||
const shouldNavigate =
|
||||
!event.defaultPrevented &&
|
||||
!event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.shiftKey;
|
||||
if (shouldNavigate) {
|
||||
onFolderSelect(folder.id);
|
||||
}
|
||||
}}
|
||||
onDoubleClick={(event) => {
|
||||
event.preventDefault();
|
||||
onFolderSelect(folder.id);
|
||||
}}
|
||||
onDragOver={(event) => onFolderDragOver(event, folder.id)}
|
||||
onDragLeave={onFolderDragLeave}
|
||||
onDrop={(event) => onFolderDrop(event, folder.id)}
|
||||
onDragStart={(event) => {
|
||||
if (canDragFolder) {
|
||||
onFolderDragStart(event, folder.id);
|
||||
}
|
||||
}}
|
||||
onDragEnd={(event) => {
|
||||
if (canDragFolder) {
|
||||
onFolderDragEnd(event);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="folder-card__icon">
|
||||
<FolderIcon className="icon-inline" size={32} />
|
||||
</div>
|
||||
<div className="folder-card__meta">
|
||||
<div className="folder-card__name" title={folder.name}>
|
||||
{folder.name}
|
||||
</div>
|
||||
<div className="folder-card__badge">Folder</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{rows.map((doc) => {
|
||||
const isSelected = selectedSet.has(doc.id);
|
||||
const isDraggingDoc = draggingSet.has(doc.id);
|
||||
const tagList = Array.isArray(doc.tags) ? doc.tags : [];
|
||||
const visibleTags = tagList.slice(0, 3);
|
||||
const remainingTagCount = tagList.length > 3 ? tagList.length - 3 : 0;
|
||||
const cardClasses = ['document-card', 'document'];
|
||||
if (isSelected) cardClasses.push('selected');
|
||||
if (isDraggingDoc) cardClasses.push('is-dragging');
|
||||
return (
|
||||
<div
|
||||
key={doc.id}
|
||||
className={cardClasses.join(' ')}
|
||||
role="listitem"
|
||||
id={`document-card-${doc.id}`}
|
||||
onClick={(event) => onDocumentRowClick(doc.id, event)}
|
||||
onDoubleClick={() => onDocumentOpen(doc.id)}
|
||||
draggable
|
||||
onDragStart={(event) => onDocumentDragStart(event, doc)}
|
||||
onDragEnd={onDocumentDragEnd}
|
||||
onDragOver={(event) => handleDocumentTagDragOver(event)}
|
||||
onDragOverCapture={(event) => handleDocumentTagDragOver(event)}
|
||||
onDragLeave={handleDocumentTagDragLeave}
|
||||
onDragLeaveCapture={handleDocumentTagDragLeave}
|
||||
onDrop={(event) => handleDocumentTagDrop(event, doc.id)}
|
||||
onDropCapture={(event) => handleDocumentTagDrop(event, doc.id)}
|
||||
>
|
||||
<DocumentThumbnailImage
|
||||
document={doc}
|
||||
ensureAssetUrl={ensureAssetUrl}
|
||||
getDocumentAsset={getDocumentAsset}
|
||||
alt={`Thumbnail for ${doc.title || doc.original_name}`}
|
||||
/>
|
||||
<div className="document-card__meta">
|
||||
<div
|
||||
className="document-card__title"
|
||||
title={doc.title || doc.original_name}
|
||||
>
|
||||
{doc.title || doc.original_name}
|
||||
</div>
|
||||
{visibleTags.length > 0 && (
|
||||
<div className="document-card__tags">
|
||||
{visibleTags.map((tag) => {
|
||||
const colorSource = tag?.color || tagLookupById?.get(tag.id)?.color;
|
||||
const style = getTagColorStyle(colorSource);
|
||||
return (
|
||||
<span
|
||||
key={tag.id}
|
||||
className="badge tag-chip"
|
||||
style={style || undefined}
|
||||
title={tag.label}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
if (onTagClick) {
|
||||
onTagClick(tag.id);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'copyMove';
|
||||
}
|
||||
const payload = JSON.stringify({
|
||||
id: tag.id,
|
||||
label: tag.label,
|
||||
sourceDocId: doc.id,
|
||||
});
|
||||
event.dataTransfer?.setData('application/x-papercrate-tag', payload);
|
||||
event.dataTransfer?.setData('text/papercrate-tag', payload);
|
||||
event.dataTransfer?.setData('text/plain', tag.label || 'Tag');
|
||||
} catch (
|
||||
// eslint-disable-next-line no-empty
|
||||
error
|
||||
) {}
|
||||
}}
|
||||
onDragEnd={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (onTagClick) {
|
||||
onTagClick(tag.id);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tag.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{remainingTagCount > 0 && (
|
||||
<span className="badge tag-chip tag-chip--more">+{remainingTagCount}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<table aria-multiselectable="true">
|
||||
<thead>
|
||||
@@ -539,7 +740,7 @@ const DocumentsTable = ({
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
{rows.length === 0 && showingSearchResults && !isSearchLoading && (
|
||||
{rows.length === 0 && showingSearchResults && !isGridView && !isSearchLoading && (
|
||||
<div className="empty-state">No documents match the current filters.</div>
|
||||
)}
|
||||
{showingSearchResults && rows.length > 0 && (
|
||||
@@ -548,6 +749,11 @@ const DocumentsTable = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isGridView && showingSearchResults && rows.length === 0 && !isSearchLoading && (
|
||||
<div className="empty-state empty-state--global">
|
||||
No documents match the current filters.
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
+22
-1
@@ -364,6 +364,13 @@ const AppLayout = () => {
|
||||
const [currentSubfolders, setCurrentSubfolders] = useState([]);
|
||||
const [documents, setDocuments] = useState([]);
|
||||
const [workspaceMode, setWorkspaceMode] = useState('table');
|
||||
const [documentsViewMode, setDocumentsViewMode] = useState(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'list';
|
||||
}
|
||||
const stored = window.localStorage.getItem('papercrate_view_mode');
|
||||
return stored === 'grid' ? 'grid' : 'list';
|
||||
});
|
||||
const initialRowSelection = routeDocumentId
|
||||
? [resolveDocumentRowKey(routeDocumentId)]
|
||||
: [];
|
||||
@@ -2514,7 +2521,10 @@ const AppLayout = () => {
|
||||
|
||||
if (item.type === 'document') {
|
||||
const doc = item.payload;
|
||||
const rowEl = doc?.id ? document.getElementById(`document-row-${doc.id}`) : null;
|
||||
const rowEl = doc?.id
|
||||
? document.getElementById(`document-row-${doc.id}`) ||
|
||||
document.getElementById(`document-card-${doc.id}`)
|
||||
: null;
|
||||
const thumbnailEl = rowEl?.querySelector('.document-thumbnail');
|
||||
const placeholderEl = rowEl?.querySelector('.thumb-placeholder');
|
||||
|
||||
@@ -4455,6 +4465,15 @@ const AppLayout = () => {
|
||||
|
||||
const showSkeuoWorkspace = useCallback(() => setWorkspaceMode('skeuo'), [setWorkspaceMode]);
|
||||
const exitSkeuoWorkspace = useCallback(() => setWorkspaceMode('table'), [setWorkspaceMode]);
|
||||
const handleDocumentsViewModeChange = useCallback((mode) => {
|
||||
setDocumentsViewMode((previous) => {
|
||||
const next = mode === 'grid' ? 'grid' : 'list';
|
||||
if (next !== previous && typeof window !== 'undefined') {
|
||||
window.localStorage.setItem('papercrate_view_mode', next);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const documentsTableProps = {
|
||||
currentFolderName,
|
||||
@@ -4501,6 +4520,8 @@ const AppLayout = () => {
|
||||
: null,
|
||||
onTagClick: toggleTagFilter,
|
||||
onDocumentTagDrop: handleDocumentTagDrop,
|
||||
viewMode: documentsViewMode,
|
||||
onViewModeChange: handleDocumentsViewModeChange,
|
||||
};
|
||||
|
||||
const detailPanelProps = {
|
||||
|
||||
+191
-4
@@ -200,6 +200,47 @@ button.icon-button.ghost:hover:not([disabled]) {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
background: var(--surface-subtle);
|
||||
margin-right: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.view-toggle__button {
|
||||
width: 2.5rem;
|
||||
height: 2.2rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.view-toggle__button:hover,
|
||||
.view-toggle__button:focus-visible {
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.view-toggle__button.active {
|
||||
background: var(--accent-soft, rgba(63, 106, 216, 0.16));
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.view-toggle__icon {
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@@ -1132,6 +1173,10 @@ button.icon-button.ghost:hover:not([disabled]) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.documents-panel--view-grid .documents-scroll {
|
||||
padding: 0.35rem 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.documents-panel th.thumb-column,
|
||||
.documents-panel td.thumb-cell {
|
||||
width: 54px;
|
||||
@@ -1141,19 +1186,31 @@ button.icon-button.ghost:hover:not([disabled]) {
|
||||
.document-thumbnail-wrapper {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: transparent;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.documents-panel--view-grid .document-thumbnail-wrapper {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.document-thumbnail {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
background: var(--surface-subtle);
|
||||
border-radius: 1px;
|
||||
box-shadow: 0 1px 3px var(--shadow-medium);
|
||||
}
|
||||
|
||||
.documents-panel--view-grid .document-thumbnail {
|
||||
box-shadow: 0 1px 12px var(--shadow-medium);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.thumb-placeholder {
|
||||
@@ -1171,6 +1228,14 @@ button.icon-button.ghost:hover:not([disabled]) {
|
||||
box-shadow: 0 1px 3px var(--shadow-medium);
|
||||
}
|
||||
|
||||
.documents-panel--view-grid .thumb-placeholder {
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.12em;
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.thumb-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -1241,6 +1306,107 @@ button.icon-button.ghost:hover:not([disabled]) {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.documents-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
|
||||
gap: 0.2rem 1.3rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.document-card {
|
||||
padding: 0rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.document-card.selected .document-thumbnail-wrapper {
|
||||
background-color: var(--accent-soft);
|
||||
}
|
||||
|
||||
.document-card:hover .document-thumbnail-wrapper,
|
||||
.document-card:focus-visible .document-thumbnail-wrapper {
|
||||
background-color: var(--selection-ring);
|
||||
}
|
||||
|
||||
.document-card.is-dragging {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.document-card__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.document-card__title {
|
||||
color: var(--fg);
|
||||
padding: 0.2rem 0.7rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.document-card.selected .document-card__title {
|
||||
background-color: var(--accent);
|
||||
color: var(--on-accent);
|
||||
}
|
||||
|
||||
.document-card__subtitle {
|
||||
font-size: 0.78rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.document-card__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.folder-card {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.folder-card__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.folder-card__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.28rem;
|
||||
padding-top: 0.35rem;
|
||||
}
|
||||
|
||||
.folder-card__name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: var(--fg);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.folder-card__badge {
|
||||
display: inline-flex;
|
||||
align-self: center;
|
||||
padding: 0.12rem 0.45rem;
|
||||
font-size: 0.72rem;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-soft);
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.doc-name__title {
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -1260,6 +1426,15 @@ button.icon-button.ghost:hover:not([disabled]) {
|
||||
box-shadow: inset 0 0 0 2px var(--accent);
|
||||
}
|
||||
|
||||
.document-card.tag-drop-target {
|
||||
box-shadow: 0 0 0 2px var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.document-card.tag-drop-target .document-card__title {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1297,6 +1472,14 @@ button.icon-button.ghost:hover:not([disabled]) {
|
||||
|
||||
.tag-chip {
|
||||
gap: 0.25rem;
|
||||
border-radius: 1rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
}
|
||||
|
||||
.tag-chip--more {
|
||||
background: transparent;
|
||||
border-color: var(--border-strong, rgba(15, 23, 42, 0.24));
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@@ -1309,6 +1492,10 @@ button.icon-button.ghost:hover:not([disabled]) {
|
||||
background: var(--surface-subtle);
|
||||
}
|
||||
|
||||
.empty-state--global {
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
position: relative;
|
||||
padding: 1.25rem;
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
IconTagFilled,
|
||||
IconUserFilled,
|
||||
IconTrash,
|
||||
IconLayoutList,
|
||||
IconLayoutGrid,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
const composeClassName = (base, extra) => (extra ? `${base} ${extra}` : base);
|
||||
@@ -73,6 +75,24 @@ export const DownloadIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const ViewListIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconLayoutList
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ViewGridIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconLayoutGrid
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
export default {
|
||||
ChevronIcon,
|
||||
TrashIcon,
|
||||
@@ -81,4 +101,6 @@ export default {
|
||||
TagIcon,
|
||||
CorrespondentIcon,
|
||||
DownloadIcon,
|
||||
ViewListIcon,
|
||||
ViewGridIcon,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user