import filters for pixiv sidecars, tag filtering, and favicon generation
This commit is contained in:
+268
-268
@@ -1,268 +1,268 @@
|
||||
<!-- /app/templates/gallery.html -->
|
||||
{% extends "layout.html" %}
|
||||
{% block title %}Gallery{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Floating Select Button (in navbar area) -->
|
||||
<div id="floatingSelectBtn" class="floating-select-btn">
|
||||
<button id="selectModeBtn" class="btn secondary-btn">Select</button>
|
||||
</div>
|
||||
|
||||
<div class="gallery-infinite-container">
|
||||
<!-- Timeline Scrollbar (left sidebar) -->
|
||||
<aside id="timelineSidebar" class="timeline-sidebar">
|
||||
<div id="timelineTrack" class="timeline-track">
|
||||
<!-- Populated by JS with year-month markers -->
|
||||
<div class="timeline-loading">Loading...</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Gallery Content -->
|
||||
<main class="gallery-main">
|
||||
<div class="gallery-header">
|
||||
<h1>Gallery</h1>
|
||||
{% if active_tag %}
|
||||
<div class="active-filter">
|
||||
Filtered by: <span class="filter-tag">{{ active_tag }}</span>
|
||||
<a href="{{ url_for('main.gallery') }}" class="clear-filter" title="Clear filter">×</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if post_metadata %}
|
||||
<!-- Post Info Header -->
|
||||
<div class="post-info-header">
|
||||
<div class="post-info-content">
|
||||
{% if post_metadata.title %}
|
||||
<h2 class="post-title">{{ post_metadata.title }}</h2>
|
||||
{% endif %}
|
||||
<div class="post-meta">
|
||||
{% if post_metadata.artist %}
|
||||
<span class="post-artist">{{ post_metadata.artist }}</span>
|
||||
{% endif %}
|
||||
{% if post_metadata.platform %}
|
||||
<span class="post-platform post-platform-{{ post_metadata.platform }}">{{ post_metadata.platform }}</span>
|
||||
{% endif %}
|
||||
{% if post_metadata.published_at %}
|
||||
<span class="post-date">{{ post_metadata.published_at.strftime('%B %d, %Y') }}</span>
|
||||
{% endif %}
|
||||
{% if post_metadata.attachment_count > 1 %}
|
||||
<span class="post-count">{{ post_metadata.attachment_count }} images</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if post_metadata.description %}
|
||||
<div class="post-description">
|
||||
{{ post_metadata.description | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if post_metadata.source_url %}
|
||||
<a href="{{ post_metadata.source_url }}" target="_blank" rel="noopener noreferrer" class="post-source-link">
|
||||
View original post
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if series_info %}
|
||||
<!-- Series Header -->
|
||||
<div class="series-header">
|
||||
<div class="series-header-content">
|
||||
<div class="series-header-left">
|
||||
<h2 class="series-title">{{ series_info.name }}</h2>
|
||||
<span id="seriesPageCount" class="series-page-count">{{ series_info.page_count }} pages</span>
|
||||
</div>
|
||||
<div class="series-header-actions">
|
||||
{% if series_info.page_count > 0 %}
|
||||
<a href="{{ url_for('main.read_series', tag_id=series_info.tag_id) }}" class="btn primary-btn">
|
||||
Read Series
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Gallery Grid with Date Sections -->
|
||||
<div id="galleryInfinite" class="gallery-infinite">
|
||||
{% for year_month_key, year_month_label, images in grouped_images %}
|
||||
<section class="date-section" data-year-month="{{ year_month_key }}">
|
||||
<h2 class="date-divider" id="section-{{ year_month_key }}">
|
||||
<span class="date-divider-text">{{ year_month_label }}</span>
|
||||
<span class="date-divider-count">{{ images|length }} images</span>
|
||||
</h2>
|
||||
<div class="gallery-grid">
|
||||
{% for image in images %}
|
||||
{% include '_gallery_item.html' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<div class="gallery-empty">
|
||||
<p>No images found{% if active_tag %} for tag "{{ active_tag }}"{% endif %}.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Loading indicator -->
|
||||
<div id="galleryLoading" class="gallery-loading" style="display:none;">
|
||||
<span class="loading-spinner"></span>
|
||||
<span>Loading more images...</span>
|
||||
</div>
|
||||
|
||||
<!-- End of content marker -->
|
||||
<div id="galleryEnd" class="gallery-end" style="display:none;">
|
||||
No more images
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
{% include '_gallery_modal.html' %}
|
||||
|
||||
<!-- Bulk Editor Panel -->
|
||||
<aside id="bulkEditorPanel" class="bulk-editor-panel">
|
||||
<div class="bulk-editor-header">
|
||||
<h3>Bulk Edit</h3>
|
||||
<button id="closeBulkEditor" class="modal-close-btn">×</button>
|
||||
</div>
|
||||
<div class="bulk-editor-stats">
|
||||
<span id="selectedCount">0</span> images selected
|
||||
</div>
|
||||
<div class="bulk-editor-section">
|
||||
<h4>Add Tags</h4>
|
||||
<form id="bulkAddTagForm" class="tag-form" autocomplete="off">
|
||||
<div class="tag-form-wrapper">
|
||||
<input type="text" id="bulkTagInput" name="name" placeholder="Add tag..." autocomplete="off">
|
||||
<div id="bulkTagAutocomplete" class="tag-autocomplete"></div>
|
||||
</div>
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="bulk-editor-section">
|
||||
<h4>Remove Tags</h4>
|
||||
<p class="form-help">Common tags across selected images:</p>
|
||||
<div id="commonTagsList" class="tags"></div>
|
||||
</div>
|
||||
{% if series_info %}
|
||||
<div class="bulk-editor-section bulk-editor-series">
|
||||
<h4>📚 Add to Series</h4>
|
||||
<p class="form-help">Add selected images as pages to "{{ series_info.name }}"</p>
|
||||
<div class="series-editor-controls">
|
||||
<div class="series-editor-input">
|
||||
<label for="startPageInput">Starting page:</label>
|
||||
<input type="number" id="startPageInput" value="{{ series_info.page_count + 1 }}" min="1" class="form-input">
|
||||
</div>
|
||||
<button id="bulkAddToSeriesBtn" class="btn primary-btn" disabled>
|
||||
Add to Series
|
||||
</button>
|
||||
</div>
|
||||
<div id="seriesEditorFeedback" class="series-editor-feedback"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="bulk-editor-actions">
|
||||
<button id="clearSelectionBtn" class="btn secondary-btn">Clear Selection</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Pass initial state to JS -->
|
||||
<script>
|
||||
window.galleryState = {
|
||||
cursor: {{ initial_cursor | tojson | safe if initial_cursor else 'null' }},
|
||||
hasMore: {{ 'true' if has_more else 'false' }},
|
||||
activeTag: {{ active_tag | tojson | safe if active_tag else 'null' }},
|
||||
seriesInfo: {{ series_info | tojson | safe if series_info else 'null' }}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/gallery-infinite.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/view-modal.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/bulk-select.js') }}"></script>
|
||||
|
||||
{% if series_info %}
|
||||
<script>
|
||||
// Series Editor functionality (integrated into bulk editor panel)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const startPageInput = document.getElementById('startPageInput');
|
||||
const bulkAddToSeriesBtn = document.getElementById('bulkAddToSeriesBtn');
|
||||
const feedbackEl = document.getElementById('seriesEditorFeedback');
|
||||
const seriesTagId = {{ series_info.tag_id }};
|
||||
|
||||
// Update button state based on selection
|
||||
function updateAddButtonState() {
|
||||
const selectedCount = window.selectedImages ? window.selectedImages.size : 0;
|
||||
bulkAddToSeriesBtn.disabled = selectedCount === 0;
|
||||
bulkAddToSeriesBtn.textContent = selectedCount > 0
|
||||
? `Add ${selectedCount} to Series`
|
||||
: 'Add to Series';
|
||||
}
|
||||
|
||||
// Listen for selection changes
|
||||
document.addEventListener('selectionChanged', updateAddButtonState);
|
||||
|
||||
// Add selected images to series
|
||||
bulkAddToSeriesBtn.addEventListener('click', async () => {
|
||||
if (!window.selectedImages || window.selectedImages.size === 0) return;
|
||||
|
||||
// Use selectionOrder (click order) for page ordering
|
||||
const imageIds = window.selectionOrder ? [...window.selectionOrder] : Array.from(window.selectedImages);
|
||||
const startPage = parseInt(startPageInput.value) || 1;
|
||||
|
||||
bulkAddToSeriesBtn.disabled = true;
|
||||
bulkAddToSeriesBtn.textContent = 'Adding...';
|
||||
feedbackEl.innerHTML = '';
|
||||
|
||||
try {
|
||||
const r = await fetch(`/api/series/${seriesTagId}/pages/bulk-add`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ image_ids: imageIds, start_page: startPage })
|
||||
});
|
||||
const data = await r.json();
|
||||
|
||||
if (data.ok) {
|
||||
let msg = `Added ${data.added.length} image(s) to series.`;
|
||||
if (data.skipped.length > 0) {
|
||||
msg += ` Skipped ${data.skipped.length}.`;
|
||||
}
|
||||
feedbackEl.innerHTML = `<div class="feedback-success">${msg}</div>`;
|
||||
|
||||
// Update the starting page input to the next available page
|
||||
if (data.added.length > 0) {
|
||||
const lastPage = data.added[data.added.length - 1].page_number;
|
||||
startPageInput.value = lastPage + 1;
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
if (window.clearSelection) {
|
||||
window.clearSelection();
|
||||
}
|
||||
|
||||
// Update page count in header
|
||||
const pageCountEl = document.getElementById('seriesPageCount');
|
||||
if (pageCountEl) {
|
||||
const currentCount = parseInt(pageCountEl.textContent) || 0;
|
||||
pageCountEl.textContent = `${currentCount + data.added.length} pages`;
|
||||
}
|
||||
|
||||
// Show "Read Series" button if it was hidden (first pages added)
|
||||
const readBtn = document.querySelector('.series-header-actions .primary-btn');
|
||||
if (!readBtn && data.added.length > 0) {
|
||||
location.reload(); // Simplest way to update the UI
|
||||
}
|
||||
} else {
|
||||
feedbackEl.innerHTML = `<div class="feedback-error">${data.error}</div>`;
|
||||
}
|
||||
} catch (e) {
|
||||
feedbackEl.innerHTML = `<div class="feedback-error">Error: ${e.message}</div>`;
|
||||
} finally {
|
||||
updateAddButtonState();
|
||||
}
|
||||
});
|
||||
|
||||
// Initial state
|
||||
updateAddButtonState();
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
<!-- /app/templates/gallery.html -->
|
||||
{% extends "layout.html" %}
|
||||
{% block title %}Gallery{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Floating Select Button (in navbar area) -->
|
||||
<div id="floatingSelectBtn" class="floating-select-btn">
|
||||
<button id="selectModeBtn" class="btn secondary-btn">Select</button>
|
||||
</div>
|
||||
|
||||
<div class="gallery-infinite-container">
|
||||
<!-- Timeline Scrollbar (left sidebar) -->
|
||||
<aside id="timelineSidebar" class="timeline-sidebar">
|
||||
<div id="timelineTrack" class="timeline-track">
|
||||
<!-- Populated by JS with year-month markers -->
|
||||
<div class="timeline-loading">Loading...</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Gallery Content -->
|
||||
<main class="gallery-main">
|
||||
<div class="gallery-header">
|
||||
<h1>Gallery</h1>
|
||||
{% if active_tag %}
|
||||
<div class="active-filter">
|
||||
Filtered by: <span class="filter-tag">{{ active_tag }}</span>
|
||||
<a href="{{ url_for('main.gallery') }}" class="clear-filter" title="Clear filter">×</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if post_metadata %}
|
||||
<!-- Post Info Header -->
|
||||
<div class="post-info-header">
|
||||
<div class="post-info-content">
|
||||
{% if post_metadata.title %}
|
||||
<h2 class="post-title">{{ post_metadata.title }}</h2>
|
||||
{% endif %}
|
||||
<div class="post-meta">
|
||||
{% if post_metadata.artist %}
|
||||
<span class="post-artist">{{ post_metadata.artist }}</span>
|
||||
{% endif %}
|
||||
{% if post_metadata.platform %}
|
||||
<span class="post-platform post-platform-{{ post_metadata.platform }}">{{ post_metadata.platform }}</span>
|
||||
{% endif %}
|
||||
{% if post_metadata.published_at %}
|
||||
<span class="post-date">{{ post_metadata.published_at.strftime('%B %d, %Y') }}</span>
|
||||
{% endif %}
|
||||
{% if post_metadata.attachment_count > 1 %}
|
||||
<span class="post-count">{{ post_metadata.attachment_count }} images</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if post_metadata.description %}
|
||||
<div class="post-description">
|
||||
{{ post_metadata.description | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if post_metadata.source_url %}
|
||||
<a href="{{ post_metadata.source_url }}" target="_blank" rel="noopener noreferrer" class="post-source-link">
|
||||
View original post
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if series_info %}
|
||||
<!-- Series Header -->
|
||||
<div class="series-header">
|
||||
<div class="series-header-content">
|
||||
<div class="series-header-left">
|
||||
<h2 class="series-title">{{ series_info.name }}</h2>
|
||||
<span id="seriesPageCount" class="series-page-count">{{ series_info.page_count }} pages</span>
|
||||
</div>
|
||||
<div class="series-header-actions">
|
||||
{% if series_info.page_count > 0 %}
|
||||
<a href="{{ url_for('main.read_series', tag_id=series_info.tag_id) }}" class="btn primary-btn">
|
||||
Read Series
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Gallery Grid with Date Sections -->
|
||||
<div id="galleryInfinite" class="gallery-infinite">
|
||||
{% for year_month_key, year_month_label, images in grouped_images %}
|
||||
<section class="date-section" data-year-month="{{ year_month_key }}">
|
||||
<h2 class="date-divider" id="section-{{ year_month_key }}">
|
||||
<span class="date-divider-text">{{ year_month_label }}</span>
|
||||
<span class="date-divider-count">{{ images|length }} images</span>
|
||||
</h2>
|
||||
<div class="gallery-grid">
|
||||
{% for image in images %}
|
||||
{% include '_gallery_item.html' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<div class="gallery-empty">
|
||||
<p>No images found{% if active_tag %} for tag "{{ active_tag }}"{% endif %}.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Loading indicator -->
|
||||
<div id="galleryLoading" class="gallery-loading" style="display:none;">
|
||||
<span class="loading-spinner"></span>
|
||||
<span>Loading more images...</span>
|
||||
</div>
|
||||
|
||||
<!-- End of content marker -->
|
||||
<div id="galleryEnd" class="gallery-end" style="display:none;">
|
||||
No more images
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
{% include '_gallery_modal.html' %}
|
||||
|
||||
<!-- Bulk Editor Panel -->
|
||||
<aside id="bulkEditorPanel" class="bulk-editor-panel">
|
||||
<div class="bulk-editor-header">
|
||||
<h3>Bulk Edit</h3>
|
||||
<button id="closeBulkEditor" class="modal-close-btn">×</button>
|
||||
</div>
|
||||
<div class="bulk-editor-stats">
|
||||
<span id="selectedCount">0</span> images selected
|
||||
</div>
|
||||
<div class="bulk-editor-section">
|
||||
<h4>Add Tags</h4>
|
||||
<form id="bulkAddTagForm" class="tag-form" autocomplete="off">
|
||||
<div class="tag-form-wrapper">
|
||||
<input type="text" id="bulkTagInput" name="name" placeholder="Add tag..." autocomplete="off">
|
||||
<div id="bulkTagAutocomplete" class="tag-autocomplete"></div>
|
||||
</div>
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="bulk-editor-section">
|
||||
<h4>Remove Tags</h4>
|
||||
<p class="form-help">Common tags across selected images:</p>
|
||||
<div id="commonTagsList" class="tags"></div>
|
||||
</div>
|
||||
{% if series_info %}
|
||||
<div class="bulk-editor-section bulk-editor-series">
|
||||
<h4>📚 Add to Series</h4>
|
||||
<p class="form-help">Add selected images as pages to "{{ series_info.name }}"</p>
|
||||
<div class="series-editor-controls">
|
||||
<div class="series-editor-input">
|
||||
<label for="startPageInput">Starting page:</label>
|
||||
<input type="number" id="startPageInput" value="{{ series_info.page_count + 1 }}" min="1" class="form-input">
|
||||
</div>
|
||||
<button id="bulkAddToSeriesBtn" class="btn primary-btn" disabled>
|
||||
Add to Series
|
||||
</button>
|
||||
</div>
|
||||
<div id="seriesEditorFeedback" class="series-editor-feedback"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="bulk-editor-actions">
|
||||
<button id="clearSelectionBtn" class="btn secondary-btn">Clear Selection</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Pass initial state to JS -->
|
||||
<script>
|
||||
window.galleryState = {
|
||||
cursor: {{ initial_cursor | tojson | safe if initial_cursor else 'null' }},
|
||||
hasMore: {{ 'true' if has_more else 'false' }},
|
||||
activeTag: {{ active_tag | tojson | safe if active_tag else 'null' }},
|
||||
seriesInfo: {{ series_info | tojson | safe if series_info else 'null' }}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/gallery-infinite.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/view-modal.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/bulk-select.js') }}"></script>
|
||||
|
||||
{% if series_info %}
|
||||
<script>
|
||||
// Series Editor functionality (integrated into bulk editor panel)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const startPageInput = document.getElementById('startPageInput');
|
||||
const bulkAddToSeriesBtn = document.getElementById('bulkAddToSeriesBtn');
|
||||
const feedbackEl = document.getElementById('seriesEditorFeedback');
|
||||
const seriesTagId = {{ series_info.tag_id }};
|
||||
|
||||
// Update button state based on selection
|
||||
function updateAddButtonState() {
|
||||
const selectedCount = window.selectedImages ? window.selectedImages.size : 0;
|
||||
bulkAddToSeriesBtn.disabled = selectedCount === 0;
|
||||
bulkAddToSeriesBtn.textContent = selectedCount > 0
|
||||
? `Add ${selectedCount} to Series`
|
||||
: 'Add to Series';
|
||||
}
|
||||
|
||||
// Listen for selection changes
|
||||
document.addEventListener('selectionChanged', updateAddButtonState);
|
||||
|
||||
// Add selected images to series
|
||||
bulkAddToSeriesBtn.addEventListener('click', async () => {
|
||||
if (!window.selectedImages || window.selectedImages.size === 0) return;
|
||||
|
||||
// Use selectionOrder (click order) for page ordering
|
||||
const imageIds = window.selectionOrder ? [...window.selectionOrder] : Array.from(window.selectedImages);
|
||||
const startPage = parseInt(startPageInput.value) || 1;
|
||||
|
||||
bulkAddToSeriesBtn.disabled = true;
|
||||
bulkAddToSeriesBtn.textContent = 'Adding...';
|
||||
feedbackEl.innerHTML = '';
|
||||
|
||||
try {
|
||||
const r = await fetch(`/api/series/${seriesTagId}/pages/bulk-add`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ image_ids: imageIds, start_page: startPage })
|
||||
});
|
||||
const data = await r.json();
|
||||
|
||||
if (data.ok) {
|
||||
let msg = `Added ${data.added.length} image(s) to series.`;
|
||||
if (data.skipped.length > 0) {
|
||||
msg += ` Skipped ${data.skipped.length}.`;
|
||||
}
|
||||
feedbackEl.innerHTML = `<div class="feedback-success">${msg}</div>`;
|
||||
|
||||
// Update the starting page input to the next available page
|
||||
if (data.added.length > 0) {
|
||||
const lastPage = data.added[data.added.length - 1].page_number;
|
||||
startPageInput.value = lastPage + 1;
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
if (window.clearSelection) {
|
||||
window.clearSelection();
|
||||
}
|
||||
|
||||
// Update page count in header
|
||||
const pageCountEl = document.getElementById('seriesPageCount');
|
||||
if (pageCountEl) {
|
||||
const currentCount = parseInt(pageCountEl.textContent) || 0;
|
||||
pageCountEl.textContent = `${currentCount + data.added.length} pages`;
|
||||
}
|
||||
|
||||
// Show "Read Series" button if it was hidden (first pages added)
|
||||
const readBtn = document.querySelector('.series-header-actions .primary-btn');
|
||||
if (!readBtn && data.added.length > 0) {
|
||||
location.reload(); // Simplest way to update the UI
|
||||
}
|
||||
} else {
|
||||
feedbackEl.innerHTML = `<div class="feedback-error">${data.error}</div>`;
|
||||
}
|
||||
} catch (e) {
|
||||
feedbackEl.innerHTML = `<div class="feedback-error">Error: ${e.message}</div>`;
|
||||
} finally {
|
||||
updateAddButtonState();
|
||||
}
|
||||
});
|
||||
|
||||
// Initial state
|
||||
updateAddButtonState();
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user