mirror of
https://github.com/michaelrausch/Party-Parrots-At-Sea.git
synced 2026-05-09 06:18:44 +00:00
Large tidying of RaceViewController class. Fixing updating for combo boxes
#story[955]
This commit is contained in:
@@ -62,8 +62,8 @@ public class App extends Application
|
|||||||
}
|
}
|
||||||
//Change the StreamReceiver in this else block to change the default data source.
|
//Change the StreamReceiver in this else block to change the default data source.
|
||||||
else{
|
else{
|
||||||
sr = new StreamReceiver("localhost", 4949, "RaceStream");
|
// sr = new StreamReceiver("localhost", 4949, "RaceStream");
|
||||||
// sr = new StreamReceiver("livedata.americascup.com", 4941, "RaceStream");
|
sr = new StreamReceiver("livedata.americascup.com", 4941, "RaceStream");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class RaceController {
|
|||||||
String teamsConfigFile = "/config/teams.xml";
|
String teamsConfigFile = "/config/teams.xml";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
race = createRace(raceConfigFile, teamsConfigFile);
|
race = createRace(raceConfigFile, teamsConfigFile); //These config files arent actually used
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.out.println("There was an error creating the race.");
|
System.out.println("There was an error creating the race.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
package seng302.controllers;
|
package seng302.controllers;
|
||||||
|
|
||||||
import javafx.animation.Animation;
|
|
||||||
import javafx.animation.KeyFrame;
|
import javafx.animation.KeyFrame;
|
||||||
import javafx.animation.Timeline;
|
import javafx.animation.Timeline;
|
||||||
import javafx.beans.value.ChangeListener;
|
|
||||||
import javafx.beans.value.ObservableValue;
|
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableArray;
|
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
@@ -62,15 +58,17 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
|
|||||||
private boolean displayFps;
|
private boolean displayFps;
|
||||||
private Timeline timerTimeline;
|
private Timeline timerTimeline;
|
||||||
private Map<Yacht, TimelineInfo> timelineInfos = new HashMap<>();
|
private Map<Yacht, TimelineInfo> timelineInfos = new HashMap<>();
|
||||||
private ArrayList<Yacht> boatOrder = new ArrayList<>();
|
|
||||||
private Race race;
|
private Race race;
|
||||||
private Stage stage;
|
private Stage stage;
|
||||||
|
|
||||||
private ImportantAnnotationsState importantAnnotations;
|
private ImportantAnnotationsState importantAnnotations;
|
||||||
|
private Yacht selectedBoat;
|
||||||
|
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
// Load a default important annotation state
|
// Load a default important annotation state
|
||||||
importantAnnotations = new ImportantAnnotationsState();
|
importantAnnotations = new ImportantAnnotationsState();
|
||||||
|
|
||||||
|
//Initialise race controller
|
||||||
RaceController raceController = new RaceController();
|
RaceController raceController = new RaceController();
|
||||||
raceController.initializeRace();
|
raceController.initializeRace();
|
||||||
race = raceController.getRace();
|
race = raceController.getRace();
|
||||||
@@ -79,15 +77,10 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
|
|||||||
|
|
||||||
includedCanvasController.setup(this);
|
includedCanvasController.setup(this);
|
||||||
includedCanvasController.initializeCanvas();
|
includedCanvasController.initializeCanvas();
|
||||||
initializeTimer();
|
initializeUpdateTimer();
|
||||||
initializeSettings();
|
initialiseFPSCheckBox();
|
||||||
initialiseWindDirection();
|
initialiseAnnotationSlider();
|
||||||
initialisePositionVBox();
|
|
||||||
initialiseBoatSelectionComboBox();
|
initialiseBoatSelectionComboBox();
|
||||||
//set wind direction!!!!!!! can't find another place to put my code --haoming
|
|
||||||
// double windDirection = new ConfigParser("/config/config.xml").getWindDirection();
|
|
||||||
// windDirectionText.setText(String.format("%.1f°", windDirection));
|
|
||||||
// windArrowText.setRotate(windDirection);
|
|
||||||
includedCanvasController.timer.start();
|
includedCanvasController.timer.start();
|
||||||
|
|
||||||
selectAnnotationBtn.setOnAction(event -> {
|
selectAnnotationBtn.setOnAction(event -> {
|
||||||
@@ -132,17 +125,14 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeSettings() {
|
|
||||||
|
private void initialiseFPSCheckBox() {
|
||||||
displayFps = true;
|
displayFps = true;
|
||||||
|
toggleFps.selectedProperty().addListener(
|
||||||
toggleFps.selectedProperty().addListener(new ChangeListener<Boolean>() {
|
(observable, oldValue, newValue) -> displayFps = !displayFps);
|
||||||
@Override
|
|
||||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
|
||||||
displayFps = !displayFps;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
//SLIDER STUFF BELOW
|
private void initialiseAnnotationSlider() {
|
||||||
annotationSlider.setLabelFormatter(new StringConverter<Double>() {
|
annotationSlider.setLabelFormatter(new StringConverter<Double>() {
|
||||||
@Override
|
@Override
|
||||||
public String toString(Double n) {
|
public String toString(Double n) {
|
||||||
@@ -175,19 +165,24 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
|
|||||||
annotationSlider.setValue(2);
|
annotationSlider.setValue(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeTimer(){
|
|
||||||
|
/**
|
||||||
|
* Initalises a timer which updates elements of the RaceView such as wind direction, boat
|
||||||
|
* orderings etc.. which are dependent on the info from the stream parser constantly.
|
||||||
|
* Updates of each of these attributes are called ONCE EACH SECOND
|
||||||
|
*/
|
||||||
|
private void initializeUpdateTimer(){
|
||||||
timerTimeline = new Timeline();
|
timerTimeline = new Timeline();
|
||||||
timerTimeline.setCycleCount(Timeline.INDEFINITE);
|
timerTimeline.setCycleCount(Timeline.INDEFINITE);
|
||||||
// Run timer update every second
|
// Run timer update every second
|
||||||
timerTimeline.getKeyFrames().add(
|
timerTimeline.getKeyFrames().add(
|
||||||
new KeyFrame(Duration.seconds(1),
|
new KeyFrame(Duration.seconds(1),
|
||||||
event -> {
|
event -> {
|
||||||
if (StreamParser.isRaceFinished()) {
|
updateRaceTime();
|
||||||
timerLabel.setFill(Color.RED);
|
updateWindDirection();
|
||||||
timerLabel.setText("Race Finished!");
|
updateOrder();
|
||||||
} else {
|
updateBoatSelectionComboBox();
|
||||||
timerLabel.setText(currentTimer());
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -195,133 +190,82 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
|
|||||||
timerTimeline.playFromStart();
|
timerTimeline.playFromStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initialiseWindDirection() {
|
|
||||||
Timeline windDirTimeline = new Timeline();
|
/**
|
||||||
windDirTimeline.setCycleCount(Timeline.INDEFINITE);
|
* Updates the wind direction arrow and text as from info from the StreamParser
|
||||||
windDirTimeline.getKeyFrames().add(
|
*/
|
||||||
new KeyFrame(Duration.seconds(1),
|
private void updateWindDirection() {
|
||||||
event -> {
|
|
||||||
windDirectionText.setText(String.format("%.1f°", StreamParser.getWindDirection()));
|
windDirectionText.setText(String.format("%.1f°", StreamParser.getWindDirection()));
|
||||||
windArrowText.setRotate(StreamParser.getWindDirection());
|
windArrowText.setRotate(StreamParser.getWindDirection());
|
||||||
})
|
|
||||||
);
|
|
||||||
windDirTimeline.playFromStart();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initialisePositionVBox() {
|
|
||||||
|
|
||||||
Timeline posVBoxTimeline = new Timeline();
|
|
||||||
posVBoxTimeline.setCycleCount(Timeline.INDEFINITE);
|
|
||||||
posVBoxTimeline.getKeyFrames().add(
|
|
||||||
new KeyFrame(Duration.seconds(1),
|
|
||||||
event -> {
|
|
||||||
showOrder();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
posVBoxTimeline.playFromStart();
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the clock for the race
|
||||||
|
*/
|
||||||
|
private void updateRaceTime() {
|
||||||
|
if (StreamParser.isRaceFinished()) {
|
||||||
|
timerLabel.setFill(Color.RED);
|
||||||
|
timerLabel.setText("Race Finished!");
|
||||||
|
} else {
|
||||||
|
timerLabel.setText(getTimeSinceStartOfRace());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initialiseBoatSelectionComboBox() {
|
|
||||||
|
|
||||||
ObservableList<Yacht> observableBoats = FXCollections.observableArrayList(startingBoats);
|
/**
|
||||||
|
* Grabs the boats currently in the race as from the StreamParser and sets them to be selectable
|
||||||
|
* in the boat selection combo box
|
||||||
|
*/
|
||||||
|
private void updateBoatSelectionComboBox() {
|
||||||
|
ObservableList<Yacht> observableBoats = FXCollections.observableArrayList(StreamParser.getBoatsPos().values());
|
||||||
boatSelectionComboBox.setItems(observableBoats);
|
boatSelectionComboBox.setItems(observableBoats);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the order of the boats as from the StreamParser and sets them in the boat order section
|
||||||
|
*/
|
||||||
|
private void updateOrder() {
|
||||||
|
positionVbox.getChildren().clear();
|
||||||
|
positionVbox.getChildren().removeAll();
|
||||||
|
positionVbox.getStylesheets().add(getClass().getResource("/css/master.css").toString());
|
||||||
|
|
||||||
|
for (Yacht boat : StreamParser.getBoatsPos().values()) {
|
||||||
|
if (boat.getBoatStatus() == 3) { // 3 is finish status
|
||||||
|
Text textToAdd = new Text(boat.getPosition() + ". " +
|
||||||
|
boat.getShortName() + " (Finished)");
|
||||||
|
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
|
||||||
|
positionVbox.getChildren().add(textToAdd);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Text textToAdd = new Text(boat.getPosition() + ". " +
|
||||||
|
boat.getShortName() + " ");
|
||||||
|
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
|
||||||
|
textToAdd.setStyle("");
|
||||||
|
positionVbox.getChildren().add(textToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialised the combo box with any boats currently in the race and adds the required listener
|
||||||
|
* for the combobox to take action upon selection
|
||||||
|
*/
|
||||||
|
private void initialiseBoatSelectionComboBox() {
|
||||||
|
updateBoatSelectionComboBox();
|
||||||
boatSelectionComboBox.valueProperty().addListener((observable, oldValue, newValue) -> {
|
boatSelectionComboBox.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
//This listener is fired whenever the combo box changes. This means when the values are updated
|
||||||
|
//We dont want to set the selected value if the values are updated but nothing clicked (null)
|
||||||
|
if (newValue != null && newValue != selectedBoat) {
|
||||||
Yacht thisYacht = (Yacht) newValue;
|
Yacht thisYacht = (Yacht) newValue;
|
||||||
setSelectedBoat(thisYacht);
|
setSelectedBoat(thisYacht);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates time line for each boat, and stores time time into timelineInfos hash map
|
|
||||||
*/
|
|
||||||
private void initializeTimelines() {
|
|
||||||
HashMap<Yacht, List> boat_events = race.getEvents();
|
|
||||||
for (Yacht boat : boat_events.keySet()) {
|
|
||||||
startingBoats.add(boat);
|
|
||||||
// // x, y are the real time coordinates
|
|
||||||
// DoubleProperty x = new SimpleDoubleProperty();
|
|
||||||
// DoubleProperty y = new SimpleDoubleProperty();
|
|
||||||
//
|
|
||||||
// List<KeyFrame> keyFrames = new ArrayList<>();
|
|
||||||
// List<Event> events = boat_events.get(boat);
|
|
||||||
//
|
|
||||||
// // iterates all events and convert each event to keyFrame, then add them into a list
|
|
||||||
// for (Event event : events) {
|
|
||||||
// if (event.getIsFinishingEvent()) {
|
|
||||||
// keyFrames.add(
|
|
||||||
// new KeyFrame(Duration.seconds(event.getTime()),
|
|
||||||
// onFinished -> {race.setBoatFinished(boat); handleEvent(event);},
|
|
||||||
// new KeyValue(x, event.getThisMark().getLatitude()),
|
|
||||||
// new KeyValue(y, event.getThisMark().getLongitude())
|
|
||||||
// )
|
|
||||||
// );
|
|
||||||
// } else {
|
|
||||||
// keyFrames.add(
|
|
||||||
// new KeyFrame(Duration.seconds(event.getTime()),
|
|
||||||
// onFinished ->{
|
|
||||||
// handleEvent(event);
|
|
||||||
// boat.setHeading(event.getBoatHeading());
|
|
||||||
// },
|
|
||||||
// new KeyValue(x, event.getThisMark().getLatitude()),
|
|
||||||
// new KeyValue(y, event.getThisMark().getLongitude())
|
|
||||||
// )
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// timelineInfos.put(boat, new TimelineInfo(new Timeline(keyFrames.toArray(new KeyFrame[keyFrames.size()])), x, y));
|
|
||||||
}
|
|
||||||
setRaceDuration();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setRaceDuration(){
|
|
||||||
Double maxDuration = 0.0;
|
|
||||||
Timeline maxTimeline = null;
|
|
||||||
|
|
||||||
for (TimelineInfo timelineInfo : timelineInfos.values()) {
|
|
||||||
|
|
||||||
Timeline timeline = timelineInfo.getTimeline();
|
|
||||||
if (timeline.getTotalDuration().toMillis() >= maxDuration) {
|
|
||||||
maxDuration = timeline.getTotalDuration().toMillis();
|
|
||||||
maxTimeline = timeline;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timelines are paused by default
|
|
||||||
timeline.play();
|
|
||||||
timeline.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
maxTimeline.setOnFinished(event -> {
|
|
||||||
race.setRaceFinished();
|
|
||||||
loadRaceResultView();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Play each boats timerTimeline
|
|
||||||
*/
|
|
||||||
public void playTimelines(){
|
|
||||||
for (TimelineInfo timelineInfo : timelineInfos.values()){
|
|
||||||
Timeline timeline = timelineInfo.getTimeline();
|
|
||||||
|
|
||||||
if (timeline.getStatus() == Animation.Status.PAUSED){
|
|
||||||
timeline.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pause each boats timerTimeline
|
|
||||||
*/
|
|
||||||
public void pauseTimelines(){
|
|
||||||
for (TimelineInfo timelineInfo : timelineInfos.values()){
|
|
||||||
Timeline timeline = timelineInfo.getTimeline();
|
|
||||||
|
|
||||||
if (timeline.getStatus() == Animation.Status.RUNNING){
|
|
||||||
timeline.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the list of boats in the order they finished the race
|
* Display the list of boats in the order they finished the race
|
||||||
@@ -342,46 +286,6 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleEvent(Event event) {
|
|
||||||
Yacht boat = event.getBoat();
|
|
||||||
boatOrder.remove(boat);
|
|
||||||
boat.setMarkLastPast(event.getMarkPosInRace());
|
|
||||||
boatOrder.add(boat);
|
|
||||||
boatOrder.sort(new Comparator<Yacht>() {
|
|
||||||
@Override
|
|
||||||
public int compare(Yacht b1, Yacht b2) {
|
|
||||||
return b2.getMarkLastPast() - b1.getMarkLastPast();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
showOrder();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showOrder() {
|
|
||||||
positionVbox.getChildren().clear();
|
|
||||||
positionVbox.getChildren().removeAll();
|
|
||||||
positionVbox.getStylesheets().add(getClass().getResource("/css/master.css").toString());
|
|
||||||
|
|
||||||
// for (Boat boat : boatOrder) {
|
|
||||||
// positionVbox.getChildren().add(new Text(boat.getShortName() + " " + boat.getSpeedInKnots() + " Knots"));
|
|
||||||
// }
|
|
||||||
|
|
||||||
for (Yacht boat : StreamParser.getBoatsPos().values()) {
|
|
||||||
if (boat.getBoatStatus() == 3) { // 3 is finish status
|
|
||||||
Text textToAdd = new Text(boat.getPosition() + ". " +
|
|
||||||
boat.getShortName() + " (Finished)");
|
|
||||||
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
|
|
||||||
positionVbox.getChildren().add(textToAdd);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
Text textToAdd = new Text(boat.getPosition() + ". " +
|
|
||||||
boat.getShortName() + " ");
|
|
||||||
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
|
|
||||||
textToAdd.setStyle("");
|
|
||||||
positionVbox.getChildren().add(textToAdd);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert seconds to a string of the format mm:ss
|
* Convert seconds to a string of the format mm:ss
|
||||||
@@ -396,7 +300,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
|
|||||||
return String.format("%02d:%02d", time / 60, time % 60);
|
return String.format("%02d:%02d", time / 60, time % 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String currentTimer() {
|
private String getTimeSinceStartOfRace() {
|
||||||
String timerString = "0:00";
|
String timerString = "0:00";
|
||||||
if (StreamParser.getTimeSinceStart() > 0) {
|
if (StreamParser.getTimeSinceStart() > 0) {
|
||||||
String timerMinute = Long.toString(StreamParser.getTimeSinceStart() / 60);
|
String timerMinute = Long.toString(StreamParser.getTimeSinceStart() / 60);
|
||||||
@@ -416,12 +320,6 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
|
|||||||
return timerString;
|
return timerString;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopTimer() {
|
|
||||||
timerTimeline.stop();
|
|
||||||
}
|
|
||||||
public void startTimer() {
|
|
||||||
timerTimeline.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDisplayFps() {
|
public boolean isDisplayFps() {
|
||||||
return displayFps;
|
return displayFps;
|
||||||
@@ -431,13 +329,6 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
|
|||||||
return race;
|
return race;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<Yacht, TimelineInfo> getTimelineInfos() {
|
|
||||||
return timelineInfos;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ArrayList<Yacht> getStartingBoats(){
|
|
||||||
return startingBoats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the important annotations for a specific BoatGroup
|
* Display the important annotations for a specific BoatGroup
|
||||||
@@ -524,6 +415,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
|
|||||||
//are to toggle its annotations, there is no other backwards knowledge of a yacht to its boatgroup.
|
//are to toggle its annotations, there is no other backwards knowledge of a yacht to its boatgroup.
|
||||||
if (bg.getBoat().getHullID().equals(yacht.getHullID())) {
|
if (bg.getBoat().getHullID().equals(yacht.getHullID())) {
|
||||||
bg.setIsSelected(true);
|
bg.setIsSelected(true);
|
||||||
|
selectedBoat = yacht;
|
||||||
} else {
|
} else {
|
||||||
bg.setIsSelected(false);
|
bg.setIsSelected(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package seng302.models.mark;
|
package seng302.models.mark;
|
||||||
|
|
||||||
import javafx.geometry.Point2D;
|
import javafx.geometry.Point2D;
|
||||||
|
import javafx.scene.CacheHint;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
import javafx.scene.shape.Circle;
|
import javafx.scene.shape.Circle;
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import java.util.concurrent.PriorityBlockingQueue;
|
|||||||
* and parsed in by turning the byte arrays into useful data. There are two public static hashmaps
|
* and parsed in by turning the byte arrays into useful data. There are two public static hashmaps
|
||||||
* that are threadsafe so the visualiser can always access the latest speed and position available
|
* that are threadsafe so the visualiser can always access the latest speed and position available
|
||||||
* Created by kre39 on 23/04/17.
|
* Created by kre39 on 23/04/17.
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public class StreamParser extends Thread{
|
public class StreamParser extends Thread{
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user