mirror of
https://github.com/2OOP/pism.git
synced 2026-02-04 10:54:51 +00:00
Merge remote-tracking branch 'origin/121-settings-menu' into UI
# Conflicts: # app/src/main/java/org/toop/app/menu/OptionsMenu.java # app/src/main/resources/assets/localization/localization_de.properties # app/src/main/resources/assets/localization/localization_en_US.properties # app/src/main/resources/assets/localization/localization_es.properties # app/src/main/resources/assets/localization/localization_fr.properties # app/src/main/resources/assets/localization/localization_it.properties # app/src/main/resources/assets/localization/localization_nl.properties # app/src/main/resources/assets/localization/localization_zh.properties
This commit is contained in:
@@ -11,7 +11,7 @@ import java.util.Locale;
|
|||||||
|
|
||||||
public final class CreditsMenu extends Menu {
|
public final class CreditsMenu extends Menu {
|
||||||
private Locale currentLocale = AppContext.getLocale();
|
private Locale currentLocale = AppContext.getLocale();
|
||||||
private LocalizationAsset loc = ResourceManager.get("localization.properties");
|
private LocalizationAsset loc = ResourceManager.get("localization_en_us.properties");
|
||||||
public CreditsMenu() {
|
public CreditsMenu() {
|
||||||
try {
|
try {
|
||||||
new EventFlow()
|
new EventFlow()
|
||||||
|
|||||||
@@ -14,10 +14,14 @@ import org.toop.framework.eventbus.EventFlow;
|
|||||||
import org.toop.local.AppContext;
|
import org.toop.local.AppContext;
|
||||||
import org.toop.local.LocalizationEvents;
|
import org.toop.local.LocalizationEvents;
|
||||||
|
|
||||||
|
import java.awt.GraphicsDevice;
|
||||||
|
import java.awt.GraphicsEnvironment;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
public final class OptionsMenu extends Menu {
|
public final class OptionsMenu extends Menu {
|
||||||
private Locale currentLocale = AppContext.getLocale();
|
private Locale currentLocale = AppContext.getLocale();
|
||||||
|
private LocalizationAsset loc = ResourceManager.get("localization");
|
||||||
|
|
||||||
private final LocalizationAsset loc = ResourceManager.get("localization.properties");
|
private final LocalizationAsset loc = ResourceManager.get("localization.properties");
|
||||||
private Text chooseLang;
|
private Text chooseLang;
|
||||||
private Button english,dutch,german,french,italian,spanish,chinese;
|
private Button english,dutch,german,french,italian,spanish,chinese;
|
||||||
@@ -65,5 +69,65 @@ public final class OptionsMenu extends Menu {
|
|||||||
chinese.setText(loc.getString("languageChinese",currentLocale));
|
chinese.setText(loc.getString("languageChinese",currentLocale));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
final Region background = createBackground();
|
||||||
|
|
||||||
|
GraphicsDevice currentScreen = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[0];
|
||||||
|
|
||||||
|
LocalizationAsset locFiles = ResourceManager.get(LocalizationAsset.class, "localization");
|
||||||
|
final Label selectLanguageLabel = new Label(loc.getString("optionsMenuLabelSelectLanguage", currentLocale));
|
||||||
|
final ChoiceBox<Locale> selectLanguage = new ChoiceBox<>();
|
||||||
|
selectLanguage.setValue(currentLocale);
|
||||||
|
for (Locale locFile : locFiles.getAvailableLocales()) {
|
||||||
|
selectLanguage.getItems().add(locFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectLanguage.setOnAction((event) -> {
|
||||||
|
Locale selectedLocale = selectLanguage.getSelectionModel().getSelectedItem();
|
||||||
|
AppContext.setLocale(selectedLocale);
|
||||||
|
App.pop();
|
||||||
|
App.push(new OptionsMenu());
|
||||||
|
});
|
||||||
|
|
||||||
|
// GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
|
||||||
|
// GraphicsDevice[] devices = ge.getScreenDevices();
|
||||||
|
// final ChoiceBox<GraphicsDevice> selectScreen = new ChoiceBox<>();
|
||||||
|
// for (GraphicsDevice screen : devices) {
|
||||||
|
// selectScreen.getItems().add(screen);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// selectScreen.setOnAction((event) -> {
|
||||||
|
// int selectedIndex = selectScreen.getSelectionModel().getSelectedIndex();
|
||||||
|
// Object selectedItem = selectScreen.getSelectionModel().getSelectedItem();
|
||||||
|
//
|
||||||
|
// System.out.println("Selection made: [" + selectedIndex + "] " + selectedItem);
|
||||||
|
// System.out.println(" ChoiceBox.getValue(): " + selectScreen.getValue());
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// final ChoiceBox<DisplayMode> selectWindowSize = new ChoiceBox<>();
|
||||||
|
// for (DisplayMode displayMode : currentScreen.getDisplayModes()) {
|
||||||
|
// selectWindowSize.getItems().add(displayMode);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//// if (currentScreen.isFullScreenSupported()) {}
|
||||||
|
// final CheckBox setFullscreen = new CheckBox("Fullscreen");
|
||||||
|
|
||||||
|
final VBox optionsBox = new VBox(10, selectLanguageLabel, selectLanguage);
|
||||||
|
optionsBox.setAlignment(Pos.CENTER);
|
||||||
|
optionsBox.setPickOnBounds(false);
|
||||||
|
optionsBox.setTranslateY(50);
|
||||||
|
optionsBox.setTranslateX(25);
|
||||||
|
|
||||||
|
final Button credits = createButton("Credits", () -> { App.push(new CreditsMenu()); });
|
||||||
|
final Button options = createButton("Exit Options", () -> { App.push(new MainMenu()); });
|
||||||
|
final Button quit = createButton("Quit", () -> { App.quitPopup(); });
|
||||||
|
|
||||||
|
final VBox controlBox = new VBox(10, credits, options, quit);
|
||||||
|
controlBox.setAlignment(Pos.BOTTOM_LEFT);
|
||||||
|
controlBox.setPickOnBounds(false);
|
||||||
|
controlBox.setTranslateY(-50);
|
||||||
|
controlBox.setTranslateX(25);
|
||||||
|
|
||||||
|
pane = new StackPane(background, optionsBox, controlBox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,10 @@ import java.util.Locale;
|
|||||||
public class AppContext {
|
public class AppContext {
|
||||||
private static Locale currentLocale = Locale.getDefault();
|
private static Locale currentLocale = Locale.getDefault();
|
||||||
|
|
||||||
|
public static void setLocale(Locale locale) {
|
||||||
|
currentLocale = locale;
|
||||||
|
}
|
||||||
|
|
||||||
public static void setCurrentLocale(Locale locale) {
|
public static void setCurrentLocale(Locale locale) {
|
||||||
currentLocale = locale;
|
currentLocale = locale;
|
||||||
new EventFlow().addPostEvent(new LocalizationEvents.LanguageHasChanged(locale.getLanguage())).asyncPostEvent();
|
new EventFlow().addPostEvent(new LocalizationEvents.LanguageHasChanged(locale.getLanguage())).asyncPostEvent();
|
||||||
|
|||||||
@@ -34,3 +34,7 @@ gameSelectMenuEnterIP=Gib hier deine IP-Adresse ein:
|
|||||||
# Game Menu
|
# Game Menu
|
||||||
gameMenuHint=Tipp
|
gameMenuHint=Tipp
|
||||||
gameMenuBack=Zur\u00FCck
|
gameMenuBack=Zur\u00FCck
|
||||||
|
|
||||||
|
|
||||||
|
# Options menu
|
||||||
|
optionsMenuLabelSelectLanguage=Sprache:
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
windowTitle=ISY Games Selector
|
windowTitle=ISY Games Selector
|
||||||
|
|
||||||
# Main Menu buttons
|
# Main Menu buttons
|
||||||
mainMenuSelectTicTacToe=Tic Tac Toe
|
mainMenuSelectTicTacToe=Tic Tac Toe\u5426
|
||||||
mainMenuSelectReversi=Reversi
|
mainMenuSelectReversi=Reversi\u5426
|
||||||
mainMenuSelectSudoku=Sudoku
|
mainMenuSelectSudoku=Sudoku
|
||||||
mainMenuSelectBattleship=Battleship
|
mainMenuSelectBattleship=Battleship
|
||||||
mainMenuSelectOther=Other
|
mainMenuSelectOther=Other
|
||||||
@@ -34,3 +34,6 @@ gameSelectMenuEnterIP=Enter your IP address here:
|
|||||||
# Game Menu
|
# Game Menu
|
||||||
gameMenuHint=Hint
|
gameMenuHint=Hint
|
||||||
gameMenuBack=Back
|
gameMenuBack=Back
|
||||||
|
|
||||||
|
# Options menu
|
||||||
|
optionsMenuLabelSelectLanguage=Language:
|
||||||
@@ -34,3 +34,7 @@ gameSelectMenuEnterIP=Introduce aqu\u00ED tu direcci\u00F3n IP:
|
|||||||
# Game Menu
|
# Game Menu
|
||||||
gameMenuHint=Pista
|
gameMenuHint=Pista
|
||||||
gameMenuBack=Atr\u00E1s
|
gameMenuBack=Atr\u00E1s
|
||||||
|
|
||||||
|
|
||||||
|
# Options menu
|
||||||
|
optionsMenuLabelSelectLanguage=Idioma:
|
||||||
@@ -34,3 +34,7 @@ gameSelectMenuEnterIP=Entrez votre adresse IP ici :
|
|||||||
# Game Menu
|
# Game Menu
|
||||||
gameMenuHint=Indice
|
gameMenuHint=Indice
|
||||||
gameMenuBack=Retour
|
gameMenuBack=Retour
|
||||||
|
|
||||||
|
|
||||||
|
# Options menu
|
||||||
|
optionsMenuLabelSelectLanguage=Langue:
|
||||||
@@ -35,3 +35,7 @@ gameSelectMenuEnterIP=Inserisci qui il tuo indirizzo IP:
|
|||||||
gameMenuHint=Suggerimento
|
gameMenuHint=Suggerimento
|
||||||
gameMenuBack=Indietro
|
gameMenuBack=Indietro
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Options menu
|
||||||
|
optionsMenuLabelSelectLanguage=Lingua:
|
||||||
@@ -34,3 +34,7 @@ gameSelectMenuEnterIP=Voer hier je IP-adres in:
|
|||||||
# Game Menu
|
# Game Menu
|
||||||
gameMenuHint=Hint
|
gameMenuHint=Hint
|
||||||
gameMenuBack=Terug
|
gameMenuBack=Terug
|
||||||
|
|
||||||
|
|
||||||
|
# Options menu
|
||||||
|
optionsMenuLabelSelectLanguage=Taal:
|
||||||
@@ -46,3 +46,7 @@ gameSelectMenuEnterIP=\u5728\u6B64\u8F93\u5165\u4F60\u7684IP\u5730\u5740\uFF1A
|
|||||||
# Game Menu
|
# Game Menu
|
||||||
gameMenuHint=\u63D0\u793A
|
gameMenuHint=\u63D0\u793A
|
||||||
gameMenuBack=\u8FD4\u56DE
|
gameMenuBack=\u8FD4\u56DE
|
||||||
|
|
||||||
|
|
||||||
|
# Options menu
|
||||||
|
optionsMenuLabelSelectLanguage=\u8BED\u8A00:
|
||||||
@@ -158,6 +158,7 @@ public class ResourceLoader {
|
|||||||
Map<String, BundledResource> bundledResources = new HashMap<>();
|
Map<String, BundledResource> bundledResources = new HashMap<>();
|
||||||
|
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
|
boolean skipAdd = false;
|
||||||
BaseResource resource = resourceMapper(file, BaseResource.class);
|
BaseResource resource = resourceMapper(file, BaseResource.class);
|
||||||
switch (resource) {
|
switch (resource) {
|
||||||
case null -> {
|
case null -> {
|
||||||
@@ -165,13 +166,11 @@ public class ResourceLoader {
|
|||||||
}
|
}
|
||||||
case BundledResource br -> {
|
case BundledResource br -> {
|
||||||
String key = resource.getClass().getName() + ":" + br.getBaseName();
|
String key = resource.getClass().getName() + ":" + br.getBaseName();
|
||||||
if (bundledResources.containsKey(key)) {
|
if (!bundledResources.containsKey(key)) {bundledResources.put(key, br);}
|
||||||
bundledResources.get(key).loadFile(file);
|
bundledResources.get(key).loadFile(file);
|
||||||
resource = (BaseResource) bundledResources.get(key);
|
resource = (BaseResource) bundledResources.get(key);
|
||||||
} else {
|
assets.add(new ResourceMeta<>(br.getBaseName(), resource));
|
||||||
br.loadFile(file);
|
skipAdd = true;
|
||||||
bundledResources.put(key, br);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case PreloadResource pr -> pr.load();
|
case PreloadResource pr -> pr.load();
|
||||||
default -> {
|
default -> {
|
||||||
@@ -181,7 +180,7 @@ public class ResourceLoader {
|
|||||||
BaseResource finalResource = resource;
|
BaseResource finalResource = resource;
|
||||||
boolean alreadyAdded = assets.stream()
|
boolean alreadyAdded = assets.stream()
|
||||||
.anyMatch(a -> a.getResource() == finalResource);
|
.anyMatch(a -> a.getResource() == finalResource);
|
||||||
if (!alreadyAdded) {
|
if (!alreadyAdded && !skipAdd) {
|
||||||
assets.add(new ResourceMeta<>(file.getName(), resource));
|
assets.add(new ResourceMeta<>(file.getName(), resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,33 +8,90 @@ import java.io.*;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a localization resource asset that loads and manages property files
|
||||||
|
* containing key-value pairs for different locales.
|
||||||
|
* <p>
|
||||||
|
* This class implements {@link LoadableResource} to support loading/unloading
|
||||||
|
* and {@link BundledResource} to represent resources that can contain multiple
|
||||||
|
* localized bundles.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* Files handled by this class must have the {@code .properties} extension,
|
||||||
|
* optionally with a locale suffix, e.g., {@code messages_en_US.properties}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Example usage:
|
||||||
|
* <pre>{@code
|
||||||
|
* LocalizationAsset asset = new LocalizationAsset(new File("messages_en_US.properties"));
|
||||||
|
* asset.load();
|
||||||
|
* String greeting = asset.getString("hello", Locale.US);
|
||||||
|
* }</pre>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
@FileExtension({"properties"})
|
@FileExtension({"properties"})
|
||||||
public class LocalizationAsset extends BaseResource implements LoadableResource, BundledResource {
|
public class LocalizationAsset extends BaseResource implements LoadableResource, BundledResource {
|
||||||
private final Map<Locale, ResourceBundle> bundles = new HashMap<>();
|
|
||||||
private boolean isLoaded = false;
|
|
||||||
private final Locale fallback = Locale.forLanguageTag("");
|
|
||||||
|
|
||||||
|
/** Map of loaded resource bundles, keyed by locale. */
|
||||||
|
private final Map<Locale, ResourceBundle> bundles = new HashMap<>();
|
||||||
|
|
||||||
|
/** Flag indicating whether this asset has been loaded. */
|
||||||
|
private boolean isLoaded = false;
|
||||||
|
|
||||||
|
/** Basename of the given asset */
|
||||||
|
private final String baseName = "localization";
|
||||||
|
|
||||||
|
/** Fallback locale used when no matching locale is found. */
|
||||||
|
private final Locale fallback = Locale.forLanguageTag("en_US");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new LocalizationAsset for the specified file.
|
||||||
|
*
|
||||||
|
* @param file the property file to load
|
||||||
|
*/
|
||||||
public LocalizationAsset(File file) {
|
public LocalizationAsset(File file) {
|
||||||
super(file);
|
super(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the resource file into memory and prepares localized bundles.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void load() {
|
public void load() {
|
||||||
loadFile(getFile());
|
loadFile(getFile());
|
||||||
isLoaded = true;
|
isLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unloads all loaded resource bundles, freeing memory.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void unload() {
|
public void unload() {
|
||||||
bundles.clear();
|
bundles.clear();
|
||||||
isLoaded = false;
|
isLoaded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether this asset has been loaded.
|
||||||
|
*
|
||||||
|
* @return {@code true} if the asset is loaded, {@code false} otherwise
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean isLoaded() {
|
public boolean isLoaded() {
|
||||||
return isLoaded;
|
return isLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a localized string for the given key and locale.
|
||||||
|
* If an exact match for the locale is not found, a fallback
|
||||||
|
* matching the language or the default locale will be used.
|
||||||
|
*
|
||||||
|
* @param key the key of the string
|
||||||
|
* @param locale the desired locale
|
||||||
|
* @return the localized string
|
||||||
|
* @throws MissingResourceException if no resource bundle is available for the locale
|
||||||
|
*/
|
||||||
public String getString(String key, Locale locale) {
|
public String getString(String key, Locale locale) {
|
||||||
Locale target = findBestLocale(locale);
|
Locale target = findBestLocale(locale);
|
||||||
ResourceBundle bundle = bundles.get(target);
|
ResourceBundle bundle = bundles.get(target);
|
||||||
@@ -43,6 +100,13 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
|
|||||||
return bundle.getString(key);
|
return bundle.getString(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the best matching locale among loaded bundles.
|
||||||
|
* Prefers an exact match, then language-only match, then fallback.
|
||||||
|
*
|
||||||
|
* @param locale the desired locale
|
||||||
|
* @return the best matching locale
|
||||||
|
*/
|
||||||
private Locale findBestLocale(Locale locale) {
|
private Locale findBestLocale(Locale locale) {
|
||||||
if (bundles.containsKey(locale)) return locale;
|
if (bundles.containsKey(locale)) return locale;
|
||||||
for (Locale l : bundles.keySet()) {
|
for (Locale l : bundles.keySet()) {
|
||||||
@@ -51,13 +115,24 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an unmodifiable set of all locales for which bundles are loaded.
|
||||||
|
*
|
||||||
|
* @return available locales
|
||||||
|
*/
|
||||||
public Set<Locale> getAvailableLocales() {
|
public Set<Locale> getAvailableLocales() {
|
||||||
return Collections.unmodifiableSet(bundles.keySet());
|
return Collections.unmodifiableSet(bundles.keySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a specific property file as a resource bundle.
|
||||||
|
* The locale is extracted from the file name if present.
|
||||||
|
*
|
||||||
|
* @param file the property file to load
|
||||||
|
* @throws RuntimeException if the file cannot be read
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void loadFile(File file) {
|
public void loadFile(File file) {
|
||||||
String baseName = getBaseName(file.getName());
|
|
||||||
try (InputStreamReader reader =
|
try (InputStreamReader reader =
|
||||||
new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) {
|
new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) {
|
||||||
Locale locale = extractLocale(file.getName(), baseName);
|
Locale locale = extractLocale(file.getName(), baseName);
|
||||||
@@ -68,20 +143,40 @@ public class LocalizationAsset extends BaseResource implements LoadableResource,
|
|||||||
isLoaded = true;
|
isLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the base name of the underlying file (without locale or extension).
|
||||||
|
*
|
||||||
|
* @return the base name
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String getBaseName() {
|
public String getBaseName() {
|
||||||
return getBaseName(getFile().getName());
|
return this.baseName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getBaseName(String fileName) {
|
// /**
|
||||||
int underscoreIndex = fileName.indexOf('_');
|
// * Extracts the base name from a file name.
|
||||||
int dotIndex = fileName.lastIndexOf('.');
|
// *
|
||||||
if (underscoreIndex > 0) {
|
// * @param fileName the file name
|
||||||
return fileName.substring(0, underscoreIndex);
|
// * @return base name without locale or extension
|
||||||
}
|
// */
|
||||||
return fileName.substring(0, dotIndex);
|
// private String getBaseName(String fileName) {
|
||||||
}
|
// int dotIndex = fileName.lastIndexOf('.');
|
||||||
|
// String nameWithoutExtension = (dotIndex > 0) ? fileName.substring(0, dotIndex) : fileName;
|
||||||
|
//
|
||||||
|
// int underscoreIndex = nameWithoutExtension.indexOf('_');
|
||||||
|
// if (underscoreIndex > 0) {
|
||||||
|
// return nameWithoutExtension.substring(0, underscoreIndex);
|
||||||
|
// }
|
||||||
|
// return nameWithoutExtension;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a locale from a file name based on the pattern "base_LOCALE.properties".
|
||||||
|
*
|
||||||
|
* @param fileName the file name
|
||||||
|
* @param baseName the base name
|
||||||
|
* @return extracted locale, or fallback if none found
|
||||||
|
*/
|
||||||
private Locale extractLocale(String fileName, String baseName) {
|
private Locale extractLocale(String fileName, String baseName) {
|
||||||
int underscoreIndex = fileName.indexOf('_');
|
int underscoreIndex = fileName.indexOf('_');
|
||||||
int dotIndex = fileName.lastIndexOf('.');
|
int dotIndex = fileName.lastIndexOf('.');
|
||||||
|
|||||||
@@ -66,4 +66,9 @@ public interface BundledResource {
|
|||||||
* @return the base name used to identify this bundled resource
|
* @return the base name used to identify this bundled resource
|
||||||
*/
|
*/
|
||||||
String getBaseName();
|
String getBaseName();
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// Returns the name
|
||||||
|
// */
|
||||||
|
// String getDefaultName();
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user