mirror of
https://github.com/status-im/react-native.git
synced 2025-01-28 10:14:49 +00:00
Revert D8978844: [react-native][PR] Performance improvement for loading cached images on iOS
Differential Revision: D8978844 Original commit changeset: 4b86043bc14c fbshipit-source-id: fdf3ddd111c8c0dec6ddf2814a4e1e0ff58ef529
This commit is contained in:
parent
7f0a4f72b4
commit
affb135d62
@ -21,26 +21,22 @@
|
|||||||
static const NSUInteger RCTMaxCachableDecodedImageSizeInBytes = 1048576; // 1MB
|
static const NSUInteger RCTMaxCachableDecodedImageSizeInBytes = 1048576; // 1MB
|
||||||
|
|
||||||
static NSString *RCTCacheKeyForImage(NSString *imageTag, CGSize size, CGFloat scale,
|
static NSString *RCTCacheKeyForImage(NSString *imageTag, CGSize size, CGFloat scale,
|
||||||
RCTResizeMode resizeMode)
|
RCTResizeMode resizeMode, NSString *responseDate)
|
||||||
{
|
{
|
||||||
return [NSString stringWithFormat:@"%@|%g|%g|%g|%lld",
|
return [NSString stringWithFormat:@"%@|%g|%g|%g|%lld|%@",
|
||||||
imageTag, size.width, size.height, scale, (long long)resizeMode];
|
imageTag, size.width, size.height, scale, (long long)resizeMode, responseDate];
|
||||||
}
|
}
|
||||||
|
|
||||||
@implementation RCTImageCache
|
@implementation RCTImageCache
|
||||||
{
|
{
|
||||||
NSOperationQueue *_imageDecodeQueue;
|
NSOperationQueue *_imageDecodeQueue;
|
||||||
NSCache *_decodedImageCache;
|
NSCache *_decodedImageCache;
|
||||||
NSMutableDictionary *_cacheStaleTimes;
|
|
||||||
|
|
||||||
NSDateFormatter *_headerDateFormatter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)init
|
- (instancetype)init
|
||||||
{
|
{
|
||||||
_decodedImageCache = [NSCache new];
|
_decodedImageCache = [NSCache new];
|
||||||
_decodedImageCache.totalCostLimit = 5 * 1024 * 1024; // 5MB
|
_decodedImageCache.totalCostLimit = 5 * 1024 * 1024; // 5MB
|
||||||
_cacheStaleTimes = [[NSMutableDictionary alloc] init];
|
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
selector:@selector(clearCache)
|
selector:@selector(clearCache)
|
||||||
@ -62,9 +58,6 @@ static NSString *RCTCacheKeyForImage(NSString *imageTag, CGSize size, CGFloat sc
|
|||||||
- (void)clearCache
|
- (void)clearCache
|
||||||
{
|
{
|
||||||
[_decodedImageCache removeAllObjects];
|
[_decodedImageCache removeAllObjects];
|
||||||
@synchronized(_cacheStaleTimes) {
|
|
||||||
[_cacheStaleTimes removeAllObjects];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)addImageToCache:(UIImage *)image
|
- (void)addImageToCache:(UIImage *)image
|
||||||
@ -85,19 +78,9 @@ static NSString *RCTCacheKeyForImage(NSString *imageTag, CGSize size, CGFloat sc
|
|||||||
size:(CGSize)size
|
size:(CGSize)size
|
||||||
scale:(CGFloat)scale
|
scale:(CGFloat)scale
|
||||||
resizeMode:(RCTResizeMode)resizeMode
|
resizeMode:(RCTResizeMode)resizeMode
|
||||||
|
responseDate:(NSString *)responseDate
|
||||||
{
|
{
|
||||||
NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode);
|
NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode, responseDate);
|
||||||
@synchronized(_cacheStaleTimes) {
|
|
||||||
id staleTime = _cacheStaleTimes[cacheKey];
|
|
||||||
if (staleTime) {
|
|
||||||
if ([[NSDate new] compare:(NSDate *)staleTime] == NSOrderedDescending) {
|
|
||||||
// cached image has expired, clear it out to make room for others
|
|
||||||
[_cacheStaleTimes removeObjectForKey:cacheKey];
|
|
||||||
[_decodedImageCache removeObjectForKey:cacheKey];
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [_decodedImageCache objectForKey:cacheKey];
|
return [_decodedImageCache objectForKey:cacheKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,44 +90,9 @@ static NSString *RCTCacheKeyForImage(NSString *imageTag, CGSize size, CGFloat sc
|
|||||||
scale:(CGFloat)scale
|
scale:(CGFloat)scale
|
||||||
resizeMode:(RCTResizeMode)resizeMode
|
resizeMode:(RCTResizeMode)resizeMode
|
||||||
responseDate:(NSString *)responseDate
|
responseDate:(NSString *)responseDate
|
||||||
cacheControl:(NSString *)cacheControl
|
|
||||||
{
|
{
|
||||||
NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode);
|
NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode, responseDate);
|
||||||
BOOL shouldCache = YES;
|
return [self addImageToCache:image forKey:cacheKey];
|
||||||
NSDate *staleTime;
|
|
||||||
NSArray<NSString *> *components = [cacheControl componentsSeparatedByString:@","];
|
|
||||||
for (NSString *component in components) {
|
|
||||||
if ([component containsString:@"no-cache"] || [component containsString:@"no-store"] || [component hasSuffix:@"max-age=0"]) {
|
|
||||||
shouldCache = NO;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
NSRange range = [component rangeOfString:@"max-age="];
|
|
||||||
if (range.location != NSNotFound) {
|
|
||||||
NSInteger seconds = [[component substringFromIndex:range.location + range.length] integerValue];
|
|
||||||
NSDate *originalDate = [self dateWithHeaderString:responseDate];
|
|
||||||
staleTime = [originalDate dateByAddingTimeInterval:(NSTimeInterval)seconds];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (shouldCache) {
|
|
||||||
if (staleTime) {
|
|
||||||
@synchronized(_cacheStaleTimes) {
|
|
||||||
_cacheStaleTimes[cacheKey] = staleTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [self addImageToCache:image forKey:cacheKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSDate *)dateWithHeaderString:(NSString *)headerDateString {
|
|
||||||
if (_headerDateFormatter == nil) {
|
|
||||||
_headerDateFormatter = [[NSDateFormatter alloc] init];
|
|
||||||
_headerDateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
|
|
||||||
_headerDateFormatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
|
|
||||||
_headerDateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [_headerDateFormatter dateFromString:headerDateString];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -24,15 +24,15 @@ typedef dispatch_block_t RCTImageLoaderCancellationBlock;
|
|||||||
- (UIImage *)imageForUrl:(NSString *)url
|
- (UIImage *)imageForUrl:(NSString *)url
|
||||||
size:(CGSize)size
|
size:(CGSize)size
|
||||||
scale:(CGFloat)scale
|
scale:(CGFloat)scale
|
||||||
resizeMode:(RCTResizeMode)resizeMode;
|
resizeMode:(RCTResizeMode)resizeMode
|
||||||
|
responseDate:(NSString *)responseDate;
|
||||||
|
|
||||||
- (void)addImageToCache:(UIImage *)image
|
- (void)addImageToCache:(UIImage *)image
|
||||||
URL:(NSString *)url
|
URL:(NSString *)url
|
||||||
size:(CGSize)size
|
size:(CGSize)size
|
||||||
scale:(CGFloat)scale
|
scale:(CGFloat)scale
|
||||||
resizeMode:(RCTResizeMode)resizeMode
|
resizeMode:(RCTResizeMode)resizeMode
|
||||||
responseDate:(NSString *)responseDate
|
responseDate:(NSString *)responseDate;
|
||||||
cacheControl:(NSString *)cacheControl;
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
@ -321,7 +321,7 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
resizeMode:(RCTResizeMode)resizeMode
|
resizeMode:(RCTResizeMode)resizeMode
|
||||||
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
|
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
|
||||||
partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
|
partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
|
||||||
completionBlock:(void (^)(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate, NSString *cacheControl))completionBlock
|
completionBlock:(void (^)(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate))completionBlock
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
NSMutableURLRequest *mutableRequest = [request mutableCopy];
|
NSMutableURLRequest *mutableRequest = [request mutableCopy];
|
||||||
@ -344,15 +344,15 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
BOOL requiresScheduling = [loadHandler respondsToSelector:@selector(requiresScheduling)] ?
|
BOOL requiresScheduling = [loadHandler respondsToSelector:@selector(requiresScheduling)] ?
|
||||||
[loadHandler requiresScheduling] : YES;
|
[loadHandler requiresScheduling] : YES;
|
||||||
|
|
||||||
BOOL cacheResult = [loadHandler respondsToSelector:@selector(shouldCacheLoadedImages)] ?
|
|
||||||
[loadHandler shouldCacheLoadedImages] : YES;
|
|
||||||
|
|
||||||
__block atomic_bool cancelled = ATOMIC_VAR_INIT(NO);
|
__block atomic_bool cancelled = ATOMIC_VAR_INIT(NO);
|
||||||
// TODO: Protect this variable shared between threads.
|
// TODO: Protect this variable shared between threads.
|
||||||
__block dispatch_block_t cancelLoad = nil;
|
__block dispatch_block_t cancelLoad = nil;
|
||||||
void (^completionHandler)(NSError *, id, NSString *, NSString *) = ^(NSError *error, id imageOrData, NSString *fetchDate, NSString *cacheControl) {
|
void (^completionHandler)(NSError *, id, NSString *) = ^(NSError *error, id imageOrData, NSString *fetchDate) {
|
||||||
cancelLoad = nil;
|
cancelLoad = nil;
|
||||||
|
|
||||||
|
BOOL cacheResult = [loadHandler respondsToSelector:@selector(shouldCacheLoadedImages)] ?
|
||||||
|
[loadHandler shouldCacheLoadedImages] : YES;
|
||||||
|
|
||||||
// If we've received an image, we should try to set it synchronously,
|
// If we've received an image, we should try to set it synchronously,
|
||||||
// if it's data, do decoding on a background thread.
|
// if it's data, do decoding on a background thread.
|
||||||
if (RCTIsMainQueue() && ![imageOrData isKindOfClass:[UIImage class]]) {
|
if (RCTIsMainQueue() && ![imageOrData isKindOfClass:[UIImage class]]) {
|
||||||
@ -360,11 +360,11 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
// expecting it, and may do expensive post-processing in the callback
|
// expecting it, and may do expensive post-processing in the callback
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
if (!atomic_load(&cancelled)) {
|
if (!atomic_load(&cancelled)) {
|
||||||
completionBlock(error, imageOrData, cacheResult, fetchDate, cacheControl);
|
completionBlock(error, imageOrData, cacheResult, fetchDate);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (!atomic_load(&cancelled)) {
|
} else if (!atomic_load(&cancelled)) {
|
||||||
completionBlock(error, imageOrData, cacheResult, fetchDate, cacheControl);
|
completionBlock(error, imageOrData, cacheResult, fetchDate);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -378,7 +378,7 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
progressHandler:progressHandler
|
progressHandler:progressHandler
|
||||||
partialLoadHandler:partialLoadHandler
|
partialLoadHandler:partialLoadHandler
|
||||||
completionHandler:^(NSError *error, UIImage *image){
|
completionHandler:^(NSError *error, UIImage *image){
|
||||||
completionHandler(error, image, nil, nil);
|
completionHandler(error, image, nil);
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,25 +402,13 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
progressHandler:progressHandler
|
progressHandler:progressHandler
|
||||||
partialLoadHandler:partialLoadHandler
|
partialLoadHandler:partialLoadHandler
|
||||||
completionHandler:^(NSError *error, UIImage *image) {
|
completionHandler:^(NSError *error, UIImage *image) {
|
||||||
completionHandler(error, image, nil, nil);
|
completionHandler(error, image, nil);
|
||||||
}];
|
}];
|
||||||
} else {
|
} else {
|
||||||
UIImage *image;
|
// Use networking module to load image
|
||||||
if (cacheResult) {
|
cancelLoad = [strongSelf _loadURLRequest:request
|
||||||
image = [[strongSelf imageCache] imageForUrl:request.URL.absoluteString
|
progressBlock:progressHandler
|
||||||
size:size
|
completionBlock:completionHandler];
|
||||||
scale:scale
|
|
||||||
resizeMode:resizeMode];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (image) {
|
|
||||||
completionHandler(nil, image, nil, nil);
|
|
||||||
} else {
|
|
||||||
// Use networking module to load image
|
|
||||||
cancelLoad = [strongSelf _loadURLRequest:request
|
|
||||||
progressBlock:progressHandler
|
|
||||||
completionBlock:completionHandler];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -439,7 +427,7 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
|
|
||||||
- (RCTImageLoaderCancellationBlock)_loadURLRequest:(NSURLRequest *)request
|
- (RCTImageLoaderCancellationBlock)_loadURLRequest:(NSURLRequest *)request
|
||||||
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
|
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
|
||||||
completionBlock:(void (^)(NSError *error, id imageOrData, NSString *fetchDate, NSString *cacheControl))completionHandler
|
completionBlock:(void (^)(NSError *error, id imageOrData, NSString *fetchDate))completionHandler
|
||||||
{
|
{
|
||||||
// Check if networking module is available
|
// Check if networking module is available
|
||||||
if (RCT_DEBUG && ![_bridge respondsToSelector:@selector(networking)]) {
|
if (RCT_DEBUG && ![_bridge respondsToSelector:@selector(networking)]) {
|
||||||
@ -461,19 +449,18 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
RCTURLRequestCompletionBlock processResponse = ^(NSURLResponse *response, NSData *data, NSError *error) {
|
RCTURLRequestCompletionBlock processResponse = ^(NSURLResponse *response, NSData *data, NSError *error) {
|
||||||
// Check for system errors
|
// Check for system errors
|
||||||
if (error) {
|
if (error) {
|
||||||
completionHandler(error, nil, nil, nil);
|
completionHandler(error, nil, nil);
|
||||||
return;
|
return;
|
||||||
} else if (!response) {
|
} else if (!response) {
|
||||||
completionHandler(RCTErrorWithMessage(@"Response metadata error"), nil, nil, nil);
|
completionHandler(RCTErrorWithMessage(@"Response metadata error"), nil, nil);
|
||||||
return;
|
return;
|
||||||
} else if (!data) {
|
} else if (!data) {
|
||||||
completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil, nil, nil);
|
completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil, nil);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for http errors
|
// Check for http errors
|
||||||
NSString *responseDate;
|
NSString *responseDate;
|
||||||
NSString *cacheControl;
|
|
||||||
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||||
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
|
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
|
||||||
if (statusCode != 200) {
|
if (statusCode != 200) {
|
||||||
@ -481,16 +468,15 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: errorMessage};
|
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: errorMessage};
|
||||||
completionHandler([[NSError alloc] initWithDomain:NSURLErrorDomain
|
completionHandler([[NSError alloc] initWithDomain:NSURLErrorDomain
|
||||||
code:statusCode
|
code:statusCode
|
||||||
userInfo:userInfo], nil, nil, nil);
|
userInfo:userInfo], nil, nil);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
responseDate = ((NSHTTPURLResponse *)response).allHeaderFields[@"Date"];
|
responseDate = ((NSHTTPURLResponse *)response).allHeaderFields[@"Date"];
|
||||||
cacheControl = ((NSHTTPURLResponse *)response).allHeaderFields[@"Cache-Control"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call handler
|
// Call handler
|
||||||
completionHandler(nil, data, responseDate, cacheControl);
|
completionHandler(nil, data, responseDate);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Download image
|
// Download image
|
||||||
@ -512,7 +498,7 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
} else {
|
} else {
|
||||||
someError = RCTErrorWithMessage(@"Unknown image download error");
|
someError = RCTErrorWithMessage(@"Unknown image download error");
|
||||||
}
|
}
|
||||||
completionHandler(someError, nil, nil, nil);
|
completionHandler(someError, nil, nil);
|
||||||
[strongSelf dequeueTasks];
|
[strongSelf dequeueTasks];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -578,7 +564,7 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
};
|
};
|
||||||
|
|
||||||
__weak RCTImageLoader *weakSelf = self;
|
__weak RCTImageLoader *weakSelf = self;
|
||||||
void (^completionHandler)(NSError *, id, BOOL, NSString *, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate, NSString *cacheControl) {
|
void (^completionHandler)(NSError *, id, BOOL, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate) {
|
||||||
__typeof(self) strongSelf = weakSelf;
|
__typeof(self) strongSelf = weakSelf;
|
||||||
if (atomic_load(&cancelled) || !strongSelf) {
|
if (atomic_load(&cancelled) || !strongSelf) {
|
||||||
return;
|
return;
|
||||||
@ -590,6 +576,20 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check decoded image cache
|
||||||
|
if (cacheResult) {
|
||||||
|
UIImage *image = [[strongSelf imageCache] imageForUrl:imageURLRequest.URL.absoluteString
|
||||||
|
size:size
|
||||||
|
scale:scale
|
||||||
|
resizeMode:resizeMode
|
||||||
|
responseDate:fetchDate];
|
||||||
|
if (image) {
|
||||||
|
cancelLoad = nil;
|
||||||
|
completionBlock(nil, image);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RCTImageLoaderCompletionBlock decodeCompletionHandler = ^(NSError *error_, UIImage *image) {
|
RCTImageLoaderCompletionBlock decodeCompletionHandler = ^(NSError *error_, UIImage *image) {
|
||||||
if (cacheResult && image) {
|
if (cacheResult && image) {
|
||||||
// Store decoded image in cache
|
// Store decoded image in cache
|
||||||
@ -598,8 +598,7 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
size:size
|
size:size
|
||||||
scale:scale
|
scale:scale
|
||||||
resizeMode:resizeMode
|
resizeMode:resizeMode
|
||||||
responseDate:fetchDate
|
responseDate:fetchDate];
|
||||||
cacheControl:cacheControl];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelLoad = nil;
|
cancelLoad = nil;
|
||||||
@ -733,7 +732,7 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
|||||||
- (RCTImageLoaderCancellationBlock)getImageSizeForURLRequest:(NSURLRequest *)imageURLRequest
|
- (RCTImageLoaderCancellationBlock)getImageSizeForURLRequest:(NSURLRequest *)imageURLRequest
|
||||||
block:(void(^)(NSError *error, CGSize size))callback
|
block:(void(^)(NSError *error, CGSize size))callback
|
||||||
{
|
{
|
||||||
void (^completion)(NSError *, id, BOOL, NSString *, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate, NSString *cacheControl) {
|
void (^completion)(NSError *, id, BOOL, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate) {
|
||||||
CGSize size;
|
CGSize size;
|
||||||
if ([imageOrData isKindOfClass:[NSData class]]) {
|
if ([imageOrData isKindOfClass:[NSData class]]) {
|
||||||
NSDictionary *meta = RCTGetImageMetadata(imageOrData);
|
NSDictionary *meta = RCTGetImageMetadata(imageOrData);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user