mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-20 06:08:34 +02:00
feat: zashi zkool sweep, and minor fixes (#2838)
feat: restore from keys zcash fix: don't display empty keys
This commit is contained in:
@@ -65,6 +65,7 @@ class PreferencesKey {
|
||||
static const customBitcoinFeeRate = 'custom_electrum_fee_rate';
|
||||
static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay';
|
||||
static const mwebCardDisplay = 'mwebCardDisplay';
|
||||
static const showZcashMissingFundsCard = 'showZcashMissingFundsCard';
|
||||
static const mwebEnabled = 'mwebEnabled';
|
||||
static const hasEnabledMwebBefore = 'hasEnabledMwebBefore';
|
||||
static const mwebAlwaysScan = 'mwebAlwaysScan';
|
||||
|
||||
@@ -333,6 +333,36 @@ class CryptoBalanceWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (dashboardViewModel.showZcashMissingFundsCard) ...[
|
||||
SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: InfoCard(
|
||||
title: S.of(context).zcash_card_missing_funds,
|
||||
description: S.of(context).zcash_card_description,
|
||||
leftButtonTitle: S.of(context).zcash_card_dismiss,
|
||||
rightButtonTitle: S.of(context).zcash_card_scan,
|
||||
image: 'assets/images/zec_icon.png',
|
||||
leftButtonAction: () => _dismissZcash(context),
|
||||
rightButtonAction: () => _rescanInternalChangeZcash(context),
|
||||
hintWidget: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => launchUrl(
|
||||
Uri.parse("https://docs.cakewallet.com/tutorials/zashi/#creating-a-new-zcash-wallet-on-cake"),
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
child: Text(
|
||||
S.of(context).learn_more,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (dashboardViewModel.showDecredInfoCard) ...[
|
||||
SizedBox(height: 10),
|
||||
@@ -462,6 +492,22 @@ class CryptoBalanceWidget extends StatelessWidget {
|
||||
}
|
||||
dashboardViewModel.setMwebEnabled();
|
||||
}
|
||||
|
||||
Future<void> _rescanInternalChangeZcash(BuildContext context) async {
|
||||
dashboardViewModel.rescanInternalChangeZcash();
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).alert_notice,
|
||||
alertContent: S.of(context).zcash_card_warning,
|
||||
buttonText: S.of(context).understand,
|
||||
buttonAction: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Future<void> _dismissMweb(BuildContext context) async {
|
||||
await showPopUp<void>(
|
||||
@@ -476,4 +522,18 @@ class CryptoBalanceWidget extends StatelessWidget {
|
||||
));
|
||||
dashboardViewModel.dismissMweb();
|
||||
}
|
||||
|
||||
Future<void> _dismissZcash(BuildContext context) async {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).alert_notice,
|
||||
alertContent: S.of(context).zcash_card_enable_later,
|
||||
buttonText: S.of(context).understand,
|
||||
buttonAction: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
));
|
||||
dashboardViewModel.dismissZcash();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,14 +232,26 @@ class WalletRestoreFromKeysFormState extends State<WalletRestoreFromKeysForm> {
|
||||
|
||||
bool nanoBased = widget.walletRestoreViewModel.type == WalletType.nano ||
|
||||
widget.walletRestoreViewModel.type == WalletType.banano;
|
||||
return AddressTextField(
|
||||
addressKey: ValueKey('wallet_restore_from_key_private_key_textfield_key'),
|
||||
controller: privateKeyController,
|
||||
placeholder: nanoBased ? S.of(context).seed_hex_form : S.of(context).private_key,
|
||||
options: [AddressTextFieldOption.paste],
|
||||
onPushPasteButton: (_) {
|
||||
_pasteText();
|
||||
},
|
||||
return Column(
|
||||
children: [
|
||||
AddressTextField(
|
||||
addressKey: ValueKey('wallet_restore_from_key_private_key_textfield_key'),
|
||||
controller: privateKeyController,
|
||||
placeholder: nanoBased ? S.of(context).seed_hex_form : S.of(context).private_key,
|
||||
options: [AddressTextFieldOption.paste],
|
||||
onPushPasteButton: (_) {
|
||||
_pasteText();
|
||||
},
|
||||
),
|
||||
if (widget.walletRestoreViewModel.hasBlockchainHeightSelector)
|
||||
BlockchainHeightWidget(
|
||||
key: blockchainHeightKey,
|
||||
hasDatePicker: widget.walletRestoreViewModel.type != WalletType.haven,
|
||||
onHeightChange: (_) => null,
|
||||
onHeightOrDateEntered: widget.onHeightOrDateEntered,
|
||||
walletType: widget.walletRestoreViewModel.type,
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,13 @@ class DisplaySettingsPage extends BasePage {
|
||||
_displaySettingsViewModel.setShowAddressBookPopup(value);
|
||||
},
|
||||
),
|
||||
SettingsSwitcherCell(
|
||||
title: S.of(context).show_zcash_card,
|
||||
value: _displaySettingsViewModel.showZcashCard,
|
||||
onValueChange: (_, bool value) {
|
||||
_displaySettingsViewModel.setShowZcashCard(value);
|
||||
},
|
||||
),
|
||||
SettingsPickerCell<SyncStatusDisplayMode>(
|
||||
title: S.current.sync_status_display_mode,
|
||||
items: SyncStatusDisplayMode.values.toList(),
|
||||
|
||||
@@ -133,6 +133,7 @@ abstract class SettingsStoreBase with Store {
|
||||
required this.silentPaymentsCardDisplay,
|
||||
required this.mwebAlwaysScan,
|
||||
required this.mwebCardDisplay,
|
||||
required this.showZcashMissingFundsCard,
|
||||
required this.mwebEnabled,
|
||||
required this.hasEnabledMwebBefore,
|
||||
required this.mwebNodeUri,
|
||||
@@ -660,6 +661,11 @@ abstract class SettingsStoreBase with Store {
|
||||
(bool mwebCardDisplay) =>
|
||||
_sharedPreferences.setBool(PreferencesKey.mwebCardDisplay, mwebCardDisplay));
|
||||
|
||||
reaction(
|
||||
(_) => showZcashMissingFundsCard,
|
||||
(bool showZcashMissingFundsCard) =>
|
||||
_sharedPreferences.setBool(PreferencesKey.showZcashMissingFundsCard, showZcashMissingFundsCard));
|
||||
|
||||
reaction((_) => mwebEnabled,
|
||||
(bool mwebEnabled) => _sharedPreferences.setBool(PreferencesKey.mwebEnabled, mwebEnabled));
|
||||
|
||||
@@ -937,6 +943,9 @@ abstract class SettingsStoreBase with Store {
|
||||
|
||||
@observable
|
||||
bool mwebCardDisplay;
|
||||
|
||||
@observable
|
||||
bool showZcashMissingFundsCard;
|
||||
|
||||
@observable
|
||||
bool mwebEnabled;
|
||||
@@ -1197,6 +1206,7 @@ abstract class SettingsStoreBase with Store {
|
||||
sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true;
|
||||
final mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false;
|
||||
final mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true;
|
||||
final showZcashMissingFundsCard = sharedPreferences.getBool(PreferencesKey.showZcashMissingFundsCard) ?? true;
|
||||
final mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false;
|
||||
final hasEnabledMwebBefore =
|
||||
sharedPreferences.getBool(PreferencesKey.hasEnabledMwebBefore) ?? false;
|
||||
@@ -1554,6 +1564,7 @@ abstract class SettingsStoreBase with Store {
|
||||
silentPaymentsCardDisplay: silentPaymentsCardDisplay,
|
||||
mwebAlwaysScan: mwebAlwaysScan,
|
||||
mwebCardDisplay: mwebCardDisplay,
|
||||
showZcashMissingFundsCard: showZcashMissingFundsCard,
|
||||
mwebEnabled: mwebEnabled,
|
||||
mwebNodeUri: mwebNodeUri,
|
||||
hasEnabledMwebBefore: hasEnabledMwebBefore,
|
||||
@@ -1768,6 +1779,7 @@ abstract class SettingsStoreBase with Store {
|
||||
sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true;
|
||||
mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false;
|
||||
mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true;
|
||||
showZcashMissingFundsCard = sharedPreferences.getBool(PreferencesKey.showZcashMissingFundsCard) ?? true;
|
||||
mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false;
|
||||
hasEnabledMwebBefore = sharedPreferences.getBool(PreferencesKey.hasEnabledMwebBefore) ?? false;
|
||||
final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
|
||||
|
||||
@@ -19,6 +19,7 @@ import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/store/dashboard/order_filter_store.dart';
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/zcash/zcash.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cake_wallet/utils/tor.dart';
|
||||
import 'package:cake_wallet/wownero/wownero.dart' as wow;
|
||||
@@ -580,6 +581,13 @@ abstract class DashboardViewModelBase with Store {
|
||||
];
|
||||
return errors;
|
||||
}
|
||||
|
||||
@computed
|
||||
bool get showZcashMissingFundsCard {
|
||||
if (wallet.type != WalletType.zcash) return false;
|
||||
if (!settingsStore.showZcashMissingFundsCard) return false;
|
||||
return zcash!.showMissingFundsCard(wallet);
|
||||
}
|
||||
|
||||
@computed
|
||||
bool get hasSilentPayments =>
|
||||
@@ -852,6 +860,16 @@ abstract class DashboardViewModelBase with Store {
|
||||
mwebEnabled = false;
|
||||
bitcoin!.setMwebEnabled(wallet, false);
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> rescanInternalChangeZcash() async {
|
||||
await zcash!.rescanInternalChange(wallet);
|
||||
}
|
||||
|
||||
@action
|
||||
void dismissZcash() {
|
||||
settingsStore.showZcashMissingFundsCard = false;
|
||||
}
|
||||
|
||||
@action
|
||||
void dismissDecredInfoCard() {
|
||||
|
||||
@@ -105,7 +105,7 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
|
||||
case WalletType.polygon:
|
||||
return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${wallet.type}');
|
||||
throw Exception('Unexpected wallet type: ${wallet.type} for bitbox');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
case WalletType.polygon:
|
||||
return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${wallet.type}');
|
||||
throw Exception('Unexpected wallet type: ${wallet.type} for ledger');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ abstract class TrezorViewModelBase extends HardwareWalletViewModel with Store {
|
||||
case WalletType.polygon:
|
||||
return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${wallet.type}');
|
||||
throw Exception('Unexpected wallet type: ${wallet.type} for trezor');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ abstract class MoneroAccountListViewModelBase with Store {
|
||||
.toList();
|
||||
}
|
||||
|
||||
throw Exception('Unexpected wallet type: ${_wallet.type}');
|
||||
throw Exception('Unexpected wallet type: ${_wallet.type} for monero');
|
||||
}
|
||||
|
||||
final WalletBase _wallet;
|
||||
|
||||
@@ -36,7 +36,7 @@ abstract class NanoAccountListViewModelBase with Store {
|
||||
.toList();
|
||||
}
|
||||
|
||||
throw Exception('Unexpected wallet type: ${_wallet.type}');
|
||||
throw Exception('Unexpected wallet type: ${_wallet.type} for nano');
|
||||
}
|
||||
|
||||
final WalletBase _wallet;
|
||||
|
||||
@@ -63,7 +63,7 @@ abstract class PowNodeListViewModelBase with Store {
|
||||
node = getNanoDefaultPowNode(nodes: _nodeSource)!;
|
||||
break;
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}');
|
||||
throw Exception('Unexpected wallet type: ${_appStore.wallet!.type} for pow node list');
|
||||
}
|
||||
|
||||
await setAsCurrent(node);
|
||||
|
||||
@@ -1035,7 +1035,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
// priority: priority,
|
||||
);
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${wallet.type}');
|
||||
throw Exception('Unexpected wallet type: ${wallet.type} for send');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,9 @@ abstract class DisplaySettingsViewModelBase with Store {
|
||||
|
||||
@computed
|
||||
bool get showAddressBookPopup => _settingsStore.showAddressBookPopupEnabled;
|
||||
|
||||
@computed
|
||||
bool get showZcashCard => _settingsStore.showZcashMissingFundsCard;
|
||||
|
||||
@computed
|
||||
SyncStatusDisplayMode get syncStatusDisplayMode => _settingsStore.syncStatusDisplayMode;
|
||||
@@ -103,6 +106,9 @@ abstract class DisplaySettingsViewModelBase with Store {
|
||||
@action
|
||||
void setBalanceDisplayMode(BalanceDisplayMode value) => _settingsStore.balanceDisplayMode = value;
|
||||
|
||||
@action
|
||||
void setShowZcashCard(bool value) => _settingsStore.showZcashMissingFundsCard = value;
|
||||
|
||||
@action
|
||||
void setShouldDisplayBalance(bool value) {
|
||||
if (value) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.d
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/wownero/wownero.dart';
|
||||
import 'package:cake_wallet/zano/zano.dart';
|
||||
import 'package:cake_wallet/zcash/zcash.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
@@ -139,13 +140,15 @@ abstract class WalletKeysViewModelBase with Store {
|
||||
case WalletType.zano:
|
||||
keys = zano!.getKeys(_wallet);
|
||||
break;
|
||||
case WalletType.zcash:
|
||||
keys = zcash!.getKeys(_wallet);
|
||||
break;
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
case WalletType.base:
|
||||
case WalletType.arbitrum:
|
||||
case WalletType.bsc:
|
||||
case WalletType.solana:
|
||||
case WalletType.zcash:
|
||||
case WalletType.tron:
|
||||
items.addAll([
|
||||
if (_wallet.privateKey != null)
|
||||
@@ -208,35 +211,47 @@ abstract class WalletKeysViewModelBase with Store {
|
||||
|
||||
if (keys != null) {
|
||||
items.addAll([
|
||||
if (keys['primaryAddress'] != null)
|
||||
if ((keys['primaryAddress']??'').isNotEmpty)
|
||||
StandartListItem(
|
||||
key: ValueKey('${_walletName}_wallet_primary_address_item_key'),
|
||||
title: S.current.primary_address,
|
||||
value: keys['primaryAddress']!),
|
||||
if (keys['publicSpendKey'] != null)
|
||||
if ((keys['publicSpendKey']??'').isNotEmpty)
|
||||
StandartListItem(
|
||||
key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'),
|
||||
title: S.current.spend_key_public,
|
||||
value: keys['publicSpendKey']!,
|
||||
),
|
||||
if (keys['privateSpendKey'] != null)
|
||||
if ((keys['privateSpendKey']??'').isNotEmpty)
|
||||
StandartListItem(
|
||||
key: ValueKey('${_walletName}_wallet_private_spend_key_item_key'),
|
||||
title: S.current.spend_key_private,
|
||||
value: keys['privateSpendKey']!,
|
||||
),
|
||||
if (keys['publicViewKey'] != null)
|
||||
if ((keys['publicViewKey']??'').isNotEmpty)
|
||||
StandartListItem(
|
||||
key: ValueKey('${_walletName}_wallet_public_view_key_item_key'),
|
||||
title: S.current.view_key_public,
|
||||
value: keys['publicViewKey']!,
|
||||
),
|
||||
if (keys['privateViewKey'] != null)
|
||||
if ((keys['privateViewKey']??'').isNotEmpty)
|
||||
StandartListItem(
|
||||
key: ValueKey('${_walletName}_wallet_private_view_key_item_key'),
|
||||
title: S.current.view_key_private,
|
||||
value: keys['privateViewKey']!,
|
||||
),
|
||||
if ((keys['tsk']??'').isNotEmpty)
|
||||
StandartListItem(
|
||||
key: ValueKey('${_walletName}_wallet_transparent_secret_key_item_key'),
|
||||
title: S.current.transparent_secret_key,
|
||||
value: keys['tsk']!,
|
||||
),
|
||||
if ((keys['uvk']??'').isNotEmpty)
|
||||
StandartListItem(
|
||||
key: ValueKey('${_walletName}_wallet_unified_view_key_item_key'),
|
||||
title: S.current.unified_view_key,
|
||||
value: keys['uvk']!,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -292,7 +307,7 @@ abstract class WalletKeysViewModelBase with Store {
|
||||
case WalletType.zcash:
|
||||
return 'zcash-wallet';
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${_wallet.type.toString()}');
|
||||
throw Exception('Unexpected wallet type: ${_wallet.type.toString()} for wallet keys');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,11 +57,11 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
|
||||
case WalletType.decred:
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.litecoin:
|
||||
case WalletType.zcash:
|
||||
availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys];
|
||||
break;
|
||||
case WalletType.bitcoinCash:
|
||||
case WalletType.zano:
|
||||
case WalletType.zcash:
|
||||
case WalletType.dogecoin:
|
||||
availableModes = [WalletRestoreMode.seed];
|
||||
break;
|
||||
@@ -95,7 +95,8 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
|
||||
WalletType.nano,
|
||||
WalletType.banano,
|
||||
WalletType.solana,
|
||||
WalletType.tron
|
||||
WalletType.tron,
|
||||
WalletType.zcash,
|
||||
].contains(type);
|
||||
|
||||
late final bool onlyViewKeyRestore =
|
||||
@@ -307,6 +308,13 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
|
||||
password: password,
|
||||
pubkey: viewKey!,
|
||||
);
|
||||
case WalletType.zcash:
|
||||
return zcash!.createZcashRestoreWalletFromPrivateKey(
|
||||
name: name,
|
||||
privateKey: options['private_key'] as String,
|
||||
password: password,
|
||||
height: height
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
+21
-4
@@ -20,8 +20,14 @@ class CWZcash extends Zcash {
|
||||
WalletCredentials createZcashRestoreWalletFromPrivateKey(
|
||||
{required String name,
|
||||
required String privateKey,
|
||||
required String password}) {
|
||||
throw UnimplementedError();
|
||||
required String password,
|
||||
required int height}) {
|
||||
return ZcashFromKeysWalletCredentials(
|
||||
name: name,
|
||||
height: height,
|
||||
privateKey: privateKey,
|
||||
password: password,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -122,11 +128,10 @@ class CWZcash extends Zcash {
|
||||
Map<String, String> getKeys(Object wallet) {
|
||||
final zcashWallet = wallet as ZcashWallet;
|
||||
final seed = zcashWallet.seed;
|
||||
final address = zcashWallet.walletAddresses.address;
|
||||
|
||||
return <String, String>{
|
||||
if (seed != null) 'seed': seed,
|
||||
'primaryAddress': address,
|
||||
...(zcashWallet.keys as Map<String, String?>).map((final k, final v) => MapEntry(k, v??'')),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -209,6 +214,18 @@ class CWZcash extends Zcash {
|
||||
Future<int> getHeightByDate(DateTime date) {
|
||||
return ZcashWalletBase.getHeightByDate(date);
|
||||
}
|
||||
|
||||
@override
|
||||
bool showMissingFundsCard(WalletBase wallet) {
|
||||
final zcashWallet = wallet as ZcashWallet;
|
||||
return zcashWallet.couldBeZashiWallet();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rescanInternalChange(WalletBase wallet) {
|
||||
final zcashWallet = wallet as ZcashWallet;
|
||||
return zcashWallet.rescanInternalChange();
|
||||
}
|
||||
}
|
||||
|
||||
const wordList = [
|
||||
|
||||
Reference in New Issue
Block a user