working general mode only with canvas, removing highlight, readonly or edit
This commit is contained in:
@@ -318,7 +318,7 @@ impl EventHandler {
|
|||||||
return Ok(EventOutcome::Ok(message));
|
return Ok(EventOutcome::Ok(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !matches!(current_mode, AppMode::Edit | AppMode::Command) {
|
if current_mode == AppMode::General {
|
||||||
if let Some(action) = config.get_action_for_key_in_mode(
|
if let Some(action) = config.get_action_for_key_in_mode(
|
||||||
&config.keybindings.global,
|
&config.keybindings.global,
|
||||||
key_code,
|
key_code,
|
||||||
@@ -352,9 +352,7 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(action) =
|
if let Some(action) = config.get_general_action(key_code, modifiers) {
|
||||||
config.get_general_action(key_code, modifiers)
|
|
||||||
{
|
|
||||||
if action == "open_search" {
|
if action == "open_search" {
|
||||||
if let Page::Form(_) = &router.current {
|
if let Page::Form(_) = &router.current {
|
||||||
if let Some(table_name) =
|
if let Some(table_name) =
|
||||||
@@ -520,143 +518,35 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppMode::ReadOnly => {
|
AppMode::General => {
|
||||||
// First let the canvas editor try to handle the key
|
match &router.current {
|
||||||
if let Page::Form(_) = &router.current {
|
Page::Form(_)
|
||||||
if let Some(editor) = &mut app_state.form_editor {
|
| Page::Login(_)
|
||||||
let outcome = editor.handle_key_event(key_event);
|
| Page::Register(_)
|
||||||
let new_mode = AppMode::from(editor.mode());
|
| Page::AddTable(_)
|
||||||
match outcome {
|
| Page::AddLogic(_) => {
|
||||||
KeyEventOutcome::Consumed(Some(msg)) => {
|
if !app_state.ui.focus_outside_canvas {
|
||||||
app_state.update_mode(new_mode);
|
if let Some(editor) = &mut app_state.form_editor {
|
||||||
return Ok(EventOutcome::Ok(msg));
|
editor.set_keymap(config.build_canvas_keymap());
|
||||||
}
|
match editor.handle_key_event(key_event) {
|
||||||
KeyEventOutcome::Consumed(None) => {
|
KeyEventOutcome::Consumed(Some(msg)) => {
|
||||||
app_state.update_mode(new_mode);
|
return Ok(EventOutcome::Ok(msg));
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
}
|
||||||
}
|
KeyEventOutcome::Consumed(None) => {
|
||||||
KeyEventOutcome::Pending => {
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
app_state.update_mode(new_mode);
|
}
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
KeyEventOutcome::Pending => {
|
||||||
}
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
KeyEventOutcome::NotMatched => {
|
}
|
||||||
app_state.update_mode(new_mode);
|
KeyEventOutcome::NotMatched => {
|
||||||
// Fall through
|
// fall through to client actions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entering command mode is still a client-level action
|
|
||||||
if config.get_app_action(key_code, modifiers) == Some("enter_command_mode")
|
|
||||||
&& ModeManager::can_enter_command_mode(current_mode)
|
|
||||||
{
|
|
||||||
if let Some(editor) = &mut app_state.form_editor {
|
|
||||||
editor.set_mode(CanvasMode::Command);
|
|
||||||
}
|
|
||||||
self.command_mode = true;
|
|
||||||
self.command_input.clear();
|
|
||||||
self.command_message.clear();
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle common actions (save, quit, etc.)
|
|
||||||
if let Some(action) = config.get_app_action(key_code, modifiers) {
|
|
||||||
match action {
|
|
||||||
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
|
||||||
return self
|
|
||||||
.handle_core_action(
|
|
||||||
action,
|
|
||||||
auth_state,
|
|
||||||
terminal,
|
|
||||||
app_state,
|
|
||||||
router,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
AppMode::Highlight => {
|
|
||||||
if let Page::Form(_) = &router.current {
|
|
||||||
if let Some(editor) = &mut app_state.form_editor {
|
|
||||||
let outcome = editor.handle_key_event(key_event);
|
|
||||||
let new_mode = AppMode::from(editor.mode());
|
|
||||||
match outcome {
|
|
||||||
KeyEventOutcome::Consumed(Some(msg)) => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(EventOutcome::Ok(msg));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::Consumed(None) => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::Pending => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::NotMatched => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
// Fall through
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
AppMode::Edit => {
|
|
||||||
// Handle common actions (save, quit, etc.)
|
|
||||||
if let Some(action) = config.get_app_action(key_code, modifiers) {
|
|
||||||
match action {
|
|
||||||
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
|
||||||
return self
|
|
||||||
.handle_core_action(
|
|
||||||
action,
|
|
||||||
auth_state,
|
|
||||||
terminal,
|
|
||||||
app_state,
|
|
||||||
router,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let the canvas editor handle edit-mode keys
|
|
||||||
if let Page::Form(_) = &router.current {
|
|
||||||
if let Some(editor) = &mut app_state.form_editor {
|
|
||||||
let outcome = editor.handle_key_event(key_event);
|
|
||||||
let new_mode = AppMode::from(editor.mode());
|
|
||||||
match outcome {
|
|
||||||
KeyEventOutcome::Consumed(Some(msg)) => {
|
|
||||||
self.command_message = msg.clone();
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(EventOutcome::Ok(msg));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::Consumed(None) => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::Pending => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
KeyEventOutcome::NotMatched => {
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
// Fall through
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AppMode::Command => {
|
AppMode::Command => {
|
||||||
|
|||||||
@@ -1,34 +1,27 @@
|
|||||||
// src/modes/handlers/mode_manager.rs
|
// src/modes/handlers/mode_manager.rs
|
||||||
|
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::modes::handlers::event::EventHandler;
|
use crate::modes::handlers::event::EventHandler;
|
||||||
use crate::state::pages::add_logic::AddLogicFocus;
|
|
||||||
use crate::pages::routing::{Router, Page};
|
use crate::pages::routing::{Router, Page};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum AppMode {
|
pub enum AppMode {
|
||||||
General, // For intro and admin screens
|
/// General mode = when focus is outside any canvas
|
||||||
ReadOnly, // Canvas read-only mode
|
/// (Intro, Admin, Login/Register buttons, AddTable/AddLogic menus, dialogs, etc.)
|
||||||
Edit, // Canvas edit mode
|
General,
|
||||||
Highlight, // Canvas highlight/visual mode
|
|
||||||
Command, // Command mode overlay
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<canvas::AppMode> for AppMode {
|
/// Command overlay (":" or "ctrl+;"), available globally
|
||||||
fn from(mode: canvas::AppMode) -> Self {
|
Command,
|
||||||
match mode {
|
|
||||||
canvas::AppMode::General => AppMode::General,
|
|
||||||
canvas::AppMode::ReadOnly => AppMode::ReadOnly,
|
|
||||||
canvas::AppMode::Edit => AppMode::Edit,
|
|
||||||
canvas::AppMode::Highlight => AppMode::Highlight,
|
|
||||||
canvas::AppMode::Command => AppMode::Command,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ModeManager;
|
pub struct ModeManager;
|
||||||
|
|
||||||
impl ModeManager {
|
impl ModeManager {
|
||||||
/// Determine current mode based on app state + router
|
/// Determine current mode:
|
||||||
|
/// - If navigation palette is active → General
|
||||||
|
/// - If command overlay is active → Command
|
||||||
|
/// - If focus is inside a canvas (Form, Login, Register, AddTable, AddLogic) → let canvas handle its own mode
|
||||||
|
/// - Otherwise → General
|
||||||
pub fn derive_mode(
|
pub fn derive_mode(
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
event_handler: &EventHandler,
|
event_handler: &EventHandler,
|
||||||
@@ -39,76 +32,28 @@ impl ModeManager {
|
|||||||
return AppMode::General;
|
return AppMode::General;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicit command mode flag
|
// Explicit command overlay flag
|
||||||
if event_handler.command_mode {
|
if event_handler.command_mode {
|
||||||
return AppMode::Command;
|
return AppMode::Command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If focus is inside a canvas, we don't duplicate canvas modes here.
|
||||||
|
// Canvas crate owns ReadOnly/Edit/Highlight internally.
|
||||||
match &router.current {
|
match &router.current {
|
||||||
// --- Form view ---
|
Page::Form(_)
|
||||||
Page::Form(_) if !app_state.ui.focus_outside_canvas => {
|
| Page::Login(_)
|
||||||
if let Some(editor) = &app_state.form_editor {
|
| Page::Register(_)
|
||||||
return AppMode::from(editor.mode());
|
| Page::AddTable(_)
|
||||||
}
|
| Page::AddLogic(_) if !app_state.ui.focus_outside_canvas => {
|
||||||
|
// Canvas active → let canvas handle its own AppMode
|
||||||
AppMode::General
|
AppMode::General
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- AddLogic view ---
|
|
||||||
Page::AddLogic(state) => match state.current_focus {
|
|
||||||
AddLogicFocus::InputLogicName
|
|
||||||
| AddLogicFocus::InputTargetColumn
|
|
||||||
| AddLogicFocus::InputDescription => {
|
|
||||||
if event_handler.is_edit_mode {
|
|
||||||
AppMode::Edit
|
|
||||||
} else {
|
|
||||||
AppMode::ReadOnly
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => AppMode::General,
|
|
||||||
},
|
|
||||||
|
|
||||||
// --- AddTable view ---
|
|
||||||
Page::AddTable(_) => {
|
|
||||||
if app_state.ui.focus_outside_canvas {
|
|
||||||
AppMode::General
|
|
||||||
} else if event_handler.is_edit_mode {
|
|
||||||
AppMode::Edit
|
|
||||||
} else {
|
|
||||||
AppMode::ReadOnly
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Login/Register views ---
|
|
||||||
Page::Login(_) | Page::Register(_) => {
|
|
||||||
if event_handler.is_edit_mode {
|
|
||||||
AppMode::Edit
|
|
||||||
} else {
|
|
||||||
AppMode::ReadOnly
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Everything else (Intro, Admin, etc.) ---
|
|
||||||
_ => AppMode::General,
|
_ => AppMode::General,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode transition rules
|
/// Command overlay can be entered from anywhere (General or Canvas).
|
||||||
pub fn can_enter_command_mode(current_mode: AppMode) -> bool {
|
pub fn can_enter_command_mode(_current_mode: AppMode) -> bool {
|
||||||
!matches!(current_mode, AppMode::Edit)
|
true
|
||||||
}
|
|
||||||
|
|
||||||
pub fn can_enter_edit_mode(current_mode: AppMode) -> bool {
|
|
||||||
matches!(current_mode, AppMode::ReadOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn can_enter_read_only_mode(current_mode: AppMode) -> bool {
|
|
||||||
matches!(
|
|
||||||
current_mode,
|
|
||||||
AppMode::Edit | AppMode::Command | AppMode::Highlight
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn can_enter_highlight_mode(current_mode: AppMode) -> bool {
|
|
||||||
matches!(current_mode, AppMode::ReadOnly)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ pub fn render_login(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// --- SUGGESTIONS DROPDOWN (if active) ---
|
// --- SUGGESTIONS DROPDOWN (if active) ---
|
||||||
if app_state.current_mode == crate::modes::handlers::mode_manager::AppMode::Edit {
|
if editor.mode() == canvas::AppMode::Edit {
|
||||||
if let Some(input_rect) = input_rect {
|
if let Some(input_rect) = input_rect {
|
||||||
render_suggestions_dropdown(
|
render_suggestions_dropdown(
|
||||||
f,
|
f,
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ pub fn render_register(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// --- AUTOCOMPLETE DROPDOWN (Using new canvas suggestions) ---
|
// --- AUTOCOMPLETE DROPDOWN (Using new canvas suggestions) ---
|
||||||
if app_state.current_mode == AppMode::Edit {
|
if editor.mode() == canvas::AppMode::Edit {
|
||||||
if let Some(input_rect) = input_rect {
|
if let Some(input_rect) = input_rect {
|
||||||
render_suggestions_dropdown(
|
render_suggestions_dropdown(
|
||||||
f,
|
f,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use crossterm::{
|
|||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
cursor::{SetCursorStyle, EnableBlinking, Show, Hide, MoveTo},
|
cursor::{SetCursorStyle, EnableBlinking, Show, Hide, MoveTo},
|
||||||
};
|
};
|
||||||
|
use crossterm::ExecutableCommand;
|
||||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||||
use std::io::{self, stdout, Write};
|
use std::io::{self, stdout, Write};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -81,6 +82,12 @@ impl TerminalCore {
|
|||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to a specific (x, y) position on screen.
|
||||||
|
pub fn set_cursor_position(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||||
|
self.terminal.backend_mut().execute(MoveTo(x, y))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for TerminalCore {
|
impl Drop for TerminalCore {
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ use crate::ui::handlers::context::DialogPurpose;
|
|||||||
use crate::utils::columns::filter_user_columns;
|
use crate::utils::columns::filter_user_columns;
|
||||||
use canvas::keymap::KeyEventOutcome;
|
use canvas::keymap::KeyEventOutcome;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use crossterm::cursor::SetCursorStyle;
|
use crossterm::cursor::{SetCursorStyle, MoveTo};
|
||||||
use crossterm::event as crossterm_event;
|
use crossterm::event as crossterm_event;
|
||||||
|
use crossterm::ExecutableCommand;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -641,53 +642,63 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
|
|
||||||
if event_processed || needs_redraw || position_changed {
|
if event_processed || needs_redraw || position_changed {
|
||||||
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &router);
|
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &router);
|
||||||
|
|
||||||
match current_mode {
|
match current_mode {
|
||||||
AppMode::Edit => { terminal.show_cursor()?; }
|
|
||||||
AppMode::Highlight => { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; terminal.show_cursor()?; }
|
|
||||||
AppMode::ReadOnly => {
|
|
||||||
if !app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?; }
|
|
||||||
else { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; }
|
|
||||||
terminal.show_cursor().context("Failed to show cursor in ReadOnly mode")?;
|
|
||||||
}
|
|
||||||
AppMode::General => {
|
AppMode::General => {
|
||||||
if app_state.ui.focus_outside_canvas { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor()?; }
|
if app_state.ui.focus_outside_canvas {
|
||||||
else { terminal.hide_cursor()?; }
|
// General mode, focus outside canvas but canvas exists
|
||||||
|
if let Some(editor) = &app_state.form_editor {
|
||||||
|
// Get last known cursor position from canvas
|
||||||
|
let x = editor.cursor_position() as u16;
|
||||||
|
let y = editor.current_field() as u16;
|
||||||
|
|
||||||
|
// Force underscore cursor at that position
|
||||||
|
terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?;
|
||||||
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
|
// Move cursor to last known canvas position
|
||||||
|
terminal.set_cursor_position(x, y)?;
|
||||||
|
} else {
|
||||||
|
// No canvas at all → hide cursor
|
||||||
|
terminal.hide_cursor()?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// General mode, focus inside canvas → let canvas handle cursor
|
||||||
|
// Do nothing here
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AppMode::Command => { terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?; terminal.show_cursor().context("Failed to show cursor in Command mode")?; }
|
AppMode::Command => {
|
||||||
|
// Command line overlay → always steady block
|
||||||
|
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
|
||||||
|
terminal
|
||||||
|
.show_cursor()
|
||||||
|
.context("Failed to show cursor in Command mode")?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily work around borrow checker by extracting needed values
|
// Workaround for borrow checker
|
||||||
let current_dir = app_state.current_dir.clone();
|
let current_dir = app_state.current_dir.clone();
|
||||||
|
|
||||||
// Since we can't borrow app_state both mutably and immutably,
|
|
||||||
// we'll need to either:
|
|
||||||
// 1. Modify render_ui to take just app_state and access form_state internally, OR
|
|
||||||
// 2. Extract the specific fields render_ui needs from app_state
|
|
||||||
|
|
||||||
// For now, using approach where we temporarily clone what we need
|
|
||||||
let form_state_clone = app_state.form_state().unwrap().clone();
|
let form_state_clone = app_state.form_state().unwrap().clone();
|
||||||
|
|
||||||
terminal.draw(|f| {
|
terminal
|
||||||
// Use a mutable clone for rendering
|
.draw(|f| {
|
||||||
let mut temp_form_state = form_state_clone.clone();
|
let mut temp_form_state = form_state_clone.clone();
|
||||||
render_ui(
|
render_ui(
|
||||||
f,
|
f,
|
||||||
&mut router,
|
&mut router,
|
||||||
&buffer_state,
|
&buffer_state,
|
||||||
&theme,
|
&theme,
|
||||||
event_handler.is_edit_mode,
|
event_handler.is_edit_mode,
|
||||||
&event_handler.command_input,
|
&event_handler.command_input,
|
||||||
event_handler.command_mode,
|
event_handler.command_mode,
|
||||||
&event_handler.command_message,
|
&event_handler.command_message,
|
||||||
&event_handler.navigation_state,
|
&event_handler.navigation_state,
|
||||||
¤t_dir,
|
¤t_dir,
|
||||||
current_fps,
|
current_fps,
|
||||||
&app_state,
|
&app_state,
|
||||||
);
|
);
|
||||||
|
})
|
||||||
// If render_ui modified the form_state, we'd need to sync it back
|
.context("Terminal draw call failed")?;
|
||||||
// But typically render functions don't modify state, just read it
|
|
||||||
}).context("Terminal draw call failed")?;
|
|
||||||
needs_redraw = false;
|
needs_redraw = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user