Add AccountManager to app
Signed-off-by: kernelkind <kernelkind@gmail.com>
This commit is contained in:
committed by
William Casarin
parent
748d9d2358
commit
11b3effa51
@@ -19,7 +19,7 @@ impl<'a> SimpleProfilePreviewController<'a> {
|
|||||||
|
|
||||||
pub fn set_profile_previews(
|
pub fn set_profile_previews(
|
||||||
&mut self,
|
&mut self,
|
||||||
account_manager: &AccountManager<'a>,
|
account_manager: &AccountManager,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
edit_mode: bool,
|
edit_mode: bool,
|
||||||
add_preview_ui: fn(
|
add_preview_ui: fn(
|
||||||
@@ -56,7 +56,7 @@ impl<'a> SimpleProfilePreviewController<'a> {
|
|||||||
|
|
||||||
pub fn view_profile_previews(
|
pub fn view_profile_previews(
|
||||||
&mut self,
|
&mut self,
|
||||||
account_manager: &'a AccountManager<'a>,
|
account_manager: &'a AccountManager,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
add_preview_ui: fn(ui: &mut egui::Ui, preview: SimpleProfilePreview, index: usize) -> bool,
|
add_preview_ui: fn(ui: &mut egui::Ui, preview: SimpleProfilePreview, index: usize) -> bool,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
@@ -86,18 +86,31 @@ impl<'a> SimpleProfilePreviewController<'a> {
|
|||||||
|
|
||||||
/// The interface for managing the user's accounts.
|
/// The interface for managing the user's accounts.
|
||||||
/// Represents all user-facing operations related to account management.
|
/// Represents all user-facing operations related to account management.
|
||||||
pub struct AccountManager<'a> {
|
pub struct AccountManager {
|
||||||
accounts: &'a mut Vec<UserAccount>,
|
accounts: Vec<UserAccount>,
|
||||||
key_store: KeyStorage,
|
key_store: KeyStorage,
|
||||||
relay_generator: RelayGenerator,
|
relay_generator: RelayGenerator,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> AccountManager<'a> {
|
impl AccountManager {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
accounts: &'a mut Vec<UserAccount>,
|
|
||||||
key_store: KeyStorage,
|
key_store: KeyStorage,
|
||||||
|
// TODO: right now, there is only one way of generating relays for all accounts. In the future
|
||||||
|
// each account should have the option of generating relays differently
|
||||||
relay_generator: RelayGenerator,
|
relay_generator: RelayGenerator,
|
||||||
|
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let accounts = if let Ok(keys) = key_store.get_keys() {
|
||||||
|
keys.into_iter()
|
||||||
|
.map(|key| {
|
||||||
|
let relays = relay_generator.generate_relays_for(&key.pubkey, wakeup.clone());
|
||||||
|
UserAccount { key, relays }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
AccountManager {
|
AccountManager {
|
||||||
accounts,
|
accounts,
|
||||||
key_store,
|
key_store,
|
||||||
@@ -105,11 +118,11 @@ impl<'a> AccountManager<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_accounts(&'a self) -> &'a Vec<UserAccount> {
|
pub fn get_accounts(&self) -> &Vec<UserAccount> {
|
||||||
self.accounts
|
&self.accounts
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_account(&'a self, index: usize) -> Option<&'a UserAccount> {
|
pub fn get_account(&self, index: usize) -> Option<&UserAccount> {
|
||||||
self.accounts.get(index)
|
self.accounts.get(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,9 +135,15 @@ impl<'a> AccountManager<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_account(&'a mut self, key: FullKeypair, ctx: &egui::Context) {
|
pub fn add_account(
|
||||||
|
&mut self,
|
||||||
|
key: FullKeypair,
|
||||||
|
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||||
|
) {
|
||||||
let _ = self.key_store.add_key(&key);
|
let _ = self.key_store.add_key(&key);
|
||||||
let relays = self.relay_generator.generate_relays_for(&key.pubkey, ctx);
|
let relays = self
|
||||||
|
.relay_generator
|
||||||
|
.generate_relays_for(&key.pubkey, wakeup);
|
||||||
let account = UserAccount { key, relays };
|
let account = UserAccount { key, relays };
|
||||||
|
|
||||||
self.accounts.push(account)
|
self.accounts.push(account)
|
||||||
|
|||||||
19
src/app.rs
19
src/app.rs
@@ -1,10 +1,11 @@
|
|||||||
use crate::account_manager::UserAccount;
|
use crate::account_manager::AccountManager;
|
||||||
use crate::app_creation::setup_cc;
|
use crate::app_creation::setup_cc;
|
||||||
use crate::app_style::user_requested_visuals_change;
|
use crate::app_style::user_requested_visuals_change;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::frame_history::FrameHistory;
|
use crate::frame_history::FrameHistory;
|
||||||
use crate::imgcache::ImageCache;
|
use crate::imgcache::ImageCache;
|
||||||
use crate::notecache::{CachedNote, NoteCache};
|
use crate::notecache::{CachedNote, NoteCache};
|
||||||
|
use crate::relay_pool_manager::create_wakeup;
|
||||||
use crate::timeline;
|
use crate::timeline;
|
||||||
use crate::timeline::{NoteRef, Timeline, ViewFilter};
|
use crate::timeline::{NoteRef, Timeline, ViewFilter};
|
||||||
use crate::ui::{is_mobile, DesktopGlobalPopup, DesktopSidePanel, View};
|
use crate::ui::{is_mobile, DesktopGlobalPopup, DesktopSidePanel, View};
|
||||||
@@ -44,7 +45,7 @@ pub struct Damus {
|
|||||||
|
|
||||||
pub img_cache: ImageCache,
|
pub img_cache: ImageCache,
|
||||||
pub ndb: Ndb,
|
pub ndb: Ndb,
|
||||||
pub accounts: Vec<UserAccount>,
|
pub account_manager: AccountManager,
|
||||||
|
|
||||||
frame_history: crate::frame_history::FrameHistory,
|
frame_history: crate::frame_history::FrameHistory,
|
||||||
}
|
}
|
||||||
@@ -646,7 +647,13 @@ impl Damus {
|
|||||||
timelines,
|
timelines,
|
||||||
textmode: false,
|
textmode: false,
|
||||||
ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"),
|
ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"),
|
||||||
accounts: Vec::new(),
|
account_manager: AccountManager::new(
|
||||||
|
// TODO: use correct KeyStorage mechanism for current OS arch
|
||||||
|
crate::key_storage::KeyStorage::None,
|
||||||
|
// TODO: setting for relay generator
|
||||||
|
crate::relay_generation::RelayGenerator::Constant,
|
||||||
|
create_wakeup(&cc.egui_ctx),
|
||||||
|
),
|
||||||
//compose: "".to_string(),
|
//compose: "".to_string(),
|
||||||
frame_history: FrameHistory::default(),
|
frame_history: FrameHistory::default(),
|
||||||
}
|
}
|
||||||
@@ -672,7 +679,11 @@ impl Damus {
|
|||||||
timelines,
|
timelines,
|
||||||
textmode: false,
|
textmode: false,
|
||||||
ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"),
|
ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"),
|
||||||
accounts: Vec::new(),
|
account_manager: AccountManager::new(
|
||||||
|
crate::key_storage::KeyStorage::None,
|
||||||
|
crate::relay_generation::RelayGenerator::Constant,
|
||||||
|
|| {},
|
||||||
|
),
|
||||||
frame_history: FrameHistory::default(),
|
frame_history: FrameHistory::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use crate::relay_pool_manager::create_wakeup;
|
|
||||||
use enostr::{Pubkey, RelayPool};
|
use enostr::{Pubkey, RelayPool};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
@@ -9,30 +8,39 @@ pub enum RelayGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RelayGenerator {
|
impl RelayGenerator {
|
||||||
pub fn generate_relays_for(&self, key: &Pubkey, ctx: &egui::Context) -> RelayPool {
|
pub fn generate_relays_for(
|
||||||
|
&self,
|
||||||
|
key: &Pubkey,
|
||||||
|
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||||
|
) -> RelayPool {
|
||||||
match self {
|
match self {
|
||||||
Self::GossipModel => generate_relays_gossip(key, ctx),
|
Self::GossipModel => generate_relays_gossip(key, wakeup),
|
||||||
Self::Nip65 => generate_relays_nip65(key, ctx),
|
Self::Nip65 => generate_relays_nip65(key, wakeup),
|
||||||
Self::Constant => generate_constant_relays(ctx),
|
Self::Constant => generate_constant_relays(wakeup),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_relays_gossip(key: &Pubkey, ctx: &egui::Context) -> RelayPool {
|
fn generate_relays_gossip(
|
||||||
let _ = ctx;
|
key: &Pubkey,
|
||||||
|
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||||
|
) -> RelayPool {
|
||||||
|
let _ = wakeup;
|
||||||
let _ = key;
|
let _ = key;
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_relays_nip65(key: &Pubkey, ctx: &egui::Context) -> RelayPool {
|
fn generate_relays_nip65(
|
||||||
let _ = ctx;
|
key: &Pubkey,
|
||||||
|
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||||
|
) -> RelayPool {
|
||||||
|
let _ = wakeup;
|
||||||
let _ = key;
|
let _ = key;
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_constant_relays(ctx: &egui::Context) -> RelayPool {
|
fn generate_constant_relays(wakeup: impl Fn() + Send + Sync + Clone + 'static) -> RelayPool {
|
||||||
let mut pool = RelayPool::new();
|
let mut pool = RelayPool::new();
|
||||||
let wakeup = create_wakeup(ctx);
|
|
||||||
|
|
||||||
if let Err(e) = pool.add_url("ws://localhost:8080".to_string(), wakeup.clone()) {
|
if let Err(e) = pool.add_url("ws://localhost:8080".to_string(), wakeup.clone()) {
|
||||||
error!("{:?}", e)
|
error!("{:?}", e)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use super::profile::preview::SimpleProfilePreview;
|
|||||||
use super::state_in_memory::STATE_ACCOUNT_MANAGEMENT;
|
use super::state_in_memory::STATE_ACCOUNT_MANAGEMENT;
|
||||||
|
|
||||||
pub struct AccountManagementView<'a> {
|
pub struct AccountManagementView<'a> {
|
||||||
account_manager: AccountManager<'a>,
|
account_manager: &'a mut AccountManager,
|
||||||
simple_preview_controller: SimpleProfilePreviewController<'a>,
|
simple_preview_controller: SimpleProfilePreviewController<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ impl<'a> View for AccountManagementView<'a> {
|
|||||||
|
|
||||||
impl<'a> AccountManagementView<'a> {
|
impl<'a> AccountManagementView<'a> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
account_manager: AccountManager<'a>,
|
account_manager: &'a mut AccountManager,
|
||||||
simple_preview_controller: SimpleProfilePreviewController<'a>,
|
simple_preview_controller: SimpleProfilePreviewController<'a>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
AccountManagementView {
|
AccountManagementView {
|
||||||
@@ -47,7 +47,7 @@ impl<'a> AccountManagementView<'a> {
|
|||||||
fn show_accounts(&mut self, ui: &mut egui::Ui) {
|
fn show_accounts(&mut self, ui: &mut egui::Ui) {
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
let maybe_remove = self.simple_preview_controller.set_profile_previews(
|
let maybe_remove = self.simple_preview_controller.set_profile_previews(
|
||||||
&self.account_manager,
|
self.account_manager,
|
||||||
ui,
|
ui,
|
||||||
STATE_ACCOUNT_MANAGEMENT.get_state(ui.ctx()),
|
STATE_ACCOUNT_MANAGEMENT.get_state(ui.ctx()),
|
||||||
desktop_account_card_ui(),
|
desktop_account_card_ui(),
|
||||||
@@ -64,7 +64,7 @@ impl<'a> AccountManagementView<'a> {
|
|||||||
|ui| {
|
|ui| {
|
||||||
// create all account 'cards' and get the indicies the user requested to remove
|
// create all account 'cards' and get the indicies the user requested to remove
|
||||||
let maybe_remove = self.simple_preview_controller.set_profile_previews(
|
let maybe_remove = self.simple_preview_controller.set_profile_previews(
|
||||||
&self.account_manager,
|
self.account_manager,
|
||||||
ui,
|
ui,
|
||||||
STATE_ACCOUNT_MANAGEMENT.get_state(ui.ctx()),
|
STATE_ACCOUNT_MANAGEMENT.get_state(ui.ctx()),
|
||||||
mobile_account_card_ui(), // closure for creating an account 'card'
|
mobile_account_card_ui(), // closure for creating an account 'card'
|
||||||
@@ -180,13 +180,8 @@ fn desktop_account_card_ui(
|
|||||||
|
|
||||||
impl<'a> FromApp<'a> for AccountManagementView<'a> {
|
impl<'a> FromApp<'a> for AccountManagementView<'a> {
|
||||||
fn from_app(app: &'a mut crate::Damus) -> Self {
|
fn from_app(app: &'a mut crate::Damus) -> Self {
|
||||||
// TODO: don't hard-code key store & relay generator
|
|
||||||
AccountManagementView::new(
|
AccountManagementView::new(
|
||||||
AccountManager::new(
|
&mut app.account_manager,
|
||||||
&mut app.accounts,
|
|
||||||
crate::key_storage::KeyStorage::None,
|
|
||||||
crate::relay_generation::RelayGenerator::Constant,
|
|
||||||
),
|
|
||||||
SimpleProfilePreviewController::new(&app.ndb, &mut app.img_cache),
|
SimpleProfilePreviewController::new(&app.ndb, &mut app.img_cache),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -239,7 +234,7 @@ fn delete_button(_dark_mode: bool) -> egui::Button<'static> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct AccountSelectionWidget<'a> {
|
pub struct AccountSelectionWidget<'a> {
|
||||||
account_manager: AccountManager<'a>,
|
account_manager: &'a mut AccountManager,
|
||||||
simple_preview_controller: SimpleProfilePreviewController<'a>,
|
simple_preview_controller: SimpleProfilePreviewController<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +244,7 @@ impl<'a> AccountSelectionWidget<'a> {
|
|||||||
scroll_area().show(ui, |ui| {
|
scroll_area().show(ui, |ui| {
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
let clicked_at = self.simple_preview_controller.view_profile_previews(
|
let clicked_at = self.simple_preview_controller.view_profile_previews(
|
||||||
&self.account_manager,
|
self.account_manager,
|
||||||
ui,
|
ui,
|
||||||
|ui, preview, index| {
|
|ui, preview, index| {
|
||||||
let resp = ui.add_sized(preview.dimensions(), |ui: &mut egui::Ui| {
|
let resp = ui.add_sized(preview.dimensions(), |ui: &mut egui::Ui| {
|
||||||
@@ -278,7 +273,7 @@ impl<'a> AccountSelectionWidget<'a> {
|
|||||||
|
|
||||||
impl<'a> AccountSelectionWidget<'a> {
|
impl<'a> AccountSelectionWidget<'a> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
account_manager: AccountManager<'a>,
|
account_manager: &'a mut AccountManager,
|
||||||
simple_preview_controller: SimpleProfilePreviewController<'a>,
|
simple_preview_controller: SimpleProfilePreviewController<'a>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
AccountSelectionWidget {
|
AccountSelectionWidget {
|
||||||
@@ -301,7 +296,7 @@ mod preview {
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub struct AccountManagementPreview {
|
pub struct AccountManagementPreview {
|
||||||
accounts: Vec<UserAccount>,
|
account_manager: AccountManager,
|
||||||
ndb: Ndb,
|
ndb: Ndb,
|
||||||
img_cache: ImageCache,
|
img_cache: ImageCache,
|
||||||
}
|
}
|
||||||
@@ -320,11 +315,16 @@ mod preview {
|
|||||||
|
|
||||||
impl AccountManagementPreview {
|
impl AccountManagementPreview {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
|
let mut account_manager =
|
||||||
|
AccountManager::new(KeyStorage::None, RelayGenerator::Constant, || {});
|
||||||
let accounts = test_data::get_test_accounts();
|
let accounts = test_data::get_test_accounts();
|
||||||
|
accounts
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|acc| account_manager.add_account(acc.key, || {}));
|
||||||
let (ndb, img_cache) = get_ndb_and_img_cache();
|
let (ndb, img_cache) = get_ndb_and_img_cache();
|
||||||
|
|
||||||
AccountManagementPreview {
|
AccountManagementPreview {
|
||||||
accounts,
|
account_manager,
|
||||||
ndb,
|
ndb,
|
||||||
img_cache,
|
img_cache,
|
||||||
}
|
}
|
||||||
@@ -333,15 +333,9 @@ mod preview {
|
|||||||
|
|
||||||
impl View for AccountManagementPreview {
|
impl View for AccountManagementPreview {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
let account_manager = AccountManager::new(
|
|
||||||
&mut self.accounts,
|
|
||||||
KeyStorage::None,
|
|
||||||
RelayGenerator::Constant,
|
|
||||||
);
|
|
||||||
|
|
||||||
ui.add_space(24.0);
|
ui.add_space(24.0);
|
||||||
AccountManagementView::new(
|
AccountManagementView::new(
|
||||||
account_manager,
|
&mut self.account_manager,
|
||||||
SimpleProfilePreviewController::new(&self.ndb, &mut self.img_cache),
|
SimpleProfilePreviewController::new(&self.ndb, &mut self.img_cache),
|
||||||
)
|
)
|
||||||
.ui(ui);
|
.ui(ui);
|
||||||
@@ -357,17 +351,22 @@ mod preview {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct AccountSelectionPreview {
|
pub struct AccountSelectionPreview {
|
||||||
accounts: Vec<UserAccount>,
|
account_manager: AccountManager,
|
||||||
ndb: Ndb,
|
ndb: Ndb,
|
||||||
img_cache: ImageCache,
|
img_cache: ImageCache,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccountSelectionPreview {
|
impl AccountSelectionPreview {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
|
let mut account_manager =
|
||||||
|
AccountManager::new(KeyStorage::None, RelayGenerator::Constant, || {});
|
||||||
let accounts = test_data::get_test_accounts();
|
let accounts = test_data::get_test_accounts();
|
||||||
|
accounts
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|acc| account_manager.add_account(acc.key, || {}));
|
||||||
let (ndb, img_cache) = get_ndb_and_img_cache();
|
let (ndb, img_cache) = get_ndb_and_img_cache();
|
||||||
AccountSelectionPreview {
|
AccountSelectionPreview {
|
||||||
accounts,
|
account_manager,
|
||||||
ndb,
|
ndb,
|
||||||
img_cache,
|
img_cache,
|
||||||
}
|
}
|
||||||
@@ -376,14 +375,8 @@ mod preview {
|
|||||||
|
|
||||||
impl View for AccountSelectionPreview {
|
impl View for AccountSelectionPreview {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
let account_manager = AccountManager::new(
|
|
||||||
&mut self.accounts,
|
|
||||||
KeyStorage::None,
|
|
||||||
RelayGenerator::Constant,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut widget = AccountSelectionWidget::new(
|
let mut widget = AccountSelectionWidget::new(
|
||||||
account_manager,
|
&mut self.account_manager,
|
||||||
SimpleProfilePreviewController::new(&self.ndb, &mut self.img_cache),
|
SimpleProfilePreviewController::new(&self.ndb, &mut self.img_cache),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ impl<'a> DesktopGlobalPopup<'a> {
|
|||||||
|
|
||||||
mod preview {
|
mod preview {
|
||||||
use crate::{
|
use crate::{
|
||||||
test_data::get_test_accounts,
|
test_data,
|
||||||
ui::{DesktopSidePanel, Preview, View},
|
ui::{DesktopSidePanel, Preview, View},
|
||||||
Damus,
|
Damus,
|
||||||
};
|
};
|
||||||
@@ -113,7 +113,10 @@ mod preview {
|
|||||||
impl GlobalPopupPreview {
|
impl GlobalPopupPreview {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let mut app = Damus::mock(".");
|
let mut app = Damus::mock(".");
|
||||||
app.accounts = get_test_accounts();
|
let accounts = test_data::get_test_accounts();
|
||||||
|
accounts
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|acc| app.account_manager.add_account(acc.key, || {}));
|
||||||
GlobalPopupPreview { app }
|
GlobalPopupPreview { app }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user