Refactors save state thumbnail loading to prevent placeholder image flickering
This commit is contained in:
parent
129cef9cb8
commit
ba6805b0f1
@ -9,12 +9,30 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import ImageIO
|
import ImageIO
|
||||||
|
|
||||||
public class LoadImageOperation: NSOperation
|
import Roxas
|
||||||
|
|
||||||
|
public class LoadImageOperation: RSTOperation
|
||||||
{
|
{
|
||||||
public let URL: NSURL
|
public let URL: NSURL
|
||||||
|
|
||||||
public var completionHandler: (UIImage? -> Void)?
|
public var completionHandler: (UIImage? -> Void)? {
|
||||||
public var imageCache: NSCache?
|
didSet {
|
||||||
|
self.completionBlock = {
|
||||||
|
rst_dispatch_sync_on_main_thread() {
|
||||||
|
self.completionHandler?(self.image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var imageCache: NSCache? {
|
||||||
|
didSet {
|
||||||
|
// Ensures if an image is cached, it will be returned immediately, to prevent temporary flash of placeholder image
|
||||||
|
self.immediate = self.imageCache?.objectForKey(self.URL) != nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var image: UIImage?
|
||||||
|
|
||||||
public init(URL: NSURL)
|
public init(URL: NSURL)
|
||||||
{
|
{
|
||||||
@ -28,23 +46,11 @@ public extension LoadImageOperation
|
|||||||
{
|
{
|
||||||
override func main()
|
override func main()
|
||||||
{
|
{
|
||||||
var image: UIImage?
|
|
||||||
|
|
||||||
defer
|
|
||||||
{
|
|
||||||
if !self.cancelled
|
|
||||||
{
|
|
||||||
dispatch_async(dispatch_get_main_queue()) {
|
|
||||||
self.completionHandler?(image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !self.cancelled else { return }
|
guard !self.cancelled else { return }
|
||||||
|
|
||||||
if let cachedImage = self.imageCache?.objectForKey(self.URL) as? UIImage
|
if let cachedImage = self.imageCache?.objectForKey(self.URL) as? UIImage
|
||||||
{
|
{
|
||||||
image = cachedImage
|
self.image = cachedImage
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +67,7 @@ public extension LoadImageOperation
|
|||||||
|
|
||||||
self.imageCache?.setObject(loadedImage, forKey: self.URL)
|
self.imageCache?.setObject(loadedImage, forKey: self.URL)
|
||||||
|
|
||||||
image = loadedImage
|
self.image = loadedImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,41 +0,0 @@
|
|||||||
//
|
|
||||||
// NSOperationQueue+KeyValue.swift
|
|
||||||
// Delta
|
|
||||||
//
|
|
||||||
// Created by Riley Testut on 2/26/16.
|
|
||||||
// Copyright © 2016 Riley Testut. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import ObjectiveC.runtime
|
|
||||||
|
|
||||||
|
|
||||||
extension NSOperationQueue
|
|
||||||
{
|
|
||||||
private struct AssociatedKeys
|
|
||||||
{
|
|
||||||
static var OperationsDictionary = "delta_operationsDictionary"
|
|
||||||
}
|
|
||||||
|
|
||||||
private var operationsDictionary: NSMapTable {
|
|
||||||
get {
|
|
||||||
return objc_getAssociatedObject(self, &AssociatedKeys.OperationsDictionary) as? NSMapTable ?? NSMapTable.strongToWeakObjectsMapTable()
|
|
||||||
}
|
|
||||||
|
|
||||||
set {
|
|
||||||
objc_setAssociatedObject(self, &AssociatedKeys.OperationsDictionary, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addOperation(operation: NSOperation, forKey key: AnyObject)
|
|
||||||
{
|
|
||||||
self.operationsDictionary.objectForKey(key)
|
|
||||||
self.addOperation(operation)
|
|
||||||
}
|
|
||||||
|
|
||||||
func operationForKey(key: AnyObject) -> NSOperation?
|
|
||||||
{
|
|
||||||
let operation = self.operationsDictionary.objectForKey(key) as? NSOperation
|
|
||||||
return operation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -10,7 +10,6 @@
|
|||||||
AF0535CD7331785FA15E0864 /* Pods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22506DA00971C4300AF90A35 /* Pods.framework */; };
|
AF0535CD7331785FA15E0864 /* Pods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22506DA00971C4300AF90A35 /* Pods.framework */; };
|
||||||
BF090CF41B490D8300DCAB45 /* UIDevice+Vibration.m in Sources */ = {isa = PBXBuildFile; fileRef = BF090CF31B490D8300DCAB45 /* UIDevice+Vibration.m */; };
|
BF090CF41B490D8300DCAB45 /* UIDevice+Vibration.m in Sources */ = {isa = PBXBuildFile; fileRef = BF090CF31B490D8300DCAB45 /* UIDevice+Vibration.m */; };
|
||||||
BF0CDDAD1C8155D200640168 /* LoadImageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0CDDAC1C8155D200640168 /* LoadImageOperation.swift */; };
|
BF0CDDAD1C8155D200640168 /* LoadImageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0CDDAC1C8155D200640168 /* LoadImageOperation.swift */; };
|
||||||
BF0CDDAF1C81604100640168 /* NSOperationQueue+KeyValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0CDDAE1C81604100640168 /* NSOperationQueue+KeyValue.swift */; };
|
|
||||||
BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF107EC31BF413F000E0C32C /* GamesViewController.swift */; };
|
BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF107EC31BF413F000E0C32C /* GamesViewController.swift */; };
|
||||||
BF172AEB1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */; };
|
BF172AEB1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */; };
|
||||||
BF172AEC1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */; };
|
BF172AEC1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */; };
|
||||||
@ -135,7 +134,6 @@
|
|||||||
BF090CF21B490D8300DCAB45 /* UIDevice+Vibration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIDevice+Vibration.h"; sourceTree = "<group>"; };
|
BF090CF21B490D8300DCAB45 /* UIDevice+Vibration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIDevice+Vibration.h"; sourceTree = "<group>"; };
|
||||||
BF090CF31B490D8300DCAB45 /* UIDevice+Vibration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIDevice+Vibration.m"; sourceTree = "<group>"; };
|
BF090CF31B490D8300DCAB45 /* UIDevice+Vibration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIDevice+Vibration.m"; sourceTree = "<group>"; };
|
||||||
BF0CDDAC1C8155D200640168 /* LoadImageOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoadImageOperation.swift; path = Components/LoadImageOperation.swift; sourceTree = "<group>"; };
|
BF0CDDAC1C8155D200640168 /* LoadImageOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoadImageOperation.swift; path = Components/LoadImageOperation.swift; sourceTree = "<group>"; };
|
||||||
BF0CDDAE1C81604100640168 /* NSOperationQueue+KeyValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSOperationQueue+KeyValue.swift"; sourceTree = "<group>"; };
|
|
||||||
BF107EC31BF413F000E0C32C /* GamesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesViewController.swift; sourceTree = "<group>"; };
|
BF107EC31BF413F000E0C32C /* GamesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesViewController.swift; sourceTree = "<group>"; };
|
||||||
BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Conveniences.swift"; sourceTree = "<group>"; };
|
BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Conveniences.swift"; sourceTree = "<group>"; };
|
||||||
BF1FB1821C5EE643007E2494 /* SaveState+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SaveState+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
BF1FB1821C5EE643007E2494 /* SaveState+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SaveState+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||||
@ -318,7 +316,6 @@
|
|||||||
children = (
|
children = (
|
||||||
BF762EAA1BC1B076002C8866 /* NSManagedObject+Conveniences.swift */,
|
BF762EAA1BC1B076002C8866 /* NSManagedObject+Conveniences.swift */,
|
||||||
BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */,
|
BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */,
|
||||||
BF0CDDAE1C81604100640168 /* NSOperationQueue+KeyValue.swift */,
|
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -723,7 +720,6 @@
|
|||||||
BF1FB1861C5EE643007E2494 /* SaveState.swift in Sources */,
|
BF1FB1861C5EE643007E2494 /* SaveState.swift in Sources */,
|
||||||
BFB141181BE46934004FBF46 /* GameCollectionViewDataSource.swift in Sources */,
|
BFB141181BE46934004FBF46 /* GameCollectionViewDataSource.swift in Sources */,
|
||||||
BF1FB1841C5EE643007E2494 /* SaveState+CoreDataProperties.swift in Sources */,
|
BF1FB1841C5EE643007E2494 /* SaveState+CoreDataProperties.swift in Sources */,
|
||||||
BF0CDDAF1C81604100640168 /* NSOperationQueue+KeyValue.swift in Sources */,
|
|
||||||
BFFB709F1AF99B1700DE56FE /* EmulationViewController.swift in Sources */,
|
BFFB709F1AF99B1700DE56FE /* EmulationViewController.swift in Sources */,
|
||||||
BFAA1FF41B8AD7F900495943 /* ControllersSettingsViewController.swift in Sources */,
|
BFAA1FF41B8AD7F900495943 /* ControllersSettingsViewController.swift in Sources */,
|
||||||
BF353FF91C5D870B00C1184C /* PauseItem.swift in Sources */,
|
BF353FF91C5D870B00C1184C /* PauseItem.swift in Sources */,
|
||||||
|
|||||||
97
Delta.xcodeproj/xcshareddata/xcschemes/Delta.xcscheme
Normal file
97
Delta.xcodeproj/xcshareddata/xcschemes/Delta.xcscheme
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "0710"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
|
||||||
|
BuildableName = "Delta.app"
|
||||||
|
BlueprintName = "Delta"
|
||||||
|
ReferencedContainer = "container:Delta.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
|
||||||
|
BuildableName = "Delta.app"
|
||||||
|
BlueprintName = "Delta"
|
||||||
|
ReferencedContainer = "container:Delta.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<AdditionalOptions>
|
||||||
|
</AdditionalOptions>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
|
||||||
|
BuildableName = "Delta.app"
|
||||||
|
BlueprintName = "Delta"
|
||||||
|
ReferencedContainer = "container:Delta.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<CommandLineArguments>
|
||||||
|
<CommandLineArgument
|
||||||
|
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</CommandLineArgument>
|
||||||
|
</CommandLineArguments>
|
||||||
|
<AdditionalOptions>
|
||||||
|
</AdditionalOptions>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
|
||||||
|
BuildableName = "Delta.app"
|
||||||
|
BlueprintName = "Delta"
|
||||||
|
ReferencedContainer = "container:Delta.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@ -45,7 +45,7 @@ class SaveStatesViewController: UICollectionViewController
|
|||||||
|
|
||||||
private var fetchedResultsController: NSFetchedResultsController!
|
private var fetchedResultsController: NSFetchedResultsController!
|
||||||
|
|
||||||
private let imageOperationQueue = NSOperationQueue()
|
private let imageOperationQueue = RSTOperationQueue()
|
||||||
private let imageCache = NSCache()
|
private let imageCache = NSCache()
|
||||||
|
|
||||||
private let dateFormatter: NSDateFormatter
|
private let dateFormatter: NSDateFormatter
|
||||||
@ -122,7 +122,7 @@ extension SaveStatesViewController
|
|||||||
|
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didReceiveMemoryWarning()
|
override func didReceiveMemoryWarning()
|
||||||
{
|
{
|
||||||
super.didReceiveMemoryWarning()
|
super.didReceiveMemoryWarning()
|
||||||
@ -157,26 +157,36 @@ private extension SaveStatesViewController
|
|||||||
self.backgroundView.hidden = false
|
self.backgroundView.hidden = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
//MARK: - Configure Cell -
|
||||||
private extension SaveStatesViewController
|
|
||||||
{
|
func configureCollectionViewCell(cell: GridCollectionViewCell, forIndexPath indexPath: NSIndexPath, ignoreExpensiveOperations ignoreOperations: Bool = false)
|
||||||
func configureCollectionViewCell(cell: GridCollectionViewCell, forIndexPath indexPath: NSIndexPath)
|
|
||||||
{
|
{
|
||||||
let saveState = self.fetchedResultsController.objectAtIndexPath(indexPath) as! SaveState
|
let saveState = self.fetchedResultsController.objectAtIndexPath(indexPath) as! SaveState
|
||||||
|
|
||||||
cell.imageView.backgroundColor = UIColor.whiteColor()
|
cell.imageView.backgroundColor = UIColor.whiteColor()
|
||||||
cell.imageView.image = UIImage(named: "DeltaPlaceholder")
|
cell.imageView.image = UIImage(named: "DeltaPlaceholder")
|
||||||
|
|
||||||
let imageOperation = LoadImageOperation(URL: saveState.imageFileURL)
|
if !ignoreOperations
|
||||||
imageOperation.completionHandler = { image in
|
{
|
||||||
if let image = image
|
let imageOperation = LoadImageOperation(URL: saveState.imageFileURL)
|
||||||
{
|
imageOperation.imageCache = self.imageCache
|
||||||
cell.imageView.image = image
|
imageOperation.completionHandler = { image in
|
||||||
|
|
||||||
|
if let image = image
|
||||||
|
{
|
||||||
|
cell.imageView.image = image
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Ensure initially visible cells have loaded their image before they appear to prevent potential flickering from placeholder to thumbnail
|
||||||
self.imageOperationQueue.addOperation(imageOperation, forKey: indexPath)
|
if self.appearing
|
||||||
|
{
|
||||||
|
imageOperation.immediate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
self.imageOperationQueue.addOperation(imageOperation, forKey: indexPath)
|
||||||
|
}
|
||||||
|
|
||||||
cell.maximumImageSize = CGSizeMake(self.prototypeCellWidthConstraint.constant, (self.prototypeCellWidthConstraint.constant / 8.0) * 7.0)
|
cell.maximumImageSize = CGSizeMake(self.prototypeCellWidthConstraint.constant, (self.prototypeCellWidthConstraint.constant / 8.0) * 7.0)
|
||||||
|
|
||||||
@ -293,7 +303,8 @@ extension SaveStatesViewController: UICollectionViewDelegateFlowLayout
|
|||||||
{
|
{
|
||||||
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
|
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
|
||||||
{
|
{
|
||||||
self.configureCollectionViewCell(self.prototypeCell, forIndexPath: indexPath)
|
// No need to load images from disk just to determine size, so we pass true for ignoreExpensiveOperations
|
||||||
|
self.configureCollectionViewCell(self.prototypeCell, forIndexPath: indexPath, ignoreExpensiveOperations: true)
|
||||||
|
|
||||||
let size = self.prototypeCell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
|
let size = self.prototypeCell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
|
||||||
return size
|
return size
|
||||||
|
|||||||
2
External/Roxas
vendored
2
External/Roxas
vendored
@ -1 +1 @@
|
|||||||
Subproject commit 6dc906703bb5870d268d341cefd1fdcc40001eb9
|
Subproject commit c795d0ace22cfe57c3b73613630e14b58d2f8f09
|
||||||
Loading…
Reference in New Issue
Block a user