Recipe component can show multiblock recipes
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
BaseController = require "../../../base_controller"
|
||||
CraftingGridController = require "../../crafting_grid/crafting_grid_controller"
|
||||
ItemDisplay = require "../../../../models/site/item_display"
|
||||
RecipeDisplay = require "../../../../models/site/recipe_display"
|
||||
SlotController = require "../../slot/slot_controller"
|
||||
{StringBuilder} = require("crafting-guide-common").util
|
||||
|
||||
@@ -24,21 +25,14 @@ module.exports = class CraftingTableController extends BaseController
|
||||
|
||||
@_imageLoader = options.imageLoader
|
||||
@_modPack = options.modPack
|
||||
@_multiplier = options.multiplier
|
||||
@_router = options.router
|
||||
|
||||
# BaseController Overrides #####################################################################
|
||||
|
||||
onDidRender: ->
|
||||
@_gridController = @addChild CraftingGridController, ".view__crafting_grid",
|
||||
modPack: @_modPack
|
||||
imageLoader: @_imageLoader
|
||||
router: @_router
|
||||
|
||||
@_outputSlotController = @addChild SlotController, ".output .view__slot",
|
||||
imageLoader: @_imageLoader
|
||||
modPack: @_modPack
|
||||
router: @_router
|
||||
options = modPack:@_modPack, imageLoader:@_imageLoader, router:@_router
|
||||
@_gridController = @addChild CraftingGridController, ".view__crafting_grid", options
|
||||
@_outputSlotController = @addChild SlotController, ".output .view__slot", options
|
||||
|
||||
@$multiplier = @$(".multiplier")
|
||||
@$outputImg = @$(".output img")
|
||||
@@ -47,32 +41,19 @@ module.exports = class CraftingTableController extends BaseController
|
||||
@$toolContainer = @$(".tool")
|
||||
super
|
||||
|
||||
onWillChangeModel: (oldModel, newModel)->
|
||||
if not newModel? then throw new Error "model is required"
|
||||
if newModel.constructor isnt RecipeDisplay then throw new Error "model must be a RecipeDisplay"
|
||||
return super oldModel, newModel
|
||||
|
||||
refresh: ->
|
||||
@_gridController.model = @model
|
||||
@_outputSlotController.model = @model?.output
|
||||
@_gridController.model = @model.recipe
|
||||
@_outputSlotController.model = @model?.recipe.output
|
||||
|
||||
@_refreshMultiplier()
|
||||
@_refreshTools()
|
||||
super
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
multiplier:
|
||||
get: ->
|
||||
@_multiplier ?= 1
|
||||
return @_multiplier
|
||||
|
||||
set: (newMultiplier)->
|
||||
oldMultiplier = @_multiplier
|
||||
return if newMultiplier is oldMultiplier
|
||||
|
||||
@_multiplier = newMultiplier
|
||||
@_refreshMultiplier()
|
||||
|
||||
@trigger "change:multiplier", this, oldMultiplier, newMultiplier
|
||||
@trigger "change", this
|
||||
|
||||
# Backbone.View Methods ########################################################################
|
||||
|
||||
events: ->
|
||||
@@ -82,8 +63,8 @@ module.exports = class CraftingTableController extends BaseController
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_refreshMultiplier: ->
|
||||
if @multiplier > 1
|
||||
@$multiplier.html "x#{@multiplier}"
|
||||
if @model.multiplier > 1
|
||||
@$multiplier.html "x#{@model.multiplier}"
|
||||
else
|
||||
@$multiplier.html ""
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
//-
|
||||
//- Crafting Guide - multiblock.jade
|
||||
//-
|
||||
//- Copyright (c) 2015 by Redwood Labs
|
||||
//- All rights reserved.
|
||||
//-
|
||||
|
||||
.view__multiblock
|
||||
img.outline(src="/images/outline.png")
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// Crafting Guide - multiblock.scss
|
||||
//
|
||||
// Copyright © 2014-2017 by Redwood Labs
|
||||
// All rights reserved.
|
||||
//
|
||||
|
||||
$animate-slow: 400ms;
|
||||
$animate-fast: 100ms;
|
||||
|
||||
.view__multiblock {
|
||||
position: relative;
|
||||
|
||||
.block {
|
||||
position: absolute;
|
||||
transition: all $animate-slow cubic-bezier(0.615, -0.090, 0.010, 1.090);
|
||||
}
|
||||
|
||||
.outline {
|
||||
position: absolute;
|
||||
opacity: 0.0;
|
||||
transition: opacity $animate-fast;
|
||||
}
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
#
|
||||
# Crafting Guide - multiblock_controller.coffee
|
||||
#
|
||||
# Copyright (c) 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseController = require "../../../../base_controller"
|
||||
ItemDisplay = require "../../../../../models/site/item_display"
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class MultiblockController extends BaseController
|
||||
|
||||
@::HOVER_TIMEOUT = 5000 # ms
|
||||
|
||||
@::IMAGE_SIZE = 48 # px
|
||||
|
||||
@::X_LAYER_H_SHIFT = -24 # px
|
||||
@::Z_LAYER_H_SHIFT = +24 # px
|
||||
|
||||
@::X_LAYER_V_SHIFT = -12 # px
|
||||
@::Y_LAYER_V_SHIFT = -30 # px
|
||||
@::Z_LAYER_V_SHIFT = -12 # px
|
||||
|
||||
@::MAX_DROP_DELAY = 250 # ms
|
||||
|
||||
constructor: (options={})->
|
||||
if not options.imageLoader? then throw new Error "options.imageLoader is required"
|
||||
if not options.modPack? then throw new Error "options.modPack is required"
|
||||
options.templateName = "common/recipe/multiblock_viewer/multiblock"
|
||||
super options
|
||||
|
||||
@_currentLayer = 0
|
||||
@_blockCache = null
|
||||
@_imageLoader = options.imageLoader
|
||||
@_modPack = options.modPack
|
||||
@_onHovering = options.onHovering ?= (itemDisplay)-> # do nothing
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
goBackLayer: ->
|
||||
@_currentLayer = Math.max 0, @_currentLayer - 1
|
||||
@_refreshAllBlocks()
|
||||
|
||||
goNextLayer: ->
|
||||
@_currentLayer = Math.min @model.height - 1, @_currentLayer + 1
|
||||
@_refreshAllBlocks()
|
||||
|
||||
hasBackLayer: ->
|
||||
return false unless @model?
|
||||
return @_currentLayer > 0
|
||||
|
||||
hasNextLayer: ->
|
||||
return false unless @model?
|
||||
return @_currentLayer < @model.height - 1
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
currentLayer:
|
||||
get: -> return @_currentLayer
|
||||
|
||||
# BaseController Overrides #####################################################################
|
||||
|
||||
onDidRender: ->
|
||||
@$outline = @$(".outline")
|
||||
super
|
||||
|
||||
refresh: ->
|
||||
@$el.css "height":@_computeMinHeight(), "width":@_computeMinWidth()
|
||||
|
||||
@_refreshBlockCache()
|
||||
@_refreshAllBlocks()
|
||||
super
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_computePosition: (x, y, z, options={})->
|
||||
if not @model? then return left:0, top:-@IMAGE_SIZE, "z-index":0, opacity:0.0
|
||||
|
||||
options.hideUpperLayers ?= false
|
||||
pxWidth = @$el.width()
|
||||
pxHeight = @$el.height()
|
||||
|
||||
left = ((pxWidth - @IMAGE_SIZE) / 2) + (x * @X_LAYER_H_SHIFT) + (z * @Z_LAYER_H_SHIFT)
|
||||
|
||||
if options.hideUpperLayers and y > @_currentLayer
|
||||
top = 0
|
||||
opacity = 0.0
|
||||
else
|
||||
top = (pxHeight - @IMAGE_SIZE) + (x * @X_LAYER_V_SHIFT) + (z * @Z_LAYER_V_SHIFT) + (y * @Y_LAYER_V_SHIFT)
|
||||
opacity = 1.0
|
||||
|
||||
zindex = ((@model.width - x) + (@model.depth - z) + ((@model.width * @model.depth) * y)) * 2
|
||||
|
||||
return left:left, top:top, "z-index":zindex, opacity:opacity
|
||||
|
||||
_computeMinHeight: ->
|
||||
return 0 unless @model?
|
||||
|
||||
minHeight = Math.max(Math.abs(@model.width * @X_LAYER_V_SHIFT), Math.abs(@model.depth * @Z_LAYER_V_SHIFT))
|
||||
minHeight += Math.abs(@model.height * @Y_LAYER_V_SHIFT)
|
||||
minHeight += 2 * @IMAGE_SIZE
|
||||
return minHeight
|
||||
|
||||
_computeMinWidth: ->
|
||||
return 0 unless @model?
|
||||
|
||||
leftPosition = @_computePosition @model.width, 0, 0
|
||||
rightPosition = @_computePosition 0, 0, @model.depth
|
||||
return rightPosition.left - leftPosition.left
|
||||
|
||||
_endHovering: ->
|
||||
@$outline.css opacity:0.0
|
||||
@_hoverTimer = null
|
||||
@_onHovering null
|
||||
|
||||
_randomDropDelay: ->
|
||||
return Math.random() * @MAX_DROP_DELAY
|
||||
|
||||
_refreshBlockCache: ->
|
||||
@_blockCache = null
|
||||
return unless @model?
|
||||
|
||||
@_blockCache = width:@model.width, height:@model.height, depth:@model.depth
|
||||
for x in [0...@model.width]
|
||||
@_blockCache[x] ?= {}
|
||||
for y in [0...@model.height]
|
||||
@_blockCache[x][y] ?= {}
|
||||
for z in [0...@model.depth]
|
||||
@_blockCache[x][y][z] ?= null
|
||||
|
||||
for el in @$(".block")
|
||||
$el = $(el)
|
||||
@_blockCache[$el.data("x")][$el.data("y")][$el.data("z")] = $el
|
||||
|
||||
_refreshBlock: (x, y, z)->
|
||||
return unless @model?
|
||||
|
||||
stack = @model.getInputAt x, y, z
|
||||
$block = @_blockCache[x][y][z]
|
||||
position = @_computePosition x, y, z, hideUpperLayers:true
|
||||
move = false
|
||||
|
||||
if stack?
|
||||
itemDisplay = new ItemDisplay stack.item
|
||||
|
||||
if not $block?
|
||||
$block = $("<img alt=\"#{itemDisplay.itemName}\" class=\"block\" />")
|
||||
$block.css position, top:-@IMAGE_SIZE, opacity:0.0
|
||||
@_registerHoverHandlers $block, y, itemDisplay
|
||||
@_imageLoader.load itemDisplay.iconUrl, $block
|
||||
@_blockCache[x][y][z] = $block
|
||||
@$el.append $block
|
||||
move = true
|
||||
else
|
||||
$block.attr "src", itemDisplay.iconUrl
|
||||
move = true
|
||||
else if $block?
|
||||
$block.attr "src", ""
|
||||
move = true
|
||||
|
||||
if move then _.delay (-> $block.css position), @_randomDropDelay()
|
||||
|
||||
_refreshAllBlocks: ->
|
||||
return unless @model?
|
||||
if @_endHovering? then @_endHovering()
|
||||
|
||||
for x in [0...@model.width]
|
||||
for y in [0...@model.height]
|
||||
for z in [0...@model.depth]
|
||||
@_refreshBlock(x, y, z)
|
||||
|
||||
_registerHoverHandlers: ($el, y, itemDisplay)->
|
||||
startHovering = =>
|
||||
return unless y is @_currentLayer
|
||||
zIndex = parseInt($el.css("z-index")) + 1
|
||||
zIndex = if _.isNaN(zIndex) then 0 else zIndex
|
||||
|
||||
@$outline.css opacity:1.0, top:$el.css("top"), left:$el.css("left"), "z-index":zIndex
|
||||
@_onHovering itemDisplay
|
||||
|
||||
if @_hoverTimer? then clearTimeout @_hoverTimer
|
||||
@_hoverTimer = setTimeout (=> @_endHovering()), @HOVER_TIMEOUT
|
||||
|
||||
$el.hover startHovering, (->)
|
||||
@@ -0,0 +1,22 @@
|
||||
//-
|
||||
//- Crafting Guide - multiblock_viewer.jade
|
||||
//-
|
||||
//- Copyright (c) 2015 by Redwood Labs
|
||||
//- All rights reserved.
|
||||
//-
|
||||
|
||||
.view__multiblock_viewer
|
||||
.left
|
||||
h3 Complete Materials
|
||||
.view__inventory.complete
|
||||
|
||||
h3 Current Layer
|
||||
.view__inventory.layer
|
||||
.right
|
||||
.buttons
|
||||
.button.back: .bezel: p back
|
||||
.button.next: .bezel: p next
|
||||
.view__multiblock
|
||||
.caption
|
||||
img
|
||||
p
|
||||
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// Crafting Guide - multiblock_viewer.scss
|
||||
//
|
||||
// Copyright © 2014-2017 by Redwood Labs
|
||||
// All rights reserved.
|
||||
//
|
||||
|
||||
.view__multiblock_viewer {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
h3 {
|
||||
margin-bottom: $size-margin-small;
|
||||
margin-left: 0;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: $size-margin-large;
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
border-right: 1px solid $color-gray-light;
|
||||
flex: 1 0 40%;
|
||||
|
||||
.view__inventory {
|
||||
margin-left: $size-margin-medium;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 0 1 60%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.buttons {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: $size-margin-medium;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.button {
|
||||
flex: 1 1 50%;
|
||||
margin-left: $size-margin-medium;
|
||||
}
|
||||
}
|
||||
|
||||
.view__multiblock {
|
||||
flex: 0 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.caption {
|
||||
flex: 0 0 auto;
|
||||
margin-top: $size-margin-medium;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
margin-right : $size-margin-medium;
|
||||
height : $size-minecraft-block;
|
||||
width : $size-minecraft-block;
|
||||
|
||||
|
||||
border : none;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: $font-family-text;
|
||||
font-size: $font-size-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include screen-size(mobile) {
|
||||
.view__multiblock_viewer {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
.left {
|
||||
flex: 0 0 auto;
|
||||
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: $size-margin-large;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
#
|
||||
# Crafting Guide - multiblock_viewer_controller.coffee
|
||||
#
|
||||
# Copyright © 2014-2017 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseController = require "../../../base_controller"
|
||||
InventoryController = require "../../inventory/inventory_controller"
|
||||
MultiblockController = require "./multiblock/multiblock_controller"
|
||||
RecipeDisplay = require "../../../../models/site/recipe_display"
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class MultiblockViewerController extends BaseController
|
||||
|
||||
constructor: (options={})->
|
||||
if not options.imageLoader? then throw new Error "options.imageLoader is required"
|
||||
if not options.modPack? then throw new Error "options.modPack is required"
|
||||
if not options.router? then throw new Error "options.router is required"
|
||||
options.templateName = "common/recipe/multiblock_viewer"
|
||||
super options
|
||||
|
||||
@_imageLoader = options.imageLoader
|
||||
@_modPack = options.modPack
|
||||
@_hoverTimer = null
|
||||
@_router = options.router
|
||||
|
||||
# Event Methods ################################################################################
|
||||
|
||||
onBackClicked: ->
|
||||
return if @$backButton.hasClass "disabled"
|
||||
tracker.trackEvent c.tracking.category.multiblock, "back"
|
||||
@multiblockController.goBackLayer()
|
||||
@refresh()
|
||||
|
||||
onNextClicked: ->
|
||||
return if @$nextButton.hasClass "disabled"
|
||||
tracker.trackEvent c.tracking.category.multiblock, "next"
|
||||
@multiblockController.goNextLayer()
|
||||
@refresh()
|
||||
|
||||
onBlockHovered: (itemDisplay)->
|
||||
if itemDisplay?
|
||||
@$captionText.html itemDisplay.name
|
||||
@_imageLoader.load itemDisplay.iconUrl, @$captionIcon
|
||||
else
|
||||
@$captionText.html " "
|
||||
@$captionIcon.attr "src", "/images/empty.png"
|
||||
|
||||
# BaseController Overrides #####################################################################
|
||||
|
||||
onDidRender: ->
|
||||
baseOptions = imageLoader:@_imageLoader, modPack:@_modPack, router:@_router
|
||||
|
||||
options = _.extend {editable:false}, baseOptions
|
||||
@completeInventoryController = @addChild InventoryController, ".view__inventory.complete", options
|
||||
@layerInventoryController = @addChild InventoryController, ".view__inventory.layer", options
|
||||
|
||||
options = _.extend {onHovering:(itemDisplay)=> @onBlockHovered(itemDisplay)}, baseOptions
|
||||
@multiblockController = @addChild MultiblockController, ".view__multiblock", options
|
||||
|
||||
@$backButton = @$(".button.back")
|
||||
@$nextButton = @$(".button.next")
|
||||
@$captionText = @$(".caption p")
|
||||
@$captionIcon = @$(".caption img")
|
||||
super
|
||||
|
||||
onWillChangeModel: (oldModel, newModel)->
|
||||
if not newModel? then throw new Error "model is required"
|
||||
if newModel.constructor isnt RecipeDisplay then throw new Error "model must be a RecipeDisplay"
|
||||
return super oldModel, newModel
|
||||
|
||||
refresh: ->
|
||||
@completeInventoryController.model = @model.getInventory()
|
||||
@layerInventoryController.model = @model.getInventory @multiblockController.currentLayer
|
||||
@multiblockController.model = @model
|
||||
|
||||
if @multiblockController.hasBackLayer()
|
||||
@$backButton.removeClass "disabled"
|
||||
else
|
||||
@$backButton.addClass "disabled"
|
||||
|
||||
if @multiblockController.hasNextLayer()
|
||||
@$nextButton.removeClass "disabled"
|
||||
else
|
||||
@$nextButton.addClass "disabled"
|
||||
|
||||
super
|
||||
|
||||
# Backbone.View Overrides ######################################################################
|
||||
|
||||
events: ->
|
||||
_.extend super,
|
||||
"click .button.back": "onBackClicked"
|
||||
"click .button.next": "onNextClicked"
|
||||
@@ -5,8 +5,10 @@
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
BaseController = require "../../base_controller"
|
||||
CraftingTableController = require "./crafting_table/crafting_table_controller"
|
||||
BaseController = require "../../base_controller"
|
||||
CraftingTableController = require "./crafting_table/crafting_table_controller"
|
||||
MultiblockViewerController = require "./multiblock_viewer/multiblock_viewer_controller"
|
||||
RecipeDisplay = require "../../../models/site/recipe_display"
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
@@ -21,73 +23,38 @@ module.exports = class RecipeController extends BaseController
|
||||
|
||||
@_imageLoader = options.imageLoader
|
||||
@_modPack = options.modPack
|
||||
@_multiplier = options.multiplier
|
||||
@_router = options.router
|
||||
|
||||
# Class Members ################################################################################
|
||||
|
||||
@::RECIPE_TYPE =
|
||||
CRAFTING_TABLE:
|
||||
css: "crafting_table"
|
||||
Controller: CraftingTableController
|
||||
|
||||
@::ALL_RECIPE_TYPES = (value for key, value of @::RECIPE_TYPE)
|
||||
|
||||
# Properties ###################################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
multiplier:
|
||||
get: ->
|
||||
@_multiplier ?= 1
|
||||
return @_multiplier
|
||||
|
||||
set: (newMultiplier)->
|
||||
oldMultiplier = @_multiplier
|
||||
return if newMultiplier is oldMultiplier
|
||||
|
||||
@_multiplier = newMultiplier
|
||||
|
||||
@trigger "change:multiplier", this, oldMultiplier, newMultiplier
|
||||
@trigger "change", this
|
||||
|
||||
recipeType:
|
||||
get: -> return @_recipeType
|
||||
set: -> throw new Error "recipeType cannot be assigned"
|
||||
|
||||
# BaseController Overrides #####################################################################
|
||||
|
||||
onWillChangeModel: (oldModel, newModel)->
|
||||
@_recomputeRecipeType(newModel)
|
||||
if not newModel? then throw new Error "model is required"
|
||||
if newModel.constructor isnt RecipeDisplay then throw new Error "model must be a RecipeDisplay"
|
||||
return super oldModel, newModel
|
||||
|
||||
refresh: ->
|
||||
return unless @model?
|
||||
@_refreshRecipeType()
|
||||
super
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_recomputeRecipeType: (recipe)->
|
||||
oldRecipeType = @_recipeType
|
||||
|
||||
if not recipe?
|
||||
newRecipeType = null
|
||||
else
|
||||
newRecipeType = @RECIPE_TYPE.CRAFTING_TABLE
|
||||
|
||||
return unless newRecipeType isnt oldRecipeType
|
||||
|
||||
@_recipeType = newRecipeType
|
||||
@trigger "change:recipeType", this, oldRecipeType, newRecipeType
|
||||
_lookupControllerFor: (recipeType)->
|
||||
if recipeType is RecipeDisplay::RECIPE_TYPE.CRAFTING_TABLE then return CraftingTableController
|
||||
if recipeType is RecipeDisplay::RECIPE_TYPE.MULTIBLOCK then return MultiblockViewerController
|
||||
|
||||
_refreshRecipeType: ->
|
||||
return if @_renderedRecipeType is @recipeType
|
||||
@_renderedRecipeType = @recipeType
|
||||
return if @_renderedRecipeType is @model.type
|
||||
@_renderedRecipeType = @model.type
|
||||
|
||||
if @_typeController?
|
||||
@_typeController.remove()
|
||||
@_typeController = null
|
||||
|
||||
return unless @recipeType?
|
||||
return unless @_renderedRecipeType?
|
||||
|
||||
@$el.append "<div class=\"view__#{@recipeType.css}\"></div>"
|
||||
TypeController = @_lookupControllerFor @model.type
|
||||
|
||||
@$el.append "<div class=\"view__#{@model.type}\"></div>"
|
||||
options = imageLoader:@_imageLoader, model:@model, modPack:@_modPack, router:@_router
|
||||
@_typeController = @addChild @recipeType.Controller, ".view__#{@recipeType.css}", options
|
||||
@_typeController = @addChild TypeController, ".view__#{@model.type}", options
|
||||
|
||||
Reference in New Issue
Block a user