From 8a3d88b3ceb7f2accfd37b908a9419637b12e69a Mon Sep 17 00:00:00 2001 From: mp-bw <59324545+mp-bw@users.noreply.github.com> Date: Tue, 31 May 2022 13:34:54 -0400 Subject: [PATCH] [SG-79] Mobile Vault Filter (#1928) * [SG-79] Vault Filter * Update vault button text after sync * formatting * cleanup * cleanup --- .../CipherViewCell/CipherViewCell.xaml | 2 +- .../Controls/SendViewCell/SendViewCell.xaml | 2 +- src/App/Pages/TabsPage.cs | 35 ++++- .../Vault/GroupingsPage/GroupingsPage.xaml | 25 +++ .../Vault/GroupingsPage/GroupingsPage.xaml.cs | 8 +- .../GroupingsPage/GroupingsPageViewModel.cs | 148 ++++++++++++++++-- src/App/Resources/AppResources.Designer.cs | 30 ++++ src/App/Resources/AppResources.resx | 15 ++ src/Core/Abstractions/IFolderService.cs | 2 +- src/Core/Abstractions/IPolicyService.cs | 1 + src/Core/BitwardenIcons.cs | 1 + src/Core/Services/FolderService.cs | 7 +- src/Core/Services/PolicyService.cs | 10 +- 13 files changed, 261 insertions(+), 25 deletions(-) diff --git a/src/App/Controls/CipherViewCell/CipherViewCell.xaml b/src/App/Controls/CipherViewCell/CipherViewCell.xaml index cda2f01d0..5024ad537 100644 --- a/src/App/Controls/CipherViewCell/CipherViewCell.xaml +++ b/src/App/Controls/CipherViewCell/CipherViewCell.xaml @@ -108,7 +108,7 @@ _logger = new LazyResolve("logger"); private NavigationPage _groupingsPage; private NavigationPage _sendGroupingsPage; @@ -19,6 +23,7 @@ namespace Bit.App.Pages public TabsPage(AppOptions appOptions = null, PreviousPageInfo previousPage = null) { + _broadcasterService = ServiceContainer.Resolve("broadcasterService"); _messagingService = ServiceContainer.Resolve("messagingService"); _keyConnectorService = ServiceContainer.Resolve("keyConnectorService"); @@ -78,12 +83,26 @@ namespace Bit.App.Pages protected override async void OnAppearing() { base.OnAppearing(); + _broadcasterService.Subscribe(nameof(TabsPage), async (message) => + { + if (message.Command == "syncCompleted") + { + Device.BeginInvokeOnMainThread(async () => await UpdateVaultButtonTitleAsync()); + } + }); + await UpdateVaultButtonTitleAsync(); if (await _keyConnectorService.UserNeedsMigration()) { _messagingService.Send("convertAccountToKeyConnector"); } } + protected override void OnDisappearing() + { + base.OnDisappearing(); + _broadcasterService.Unsubscribe(nameof(TabsPage)); + } + public void ResetToVaultPage() { CurrentPage = _groupingsPage; @@ -131,5 +150,19 @@ namespace Bit.App.Pages groupingsPage.HideAccountSwitchingOverlayAsync().FireAndForget(); } } + + private async Task UpdateVaultButtonTitleAsync() + { + try + { + var policyService = ServiceContainer.Resolve("policyService"); + var isShowingVaultFilter = await policyService.ShouldShowVaultFilterAsync(); + _groupingsPage.Title = isShowingVaultFilter ? AppResources.Vaults : AppResources.MyVault; + } + catch (Exception ex) + { + _logger.Value.Exception(ex); + } + } } } diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml index bb7c71535..7df644850 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml @@ -6,6 +6,7 @@ xmlns:u="clr-namespace:Bit.App.Utilities" xmlns:effects="clr-namespace:Bit.App.Effects" xmlns:controls="clr-namespace:Bit.App.Controls" + xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore" x:DataType="pages:GroupingsPageViewModel" Title="{Binding PageTitle}" x:Name="_page"> @@ -106,6 +107,30 @@ GroupTemplate="{StaticResource groupTemplate}" /> + + + _organizations; private List _allCiphers; private Dictionary _folderCounts = new Dictionary(); private Dictionary _collectionCounts = new Dictionary(); @@ -46,6 +50,8 @@ namespace Bit.App.Pages private readonly IMessagingService _messagingService; private readonly IStateService _stateService; private readonly IPasswordRepromptService _passwordRepromptService; + private readonly IOrganizationService _organizationService; + private readonly IPolicyService _policyService; private readonly ILogger _logger; public GroupingsPageViewModel() @@ -60,10 +66,11 @@ namespace Bit.App.Pages _messagingService = ServiceContainer.Resolve("messagingService"); _stateService = ServiceContainer.Resolve("stateService"); _passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); + _organizationService = ServiceContainer.Resolve("organizationService"); + _policyService = ServiceContainer.Resolve("policyService"); _logger = ServiceContainer.Resolve("logger"); Loading = true; - PageTitle = AppResources.MyVault; GroupedItems = new ObservableRangeCollection(); RefreshCommand = new Command(async () => { @@ -71,6 +78,9 @@ namespace Bit.App.Pages await LoadAsync(); }); CipherOptionsCommand = new Command(CipherOptionsAsync); + VaultFilterCommand = new AsyncCommand(VaultFilterOptionsAsync, + onException: ex => _logger.Exception(ex), + allowsMultipleExecutions: false); AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger) { @@ -87,8 +97,9 @@ namespace Bit.App.Pages public bool HasCiphers { get; set; } public bool HasFolders { get; set; } public bool HasCollections { get; set; } - public bool ShowNoFolderCiphers => (NoFolderCiphers?.Count ?? int.MaxValue) < NoFolderListSize && - (!Collections?.Any() ?? true); + public bool ShowNoFolderCipherGroup => NoFolderCiphers != null + && NoFolderCiphers.Count < NoFolderListSize + && (Collections is null || !Collections.Any()); public List Ciphers { get; set; } public List FavoriteCiphers { get; set; } public List NoFolderCiphers { get; set; } @@ -142,12 +153,30 @@ namespace Bit.App.Pages get => _websiteIconsEnabled; set => SetProperty(ref _websiteIconsEnabled, value); } + public bool ShowVaultFilter + { + get => _showVaultFilter; + set => SetProperty(ref _showVaultFilter, value); + } + public string VaultFilterDescription + { + get + { + if (_vaultFilterSelection == null || _vaultFilterSelection == AppResources.AllVaults) + { + return string.Format(AppResources.VaultFilterDescription, AppResources.All); + } + return string.Format(AppResources.VaultFilterDescription, _vaultFilterSelection); + } + set => SetProperty(ref _vaultFilterSelection, value); + } public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; } public ObservableRangeCollection GroupedItems { get; set; } public Command RefreshCommand { get; set; } public Command CipherOptionsCommand { get; set; } + public ICommand VaultFilterCommand { get; } public bool LoadedOnce { get; set; } public async Task LoadAsync() @@ -172,6 +201,17 @@ namespace Bit.App.Pages return; } + _organizations = await _organizationService.GetAllAsync(); + if (MainPage) + { + ShowVaultFilter = await _policyService.ShouldShowVaultFilterAsync(); + if (ShowVaultFilter && _vaultFilterSelection == null) + { + _vaultFilterSelection = AppResources.AllVaults; + } + PageTitle = ShowVaultFilter ? AppResources.Vaults : AppResources.MyVault; + } + _doingLoad = true; LoadedOnce = true; ShowNoData = false; @@ -185,9 +225,9 @@ namespace Bit.App.Pages try { await LoadDataAsync(); - if (ShowNoFolderCiphers && (NestedFolders?.Any() ?? false)) + if (ShowNoFolderCipherGroup && (NestedFolders?.Any() ?? false)) { - // Remove "No Folder" from folder listing + // Remove "No Folder" folder from folders group NestedFolders = NestedFolders.GetRange(0, NestedFolders.Count - 1); } @@ -262,7 +302,7 @@ namespace Bit.App.Pages groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items, ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any())); } - if (ShowNoFolderCiphers) + if (ShowNoFolderCipherGroup) { var noFolderCiphersListItems = NoFolderCiphers.Select( c => new GroupingsPageListItem { Cipher = c }).ToList(); @@ -354,6 +394,25 @@ namespace Bit.App.Pages SyncRefreshing = false; } + public async Task VaultFilterOptionsAsync() + { + var options = new List { AppResources.AllVaults, AppResources.MyVault }; + if (_organizations.Any()) + { + options.AddRange(_organizations.Select(o => o.Name)); + } + var selection = await Page.DisplayActionSheet(AppResources.FilterByVault, AppResources.Cancel, null, + options.ToArray()); + if (selection == AppResources.Cancel || + (_vaultFilterSelection == null && selection == AppResources.AllVaults) || + (_vaultFilterSelection != null && _vaultFilterSelection == selection)) + { + return; + } + VaultFilterDescription = selection; + await LoadAsync(); + } + public async Task SelectCipherAsync(CipherView cipher) { var page = new ViewPage(cipher.Id); @@ -380,25 +439,26 @@ namespace Bit.App.Pages default: break; } - var page = new GroupingsPage(false, type, null, null, title); + var page = new GroupingsPage(false, type, null, null, title, _vaultFilterSelection); await Page.Navigation.PushAsync(page); } public async Task SelectFolderAsync(FolderView folder) { - var page = new GroupingsPage(false, null, folder.Id ?? "none", null, folder.Name); + var page = new GroupingsPage(false, null, folder.Id ?? "none", null, folder.Name, _vaultFilterSelection); await Page.Navigation.PushAsync(page); } public async Task SelectCollectionAsync(Core.Models.View.CollectionView collection) { - var page = new GroupingsPage(false, null, null, collection.Id, collection.Name); + var page = new GroupingsPage(false, null, null, collection.Id, collection.Name, _vaultFilterSelection); await Page.Navigation.PushAsync(page); } public async Task SelectTrashAsync() { - var page = new GroupingsPage(false, null, null, null, AppResources.Trash, null, true); + var page = new GroupingsPage(false, null, null, null, AppResources.Trash, _vaultFilterSelection, null, + true); await Page.Navigation.PushAsync(page); } @@ -436,8 +496,8 @@ namespace Bit.App.Pages private async Task LoadDataAsync() { + var orgId = await FillAllCiphersAndGetOrgIdIfNeededAsync(); NoDataText = AppResources.NoItems; - _allCiphers = await _cipherService.GetAllDecryptedAsync(); HasCiphers = _allCiphers.Any(); FavoriteCiphers?.Clear(); NoFolderCiphers?.Clear(); @@ -451,12 +511,11 @@ namespace Bit.App.Pages if (MainPage) { - Folders = await _folderService.GetAllDecryptedAsync(); - NestedFolders = await _folderService.GetAllNestedAsync(); + await FillFoldersAndCollectionsAsync(orgId); + NestedFolders = await _folderService.GetAllNestedAsync(Folders); HasFolders = NestedFolders.Any(f => f.Node?.Id != null); - Collections = await _collectionService.GetAllDecryptedAsync(); - NestedCollections = await _collectionService.GetAllNestedAsync(Collections); - HasCollections = NestedCollections.Any(); + NestedCollections = Collections != null ? await _collectionService.GetAllNestedAsync(Collections) : null; + HasCollections = NestedCollections?.Any() ?? false; } else { @@ -576,6 +635,63 @@ namespace Bit.App.Pages } } + private async Task FillAllCiphersAndGetOrgIdIfNeededAsync() + { + string orgId = null; + var decCiphers = await _cipherService.GetAllDecryptedAsync(); + if (IsVaultFilterMyVault) + { + _allCiphers = decCiphers.Where(c => c.OrganizationId == null).ToList(); + } + else if (IsVaultFilterOrgVault) + { + orgId = GetVaultFilterOrgId(); + _allCiphers = decCiphers.Where(c => c.OrganizationId == orgId).ToList(); + } + else + { + _allCiphers = decCiphers; + } + return orgId; + } + + private async Task FillFoldersAndCollectionsAsync(string orgId) + { + var decFolders = await _folderService.GetAllDecryptedAsync(); + var decCollections = await _collectionService.GetAllDecryptedAsync(); + if (IsVaultFilterMyVault) + { + Folders = BuildFolders(decFolders); + Collections = null; + } + else if (IsVaultFilterOrgVault && !string.IsNullOrWhiteSpace(orgId)) + { + Folders = BuildFolders(decFolders); + Collections = decCollections?.Where(c => c.OrganizationId == orgId).ToList(); + } + else + { + Folders = decFolders; + Collections = decCollections; + } + } + + private bool IsVaultFilterMyVault => _vaultFilterSelection == AppResources.MyVault; + + private bool IsVaultFilterOrgVault => _vaultFilterSelection != AppResources.AllVaults && + _vaultFilterSelection != AppResources.MyVault; + + private string GetVaultFilterOrgId() + { + return _organizations?.FirstOrDefault(o => o.Name == _vaultFilterSelection)?.Id; + } + + private List BuildFolders(List decFolders) + { + var folders = decFolders.Where(f => _allCiphers.Any(c => c.FolderId == f.Id)).ToList(); + return folders.Any() ? folders : null; + } + private async void CipherOptionsAsync(CipherView cipher) { if ((Page as BaseContentPage).DoOnce()) diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 0e548abe7..6dfdfd5ce 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -3970,5 +3970,35 @@ namespace Bit.App.Resources { return ResourceManager.GetString("SpecialCharacters", resourceCulture); } } + + public static string FilterByVault { + get { + return ResourceManager.GetString("FilterByVault", resourceCulture); + } + } + + public static string AllVaults { + get { + return ResourceManager.GetString("AllVaults", resourceCulture); + } + } + + public static string Vaults { + get { + return ResourceManager.GetString("Vaults", resourceCulture); + } + } + + public static string VaultFilterDescription { + get { + return ResourceManager.GetString("VaultFilterDescription", resourceCulture); + } + } + + public static string All { + get { + return ResourceManager.GetString("All", resourceCulture); + } + } } } diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 178eb5e0f..2d154508e 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -2220,4 +2220,19 @@ Special Characters (!@#$%^&*) + + Filter items by vault + + + All Vaults + + + Vaults + + + Vault: {0} + + + All + diff --git a/src/Core/Abstractions/IFolderService.cs b/src/Core/Abstractions/IFolderService.cs index 4fe215d1a..b1feae03d 100644 --- a/src/Core/Abstractions/IFolderService.cs +++ b/src/Core/Abstractions/IFolderService.cs @@ -15,7 +15,7 @@ namespace Bit.Core.Abstractions Task EncryptAsync(FolderView model, SymmetricCryptoKey key = null); Task> GetAllAsync(); Task> GetAllDecryptedAsync(); - Task>> GetAllNestedAsync(); + Task>> GetAllNestedAsync(List folders = null); Task GetAsync(string id); Task> GetNestedAsync(string id); Task ReplaceAsync(Dictionary folders); diff --git a/src/Core/Abstractions/IPolicyService.cs b/src/Core/Abstractions/IPolicyService.cs index d9dfb7263..caabeca3d 100644 --- a/src/Core/Abstractions/IPolicyService.cs +++ b/src/Core/Abstractions/IPolicyService.cs @@ -20,5 +20,6 @@ namespace Bit.Core.Abstractions string orgId); Task PolicyAppliesToUser(PolicyType policyType, Func policyFilter = null, string userId = null); int? GetPolicyInt(Policy policy, string key); + Task ShouldShowVaultFilterAsync(); } } diff --git a/src/Core/BitwardenIcons.cs b/src/Core/BitwardenIcons.cs index c621e9091..7e972baba 100644 --- a/src/Core/BitwardenIcons.cs +++ b/src/Core/BitwardenIcons.cs @@ -111,5 +111,6 @@ public const string EyeSlash = "\xe96d"; public const string File = "\xe96e"; public const string Paste = "\xe96f"; + public const string ViewCellMenu = "\xe5d3"; } } diff --git a/src/Core/Services/FolderService.cs b/src/Core/Services/FolderService.cs index e2ed35385..044ee2b93 100644 --- a/src/Core/Services/FolderService.cs +++ b/src/Core/Services/FolderService.cs @@ -107,9 +107,12 @@ namespace Bit.Core.Services return _decryptedFolderCache; } - public async Task>> GetAllNestedAsync() + public async Task>> GetAllNestedAsync(List folders = null) { - var folders = await GetAllDecryptedAsync(); + if (folders == null) + { + folders = await GetAllDecryptedAsync(); + } var nodes = new List>(); foreach (var f in folders) { diff --git a/src/Core/Services/PolicyService.cs b/src/Core/Services/PolicyService.cs index d81370870..1f15e192c 100644 --- a/src/Core/Services/PolicyService.cs +++ b/src/Core/Services/PolicyService.cs @@ -193,7 +193,8 @@ namespace Bit.Core.Services return new Tuple(resetPasswordPolicyOptions, policy != null); } - public async Task PolicyAppliesToUser(PolicyType policyType, Func policyFilter, string userId = null) + public async Task PolicyAppliesToUser(PolicyType policyType, Func policyFilter = null, + string userId = null) { var policies = await GetAll(policyType, userId); if (policies == null) @@ -246,6 +247,13 @@ namespace Bit.Core.Services return null; } + public async Task ShouldShowVaultFilterAsync() + { + var organizations = await _organizationService.GetAllAsync(); + var personalOwnershipPolicyApplies = await PolicyAppliesToUser(PolicyType.PersonalOwnership); + return (organizations?.Any() ?? false) && !personalOwnershipPolicyApplies; + } + private bool? GetPolicyBool(Policy policy, string key) { if (policy.Data.ContainsKey(key))