mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-26 11:17:04 +08:00
file-name-resolver module
This commit is contained in:
@ -109,6 +109,7 @@
|
|||||||
<TypeScriptCompile Include="apps\tests\gestures-tests.ts" />
|
<TypeScriptCompile Include="apps\tests\gestures-tests.ts" />
|
||||||
<TypeScriptCompile Include="apps\tests\layouts\dock-layout-tests.ts" />
|
<TypeScriptCompile Include="apps\tests\layouts\dock-layout-tests.ts" />
|
||||||
<TypeScriptCompile Include="apps\tests\pages\app.ts" />
|
<TypeScriptCompile Include="apps\tests\pages\app.ts" />
|
||||||
|
<TypeScriptCompile Include="apps\tests\pages\file-load-test.ts" />
|
||||||
<TypeScriptCompile Include="apps\tests\pages\page12.ts" />
|
<TypeScriptCompile Include="apps\tests\pages\page12.ts" />
|
||||||
<TypeScriptCompile Include="apps\tests\layouts\absolute-layout-tests.ts" />
|
<TypeScriptCompile Include="apps\tests\layouts\absolute-layout-tests.ts" />
|
||||||
<TypeScriptCompile Include="apps\tests\layouts\layout-helper.ts" />
|
<TypeScriptCompile Include="apps\tests\layouts\layout-helper.ts" />
|
||||||
@ -167,6 +168,10 @@
|
|||||||
<TypeScriptCompile Include="apps\ui-tests-app\pages\i61.ts" />
|
<TypeScriptCompile Include="apps\ui-tests-app\pages\i61.ts" />
|
||||||
<TypeScriptCompile Include="apps\ui-tests-app\pages\i73.ts" />
|
<TypeScriptCompile Include="apps\ui-tests-app\pages\i73.ts" />
|
||||||
<TypeScriptCompile Include="apps\ui-tests-app\pages\gestures.ts" />
|
<TypeScriptCompile Include="apps\ui-tests-app\pages\gestures.ts" />
|
||||||
|
<TypeScriptCompile Include="file-system\file-name-resolver.d.ts" />
|
||||||
|
<TypeScriptCompile Include="file-system\file-name-resolver.ts">
|
||||||
|
<DependentUpon>file-name-resolver.d.ts</DependentUpon>
|
||||||
|
</TypeScriptCompile>
|
||||||
<TypeScriptCompile Include="image-source\image-source-common.ts">
|
<TypeScriptCompile Include="image-source\image-source-common.ts">
|
||||||
<DependentUpon>image-source.d.ts</DependentUpon>
|
<DependentUpon>image-source.d.ts</DependentUpon>
|
||||||
</TypeScriptCompile>
|
</TypeScriptCompile>
|
||||||
@ -540,6 +545,12 @@
|
|||||||
<Content Include="apps\template-settings\app.css" />
|
<Content Include="apps\template-settings\app.css" />
|
||||||
<Content Include="apps\tests\app\binding_tests.xml" />
|
<Content Include="apps\tests\app\binding_tests.xml" />
|
||||||
<Content Include="apps\tests\ui\label\label-tests-wrong.css" />
|
<Content Include="apps\tests\ui\label\label-tests-wrong.css" />
|
||||||
|
<Content Include="apps\tests\pages\files\other.xml" />
|
||||||
|
<Content Include="apps\tests\pages\files\test.android.phone.xml" />
|
||||||
|
<Content Include="apps\tests\pages\files\test.android.xml" />
|
||||||
|
<Content Include="apps\tests\pages\files\test.minWH300.xml" />
|
||||||
|
<Content Include="apps\tests\pages\files\test.minWH450.xml" />
|
||||||
|
<Content Include="apps\tests\pages\files\test.xml" />
|
||||||
<Content Include="apps\ui-tests-app\pages\i86.xml" />
|
<Content Include="apps\ui-tests-app\pages\i86.xml" />
|
||||||
<Content Include="apps\template-blank\app.css" />
|
<Content Include="apps\template-blank\app.css" />
|
||||||
<Content Include="apps\template-hello-world\app.css" />
|
<Content Include="apps\template-hello-world\app.css" />
|
||||||
@ -1500,7 +1511,7 @@
|
|||||||
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
|
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
|
||||||
</WebProjectProperties>
|
</WebProjectProperties>
|
||||||
</FlavorProperties>
|
</FlavorProperties>
|
||||||
<UserProperties ui_2scroll-view_2package_1json__JSONSchema="http://json.schemastore.org/package" apps_2editable-text-demo_2package_1json__JSONSchema="http://json.schemastore.org/package" apps_2absolute-layout-demo_2package_1json__JSONSchema="http://json.schemastore.org/package" apps_2gallery-app_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2content-view_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2web-view_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2layouts_2linear-layout_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2layouts_2absolute-layout_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2layouts_2dock-layout_2package_1json__JSONSchema="" ui_2layouts_2grid-layout_2package_1json__JSONSchema="" ui_2layouts_2wrap-layout_2package_1json__JSONSchema="http://json.schemastore.org/package" />
|
<UserProperties ui_2layouts_2wrap-layout_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2layouts_2grid-layout_2package_1json__JSONSchema="" ui_2layouts_2dock-layout_2package_1json__JSONSchema="" ui_2layouts_2absolute-layout_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2layouts_2linear-layout_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2web-view_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2content-view_2package_1json__JSONSchema="http://json.schemastore.org/package" apps_2gallery-app_2package_1json__JSONSchema="http://json.schemastore.org/package" apps_2absolute-layout-demo_2package_1json__JSONSchema="http://json.schemastore.org/package" apps_2editable-text-demo_2package_1json__JSONSchema="http://json.schemastore.org/package" ui_2scroll-view_2package_1json__JSONSchema="http://json.schemastore.org/package" />
|
||||||
</VisualStudio>
|
</VisualStudio>
|
||||||
</ProjectExtensions>
|
</ProjectExtensions>
|
||||||
</Project>
|
</Project>
|
29
apps/tests/pages/file-load-test.ts
Normal file
29
apps/tests/pages/file-load-test.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import label = require("ui/label");
|
||||||
|
import pages = require("ui/page");
|
||||||
|
import fs = require("file-system");
|
||||||
|
import fileResolverModule = require("file-system/file-name-resolver");
|
||||||
|
|
||||||
|
export function createPage() {
|
||||||
|
var page = new pages.Page();
|
||||||
|
var lbl = new label.Label();
|
||||||
|
|
||||||
|
var moduleName = "app/tests/pages/files/test";
|
||||||
|
|
||||||
|
var resolver = new fileResolverModule.FileNameResolver({
|
||||||
|
width: 400,
|
||||||
|
height: 600,
|
||||||
|
os: "android",
|
||||||
|
deviceType: "phone"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Current app full path.
|
||||||
|
var currentAppPath = fs.knownFolders.currentApp().path;
|
||||||
|
var moduleNamePath = fs.path.join(currentAppPath, moduleName);
|
||||||
|
|
||||||
|
var fileName = resolver.resolveFileName(moduleNamePath, "xml");
|
||||||
|
lbl.text = fileName;
|
||||||
|
lbl.textWrap = true;;
|
||||||
|
|
||||||
|
page.content = lbl;
|
||||||
|
return page;
|
||||||
|
}
|
1
apps/tests/pages/files/other.xml
Normal file
1
apps/tests/pages/files/other.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
other.xml
|
1
apps/tests/pages/files/test.android.phone.xml
Normal file
1
apps/tests/pages/files/test.android.phone.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
test.android.phone.xml
|
1
apps/tests/pages/files/test.android.xml
Normal file
1
apps/tests/pages/files/test.android.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
test.android.xml
|
1
apps/tests/pages/files/test.minWH300.xml
Normal file
1
apps/tests/pages/files/test.minWH300.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
test.minWH300.xml
|
1
apps/tests/pages/files/test.minWH450.xml
Normal file
1
apps/tests/pages/files/test.minWH450.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
test.monWH450.xml
|
1
apps/tests/pages/files/test.xml
Normal file
1
apps/tests/pages/files/test.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
test.xml
|
16
file-system/file-name-resolver.d.ts
vendored
Normal file
16
file-system/file-name-resolver.d.ts
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Provides FileNameResolver class used for loading files based on device capabilities.
|
||||||
|
*/
|
||||||
|
declare module "file-system/file-name-resolver" {
|
||||||
|
interface PlatformContext {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
os: string;
|
||||||
|
deviceType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileNameResolver {
|
||||||
|
constructor(context: PlatformContext);
|
||||||
|
resolveFileName(path: string, ext: string): string;
|
||||||
|
}
|
||||||
|
}
|
206
file-system/file-name-resolver.ts
Normal file
206
file-system/file-name-resolver.ts
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import definition = require("file-system/file-name-resolver");
|
||||||
|
import fs = require("file-system");
|
||||||
|
import types = require("utils/types");
|
||||||
|
|
||||||
|
var MIN_WH: string = "minWH";
|
||||||
|
var MIN_W: string = "minW";
|
||||||
|
var MIN_H: string = "minH";
|
||||||
|
var PRIORITY_STEP = 10000;
|
||||||
|
|
||||||
|
interface QualifierSpec {
|
||||||
|
isMatch(value: string): boolean;
|
||||||
|
getMatchValue(value: string, context: definition.PlatformContext): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
var minWidthHeightQualifier: QualifierSpec = {
|
||||||
|
isMatch: function (value: string): boolean {
|
||||||
|
return value.indexOf(MIN_WH) === 0;
|
||||||
|
|
||||||
|
},
|
||||||
|
getMatchValue(value: string, context: definition.PlatformContext): number {
|
||||||
|
var numVal = parseInt(value.substr(MIN_WH.length));
|
||||||
|
if (isNaN(numVal)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var actualLength = Math.min(context.width, context.height);
|
||||||
|
if (actualLength < numVal) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PRIORITY_STEP - (actualLength - numVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var minWidthQualifier: QualifierSpec = {
|
||||||
|
isMatch: function (value: string): boolean {
|
||||||
|
return value.indexOf(MIN_W) === 0 && value.indexOf(MIN_WH) < 0;
|
||||||
|
|
||||||
|
},
|
||||||
|
getMatchValue(value: string, context: definition.PlatformContext): number {
|
||||||
|
var numVal = parseInt(value.substr(MIN_W.length));
|
||||||
|
if (isNaN(numVal)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var actualWidth = context.width;
|
||||||
|
if (actualWidth < numVal) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PRIORITY_STEP - (actualWidth - numVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var minHeightQualifier: QualifierSpec = {
|
||||||
|
isMatch: function (value: string): boolean {
|
||||||
|
return value.indexOf(MIN_H) === 0 && value.indexOf(MIN_WH) < 0;
|
||||||
|
|
||||||
|
},
|
||||||
|
getMatchValue(value: string, context: definition.PlatformContext): number {
|
||||||
|
var numVal = parseInt(value.substr(MIN_H.length));
|
||||||
|
if (isNaN(numVal)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var actualHeight = context.height;
|
||||||
|
if (actualHeight < numVal) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PRIORITY_STEP - (actualHeight - numVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fromQualifier: QualifierSpec = {
|
||||||
|
isMatch: function (value: string): boolean {
|
||||||
|
return value === "tablet" || value === "phone";
|
||||||
|
|
||||||
|
},
|
||||||
|
getMatchValue(value: string, context: definition.PlatformContext): number{
|
||||||
|
if (value !== context.deviceType.toLocaleLowerCase()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var paltformQualifier: QualifierSpec = {
|
||||||
|
isMatch: function (value: string): boolean {
|
||||||
|
return value === "android" ||
|
||||||
|
value === "ios";
|
||||||
|
|
||||||
|
},
|
||||||
|
getMatchValue(value: string, context: definition.PlatformContext): number{
|
||||||
|
return value === context.os.toLowerCase() ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List of supported qualifiers ordered by priority
|
||||||
|
var supportedQualifiers: Array<QualifierSpec> = [
|
||||||
|
minWidthHeightQualifier,
|
||||||
|
minWidthQualifier,
|
||||||
|
minHeightQualifier,
|
||||||
|
paltformQualifier,
|
||||||
|
fromQualifier];
|
||||||
|
|
||||||
|
export class FileNameResolver implements definition.FileNameResolver {
|
||||||
|
private _context: definition.PlatformContext;
|
||||||
|
private _cache = {};
|
||||||
|
|
||||||
|
constructor(context: definition.PlatformContext) {
|
||||||
|
this._context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public resolveFileName(path: string, ext: string): string {
|
||||||
|
var key = path + ext;
|
||||||
|
var result: string = this._cache[key];
|
||||||
|
if(types.isUndefined(result)) {
|
||||||
|
result = this.resolveFileNameImpl(path, ext);
|
||||||
|
this._cache[key] = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveFileNameImpl(path: string, ext: string): string {
|
||||||
|
path = fs.path.normalize(path);
|
||||||
|
ext = "." + ext;
|
||||||
|
var folderPath = path.substring(0, path.lastIndexOf(fs.path.separator) + 1);
|
||||||
|
console.log("search folderPath: " + folderPath);
|
||||||
|
|
||||||
|
if (fs.Folder.exists(folderPath)) {
|
||||||
|
var folder = fs.Folder.fromPath(folderPath);
|
||||||
|
|
||||||
|
var candidates = new Array<fs.File>();
|
||||||
|
folder.eachEntity((e) => {
|
||||||
|
|
||||||
|
if (e instanceof fs.File) {
|
||||||
|
var file = <fs.File>e;
|
||||||
|
console.log("File path: " + e.path);
|
||||||
|
if (file.path.indexOf(path) === 0 && file.extension === ext) {
|
||||||
|
candidates.push(file);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
var bestValue = Number.MIN_VALUE;
|
||||||
|
var bestCandidate: fs.File = null;
|
||||||
|
|
||||||
|
console.log("Candidates:");
|
||||||
|
for (var i = 0; i < candidates.length; i++) {
|
||||||
|
console.log("---------- candiate[" + i + "]: " + candidates[i].name);
|
||||||
|
var filePath = candidates[i].path;
|
||||||
|
var qualifiersStr: string = filePath.substr(path.length, filePath.length - path.length - ext.length);
|
||||||
|
|
||||||
|
var qualifiers = qualifiersStr.split(".");
|
||||||
|
|
||||||
|
var value = this.checkQualifiers(qualifiers);
|
||||||
|
console.log("qualifiers: " + qualifiersStr + " result: " + value);
|
||||||
|
|
||||||
|
if (value >= 0 && value > bestValue) {
|
||||||
|
bestValue = value;
|
||||||
|
bestCandidate = candidates[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestCandidate ? bestCandidate.path : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkQualifiers(qualifiers: Array<string>): number {
|
||||||
|
var result = 0;
|
||||||
|
for (var i = 0; i < qualifiers.length; i++) {
|
||||||
|
if (qualifiers[i]) {
|
||||||
|
var value = this.checkQualifier(qualifiers[i]);
|
||||||
|
|
||||||
|
console.log("checking qualifier: " + qualifiers[i] + " result: " + value);
|
||||||
|
if (value < 0) {
|
||||||
|
// Non of the supported qualifiers matched this or the match was not satisified
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
result += value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkQualifier(value: string) {
|
||||||
|
for (var i = 0; i < supportedQualifiers.length; i++) {
|
||||||
|
if (supportedQualifiers[i].isMatch(value)) {
|
||||||
|
var result = supportedQualifiers[i].getMatchValue(value, this._context);
|
||||||
|
if (result > 0) {
|
||||||
|
result += (supportedQualifiers.length - i) * PRIORITY_STEP;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
@ -73,7 +73,9 @@ export class screen implements definition.screen {
|
|||||||
mainScreenInfo = {
|
mainScreenInfo = {
|
||||||
widthPixels: metrics.widthPixels,
|
widthPixels: metrics.widthPixels,
|
||||||
heightPixels: metrics.heightPixels,
|
heightPixels: metrics.heightPixels,
|
||||||
scale: metrics.density
|
scale: metrics.density,
|
||||||
|
widthDIPs: metrics.widthPixels / metrics.density,
|
||||||
|
heightDIPs: metrics.heightPixels / metrics.density
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return mainScreenInfo;
|
return mainScreenInfo;
|
||||||
|
10
platform/platform.d.ts
vendored
10
platform/platform.d.ts
vendored
@ -61,6 +61,16 @@ declare module "platform" {
|
|||||||
*/
|
*/
|
||||||
heightPixels: number;
|
heightPixels: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the absolute width of the screen in density independent pixels.
|
||||||
|
*/
|
||||||
|
widthDIPs: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the absolute height of the screen in density independent pixels.
|
||||||
|
*/
|
||||||
|
heightDIPs: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The logical density of the display. This is a scaling factor for the Density Independent Pixel unit.
|
* The logical density of the display. This is a scaling factor for the Density Independent Pixel unit.
|
||||||
*/
|
*/
|
||||||
|
@ -71,7 +71,9 @@ export class screen implements definition.screen {
|
|||||||
mainScreenInfo = {
|
mainScreenInfo = {
|
||||||
widthPixels: size.width * scale,
|
widthPixels: size.width * scale,
|
||||||
heightPixels: size.height * scale,
|
heightPixels: size.height * scale,
|
||||||
scale: scale
|
scale: scale,
|
||||||
|
widthDIPs: size.width,
|
||||||
|
heightDIPs: size.height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import builder = require("ui/builder");
|
|||||||
import fs = require("file-system");
|
import fs = require("file-system");
|
||||||
import utils = require("utils/utils");
|
import utils = require("utils/utils");
|
||||||
import platform = require("platform");
|
import platform = require("platform");
|
||||||
|
import fileResolverModule = require("file-system/file-name-resolver");
|
||||||
|
|
||||||
var frameStack: Array<Frame> = [];
|
var frameStack: Array<Frame> = [];
|
||||||
|
|
||||||
@ -68,14 +69,17 @@ function resolvePageFromEntry(entry: definition.NavigationEntry): pages.Page {
|
|||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePlatformPath(path, ext) {
|
var fileNameResolver: fileResolverModule.FileNameResolver;
|
||||||
var platformName = platform.device.os.toLowerCase();
|
function resolveFilePath(path, ext) {
|
||||||
var platformPath = [path, platformName, ext].join(".");
|
if (!fileNameResolver) {
|
||||||
if (fs.File.exists(platformPath)) {
|
fileNameResolver = new fileResolverModule.FileNameResolver({
|
||||||
return platformPath;
|
width: platform.screen.mainScreen.widthDIPs,
|
||||||
}
|
height: platform.screen.mainScreen.heightDIPs,
|
||||||
|
os: platform.device.os,
|
||||||
return [path, ext].join(".");
|
deviceType: platform.device.deviceType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return fileNameResolver.resolveFileName(path, ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pageFromBuilder(moduleNamePath: string, moduleName: string, moduleExports: any): pages.Page {
|
function pageFromBuilder(moduleNamePath: string, moduleName: string, moduleExports: any): pages.Page {
|
||||||
@ -83,7 +87,7 @@ function pageFromBuilder(moduleNamePath: string, moduleName: string, moduleExpor
|
|||||||
var element: view.View;
|
var element: view.View;
|
||||||
|
|
||||||
// Possible XML file path.
|
// Possible XML file path.
|
||||||
var fileName = resolvePlatformPath(moduleNamePath, "xml");
|
var fileName = resolveFilePath(moduleNamePath, "xml");
|
||||||
|
|
||||||
if (fs.File.exists(fileName)) {
|
if (fs.File.exists(fileName)) {
|
||||||
trace.write("Loading XML file: " + fileName, trace.categories.Navigation);
|
trace.write("Loading XML file: " + fileName, trace.categories.Navigation);
|
||||||
@ -94,7 +98,7 @@ function pageFromBuilder(moduleNamePath: string, moduleName: string, moduleExpor
|
|||||||
page = <pages.Page>element;
|
page = <pages.Page>element;
|
||||||
|
|
||||||
// Possible CSS file path.
|
// Possible CSS file path.
|
||||||
var cssFileName = resolvePlatformPath(moduleName, "css");
|
var cssFileName = resolveFilePath(moduleName, "css");
|
||||||
page.addCssFile(cssFileName);
|
page.addCssFile(cssFileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user