mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-16 10:55:01 -04:00
Launcher: manage active connections to a server (=sessions)
This commit is contained in:
parent
f7dd739aad
commit
744189c745
@ -30,7 +30,7 @@ import java.util.UUID;
|
||||
import static de.bixilon.minosoft.protocol.protocol.ProtocolDefinition.PLAYER_INVENTORY_ID;
|
||||
|
||||
public class Player {
|
||||
final MojangAccount acc;
|
||||
final MojangAccount account;
|
||||
final ScoreboardManager scoreboardManager = new ScoreboardManager();
|
||||
public final HashMap<UUID, PlayerListItem> playerList = new HashMap<>();
|
||||
float health;
|
||||
@ -49,22 +49,22 @@ public class Player {
|
||||
TextComponent tabHeader;
|
||||
TextComponent tabFooter;
|
||||
|
||||
public Player(MojangAccount acc) {
|
||||
this.acc = acc;
|
||||
public Player(MojangAccount account) {
|
||||
this.account = account;
|
||||
// create our own inventory without any properties
|
||||
inventories.put(PLAYER_INVENTORY_ID, new Inventory(null));
|
||||
}
|
||||
|
||||
public String getPlayerName() {
|
||||
return acc.getPlayerName();
|
||||
return account.getPlayerName();
|
||||
}
|
||||
|
||||
public UUID getPlayerUUID() {
|
||||
return acc.getUUID();
|
||||
return account.getUUID();
|
||||
}
|
||||
|
||||
public MojangAccount getAccount() {
|
||||
return this.acc;
|
||||
return this.account;
|
||||
}
|
||||
|
||||
public float getHealth() {
|
||||
|
@ -112,6 +112,7 @@ public class AccountListCell extends ListCell<MojangAccount> implements Initiali
|
||||
public void select() {
|
||||
Minosoft.selectAccount(account);
|
||||
listView.refresh();
|
||||
ServerListCell.listView.refresh();
|
||||
}
|
||||
|
||||
private void resetCell() {
|
||||
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Codename Minosoft
|
||||
* Copyright (C) 2020 Moritz Zwerger
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
|
||||
*/
|
||||
|
||||
package de.bixilon.minosoft.gui.main;
|
||||
|
||||
import de.bixilon.minosoft.protocol.network.Connection;
|
||||
|
||||
public interface ConnectionChangeCallback {
|
||||
void handle(Connection connection);
|
||||
}
|
@ -20,6 +20,7 @@ import de.bixilon.minosoft.protocol.protocol.ConnectionReasons;
|
||||
import javafx.scene.image.Image;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class Server {
|
||||
static int highestServerId;
|
||||
@ -29,6 +30,7 @@ public class Server {
|
||||
int desiredVersion;
|
||||
String favicon;
|
||||
Connection lastPing;
|
||||
ArrayList<Connection> connections = new ArrayList<>();
|
||||
|
||||
public Server(int id, String name, String address, int desiredVersion) {
|
||||
this.id = id;
|
||||
@ -121,4 +123,21 @@ public class Server {
|
||||
}
|
||||
lastPing.resolve(ConnectionReasons.PING, getDesiredVersion()); // resolve dns address and ping
|
||||
}
|
||||
|
||||
public ArrayList<Connection> getConnections() {
|
||||
return connections;
|
||||
}
|
||||
|
||||
public void addConnection(Connection connection) {
|
||||
connections.add(connection);
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
for (Connection connection : connections) {
|
||||
if (connection.isConnected()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ import javafx.fxml.FXMLLoader;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
@ -35,6 +37,8 @@ import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Pair;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -59,6 +63,8 @@ public class ServerListCell extends ListCell<Server> implements Initializable {
|
||||
public MenuButton optionsMenu;
|
||||
@FXML
|
||||
public Label serverBrand;
|
||||
@FXML
|
||||
public MenuItem optionsSessions;
|
||||
boolean canConnect = false;
|
||||
@FXML
|
||||
private Label serverName;
|
||||
@ -114,6 +120,12 @@ public class ServerListCell extends ListCell<Server> implements Initializable {
|
||||
favicon = GUITools.logo;
|
||||
}
|
||||
icon.setImage(favicon);
|
||||
if (server.isConnected()) {
|
||||
setStyle("-fx-background-color: darkseagreen;");
|
||||
optionsSessions.setDisable(false);
|
||||
} else {
|
||||
optionsSessions.setDisable(true);
|
||||
}
|
||||
if (server.getLastPing() == null) {
|
||||
server.ping();
|
||||
}
|
||||
@ -123,6 +135,10 @@ public class ServerListCell extends ListCell<Server> implements Initializable {
|
||||
return;
|
||||
}
|
||||
resetCell();
|
||||
|
||||
if (server.isConnected()) {
|
||||
setStyle("-fx-background-color: darkseagreen;");
|
||||
}
|
||||
if (ping == null) {
|
||||
// Offline
|
||||
players.setText("");
|
||||
@ -257,10 +273,14 @@ public class ServerListCell extends ListCell<Server> implements Initializable {
|
||||
} else {
|
||||
version = Versions.getVersionById(server.getDesiredVersion());
|
||||
}
|
||||
optionsConnect.setDisable(true);
|
||||
connection.connect(server.getLastPing().getAddress(), version);
|
||||
setStyle("-fx-background-color: darkseagreen;");
|
||||
connection.addConnectionChangeCallback(this::handleConnectionCallback);
|
||||
server.addConnection(connection);
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void resetCell() {
|
||||
// clear all cells
|
||||
setStyle(null);
|
||||
@ -384,4 +404,43 @@ public class ServerListCell extends ListCell<Server> implements Initializable {
|
||||
|
||||
dialog.showAndWait();
|
||||
}
|
||||
|
||||
public void manageSessions() {
|
||||
try {
|
||||
FXMLLoader loader = new FXMLLoader(getClass().getResource("/layout/sessions.fxml"));
|
||||
Parent parent = loader.load();
|
||||
((SessionsWindow) loader.getController()).setServer(server);
|
||||
Stage stage = new Stage();
|
||||
stage.initModality(Modality.APPLICATION_MODAL);
|
||||
stage.setTitle(String.format("Sessions - %s - Minosoft", server.getName()));
|
||||
stage.setScene(new Scene(parent));
|
||||
stage.show();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleConnectionCallback(Connection connection) {
|
||||
if (!server.getConnections().contains(connection)) {
|
||||
// the card got recycled
|
||||
return;
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
if (!connection.isConnected()) {
|
||||
// maybe we got disconnected
|
||||
if (!server.isConnected()) {
|
||||
setStyle(null);
|
||||
optionsSessions.setDisable(true);
|
||||
optionsConnect.setDisable(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (Minosoft.getSelectedAccount() != connection.getPlayer().getAccount()) {
|
||||
optionsConnect.setDisable(false);
|
||||
}
|
||||
setStyle("-fx-background-color: darkseagreen;");
|
||||
optionsSessions.setDisable(false);
|
||||
});
|
||||
}
|
||||
}
|
111
src/main/java/de/bixilon/minosoft/gui/main/SessionListCell.java
Normal file
111
src/main/java/de/bixilon/minosoft/gui/main/SessionListCell.java
Normal file
@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Codename Minosoft
|
||||
* Copyright (C) 2020 Moritz Zwerger
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
|
||||
*/
|
||||
|
||||
package de.bixilon.minosoft.gui.main;
|
||||
|
||||
import de.bixilon.minosoft.protocol.network.Connection;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
public class SessionListCell extends ListCell<Connection> implements Initializable {
|
||||
public static ListView<Connection> listView = new ListView<>();
|
||||
@FXML
|
||||
public Label account;
|
||||
@FXML
|
||||
public Label connectionId;
|
||||
@FXML
|
||||
private AnchorPane root;
|
||||
private Connection connection;
|
||||
|
||||
public static SessionListCell newInstance() {
|
||||
FXMLLoader loader = new FXMLLoader(SessionListCell.class.getResource("/layout/cells/session.fxml"));
|
||||
try {
|
||||
loader.load();
|
||||
return loader.getController();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
updateSelected(false);
|
||||
setGraphic(root);
|
||||
}
|
||||
|
||||
public AnchorPane getRoot() {
|
||||
return root;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void updateItem(Connection connection, boolean empty) {
|
||||
super.updateItem(connection, empty);
|
||||
|
||||
root.setVisible(!empty);
|
||||
if (empty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (connection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (connection.equals(this.connection)) {
|
||||
return;
|
||||
}
|
||||
setStyle(null);
|
||||
this.connection = connection;
|
||||
connection.addConnectionChangeCallback(this::handleConnectionCallback);
|
||||
connectionId.setText(String.format("#%d", connection.getConnectionId()));
|
||||
account.setText(connection.getPlayer().getAccount().getPlayerName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSelected(boolean selected) {
|
||||
super.updateSelected(selected);
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
setStyle("-fx-background-color: indianred");
|
||||
connection.disconnect();
|
||||
}
|
||||
|
||||
private void handleConnectionCallback(Connection connection) {
|
||||
if (this.connection != connection) {
|
||||
// the card got recycled
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connection.isConnected()) {
|
||||
Platform.runLater(() -> {
|
||||
listView.getItems().remove(connection);
|
||||
if (listView.getItems().size() == 0) {
|
||||
((Stage) root.getScene().getWindow()).close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Codename Minosoft
|
||||
* Copyright (C) 2020 Moritz Zwerger
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
|
||||
*/
|
||||
|
||||
package de.bixilon.minosoft.gui.main;
|
||||
|
||||
import de.bixilon.minosoft.protocol.network.Connection;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
public class SessionsWindow implements Initializable {
|
||||
@FXML
|
||||
public BorderPane accountPane;
|
||||
Server server;
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle resourceBundle) {
|
||||
SessionListCell.listView.setCellFactory((lv) -> SessionListCell.newInstance());
|
||||
}
|
||||
|
||||
public void setServer(Server server) {
|
||||
this.server = server;
|
||||
ObservableList<Connection> connections = FXCollections.observableArrayList();
|
||||
for (Connection connection : server.getConnections()) {
|
||||
if (!connection.isConnected()) {
|
||||
server.getConnections().remove(connection);
|
||||
}
|
||||
connections.add(connection);
|
||||
}
|
||||
SessionListCell.listView.setItems(connections);
|
||||
accountPane.setCenter(SessionListCell.listView);
|
||||
}
|
||||
|
||||
public void disconnectAll() {
|
||||
server.getConnections().forEach(Connection::disconnect);
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ import de.bixilon.minosoft.game.datatypes.objectLoader.CustomMapping;
|
||||
import de.bixilon.minosoft.game.datatypes.objectLoader.recipes.Recipes;
|
||||
import de.bixilon.minosoft.game.datatypes.objectLoader.versions.Version;
|
||||
import de.bixilon.minosoft.game.datatypes.objectLoader.versions.Versions;
|
||||
import de.bixilon.minosoft.gui.main.ConnectionChangeCallback;
|
||||
import de.bixilon.minosoft.logging.Log;
|
||||
import de.bixilon.minosoft.logging.LogLevels;
|
||||
import de.bixilon.minosoft.ping.ServerListPing;
|
||||
@ -52,6 +53,7 @@ public class Connection {
|
||||
final LinkedBlockingQueue<ClientboundPacket> handlingQueue = new LinkedBlockingQueue<>();
|
||||
final VelocityHandler velocityHandler = new VelocityHandler(this);
|
||||
final HashSet<PingCallback> pingCallbacks = new HashSet<>();
|
||||
final HashSet<ConnectionChangeCallback> connectionChangeCallbacks = new HashSet<>();
|
||||
final int connectionId;
|
||||
final Player player;
|
||||
final String hostname;
|
||||
@ -197,13 +199,15 @@ public class Connection {
|
||||
resolve(address);
|
||||
} else {
|
||||
// no connection and no servers available anymore... sorry, but you can not play today :(
|
||||
handleCallbacks(null);
|
||||
handlePingCallbacks(null);
|
||||
}
|
||||
break;
|
||||
case FAILED_NO_RETRY:
|
||||
handleCallbacks(null);
|
||||
handlePingCallbacks(null);
|
||||
break;
|
||||
}
|
||||
// handle callbacks
|
||||
connectionChangeCallbacks.forEach((callback -> callback.handle(this)));
|
||||
}
|
||||
|
||||
public Version getVersion() {
|
||||
@ -361,13 +365,13 @@ public class Connection {
|
||||
return connectionId;
|
||||
}
|
||||
|
||||
public void addPingCallback(PingCallback pingCallback) {
|
||||
public void addPingCallback(PingCallback callback) {
|
||||
if (getConnectionState() == ConnectionStates.FAILED || getConnectionState() == ConnectionStates.FAILED_NO_RETRY || lastPing != null) {
|
||||
// ping done
|
||||
pingCallback.handle(lastPing);
|
||||
callback.handle(lastPing);
|
||||
return;
|
||||
}
|
||||
pingCallbacks.add(pingCallback);
|
||||
pingCallbacks.add(callback);
|
||||
}
|
||||
|
||||
public HashSet<PingCallback> getPingCallbacks() {
|
||||
@ -382,17 +386,23 @@ public class Connection {
|
||||
this.desiredVersionNumber = desiredVersionNumber;
|
||||
}
|
||||
|
||||
public void handleCallbacks(ServerListPing ping) {
|
||||
public void handlePingCallbacks(ServerListPing ping) {
|
||||
this.lastPing = ping;
|
||||
for (PingCallback callback : getPingCallbacks()) {
|
||||
callback.handle(ping);
|
||||
}
|
||||
pingCallbacks.forEach((callback -> callback.handle(ping)));
|
||||
}
|
||||
|
||||
public Exception getLastConnectionException() {
|
||||
return network.lastException;
|
||||
}
|
||||
|
||||
public void addConnectionChangeCallback(ConnectionChangeCallback callback) {
|
||||
connectionChangeCallbacks.add(callback);
|
||||
}
|
||||
|
||||
public HashSet<ConnectionChangeCallback> getConnectionChangeCallbacks() {
|
||||
return connectionChangeCallbacks;
|
||||
}
|
||||
|
||||
public ServerListPing getLastPing() {
|
||||
return lastPing;
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ public class PacketHandler {
|
||||
connection.setVersion(version);
|
||||
}
|
||||
Log.info(String.format("Status response received: %s/%s online. MotD: '%s'", pkg.getResponse().getPlayerOnline(), pkg.getResponse().getMaxPlayers(), pkg.getResponse().getMotd().getColoredMessage()));
|
||||
connection.handleCallbacks(pkg.getResponse());
|
||||
connection.handlePingCallbacks(pkg.getResponse());
|
||||
}
|
||||
|
||||
public void handle(PacketStatusPong pkg) {
|
||||
|
@ -31,6 +31,8 @@
|
||||
<MenuItem mnemonicParsing="false" onAction="#showInfo" text="Info"/>
|
||||
<MenuItem mnemonicParsing="false" onAction="#edit" text="Edit"/>
|
||||
<MenuItem mnemonicParsing="false" onAction="#refresh" text="Refresh"/>
|
||||
<MenuItem fx:id="optionsSessions" mnemonicParsing="false" onAction="#manageSessions" disable="true"
|
||||
text="Sessions"/>
|
||||
<MenuItem mnemonicParsing="false" style="-fx-text-fill: red" onAction="#delete" text="Delete"/>
|
||||
</items>
|
||||
</MenuButton>
|
||||
|
25
src/main/resources/layout/cells/session.fxml
Normal file
25
src/main/resources/layout/cells/session.fxml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.MenuButton?>
|
||||
<?import javafx.scene.control.MenuItem?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
<AnchorPane xmlns:fx="http://javafx.com/fxml/1" fx:id="root" maxHeight="-Infinity" maxWidth="500.0"
|
||||
minHeight="-Infinity" minWidth="500.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1"
|
||||
fx:controller="de.bixilon.minosoft.gui.main.SessionListCell">
|
||||
<Label fx:id="connectionId" layoutX="111.0" layoutY="14.0" maxWidth="200.0" minWidth="10.0" text="#1"
|
||||
underline="true" AnchorPane.bottomAnchor="5.0" AnchorPane.leftAnchor="5.0" AnchorPane.topAnchor="5.0">
|
||||
<font>
|
||||
<Font size="17.0"/>
|
||||
</font>
|
||||
</Label>
|
||||
<Label fx:id="account" layoutX="408.0" layoutY="8.0" text="account" AnchorPane.bottomAnchor="5.0"
|
||||
AnchorPane.leftAnchor="100.0" AnchorPane.topAnchor="5.0"/>
|
||||
<MenuButton layoutX="389.0" layoutY="81.0" mnemonicParsing="false" text="⋮" AnchorPane.bottomAnchor="5.0"
|
||||
AnchorPane.rightAnchor="5.0" AnchorPane.topAnchor="5.0">
|
||||
<items>
|
||||
<MenuItem mnemonicParsing="false" onAction="#disconnect" style="-fx-text-fill: red" text="Disconnect"/>
|
||||
</items>
|
||||
</MenuButton>
|
||||
</AnchorPane>
|
22
src/main/resources/layout/sessions.fxml
Normal file
22
src/main/resources/layout/sessions.fxml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.Menu?>
|
||||
<?import javafx.scene.control.MenuBar?>
|
||||
<?import javafx.scene.control.MenuItem?>
|
||||
<?import javafx.scene.control.ScrollPane?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml/1" prefHeight="400.0" prefWidth="640.0" xmlns="http://javafx.com/javafx/11.0.1"
|
||||
fx:controller="de.bixilon.minosoft.gui.main.SessionsWindow">
|
||||
<MenuBar VBox.vgrow="NEVER">
|
||||
<Menu mnemonicParsing="false" text="_Disconnect">
|
||||
<MenuItem mnemonicParsing="false" onAction="#disconnectAll" text="From all"/>
|
||||
</Menu>
|
||||
</MenuBar>
|
||||
<AnchorPane VBox.vgrow="ALWAYS">
|
||||
<ScrollPane fitToHeight="true" fitToWidth="true" layoutX="14.0" layoutY="14.0" minHeight="-Infinity"
|
||||
minWidth="-Infinity" prefHeight="500.0" prefWidth="500.0" AnchorPane.bottomAnchor="0.0"
|
||||
AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
|
||||
<BorderPane fx:id="accountPane"/>
|
||||
</ScrollPane>
|
||||
</AnchorPane>
|
||||
</VBox>
|
Loading…
x
Reference in New Issue
Block a user