mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Getting Ready for Release (#1180)
* One more unit test for Tachiyomi * Removed some debug code in the manga reader menu * Fixed a typeahead bug where using Enter on add new item or selected options could cause items to disappear from selected state or other visual glitches * Actually fix the selection issue. We needed to filter out selected before we access element * Cleaned up collection detail page to align to new side nav design * Cleaned up some styling on the reading list page * Fixed a bug where side nav would not be visible on the main app due to some weird redirect logic * Fixed a bug where when paging to the last page, a page will be skipped and user will have to refresh manually to view * Fixed some styling bugs on drawer for light themes. Added missing pagination colors on light themes * On mobile screens, add some padding on series-detail page * Fixed a bad test case helper
This commit is contained in:
parent
f55dbc0a2a
commit
d639360e3c
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
@ -25,12 +26,14 @@ namespace API.Tests.Helpers
|
||||
|
||||
public static Volume CreateVolume(string volumeNumber, List<Chapter> chapters = null)
|
||||
{
|
||||
var chaps = chapters ?? new List<Chapter>();
|
||||
var pages = chaps.Count > 0 ? chaps.Max(c => c.Pages) : 0;
|
||||
return new Volume()
|
||||
{
|
||||
Name = volumeNumber,
|
||||
Number = (int) API.Parser.Parser.MinimumNumberFromRange(volumeNumber),
|
||||
Pages = 0,
|
||||
Chapters = chapters ?? new List<Chapter>()
|
||||
Pages = pages,
|
||||
Chapters = chaps
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1353,6 +1353,84 @@ public class ReaderServiceTests
|
||||
Assert.Equal("22", nextChapter.Range);
|
||||
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetContinuePoint_ShouldReturnFirstNonSpecial2()
|
||||
{
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
// Loose chapters
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("45", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("46", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("47", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("48", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("Some Special Title", true, new List<MangaFile>(), 1),
|
||||
}),
|
||||
|
||||
// One file volume
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>(), 1), // Read
|
||||
}),
|
||||
// Chapter-based volume
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("21", false, new List<MangaFile>(), 1), // Read
|
||||
EntityFactory.CreateChapter("22", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
// Chapter-based volume
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("32", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
}
|
||||
});
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
UserName = "majora2007"
|
||||
});
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>());
|
||||
|
||||
// Save progress on first volume and 1st chapter of second volume
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 6, // Chapter 0 volume 1 id
|
||||
SeriesId = 1,
|
||||
VolumeId = 2 // Volume 1 id
|
||||
}, 1);
|
||||
|
||||
|
||||
await readerService.SaveReadingProgress(new ProgressDto()
|
||||
{
|
||||
PageNum = 1,
|
||||
ChapterId = 7, // Chapter 21 volume 2 id
|
||||
SeriesId = 1,
|
||||
VolumeId = 3 // Volume 2 id
|
||||
}, 1);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||
|
||||
Assert.Equal("22", nextChapter.Range);
|
||||
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -5,11 +5,8 @@
|
||||
<app-drawer #commentDrawer="drawer" [isOpen]="drawerOpen" [style.--drawer-width]="'300px'" [options]="{topOffset: topOffset}" [style.--drawer-background-color]="drawerBackgroundColor" (drawerClosed)="closeDrawer()">
|
||||
<div header>
|
||||
<h2 style="margin-top: 0.5rem">Book Settings
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="commentDrawer.close()">
|
||||
|
||||
</button>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="commentDrawer.close()"></button>
|
||||
</h2>
|
||||
|
||||
</div>
|
||||
<div body class="drawer-body">
|
||||
<div class="control-container">
|
||||
|
@ -782,13 +782,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
setPageNum(pageNum: number) {
|
||||
if (pageNum < 0) {
|
||||
this.pageNum = 0;
|
||||
} else if (pageNum >= this.maxPages - 1) { // This case handles when we are using the pager to move to the next volume/chapter, the pageNum will get incremented past maxPages // NOTE: I made a change where I removed - 1 in comparison, it's breaking page progress
|
||||
this.pageNum = this.maxPages; //
|
||||
} else {
|
||||
this.pageNum = pageNum;
|
||||
}
|
||||
this.pageNum = Math.max(Math.min(pageNum, this.maxPages), 0);
|
||||
}
|
||||
|
||||
goBack() {
|
||||
|
@ -219,10 +219,10 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
||||
return {id: 0, title: title, promoted: false, coverImage: '', summary: '', coverImageLocked: false };
|
||||
});
|
||||
this.collectionTagSettings.compareFn = (options: CollectionTag[], filter: string) => {
|
||||
console.log('compareFN:')
|
||||
console.log('options: ', options);
|
||||
console.log('filter: ', filter);
|
||||
console.log('results: ', options.filter(m => this.utilityService.filter(m.title, filter)));
|
||||
// console.log('compareFN:')
|
||||
// console.log('options: ', options);
|
||||
// console.log('filter: ', filter);
|
||||
// console.log('results: ', options.filter(m => this.utilityService.filter(m.title, filter)));
|
||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||
}
|
||||
this.collectionTagSettings.selectionCompareFn = (a: CollectionTag, b: CollectionTag) => {
|
||||
|
@ -1,26 +1,18 @@
|
||||
<app-side-nav-companion-bar *ngIf="series !== undefined">
|
||||
<ng-container title>
|
||||
<h2 style="margin-bottom: 0px">
|
||||
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="collectionTagActions" [labelBy]="collectionTag.title" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
{{collectionTag.title}}
|
||||
</h2>
|
||||
</ng-container>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid" *ngIf="collectionTag !== undefined" style="padding-top: 10px">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6 poster">
|
||||
<app-image maxWidth="481px" [imageUrl]="tagImage"></app-image>
|
||||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6">
|
||||
<div class="row g-0">
|
||||
<h2>
|
||||
{{collectionTag.title}}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="row g-0 mt-2 mb-2">
|
||||
<div class="ms-2" *ngIf="isAdmin">
|
||||
<button class="btn btn-secondary" (click)="openEditCollectionTagModal(collectionTag)" title="Edit Series information">
|
||||
<span>
|
||||
<i class="fa fa-pen" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
@ -42,6 +42,9 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
filterSettings: FilterSettings = new FilterSettings();
|
||||
summary: string = '';
|
||||
|
||||
actionInProgress: boolean = false;
|
||||
|
||||
|
||||
private onDestory: Subject<void> = new Subject<void>();
|
||||
|
||||
bulkActionCallback = (action: Action, data: any) => {
|
||||
@ -197,6 +200,12 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, this.collectionTag);
|
||||
}
|
||||
}
|
||||
|
||||
openEditCollectionTagModal(collectionTag: CollectionTag) {
|
||||
const modalRef = this.modalService.open(EditCollectionTagsComponent, { size: 'lg', scrollable: true });
|
||||
modalRef.componentInstance.tag = this.collectionTag;
|
||||
|
@ -19,7 +19,7 @@
|
||||
<span class="visually-hidden">Keyboard Shortcuts Modal</span>
|
||||
</button>
|
||||
<!-- {{this.pageNum}} -->
|
||||
{{readerService.imageUrlToPageNum(canvasImage.src)}}<ng-container *ngIf="ShouldRenderDoublePage && (this.pageNum + 1 <= maxPages - 1 && this.pageNum > 0)"> - {{PageNumber + 1}}</ng-container>
|
||||
<!-- {{readerService.imageUrlToPageNum(canvasImage.src)}}<ng-container *ngIf="ShouldRenderDoublePage && (this.pageNum + 1 <= maxPages - 1 && this.pageNum > 0)"> - {{PageNumber + 1}}</ng-container> -->
|
||||
|
||||
<button class="btn btn-icon btn-small" role="checkbox" [attr.aria-checked]="isCurrentPageBookmarked" title="{{isCurrentPageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}" (click)="bookmarkPage()"><i class="{{isCurrentPageBookmarked ? 'fa' : 'far'}} fa-bookmark" aria-hidden="true"></i><span class="visually-hidden">{{isCurrentPageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}</span></button>
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@
|
||||
</div>
|
||||
|
||||
<div class="not-phone-hidden">
|
||||
<app-drawer #commentDrawer="drawer" [isOpen]="!filteringCollapsed" [style.--drawer-width]="'300px'" [style.--drawer-background-color]="'#010409'" [options]="{topOffset: 56}" (drawerClosed)="filteringCollapsed = !filteringCollapsed">
|
||||
<app-drawer #commentDrawer="drawer" [isOpen]="!filteringCollapsed" [style.--drawer-width]="'300px'" [options]="{topOffset: 56}" (drawerClosed)="filteringCollapsed = !filteringCollapsed">
|
||||
<div header>
|
||||
<h2 style="margin-top: 0.5rem">Book Settings
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="commentDrawer.close()"></button>
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
<div class="align-middle" style="padding-top: 40px">
|
||||
<label for="reorder-{{i}}" class="form-label visually-hidden">Reorder</label>
|
||||
<input *ngIf="accessibilityMode" id="reorder-{{i}}" type="number" min="0" [max]="items.length - 1" [value]="i" style="width: 40px" (focusout)="updateIndex(i, item)" (keydown.enter)="updateIndex(i, item)" aria-describedby="instructions">
|
||||
<input *ngIf="accessibilityMode" id="reorder-{{i}}" class="form-control" type="number" min="0" [max]="items.length - 1" [value]="i" style="width: 60px" (focusout)="updateIndex(i, item)" (keydown.enter)="updateIndex(i, item)" aria-describedby="instructions">
|
||||
</div>
|
||||
<button class="btn btn-icon pull-right" (click)="removeItem(item, i)">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
@ -19,5 +19,5 @@
|
||||
</div>
|
||||
|
||||
<p class="visually-hidden" id="instructions">
|
||||
|
||||
When you put a number in the reorder input, the item will be inserted at that location and all other items will have their order updated.
|
||||
</p>
|
@ -14,7 +14,7 @@
|
||||
<div class="col-md-2 col-xs-4 col-sm-6">
|
||||
<app-image class="poster" maxWidth="300px" [imageUrl]="seriesImage"></app-image>
|
||||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6">
|
||||
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
|
||||
<div class="row g-0">
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-primary" (click)="read()">
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div
|
||||
class="drawer-container"
|
||||
[ngStyle]="{'top': options.topOffset + 'px'}"
|
||||
[ngStyle]="{'top': options.topOffset + 'px', 'padding-bottom': options.topOffset + 'px'}"
|
||||
[class.is-open]="isOpen"
|
||||
[class.position-right]="position === 'right'"
|
||||
[class.position-left]="position === 'left'"
|
||||
|
@ -2,7 +2,7 @@
|
||||
--drawer-height: 100vh;
|
||||
--drawer-width: 400px;
|
||||
--drawer-top-offset: 0px;
|
||||
--drawer-background-color: '#fff';
|
||||
//--drawer-background-color: #fff;
|
||||
}
|
||||
|
||||
.drawer-container {
|
||||
@ -11,7 +11,7 @@
|
||||
right: 0;
|
||||
width: var(--drawer-width);
|
||||
height: 100vh;
|
||||
background: var(--drawer-background-color);
|
||||
background: var(--drawer-background-color, #fff);
|
||||
transition: all 300ms;
|
||||
box-shadow: 0 6px 4px 2px rgb(0 0 0 / 70%);
|
||||
padding: 10px 10px;
|
||||
|
@ -1,3 +1,9 @@
|
||||
.hide-if-empty:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::ng-deep .companion-bar {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
@ -80,6 +80,7 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--side-nav-hover-color);
|
||||
background-color: var(--side-nav-hover-bg-color);
|
||||
}
|
||||
}
|
||||
@ -136,71 +137,4 @@ a {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// @media (max-width: 576px) {
|
||||
// .side-nav-item {
|
||||
// align-items: center;
|
||||
// display: flex;
|
||||
// justify-content: space-between;
|
||||
// padding: 15px 10px;
|
||||
// width: 100%;
|
||||
// height: 70px;
|
||||
// min-height: 40px;
|
||||
// overflow: hidden;
|
||||
// font-size: 1rem;
|
||||
|
||||
// cursor: pointer;
|
||||
|
||||
// .side-nav-text {
|
||||
// padding-left: 10px;
|
||||
// opacity: 1;
|
||||
// min-width: 100px;
|
||||
// width: 100%;
|
||||
|
||||
// div {
|
||||
// min-width: 102px;
|
||||
// width: 100%
|
||||
// }
|
||||
// }
|
||||
|
||||
// .card-actions {
|
||||
// opacity: 1;
|
||||
// }
|
||||
|
||||
// div {
|
||||
// align-items: center;
|
||||
// display: flex;
|
||||
// height: 100%;
|
||||
// justify-content: inherit;
|
||||
// min-width: 30px;
|
||||
// width: 100%;
|
||||
// padding-left: 5px;
|
||||
|
||||
// i {
|
||||
// font-size: 2rem;
|
||||
// width: 50px;
|
||||
// }
|
||||
// }
|
||||
|
||||
// &.closed {
|
||||
// .side-nav-text {
|
||||
// opacity: 0;
|
||||
// }
|
||||
|
||||
// .card-actions {
|
||||
// opacity: 0;
|
||||
// font-size: inherit
|
||||
// }
|
||||
// }
|
||||
|
||||
// span {
|
||||
// &:last-child {
|
||||
// flex-grow: 1;
|
||||
// justify-content: end;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
@ -223,6 +223,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
switchMap(val => {
|
||||
this.isLoadingOptions = true;
|
||||
let results: Observable<any[]>;
|
||||
console.log('val: ', val);
|
||||
if (Array.isArray(this.settings.fetchFn)) {
|
||||
const filteredArray = this.settings.compareFn(this.settings.fetchFn, val.trim());
|
||||
results = of(filteredArray).pipe(takeUntil(this.onDestroy), map((items: any[]) => items.filter(item => this.filterSelected(item))));
|
||||
@ -232,14 +233,14 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
|
||||
return results;
|
||||
}),
|
||||
tap((val) => {
|
||||
tap((filteredOptions) => {
|
||||
this.isLoadingOptions = false;
|
||||
this.focusedIndex = 0;
|
||||
this.updateShowAddItem(val);
|
||||
// setTimeout(() => {
|
||||
// this.updateShowAddItem(val);
|
||||
// this.updateHighlight();
|
||||
// }, 10);
|
||||
//this.updateShowAddItem(filteredOptions);
|
||||
setTimeout(() => {
|
||||
this.updateShowAddItem(filteredOptions);
|
||||
this.updateHighlight();
|
||||
}, 10);
|
||||
setTimeout(() => this.updateHighlight(), 20);
|
||||
}),
|
||||
shareReplay(),
|
||||
@ -296,22 +297,29 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
{
|
||||
this.document.querySelectorAll('.list-group-item').forEach((item, index) => {
|
||||
if (item.classList.contains('active')) {
|
||||
this.filteredOptions.pipe(take(1)).subscribe((res: any[]) => {
|
||||
this.filteredOptions.pipe(take(1)).subscribe((opts: any[]) => {
|
||||
// This isn't giving back the filtered array, but everything
|
||||
|
||||
console.log(item.classList.contains('add-item'));
|
||||
if (this.settings.addIfNonExisting && item.classList.contains('add-item')) {
|
||||
this.addNewItem(this.typeaheadControl.value);
|
||||
this.resetField();
|
||||
this.focusedIndex = 0;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = this.settings.compareFn(res, (this.typeaheadControl.value || '').trim());
|
||||
if (result.length === 1) {
|
||||
this.toggleSelection(result[0]);
|
||||
this.resetField();
|
||||
this.focusedIndex = 0;
|
||||
}
|
||||
|
||||
const filteredResults = opts.filter(item => this.filterSelected(item));
|
||||
|
||||
if (filteredResults.length < this.focusedIndex) return;
|
||||
const option = filteredResults[this.focusedIndex];
|
||||
|
||||
this.toggleSelection(option);
|
||||
this.resetField();
|
||||
this.focusedIndex = 0;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -445,6 +453,10 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
||||
&& this.typeaheadControl.value.trim().length >= Math.max(this.settings.minCharacters, 1)
|
||||
&& this.typeaheadControl.dirty
|
||||
&& (typeof this.settings.compareFn == 'function' && this.settings.compareFn(options, this.typeaheadControl.value.trim()).length === 0);
|
||||
|
||||
if (this.showAddItem) {
|
||||
this.hasFocus = true;
|
||||
}
|
||||
console.log('show Add item: ', this.showAddItem);
|
||||
console.log('compare func: ', this.settings.compareFn(options, this.typeaheadControl.value.trim()));
|
||||
|
||||
|
@ -44,6 +44,7 @@ export class UserLoginComponent implements OnInit {
|
||||
this.navService.hideSideNav();
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.navService.showSideNav();
|
||||
this.router.navigateByUrl('/library');
|
||||
}
|
||||
});
|
||||
|
@ -208,4 +208,7 @@
|
||||
--carousel-header-text-color: var(--body-text-color);
|
||||
--carousel-header-text-decoration: none;
|
||||
--carousel-hover-header-text-decoration: none;
|
||||
|
||||
/** Drawer */
|
||||
--drawer-background-color: black;
|
||||
}
|
||||
|
@ -143,5 +143,18 @@
|
||||
--carousel-header-text-decoration: none;
|
||||
--carousel-hover-header-text-decoration: none;
|
||||
|
||||
/** Drawer */
|
||||
--drawer-background-color: white;
|
||||
|
||||
/* Pagination */
|
||||
--pagination-active-link-border-color: var(--primary-color);
|
||||
--pagination-active-link-bg-color: var(--primary-color);
|
||||
--pagination-active-link-text-color: white;
|
||||
--pagination-link-border-color: rgba(239, 239, 239, 1);
|
||||
--pagination-link-text-color: black;
|
||||
--pagination-link-bg-color: white;
|
||||
--pagination-focus-border-color: var(--primary-color);
|
||||
--pagination-link-hover-color: var(--primary-color);
|
||||
|
||||
|
||||
}
|
@ -135,4 +135,17 @@
|
||||
--carousel-header-text-color: black;
|
||||
--carousel-header-text-decoration: none;
|
||||
--carousel-hover-header-text-decoration: underline;
|
||||
}
|
||||
|
||||
/** Drawer */
|
||||
--drawer-background-color: white;
|
||||
|
||||
/* Pagination */
|
||||
--pagination-active-link-border-color: var(--primary-color);
|
||||
--pagination-active-link-bg-color: var(--primary-color);
|
||||
--pagination-active-link-text-color: white;
|
||||
--pagination-link-border-color: rgba(239, 239, 239, 1);
|
||||
--pagination-link-text-color: black;
|
||||
--pagination-link-bg-color: white;
|
||||
--pagination-focus-border-color: var(--primary-color);
|
||||
--pagination-link-hover-color: var(--primary-color);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user