Remove 'Preview Node' operator #1
@ -32,10 +32,6 @@ def register():
|
|||||||
name="Source Socket!",
|
name="Source Socket!",
|
||||||
default=0,
|
default=0,
|
||||||
description="An internal property used to store the source socket in a Lazy Connect operation")
|
description="An internal property used to store the source socket in a Lazy Connect operation")
|
||||||
bpy.types.NodeTreeInterfaceSocket.NWViewerSocket = BoolProperty(
|
|
||||||
name="NW Socket",
|
|
||||||
default=False,
|
|
||||||
description="An internal property used to determine if a socket is generated by the addon")
|
|
||||||
|
|
||||||
operators.register()
|
operators.register()
|
||||||
interface.register()
|
interface.register()
|
||||||
@ -51,5 +47,4 @@ def unregister():
|
|||||||
del bpy.types.Scene.NWBusyDrawing
|
del bpy.types.Scene.NWBusyDrawing
|
||||||
del bpy.types.Scene.NWLazySource
|
del bpy.types.Scene.NWLazySource
|
||||||
del bpy.types.Scene.NWLazyTarget
|
del bpy.types.Scene.NWLazyTarget
|
||||||
del bpy.types.Scene.NWSourceSocket
|
del bpy.types.Scene.NWSourceSocket
|
||||||
del bpy.types.NodeTreeInterfaceSocket.NWViewerSocket
|
|
@ -27,11 +27,11 @@ from .interface import NWConnectionListInputs, NWConnectionListOutputs
|
|||||||
from .utils.constants import blend_types, geo_combine_operations, operations, navs, get_texture_node_types, rl_outputs
|
from .utils.constants import blend_types, geo_combine_operations, operations, navs, get_texture_node_types, rl_outputs
|
||||||
from .utils.draw import draw_callback_nodeoutline
|
from .utils.draw import draw_callback_nodeoutline
|
||||||
from .utils.paths import match_files_to_socket_names, split_into_components
|
from .utils.paths import match_files_to_socket_names, split_into_components
|
||||||
from .utils.nodes import (node_mid_pt, autolink, node_at_pos, get_nodes_links, is_viewer_socket, is_viewer_link,
|
from .utils.nodes import (node_mid_pt, autolink, node_at_pos, get_nodes_links,
|
||||||
get_group_output_node, get_output_location, force_update, get_internal_socket, nw_check,
|
get_group_output_node, get_output_location, force_update, get_internal_socket, nw_check,
|
||||||
nw_check_not_empty, nw_check_selected, nw_check_active, nw_check_space_type,
|
nw_check_not_empty, nw_check_selected, nw_check_active, nw_check_space_type,
|
||||||
nw_check_node_type, nw_check_visible_outputs, nw_check_viewer_node, NWBase,
|
nw_check_node_type, nw_check_visible_outputs, nw_check_viewer_node, NWBase,
|
||||||
get_first_enabled_output, is_visible_socket, viewer_socket_name)
|
get_first_enabled_output, is_visible_socket)
|
||||||
|
|
||||||
class NWLazyMix(Operator, NWBase):
|
class NWLazyMix(Operator, NWBase):
|
||||||
"""Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
|
"""Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
|
||||||
@ -486,323 +486,6 @@ class NWAddAttrNode(Operator, NWBase):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class NWPreviewNode(Operator, NWBase):
|
|
||||||
bl_idname = "node.nw_preview_node"
|
|
||||||
bl_label = "Preview Node"
|
|
||||||
bl_description = "Connect active node to the Node Group output or the Material Output"
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
# If false, the operator is not executed if the current node group happens to be a geometry nodes group.
|
|
||||||
# This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
|
|
||||||
run_in_geometry_nodes: BoolProperty(default=True)
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.shader_output_type = ""
|
|
||||||
self.shader_output_ident = ""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
"""Already implemented natively for compositing nodes."""
|
|
||||||
return (nw_check(cls, context) and nw_check_not_empty(cls, context)
|
|
||||||
and nw_check_space_type(cls, context, {'ShaderNodeTree', 'GeometryNodeTree'}))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_output_sockets(node_tree):
|
|
||||||
return [item for item in node_tree.interface.items_tree
|
|
||||||
if item.item_type == 'SOCKET' and item.in_out in {'OUTPUT', 'BOTH'}]
|
|
||||||
|
|
||||||
def init_shader_variables(self, space, shader_type):
|
|
||||||
if shader_type == 'OBJECT':
|
|
||||||
if space.id in bpy.data.lights.values():
|
|
||||||
self.shader_output_type = "OUTPUT_LIGHT"
|
|
||||||
self.shader_output_ident = "ShaderNodeOutputLight"
|
|
||||||
else:
|
|
||||||
self.shader_output_type = "OUTPUT_MATERIAL"
|
|
||||||
self.shader_output_ident = "ShaderNodeOutputMaterial"
|
|
||||||
|
|
||||||
elif shader_type == 'WORLD':
|
|
||||||
self.shader_output_type = "OUTPUT_WORLD"
|
|
||||||
self.shader_output_ident = "ShaderNodeOutputWorld"
|
|
||||||
|
|
||||||
def ensure_viewer_socket(self, node_tree, socket_type, connect_socket=None):
|
|
||||||
"""Check if a viewer output already exists in a node group, otherwise create it"""
|
|
||||||
viewer_socket = None
|
|
||||||
output_sockets = self.get_output_sockets(node_tree)
|
|
||||||
if len(output_sockets):
|
|
||||||
for i, socket in enumerate(output_sockets):
|
|
||||||
if is_viewer_socket(socket) and socket.socket_type == socket_type:
|
|
||||||
# If viewer output is already used but leads to the same socket we can still use it
|
|
||||||
is_used = self.has_socket_other_users(socket)
|
|
||||||
if is_used:
|
|
||||||
if connect_socket is None:
|
|
||||||
continue
|
|
||||||
groupout = get_group_output_node(node_tree)
|
|
||||||
groupout_input = groupout.inputs[i]
|
|
||||||
links = groupout_input.links
|
|
||||||
if connect_socket not in [link.from_socket for link in links]:
|
|
||||||
continue
|
|
||||||
viewer_socket = socket
|
|
||||||
break
|
|
||||||
|
|
||||||
if viewer_socket is None:
|
|
||||||
# Create viewer socket
|
|
||||||
viewer_socket = node_tree.interface.new_socket(
|
|
||||||
viewer_socket_name, in_out='OUTPUT', socket_type=socket_type)
|
|
||||||
viewer_socket.NWViewerSocket = True
|
|
||||||
return viewer_socket
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def ensure_group_output(node_tree):
|
|
||||||
"""Check if a group output node exists, otherwise create it"""
|
|
||||||
groupout = get_group_output_node(node_tree)
|
|
||||||
if groupout is None:
|
|
||||||
groupout = node_tree.nodes.new('NodeGroupOutput')
|
|
||||||
loc_x, loc_y = get_output_location(node_tree)
|
|
||||||
groupout.location.x = loc_x
|
|
||||||
groupout.location.y = loc_y
|
|
||||||
groupout.select = False
|
|
||||||
# So that we don't keep on adding new group outputs
|
|
||||||
groupout.is_active_output = True
|
|
||||||
return groupout
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def search_sockets(cls, node, sockets, index=None):
|
|
||||||
"""Recursively scan nodes for viewer sockets and store them in a list"""
|
|
||||||
for i, input_socket in enumerate(node.inputs):
|
|
||||||
if index and i != index:
|
|
||||||
continue
|
|
||||||
if len(input_socket.links):
|
|
||||||
link = input_socket.links[0]
|
|
||||||
next_node = link.from_node
|
|
||||||
external_socket = link.from_socket
|
|
||||||
if hasattr(next_node, "node_tree"):
|
|
||||||
for socket_index, socket in enumerate(next_node.node_tree.interface.items_tree):
|
|
||||||
if socket.identifier == external_socket.identifier:
|
|
||||||
break
|
|
||||||
if is_viewer_socket(socket) and socket not in sockets:
|
|
||||||
sockets.append(socket)
|
|
||||||
# continue search inside of node group but restrict socket to where we came from
|
|
||||||
groupout = get_group_output_node(next_node.node_tree)
|
|
||||||
cls.search_sockets(groupout, sockets, index=socket_index)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def scan_nodes(cls, tree, sockets):
|
|
||||||
"""Recursively get all viewer sockets in a material tree"""
|
|
||||||
for node in tree.nodes:
|
|
||||||
if hasattr(node, "node_tree"):
|
|
||||||
if node.node_tree is None:
|
|
||||||
continue
|
|
||||||
for socket in cls.get_output_sockets(node.node_tree):
|
|
||||||
if is_viewer_socket(socket) and (socket not in sockets):
|
|
||||||
sockets.append(socket)
|
|
||||||
cls.scan_nodes(node.node_tree, sockets)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def remove_socket(tree, socket):
|
|
||||||
interface = tree.interface
|
|
||||||
interface.remove(socket)
|
|
||||||
interface.active_index = min(interface.active_index, len(interface.items_tree) - 1)
|
|
||||||
|
|
||||||
def link_leads_to_used_socket(self, link):
|
|
||||||
"""Return True if link leads to a socket that is already used in this node"""
|
|
||||||
socket = get_internal_socket(link.to_socket)
|
|
||||||
return socket and self.is_socket_used_active_tree(socket)
|
|
||||||
|
|
||||||
def is_socket_used_active_tree(self, socket):
|
|
||||||
"""Ensure used sockets in active node tree is calculated and check given socket"""
|
|
||||||
if not hasattr(self, "used_viewer_sockets_active_mat"):
|
|
||||||
self.used_viewer_sockets_active_mat = []
|
|
||||||
|
|
||||||
node_tree = bpy.context.space_data.node_tree
|
|
||||||
output_node = None
|
|
||||||
if node_tree.type == 'GEOMETRY':
|
|
||||||
output_node = get_group_output_node(node_tree)
|
|
||||||
elif node_tree.type == 'SHADER':
|
|
||||||
output_node = get_group_output_node(node_tree,
|
|
||||||
output_node_type=self.shader_output_type)
|
|
||||||
|
|
||||||
if output_node is not None:
|
|
||||||
self.search_sockets(output_node, self.used_viewer_sockets_active_mat)
|
|
||||||
return socket in self.used_viewer_sockets_active_mat
|
|
||||||
|
|
||||||
def has_socket_other_users(self, socket):
|
|
||||||
"""List the other users for this socket (other materials or GN groups)"""
|
|
||||||
if not hasattr(self, "other_viewer_sockets_users"):
|
|
||||||
self.other_viewer_sockets_users = []
|
|
||||||
if socket.socket_type == 'NodeSocketShader':
|
|
||||||
for mat in bpy.data.materials:
|
|
||||||
if mat.node_tree == bpy.context.space_data.node_tree or not hasattr(mat.node_tree, "nodes"):
|
|
||||||
continue
|
|
||||||
# Get viewer node
|
|
||||||
output_node = get_group_output_node(mat.node_tree,
|
|
||||||
output_node_type=self.shader_output_type)
|
|
||||||
if output_node is not None:
|
|
||||||
self.search_sockets(output_node, self.other_viewer_sockets_users)
|
|
||||||
elif socket.socket_type == 'NodeSocketGeometry':
|
|
||||||
for obj in bpy.data.objects:
|
|
||||||
for mod in obj.modifiers:
|
|
||||||
if mod.type != 'NODES' or mod.node_group == bpy.context.space_data.node_tree:
|
|
||||||
continue
|
|
||||||
# Get viewer node
|
|
||||||
output_node = get_group_output_node(mod.node_group)
|
|
||||||
if output_node is not None:
|
|
||||||
self.search_sockets(output_node, self.other_viewer_sockets_users)
|
|
||||||
return socket in self.other_viewer_sockets_users
|
|
||||||
|
|
||||||
def get_output_index(self, node, output_node, is_base_node_tree, socket_type, check_type=False):
|
|
||||||
"""Get the next available output socket in the active node"""
|
|
||||||
out_i = None
|
|
||||||
valid_outputs = []
|
|
||||||
for i, out in enumerate(node.outputs):
|
|
||||||
if is_visible_socket(out) and (not check_type or out.type == socket_type):
|
|
||||||
valid_outputs.append(i)
|
|
||||||
if valid_outputs:
|
|
||||||
out_i = valid_outputs[0] # Start index of node's outputs
|
|
||||||
for i, valid_i in enumerate(valid_outputs):
|
|
||||||
for out_link in node.outputs[valid_i].links:
|
|
||||||
if is_viewer_link(out_link, output_node):
|
|
||||||
if is_base_node_tree or self.link_leads_to_used_socket(out_link):
|
|
||||||
if i < len(valid_outputs) - 1:
|
|
||||||
out_i = valid_outputs[i + 1]
|
|
||||||
else:
|
|
||||||
out_i = valid_outputs[0]
|
|
||||||
return out_i
|
|
||||||
|
|
||||||
def create_links(self, path, node, active_node_socket_id, socket_type):
|
|
||||||
"""Create links at each step in the node group path."""
|
|
||||||
path = list(reversed(path))
|
|
||||||
# Starting from the level of the active node
|
|
||||||
for path_index, path_element in enumerate(path[:-1]):
|
|
||||||
# Ensure there is a viewer node and it has an input
|
|
||||||
tree = path_element.node_tree
|
|
||||||
viewer_socket = self.ensure_viewer_socket(
|
|
||||||
tree, socket_type,
|
|
||||||
connect_socket = node.outputs[active_node_socket_id]
|
|
||||||
if path_index == 0 else None)
|
|
||||||
if viewer_socket in self.delete_sockets:
|
|
||||||
self.delete_sockets.remove(viewer_socket)
|
|
||||||
|
|
||||||
# Connect the current to its viewer
|
|
||||||
link_start = node.outputs[active_node_socket_id]
|
|
||||||
link_end = self.ensure_group_output(tree).inputs[viewer_socket.identifier]
|
|
||||||
connect_sockets(link_start, link_end)
|
|
||||||
|
|
||||||
# Go up in the node group hierarchy
|
|
||||||
next_tree = path[path_index + 1].node_tree
|
|
||||||
node = next(n for n in next_tree.nodes
|
|
||||||
if n.type == 'GROUP'
|
|
||||||
and n.node_tree == tree)
|
|
||||||
tree = next_tree
|
|
||||||
active_node_socket_id = viewer_socket.identifier
|
|
||||||
return node.outputs[active_node_socket_id]
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
# Delete sockets
|
|
||||||
for socket in self.delete_sockets:
|
|
||||||
if not self.has_socket_other_users(socket):
|
|
||||||
tree = socket.id_data
|
|
||||||
self.remove_socket(tree, socket)
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
space = context.space_data
|
|
||||||
# Ignore operator when running in wrong context.
|
|
||||||
if self.run_in_geometry_nodes != (space.tree_type == "GeometryNodeTree"):
|
|
||||||
return {'PASS_THROUGH'}
|
|
||||||
|
|
||||||
mlocx = event.mouse_region_x
|
|
||||||
mlocy = event.mouse_region_y
|
|
||||||
select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
|
|
||||||
if 'FINISHED' not in select_node: # only run if mouse click is on a node
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
base_node_tree = space.node_tree
|
|
||||||
active_tree = context.space_data.edit_tree
|
|
||||||
path = context.space_data.path
|
|
||||||
nodes = active_tree.nodes
|
|
||||||
active = nodes.active
|
|
||||||
|
|
||||||
if not active and not any(is_visible_socket(out) for out in active.outputs):
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
# Scan through all nodes in tree including nodes inside of groups to find viewer sockets
|
|
||||||
self.delete_sockets = []
|
|
||||||
self.scan_nodes(base_node_tree, self.delete_sockets)
|
|
||||||
|
|
||||||
if not active.outputs:
|
|
||||||
self.cleanup()
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
# For geometry node trees, we just connect to the group output
|
|
||||||
if space.tree_type == "GeometryNodeTree":
|
|
||||||
socket_type = 'NodeSocketGeometry'
|
|
||||||
|
|
||||||
# Find (or create if needed) the output of this node tree
|
|
||||||
output_node = self.ensure_group_output(base_node_tree)
|
|
||||||
|
|
||||||
active_node_socket_index = self.get_output_index(
|
|
||||||
active, output_node, base_node_tree == active_tree, 'GEOMETRY', check_type=True
|
|
||||||
)
|
|
||||||
# If there is no 'GEOMETRY' output type - We can't preview the node
|
|
||||||
if active_node_socket_index is None:
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
# Find an input socket of the output of type geometry
|
|
||||||
output_node_socket_index = None
|
|
||||||
for i, inp in enumerate(output_node.inputs):
|
|
||||||
if inp.type == 'GEOMETRY':
|
|
||||||
output_node_socket_index = i
|
|
||||||
break
|
|
||||||
if output_node_socket_index is None:
|
|
||||||
# Create geometry socket
|
|
||||||
geometry_out_socket = base_node_tree.interface.new_socket(
|
|
||||||
'Geometry', in_out='OUTPUT', socket_type=socket_type
|
|
||||||
)
|
|
||||||
output_node_socket_index = geometry_out_socket.index
|
|
||||||
|
|
||||||
# For shader node trees, we connect to a material output
|
|
||||||
elif space.tree_type == "ShaderNodeTree":
|
|
||||||
socket_type = 'NodeSocketShader'
|
|
||||||
self.init_shader_variables(space, space.shader_type)
|
|
||||||
|
|
||||||
# Get or create material_output node
|
|
||||||
output_node = get_group_output_node(base_node_tree,
|
|
||||||
output_node_type=self.shader_output_type)
|
|
||||||
if not output_node:
|
|
||||||
output_node = base_node_tree.nodes.new(self.shader_output_ident)
|
|
||||||
output_node.location = get_output_location(base_node_tree)
|
|
||||||
output_node.select = False
|
|
||||||
|
|
||||||
active_node_socket_index = self.get_output_index(
|
|
||||||
active, output_node, base_node_tree == active_tree, 'SHADER'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Cancel if no socket was found. This can happen for group input
|
|
||||||
# nodes with only a virtual socket output.
|
|
||||||
if active_node_socket_index is None:
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
if active.outputs[active_node_socket_index].name == "Volume":
|
|
||||||
output_node_socket_index = 1
|
|
||||||
else:
|
|
||||||
output_node_socket_index = 0
|
|
||||||
|
|
||||||
# If there are no nested node groups, the link starts at the active node
|
|
||||||
node_output = active.outputs[active_node_socket_index]
|
|
||||||
if len(path) > 1:
|
|
||||||
# Recursively connect inside nested node groups and get the one from base level
|
|
||||||
node_output = self.create_links(path, active, active_node_socket_index, socket_type)
|
|
||||||
output_node_input = output_node.inputs[output_node_socket_index]
|
|
||||||
|
|
||||||
# Connect at base level
|
|
||||||
connect_sockets(node_output, output_node_input)
|
|
||||||
|
|
||||||
self.cleanup()
|
|
||||||
nodes.active = active
|
|
||||||
active.select = True
|
|
||||||
force_update(context)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class NWFrameSelected(Operator, NWBase):
|
class NWFrameSelected(Operator, NWBase):
|
||||||
bl_idname = "node.nw_frame_selected"
|
bl_idname = "node.nw_frame_selected"
|
||||||
bl_label = "Frame Selected"
|
bl_label = "Frame Selected"
|
||||||
@ -2759,7 +2442,6 @@ classes = (
|
|||||||
NWSwapLinks,
|
NWSwapLinks,
|
||||||
NWResetBG,
|
NWResetBG,
|
||||||
NWAddAttrNode,
|
NWAddAttrNode,
|
||||||
NWPreviewNode,
|
|
||||||
NWFrameSelected,
|
NWFrameSelected,
|
||||||
NWReloadImages,
|
NWReloadImages,
|
||||||
NWMergeNodes,
|
NWMergeNodes,
|
||||||
|
@ -333,11 +333,6 @@ kmi_defs = (
|
|||||||
(operators.NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
|
(operators.NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
|
||||||
# Swap Links
|
# Swap Links
|
||||||
(operators.NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Links"),
|
(operators.NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Links"),
|
||||||
# Preview Node
|
|
||||||
(operators.NWPreviewNode.bl_idname, 'LEFTMOUSE', 'PRESS', True, True,
|
|
||||||
False, (('run_in_geometry_nodes', False),), "Preview node output"),
|
|
||||||
(operators.NWPreviewNode.bl_idname, 'LEFTMOUSE', 'PRESS', False, True,
|
|
||||||
True, (('run_in_geometry_nodes', True),), "Preview node output"),
|
|
||||||
# Reload Images
|
# Reload Images
|
||||||
(operators.NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
|
(operators.NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
|
||||||
# Lazy Mix
|
# Lazy Mix
|
||||||
|
@ -143,14 +143,6 @@ def get_nodes_links(context):
|
|||||||
return tree.nodes, tree.links
|
return tree.nodes, tree.links
|
||||||
|
|
||||||
|
|
||||||
viewer_socket_name = "tmp_viewer"
|
|
||||||
|
|
||||||
|
|
||||||
def is_viewer_socket(socket):
|
|
||||||
# checks if a internal socket is a valid viewer socket
|
|
||||||
return socket.name == viewer_socket_name and socket.NWViewerSocket
|
|
||||||
|
|
||||||
|
|
||||||
def get_internal_socket(socket):
|
def get_internal_socket(socket):
|
||||||
# get the internal socket from a socket inside or outside the group
|
# get the internal socket from a socket inside or outside the group
|
||||||
node = socket.node
|
node = socket.node
|
||||||
@ -169,16 +161,6 @@ def get_internal_socket(socket):
|
|||||||
return iterator[0]
|
return iterator[0]
|
||||||
|
|
||||||
|
|
||||||
def is_viewer_link(link, output_node):
|
|
||||||
if link.to_node == output_node and link.to_socket == output_node.inputs[0]:
|
|
||||||
return True
|
|
||||||
if link.to_node.type == 'GROUP_OUTPUT':
|
|
||||||
socket = get_internal_socket(link.to_socket)
|
|
||||||
if is_viewer_socket(socket):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_group_output_node(tree, output_node_type='GROUP_OUTPUT'):
|
def get_group_output_node(tree, output_node_type='GROUP_OUTPUT'):
|
||||||
for node in tree.nodes:
|
for node in tree.nodes:
|
||||||
if node.type == output_node_type and node.is_active_output:
|
if node.type == output_node_type and node.is_active_output:
|
||||||
|
Loading…
Reference in New Issue
Block a user