mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-05 09:01:11 +01:00
[PS-1116] Improved network error handling (#2007)
* PS-1116 Improved network error handling on ViewPageViewModel and AddEditPageViewModel * PS-1116 Renamed ViewPage and AddEditPage pages to a more explicit name. Refactored CheckPassword from the CipherPages to a single Base class. * PS-1116 Updated variables relative to the AddEditPage and ViewPage refactor to CipherAddEditPage and CipherDetailPage * Renamed CipherDetailPage to CipherDetailsPage * Code improvement * PS-1116 Improved code formatting * PS-1116 Improved formatting * PS-1116 Improved code formatting
This commit is contained in:
parent
90a6850d76
commit
8ec6545bbc
@ -97,11 +97,11 @@
|
||||
<Compile Update="Pages\Vault\PasswordHistoryPage.xaml.cs">
|
||||
<DependentUpon>PasswordHistoryPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Vault\AddEditPage.xaml.cs">
|
||||
<DependentUpon>AddEditPage.xaml</DependentUpon>
|
||||
<Compile Update="Pages\Vault\CipherDetailsPage.xaml.cs">
|
||||
<DependentUpon>CipherDetailsPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Vault\ViewPage.xaml.cs">
|
||||
<DependentUpon>ViewPage.xaml</DependentUpon>
|
||||
<Compile Update="Pages\Vault\CipherAddEditPage.xaml.cs">
|
||||
<DependentUpon>CipherAddEditPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Settings\SettingsPage\SettingsPage.xaml.cs">
|
||||
<DependentUpon>SettingsPage.xaml</DependentUpon>
|
||||
|
@ -330,20 +330,20 @@ namespace Bit.App
|
||||
var topPage = tabbedPage.Navigation.ModalStack[tabbedPage.Navigation.ModalStack.Count - 1];
|
||||
if (topPage is NavigationPage navPage)
|
||||
{
|
||||
if (navPage.CurrentPage is ViewPage viewPage)
|
||||
if (navPage.CurrentPage is CipherDetailsPage cipherDetailsPage)
|
||||
{
|
||||
lastPageBeforeLock = new PreviousPageInfo
|
||||
{
|
||||
Page = "view",
|
||||
CipherId = viewPage.ViewModel.CipherId
|
||||
CipherId = cipherDetailsPage.ViewModel.CipherId
|
||||
};
|
||||
}
|
||||
else if (navPage.CurrentPage is AddEditPage addEditPage && addEditPage.ViewModel.EditMode)
|
||||
else if (navPage.CurrentPage is CipherAddEditPage cipherAddEditPage && cipherAddEditPage.ViewModel.EditMode)
|
||||
{
|
||||
lastPageBeforeLock = new PreviousPageInfo
|
||||
{
|
||||
Page = "edit",
|
||||
CipherId = addEditPage.ViewModel.CipherId
|
||||
CipherId = cipherAddEditPage.ViewModel.CipherId
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -378,7 +378,7 @@ namespace Bit.App
|
||||
Current.MainPage = new TabsPage(Options);
|
||||
break;
|
||||
case NavigationTarget.AddEditCipher:
|
||||
Current.MainPage = new NavigationPage(new AddEditPage(appOptions: Options));
|
||||
Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: Options));
|
||||
break;
|
||||
case NavigationTarget.AutofillCiphers:
|
||||
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
||||
|
@ -137,11 +137,11 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (_appOptions.FillType.HasValue && _appOptions.FillType != CipherType.Login)
|
||||
{
|
||||
var pageForOther = new AddEditPage(type: _appOptions.FillType, fromAutofill: true);
|
||||
var pageForOther = new CipherAddEditPage(type: _appOptions.FillType, fromAutofill: true);
|
||||
await Navigation.PushModalAsync(new NavigationPage(pageForOther));
|
||||
return;
|
||||
}
|
||||
var pageForLogin = new AddEditPage(null, CipherType.Login, uri: _vm.Uri, name: _vm.Name,
|
||||
var pageForLogin = new CipherAddEditPage(null, CipherType.Login, uri: _vm.Uri, name: _vm.Name,
|
||||
fromAutofill: true);
|
||||
await Navigation.PushModalAsync(new NavigationPage(pageForLogin));
|
||||
}
|
||||
|
76
src/App/Pages/Vault/BaseCipherViewModel.cs
Normal file
76
src/App/Pages/Vault/BaseCipherViewModel.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public abstract class BaseCipherViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IAuditService _auditService;
|
||||
protected readonly IDeviceActionService _deviceActionService;
|
||||
protected readonly ILogger _logger;
|
||||
protected readonly IPlatformUtilsService _platformUtilsService;
|
||||
private CipherView _cipher;
|
||||
protected abstract string[] AdditionalPropertiesToRaiseOnCipherChanged { get; }
|
||||
|
||||
public BaseCipherViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
CheckPasswordCommand = new AsyncCommand(CheckPasswordAsync, allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public CipherView Cipher
|
||||
{
|
||||
get => _cipher;
|
||||
set => SetProperty(ref _cipher, value, additionalPropertyNames: AdditionalPropertiesToRaiseOnCipherChanged);
|
||||
}
|
||||
|
||||
public AsyncCommand CheckPasswordCommand { get; }
|
||||
|
||||
protected async Task CheckPasswordAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Cipher?.Login?.Password))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword);
|
||||
var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
await _platformUtilsService.ShowDialogAsync(matches > 0
|
||||
? string.Format(AppResources.PasswordExposed, matches.ToString("N0"))
|
||||
: AppResources.PasswordSafe);
|
||||
}
|
||||
catch (ApiException apiException)
|
||||
{
|
||||
_logger.Exception(apiException);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (apiException?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(apiException.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.AddEditPage"
|
||||
x:Class="Bit.App.Pages.CipherAddEditPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
@ -10,11 +10,11 @@
|
||||
xmlns:behaviors="clr-namespace:Bit.App.Behaviors"
|
||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||
x:DataType="pages:AddEditPageViewModel"
|
||||
x:DataType="pages:CipherAddEditPageViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:AddEditPageViewModel />
|
||||
<pages:CipherAddEditPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
@ -608,7 +608,7 @@
|
||||
</StackLayout>
|
||||
<controls:RepeaterView ItemsSource="{Binding Fields}">
|
||||
<controls:RepeaterView.ItemTemplate>
|
||||
<DataTemplate x:DataType="pages:AddEditPageFieldViewModel">
|
||||
<DataTemplate x:DataType="pages:CipherAddEditPageFieldViewModel">
|
||||
<StackLayout Spacing="0" Padding="0">
|
||||
<Grid StyleClass="box-row, box-row-input">
|
||||
<Grid.RowDefinitions>
|
@ -14,7 +14,7 @@ using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AddEditPage : BaseContentPage
|
||||
public partial class CipherAddEditPage : BaseContentPage
|
||||
{
|
||||
private readonly AppOptions _appOptions;
|
||||
private readonly IStateService _stateService;
|
||||
@ -22,10 +22,10 @@ namespace Bit.App.Pages
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly IKeyConnectorService _keyConnectorService;
|
||||
|
||||
private AddEditPageViewModel _vm;
|
||||
private CipherAddEditPageViewModel _vm;
|
||||
private bool _fromAutofill;
|
||||
|
||||
public AddEditPage(
|
||||
public CipherAddEditPage(
|
||||
string cipherId = null,
|
||||
CipherType? type = null,
|
||||
string folderId = null,
|
||||
@ -36,7 +36,7 @@ namespace Bit.App.Pages
|
||||
bool fromAutofill = false,
|
||||
AppOptions appOptions = null,
|
||||
bool cloneMode = false,
|
||||
ViewPage viewPage = null)
|
||||
CipherDetailsPage cipherDetailsPage = null)
|
||||
{
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
@ -47,7 +47,7 @@ namespace Bit.App.Pages
|
||||
_fromAutofill = fromAutofill;
|
||||
FromAutofillFramework = _appOptions?.FromAutofillFramework ?? false;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as AddEditPageViewModel;
|
||||
_vm = BindingContext as CipherAddEditPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.CipherId = cipherId;
|
||||
_vm.FolderId = folderId == "none" ? null : folderId;
|
||||
@ -57,7 +57,7 @@ namespace Bit.App.Pages
|
||||
_vm.DefaultName = name ?? appOptions?.SaveName;
|
||||
_vm.DefaultUri = uri ?? appOptions?.Uri;
|
||||
_vm.CloneMode = cloneMode;
|
||||
_vm.ViewPage = viewPage;
|
||||
_vm.CipherDetailsPage = cipherDetailsPage;
|
||||
_vm.Init();
|
||||
SetActivityIndicator();
|
||||
if (_vm.EditMode && !_vm.CloneMode && Device.RuntimePlatform == Device.Android)
|
||||
@ -145,7 +145,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
|
||||
public bool FromAutofillFramework { get; set; }
|
||||
public AddEditPageViewModel ViewModel => _vm;
|
||||
public CipherAddEditPageViewModel ViewModel => _vm;
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
@ -15,22 +14,17 @@ using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class AddEditPageViewModel : BaseViewModel
|
||||
public class CipherAddEditPageViewModel : BaseCipherViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly ICollectionService _collectionService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private CipherView _cipher;
|
||||
private bool _showNotesSeparator;
|
||||
private bool _showPassword;
|
||||
private bool _showCardNumber;
|
||||
@ -44,7 +38,7 @@ namespace Bit.App.Pages
|
||||
private bool _hasCollections;
|
||||
private string _previousCipherId;
|
||||
private List<Core.Models.View.CollectionView> _writeableCollections;
|
||||
private string[] _additionalCipherProperties = new string[]
|
||||
protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[]
|
||||
{
|
||||
nameof(IsLogin),
|
||||
nameof(IsIdentity),
|
||||
@ -54,6 +48,7 @@ namespace Bit.App.Pages
|
||||
nameof(ShowAttachments),
|
||||
nameof(ShowCollections),
|
||||
};
|
||||
|
||||
private List<KeyValuePair<UriMatchType?, string>> _matchDetectionOptions =
|
||||
new List<KeyValuePair<UriMatchType?, string>>
|
||||
{
|
||||
@ -66,31 +61,26 @@ namespace Bit.App.Pages
|
||||
new KeyValuePair<UriMatchType?, string>(UriMatchType.Never, AppResources.Never)
|
||||
};
|
||||
|
||||
public AddEditPageViewModel()
|
||||
public CipherAddEditPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
_folderService = ServiceContainer.Resolve<IFolderService>("folderService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
|
||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
GeneratePasswordCommand = new Command(GeneratePassword);
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleCardNumberCommand = new Command(ToggleCardNumber);
|
||||
ToggleCardCodeCommand = new Command(ToggleCardCode);
|
||||
CheckPasswordCommand = new Command(CheckPasswordAsync);
|
||||
UriOptionsCommand = new Command<LoginUriView>(UriOptions);
|
||||
FieldOptionsCommand = new Command<AddEditPageFieldViewModel>(FieldOptions);
|
||||
FieldOptionsCommand = new Command<CipherAddEditPageFieldViewModel>(FieldOptions);
|
||||
PasswordPromptHelpCommand = new Command(PasswordPromptHelp);
|
||||
Uris = new ExtendedObservableCollection<LoginUriView>();
|
||||
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
|
||||
Fields = new ExtendedObservableCollection<CipherAddEditPageFieldViewModel>();
|
||||
Collections = new ExtendedObservableCollection<CollectionViewModel>();
|
||||
AllowPersonal = true;
|
||||
|
||||
@ -146,7 +136,6 @@ namespace Bit.App.Pages
|
||||
public Command TogglePasswordCommand { get; set; }
|
||||
public Command ToggleCardNumberCommand { get; set; }
|
||||
public Command ToggleCardCodeCommand { get; set; }
|
||||
public Command CheckPasswordCommand { get; set; }
|
||||
public Command UriOptionsCommand { get; set; }
|
||||
public Command FieldOptionsCommand { get; set; }
|
||||
public Command PasswordPromptHelpCommand { get; set; }
|
||||
@ -164,7 +153,7 @@ namespace Bit.App.Pages
|
||||
public List<KeyValuePair<string, string>> FolderOptions { get; set; }
|
||||
public List<KeyValuePair<string, string>> OwnershipOptions { get; set; }
|
||||
public ExtendedObservableCollection<LoginUriView> Uris { get; set; }
|
||||
public ExtendedObservableCollection<AddEditPageFieldViewModel> Fields { get; set; }
|
||||
public ExtendedObservableCollection<CipherAddEditPageFieldViewModel> Fields { get; set; }
|
||||
public ExtendedObservableCollection<CollectionViewModel> Collections { get; set; }
|
||||
|
||||
public int TypeSelectedIndex
|
||||
@ -233,11 +222,6 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
}
|
||||
public CipherView Cipher
|
||||
{
|
||||
get => _cipher;
|
||||
set => SetProperty(ref _cipher, value, additionalPropertyNames: _additionalCipherProperties);
|
||||
}
|
||||
public bool ShowNotesSeparator
|
||||
{
|
||||
get => _showNotesSeparator;
|
||||
@ -285,7 +269,7 @@ namespace Bit.App.Pages
|
||||
public bool ShowOwnershipOptions => !EditMode || CloneMode;
|
||||
public bool OwnershipPolicyInEffect => ShowOwnershipOptions && !AllowPersonal;
|
||||
public bool CloneMode { get; set; }
|
||||
public ViewPage ViewPage { get; set; }
|
||||
public CipherDetailsPage CipherDetailsPage { get; set; }
|
||||
public bool IsLogin => Cipher?.Type == CipherType.Login;
|
||||
public bool IsIdentity => Cipher?.Type == CipherType.Identity;
|
||||
public bool IsCard => Cipher?.Type == CipherType.Card;
|
||||
@ -421,7 +405,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (Cipher.Fields != null)
|
||||
{
|
||||
Fields.ResetWithRange(Cipher.Fields?.Select(f => new AddEditPageFieldViewModel(Cipher, f)));
|
||||
Fields.ResetWithRange(Cipher.Fields?.Select(f => new CipherAddEditPageFieldViewModel(Cipher, f)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -509,7 +493,7 @@ namespace Bit.App.Pages
|
||||
EditMode && !CloneMode ? AppResources.ItemUpdated : AppResources.NewItemCreated);
|
||||
_messagingService.Send(EditMode && !CloneMode ? "editedCipher" : "addedCipher", Cipher.Id);
|
||||
|
||||
if (Page is AddEditPage page && page.FromAutofillFramework)
|
||||
if (Page is CipherAddEditPage page && page.FromAutofillFramework)
|
||||
{
|
||||
// Close and go back to app
|
||||
_deviceActionService.CloseAutofill();
|
||||
@ -518,7 +502,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (CloneMode)
|
||||
{
|
||||
ViewPage?.UpdateCipherId(this.Cipher.Id);
|
||||
CipherDetailsPage?.UpdateCipherId(this.Cipher.Id);
|
||||
}
|
||||
// if the app is tombstoned then PopModalAsync would throw index out of bounds
|
||||
if (Page.Navigation?.ModalStack?.Count > 0)
|
||||
@ -603,7 +587,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public async void UriOptions(LoginUriView uri)
|
||||
{
|
||||
if (!(Page as AddEditPage).DoOnce())
|
||||
if (!(Page as CipherAddEditPage).DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -639,9 +623,9 @@ namespace Bit.App.Pages
|
||||
Uris.Add(new LoginUriView());
|
||||
}
|
||||
|
||||
public async void FieldOptions(AddEditPageFieldViewModel field)
|
||||
public async void FieldOptions(CipherAddEditPageFieldViewModel field)
|
||||
{
|
||||
if (!(Page as AddEditPage).DoOnce())
|
||||
if (!(Page as CipherAddEditPage).DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -701,10 +685,10 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (Fields == null)
|
||||
{
|
||||
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
|
||||
Fields = new ExtendedObservableCollection<CipherAddEditPageFieldViewModel>();
|
||||
}
|
||||
var type = fieldTypeOptions.FirstOrDefault(f => f.Value == typeSelection).Key;
|
||||
Fields.Add(new AddEditPageFieldViewModel(Cipher, new FieldView
|
||||
Fields.Add(new CipherAddEditPageFieldViewModel(Cipher, new FieldView
|
||||
{
|
||||
Type = type,
|
||||
Name = string.IsNullOrWhiteSpace(name) ? null : name,
|
||||
@ -832,35 +816,11 @@ namespace Bit.App.Pages
|
||||
|
||||
private void TriggerCipherChanged()
|
||||
{
|
||||
TriggerPropertyChanged(nameof(Cipher), _additionalCipherProperties);
|
||||
}
|
||||
|
||||
private async void CheckPasswordAsync()
|
||||
{
|
||||
if (!(Page as BaseContentPage).DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Cipher.Login?.Password))
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword);
|
||||
var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (matches > 0)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.PasswordExposed,
|
||||
matches.ToString("N0")));
|
||||
}
|
||||
else
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe);
|
||||
}
|
||||
TriggerPropertyChanged(nameof(Cipher), AdditionalPropertiesToRaiseOnCipherChanged);
|
||||
}
|
||||
}
|
||||
|
||||
public class AddEditPageFieldViewModel : ExtendedViewModel
|
||||
public class CipherAddEditPageFieldViewModel : ExtendedViewModel
|
||||
{
|
||||
private II18nService _i18nService;
|
||||
private FieldView _field;
|
||||
@ -876,7 +836,7 @@ namespace Bit.App.Pages
|
||||
nameof(IsLinkedType),
|
||||
};
|
||||
|
||||
public AddEditPageFieldViewModel(CipherView cipher, FieldView field)
|
||||
public CipherAddEditPageFieldViewModel(CipherView cipher, FieldView field)
|
||||
{
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_cipher = cipher;
|
@ -2,18 +2,18 @@
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.ViewPage"
|
||||
x:Class="Bit.App.Pages.CipherDetailsPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||
xmlns:views="clr-namespace:Bit.Core.Models.View;assembly=BitwardenCore"
|
||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||
x:DataType="pages:ViewPageViewModel"
|
||||
x:DataType="pages:CipherDetailsPageViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:ViewPageViewModel />
|
||||
<pages:CipherDetailsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
@ -540,7 +540,7 @@
|
||||
</StackLayout>
|
||||
<controls:RepeaterView ItemsSource="{Binding Fields}">
|
||||
<controls:RepeaterView.ItemTemplate>
|
||||
<DataTemplate x:DataType="pages:ViewPageFieldViewModel">
|
||||
<DataTemplate x:DataType="pages:CipherDetailsPageFieldViewModel">
|
||||
<StackLayout Spacing="0" Padding="0">
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
@ -9,18 +9,18 @@ using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class ViewPage : BaseContentPage
|
||||
public partial class CipherDetailsPage : BaseContentPage
|
||||
{
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly ISyncService _syncService;
|
||||
private ViewPageViewModel _vm;
|
||||
private CipherDetailsPageViewModel _vm;
|
||||
|
||||
public ViewPage(string cipherId)
|
||||
public CipherDetailsPage(string cipherId)
|
||||
{
|
||||
InitializeComponent();
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_vm = BindingContext as ViewPageViewModel;
|
||||
_vm = BindingContext as CipherDetailsPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.CipherId = cipherId;
|
||||
SetActivityIndicator(_mainContent);
|
||||
@ -40,7 +40,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public ViewPageViewModel ViewModel => _vm;
|
||||
public CipherDetailsPageViewModel ViewModel => _vm;
|
||||
|
||||
public void UpdateCipherId(string cipherId)
|
||||
{
|
||||
@ -55,7 +55,7 @@ namespace Bit.App.Pages
|
||||
IsBusy = true;
|
||||
}
|
||||
|
||||
_broadcasterService.Subscribe(nameof(ViewPage), async (message) =>
|
||||
_broadcasterService.Subscribe(nameof(CipherDetailsPage), async (message) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -111,7 +111,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
base.OnDisappearing();
|
||||
IsBusy = false;
|
||||
_broadcasterService.Unsubscribe(nameof(ViewPage));
|
||||
_broadcasterService.Unsubscribe(nameof(CipherDetailsPage));
|
||||
_vm.CleanUp();
|
||||
}
|
||||
|
||||
@ -140,7 +140,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
return;
|
||||
}
|
||||
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_vm.CipherId)));
|
||||
await Navigation.PushModalAsync(new NavigationPage(new CipherAddEditPage(_vm.CipherId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -212,7 +212,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
return;
|
||||
}
|
||||
var page = new AddEditPage(_vm.CipherId, cloneMode: true, viewPage: this);
|
||||
var page = new CipherAddEditPage(_vm.CipherId, cloneMode: true, cipherDetailsPage: this);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
@ -267,7 +267,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else if (selection == AppResources.Clone)
|
||||
{
|
||||
var page = new AddEditPage(_vm.CipherId, cloneMode: true, viewPage: this);
|
||||
var page = new CipherAddEditPage(_vm.CipherId, cloneMode: true, cipherDetailsPage: this);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
@ -17,23 +17,18 @@ using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class ViewPageViewModel : BaseViewModel
|
||||
public class CipherDetailsPageViewModel : BaseCipherViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ITotpService _totpService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IPasswordRepromptService _passwordRepromptService;
|
||||
private readonly ILocalizeService _localizeService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private CipherView _cipher;
|
||||
private List<ViewPageFieldViewModel> _fields;
|
||||
private List<CipherDetailsPageFieldViewModel> _fields;
|
||||
private bool _canAccessPremium;
|
||||
private bool _showPassword;
|
||||
private bool _showCardNumber;
|
||||
@ -48,20 +43,16 @@ namespace Bit.App.Pages
|
||||
private string _attachmentFilename;
|
||||
private bool _passwordReprompted;
|
||||
|
||||
public ViewPageViewModel()
|
||||
public CipherDetailsPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
CopyCommand = new AsyncCommand<string>((id) => CopyAsync(id, null), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||
CopyUriCommand = new AsyncCommand<LoginUriView>(uriView => CopyAsync("LoginUri", uriView.Uri), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||
@ -70,8 +61,7 @@ namespace Bit.App.Pages
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleCardNumberCommand = new Command(ToggleCardNumber);
|
||||
ToggleCardCodeCommand = new Command(ToggleCardCode);
|
||||
CheckPasswordCommand = new Command(CheckPasswordAsync);
|
||||
DownloadAttachmentCommand = new Command<AttachmentView>(DownloadAttachmentAsync);
|
||||
DownloadAttachmentCommand = new AsyncCommand<AttachmentView>(DownloadAttachmentAsync, allowsMultipleExecutions: false);
|
||||
|
||||
PageTitle = AppResources.ViewItem;
|
||||
}
|
||||
@ -83,32 +73,26 @@ namespace Bit.App.Pages
|
||||
public Command TogglePasswordCommand { get; set; }
|
||||
public Command ToggleCardNumberCommand { get; set; }
|
||||
public Command ToggleCardCodeCommand { get; set; }
|
||||
public Command CheckPasswordCommand { get; set; }
|
||||
public Command DownloadAttachmentCommand { get; set; }
|
||||
public AsyncCommand<AttachmentView> DownloadAttachmentCommand { get; set; }
|
||||
public string CipherId { get; set; }
|
||||
public CipherView Cipher
|
||||
protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[]
|
||||
{
|
||||
get => _cipher;
|
||||
set => SetProperty(ref _cipher, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(IsLogin),
|
||||
nameof(IsIdentity),
|
||||
nameof(IsCard),
|
||||
nameof(IsSecureNote),
|
||||
nameof(ShowUris),
|
||||
nameof(ShowAttachments),
|
||||
nameof(ShowTotp),
|
||||
nameof(ColoredPassword),
|
||||
nameof(UpdatedText),
|
||||
nameof(PasswordUpdatedText),
|
||||
nameof(PasswordHistoryText),
|
||||
nameof(ShowIdentityAddress),
|
||||
nameof(IsDeleted),
|
||||
nameof(CanEdit),
|
||||
});
|
||||
}
|
||||
public List<ViewPageFieldViewModel> Fields
|
||||
nameof(IsLogin),
|
||||
nameof(IsIdentity),
|
||||
nameof(IsCard),
|
||||
nameof(IsSecureNote),
|
||||
nameof(ShowUris),
|
||||
nameof(ShowAttachments),
|
||||
nameof(ShowTotp),
|
||||
nameof(ColoredPassword),
|
||||
nameof(UpdatedText),
|
||||
nameof(PasswordUpdatedText),
|
||||
nameof(PasswordHistoryText),
|
||||
nameof(ShowIdentityAddress),
|
||||
nameof(IsDeleted),
|
||||
nameof(CanEdit),
|
||||
};
|
||||
public List<CipherDetailsPageFieldViewModel> Fields
|
||||
{
|
||||
get => _fields;
|
||||
set => SetProperty(ref _fields, value);
|
||||
@ -256,7 +240,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
Cipher = await cipher.DecryptAsync();
|
||||
CanAccessPremium = await _stateService.CanAccessPremiumAsync();
|
||||
Fields = Cipher.Fields?.Select(f => new ViewPageFieldViewModel(this, Cipher, f)).ToList();
|
||||
Fields = Cipher.Fields?.Select(f => new CipherDetailsPageFieldViewModel(this, Cipher, f)).ToList();
|
||||
|
||||
if (Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
|
||||
(Cipher.OrganizationUseTotp || CanAccessPremium))
|
||||
@ -455,86 +439,52 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void CheckPasswordAsync()
|
||||
private async Task DownloadAttachmentAsync(AttachmentView attachment)
|
||||
{
|
||||
if (!(Page as BaseContentPage).DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Cipher.Login?.Password))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return;
|
||||
}
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword);
|
||||
var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (matches > 0)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.PasswordExposed,
|
||||
matches.ToString("N0")));
|
||||
}
|
||||
else
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe);
|
||||
}
|
||||
}
|
||||
|
||||
private async void DownloadAttachmentAsync(AttachmentView attachment)
|
||||
{
|
||||
if (!(Page as BaseContentPage).DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return;
|
||||
}
|
||||
if (Cipher.OrganizationId == null && !CanAccessPremium)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
|
||||
return;
|
||||
}
|
||||
if (attachment.FileSize >= 10485760) // 10 MB
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.AttachmentLargeWarning, attachment.SizeName), null,
|
||||
AppResources.Yes, AppResources.No);
|
||||
if (!confirmed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var canOpenFile = true;
|
||||
if (!_deviceActionService.CanOpenFile(attachment.FileName))
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
// iOS is currently hardcoded to always return CanOpenFile == true, but should it ever return false
|
||||
// for any reason we want to be sure to catch it here.
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
|
||||
return;
|
||||
}
|
||||
|
||||
canOpenFile = false;
|
||||
}
|
||||
|
||||
if (!await PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Downloading);
|
||||
try
|
||||
{
|
||||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return;
|
||||
}
|
||||
if (Cipher.OrganizationId == null && !CanAccessPremium)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
|
||||
return;
|
||||
}
|
||||
if (attachment.FileSize >= 10485760) // 10 MB
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.AttachmentLargeWarning, attachment.SizeName), null,
|
||||
AppResources.Yes, AppResources.No);
|
||||
if (!confirmed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var canOpenFile = true;
|
||||
if (!_deviceActionService.CanOpenFile(attachment.FileName))
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
// iOS is currently hardcoded to always return CanOpenFile == true, but should it ever return false
|
||||
// for any reason we want to be sure to catch it here.
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
|
||||
return;
|
||||
}
|
||||
|
||||
canOpenFile = false;
|
||||
}
|
||||
|
||||
if (!await PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Downloading);
|
||||
var data = await _cipherService.DownloadAndDecryptAttachmentAsync(Cipher.Id, attachment, Cipher.OrganizationId);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (data == null)
|
||||
@ -561,9 +511,11 @@ namespace Bit.App.Pages
|
||||
OpenAttachment(data, attachment);
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
|
||||
@ -703,15 +655,15 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public class ViewPageFieldViewModel : ExtendedViewModel
|
||||
public class CipherDetailsPageFieldViewModel : ExtendedViewModel
|
||||
{
|
||||
private II18nService _i18nService;
|
||||
private ViewPageViewModel _vm;
|
||||
private CipherDetailsPageViewModel _vm;
|
||||
private FieldView _field;
|
||||
private CipherView _cipher;
|
||||
private bool _showHiddenValue;
|
||||
|
||||
public ViewPageFieldViewModel(ViewPageViewModel vm, CipherView cipher, FieldView field)
|
||||
public CipherDetailsPageFieldViewModel(CipherDetailsPageViewModel vm, CipherView cipher, FieldView field)
|
||||
{
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_vm = vm;
|
@ -10,7 +10,6 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@ -157,7 +156,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (selection == AppResources.View || string.IsNullOrWhiteSpace(AutofillUrl))
|
||||
{
|
||||
var page = new ViewPage(cipher.Id);
|
||||
var page = new CipherDetailsPage(cipher.Id);
|
||||
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
else if (selection == AppResources.Autofill || selection == AppResources.AutofillAndSave)
|
||||
|
@ -271,7 +271,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (!_vm.Deleted && DoOnce())
|
||||
{
|
||||
var page = new AddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId, _vm.GetVaultFilterOrgId());
|
||||
var page = new CipherAddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId, _vm.GetVaultFilterOrgId());
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
@ -285,11 +285,11 @@ namespace Bit.App.Pages
|
||||
await _accountListOverlay.HideAsync();
|
||||
if (_previousPage.Page == "view" && !string.IsNullOrWhiteSpace(_previousPage.CipherId))
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new ViewPage(_previousPage.CipherId)));
|
||||
await Navigation.PushModalAsync(new NavigationPage(new CipherDetailsPage(_previousPage.CipherId)));
|
||||
}
|
||||
else if (_previousPage.Page == "edit" && !string.IsNullOrWhiteSpace(_previousPage.CipherId))
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_previousPage.CipherId)));
|
||||
await Navigation.PushModalAsync(new NavigationPage(new CipherAddEditPage(_previousPage.CipherId)));
|
||||
}
|
||||
_previousPage = null;
|
||||
}
|
||||
|
@ -378,7 +378,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task SelectCipherAsync(CipherView cipher)
|
||||
{
|
||||
var page = new ViewPage(cipher.Id);
|
||||
var page = new CipherDetailsPage(cipher.Id);
|
||||
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
|
@ -85,13 +85,13 @@ namespace Bit.App.Utilities
|
||||
}
|
||||
else if (selection == AppResources.View)
|
||||
{
|
||||
await page.Navigation.PushModalAsync(new NavigationPage(new ViewPage(cipher.Id)));
|
||||
await page.Navigation.PushModalAsync(new NavigationPage(new CipherDetailsPage(cipher.Id)));
|
||||
}
|
||||
else if (selection == AppResources.Edit)
|
||||
{
|
||||
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
|
||||
{
|
||||
await page.Navigation.PushModalAsync(new NavigationPage(new AddEditPage(cipher.Id)));
|
||||
await page.Navigation.PushModalAsync(new NavigationPage(new CipherAddEditPage(cipher.Id)));
|
||||
}
|
||||
}
|
||||
else if (selection == AppResources.CopyUsername)
|
||||
@ -427,7 +427,7 @@ namespace Bit.App.Utilities
|
||||
{
|
||||
if (appOptions.FromAutofillFramework && appOptions.SaveType.HasValue)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: appOptions));
|
||||
Application.Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: appOptions));
|
||||
return true;
|
||||
}
|
||||
if (appOptions.Uri != null)
|
||||
|
Loading…
Reference in New Issue
Block a user