Merge branch 'master' of https://github.com/Anuken/Mindustry into 4.0
# Conflicts: # build.gradle # core/src/io/anuke/mindustry/Vars.java # core/src/io/anuke/mindustry/core/Control.java # core/src/io/anuke/mindustry/core/Platform.java # core/src/io/anuke/mindustry/mapeditor/MapEditorDialog.java # core/src/io/anuke/mindustry/ui/dialogs/FileChooser.java # core/src/io/anuke/mindustry/ui/fragments/MenuFragment.java # desktop/src/io/anuke/mindustry/desktop/DesktopPlatform.java
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@
|
|||||||
/desktop/mindustry-maps/
|
/desktop/mindustry-maps/
|
||||||
/desktop/gifexport/
|
/desktop/gifexport/
|
||||||
/core/lib/
|
/core/lib/
|
||||||
|
/annotations/build/
|
||||||
/kryonet/build/
|
/kryonet/build/
|
||||||
/server/build/
|
/server/build/
|
||||||
/annotations/build/
|
/annotations/build/
|
||||||
|
|||||||
Binary file not shown.
BIN
core/assets/sprites/icon@2x.icns
Normal file
BIN
core/assets/sprites/icon@2x.icns
Normal file
Binary file not shown.
@@ -69,6 +69,16 @@ public abstract class Platform {
|
|||||||
public void downloadFile(String name, byte[] bytes){}
|
public void downloadFile(String name, byte[] bytes){}
|
||||||
/**Open a file chooser. Only used on GWT backend.*/
|
/**Open a file chooser. Only used on GWT backend.*/
|
||||||
public void openFile(Consumer<InputStream> cons){}
|
public void openFile(Consumer<InputStream> cons){}
|
||||||
|
|
||||||
|
/**Show a file chooser. Desktop only.
|
||||||
|
*
|
||||||
|
* @param text File chooser title text
|
||||||
|
* @param content Type of files to be loaded
|
||||||
|
* @param cons Selection listener
|
||||||
|
* @param open Whether to open or save files.
|
||||||
|
* @param filetype File extensions to filter.
|
||||||
|
*/
|
||||||
|
public void showFileChooser(String text, String content, Consumer<FileHandle> cons, boolean open, String filetype){}
|
||||||
/**Use the default thread provider from the kryonet module for this.*/
|
/**Use the default thread provider from the kryonet module for this.*/
|
||||||
public ThreadProvider getThreadProvider(){
|
public ThreadProvider getThreadProvider(){
|
||||||
return new ThreadProvider() {
|
return new ThreadProvider() {
|
||||||
|
|||||||
@@ -2,17 +2,20 @@ package io.anuke.mindustry.ui.dialogs;
|
|||||||
|
|
||||||
import com.badlogic.gdx.Gdx;
|
import com.badlogic.gdx.Gdx;
|
||||||
import com.badlogic.gdx.graphics.Color;
|
import com.badlogic.gdx.graphics.Color;
|
||||||
|
import com.badlogic.gdx.utils.ObjectSet;
|
||||||
import io.anuke.mindustry.ui.Links;
|
import io.anuke.mindustry.ui.Links;
|
||||||
import io.anuke.mindustry.ui.Links.LinkEntry;
|
import io.anuke.mindustry.ui.Links.LinkEntry;
|
||||||
import io.anuke.ucore.core.Core;
|
import io.anuke.ucore.core.Core;
|
||||||
import io.anuke.ucore.core.Timers;
|
import io.anuke.ucore.core.Timers;
|
||||||
import io.anuke.ucore.scene.ui.ScrollPane;
|
import io.anuke.ucore.scene.ui.ScrollPane;
|
||||||
import io.anuke.ucore.scene.ui.layout.Table;
|
import io.anuke.ucore.scene.ui.layout.Table;
|
||||||
|
import io.anuke.ucore.util.OS;
|
||||||
|
|
||||||
import static io.anuke.mindustry.Vars.ios;
|
import static io.anuke.mindustry.Vars.ios;
|
||||||
import static io.anuke.mindustry.Vars.ui;
|
import static io.anuke.mindustry.Vars.ui;
|
||||||
|
|
||||||
public class AboutDialog extends FloatingDialog {
|
public class AboutDialog extends FloatingDialog {
|
||||||
|
private static ObjectSet<String> bannedItems = ObjectSet.with("google-play", "itch.io", "dev-builds", "trello");
|
||||||
|
|
||||||
public AboutDialog(){
|
public AboutDialog(){
|
||||||
super("$text.about.button");
|
super("$text.about.button");
|
||||||
@@ -26,7 +29,7 @@ public class AboutDialog extends FloatingDialog {
|
|||||||
ScrollPane pane = new ScrollPane(in, "clear");
|
ScrollPane pane = new ScrollPane(in, "clear");
|
||||||
|
|
||||||
for(LinkEntry link : Links.getLinks()){
|
for(LinkEntry link : Links.getLinks()){
|
||||||
if(ios && link.name.equals("google-play")){ //because Apple doesn't like me mentioning android
|
if((ios || OS.isMac) && bannedItems.contains(link.name)){ //because Apple doesn't like me mentioning things
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,13 +67,14 @@ public class AboutDialog extends FloatingDialog {
|
|||||||
content().add(pane).growX();
|
content().add(pane).growX();
|
||||||
|
|
||||||
buttons().addButton("$text.credits", this::showCredits).size(200f, 64f);
|
buttons().addButton("$text.credits", this::showCredits).size(200f, 64f);
|
||||||
if(!ios){
|
|
||||||
|
if(!ios && !OS.isMac){
|
||||||
buttons().addButton("$text.changelog.title", ui.changelog::show).size(200f, 64f);
|
buttons().addButton("$text.changelog.title", ui.changelog::show).size(200f, 64f);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showCredits(){
|
public void showCredits(){
|
||||||
FloatingDialog dialog = new FloatingDialog("$text.credits");
|
FloatingDialog dialog = new FloatingDialog("$text.credits");
|
||||||
dialog.addCloseButton();
|
dialog.addCloseButton();
|
||||||
dialog.content().add("$text.about");
|
dialog.content().add("$text.about");
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import io.anuke.ucore.core.Settings;
|
|||||||
import io.anuke.ucore.scene.ui.ScrollPane;
|
import io.anuke.ucore.scene.ui.ScrollPane;
|
||||||
import io.anuke.ucore.scene.ui.layout.Table;
|
import io.anuke.ucore.scene.ui.layout.Table;
|
||||||
import io.anuke.ucore.util.Log;
|
import io.anuke.ucore.util.Log;
|
||||||
|
import io.anuke.ucore.util.OS;
|
||||||
|
|
||||||
import static io.anuke.mindustry.Vars.ios;
|
import static io.anuke.mindustry.Vars.ios;
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ public class ChangelogDialog extends FloatingDialog{
|
|||||||
|
|
||||||
content().add("$text.changelog.loading");
|
content().add("$text.changelog.loading");
|
||||||
|
|
||||||
if(!ios) {
|
if(!ios && !OS.isMac) {
|
||||||
Changelogs.getChangelog(result -> {
|
Changelogs.getChangelog(result -> {
|
||||||
versions = result;
|
versions = result;
|
||||||
Gdx.app.postRunnable(this::setup);
|
Gdx.app.postRunnable(this::setup);
|
||||||
@@ -55,6 +56,10 @@ public class ChangelogDialog extends FloatingDialog{
|
|||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
for(VersionInfo info : versions){
|
for(VersionInfo info : versions){
|
||||||
|
String desc = info.description;
|
||||||
|
|
||||||
|
desc = desc.replace("Android", "Mobile");
|
||||||
|
|
||||||
Table in = new Table("clear");
|
Table in = new Table("clear");
|
||||||
in.top().left().margin(10);
|
in.top().left().margin(10);
|
||||||
|
|
||||||
@@ -67,7 +72,7 @@ public class ChangelogDialog extends FloatingDialog{
|
|||||||
in.add("$text.changelog.latest");
|
in.add("$text.changelog.latest");
|
||||||
}
|
}
|
||||||
in.row();
|
in.row();
|
||||||
in.labelWrap("[lightgray]" + info.description).width(vw - 20).padTop(12);
|
in.labelWrap("[lightgray]" + desc).width(vw - 20).padTop(12);
|
||||||
|
|
||||||
table.add(in).width(vw).pad(8).row();
|
table.add(in).width(vw).pad(8).row();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.badlogic.gdx.utils.Array;
|
|||||||
import com.badlogic.gdx.utils.Pools;
|
import com.badlogic.gdx.utils.Pools;
|
||||||
import io.anuke.mindustry.Vars;
|
import io.anuke.mindustry.Vars;
|
||||||
import io.anuke.mindustry.core.Platform;
|
import io.anuke.mindustry.core.Platform;
|
||||||
|
import io.anuke.ucore.UCore;
|
||||||
import io.anuke.ucore.core.Core;
|
import io.anuke.ucore.core.Core;
|
||||||
import io.anuke.ucore.core.Timers;
|
import io.anuke.ucore.core.Timers;
|
||||||
import io.anuke.ucore.function.Consumer;
|
import io.anuke.ucore.function.Consumer;
|
||||||
@@ -16,6 +17,7 @@ import io.anuke.ucore.scene.event.Touchable;
|
|||||||
import io.anuke.ucore.scene.ui.*;
|
import io.anuke.ucore.scene.ui.*;
|
||||||
import io.anuke.ucore.scene.ui.layout.Table;
|
import io.anuke.ucore.scene.ui.layout.Table;
|
||||||
import io.anuke.ucore.scene.ui.layout.Unit;
|
import io.anuke.ucore.scene.ui.layout.Unit;
|
||||||
|
import io.anuke.ucore.util.OS;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
@@ -23,7 +25,8 @@ import static io.anuke.mindustry.Vars.gwt;
|
|||||||
|
|
||||||
public class FileChooser extends FloatingDialog {
|
public class FileChooser extends FloatingDialog {
|
||||||
private Table files;
|
private Table files;
|
||||||
private FileHandle homeDirectory = gwt ? Gdx.files.internal("") : Gdx.files.absolute(Gdx.files.getExternalStoragePath());
|
private FileHandle homeDirectory = gwt? Gdx.files.internal("") : Gdx.files.absolute(OS.isMac ? UCore.getProperty("user.home") + "/Downloads/" :
|
||||||
|
Gdx.files.getExternalStoragePath());
|
||||||
private FileHandle directory = homeDirectory;
|
private FileHandle directory = homeDirectory;
|
||||||
private ScrollPane pane;
|
private ScrollPane pane;
|
||||||
private TextField navigation, filefield;
|
private TextField navigation, filefield;
|
||||||
@@ -99,6 +102,11 @@ public class FileChooser extends FloatingDialog {
|
|||||||
updateFiles(true);
|
updateFiles(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Macs are confined to the Downloads/ directory
|
||||||
|
if(OS.isMac){
|
||||||
|
up.setDisabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
ImageButton back = new ImageButton("icon-arrow-left");
|
ImageButton back = new ImageButton("icon-arrow-left");
|
||||||
back.resizeImage(isize);
|
back.resizeImage(isize);
|
||||||
|
|
||||||
@@ -169,7 +177,8 @@ public class FileChooser extends FloatingDialog {
|
|||||||
|
|
||||||
private void updateFiles(boolean push){
|
private void updateFiles(boolean push){
|
||||||
if(push) stack.push(directory);
|
if(push) stack.push(directory);
|
||||||
navigation.setText(directory.toString());
|
//if is mac, don't display extra info since you can only ever go to downloads
|
||||||
|
navigation.setText(OS.isMac ? directory.name() : directory.toString());
|
||||||
|
|
||||||
GlyphLayout layout = Pools.obtain(GlyphLayout.class);
|
GlyphLayout layout = Pools.obtain(GlyphLayout.class);
|
||||||
|
|
||||||
@@ -184,23 +193,25 @@ public class FileChooser extends FloatingDialog {
|
|||||||
Pools.free(layout);
|
Pools.free(layout);
|
||||||
|
|
||||||
files.clearChildren();
|
files.clearChildren();
|
||||||
|
files.top().left();
|
||||||
FileHandle[] names = getFileNames();
|
FileHandle[] names = getFileNames();
|
||||||
|
|
||||||
Image upimage = new Image("icon-folder-parent");
|
//macs are confined to the Downloads/ directory
|
||||||
|
if(!OS.isMac) {
|
||||||
|
Image upimage = new Image("icon-folder-parent");
|
||||||
|
TextButton upbutton = new TextButton(".." + directory.toString());
|
||||||
|
upbutton.clicked(() -> {
|
||||||
|
directory = directory.parent();
|
||||||
|
updateFiles(true);
|
||||||
|
});
|
||||||
|
|
||||||
TextButton upbutton = new TextButton(".." + directory.toString());
|
upbutton.left().add(upimage).padRight(4f).size(14 * 2);
|
||||||
upbutton.clicked(()->{
|
upbutton.getLabel().setAlignment(Align.left);
|
||||||
directory = directory.parent();
|
upbutton.getCells().reverse();
|
||||||
updateFiles(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
upbutton.left().add(upimage).padRight(4f).size(14*2);
|
|
||||||
upbutton.getCells().reverse();
|
|
||||||
|
|
||||||
files.top().left().add(upbutton).align(Align.topLeft).fillX().expandX().height(50).pad(2).colspan(2);
|
|
||||||
upbutton.getLabel().setAlignment(Align.left);
|
|
||||||
|
|
||||||
files.row();
|
files.add(upbutton).align(Align.topLeft).fillX().expandX().height(50).pad(2).colspan(2);
|
||||||
|
files.row();
|
||||||
|
}
|
||||||
|
|
||||||
ButtonGroup<TextButton> group = new ButtonGroup<TextButton>();
|
ButtonGroup<TextButton> group = new ButtonGroup<TextButton>();
|
||||||
group.setMinCheckCount(0);
|
group.setMinCheckCount(0);
|
||||||
|
|||||||
@@ -87,14 +87,14 @@ public class LoadDialog extends FloatingDialog{
|
|||||||
if(!gwt) {
|
if(!gwt) {
|
||||||
t.addImageButton("icon-save", "empty", 14 * 3, () -> {
|
t.addImageButton("icon-save", "empty", 14 * 3, () -> {
|
||||||
if(!ios) {
|
if(!ios) {
|
||||||
new FileChooser("$text.save.export", false, file -> {
|
Platform.instance.showFileChooser(Bundles.get("text.save.export"), "Mindustry Save", file -> {
|
||||||
try {
|
try {
|
||||||
slot.exportFile(file);
|
slot.exportFile(file);
|
||||||
setup();
|
setup();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
ui.showError(Bundles.format("text.save.export.fail", Strings.parseException(e, false)));
|
ui.showError(Bundles.format("text.save.export.fail", Strings.parseException(e, false)));
|
||||||
}
|
}
|
||||||
}).show();
|
}, false, "mins");
|
||||||
}else{
|
}else{
|
||||||
try {
|
try {
|
||||||
FileHandle file = Gdx.files.local("save-" + slot.getName() + ".mins");
|
FileHandle file = Gdx.files.local("save-" + slot.getName() + ".mins");
|
||||||
@@ -150,7 +150,7 @@ public class LoadDialog extends FloatingDialog{
|
|||||||
if(gwt || ios) return;
|
if(gwt || ios) return;
|
||||||
|
|
||||||
slots.addImageTextButton("$text.save.import", "icon-add", "clear", 14*3, () -> {
|
slots.addImageTextButton("$text.save.import", "icon-add", "clear", 14*3, () -> {
|
||||||
new FileChooser("$text.save.import", f -> f.extension().equals("mins"), true, file -> {
|
Platform.instance.showFileChooser(Bundles.get("text.save.import"), "Mindustry Save", file -> {
|
||||||
if(SaveIO.isSaveValid(file)){
|
if(SaveIO.isSaveValid(file)){
|
||||||
try{
|
try{
|
||||||
control.getSaves().importSave(file);
|
control.getSaves().importSave(file);
|
||||||
@@ -161,7 +161,7 @@ public class LoadDialog extends FloatingDialog{
|
|||||||
}else{
|
}else{
|
||||||
ui.showError("$text.save.import.invalid");
|
ui.showError("$text.save.import.invalid");
|
||||||
}
|
}
|
||||||
}).show();
|
}, true, "mins");
|
||||||
}).fillX().margin(10f).minWidth(300f).height(70f).pad(4f).padRight(-4);
|
}).fillX().margin(10f).minWidth(300f).height(70f).pad(4f).padRight(-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import io.anuke.ucore.scene.Group;
|
|||||||
import io.anuke.ucore.scene.builders.imagebutton;
|
import io.anuke.ucore.scene.builders.imagebutton;
|
||||||
import io.anuke.ucore.scene.builders.label;
|
import io.anuke.ucore.scene.builders.label;
|
||||||
import io.anuke.ucore.scene.builders.table;
|
import io.anuke.ucore.scene.builders.table;
|
||||||
|
import io.anuke.ucore.util.OS;
|
||||||
|
|
||||||
import static io.anuke.mindustry.Vars.*;
|
import static io.anuke.mindustry.Vars.*;
|
||||||
|
|
||||||
@@ -39,7 +40,13 @@ public class MenuFragment implements Fragment{
|
|||||||
|
|
||||||
add(new MenuButton("icon-info", "$text.about.button", ui.about::show));
|
add(new MenuButton("icon-info", "$text.about.button", ui.about::show));
|
||||||
|
|
||||||
add(new MenuButton("icon-tools", "$text.settings", ui.settings::show));
|
add(new MenuButton("icon-menu", OS.isMac ? "$text.credits" : "$text.changelog.title", () -> {
|
||||||
|
if(OS.isMac){
|
||||||
|
ui.about.showCredits();
|
||||||
|
}else {
|
||||||
|
ui.changelog.show();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
row();
|
row();
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ task packrCmd() {
|
|||||||
|
|
||||||
commandLine("java", "-jar", PACKR_DIR + "packr.jar",
|
commandLine("java", "-jar", PACKR_DIR + "packr.jar",
|
||||||
"--verbose",
|
"--verbose",
|
||||||
"--bundle", getPackage(),
|
"--bundle", getPackage() + ".mac",
|
||||||
"--platform", getPlatform(),
|
"--platform", getPlatform(),
|
||||||
"--executable", appName,
|
"--executable", appName,
|
||||||
"--output", "packr-out/",
|
"--output", "packr-out/",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.anuke.mindustry.desktop;
|
package io.anuke.mindustry.desktop;
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Files.FileType;
|
||||||
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
|
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
|
||||||
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
|
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
|
||||||
import io.anuke.kryonet.KryoClient;
|
import io.anuke.kryonet.KryoClient;
|
||||||
@@ -7,6 +8,8 @@ import io.anuke.kryonet.KryoServer;
|
|||||||
import io.anuke.mindustry.Mindustry;
|
import io.anuke.mindustry.Mindustry;
|
||||||
import io.anuke.mindustry.core.Platform;
|
import io.anuke.mindustry.core.Platform;
|
||||||
import io.anuke.mindustry.net.Net;
|
import io.anuke.mindustry.net.Net;
|
||||||
|
import io.anuke.ucore.UCore;
|
||||||
|
import io.anuke.ucore.util.OS;
|
||||||
|
|
||||||
public class DesktopLauncher {
|
public class DesktopLauncher {
|
||||||
|
|
||||||
@@ -18,6 +21,10 @@ public class DesktopLauncher {
|
|||||||
config.setWindowedMode(960, 540);
|
config.setWindowedMode(960, 540);
|
||||||
config.setWindowIcon("sprites/icon.png");
|
config.setWindowIcon("sprites/icon.png");
|
||||||
|
|
||||||
|
if(OS.isMac) {
|
||||||
|
config.setPreferencesConfig(UCore.getProperty("user.home") + "/Library/Application Support/Mindustry", FileType.Absolute);
|
||||||
|
}
|
||||||
|
|
||||||
Platform.instance = new DesktopPlatform(arg);
|
Platform.instance = new DesktopPlatform(arg);
|
||||||
|
|
||||||
Net.setClientProvider(new KryoClient());
|
Net.setClientProvider(new KryoClient());
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package io.anuke.mindustry.desktop;
|
|||||||
import club.minnced.discord.rpc.DiscordEventHandlers;
|
import club.minnced.discord.rpc.DiscordEventHandlers;
|
||||||
import club.minnced.discord.rpc.DiscordRPC;
|
import club.minnced.discord.rpc.DiscordRPC;
|
||||||
import club.minnced.discord.rpc.DiscordRichPresence;
|
import club.minnced.discord.rpc.DiscordRichPresence;
|
||||||
|
import com.badlogic.gdx.files.FileHandle;
|
||||||
import com.badlogic.gdx.utils.Base64Coder;
|
import com.badlogic.gdx.utils.Base64Coder;
|
||||||
import io.anuke.kryonet.DefaultThreadImpl;
|
import io.anuke.kryonet.DefaultThreadImpl;
|
||||||
import io.anuke.mindustry.core.GameState.State;
|
import io.anuke.mindustry.core.GameState.State;
|
||||||
import io.anuke.mindustry.core.Platform;
|
import io.anuke.mindustry.core.Platform;
|
||||||
import io.anuke.mindustry.core.ThreadHandler.ThreadProvider;
|
import io.anuke.mindustry.core.ThreadHandler.ThreadProvider;
|
||||||
import io.anuke.mindustry.net.Net;
|
import io.anuke.mindustry.net.Net;
|
||||||
|
import io.anuke.mindustry.ui.dialogs.FileChooser;
|
||||||
import io.anuke.ucore.core.Settings;
|
import io.anuke.ucore.core.Settings;
|
||||||
import io.anuke.ucore.util.OS;
|
import io.anuke.ucore.util.OS;
|
||||||
import io.anuke.ucore.util.Strings;
|
import io.anuke.ucore.util.Strings;
|
||||||
@@ -41,6 +43,11 @@ public class DesktopPlatform extends Platform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showFileChooser(String text, String content, Consumer<FileHandle> cons, boolean open, String filter) {
|
||||||
|
new FileChooser(text, file -> file.extension().equalsIgnoreCase(filter), open, cons).show();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String format(Date date){
|
public String format(Date date){
|
||||||
return format.format(date);
|
return format.format(date);
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ app.version=3.5
|
|||||||
app.id=io.anuke.mindustry
|
app.id=io.anuke.mindustry
|
||||||
app.mainclass=io.anuke.mindustry.IOSLauncher
|
app.mainclass=io.anuke.mindustry.IOSLauncher
|
||||||
app.executable=IOSLauncher
|
app.executable=IOSLauncher
|
||||||
app.build=10
|
app.build=11
|
||||||
app.name=Mindustry
|
app.name=Mindustry
|
||||||
|
|||||||
Reference in New Issue
Block a user