auth not working with canvas crate yet
This commit is contained in:
@@ -5,7 +5,6 @@ use crate::{
|
|||||||
state::pages::auth::LoginState,
|
state::pages::auth::LoginState,
|
||||||
components::common::dialog,
|
components::common::dialog,
|
||||||
state::app::state::AppState,
|
state::app::state::AppState,
|
||||||
components::handlers::canvas_bridge::render_canvas_form, // Use our bridge function
|
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect, Margin},
|
layout::{Alignment, Constraint, Direction, Layout, Rect, Margin},
|
||||||
@@ -14,6 +13,16 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::state::app::highlight::HighlightState;
|
use crate::state::app::highlight::HighlightState;
|
||||||
|
use canvas::canvas::{render_canvas, HighlightState as CanvasHighlightState}; // Use canvas library's render function
|
||||||
|
|
||||||
|
// Helper function to convert between HighlightState types
|
||||||
|
fn convert_highlight_state(local: &HighlightState) -> CanvasHighlightState {
|
||||||
|
match local {
|
||||||
|
HighlightState::Off => CanvasHighlightState::Off,
|
||||||
|
HighlightState::Characterwise { anchor } => CanvasHighlightState::Characterwise { anchor: *anchor },
|
||||||
|
HighlightState::Linewise { anchor_line } => CanvasHighlightState::Linewise { anchor_line: *anchor_line },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_login(
|
pub fn render_login(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
@@ -49,14 +58,15 @@ pub fn render_login(
|
|||||||
])
|
])
|
||||||
.split(inner_area);
|
.split(inner_area);
|
||||||
|
|
||||||
// --- FORM RENDERING (Using bridge function) ---
|
// --- FORM RENDERING (Using canvas library directly) ---
|
||||||
render_canvas_form(
|
let canvas_highlight_state = convert_highlight_state(highlight_state);
|
||||||
|
render_canvas(
|
||||||
f,
|
f,
|
||||||
chunks[0],
|
chunks[0],
|
||||||
login_state, // LoginState implements CanvasState
|
login_state, // LoginState implements CanvasState
|
||||||
theme,
|
theme, // Theme implements CanvasTheme
|
||||||
is_edit_mode,
|
is_edit_mode,
|
||||||
highlight_state,
|
&canvas_highlight_state,
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- ERROR MESSAGE ---
|
// --- ERROR MESSAGE ---
|
||||||
|
|||||||
@@ -6,15 +6,25 @@ use crate::{
|
|||||||
components::common::dialog,
|
components::common::dialog,
|
||||||
state::app::state::AppState,
|
state::app::state::AppState,
|
||||||
modes::handlers::mode_manager::AppMode,
|
modes::handlers::mode_manager::AppMode,
|
||||||
components::handlers::canvas_bridge::render_canvas_form, // Use our bridge function
|
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect, Margin},
|
layout::{Alignment, Constraint, Direction, Layout, Rect, Margin},
|
||||||
style::{Style, Modifier, Color},
|
style::{Style, Modifier, Color},
|
||||||
widgets::{Block, BorderType, Borders, Paragraph, List, ListItem, ListState},
|
widgets::{Block, BorderType, Borders, Paragraph},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::state::app::highlight::HighlightState;
|
use crate::state::app::highlight::HighlightState;
|
||||||
|
use canvas::canvas::{render_canvas, HighlightState as CanvasHighlightState}; // Use canvas library's render function
|
||||||
|
use canvas::autocomplete::gui::render_autocomplete_dropdown; // Use canvas library's autocomplete dropdown
|
||||||
|
|
||||||
|
// Helper function to convert between HighlightState types
|
||||||
|
fn convert_highlight_state(local: &HighlightState) -> CanvasHighlightState {
|
||||||
|
match local {
|
||||||
|
HighlightState::Off => CanvasHighlightState::Off,
|
||||||
|
HighlightState::Characterwise { anchor } => CanvasHighlightState::Characterwise { anchor: *anchor },
|
||||||
|
HighlightState::Linewise { anchor_line } => CanvasHighlightState::Linewise { anchor_line: *anchor_line },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_register(
|
pub fn render_register(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
@@ -49,14 +59,15 @@ pub fn render_register(
|
|||||||
])
|
])
|
||||||
.split(inner_area);
|
.split(inner_area);
|
||||||
|
|
||||||
// --- FORM RENDERING (Using bridge function) ---
|
// --- FORM RENDERING (Using canvas library directly) ---
|
||||||
let input_rect = render_canvas_form(
|
let canvas_highlight_state = convert_highlight_state(highlight_state);
|
||||||
|
let input_rect = render_canvas(
|
||||||
f,
|
f,
|
||||||
chunks[0],
|
chunks[0],
|
||||||
state, // RegisterState implements CanvasState
|
state, // RegisterState implements CanvasState
|
||||||
theme,
|
theme, // Theme implements CanvasTheme
|
||||||
is_edit_mode,
|
is_edit_mode,
|
||||||
highlight_state,
|
&canvas_highlight_state,
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- HELP TEXT ---
|
// --- HELP TEXT ---
|
||||||
@@ -135,13 +146,17 @@ pub fn render_register(
|
|||||||
button_chunks[1],
|
button_chunks[1],
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- AUTOCOMPLETE DROPDOWN (Simple bridge implementation) ---
|
// --- AUTOCOMPLETE DROPDOWN (Using canvas library directly) ---
|
||||||
if app_state.current_mode == AppMode::Edit {
|
if app_state.current_mode == AppMode::Edit {
|
||||||
if let Some(autocomplete_state) = state.autocomplete_state() {
|
if let Some(autocomplete_state) = state.autocomplete_state() {
|
||||||
if autocomplete_state.is_active && !autocomplete_state.suggestions.is_empty() {
|
if let Some(input_rect) = input_rect {
|
||||||
if let Some(field_rect) = input_rect {
|
render_autocomplete_dropdown(
|
||||||
render_simple_autocomplete_dropdown(f, field_rect, f.area(), theme, autocomplete_state);
|
f,
|
||||||
}
|
f.area(), // Frame area
|
||||||
|
input_rect, // Current input field rect
|
||||||
|
theme, // Theme implements CanvasTheme
|
||||||
|
autocomplete_state,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,89 +175,3 @@ pub fn render_register(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simple autocomplete dropdown renderer (bridge implementation)
|
|
||||||
fn render_simple_autocomplete_dropdown(
|
|
||||||
f: &mut Frame,
|
|
||||||
input_rect: Rect,
|
|
||||||
frame_area: Rect,
|
|
||||||
theme: &Theme,
|
|
||||||
autocomplete_state: &canvas::AutocompleteState<String>,
|
|
||||||
) {
|
|
||||||
if autocomplete_state.is_loading {
|
|
||||||
// Show loading indicator
|
|
||||||
let loading_area = Rect {
|
|
||||||
x: input_rect.x,
|
|
||||||
y: input_rect.y + 1,
|
|
||||||
width: input_rect.width,
|
|
||||||
height: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
let loading_paragraph = Paragraph::new("Loading suggestions...")
|
|
||||||
.style(Style::default().fg(theme.fg))
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(ratatui::widgets::Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(theme.accent))
|
|
||||||
.style(Style::default().bg(theme.bg)),
|
|
||||||
);
|
|
||||||
|
|
||||||
f.render_widget(loading_paragraph, loading_area);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if autocomplete_state.suggestions.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate dropdown position
|
|
||||||
let dropdown_height = (autocomplete_state.suggestions.len() as u16).min(8) + 2;
|
|
||||||
let dropdown_width = input_rect.width.max(20);
|
|
||||||
|
|
||||||
let mut dropdown_area = Rect {
|
|
||||||
x: input_rect.x,
|
|
||||||
y: input_rect.y + 1,
|
|
||||||
width: dropdown_width,
|
|
||||||
height: dropdown_height,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Keep dropdown within bounds
|
|
||||||
if dropdown_area.bottom() > frame_area.height {
|
|
||||||
dropdown_area.y = input_rect.y.saturating_sub(dropdown_height);
|
|
||||||
}
|
|
||||||
if dropdown_area.right() > frame_area.width {
|
|
||||||
dropdown_area.x = frame_area.width.saturating_sub(dropdown_width);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create list items
|
|
||||||
let items: Vec<ListItem> = autocomplete_state
|
|
||||||
.suggestions
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, suggestion)| {
|
|
||||||
let is_selected = autocomplete_state.selected_index == Some(i);
|
|
||||||
let style = if is_selected {
|
|
||||||
Style::default()
|
|
||||||
.fg(theme.bg)
|
|
||||||
.bg(theme.highlight)
|
|
||||||
.add_modifier(Modifier::BOLD)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(theme.fg).bg(theme.bg)
|
|
||||||
};
|
|
||||||
|
|
||||||
ListItem::new(suggestion.display_text.as_str()).style(style)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let list = List::new(items).block(
|
|
||||||
Block::default()
|
|
||||||
.borders(ratatui::widgets::Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(theme.accent))
|
|
||||||
.style(Style::default().bg(theme.bg)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut list_state = ListState::default();
|
|
||||||
list_state.select(autocomplete_state.selected_index);
|
|
||||||
|
|
||||||
f.render_stateful_widget(list, dropdown_area, &mut list_state);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/state/pages/auth.rs
|
// src/state/pages/auth.rs
|
||||||
use canvas::{CanvasState, ActionContext, CanvasAction}; // Import from external library
|
use canvas::canvas::{CanvasState, ActionContext, CanvasAction};
|
||||||
use canvas::{AutocompleteCanvasState, AutocompleteState, SuggestionItem}; // For autocomplete
|
use canvas::autocomplete::{AutocompleteCanvasState, AutocompleteState, SuggestionItem};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
@@ -45,7 +45,6 @@ pub struct RegisterState {
|
|||||||
pub current_field: usize,
|
pub current_field: usize,
|
||||||
pub current_cursor_pos: usize,
|
pub current_cursor_pos: usize,
|
||||||
pub has_unsaved_changes: bool,
|
pub has_unsaved_changes: bool,
|
||||||
|
|
||||||
// NEW: Replace old autocomplete with external library's system
|
// NEW: Replace old autocomplete with external library's system
|
||||||
pub autocomplete: AutocompleteState<String>,
|
pub autocomplete: AutocompleteState<String>,
|
||||||
}
|
}
|
||||||
@@ -123,7 +122,6 @@ impl CanvasState for LoginState {
|
|||||||
self.has_unsaved_changes = changed;
|
self.has_unsaved_changes = changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle custom actions (like submit)
|
|
||||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||||
match action {
|
match action {
|
||||||
CanvasAction::Custom(action_str) if action_str == "submit" => {
|
CanvasAction::Custom(action_str) if action_str == "submit" => {
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ use crate::components::{
|
|||||||
};
|
};
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::modes::general::command_navigation::NavigationState;
|
use crate::modes::general::command_navigation::NavigationState;
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
use crate::state::pages::canvas_state::CanvasState as LocalCanvasState; // Keep local one with alias
|
||||||
|
use canvas::canvas::CanvasState; // Import external library's CanvasState trait
|
||||||
use crate::state::app::buffer::BufferState;
|
use crate::state::app::buffer::BufferState;
|
||||||
use crate::state::app::highlight::HighlightState as LocalHighlightState; // CHANGED: Alias local version
|
use crate::state::app::highlight::HighlightState as LocalHighlightState; // CHANGED: Alias local version
|
||||||
use canvas::canvas::HighlightState as CanvasHighlightState; // CHANGED: Import canvas version with alias
|
use canvas::canvas::HighlightState as CanvasHighlightState; // CHANGED: Import canvas version with alias
|
||||||
@@ -136,7 +137,7 @@ pub fn render_ui(
|
|||||||
theme,
|
theme,
|
||||||
register_state,
|
register_state,
|
||||||
app_state,
|
app_state,
|
||||||
register_state.current_field() < 4,
|
register_state.current_field() < 4, // Now using CanvasState trait method
|
||||||
highlight_state, // Uses local version
|
highlight_state, // Uses local version
|
||||||
);
|
);
|
||||||
} else if app_state.ui.show_add_table {
|
} else if app_state.ui.show_add_table {
|
||||||
@@ -166,7 +167,7 @@ pub fn render_ui(
|
|||||||
theme,
|
theme,
|
||||||
login_state,
|
login_state,
|
||||||
app_state,
|
app_state,
|
||||||
login_state.current_field() < 2,
|
login_state.current_field() < 2, // Now using CanvasState trait method
|
||||||
highlight_state, // Uses local version
|
highlight_state, // Uses local version
|
||||||
);
|
);
|
||||||
} else if app_state.ui.show_admin {
|
} else if app_state.ui.show_admin {
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ use crate::config::storage::storage::load_auth_data;
|
|||||||
use crate::modes::common::commands::CommandHandler;
|
use crate::modes::common::commands::CommandHandler;
|
||||||
use crate::modes::handlers::event::{EventHandler, EventOutcome};
|
use crate::modes::handlers::event::{EventHandler, EventOutcome};
|
||||||
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
|
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
|
||||||
use crate::state::pages::canvas_state::CanvasState;
|
use crate::state::pages::canvas_state::CanvasState as LocalCanvasState; // Keep local one with alias
|
||||||
|
use canvas::canvas::CanvasState; // Import external library's CanvasState trait
|
||||||
use crate::state::pages::form::{FormState, FieldDefinition}; // Import FieldDefinition
|
use crate::state::pages::form::{FormState, FieldDefinition}; // Import FieldDefinition
|
||||||
use crate::state::pages::auth::AuthState;
|
use crate::state::pages::auth::AuthState;
|
||||||
use crate::state::pages::auth::LoginState;
|
use crate::state::pages::auth::LoginState;
|
||||||
@@ -38,6 +39,7 @@ use crate::state::app::state::DebugState;
|
|||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
use crate::utils::debug_logger::pop_next_debug_message;
|
use crate::utils::debug_logger::pop_next_debug_message;
|
||||||
|
|
||||||
|
// Rest of the file remains the same...
|
||||||
pub async fn run_ui() -> Result<()> {
|
pub async fn run_ui() -> Result<()> {
|
||||||
let config = Config::load().context("Failed to load configuration")?;
|
let config = Config::load().context("Failed to load configuration")?;
|
||||||
let theme = Theme::from_str(&config.colors.theme);
|
let theme = Theme::from_str(&config.colors.theme);
|
||||||
@@ -346,25 +348,25 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Continue with the rest of the function...
|
||||||
|
// (The rest remains the same, but now CanvasState trait methods are available)
|
||||||
|
|
||||||
if app_state.ui.show_form {
|
if app_state.ui.show_form {
|
||||||
let current_view_profile = app_state.current_view_profile_name.clone();
|
let current_view_profile = app_state.current_view_profile_name.clone();
|
||||||
let current_view_table = app_state.current_view_table_name.clone();
|
let current_view_table = app_state.current_view_table_name.clone();
|
||||||
|
|
||||||
// This condition correctly detects a table switch.
|
|
||||||
if prev_view_profile_name != current_view_profile
|
if prev_view_profile_name != current_view_profile
|
||||||
|| prev_view_table_name != current_view_table
|
|| prev_view_table_name != current_view_table
|
||||||
{
|
{
|
||||||
if let (Some(prof_name), Some(tbl_name)) =
|
if let (Some(prof_name), Some(tbl_name)) =
|
||||||
(current_view_profile.as_ref(), current_view_table.as_ref())
|
(current_view_profile.as_ref(), current_view_table.as_ref())
|
||||||
{
|
{
|
||||||
// --- START OF REFACTORED LOGIC ---
|
|
||||||
app_state.show_loading_dialog(
|
app_state.show_loading_dialog(
|
||||||
"Loading Table",
|
"Loading Table",
|
||||||
&format!("Fetching data for {}.{}...", prof_name, tbl_name),
|
&format!("Fetching data for {}.{}...", prof_name, tbl_name),
|
||||||
);
|
);
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
|
|
||||||
// 1. Call our new, central function. It handles fetching AND caching.
|
|
||||||
match UiService::load_table_view(
|
match UiService::load_table_view(
|
||||||
&mut grpc_client,
|
&mut grpc_client,
|
||||||
&mut app_state,
|
&mut app_state,
|
||||||
@@ -374,72 +376,62 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(mut new_form_state) => {
|
Ok(mut new_form_state) => {
|
||||||
// 2. The function succeeded, we have a new FormState.
|
|
||||||
// Now, fetch its data.
|
|
||||||
if let Err(e) = UiService::fetch_and_set_table_count(
|
if let Err(e) = UiService::fetch_and_set_table_count(
|
||||||
&mut grpc_client,
|
&mut grpc_client,
|
||||||
&mut new_form_state,
|
&mut new_form_state,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
// Handle count fetching error
|
|
||||||
app_state.update_dialog_content(
|
app_state.update_dialog_content(
|
||||||
&format!("Error fetching count: {}", e),
|
&format!("Error fetching count: {}", e),
|
||||||
vec!["OK".to_string()],
|
vec!["OK".to_string()],
|
||||||
DialogPurpose::LoginFailed, // Or a more appropriate purpose
|
DialogPurpose::LoginFailed,
|
||||||
);
|
);
|
||||||
} else if new_form_state.total_count > 0 {
|
} else if new_form_state.total_count > 0 {
|
||||||
// If there are records, load the first/last one
|
|
||||||
if let Err(e) = UiService::load_table_data_by_position(
|
if let Err(e) = UiService::load_table_data_by_position(
|
||||||
&mut grpc_client,
|
&mut grpc_client,
|
||||||
&mut new_form_state,
|
&mut new_form_state,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
// Handle data loading error
|
|
||||||
app_state.update_dialog_content(
|
app_state.update_dialog_content(
|
||||||
&format!("Error loading data: {}", e),
|
&format!("Error loading data: {}", e),
|
||||||
vec!["OK".to_string()],
|
vec!["OK".to_string()],
|
||||||
DialogPurpose::LoginFailed, // Or a more appropriate purpose
|
DialogPurpose::LoginFailed,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Success! Hide the loading dialog.
|
|
||||||
app_state.hide_dialog();
|
app_state.hide_dialog();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No records, so just reset to an empty form.
|
|
||||||
new_form_state.reset_to_empty();
|
new_form_state.reset_to_empty();
|
||||||
app_state.hide_dialog();
|
app_state.hide_dialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. CRITICAL: Replace the old form_state with the new one.
|
|
||||||
form_state = new_form_state;
|
form_state = new_form_state;
|
||||||
|
|
||||||
// 4. Update our tracking variables.
|
|
||||||
prev_view_profile_name = current_view_profile;
|
prev_view_profile_name = current_view_profile;
|
||||||
prev_view_table_name = current_view_table;
|
prev_view_table_name = current_view_table;
|
||||||
table_just_switched = true;
|
table_just_switched = true;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// This handles errors from load_table_view (e.g., schema fetch failed)
|
|
||||||
app_state.update_dialog_content(
|
app_state.update_dialog_content(
|
||||||
&format!("Error loading table: {}", e),
|
&format!("Error loading table: {}", e),
|
||||||
vec!["OK".to_string()],
|
vec!["OK".to_string()],
|
||||||
DialogPurpose::LoginFailed, // Or a more appropriate purpose
|
DialogPurpose::LoginFailed,
|
||||||
);
|
);
|
||||||
// Revert the view change in app_state to avoid a loop
|
|
||||||
app_state.current_view_profile_name =
|
app_state.current_view_profile_name =
|
||||||
prev_view_profile_name.clone();
|
prev_view_profile_name.clone();
|
||||||
app_state.current_view_table_name =
|
app_state.current_view_table_name =
|
||||||
prev_view_table_name.clone();
|
prev_view_table_name.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- END OF REFACTORED LOGIC ---
|
|
||||||
}
|
}
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Continue with the rest of the positioning logic...
|
||||||
|
// Now we can use CanvasState methods like get_current_input(), current_field(), etc.
|
||||||
|
|
||||||
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
|
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
|
||||||
if app_state.ui.show_add_logic {
|
if app_state.ui.show_add_logic {
|
||||||
if admin_state.add_logic_state.profile_name == profile_name &&
|
if admin_state.add_logic_state.profile_name == profile_name &&
|
||||||
|
|||||||
Reference in New Issue
Block a user