fixes
This commit is contained in:
@@ -142,7 +142,7 @@ impl ClientApiConnection {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn server_attach(&mut self) -> Result<bool> {
|
||||
pub async fn server_attach(&mut self) -> Result<()> {
|
||||
trace!("ClientApiConnection::server_attach");
|
||||
let server = {
|
||||
let inner = self.inner.borrow();
|
||||
@@ -154,10 +154,10 @@ impl ClientApiConnection {
|
||||
};
|
||||
let request = server.borrow().attach_request();
|
||||
let response = request.send().promise.await?;
|
||||
Ok(response.get()?.get_result())
|
||||
response.get().map(drop).map_err(|e| anyhow!(e))
|
||||
}
|
||||
|
||||
pub async fn server_detach(&mut self) -> Result<bool> {
|
||||
pub async fn server_detach(&mut self) -> Result<()> {
|
||||
trace!("ClientApiConnection::server_detach");
|
||||
let server = {
|
||||
let inner = self.inner.borrow();
|
||||
@@ -169,10 +169,10 @@ impl ClientApiConnection {
|
||||
};
|
||||
let request = server.borrow().detach_request();
|
||||
let response = request.send().promise.await?;
|
||||
Ok(response.get()?.get_result())
|
||||
response.get().map(drop).map_err(|e| anyhow!(e))
|
||||
}
|
||||
|
||||
pub async fn server_shutdown(&mut self) -> Result<bool> {
|
||||
pub async fn server_shutdown(&mut self) -> Result<()> {
|
||||
trace!("ClientApiConnection::server_shutdown");
|
||||
let server = {
|
||||
let inner = self.inner.borrow();
|
||||
@@ -184,7 +184,27 @@ impl ClientApiConnection {
|
||||
};
|
||||
let request = server.borrow().shutdown_request();
|
||||
let response = request.send().promise.await?;
|
||||
Ok(response.get()?.get_result())
|
||||
response.get().map(drop).map_err(|e| anyhow!(e))
|
||||
}
|
||||
|
||||
pub async fn server_debug(&mut self, what: String) -> Result<String> {
|
||||
trace!("ClientApiConnection::server_debug");
|
||||
let server = {
|
||||
let inner = self.inner.borrow();
|
||||
inner
|
||||
.server
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("Not connected, ignoring attach request"))?
|
||||
.clone()
|
||||
};
|
||||
let mut request = server.borrow().debug_request();
|
||||
request.get().set_what(&what);
|
||||
let response = request.send().promise.await?;
|
||||
response
|
||||
.get()?
|
||||
.get_output()
|
||||
.map(|o| o.to_owned())
|
||||
.map_err(|e| anyhow!(e))
|
||||
}
|
||||
|
||||
// Start Client API connection
|
||||
|
@@ -97,19 +97,19 @@ disconnect - disconnect the client from the Veilid node
|
||||
shutdown - shut the server down
|
||||
attach - attach the server to the Veilid network
|
||||
detach - detach the server from the Veilid network
|
||||
debug - send a debugging command to the Veilid server
|
||||
"#,
|
||||
);
|
||||
let ui = self.ui();
|
||||
callback(ui);
|
||||
ui.send_callback(callback);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cmd_exit(&self, callback: UICallback) -> Result<(), String> {
|
||||
trace!("CommandProcessor::cmd_exit");
|
||||
let ui = self.ui();
|
||||
callback(ui);
|
||||
//
|
||||
self.ui().quit();
|
||||
ui.send_callback(callback);
|
||||
ui.quit();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ detach - detach the server from the Veilid network
|
||||
if let Err(e) = capi.server_shutdown().await {
|
||||
error!("Server command 'shutdown' failed to execute: {}", e);
|
||||
}
|
||||
callback(ui);
|
||||
ui.send_callback(callback);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -134,7 +134,7 @@ detach - detach the server from the Veilid network
|
||||
if let Err(e) = capi.server_attach().await {
|
||||
error!("Server command 'attach' failed to execute: {}", e);
|
||||
}
|
||||
callback(ui);
|
||||
ui.send_callback(callback);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -147,7 +147,7 @@ detach - detach the server from the Veilid network
|
||||
if let Err(e) = capi.server_detach().await {
|
||||
error!("Server command 'detach' failed to execute: {}", e);
|
||||
}
|
||||
callback(ui);
|
||||
ui.send_callback(callback);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -158,7 +158,23 @@ detach - detach the server from the Veilid network
|
||||
let ui = self.ui();
|
||||
async_std::task::spawn_local(async move {
|
||||
capi.disconnect().await;
|
||||
callback(ui);
|
||||
ui.send_callback(callback);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cmd_debug(&self, rest: Option<String>, callback: UICallback) -> Result<(), String> {
|
||||
trace!("CommandProcessor::cmd_debug");
|
||||
let mut capi = self.capi();
|
||||
let ui = self.ui();
|
||||
async_std::task::spawn_local(async move {
|
||||
match capi.server_debug(rest.unwrap_or_default()).await {
|
||||
Ok(output) => ui.display_string_dialog("Debug Output", output, callback),
|
||||
Err(e) => {
|
||||
error!("Server command 'debug' failed to execute: {}", e);
|
||||
ui.send_callback(callback);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -166,7 +182,6 @@ detach - detach the server from the Veilid network
|
||||
pub fn run_command(&self, command_line: &str, callback: UICallback) -> Result<(), String> {
|
||||
//
|
||||
let (cmd, rest) = Self::word_split(command_line);
|
||||
|
||||
match cmd.as_str() {
|
||||
"help" => self.cmd_help(rest, callback),
|
||||
"exit" => self.cmd_exit(callback),
|
||||
@@ -175,8 +190,10 @@ detach - detach the server from the Veilid network
|
||||
"shutdown" => self.cmd_shutdown(callback),
|
||||
"attach" => self.cmd_attach(callback),
|
||||
"detach" => self.cmd_detach(callback),
|
||||
"debug" => self.cmd_debug(rest, callback),
|
||||
_ => {
|
||||
callback(self.ui());
|
||||
let ui = self.ui();
|
||||
ui.send_callback(callback);
|
||||
Err(format!("Invalid command: {}", cmd))
|
||||
}
|
||||
}
|
||||
|
@@ -44,7 +44,7 @@ impl<T> Dirty<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub type UICallback = Box<dyn Fn(UI) + 'static>;
|
||||
pub type UICallback = Box<dyn Fn(&mut Cursive) + Send>;
|
||||
|
||||
struct UIState {
|
||||
attachment_state: Dirty<AttachmentState>,
|
||||
@@ -84,126 +84,8 @@ pub struct UI {
|
||||
}
|
||||
|
||||
impl UI {
|
||||
pub fn new(node_log_scrollback: usize, settings: &Settings) -> Self {
|
||||
cursive_flexi_logger_view::resize(node_log_scrollback);
|
||||
|
||||
// Instantiate the cursive runnable
|
||||
/*
|
||||
// reduces flicker, but it costs cpu
|
||||
let mut runnable = CursiveRunnable::new(
|
||||
|| -> Result<Box<dyn cursive_buffered_backend::Backend>, UIError> {
|
||||
let crossterm_backend = cursive::backends::crossterm::Backend::init().unwrap();
|
||||
let buffered_backend =
|
||||
cursive_buffered_backend::BufferedBackend::new(crossterm_backend);
|
||||
|
||||
Ok(Box::new(buffered_backend))
|
||||
},
|
||||
);
|
||||
*/
|
||||
let runnable = cursive::default();
|
||||
// Make the callback mechanism easily reachable
|
||||
let cb_sink = runnable.cb_sink().clone();
|
||||
|
||||
// Create the UI object
|
||||
let this = Self {
|
||||
siv: Rc::new(RefCell::new(runnable)),
|
||||
inner: Rc::new(RefCell::new(UIInner {
|
||||
ui_state: UIState::new(),
|
||||
log_colors: Default::default(),
|
||||
cmdproc: None,
|
||||
cmd_history: {
|
||||
let mut vd = VecDeque::new();
|
||||
vd.push_back("".to_string());
|
||||
vd
|
||||
},
|
||||
cmd_history_position: 0,
|
||||
cmd_history_max_size: settings.interface.command_line.history_size,
|
||||
connection_dialog_state: None,
|
||||
cb_sink,
|
||||
})),
|
||||
};
|
||||
|
||||
let mut siv = this.siv.borrow_mut();
|
||||
let mut inner = this.inner.borrow_mut();
|
||||
|
||||
// Make the inner object accessible in callbacks easily
|
||||
siv.set_user_data(this.inner.clone());
|
||||
|
||||
// Create layouts
|
||||
let mut mainlayout = LinearLayout::vertical().with_name("main-layout");
|
||||
mainlayout.get_mut().add_child(
|
||||
Panel::new(
|
||||
FlexiLoggerView::new_scrollable()
|
||||
.with_name("node-events")
|
||||
.full_screen(),
|
||||
)
|
||||
.title_position(HAlign::Left)
|
||||
.title("Node Events"),
|
||||
);
|
||||
mainlayout.get_mut().add_child(
|
||||
Panel::new(ScrollView::new(
|
||||
TextView::new("Peer Table")
|
||||
.with_name("peers")
|
||||
.fixed_height(8)
|
||||
.scrollable(),
|
||||
))
|
||||
.title_position(HAlign::Left)
|
||||
.title("Peers"),
|
||||
);
|
||||
let mut command = StyledString::new();
|
||||
command.append_styled("Command> ", ColorStyle::title_primary());
|
||||
//
|
||||
mainlayout.get_mut().add_child(
|
||||
LinearLayout::horizontal()
|
||||
.child(TextView::new(command))
|
||||
.child(
|
||||
EditView::new()
|
||||
.on_submit(UI::on_command_line_entered)
|
||||
.on_edit(UI::on_command_line_edit)
|
||||
.on_up_down(UI::on_command_line_history)
|
||||
.style(ColorStyle::new(
|
||||
PaletteColor::Background,
|
||||
PaletteColor::Secondary,
|
||||
))
|
||||
.with_name("command-line")
|
||||
.full_screen()
|
||||
.fixed_height(1),
|
||||
)
|
||||
.child(
|
||||
Button::new("Attach", |s| {
|
||||
UI::on_button_attach_pressed(s);
|
||||
})
|
||||
.with_name("button-attach"),
|
||||
),
|
||||
);
|
||||
let mut version = StyledString::new();
|
||||
version.append_styled(
|
||||
concat!(" | veilid-cli v", env!("CARGO_PKG_VERSION")),
|
||||
ColorStyle::highlight_inactive(),
|
||||
);
|
||||
|
||||
mainlayout.get_mut().add_child(
|
||||
LinearLayout::horizontal()
|
||||
.color(Some(ColorStyle::highlight_inactive()))
|
||||
.child(
|
||||
TextView::new("")
|
||||
.with_name("status-bar")
|
||||
.full_screen()
|
||||
.fixed_height(1),
|
||||
)
|
||||
.child(TextView::new(version)),
|
||||
);
|
||||
|
||||
siv.add_fullscreen_layer(mainlayout);
|
||||
|
||||
UI::setup_colors(&mut siv, &mut inner, settings);
|
||||
UI::setup_quit_handler(&mut siv);
|
||||
|
||||
drop(inner);
|
||||
drop(siv);
|
||||
|
||||
this
|
||||
}
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private functions
|
||||
|
||||
fn command_processor(s: &mut Cursive) -> CommandProcessor {
|
||||
let inner = Self::inner(s);
|
||||
@@ -317,53 +199,6 @@ impl UI {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn cursive_flexi_logger(&mut self) -> Box<CursiveLogWriter> {
|
||||
let mut flv =
|
||||
cursive_flexi_logger_view::cursive_flexi_logger(self.siv.borrow().cb_sink().clone());
|
||||
flv.set_colors(self.inner.borrow().log_colors.clone());
|
||||
flv
|
||||
}
|
||||
pub fn set_command_processor(&mut self, cmdproc: CommandProcessor) {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
inner.cmdproc = Some(cmdproc);
|
||||
let _ = inner.cb_sink.send(Box::new(UI::update_cb));
|
||||
}
|
||||
pub fn set_attachment_state(&mut self, state: AttachmentState) {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
inner.ui_state.attachment_state.set(state);
|
||||
let _ = inner.cb_sink.send(Box::new(UI::update_cb));
|
||||
}
|
||||
pub fn set_connection_state(&mut self, state: ConnectionState) {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
inner.ui_state.connection_state.set(state);
|
||||
let _ = inner.cb_sink.send(Box::new(UI::update_cb));
|
||||
}
|
||||
pub fn add_node_event(&mut self, event: &str) {
|
||||
let inner = self.inner.borrow_mut();
|
||||
let color = *inner.log_colors.get(&Level::Info).unwrap();
|
||||
for line in event.lines() {
|
||||
cursive_flexi_logger_view::push_to_log(StyledString::styled(line, color));
|
||||
}
|
||||
let _ = inner.cb_sink.send(Box::new(UI::update_cb));
|
||||
}
|
||||
pub fn quit(&mut self) {
|
||||
let inner = self.inner.borrow_mut();
|
||||
let _ = inner.cb_sink.send(Box::new(|s| {
|
||||
s.quit();
|
||||
}));
|
||||
}
|
||||
// Note: Cursive is not re-entrant, can't borrow_mut self.siv again after this
|
||||
pub async fn run_async(&mut self) {
|
||||
let mut siv = self.siv.borrow_mut();
|
||||
siv.run_async().await;
|
||||
}
|
||||
// pub fn run(&mut self) {
|
||||
// let mut siv = self.siv.borrow_mut();
|
||||
// siv.run();
|
||||
// }
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private functions
|
||||
// fn main_layout(s: &mut Cursive) -> ViewRef<LinearLayout> {
|
||||
// s.find_name("main-layout").unwrap()
|
||||
// }
|
||||
@@ -426,11 +261,30 @@ impl UI {
|
||||
inner.cmd_history[hlen - 1] = text.to_owned();
|
||||
}
|
||||
|
||||
pub fn enable_command_ui(s: &mut Cursive, enabled: bool) {
|
||||
fn enable_command_ui(s: &mut Cursive, enabled: bool) {
|
||||
Self::command_line(s).set_enabled(enabled);
|
||||
Self::button_attach(s).set_enabled(enabled);
|
||||
}
|
||||
|
||||
fn display_string_dialog_cb(
|
||||
s: &mut Cursive,
|
||||
title: String,
|
||||
contents: String,
|
||||
close_cb: UICallback,
|
||||
) {
|
||||
// Creates a dialog around some text with a single button
|
||||
s.add_layer(
|
||||
Dialog::around(TextView::new(contents))
|
||||
.title(title)
|
||||
.button("Close", move |s| {
|
||||
s.pop_layer();
|
||||
close_cb(s);
|
||||
})
|
||||
//.wrap_with(CircularFocus::new)
|
||||
//.wrap_tab(),
|
||||
);
|
||||
}
|
||||
|
||||
fn run_command(s: &mut Cursive, text: &str) -> Result<(), String> {
|
||||
// disable ui
|
||||
Self::enable_command_ui(s, false);
|
||||
@@ -438,15 +292,8 @@ impl UI {
|
||||
let cmdproc = Self::command_processor(s);
|
||||
cmdproc.run_command(
|
||||
text,
|
||||
Box::new(|ui: UI| {
|
||||
let _ = ui
|
||||
.inner
|
||||
.borrow()
|
||||
.cb_sink
|
||||
.send(Box::new(|s| {
|
||||
Self::enable_command_ui(s, true);
|
||||
}))
|
||||
.unwrap();
|
||||
Box::new(|s| {
|
||||
Self::enable_command_ui(s, true);
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -772,4 +619,193 @@ impl UI {
|
||||
Self::refresh_connection_dialog(s);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Public functions
|
||||
|
||||
pub fn new(node_log_scrollback: usize, settings: &Settings) -> Self {
|
||||
cursive_flexi_logger_view::resize(node_log_scrollback);
|
||||
|
||||
// Instantiate the cursive runnable
|
||||
/*
|
||||
// reduces flicker, but it costs cpu
|
||||
let mut runnable = CursiveRunnable::new(
|
||||
|| -> Result<Box<dyn cursive_buffered_backend::Backend>, UIError> {
|
||||
let crossterm_backend = cursive::backends::crossterm::Backend::init().unwrap();
|
||||
let buffered_backend =
|
||||
cursive_buffered_backend::BufferedBackend::new(crossterm_backend);
|
||||
|
||||
Ok(Box::new(buffered_backend))
|
||||
},
|
||||
);
|
||||
*/
|
||||
let runnable = cursive::default();
|
||||
// Make the callback mechanism easily reachable
|
||||
let cb_sink = runnable.cb_sink().clone();
|
||||
|
||||
// Create the UI object
|
||||
let this = Self {
|
||||
siv: Rc::new(RefCell::new(runnable)),
|
||||
inner: Rc::new(RefCell::new(UIInner {
|
||||
ui_state: UIState::new(),
|
||||
log_colors: Default::default(),
|
||||
cmdproc: None,
|
||||
cmd_history: {
|
||||
let mut vd = VecDeque::new();
|
||||
vd.push_back("".to_string());
|
||||
vd
|
||||
},
|
||||
cmd_history_position: 0,
|
||||
cmd_history_max_size: settings.interface.command_line.history_size,
|
||||
connection_dialog_state: None,
|
||||
cb_sink,
|
||||
})),
|
||||
};
|
||||
|
||||
let mut siv = this.siv.borrow_mut();
|
||||
let mut inner = this.inner.borrow_mut();
|
||||
|
||||
// Make the inner object accessible in callbacks easily
|
||||
siv.set_user_data(this.inner.clone());
|
||||
|
||||
// Create layouts
|
||||
let mut mainlayout = LinearLayout::vertical().with_name("main-layout");
|
||||
mainlayout.get_mut().add_child(
|
||||
Panel::new(
|
||||
FlexiLoggerView::new_scrollable()
|
||||
.with_name("node-events")
|
||||
.full_screen(),
|
||||
)
|
||||
.title_position(HAlign::Left)
|
||||
.title("Node Events"),
|
||||
);
|
||||
mainlayout.get_mut().add_child(
|
||||
Panel::new(ScrollView::new(
|
||||
TextView::new("Peer Table")
|
||||
.with_name("peers")
|
||||
.fixed_height(8)
|
||||
.scrollable(),
|
||||
))
|
||||
.title_position(HAlign::Left)
|
||||
.title("Peers"),
|
||||
);
|
||||
let mut command = StyledString::new();
|
||||
command.append_styled("Command> ", ColorStyle::title_primary());
|
||||
//
|
||||
mainlayout.get_mut().add_child(
|
||||
LinearLayout::horizontal()
|
||||
.child(TextView::new(command))
|
||||
.child(
|
||||
EditView::new()
|
||||
.on_submit(UI::on_command_line_entered)
|
||||
.on_edit(UI::on_command_line_edit)
|
||||
.on_up_down(UI::on_command_line_history)
|
||||
.style(ColorStyle::new(
|
||||
PaletteColor::Background,
|
||||
PaletteColor::Secondary,
|
||||
))
|
||||
.with_name("command-line")
|
||||
.full_screen()
|
||||
.fixed_height(1),
|
||||
)
|
||||
.child(
|
||||
Button::new("Attach", |s| {
|
||||
UI::on_button_attach_pressed(s);
|
||||
})
|
||||
.with_name("button-attach"),
|
||||
),
|
||||
);
|
||||
let mut version = StyledString::new();
|
||||
version.append_styled(
|
||||
concat!(" | veilid-cli v", env!("CARGO_PKG_VERSION")),
|
||||
ColorStyle::highlight_inactive(),
|
||||
);
|
||||
|
||||
mainlayout.get_mut().add_child(
|
||||
LinearLayout::horizontal()
|
||||
.color(Some(ColorStyle::highlight_inactive()))
|
||||
.child(
|
||||
TextView::new("")
|
||||
.with_name("status-bar")
|
||||
.full_screen()
|
||||
.fixed_height(1),
|
||||
)
|
||||
.child(TextView::new(version)),
|
||||
);
|
||||
|
||||
siv.add_fullscreen_layer(mainlayout);
|
||||
|
||||
UI::setup_colors(&mut siv, &mut inner, settings);
|
||||
UI::setup_quit_handler(&mut siv);
|
||||
|
||||
drop(inner);
|
||||
drop(siv);
|
||||
|
||||
this
|
||||
}
|
||||
pub fn cursive_flexi_logger(&self) -> Box<CursiveLogWriter> {
|
||||
let mut flv =
|
||||
cursive_flexi_logger_view::cursive_flexi_logger(self.siv.borrow().cb_sink().clone());
|
||||
flv.set_colors(self.inner.borrow().log_colors.clone());
|
||||
flv
|
||||
}
|
||||
pub fn set_command_processor(&mut self, cmdproc: CommandProcessor) {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
inner.cmdproc = Some(cmdproc);
|
||||
let _ = inner.cb_sink.send(Box::new(UI::update_cb));
|
||||
}
|
||||
pub fn set_attachment_state(&mut self, state: AttachmentState) {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
inner.ui_state.attachment_state.set(state);
|
||||
let _ = inner.cb_sink.send(Box::new(UI::update_cb));
|
||||
}
|
||||
pub fn set_connection_state(&mut self, state: ConnectionState) {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
inner.ui_state.connection_state.set(state);
|
||||
let _ = inner.cb_sink.send(Box::new(UI::update_cb));
|
||||
}
|
||||
pub fn add_node_event(&self, event: &str) {
|
||||
let inner = self.inner.borrow();
|
||||
let color = *inner.log_colors.get(&Level::Info).unwrap();
|
||||
for line in event.lines() {
|
||||
cursive_flexi_logger_view::push_to_log(StyledString::styled(line, color));
|
||||
}
|
||||
let _ = inner.cb_sink.send(Box::new(UI::update_cb));
|
||||
}
|
||||
|
||||
pub fn display_string_dialog<T: ToString, S: ToString>(
|
||||
&self,
|
||||
title: T,
|
||||
text: S,
|
||||
close_cb: UICallback,
|
||||
) {
|
||||
let title = title.to_string();
|
||||
let text = text.to_string();
|
||||
let inner = self.inner.borrow();
|
||||
let _ = inner.cb_sink.send(Box::new(move |s| {
|
||||
UI::display_string_dialog_cb(s, title, text, close_cb)
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn quit(&self) {
|
||||
let inner = self.inner.borrow();
|
||||
let _ = inner.cb_sink.send(Box::new(|s| {
|
||||
s.quit();
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn send_callback(&self, callback: UICallback) {
|
||||
let inner = self.inner.borrow();
|
||||
let _ = inner.cb_sink.send(Box::new(move |s| callback(s)));
|
||||
}
|
||||
|
||||
// Note: Cursive is not re-entrant, can't borrow_mut self.siv again after this
|
||||
pub async fn run_async(&mut self) {
|
||||
let mut siv = self.siv.borrow_mut();
|
||||
siv.run_async().await;
|
||||
}
|
||||
// pub fn run(&mut self) {
|
||||
// let mut siv = self.siv.borrow_mut();
|
||||
// siv.run();
|
||||
// }
|
||||
}
|
||||
|
Reference in New Issue
Block a user