mirror of
https://github.com/jwinterm/grim.git
synced 2026-10-24 00:28:31 +02:00
ui: wallet creation button, exit modal buttons order, back button behaviour at wallet, remember window position and size on exit, desktop icon
This commit is contained in:
+48
-2
@@ -18,11 +18,11 @@ use crate::node::NodeConfig;
|
||||
use crate::Settings;
|
||||
use crate::wallet::ConnectionsConfig;
|
||||
|
||||
/// Application configuration.
|
||||
/// Application configuration, stored at toml file.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct AppConfig {
|
||||
/// Run node server on startup.
|
||||
pub auto_start_node: bool,
|
||||
pub(crate) auto_start_node: bool,
|
||||
/// Chain type for node and wallets.
|
||||
pub(crate) chain_type: ChainTypes,
|
||||
|
||||
@@ -30,8 +30,19 @@ pub struct AppConfig {
|
||||
show_wallets_at_dual_panel: bool,
|
||||
/// Flag to show all connections at network panel or integrated node info.
|
||||
show_connections_network_panel: bool,
|
||||
|
||||
/// Width of the desktop window.
|
||||
width: f32,
|
||||
/// Height of the desktop window.
|
||||
height: f32,
|
||||
|
||||
/// Position of the desktop window.
|
||||
x: Option<f32>, y: Option<f32>
|
||||
}
|
||||
|
||||
pub const DEFAULT_WIDTH: f32 = 1200.0;
|
||||
pub const DEFAULT_HEIGHT: f32 = 720.0;
|
||||
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -39,6 +50,10 @@ impl Default for AppConfig {
|
||||
chain_type: ChainTypes::default(),
|
||||
show_wallets_at_dual_panel: false,
|
||||
show_connections_network_panel: false,
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
x: None,
|
||||
y: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,4 +139,35 @@ impl AppConfig {
|
||||
w_app_config.show_connections_network_panel = !show;
|
||||
w_app_config.save();
|
||||
}
|
||||
|
||||
/// Save desktop window width and height.
|
||||
pub fn save_window_size(w: f32, h: f32) {
|
||||
let mut w_app_config = Settings::app_config_to_update();
|
||||
w_app_config.width = w;
|
||||
w_app_config.height = h;
|
||||
w_app_config.save();
|
||||
}
|
||||
|
||||
/// Get desktop window width and height.
|
||||
pub fn window_size() -> (f32, f32) {
|
||||
let r_config = Settings::app_config_to_read();
|
||||
(r_config.width, r_config.height)
|
||||
}
|
||||
|
||||
/// Save desktop window position.
|
||||
pub fn save_window_pos(x: f32, y: f32) {
|
||||
let mut w_app_config = Settings::app_config_to_update();
|
||||
w_app_config.x = Some(x);
|
||||
w_app_config.y = Some(y);
|
||||
w_app_config.save();
|
||||
}
|
||||
|
||||
/// Get desktop window position.
|
||||
pub fn window_pos() -> Option<(f32, f32)> {
|
||||
let r_config = Settings::app_config_to_read();
|
||||
if r_config.x.is_some() && r_config.y.is_some() {
|
||||
return Some((r_config.x.unwrap(), r_config.y.unwrap()))
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use egui::{Context, Modifiers};
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::Root;
|
||||
@@ -59,6 +60,15 @@ impl<Platform: PlatformCallbacks> eframe::App for PlatformApp<Platform> {
|
||||
if !self.root.exit_allowed {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
|
||||
Root::show_exit_modal();
|
||||
} else {
|
||||
ctx.input(|i| {
|
||||
if let Some(rect) = i.viewport().inner_rect {
|
||||
AppConfig::save_window_size(rect.width(), rect.height());
|
||||
}
|
||||
if let Some(rect) = i.viewport().outer_rect {
|
||||
AppConfig::save_window_pos(rect.left(), rect.top());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, NetworkContent, View, WalletsContent};
|
||||
use crate::gui::views::types::ModalContainer;
|
||||
use crate::node::Node;
|
||||
use crate::AppConfig;
|
||||
|
||||
lazy_static! {
|
||||
/// Global state to check if [`NetworkContent`] panel is open.
|
||||
@@ -200,6 +201,11 @@ impl Root {
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::WHITE, || {
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button_ui(ui, t!("modal_exit.exit"), Colors::WHITE, |ui| {
|
||||
if !Node::is_running() {
|
||||
self.exit_allowed = true;
|
||||
@@ -212,11 +218,6 @@ impl Root {
|
||||
}
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::WHITE, || {
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
|
||||
+2
-42
@@ -18,7 +18,7 @@ use std::sync::{Arc, RwLock};
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use egui::{Align, Button, CursorIcon, Layout, PointerState, Rect, Response, RichText, Sense, Spinner, TextBuffer, TextStyle, Widget};
|
||||
use egui::epaint::{CircleShape, Color32, FontId, RectShape, Rounding, Stroke};
|
||||
use egui::epaint::{Color32, FontId, RectShape, Rounding, Stroke};
|
||||
use egui::epaint::text::TextWrapping;
|
||||
use egui::os::OperatingSystem;
|
||||
use egui::text::{LayoutJob, TextFormat};
|
||||
@@ -271,46 +271,6 @@ impl View {
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw circle [`Button`] with icon.
|
||||
pub fn circle_button(ui: &mut egui::Ui, icon: &'static str, action: impl FnOnce()) {
|
||||
ui.scope(|ui| {
|
||||
// Setup colors.
|
||||
ui.visuals_mut().widgets.inactive.bg_fill = Colors::GOLD;
|
||||
ui.visuals_mut().widgets.hovered.bg_fill = Colors::GOLD;
|
||||
ui.visuals_mut().widgets.active.bg_fill = Colors::YELLOW;
|
||||
|
||||
// Setup radius.
|
||||
let mut r = 44.0 * 0.5;
|
||||
let size = egui::Vec2::splat(2.0 * r + 5.0);
|
||||
let (rect, mut br) = ui.allocate_at_least(size, Sense::click_and_drag());
|
||||
br = br.on_hover_cursor(CursorIcon::PointingHand);
|
||||
|
||||
let mut icon_color = Colors::TEXT;
|
||||
|
||||
// Increase radius and change icon size and color on-hover.
|
||||
if br.hovered() {
|
||||
r = r * 1.04;
|
||||
icon_color = Colors::TITLE;
|
||||
}
|
||||
|
||||
let visuals = ui.style().interact(&br);
|
||||
ui.painter().add(CircleShape {
|
||||
center: rect.center(),
|
||||
radius: r,
|
||||
fill: visuals.bg_fill,
|
||||
stroke: Self::DEFAULT_STROKE
|
||||
});
|
||||
ui.allocate_ui_at_rect(rect, |ui| {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label(RichText::new(icon).color(icon_color).size(25.0));
|
||||
});
|
||||
});
|
||||
if Self::touched(ui, br) {
|
||||
(action)();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Default height of [`egui::TextEdit`] view.
|
||||
const TEXT_EDIT_HEIGHT: f32 = 37.0;
|
||||
|
||||
@@ -524,7 +484,7 @@ impl View {
|
||||
pub fn center_content(ui: &mut egui::Ui, height: f32, content: impl FnOnce(&mut egui::Ui)) {
|
||||
ui.vertical_centered(|ui| {
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
let side_margin = 24.0;
|
||||
let side_margin = 28.0;
|
||||
rect.min += egui::emath::vec2(side_margin, ui.available_height() / 2.0 - height / 2.0);
|
||||
rect.max -= egui::emath::vec2(side_margin, 0.0);
|
||||
ui.allocate_ui_at_rect(rect, |ui| {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{Align, Align2, Id, Layout, Margin, RichText, Rounding, ScrollArea, Widget};
|
||||
use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea, Widget};
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::gui::Colors;
|
||||
@@ -159,13 +159,43 @@ impl WalletsContent {
|
||||
}
|
||||
});
|
||||
|
||||
// Flag to check if wallet list is hidden on the screen.
|
||||
let list_hidden = content_width == 0.0
|
||||
|| (dual_panel && show_wallet && !self.show_wallets_at_dual_panel)
|
||||
|| (!dual_panel && show_wallet);
|
||||
|
||||
// Setup flag to show wallets bottom panel if wallet is not showing
|
||||
// at non-dual panel mode and network is no open or showing at dual panel mode.
|
||||
let show_bottom_panel =
|
||||
(!show_wallet && !dual_panel && !Root::is_network_panel_open()) ||
|
||||
(dual_panel && show_wallet);
|
||||
|
||||
// Show wallets bottom panel.
|
||||
egui::TopBottomPanel::bottom("wallets_bottom_panel")
|
||||
.frame(egui::Frame {
|
||||
fill: Colors::FILL,
|
||||
stroke: View::DEFAULT_STROKE,
|
||||
inner_margin: Margin {
|
||||
left: View::get_left_inset() + 4.0,
|
||||
right: View::far_right_inset_margin(ui) + 4.0,
|
||||
top: 4.0,
|
||||
bottom: View::get_bottom_inset() + 4.0,
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.show_animated_inside(ui, !list_hidden && show_bottom_panel, |ui| {
|
||||
// Setup vertical padding inside buttons.
|
||||
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
View::tab_button(ui, PLUS, false, || {
|
||||
self.creation_content.show_name_pass_modal(cb);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Show non-empty list if wallet is not creating.
|
||||
if !empty_list && !create_wallet {
|
||||
// Flag to check if wallet list is hidden on the screen.
|
||||
let list_hidden = content_width == 0.0
|
||||
|| (dual_panel && show_wallet && !self.show_wallets_at_dual_panel)
|
||||
|| (!dual_panel && show_wallet);
|
||||
|
||||
// Show wallet list panel.
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
@@ -192,23 +222,8 @@ impl WalletsContent {
|
||||
if list_hidden {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup flag to show wallet creation button if wallet is not showing
|
||||
// at non-dual panel mode or showing at dual panel mode.
|
||||
let show_creation_button
|
||||
= (!show_wallet && !dual_panel) || (dual_panel && show_wallet);
|
||||
|
||||
// Show list of wallets.
|
||||
let scroll =
|
||||
self.wallet_list_ui(ui, dual_panel, show_creation_button, cb);
|
||||
|
||||
if show_creation_button {
|
||||
// Setup right margin for button.
|
||||
let mut right_margin = if dual_panel { wallet_panel_width } else { 0.0 };
|
||||
if scroll { right_margin += 6.0 }
|
||||
// Show wallet creation button.
|
||||
self.create_wallet_btn_ui(ui, right_margin, cb);
|
||||
}
|
||||
self.wallet_list_ui(ui, dual_panel, cb);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -309,13 +324,11 @@ impl WalletsContent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw list of wallets. Returns `true` if scroller is showing.
|
||||
/// Draw list of wallets.
|
||||
fn wallet_list_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
dual_panel: bool,
|
||||
show_creation_btn: bool,
|
||||
cb: &dyn PlatformCallbacks) -> bool {
|
||||
let mut scroller_showing = false;
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
ui.scope(|ui| {
|
||||
// Setup scroll bar color.
|
||||
ui.style_mut().visuals.widgets.inactive.bg_fill = Colors::ITEM_HOVER;
|
||||
@@ -344,22 +357,14 @@ impl WalletsContent {
|
||||
self.wallets.select(Some(wallet.get_config().id));
|
||||
self.show_open_wallet_modal(cb);
|
||||
}
|
||||
|
||||
// Draw wallet list item.
|
||||
self.wallet_item_ui(ui, wallet, cb);
|
||||
ui.add_space(5.0);
|
||||
}
|
||||
// Add space for wallet creation button.
|
||||
if show_creation_btn {
|
||||
ui.add_space(52.0);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
// Scroller is showing if content size is larger than content on the screen.
|
||||
scroller_showing = scroll.content_size.y > scroll.inner_rect.size().y;
|
||||
});
|
||||
scroller_showing
|
||||
}
|
||||
|
||||
/// Draw wallet list item.
|
||||
@@ -485,24 +490,6 @@ impl WalletsContent {
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw floating button to show wallet creation [`Modal`].
|
||||
fn create_wallet_btn_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
right_margin: f32,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
egui::Window::new("create_wallet_button")
|
||||
.title_bar(false)
|
||||
.resizable(false)
|
||||
.collapsible(false)
|
||||
.anchor(Align2::RIGHT_BOTTOM, egui::Vec2::new(-6.0 - right_margin, -6.0))
|
||||
.frame(egui::Frame::default())
|
||||
.show(ui.ctx(), |ui| {
|
||||
View::circle_button(ui, PLUS, || {
|
||||
self.creation_content.show_name_pass_modal(cb);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Show [`Modal`] to open selected wallet.
|
||||
pub fn show_open_wallet_modal(&mut self, cb: &dyn PlatformCallbacks) {
|
||||
// Reset modal values.
|
||||
@@ -599,8 +586,14 @@ impl WalletsContent {
|
||||
let can_go_back = self.creation_content.can_go_back();
|
||||
if can_go_back {
|
||||
self.creation_content.back();
|
||||
return false
|
||||
} else {
|
||||
if self.wallets.is_selected_open() {
|
||||
self.wallets.select(None);
|
||||
return false
|
||||
}
|
||||
}
|
||||
!can_go_back
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,8 +75,8 @@ impl WalletCreation {
|
||||
stroke: View::DEFAULT_STROKE,
|
||||
fill: Colors::FILL_DARK,
|
||||
inner_margin: Margin {
|
||||
left: View::far_left_inset_margin(ui) + 4.0,
|
||||
right: View::get_right_inset() + 4.0,
|
||||
left: View::far_left_inset_margin(ui) + 6.0,
|
||||
right: View::get_right_inset() + 6.0,
|
||||
top: 4.0,
|
||||
bottom: View::get_bottom_inset() + 4.0,
|
||||
},
|
||||
@@ -180,7 +180,7 @@ impl WalletCreation {
|
||||
self.copy_or_paste_button_ui(ui, cb);
|
||||
} else {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0);
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
// Show copy or paste button for mnemonic phrase step.
|
||||
@@ -239,7 +239,6 @@ impl WalletCreation {
|
||||
(text, Colors::WHITE)
|
||||
};
|
||||
|
||||
|
||||
// Show next step button.
|
||||
View::button(ui, next_text.to_uppercase(), color, || {
|
||||
self.step = if let Some(step) = &self.step {
|
||||
@@ -297,10 +296,10 @@ impl WalletCreation {
|
||||
match &self.step {
|
||||
None => {
|
||||
// Show wallet creation message if step is empty.
|
||||
View::center_content(ui, 415.0 + View::get_bottom_inset(), |ui| {
|
||||
View::center_content(ui, 350.0 + View::get_bottom_inset(), |ui| {
|
||||
ui.add(
|
||||
egui::Image::new(egui::include_image!("../../../../../img/logo.png"))
|
||||
.fit_to_exact_size(vec2(200.0, 200.0))
|
||||
.fit_to_exact_size(vec2(180.0, 180.0))
|
||||
);
|
||||
ui.add_space(-15.0);
|
||||
ui.label(RichText::new("GRIM")
|
||||
@@ -362,7 +361,7 @@ impl WalletCreation {
|
||||
pub fn show_name_pass_modal(&mut self, cb: &dyn PlatformCallbacks) {
|
||||
// Reset modal values.
|
||||
self.modal_just_opened = true;
|
||||
self.name_edit = String::from("");
|
||||
self.name_edit = t!("wallets.default_wallet");
|
||||
self.pass_edit = String::from("");
|
||||
// Show modal.
|
||||
Modal::new(Self::NAME_PASS_MODAL)
|
||||
|
||||
@@ -210,7 +210,7 @@ impl WalletContent {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
// Show spendable amount.
|
||||
let amount = amount_to_hr_string(data.info.amount_currently_spendable, false);
|
||||
let amount = amount_to_hr_string(data.info.amount_currently_spendable, true);
|
||||
let amount_text = format!("{} {}", amount, GRIN);
|
||||
ui.label(RichText::new(amount_text).size(18.0).color(Colors::BLACK));
|
||||
ui.add_space(-2.0);
|
||||
|
||||
@@ -79,7 +79,7 @@ impl WalletInfo {
|
||||
|
||||
// Show non-zero awaiting confirmation amount.
|
||||
if amount_awaiting_conf != 0 {
|
||||
let awaiting_conf = amount_to_hr_string(amount_awaiting_conf, false);
|
||||
let awaiting_conf = amount_to_hr_string(amount_awaiting_conf, true);
|
||||
let rounding = if amount_awaiting_fin != 0 || amount_locked != 0 {
|
||||
[false, false, false, false]
|
||||
} else {
|
||||
@@ -93,7 +93,7 @@ impl WalletInfo {
|
||||
|
||||
// Show non-zero awaiting finalization amount.
|
||||
if amount_awaiting_fin != 0 {
|
||||
let awaiting_conf = amount_to_hr_string(amount_awaiting_fin, false);
|
||||
let awaiting_conf = amount_to_hr_string(amount_awaiting_fin, true);
|
||||
let rounding = if amount_locked != 0 {
|
||||
[false, false, false, false]
|
||||
} else {
|
||||
@@ -107,7 +107,7 @@ impl WalletInfo {
|
||||
|
||||
// Show non-zero locked amount.
|
||||
if amount_locked != 0 {
|
||||
let awaiting_conf = amount_to_hr_string(amount_locked, false);
|
||||
let awaiting_conf = amount_to_hr_string(amount_locked, true);
|
||||
View::rounded_box(ui,
|
||||
format!("{} ツ", awaiting_conf),
|
||||
t!("wallets.locked_amount"),
|
||||
|
||||
+3
-1
@@ -122,8 +122,10 @@ pub fn setup_visuals(ctx: &Context) {
|
||||
let mut style = (*ctx.style()).clone();
|
||||
// Setup spacing for buttons.
|
||||
style.spacing.button_padding = egui::vec2(12.0, 8.0);
|
||||
// Make scroll-bar thinner.
|
||||
// Make scroll-bar thinner and lighter.
|
||||
style.spacing.scroll.bar_width = 4.0;
|
||||
style.spacing.scroll.bar_outer_margin = -2.0;
|
||||
style.spacing.scroll.foreground_color = false;
|
||||
// Disable spacing between items.
|
||||
style.spacing.item_spacing = egui::vec2(0.0, 0.0);
|
||||
// Setup radio button/checkbox size and spacing.
|
||||
|
||||
+26
-1
@@ -12,6 +12,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
use egui::{IconData, pos2};
|
||||
use grim::AppConfig;
|
||||
|
||||
pub fn main() {
|
||||
#[allow(dead_code)]
|
||||
#[cfg(not(target_os = "android"))]
|
||||
@@ -31,8 +35,29 @@ fn real_main() {
|
||||
use grim::gui::PlatformApp;
|
||||
|
||||
let platform = Desktop::default();
|
||||
|
||||
// Desktop window size.
|
||||
let (width, height) = AppConfig::window_size();
|
||||
|
||||
// Setup an icon.
|
||||
let icon = image::open("img/icon.png").expect("Failed to open icon path").to_rgba8();
|
||||
let (icon_width, icon_height) = icon.dimensions();
|
||||
|
||||
let mut viewport = egui::ViewportBuilder::default()
|
||||
.with_inner_size([width, height])
|
||||
.with_icon(Arc::new(IconData {
|
||||
rgba: icon.into_raw(),
|
||||
width: icon_width,
|
||||
height: icon_height,
|
||||
}));
|
||||
|
||||
// Desktop window position.
|
||||
if let Some((x, y)) = AppConfig::window_pos() {
|
||||
viewport = viewport.with_position(pos2(x, y));
|
||||
}
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default().with_inner_size([1200.0, 720.0]),
|
||||
viewport,
|
||||
..Default::default()
|
||||
};
|
||||
grim::start(options, grim::app_creator(PlatformApp::new(platform)));
|
||||
|
||||
@@ -62,7 +62,7 @@ pub struct Wallet {
|
||||
reopen: Arc<AtomicBool>,
|
||||
/// Flag to check if wallet is open.
|
||||
is_open: Arc<AtomicBool>,
|
||||
/// Flag to check if wallet is loading.
|
||||
/// Flag to check if wallet is closing.
|
||||
closing: Arc<AtomicBool>,
|
||||
/// Flag to check if wallet was deleted to remove it from the list.
|
||||
deleted: Arc<AtomicBool>,
|
||||
@@ -306,7 +306,7 @@ impl Wallet {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if wallet was open.
|
||||
/// Check if wallet is open.
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.is_open.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user