mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Move all persistent node metadata into the network
This commit is contained in:
@@ -242,7 +242,12 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
// Take until another layer node is found (but not the first layer node)
|
||||
let existing_node_id = upstream
|
||||
.take_while(|node_id| is_traversal_start(*node_id) || !self.network_interface.is_layer(node_id, &[]))
|
||||
.find(|node_id| self.network_interface.reference(node_id, &[]).is_some_and(|node_reference| node_reference == reference));
|
||||
.find(|node_id| {
|
||||
let Some(node_reference) = self.network_interface.reference(node_id, &[]) else {
|
||||
log::error!("Node reference does not exist in ModifyInputsContext::existing_node_id");
|
||||
return false;
|
||||
};
|
||||
node_reference.as_ref().is_some_and(|node_reference| node_reference == reference)});
|
||||
|
||||
// Create a new node if the node does not exist and update its inputs
|
||||
existing_node_id.or_else(|| {
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::node_properties;
|
||||
use super::utility_types::FrontendNodeType;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{
|
||||
DocumentNodeMetadata, DocumentNodePersistentMetadata, NodeNetworkInterface, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate, NodeTypePersistentMetadata,
|
||||
DocumentNodeMetadata, DocumentNodePersistentMetadata, NodeNetworkInterface, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate,
|
||||
};
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::prelude::Message;
|
||||
|
||||
@@ -1087,9 +1087,11 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
||||
input,
|
||||
});
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
if (!network_interface.reference(&node_id, selection_network_path).is_some_and(|reference| reference == "Imaginate") || input_index == 0)
|
||||
&& network_interface.connected_to_output(&node_id, selection_network_path)
|
||||
{
|
||||
let Some(reference) = network_interface.reference(&node_id, selection_network_path) else {
|
||||
log::error!("Could not get reference for node: {node_id:?} in NodeGraphMessage::SetInputValue");
|
||||
return;
|
||||
};
|
||||
if (!reference.as_ref().is_some_and(|reference| reference == "Imaginate") || input_index == 0) && network_interface.connected_to_output(&node_id, selection_network_path) {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
@@ -1182,12 +1184,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
||||
responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids })
|
||||
}
|
||||
NodeGraphMessage::ToggleLocked { node_id } => {
|
||||
let Some(node_metadata) = network_interface.network_metadata(&[]).unwrap().persistent_metadata.node_metadata.get(&node_id) else {
|
||||
log::error!("Cannot get node {:?} in NodeGraphMessage::ToggleLocked", node_id);
|
||||
return;
|
||||
};
|
||||
|
||||
let locked = !node_metadata.persistent_metadata.locked;
|
||||
let locked = !network_interface.is_locked(&node_id, &[]);
|
||||
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
responses.add(NodeGraphMessage::SetLocked { node_id, locked });
|
||||
@@ -1637,17 +1634,19 @@ impl NodeGraphMessageHandler {
|
||||
log::error!("Could not get nested network when collecting nodes");
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(network_metadata) = network_interface.network_metadata(breadcrumb_network_path) else {
|
||||
log::error!("Could not get network_metadata when collecting nodes");
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
for (&node_id, node) in &network.nodes {
|
||||
let node_id_path = &[breadcrumb_network_path, (&[node_id])].concat();
|
||||
let Some(node_metadata) = network_metadata.persistent_metadata.node_metadata.get(&node_id) else {
|
||||
log::error!("Could not get node_metadata for {node_id_path:?}");
|
||||
continue;
|
||||
|
||||
let Some(input_names) = network_interface.input_names(&node_id, breadcrumb_network_path) else {
|
||||
log::error!("Could not get input names for node: {node_id}");
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let Some(output_names) = network_interface.output_names(&node_id, breadcrumb_network_path) else {
|
||||
log::error!("Could not get output names for node: {node_id}");
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let frontend_graph_inputs = node.inputs.iter().enumerate().map(|(index, _)| {
|
||||
@@ -1657,9 +1656,7 @@ impl NodeGraphMessageHandler {
|
||||
// TODO: Should display the color of the "most commonly relevant" (we'd need some sort of precedence) data type it allows given the current generic form that's constrained by the other present connections.
|
||||
let data_type = FrontendGraphDataType::with_type(&node_type);
|
||||
|
||||
let input_name = node_metadata
|
||||
.persistent_metadata
|
||||
.input_names
|
||||
let input_name = input_names
|
||||
.get(index)
|
||||
.cloned()
|
||||
.unwrap_or(network_interface.input_type(&InputConnector::node(node_id, index), breadcrumb_network_path).nested_type().to_string());
|
||||
@@ -1727,16 +1724,8 @@ impl NodeGraphMessageHandler {
|
||||
} else {
|
||||
FrontendGraphDataType::General
|
||||
};
|
||||
let Some(node_metadata) = network_metadata.persistent_metadata.node_metadata.get(&node_id) else {
|
||||
log::error!("Could not get node_metadata when getting output for {node_id}");
|
||||
continue;
|
||||
};
|
||||
let output_name = node_metadata
|
||||
.persistent_metadata
|
||||
.output_names
|
||||
.get(index)
|
||||
.map(|output_name| output_name.to_string())
|
||||
.unwrap_or(format!("Output {}", index + 1));
|
||||
|
||||
let output_name = output_names.get(index).map(|output_name| output_name.to_string()).unwrap_or(format!("Output {}", index + 1));
|
||||
|
||||
let connected_to = outward_wires.get(&OutputConnector::node(node_id, index)).cloned().unwrap_or_default();
|
||||
exposed_outputs.push(FrontendGraphOutput {
|
||||
@@ -1776,9 +1765,7 @@ impl NodeGraphMessageHandler {
|
||||
|
||||
nodes.push(FrontendNode {
|
||||
id: node_id,
|
||||
is_layer: network_interface
|
||||
.node_metadata(&node_id, breadcrumb_network_path)
|
||||
.is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()),
|
||||
is_layer: network_interface.is_layer(&node_id, breadcrumb_network_path),
|
||||
can_be_layer: can_be_layer_lookup.contains(&node_id),
|
||||
reference: None,
|
||||
display_name: network_interface.frontend_display_name(&node_id, breadcrumb_network_path),
|
||||
@@ -1836,8 +1823,8 @@ impl NodeGraphMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
for (&node_id, node_metadata) in &network_interface.network_metadata(&[]).unwrap().persistent_metadata.node_metadata {
|
||||
if node_metadata.persistent_metadata.is_layer() {
|
||||
for &node_id in network_interface.network(&[]).unwrap().nodes.keys() {
|
||||
if network_interface.is_layer(&node_id, &[]) {
|
||||
let layer = LayerNodeIdentifier::new(node_id, network_interface, &[]);
|
||||
|
||||
let children_allowed =
|
||||
|
||||
@@ -2343,7 +2343,15 @@ pub fn index_properties(document_node: &DocumentNode, node_id: NodeId, _context:
|
||||
}
|
||||
|
||||
pub fn generate_node_properties(document_node: &DocumentNode, node_id: NodeId, context: &mut NodePropertiesContext) -> LayoutGroup {
|
||||
let reference = context.network_interface.reference(&node_id, context.selection_network_path).clone();
|
||||
let Some(reference) = context.network_interface.reference(&node_id, context.selection_network_path).cloned() else {
|
||||
log::error!("Node {node_id} has no reference in generate_node_properties");
|
||||
return LayoutGroup::Section {
|
||||
name: "Unknown".to_string(),
|
||||
visible: true,
|
||||
id: node_id.0,
|
||||
layout: unknown_node_properties(&"Unknown".to_string()),
|
||||
};
|
||||
};
|
||||
let layout = if let Some(ref reference) = reference {
|
||||
match super::document_node_definitions::resolve_document_node_type(reference) {
|
||||
Some(document_node_type) => (document_node_type.properties)(document_node, node_id, context),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -415,15 +415,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
||||
.cloned()
|
||||
.collect::<Vec<NodeId>>()
|
||||
{
|
||||
if let Some(reference) = document
|
||||
.network_interface
|
||||
.network_metadata(&[])
|
||||
.unwrap()
|
||||
.persistent_metadata
|
||||
.node_metadata
|
||||
.get(node_id)
|
||||
.and_then(|node| node.persistent_metadata.reference.as_ref())
|
||||
{
|
||||
let Some(reference) = document.network_interface.reference(&node_id, &[]) else {
|
||||
log::error!("could not get reference in deserialize_document");
|
||||
continue;
|
||||
};
|
||||
if let Some(reference) = reference {
|
||||
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
|
||||
let default_definition_node = node_definition.default_node_template();
|
||||
document.network_interface.set_implementation(node_id, &[], default_definition_node.document_node.implementation);
|
||||
@@ -433,14 +429,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
||||
|
||||
if document
|
||||
.network_interface
|
||||
.network_metadata(&[])
|
||||
.network(&[])
|
||||
.unwrap()
|
||||
.persistent_metadata
|
||||
.node_metadata
|
||||
.iter()
|
||||
.any(|(node_id, node)| node.persistent_metadata.reference.as_ref().is_some_and(|reference| reference == "Output") && *node_id == NodeId(0))
|
||||
.nodes
|
||||
.keys()
|
||||
.any(|node_id|*node_id == NodeId(0) && document.network_interface.reference(node_id, &[]).cloned().flatten().is_some_and(|reference| reference == "Output"))
|
||||
{
|
||||
document.network_interface.delete_nodes(vec![NodeId(0)], true, &[]);
|
||||
document.network_interface.delete_nodes(vec![NodeId(0)], false, &[]);
|
||||
}
|
||||
|
||||
let node_ids = document.network_interface.network(&[]).unwrap().nodes.keys().cloned().collect::<Vec<_>>();
|
||||
@@ -449,14 +444,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
||||
log::error!("could not get node in deserialize_document");
|
||||
continue;
|
||||
};
|
||||
let Some(node_metadata) = document.network_interface.network_metadata(&[]).unwrap().persistent_metadata.node_metadata.get(node_id) else {
|
||||
log::error!("could not get node metadata for node {node_id} in deserialize_document");
|
||||
continue;
|
||||
};
|
||||
|
||||
// Upgrade Fill nodes to the format change in #1778
|
||||
// TODO: Eventually remove this (probably starting late 2024)
|
||||
let Some(ref reference) = node_metadata.persistent_metadata.reference.clone() else {
|
||||
let Some(ref reference) = document.network_interface.reference(node_id, &[]).cloned().flatten() else {
|
||||
continue;
|
||||
};
|
||||
if reference == "Fill" && node.inputs.len() == 8 {
|
||||
|
||||
@@ -174,8 +174,14 @@ impl<'a> NodeGraphLayer<'a> {
|
||||
|
||||
/// Node id of a node if it exists in the layer's primary flow
|
||||
pub fn upstream_node_id_from_name(&self, node_name: &str) -> Option<NodeId> {
|
||||
|
||||
self.horizontal_layer_flow()
|
||||
.find(|node_id| self.network_interface.reference(node_id, &[]).is_some_and(|reference| reference == node_name))
|
||||
.find(|node_id| {
|
||||
let Some(reference) = self.network_interface.reference(node_id, &[]) else {
|
||||
log::error!("Reference could not be found for node {node_id} in upstream_node_id_from_name");
|
||||
return false };
|
||||
reference.as_ref().is_some_and(|reference| reference == node_name)
|
||||
})
|
||||
}
|
||||
|
||||
/// Find all of the inputs of a specific node within the layer's primary flow, up until the next layer is reached.
|
||||
@@ -183,7 +189,13 @@ impl<'a> NodeGraphLayer<'a> {
|
||||
self.horizontal_layer_flow()
|
||||
.skip(1)// Skip self
|
||||
.take_while(|node_id| !self.network_interface.is_layer(node_id,&[]))
|
||||
.find(|node_id| self.network_interface.reference(node_id,&[]).is_some_and(|reference| reference == node_name))
|
||||
.find(|node_id|
|
||||
{
|
||||
let Some(reference) = self.network_interface.reference(node_id, &[]) else {
|
||||
log::error!("Reference could not be found for node {node_id} in upstream_node_id_from_name");
|
||||
return false };
|
||||
reference.as_ref().is_some_and(|reference| reference == node_name)
|
||||
})
|
||||
.and_then(|node_id| self.network_interface.network(&[]).unwrap().nodes.get(&node_id).map(|node| &node.inputs))
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::messages::portfolio::document::graph_operation::transform_utils::{get
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::FlowType;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::NodeId;
|
||||
@@ -272,6 +272,10 @@ impl BrushToolData {
|
||||
continue;
|
||||
};
|
||||
let Some(reference) = document.network_interface.reference(&node_id, &[]) else {
|
||||
log::error!("Could not get reference for node {node_id} in load_existing_strokes");
|
||||
continue;
|
||||
};
|
||||
let Some(reference) = reference else {
|
||||
continue;
|
||||
};
|
||||
if reference == "Brush" && node_id != layer.to_node() {
|
||||
|
||||
@@ -702,13 +702,12 @@ impl EditorHandle {
|
||||
let document = editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap();
|
||||
for node in document
|
||||
.network_interface
|
||||
.network_metadata(&[])
|
||||
.network(&[])
|
||||
.unwrap()
|
||||
.persistent_metadata
|
||||
.node_metadata
|
||||
.iter()
|
||||
.filter(|(_, d)| d.persistent_metadata.reference.as_ref().is_some_and(|reference| reference == "Artboard"))
|
||||
.map(|(id, _)| *id)
|
||||
.nodes
|
||||
.keys()
|
||||
.filter(|node_id| document.network_interface.reference(node_id, &[]).cloned().flatten().is_some_and(|reference| reference == "Artboard"))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
let Some(document_node) = document.network_interface.network(&[]).unwrap().nodes.get(&node) else {
|
||||
@@ -718,7 +717,12 @@ impl EditorHandle {
|
||||
if let Some(network) = document_node.implementation.get_network() {
|
||||
let mut nodes_to_upgrade = Vec::new();
|
||||
for (node_id, _) in network.nodes.iter().collect::<Vec<_>>() {
|
||||
if document.network_interface.reference(node_id, &[]).is_some_and(|reference| reference == "To Artboard")
|
||||
if document
|
||||
.network_interface
|
||||
.reference(node_id, &[])
|
||||
.cloned()
|
||||
.flatten()
|
||||
.is_some_and(|reference| reference == "To Artboard")
|
||||
&& document
|
||||
.network_interface
|
||||
.network(&[])
|
||||
@@ -773,13 +777,12 @@ impl EditorHandle {
|
||||
document.network_interface.load_structure();
|
||||
for node in document
|
||||
.network_interface
|
||||
.network_metadata(&[])
|
||||
.network(&[])
|
||||
.unwrap()
|
||||
.persistent_metadata
|
||||
.node_metadata
|
||||
.iter()
|
||||
.filter(|(_, d)| d.persistent_metadata.reference.as_ref().is_some_and(|reference| reference == "Merge"))
|
||||
.map(|(id, _)| *id)
|
||||
.nodes
|
||||
.keys()
|
||||
.filter(|node_id| document.network_interface.reference(node_id, &[]).cloned().flatten().is_some_and(|reference| reference == "Merge"))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
let layer = LayerNodeIdentifier::new(node, &document.network_interface, &[]);
|
||||
|
||||
@@ -12,7 +12,7 @@ use glam::{DVec2, IVec2};
|
||||
use log::Metadata;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
pub mod value;
|
||||
@@ -1630,6 +1630,106 @@ pub struct NetworkEdgeDistance {
|
||||
/// The viewport pixel distance between the left edge of the node graph and the imports.
|
||||
pub imports_to_edge_distance: DVec2,
|
||||
}
|
||||
|
||||
/// A layer can either be position as Absolute or in a Stack
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Hash, DynAny)]
|
||||
pub enum LayerPosition {
|
||||
// Position of the layer in grid spaces. Measured from the top left corner of the layer, not including the left chain. This means it is always half a grid space to the left of the thumbnail.
|
||||
Absolute(IVec2),
|
||||
// A layer is in a Stack when it feeds into the bottom input of a layer. The Y position stores the vertical distance between the layer and its upstream sibling/parent.
|
||||
Stack(u32),
|
||||
}
|
||||
|
||||
/// A node can either be position as Absolute or in a Chain
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Hash, DynAny)]
|
||||
pub enum NodePosition {
|
||||
// Position of the node in grid spaces
|
||||
Absolute(IVec2),
|
||||
// In a chain the position is based on the number of nodes to the first layer node
|
||||
Chain,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Hash, DynAny)]
|
||||
pub enum NodeTypePersistentMetadata {
|
||||
Layer(LayerMetadata),
|
||||
Node(NodePersistentMetadata),
|
||||
}
|
||||
|
||||
impl Default for NodeTypePersistentMetadata {
|
||||
fn default() -> Self {
|
||||
NodeTypePersistentMetadata::node(IVec2::ZERO)
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeTypePersistentMetadata {
|
||||
pub fn node(position: IVec2) -> NodeTypePersistentMetadata {
|
||||
NodeTypePersistentMetadata::Node(NodePersistentMetadata {
|
||||
position: NodePosition::Absolute(position),
|
||||
})
|
||||
}
|
||||
pub fn layer(position: IVec2) -> NodeTypePersistentMetadata {
|
||||
NodeTypePersistentMetadata::Layer(LayerMetadata {
|
||||
persistent_metadata: LayerPersistentMetadata {
|
||||
position: LayerPosition::Absolute(position),
|
||||
},
|
||||
transient_metadata: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, DynAny)]
|
||||
pub struct LayerMetadata {
|
||||
pub persistent_metadata: LayerPersistentMetadata,
|
||||
#[serde(skip)]
|
||||
pub transient_metadata: LayerTransientMetadata,
|
||||
}
|
||||
|
||||
impl Hash for LayerMetadata {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.persistent_metadata.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for LayerMetadata {
|
||||
fn clone(&self) -> Self {
|
||||
LayerMetadata {
|
||||
persistent_metadata: self.persistent_metadata.clone(),
|
||||
transient_metadata: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for LayerMetadata {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.persistent_metadata == other.persistent_metadata
|
||||
}
|
||||
}
|
||||
|
||||
/// All fields in LayerMetadata should automatically be updated by using the network interface API
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Hash, DynAny)]
|
||||
pub struct LayerPersistentMetadata {
|
||||
/// Stores the position of a layer node, which can either be Absolute or Stack
|
||||
pub position: LayerPosition,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, DynAny)]
|
||||
pub struct LayerTransientMetadata {
|
||||
/// All nodes that should be moved when the layer is moved.
|
||||
pub owned_nodes: TransientMetadata<HashSet<NodeId>>,
|
||||
// Stores the width in grid cell units for layer nodes from the left edge of the thumbnail (+12px padding since thumbnail ends between grid spaces) to the left end of the node
|
||||
/// This is necessary since calculating the layer width through web_sys is very slow
|
||||
pub layer_width: TransientMetadata<u32>,
|
||||
// Should not be a performance concern to calculate when needed with chain_width.
|
||||
// Stores the width in grid cell units for layer nodes from the left edge of the thumbnail to the end of the chain
|
||||
// chain_width: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Hash, DynAny)]
|
||||
pub struct NodePersistentMetadata {
|
||||
/// Stores the position of a non layer node, which can either be Absolute or Chain
|
||||
pub position: NodePosition,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
@@ -189,6 +189,10 @@ tagged_value! {
|
||||
OutwardWires(TransientMetadata<HashMap<crate::document::OutputConnector, Vec<crate::document::InputConnector>>>),
|
||||
ImportExportPorts(TransientMetadata<crate::document::Ports>),
|
||||
RoundedNetworkEdgeDistance(TransientMetadata<crate::document::NetworkEdgeDistance>),
|
||||
// Persistent Node Metadata
|
||||
OptionalString(Option<String>),
|
||||
VecString(Vec<String>),
|
||||
NodeTypeMetadata(crate::document::NodeTypePersistentMetadata),
|
||||
}
|
||||
|
||||
impl TaggedValue {
|
||||
|
||||
Reference in New Issue
Block a user