mirror of
https://github.com/TriliumNext/Notes.git
synced 2026-01-06 12:59:55 -06:00
notes_tree now has note_tree_id so we stricly distinguish between working on notes or note trees
This commit is contained in:
@@ -4,23 +4,23 @@ const contextMenu = (function() {
|
||||
const treeEl = $("#tree");
|
||||
|
||||
function pasteAfter(node) {
|
||||
const subjectNode = treeUtils.getNodeByKey(noteTree.getClipboardNoteId());
|
||||
const subjectNode = treeUtils.getNodeByNoteTreeId(noteTree.getClipboardNoteTreeId());
|
||||
|
||||
treeChanges.moveAfterNode(subjectNode, node);
|
||||
|
||||
noteTree.setClipboardNoteId(null);
|
||||
noteTree.setClipboardNoteTreeId(null);
|
||||
}
|
||||
|
||||
function pasteInto(node) {
|
||||
const subjectNode = treeUtils.getNodeByKey(noteTree.getClipboardNoteId());
|
||||
const subjectNode = treeUtils.getNodeByNoteTreeId(noteTree.getClipboardNoteTreeId());
|
||||
|
||||
treeChanges.moveToNode(subjectNode, node);
|
||||
|
||||
noteTree.setClipboardNoteId(null);
|
||||
noteTree.setClipboardNoteTreeId(null);
|
||||
}
|
||||
|
||||
function cut(node) {
|
||||
noteTree.setClipboardNoteId(node.key);
|
||||
noteTree.setClipboardNoteTreeId(node.note_tree_id);
|
||||
}
|
||||
|
||||
const contextMenuSettings = {
|
||||
@@ -42,8 +42,8 @@ const contextMenu = (function() {
|
||||
beforeOpen: (event, ui) => {
|
||||
const node = $.ui.fancytree.getNode(ui.target);
|
||||
// Modify menu entries depending on node status
|
||||
treeEl.contextmenu("enableEntry", "pasteAfter", noteTree.getClipboardNoteId() !== null);
|
||||
treeEl.contextmenu("enableEntry", "pasteInto", noteTree.getClipboardNoteId() !== null);
|
||||
treeEl.contextmenu("enableEntry", "pasteAfter", noteTree.getClipboardNoteTreeId() !== null);
|
||||
treeEl.contextmenu("enableEntry", "pasteInto", noteTree.getClipboardNoteTreeId() !== null);
|
||||
|
||||
treeEl.contextmenu("enableEntry", "protectSubTree", protected_session.isProtectedSessionAvailable());
|
||||
treeEl.contextmenu("enableEntry", "unprotectSubTree", protected_session.isProtectedSessionAvailable());
|
||||
@@ -59,10 +59,10 @@ const contextMenu = (function() {
|
||||
const node = $.ui.fancytree.getNode(ui.target);
|
||||
|
||||
if (ui.cmd === "insertNoteHere") {
|
||||
const parentKey = treeUtils.getParentKey(node);
|
||||
const parentNoteTreeId = treeUtils.getParentNoteTreeId(node);
|
||||
const isProtected = treeUtils.getParentProtectedStatus(node);
|
||||
|
||||
noteEditor.createNote(node, parentKey, 'after', isProtected);
|
||||
noteEditor.createNote(node, parentNoteTreeId, 'after', isProtected);
|
||||
}
|
||||
else if (ui.cmd === "insertChildNote") {
|
||||
noteEditor.createNote(node, node.key, 'into');
|
||||
|
||||
@@ -13,31 +13,31 @@ const recentNotes = (function() {
|
||||
type: 'GET',
|
||||
error: () => showError("Error getting recent notes.")
|
||||
}).then(result => {
|
||||
list = result.map(r => r.note_id);
|
||||
list = result.map(r => r.note_tree_id);
|
||||
});
|
||||
|
||||
function addRecentNote(noteTreeId, noteContentId) {
|
||||
function addRecentNote(noteTreeId) {
|
||||
setTimeout(() => {
|
||||
// we include the note into recent list only if the user stayed on the note at least 5 seconds
|
||||
if (noteTreeId === noteEditor.getCurrentNoteId() || noteContentId === noteEditor.getCurrentNoteId()) {
|
||||
if (noteTreeId === noteEditor.getCurrentNoteId()) {
|
||||
$.ajax({
|
||||
url: baseApiUrl + 'recent-notes/' + noteTreeId,
|
||||
type: 'PUT',
|
||||
error: () => showError("Error setting recent notes.")
|
||||
}).then(result => {
|
||||
list = result.map(r => r.note_id);
|
||||
list = result.map(r => r.note_tree_id);
|
||||
});
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function removeRecentNote(noteIdToRemove) {
|
||||
function removeRecentNote(noteTreeIdToRemove) {
|
||||
$.ajax({
|
||||
url: baseApiUrl + 'recent-notes/' + noteIdToRemove,
|
||||
url: baseApiUrl + 'recent-notes/' + noteTreeIdToRemove,
|
||||
type: 'DELETE',
|
||||
error: () => showError("Error removing note from recent notes.")
|
||||
}).then(result => {
|
||||
list = result.map(r => r.note_id);
|
||||
list = result.map(r => r.note_tree_id);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,15 +56,15 @@ const recentNotes = (function() {
|
||||
// remove the current note
|
||||
const recNotes = list.filter(note => note !== noteEditor.getCurrentNoteId());
|
||||
|
||||
$.each(recNotes, (key, valueNoteId) => {
|
||||
const noteTitle = treeUtils.getFullName(valueNoteId);
|
||||
$.each(recNotes, (key, valueNoteTreeId) => {
|
||||
const noteTitle = treeUtils.getFullName(valueNoteTreeId);
|
||||
|
||||
if (!noteTitle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const option = $("<option></option>")
|
||||
.attr("value", valueNoteId)
|
||||
.attr("value", valueNoteTreeId)
|
||||
.text(noteTitle);
|
||||
|
||||
// select the first one (most recent one) by default
|
||||
|
||||
@@ -92,7 +92,7 @@ const noteEditor = (function() {
|
||||
|
||||
const title = noteTitleEl.val();
|
||||
|
||||
treeUtils.getNodeByKey(note.detail.note_id).setTitle(title);
|
||||
noteTree.getCurrentNode().setTitle(title);
|
||||
|
||||
note.detail.note_title = title;
|
||||
}
|
||||
@@ -121,7 +121,7 @@ const noteEditor = (function() {
|
||||
|
||||
let newNoteCreated = false;
|
||||
|
||||
async function createNote(node, parentKey, target, isProtected) {
|
||||
async function createNote(node, parentTreeId, target, isProtected) {
|
||||
// if isProtected isn't available (user didn't enter password yet), then note is created as unencrypted
|
||||
// but this is quite weird since user doesn't see where the note is being created so it shouldn't occur often
|
||||
if (!isProtected || !protected_session.isProtectedSessionAvailable()) {
|
||||
@@ -131,12 +131,12 @@ const noteEditor = (function() {
|
||||
const newNoteName = "new note";
|
||||
|
||||
const result = await $.ajax({
|
||||
url: baseApiUrl + 'notes/' + parentKey + '/children' ,
|
||||
url: baseApiUrl + 'notes/' + parentTreeId + '/children' ,
|
||||
type: 'POST',
|
||||
data: JSON.stringify({
|
||||
note_title: newNoteName,
|
||||
target: target,
|
||||
target_note_id: node.key,
|
||||
target_note_id: node.note_tree_id,
|
||||
is_protected: isProtected
|
||||
}),
|
||||
contentType: "application/json"
|
||||
@@ -144,13 +144,14 @@ const noteEditor = (function() {
|
||||
|
||||
const newNode = {
|
||||
title: newNoteName,
|
||||
key: result.note_id,
|
||||
key: counter++,
|
||||
note_id: result.note_id,
|
||||
note_tree_id: result.note_tree_id,
|
||||
is_protected: isProtected,
|
||||
extraClasses: isProtected ? "protected" : ""
|
||||
};
|
||||
|
||||
glob.allNoteIds.push(result.note_id);
|
||||
glob.allNoteIds.push(result.note_tree_id);
|
||||
|
||||
newNoteCreated = true;
|
||||
|
||||
@@ -167,11 +168,6 @@ const noteEditor = (function() {
|
||||
showMessage("Created!");
|
||||
}
|
||||
|
||||
function setTreeBasedOnProtectedStatus(note) {
|
||||
const node = treeUtils.getNodeByKey(note.detail.note_id);
|
||||
node.toggleClass("protected", !!note.detail.is_protected);
|
||||
}
|
||||
|
||||
function setNoteBackgroundIfProtected(note) {
|
||||
if (note.detail.is_protected) {
|
||||
$(".note-editable").addClass("protected");
|
||||
@@ -184,7 +180,7 @@ const noteEditor = (function() {
|
||||
unprotectButton.hide();
|
||||
}
|
||||
|
||||
setTreeBasedOnProtectedStatus(note);
|
||||
noteTree.setCurrentNoteTreeBasedOnProtectedStatus();
|
||||
}
|
||||
|
||||
async function loadNoteToEditor(noteId) {
|
||||
@@ -217,10 +213,6 @@ const noteEditor = (function() {
|
||||
|
||||
noteDetailEl.summernote('code', currentNote.detail.note_text);
|
||||
|
||||
document.location.hash = noteId;
|
||||
|
||||
recentNotes.addRecentNote(noteId, currentNote.detail.note_id);
|
||||
|
||||
noteChangeDisabled = false;
|
||||
|
||||
setNoteBackgroundIfProtected(currentNote);
|
||||
|
||||
@@ -3,35 +3,59 @@
|
||||
const noteTree = (function() {
|
||||
const noteDetailEl = $('#note-detail');
|
||||
const treeEl = $("#tree");
|
||||
let startNoteId = null;
|
||||
let startNoteTreeId = null;
|
||||
let treeLoadTime = null;
|
||||
let clipboardNoteId = null;
|
||||
let clipboardNoteTreeId = null;
|
||||
let notesMap = {};
|
||||
let parentToNotes = {};
|
||||
let counter = 1;
|
||||
let noteTreeIdToKey = {};
|
||||
|
||||
function getNoteTreeIdFromKey(key) {
|
||||
const node = treeUtils.getNodeByKey(key);
|
||||
|
||||
return node.note_tree_id;
|
||||
}
|
||||
|
||||
function getKeyFromNoteTreeId(noteTreeId) {
|
||||
return noteTreeIdToKey[noteTreeId];
|
||||
}
|
||||
|
||||
function getTreeLoadTime() {
|
||||
return treeLoadTime;
|
||||
}
|
||||
|
||||
function getClipboardNoteId() {
|
||||
return clipboardNoteId;
|
||||
function getClipboardNoteTreeId() {
|
||||
return clipboardNoteTreeId;
|
||||
}
|
||||
|
||||
function setClipboardNoteId(cbNoteId) {
|
||||
clipboardNoteId = cbNoteId;
|
||||
function setClipboardNoteTreeId(cbNoteId) {
|
||||
clipboardNoteTreeId = cbNoteId;
|
||||
}
|
||||
|
||||
function prepareNoteTree() {
|
||||
function prepareNoteTree(notes) { showAppIfHidden();
|
||||
parentToNotes = {};
|
||||
notesMap = {};
|
||||
|
||||
for (const note of notes) {
|
||||
if (!parentToNotes[note.note_pid]) {
|
||||
parentToNotes[note.note_pid] = [];
|
||||
}
|
||||
|
||||
notesMap[note.note_tree_id] = note;
|
||||
parentToNotes[note.note_pid].push(note.note_tree_id);
|
||||
}
|
||||
|
||||
glob.allNoteIds = Object.keys(notesMap);
|
||||
|
||||
return prepareNoteTreeInner(parentToNotes['root']);
|
||||
}
|
||||
|
||||
function prepareNoteTreeInner(noteIds) {
|
||||
function prepareNoteTreeInner(noteTreeIds) {
|
||||
const noteList = [];
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
const note = notesMap[noteId];
|
||||
for (const noteTreeId of noteTreeIds) {
|
||||
const note = notesMap[noteTreeId];
|
||||
|
||||
note.title = note.note_title;
|
||||
|
||||
@@ -39,14 +63,16 @@ const noteTree = (function() {
|
||||
note.extraClasses = "protected";
|
||||
}
|
||||
|
||||
note.key = note.note_id;
|
||||
note.key = counter++ + ""; // key needs to be string
|
||||
note.expanded = note.is_expanded;
|
||||
|
||||
if (parentToNotes[noteId] && parentToNotes[noteId].length > 0) {
|
||||
noteTreeIdToKey[noteTreeId] = note.key;
|
||||
|
||||
if (parentToNotes[noteTreeId] && parentToNotes[noteTreeId].length > 0) {
|
||||
note.folder = true;
|
||||
|
||||
if (note.expanded) {
|
||||
note.children = prepareNoteTreeInner(parentToNotes[noteId], notesMap, parentToNotes);
|
||||
note.children = prepareNoteTreeInner(parentToNotes[noteTreeId], notesMap, parentToNotes);
|
||||
}
|
||||
else {
|
||||
note.lazy = true;
|
||||
@@ -59,27 +85,27 @@ const noteTree = (function() {
|
||||
return noteList;
|
||||
}
|
||||
|
||||
function setExpandedToServer(note_id, is_expanded) {
|
||||
const expandedNum = is_expanded ? 1 : 0;
|
||||
function setExpandedToServer(noteTreeId, isExpanded) {
|
||||
const expandedNum = isExpanded ? 1 : 0;
|
||||
|
||||
$.ajax({
|
||||
url: baseApiUrl + 'notes/' + note_id + '/expanded/' + expandedNum,
|
||||
url: baseApiUrl + 'notes/' + noteTreeId + '/expanded/' + expandedNum,
|
||||
type: 'PUT',
|
||||
contentType: "application/json",
|
||||
success: result => {}
|
||||
});
|
||||
}
|
||||
|
||||
function initFancyTree(notes) {
|
||||
function initFancyTree(noteTree) {
|
||||
const keybindings = {
|
||||
"insert": node => {
|
||||
const parentKey = treeUtils.getParentKey(node);
|
||||
const parentNoteTreeId = treeUtils.getParentNoteTreeId(node);
|
||||
const isProtected = treeUtils.getParentProtectedStatus(node);
|
||||
|
||||
noteEditor.createNote(node, parentKey, 'after', isProtected);
|
||||
noteEditor.createNote(node, parentNoteTreeId, 'after', isProtected);
|
||||
},
|
||||
"ctrl+insert": node => {
|
||||
noteEditor.createNote(node, node.key, 'into', node.data.is_protected);
|
||||
noteEditor.createNote(node, node.note_id, 'into', node.data.is_protected);
|
||||
},
|
||||
"del": node => {
|
||||
treeChanges.deleteNode(node);
|
||||
@@ -116,22 +142,26 @@ const noteTree = (function() {
|
||||
treeEl.fancytree({
|
||||
autoScroll: true,
|
||||
extensions: ["hotkeys", "filter", "dnd"],
|
||||
source: notes,
|
||||
source: noteTree,
|
||||
scrollParent: $("#tree"),
|
||||
activate: (event, data) => {
|
||||
const node = data.node.data;
|
||||
|
||||
document.location.hash = node.note_tree_id;
|
||||
|
||||
recentNotes.addRecentNote(node.note_tree_id);
|
||||
|
||||
noteEditor.switchToNote(node.note_id);
|
||||
},
|
||||
expand: (event, data) => {
|
||||
setExpandedToServer(data.node.key, true);
|
||||
setExpandedToServer(getNoteTreeIdFromKey(data.node.key), true);
|
||||
},
|
||||
collapse: (event, data) => {
|
||||
setExpandedToServer(data.node.key, false);
|
||||
setExpandedToServer(getNoteTreeIdFromKey(data.node.key), false);
|
||||
},
|
||||
init: (event, data) => {
|
||||
if (startNoteId) {
|
||||
data.tree.activateKey(startNoteId);
|
||||
if (startNoteTreeId) {
|
||||
treeUtils.activateNode(startNoteTreeId);
|
||||
}
|
||||
},
|
||||
hotkeys: {
|
||||
@@ -197,13 +227,14 @@ const noteTree = (function() {
|
||||
}
|
||||
},
|
||||
lazyLoad: function(event, data){
|
||||
const node = data.node;
|
||||
const node = data.node.data;
|
||||
const noteTreeId = node.note_tree_id;
|
||||
|
||||
if (parentToNotes[node.key]) {
|
||||
data.result = prepareNoteTreeInner(parentToNotes[node.key]);
|
||||
if (parentToNotes[noteTreeId]) {
|
||||
data.result = prepareNoteTreeInner(parentToNotes[noteTreeId]);
|
||||
}
|
||||
else {
|
||||
console.log("No children. This shouldn't happen.");
|
||||
console.log("No children for " + noteTreeId + ". This shouldn't happen.");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -212,28 +243,26 @@ const noteTree = (function() {
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
const notesMap = await loadTree();
|
||||
const notes = await loadTree();
|
||||
|
||||
// this will also reload the note content
|
||||
await treeEl.fancytree('getTree').reload(notesMap);
|
||||
await treeEl.fancytree('getTree').reload(notes);
|
||||
}
|
||||
|
||||
function loadTree() {
|
||||
return $.get(baseApiUrl + 'tree').then(resp => {
|
||||
notesMap = resp.notes_map;
|
||||
parentToNotes = resp.parent_to_notes;
|
||||
startNoteId = resp.start_note_id;
|
||||
startNoteTreeId = resp.start_note_tree_id;
|
||||
treeLoadTime = resp.tree_load_time;
|
||||
|
||||
if (document.location.hash) {
|
||||
startNoteId = document.location.hash.substr(1); // strip initial #
|
||||
startNoteTreeId = document.location.hash.substr(1); // strip initial #
|
||||
}
|
||||
|
||||
return prepareNoteTree();
|
||||
return prepareNoteTree(resp.notes);
|
||||
});
|
||||
}
|
||||
|
||||
$(() => loadTree().then(notesMap => initFancyTree(notesMap)));
|
||||
$(() => loadTree().then(noteTree => initFancyTree(noteTree)));
|
||||
|
||||
function collapseTree() {
|
||||
treeEl.fancytree("getRootNode").visit(node => {
|
||||
@@ -244,7 +273,7 @@ const noteTree = (function() {
|
||||
$(document).bind('keydown', 'alt+c', collapseTree);
|
||||
|
||||
function scrollToCurrentNote() {
|
||||
const node = treeUtils.getNodeByKey(noteEditor.getCurrentNoteId());
|
||||
const node = treeUtils.getNodeByNoteTreeId(noteEditor.getCurrentNoteId());
|
||||
|
||||
if (node) {
|
||||
node.makeVisible({scrollIntoView: true});
|
||||
@@ -281,6 +310,22 @@ const noteTree = (function() {
|
||||
return notesMap[noteId];
|
||||
}
|
||||
|
||||
// note that if you want to access data like note_id or is_protected, you need to go into "data" property
|
||||
function getCurrentNode() {
|
||||
return treeEl.fancytree("getActiveNode");
|
||||
}
|
||||
|
||||
function getCurrentNoteTreeId() {
|
||||
const node = getCurrentNode();
|
||||
return node.note_tree_id;
|
||||
}
|
||||
|
||||
function setCurrentNoteTreeBasedOnProtectedStatus() {
|
||||
const node = getCurrentNode();
|
||||
|
||||
node.toggleClass("protected", !!node.data.is_protected);
|
||||
}
|
||||
|
||||
$("button#reset-search-button").click(resetSearch);
|
||||
|
||||
$("input[name=search]").keyup(e => {
|
||||
@@ -308,12 +353,17 @@ const noteTree = (function() {
|
||||
|
||||
return {
|
||||
getTreeLoadTime,
|
||||
getClipboardNoteId,
|
||||
setClipboardNoteId,
|
||||
getClipboardNoteTreeId,
|
||||
setClipboardNoteTreeId,
|
||||
reload,
|
||||
collapseTree,
|
||||
scrollToCurrentNote,
|
||||
toggleSearch,
|
||||
getByNoteId
|
||||
getByNoteId,
|
||||
getKeyFromNoteTreeId,
|
||||
getNoteTreeIdFromKey,
|
||||
setCurrentNoteTreeBasedOnProtectedStatus,
|
||||
getCurrentNode,
|
||||
getCurrentNoteTreeId,
|
||||
};
|
||||
})();
|
||||
@@ -37,7 +37,7 @@ const protected_session = (function() {
|
||||
open: () => {
|
||||
if (!modal) {
|
||||
// dialog steals focus for itself, which is not what we want for non-modal (viewing)
|
||||
treeUtils.getNodeByKey(noteEditor.getCurrentNoteId()).setFocus();
|
||||
noteTree.getCurrentNode().setFocus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ const treeChanges = (function() {
|
||||
|
||||
glob.allNoteIds = glob.allNoteIds.filter(e => e !== node.key);
|
||||
|
||||
recentNotes.removeRecentNote(node.key);
|
||||
recentNotes.removeRecentNote(node.note_tree_id);
|
||||
|
||||
let next = node.getNextSibling();
|
||||
if (!next) {
|
||||
|
||||
@@ -3,37 +3,41 @@
|
||||
const treeUtils = (function() {
|
||||
const treeEl = $("#tree");
|
||||
|
||||
function getParentKey(node) {
|
||||
return (node.getParent() === null || node.getParent().key === "root_1") ? "root" : node.getParent().key;
|
||||
function getParentNoteTreeId(node) {
|
||||
return node.note_pid;
|
||||
}
|
||||
|
||||
function getParentProtectedStatus(node) {
|
||||
return node.getParent() === null ? 0 : node.getParent().data.is_protected;
|
||||
}
|
||||
|
||||
function getNodeByKey(noteId) {
|
||||
return treeEl.fancytree('getNodeByKey', noteId);
|
||||
function getNodeByKey(key) {
|
||||
return treeEl.fancytree('getNodeByKey', key);
|
||||
}
|
||||
|
||||
async function activateNode(noteIdToActivate) {
|
||||
const noteIdPath = [ noteIdToActivate ];
|
||||
function getNodeByNoteTreeId(noteTreeId) {
|
||||
const key = noteTree.getKeyFromNoteTreeId(noteTreeId);
|
||||
|
||||
let note = noteTree.getByNoteId(noteIdToActivate);
|
||||
return getNodeByKey(key);
|
||||
}
|
||||
|
||||
async function activateNode(noteTreeIdToActivate) {
|
||||
const noteTreeIdPath = [ noteTreeIdToActivate ];
|
||||
|
||||
let note = noteTree.getByNoteId(noteTreeIdToActivate);
|
||||
|
||||
while (note) {
|
||||
if (note.note_pid !== 'root') {
|
||||
noteIdPath.push(note.note_pid);
|
||||
noteTreeIdPath.push(note.note_pid);
|
||||
}
|
||||
|
||||
note = noteTree.getByNoteId(note.note_pid);
|
||||
}
|
||||
|
||||
for (const noteId of noteIdPath.reverse()) {
|
||||
console.log("Activating/expanding " + noteId);
|
||||
for (const noteTreeId of noteTreeIdPath.reverse()) {
|
||||
const node = treeUtils.getNodeByNoteTreeId(noteTreeId);
|
||||
|
||||
const node = treeUtils.getNodeByKey(noteId);
|
||||
|
||||
if (noteId !== noteIdToActivate) {
|
||||
if (noteTreeId !== noteTreeIdToActivate) {
|
||||
await node.setExpanded();
|
||||
}
|
||||
else {
|
||||
@@ -57,8 +61,8 @@ const treeUtils = (function() {
|
||||
return noteTitle;
|
||||
}
|
||||
|
||||
function getFullName(noteId) {
|
||||
let note = noteTree.getByNoteId(noteId);
|
||||
function getFullName(noteTreeId) {
|
||||
let note = noteTree.getByNoteId(noteTreeId);
|
||||
|
||||
if (note === null) {
|
||||
return "[unknown]";
|
||||
@@ -76,9 +80,10 @@ const treeUtils = (function() {
|
||||
}
|
||||
|
||||
return {
|
||||
getParentKey,
|
||||
getParentNoteTreeId,
|
||||
getParentProtectedStatus,
|
||||
getNodeByKey,
|
||||
getNodeByNoteTreeId,
|
||||
activateNode,
|
||||
getNoteTitle,
|
||||
getFullName
|
||||
|
||||
Reference in New Issue
Block a user