mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 21:19:01 +01:00 
			
		
		
		
	refactor uploading files
This commit is contained in:
		
							parent
							
								
									0802b81807
								
							
						
					
					
						commit
						a0d958bf12
					
				@ -315,14 +315,25 @@ class Froca {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** @returns {Promise<FAttachment>} */
 | 
					    /** @returns {Promise<FAttachment>} */
 | 
				
			||||||
    async getAttachment(attachmentId) {
 | 
					    async getAttachment(attachmentId, silentNotFoundError = false) {
 | 
				
			||||||
        const attachment = this.attachments[attachmentId];
 | 
					        const attachment = this.attachments[attachmentId];
 | 
				
			||||||
        if (attachment) {
 | 
					        if (attachment) {
 | 
				
			||||||
            return attachment;
 | 
					            return attachment;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // load all attachments for the given note even if one is requested, don't load one by one
 | 
					        // load all attachments for the given note even if one is requested, don't load one by one
 | 
				
			||||||
        const attachmentRows = await server.get(`attachments/${attachmentId}/all`);
 | 
					        let attachmentRows;
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            attachmentRows = await server.get(`attachments/${attachmentId}/all`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        catch (e) {
 | 
				
			||||||
 | 
					            if (silentNotFoundError) {
 | 
				
			||||||
 | 
					                logInfo(`Attachment '${attachmentId} not found, but silentNotFoundError is enabled: ` + e.message);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                throw e;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const attachments = this.processAttachmentRows(attachmentRows);
 | 
					        const attachments = this.processAttachmentRows(attachmentRows);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (attachments.length) {
 | 
					        if (attachments.length) {
 | 
				
			||||||
 | 
				
			|||||||
@ -7,8 +7,6 @@ import froca from "./froca.js";
 | 
				
			|||||||
import linkService from "./link.js";
 | 
					import linkService from "./link.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setupGlobs() {
 | 
					function setupGlobs() {
 | 
				
			||||||
    window.glob.PROFILING_LOG = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    window.glob.isDesktop = utils.isDesktop;
 | 
					    window.glob.isDesktop = utils.isDesktop;
 | 
				
			||||||
    window.glob.isMobile = utils.isMobile;
 | 
					    window.glob.isMobile = utils.isMobile;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,6 @@
 | 
				
			|||||||
import utils from './utils.js';
 | 
					import utils from './utils.js';
 | 
				
			||||||
import ValidationError from "./validation_error.js";
 | 
					import ValidationError from "./validation_error.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const REQUEST_LOGGING_ENABLED = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function getHeaders(headers) {
 | 
					async function getHeaders(headers) {
 | 
				
			||||||
    const appContext = (await import('../components/app_context.js')).default;
 | 
					    const appContext = (await import('../components/app_context.js')).default;
 | 
				
			||||||
    const activeNoteContext = appContext.tabManager ? appContext.tabManager.getActiveContext() : null;
 | 
					    const activeNoteContext = appContext.tabManager ? appContext.tabManager.getActiveContext() : null;
 | 
				
			||||||
@ -50,6 +48,21 @@ async function remove(url, componentId) {
 | 
				
			|||||||
    return await call('DELETE', url, null, {'trilium-component-id': componentId});
 | 
					    return await call('DELETE', url, null, {'trilium-component-id': componentId});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function upload(url, fileToUpload) {
 | 
				
			||||||
 | 
					    const formData = new FormData();
 | 
				
			||||||
 | 
					    formData.append('upload', fileToUpload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return await $.ajax({
 | 
				
			||||||
 | 
					        url: window.glob.baseApiUrl + url,
 | 
				
			||||||
 | 
					        headers: await getHeaders(),
 | 
				
			||||||
 | 
					        data: formData,
 | 
				
			||||||
 | 
					        type: 'PUT',
 | 
				
			||||||
 | 
					        timeout: 60 * 60 * 1000,
 | 
				
			||||||
 | 
					        contentType: false, // NEEDED, DON'T REMOVE THIS
 | 
				
			||||||
 | 
					        processData: false, // NEEDED, DON'T REMOVE THIS
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let i = 1;
 | 
					let i = 1;
 | 
				
			||||||
const reqResolves = {};
 | 
					const reqResolves = {};
 | 
				
			||||||
const reqRejects = {};
 | 
					const reqRejects = {};
 | 
				
			||||||
@ -59,8 +72,6 @@ let maxKnownEntityChangeId = 0;
 | 
				
			|||||||
async function call(method, url, data, headers = {}) {
 | 
					async function call(method, url, data, headers = {}) {
 | 
				
			||||||
    let resp;
 | 
					    let resp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const start = Date.now();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    headers = await getHeaders(headers);
 | 
					    headers = await getHeaders(headers);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (utils.isElectron()) {
 | 
					    if (utils.isElectron()) {
 | 
				
			||||||
@ -71,10 +82,6 @@ async function call(method, url, data, headers = {}) {
 | 
				
			|||||||
            reqResolves[requestId] = resolve;
 | 
					            reqResolves[requestId] = resolve;
 | 
				
			||||||
            reqRejects[requestId] = reject;
 | 
					            reqRejects[requestId] = reject;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (REQUEST_LOGGING_ENABLED) {
 | 
					 | 
				
			||||||
                console.log(utils.now(), `Request #${requestId} to ${method} ${url}`);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            ipc.send('server-request', {
 | 
					            ipc.send('server-request', {
 | 
				
			||||||
                requestId: requestId,
 | 
					                requestId: requestId,
 | 
				
			||||||
                headers: headers,
 | 
					                headers: headers,
 | 
				
			||||||
@ -88,12 +95,6 @@ async function call(method, url, data, headers = {}) {
 | 
				
			|||||||
        resp = await ajax(url, method, data, headers);
 | 
					        resp = await ajax(url, method, data, headers);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const end = Date.now();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (glob.PROFILING_LOG) {
 | 
					 | 
				
			||||||
        console.log(`${method} ${url} took ${end - start}ms`);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const maxEntityChangeIdStr = resp.headers['trilium-max-entity-change-id'];
 | 
					    const maxEntityChangeIdStr = resp.headers['trilium-max-entity-change-id'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (maxEntityChangeIdStr && maxEntityChangeIdStr.trim()) {
 | 
					    if (maxEntityChangeIdStr && maxEntityChangeIdStr.trim()) {
 | 
				
			||||||
@ -103,33 +104,6 @@ async function call(method, url, data, headers = {}) {
 | 
				
			|||||||
    return resp.body;
 | 
					    return resp.body;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function reportError(method, url, statusCode, response) {
 | 
					 | 
				
			||||||
    const toastService = (await import("./toast.js")).default;
 | 
					 | 
				
			||||||
    let message = response;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (typeof response === 'string') {
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            response = JSON.parse(response);
 | 
					 | 
				
			||||||
            message = response.message;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        catch (e) {}
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if ([400, 404].includes(statusCode) && response && typeof response === 'object') {
 | 
					 | 
				
			||||||
        toastService.showError(message);
 | 
					 | 
				
			||||||
        throw new ValidationError({
 | 
					 | 
				
			||||||
            requestUrl: url,
 | 
					 | 
				
			||||||
            method,
 | 
					 | 
				
			||||||
            statusCode,
 | 
					 | 
				
			||||||
            ...response
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        const title = `${statusCode} ${method} ${url}`;
 | 
					 | 
				
			||||||
        toastService.showErrorTitleAndMessage(title, message);
 | 
					 | 
				
			||||||
        toastService.throwError(`${title} - ${message}`);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function ajax(url, method, data, headers) {
 | 
					function ajax(url, method, data, headers) {
 | 
				
			||||||
    return new Promise((res, rej) => {
 | 
					    return new Promise((res, rej) => {
 | 
				
			||||||
        const options = {
 | 
					        const options = {
 | 
				
			||||||
@ -175,24 +149,8 @@ if (utils.isElectron()) {
 | 
				
			|||||||
    const ipc = utils.dynamicRequire('electron').ipcRenderer;
 | 
					    const ipc = utils.dynamicRequire('electron').ipcRenderer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ipc.on('server-response', async (event, arg) => {
 | 
					    ipc.on('server-response', async (event, arg) => {
 | 
				
			||||||
        if (REQUEST_LOGGING_ENABLED) {
 | 
					 | 
				
			||||||
            console.log(utils.now(), `Response #${arg.requestId}: ${arg.statusCode}`);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (arg.statusCode >= 200 && arg.statusCode < 300) {
 | 
					        if (arg.statusCode >= 200 && arg.statusCode < 300) {
 | 
				
			||||||
            if (arg.headers['Content-Type'] === 'application/json') {
 | 
					            handleSuccessfulResponse(arg);
 | 
				
			||||||
                arg.body = JSON.parse(arg.body);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (!(arg.requestId in reqResolves)) {
 | 
					 | 
				
			||||||
                // this can happen when reload happens between firing up the request and receiving the response
 | 
					 | 
				
			||||||
                throw new Error(`Unknown requestId="${arg.requestId}"`);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            reqResolves[arg.requestId]({
 | 
					 | 
				
			||||||
                body: arg.body,
 | 
					 | 
				
			||||||
                headers: arg.headers
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
            await reportError(arg.method, arg.url, arg.statusCode, arg.body);
 | 
					            await reportError(arg.method, arg.url, arg.statusCode, arg.body);
 | 
				
			||||||
@ -203,6 +161,49 @@ if (utils.isElectron()) {
 | 
				
			|||||||
        delete reqResolves[arg.requestId];
 | 
					        delete reqResolves[arg.requestId];
 | 
				
			||||||
        delete reqRejects[arg.requestId];
 | 
					        delete reqRejects[arg.requestId];
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function handleSuccessfulResponse(arg) {
 | 
				
			||||||
 | 
					        if (arg.headers['Content-Type'] === 'application/json') {
 | 
				
			||||||
 | 
					            arg.body = JSON.parse(arg.body);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!(arg.requestId in reqResolves)) {
 | 
				
			||||||
 | 
					            // this can happen when reload happens between firing up the request and receiving the response
 | 
				
			||||||
 | 
					            throw new Error(`Unknown requestId="${arg.requestId}"`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        reqResolves[arg.requestId]({
 | 
				
			||||||
 | 
					            body: arg.body,
 | 
				
			||||||
 | 
					            headers: arg.headers
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function reportError(method, url, statusCode, response) {
 | 
				
			||||||
 | 
					    const toastService = (await import("./toast.js")).default;
 | 
				
			||||||
 | 
					    let message = response;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (typeof response === 'string') {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            response = JSON.parse(response);
 | 
				
			||||||
 | 
					            message = response.message;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        catch (e) {}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if ([400, 404].includes(statusCode) && response && typeof response === 'object') {
 | 
				
			||||||
 | 
					        toastService.showError(message);
 | 
				
			||||||
 | 
					        throw new ValidationError({
 | 
				
			||||||
 | 
					            requestUrl: url,
 | 
				
			||||||
 | 
					            method,
 | 
				
			||||||
 | 
					            statusCode,
 | 
				
			||||||
 | 
					            ...response
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        const title = `${statusCode} ${method} ${url}`;
 | 
				
			||||||
 | 
					        toastService.showErrorTitleAndMessage(title, message);
 | 
				
			||||||
 | 
					        toastService.throwError(`${title} - ${message}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
@ -211,7 +212,7 @@ export default {
 | 
				
			|||||||
    put,
 | 
					    put,
 | 
				
			||||||
    patch,
 | 
					    patch,
 | 
				
			||||||
    remove,
 | 
					    remove,
 | 
				
			||||||
    ajax,
 | 
					    upload,
 | 
				
			||||||
    // don't remove, used from CKEditor image upload!
 | 
					    // don't remove, used from CKEditor image upload!
 | 
				
			||||||
    getHeaders,
 | 
					    getHeaders,
 | 
				
			||||||
    getMaxKnownEntityChangeId: () => maxKnownEntityChangeId
 | 
					    getMaxKnownEntityChangeId: () => maxKnownEntityChangeId
 | 
				
			||||||
 | 
				
			|||||||
@ -64,18 +64,7 @@ export default class AttachmentActionsWidget extends BasicWidget {
 | 
				
			|||||||
            const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
 | 
					            const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
 | 
				
			||||||
            this.$uploadNewRevisionInput.val('');
 | 
					            this.$uploadNewRevisionInput.val('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const formData = new FormData();
 | 
					            const result = await server.upload(`attachments/${this.attachmentId}/file`, fileToUpload);
 | 
				
			||||||
            formData.append('upload', fileToUpload);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const result = await $.ajax({
 | 
					 | 
				
			||||||
                url: `${window.glob.baseApiUrl}attachments/${this.attachmentId}/file`,
 | 
					 | 
				
			||||||
                headers: await server.getHeaders(),
 | 
					 | 
				
			||||||
                data: formData,
 | 
					 | 
				
			||||||
                type: 'PUT',
 | 
					 | 
				
			||||||
                timeout: 60 * 60 * 1000,
 | 
					 | 
				
			||||||
                contentType: false, // NEEDED, DON'T REMOVE THIS
 | 
					 | 
				
			||||||
                processData: false, // NEEDED, DON'T REMOVE THIS
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (result.uploaded) {
 | 
					            if (result.uploaded) {
 | 
				
			||||||
                toastService.showMessage("New attachment revision has been uploaded.");
 | 
					                toastService.showMessage("New attachment revision has been uploaded.");
 | 
				
			||||||
 | 
				
			|||||||
@ -49,16 +49,8 @@ export default class NoteContextAwareWidget extends BasicWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    async refresh() {
 | 
					    async refresh() {
 | 
				
			||||||
        if (this.isEnabled()) {
 | 
					        if (this.isEnabled()) {
 | 
				
			||||||
            const start = Date.now();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            this.toggleInt(true);
 | 
					            this.toggleInt(true);
 | 
				
			||||||
            await this.refreshWithNote(this.note);
 | 
					            await this.refreshWithNote(this.note);
 | 
				
			||||||
 | 
					 | 
				
			||||||
            const end = Date.now();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (glob.PROFILING_LOG && end - start > 10) {
 | 
					 | 
				
			||||||
                console.log(`Refresh of ${this.componentId} took ${end-start}ms`);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
            this.toggleInt(false);
 | 
					            this.toggleInt(false);
 | 
				
			||||||
 | 
				
			|||||||
@ -100,18 +100,7 @@ export default class FilePropertiesWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
            const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
 | 
					            const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
 | 
				
			||||||
            this.$uploadNewRevisionInput.val('');
 | 
					            this.$uploadNewRevisionInput.val('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const formData = new FormData();
 | 
					            const result = await server.upload(`notes/${this.noteId}/file`, fileToUpload);
 | 
				
			||||||
            formData.append('upload', fileToUpload);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const result = await $.ajax({
 | 
					 | 
				
			||||||
                url: `${window.glob.baseApiUrl}notes/${this.noteId}/file`,
 | 
					 | 
				
			||||||
                headers: await server.getHeaders(),
 | 
					 | 
				
			||||||
                data: formData,
 | 
					 | 
				
			||||||
                type: 'PUT',
 | 
					 | 
				
			||||||
                timeout: 60 * 60 * 1000,
 | 
					 | 
				
			||||||
                contentType: false, // NEEDED, DON'T REMOVE THIS
 | 
					 | 
				
			||||||
                processData: false, // NEEDED, DON'T REMOVE THIS
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (result.uploaded) {
 | 
					            if (result.uploaded) {
 | 
				
			||||||
                toastService.showMessage("New file revision has been uploaded.");
 | 
					                toastService.showMessage("New file revision has been uploaded.");
 | 
				
			||||||
 | 
				
			|||||||
@ -84,18 +84,7 @@ export default class ImagePropertiesWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
            const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
 | 
					            const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
 | 
				
			||||||
            this.$uploadNewRevisionInput.val('');
 | 
					            this.$uploadNewRevisionInput.val('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const formData = new FormData();
 | 
					            const result = await server.upload(`images/${this.noteId}`, fileToUpload);
 | 
				
			||||||
            formData.append('upload', fileToUpload);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const result = await $.ajax({
 | 
					 | 
				
			||||||
                url: `${window.glob.baseApiUrl}images/${this.noteId}`,
 | 
					 | 
				
			||||||
                headers: await server.getHeaders(),
 | 
					 | 
				
			||||||
                data: formData,
 | 
					 | 
				
			||||||
                type: 'PUT',
 | 
					 | 
				
			||||||
                timeout: 60 * 60 * 1000,
 | 
					 | 
				
			||||||
                contentType: false, // NEEDED, DON'T REMOVE THIS
 | 
					 | 
				
			||||||
                processData: false, // NEEDED, DON'T REMOVE THIS
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (result.uploaded) {
 | 
					            if (result.uploaded) {
 | 
				
			||||||
                toastService.showMessage("New image revision has been uploaded.");
 | 
					                toastService.showMessage("New image revision has been uploaded.");
 | 
				
			||||||
 | 
				
			|||||||
@ -57,7 +57,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget {
 | 
				
			|||||||
            })
 | 
					            })
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const attachment = await froca.getAttachment(this.attachmentId);
 | 
					        const attachment = await froca.getAttachment(this.attachmentId, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!attachment) {
 | 
					        if (!attachment) {
 | 
				
			||||||
            this.$wrapper.html("<strong>This attachment has been deleted.</strong>");
 | 
					            this.$wrapper.html("<strong>This attachment has been deleted.</strong>");
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user