mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 09:50:15 +01:00
Merge branch 'master' into PSBT---File-can-only-be-saved-on-APP,-not-on-Internal/External-Storage-#6220
This commit is contained in:
commit
8a12b2a349
@ -15,7 +15,6 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
I18nManager,
|
||||
ImageBackground,
|
||||
} from 'react-native';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import NetworkTransactionFees, { NetworkTransactionFee, NetworkTransactionFeeType } from './models/networkTransactionFees';
|
||||
@ -191,18 +190,6 @@ export const BlueButtonLink = forwardRef((props, ref) => {
|
||||
);
|
||||
});
|
||||
|
||||
export const BluePrivateBalance = () => {
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginTop: 13, borderRadius: 9 }}>
|
||||
<ImageBackground
|
||||
blurRadius={6}
|
||||
style={{ backgroundColor: '#FFFFFF', opacity: 0.5, height: 30, width: 110, marginRight: 8, borderRadius: 9 }}
|
||||
/>
|
||||
<Icon name="eye-slash" type="font-awesome" color="#FFFFFF" />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const BlueCard = props => {
|
||||
return <View {...props} style={{ padding: 20 }} />;
|
||||
};
|
||||
|
18
components/BlurredBalanceView.tsx
Normal file
18
components/BlurredBalanceView.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { ImageBackground, StyleSheet, View } from 'react-native';
|
||||
import { Icon } from 'react-native-elements';
|
||||
|
||||
export const BlurredBalanceView = () => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* @ts-ignore: We just want the blur effect. No source prop needed */}
|
||||
<ImageBackground blurRadius={6} style={styles.background} />
|
||||
<Icon name="eye-slash" type="font-awesome" color="#FFFFFF" />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flexDirection: 'row', alignItems: 'center', borderRadius: 9 },
|
||||
background: { backgroundColor: '#FFFFFF', opacity: 0.5, height: 30, width: 110, marginRight: 8, borderRadius: 9 },
|
||||
});
|
@ -41,6 +41,7 @@ const ToolTipMenu = (props, ref) => {
|
||||
const isMenuPrimaryAction = props.isMenuPrimaryAction ? props.isMenuPrimaryAction : false;
|
||||
const renderPreview = props.renderPreview ?? undefined;
|
||||
const disabled = props.disabled ?? false;
|
||||
const onPress = props.onPress ?? undefined;
|
||||
|
||||
const buttonStyle = props.buttonStyle;
|
||||
return isButton ? (
|
||||
@ -55,14 +56,11 @@ const ToolTipMenu = (props, ref) => {
|
||||
menuTitle,
|
||||
menuItems,
|
||||
}}
|
||||
style={buttonStyle}
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
{props.onPress ? (
|
||||
<TouchableOpacity accessibilityRole="button" style={buttonStyle} onPress={props.onPress}>
|
||||
{props.children}
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
props.children
|
||||
)}
|
||||
{props.children}
|
||||
</ContextMenuButton>
|
||||
) : props.onPress ? (
|
||||
<TouchableOpacity accessibilityRole="button" onPress={props.onPress}>
|
||||
|
@ -1,17 +1,16 @@
|
||||
import React, { useState, useEffect, useRef, useContext, useCallback, useMemo } from 'react';
|
||||
import { Image, Text, TouchableOpacity, View, I18nManager, StyleSheet } from 'react-native';
|
||||
import { Image, Text, TouchableOpacity, View, I18nManager, StyleSheet, LayoutAnimation } from 'react-native';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import { HDSegwitBech32Wallet, LightningCustodianWallet, LightningLdkWallet, MultisigHDWallet } from '../class';
|
||||
import { BitcoinUnit } from '../models/bitcoinUnits';
|
||||
import WalletGradient from '../class/wallet-gradient';
|
||||
import Biometric from '../class/biometrics';
|
||||
import loc, { formatBalance } from '../loc';
|
||||
import loc, { formatBalance, formatBalanceWithoutSuffix } from '../loc';
|
||||
import { BlueStorageContext } from '../blue_modules/storage-context';
|
||||
import ToolTipMenu from './TooltipMenu';
|
||||
import { BluePrivateBalance } from '../BlueComponents';
|
||||
import { FiatUnit } from '../models/fiatUnit';
|
||||
import { TWallet } from '../class/wallets/types';
|
||||
import { BlurredBalanceView } from './BlurredBalanceView';
|
||||
|
||||
interface TransactionsNavigationHeaderProps {
|
||||
wallet: TWallet;
|
||||
@ -20,7 +19,8 @@ interface TransactionsNavigationHeaderProps {
|
||||
navigate: (route: string, params?: any) => void;
|
||||
goBack: () => void;
|
||||
};
|
||||
onManageFundsPressed?: (id: string) => void; // Add a type definition for this prop
|
||||
onManageFundsPressed?: (id: string) => void;
|
||||
onWalletBalanceVisibilityChange?: (isShouldBeVisible: boolean) => void;
|
||||
actionKeys: {
|
||||
CopyToClipboard: 'copyToClipboard';
|
||||
WalletBalanceVisibility: 'walletBalanceVisibility';
|
||||
@ -38,11 +38,13 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
||||
navigation,
|
||||
// @ts-ignore: Ugh
|
||||
onManageFundsPressed,
|
||||
// @ts-ignore: Ugh
|
||||
onWalletBalanceVisibilityChange,
|
||||
}) => {
|
||||
const [wallet, setWallet] = useState(initialWallet);
|
||||
const [allowOnchainAddress, setAllowOnchainAddress] = useState(false);
|
||||
|
||||
const { preferredFiatCurrency, saveToDisk } = useContext(BlueStorageContext);
|
||||
const { preferredFiatCurrency } = useContext(BlueStorageContext);
|
||||
const menuRef = useRef(null);
|
||||
|
||||
const verifyIfWalletAllowsOnchainAddress = useCallback(() => {
|
||||
@ -72,23 +74,8 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
||||
}
|
||||
};
|
||||
|
||||
const updateWalletVisibility = (w: TWallet, newHideBalance: boolean) => {
|
||||
w.hideBalance = newHideBalance;
|
||||
return w;
|
||||
};
|
||||
|
||||
const handleBalanceVisibility = async () => {
|
||||
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
|
||||
|
||||
if (isBiometricsEnabled && wallet.hideBalance) {
|
||||
if (!(await Biometric.unlockWithBiometrics())) {
|
||||
return navigation.goBack();
|
||||
}
|
||||
}
|
||||
|
||||
const updatedWallet = updateWalletVisibility(wallet, !wallet.hideBalance);
|
||||
setWallet(updatedWallet);
|
||||
saveToDisk();
|
||||
const handleBalanceVisibility = () => {
|
||||
onWalletBalanceVisibilityChange?.(!wallet.hideBalance);
|
||||
};
|
||||
|
||||
const updateWalletWithNewUnit = (w: TWallet, newPreferredUnit: BitcoinUnit) => {
|
||||
@ -109,6 +96,8 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
||||
newWalletPreferredUnit = BitcoinUnit.BTC;
|
||||
}
|
||||
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
|
||||
const updatedWallet = updateWalletWithNewUnit(wallet, newWalletPreferredUnit);
|
||||
setWallet(updatedWallet);
|
||||
onWalletUnitChange?.(updatedWallet);
|
||||
@ -136,8 +125,11 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
||||
const balance = useMemo(() => {
|
||||
const hideBalance = wallet.hideBalance;
|
||||
const balanceUnit = wallet.getPreferredBalanceUnit();
|
||||
const balanceFormatted = formatBalance(wallet.getBalance(), balanceUnit, true);
|
||||
return !hideBalance && balanceFormatted?.toString();
|
||||
const balanceFormatted =
|
||||
balanceUnit === BitcoinUnit.LOCAL_CURRENCY
|
||||
? formatBalance(wallet.getBalance(), balanceUnit, true)
|
||||
: formatBalanceWithoutSuffix(wallet.getBalance(), balanceUnit, true);
|
||||
return !hideBalance && balanceFormatted;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [wallet.hideBalance, wallet.getPreferredBalanceUnit()]);
|
||||
|
||||
@ -163,68 +155,82 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
||||
style={styles.chainIcon}
|
||||
/>
|
||||
|
||||
<Text testID="WalletLabel" numberOfLines={1} style={styles.walletLabel}>
|
||||
<Text testID="WalletLabel" numberOfLines={1} style={styles.walletLabel} selectable>
|
||||
{wallet.getLabel()}
|
||||
</Text>
|
||||
<ToolTipMenu
|
||||
enableAndroidRipple={false}
|
||||
onPress={changeWalletBalanceUnit}
|
||||
ref={menuRef}
|
||||
title={`${loc.wallets.balance} (${
|
||||
wallet.getPreferredBalanceUnit() === BitcoinUnit.LOCAL_CURRENCY
|
||||
? preferredFiatCurrency?.endPointKey ?? FiatUnit.USD
|
||||
: wallet.getPreferredBalanceUnit()
|
||||
})`}
|
||||
onPressMenuItem={onPressMenuItem}
|
||||
actions={
|
||||
wallet.hideBalance
|
||||
? [
|
||||
{
|
||||
id: 'walletBalanceVisibility',
|
||||
text: loc.transactions.details_balance_show,
|
||||
icon: {
|
||||
iconType: 'SYSTEM',
|
||||
iconValue: 'eye',
|
||||
<View style={styles.walletBalanceAndUnitContainer}>
|
||||
<ToolTipMenu
|
||||
isMenuPrimaryAction
|
||||
isButton
|
||||
enableAndroidRipple={false}
|
||||
ref={menuRef}
|
||||
buttonStyle={styles.walletBalance}
|
||||
title={`${loc.wallets.balance} (${
|
||||
wallet.getPreferredBalanceUnit() === BitcoinUnit.LOCAL_CURRENCY
|
||||
? preferredFiatCurrency?.endPointKey ?? FiatUnit.USD
|
||||
: wallet.getPreferredBalanceUnit()
|
||||
})`}
|
||||
onPressMenuItem={onPressMenuItem}
|
||||
actions={
|
||||
wallet.hideBalance
|
||||
? [
|
||||
{
|
||||
id: 'walletBalanceVisibility',
|
||||
text: loc.transactions.details_balance_show,
|
||||
icon: {
|
||||
iconType: 'SYSTEM',
|
||||
iconValue: 'eye',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
id: 'walletBalanceVisibility',
|
||||
text: loc.transactions.details_balance_hide,
|
||||
icon: {
|
||||
iconType: 'SYSTEM',
|
||||
iconValue: 'eye.slash',
|
||||
]
|
||||
: [
|
||||
{
|
||||
id: 'walletBalanceVisibility',
|
||||
text: loc.transactions.details_balance_hide,
|
||||
icon: {
|
||||
iconType: 'SYSTEM',
|
||||
iconValue: 'eye.slash',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'copyToClipboard',
|
||||
text: loc.transactions.details_copy,
|
||||
icon: {
|
||||
iconType: 'SYSTEM',
|
||||
iconValue: 'doc.on.doc',
|
||||
{
|
||||
id: 'copyToClipboard',
|
||||
text: loc.transactions.details_copy,
|
||||
icon: {
|
||||
iconType: 'SYSTEM',
|
||||
iconValue: 'doc.on.doc',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View style={styles.walletBalance}>
|
||||
{wallet.hideBalance ? (
|
||||
<BluePrivateBalance />
|
||||
) : (
|
||||
<Text
|
||||
testID="WalletBalance"
|
||||
// @ts-ignore: Ugh
|
||||
key={balance} // force component recreation on balance change. To fix right-to-left languages, like Farsi
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
style={styles.walletBalance}
|
||||
>
|
||||
{balance}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</ToolTipMenu>
|
||||
]
|
||||
}
|
||||
>
|
||||
<View style={styles.walletBalance}>
|
||||
{wallet.hideBalance ? (
|
||||
<BlurredBalanceView />
|
||||
) : (
|
||||
<TouchableOpacity>
|
||||
<Text
|
||||
testID="WalletBalance"
|
||||
// @ts-ignore: Ugh
|
||||
key={balance} // force component recreation on balance change. To fix right-to-left languages, like Farsi
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
style={styles.walletBalanceText}
|
||||
ellipsizeMode="middle"
|
||||
>
|
||||
{balance}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</ToolTipMenu>
|
||||
<TouchableOpacity style={styles.walletPreferredUnitView} onPress={changeWalletBalanceUnit}>
|
||||
<Text style={styles.walletPreferredUnitText}>
|
||||
{wallet.getPreferredBalanceUnit() === BitcoinUnit.LOCAL_CURRENCY
|
||||
? preferredFiatCurrency?.endPointKey ?? FiatUnit.USD
|
||||
: wallet.getPreferredBalanceUnit()}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{wallet.type === LightningCustodianWallet.type && allowOnchainAddress && (
|
||||
<ToolTipMenu
|
||||
isMenuPrimaryAction
|
||||
@ -291,13 +297,11 @@ const styles = StyleSheet.create({
|
||||
fontSize: 19,
|
||||
color: '#fff',
|
||||
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
|
||||
marginBottom: 10,
|
||||
},
|
||||
walletBalance: {
|
||||
backgroundColor: 'transparent',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 36,
|
||||
color: '#fff',
|
||||
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
|
||||
flexShrink: 1,
|
||||
marginRight: 6,
|
||||
},
|
||||
manageFundsButton: {
|
||||
marginTop: 14,
|
||||
@ -315,6 +319,31 @@ const styles = StyleSheet.create({
|
||||
color: '#FFFFFF',
|
||||
padding: 12,
|
||||
},
|
||||
walletBalanceAndUnitContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingRight: 10, // Ensure there's some padding to the right
|
||||
},
|
||||
walletBalanceText: {
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 36,
|
||||
flexShrink: 1, // Allow the text to shrink if there's not enough space
|
||||
},
|
||||
walletPreferredUnitView: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
||||
borderRadius: 8,
|
||||
paddingVertical: 5,
|
||||
minHeight: 35,
|
||||
minWidth: 65,
|
||||
padding: 10, // Adjust padding as needed to fit the content
|
||||
},
|
||||
walletPreferredUnitText: {
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export const actionKeys = {
|
||||
|
@ -19,10 +19,11 @@ import LinearGradient from 'react-native-linear-gradient';
|
||||
import loc, { formatBalance, transactionTimeToReadable } from '../loc';
|
||||
import { LightningCustodianWallet, LightningLdkWallet, MultisigHDWallet } from '../class';
|
||||
import WalletGradient from '../class/wallet-gradient';
|
||||
import { BluePrivateBalance } from '../BlueComponents';
|
||||
import { BlueSpacing10 } from '../BlueComponents';
|
||||
import { BlueStorageContext, WalletTransactionsStatus } from '../blue_modules/storage-context';
|
||||
import { isTablet, isDesktop } from '../blue_modules/environment';
|
||||
import { useTheme } from './themes';
|
||||
import { BlurredBalanceView } from './BlurredBalanceView';
|
||||
|
||||
const nStyles = StyleSheet.create({
|
||||
container: {
|
||||
@ -218,7 +219,10 @@ export const WalletCarouselItem = ({ item, _, onPress, handleLongPress, isSelect
|
||||
{item.getLabel()}
|
||||
</Text>
|
||||
{item.hideBalance ? (
|
||||
<BluePrivateBalance />
|
||||
<>
|
||||
<BlueSpacing10 />
|
||||
<BlurredBalanceView />
|
||||
</>
|
||||
) : (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
|
@ -12,6 +12,7 @@ import presentAlert from '../../components/Alert';
|
||||
import { useTheme } from '../../components/themes';
|
||||
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
|
||||
import CopyToClipboardButton from '../../components/CopyToClipboardButton';
|
||||
import { BitcoinUnit } from '../../models/bitcoinUnits';
|
||||
const dayjs = require('dayjs');
|
||||
|
||||
function onlyUnique(value, index, self) {
|
||||
@ -262,7 +263,7 @@ const TransactionsDetails = () => {
|
||||
{tx.fee && (
|
||||
<>
|
||||
<BlueText style={styles.rowCaption}>{loc.send.create_fee}</BlueText>
|
||||
<BlueText style={styles.rowValue}>{tx.fee + ' sats'}</BlueText>
|
||||
<BlueText style={styles.rowValue}>{tx.fee + ` ${BitcoinUnit.SATS}`}</BlueText>
|
||||
<View style={styles.marginBottom18} />
|
||||
</>
|
||||
)}
|
||||
|
@ -271,6 +271,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
borderRadius6: {
|
||||
borderRadius: 6,
|
||||
minHeight: 54,
|
||||
},
|
||||
buttonContainer: {
|
||||
padding: 24,
|
||||
|
@ -3,7 +3,7 @@ import { View, ActivityIndicator, Image, Text, TouchableOpacity, I18nManager, Fl
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import { useRoute, useNavigation, useNavigationState } from '@react-navigation/native';
|
||||
|
||||
import { BlueText, BlueSpacing20, BluePrivateBalance } from '../../BlueComponents';
|
||||
import { BlueText, BlueSpacing20 } from '../../BlueComponents';
|
||||
import navigationStyle from '../../components/navigationStyle';
|
||||
import WalletGradient from '../../class/wallet-gradient';
|
||||
import loc, { formatBalance, transactionTimeToReadable } from '../../loc';
|
||||
@ -13,6 +13,7 @@ import { useTheme } from '../../components/themes';
|
||||
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
|
||||
import SafeArea from '../../components/SafeArea';
|
||||
import { Chain } from '../../models/bitcoinUnits';
|
||||
import { BlurredBalanceView } from '../../components/BlurredBalanceView';
|
||||
|
||||
const SelectWallet = () => {
|
||||
const { chainType, onWalletSelect, availableWallets, noWalletExplanationText, onChainRequireSend = false } = useRoute().params;
|
||||
@ -167,7 +168,7 @@ const SelectWallet = () => {
|
||||
{item.getLabel()}
|
||||
</Text>
|
||||
{item.hideBalance ? (
|
||||
<BluePrivateBalance />
|
||||
<BlurredBalanceView />
|
||||
) : (
|
||||
<Text numberOfLines={1} adjustsFontSizeToFit style={styles.balance}>
|
||||
{formatBalance(Number(item.getBalance()), item.getPreferredBalanceUnit(), true)}
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
findNodeHandle,
|
||||
LayoutAnimation,
|
||||
} from 'react-native';
|
||||
import { Icon } from 'react-native-elements';
|
||||
|
||||
@ -38,6 +39,7 @@ import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
|
||||
import loc from '../../loc';
|
||||
import { Chain } from '../../models/bitcoinUnits';
|
||||
import ActionSheet from '../ActionSheet';
|
||||
import Biometric from '../../class/biometrics';
|
||||
|
||||
const BlueElectrum = require('../../blue_modules/BlueElectrum');
|
||||
|
||||
@ -511,6 +513,20 @@ const WalletTransactions = ({ navigation }) => {
|
||||
saveToDisk();
|
||||
})
|
||||
}
|
||||
onWalletBalanceVisibilityChange={async isShouldBeVisible => {
|
||||
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
|
||||
|
||||
if (wallet.hideBalance && isBiometricsEnabled) {
|
||||
const unlocked = await Biometric.unlockWithBiometrics();
|
||||
if (!unlocked) {
|
||||
throw new Error('Biometrics failed');
|
||||
}
|
||||
}
|
||||
|
||||
wallet.hideBalance = isShouldBeVisible;
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
await saveToDisk();
|
||||
}}
|
||||
onManageFundsPressed={id => {
|
||||
if (wallet.type === MultisigHDWallet.type) {
|
||||
navigateToViewEditCosigners();
|
||||
|
@ -18,7 +18,7 @@ beforeAll(async () => {
|
||||
await device.launchApp({ delete: true });
|
||||
|
||||
console.log('before all - importing bip48...');
|
||||
await helperImportWallet(process.env.HD_MNEMONIC_BIP84, 'HDsegwitBech32', 'Imported HD SegWit (BIP84 Bech32 Native)', '0.00105526 BTC');
|
||||
await helperImportWallet(process.env.HD_MNEMONIC_BIP84, 'HDsegwitBech32', 'Imported HD SegWit (BIP84 Bech32 Native)', '0.00105526');
|
||||
console.log('...imported!');
|
||||
await device.pressBack();
|
||||
await sleep(15000);
|
||||
|
@ -26,7 +26,7 @@ describe('BlueWallet UI Tests - import Watch-only wallet (zpub)', () => {
|
||||
'zpub6s2EvLxwvDpaHNVP5vfordTyi8cH1fR8usmEjz7RsSQjfTTGU2qA5VEcEyYYBxpZAyBarJoTraB4VRJKVz97Au9jRNYfLAeeHC5UnRZbz8Y',
|
||||
'watchOnly',
|
||||
'Imported Watch-only',
|
||||
'0.0001 BTC',
|
||||
'0.0001',
|
||||
);
|
||||
await sleep(15000);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user