mirror of
https://github.com/bitwarden/android.git
synced 2024-12-25 10:28:28 +03:00
Soft delete feature (#890)
* [Soft Delete] Added trash folder to mobile (#856) * [Soft Delete] Added trash folder to mobile * [Soft Delete] - Revert send to trash label Co-authored-by: Chad Scharf <cscharf@users.noreply.github.com> * [Soft Delete] - Fix for iOS autofill index behavior (#859) * [Soft Delete] Added trash folder to mobile * [Soft Delete] - Revert send to trash label * [Soft Delete] - iOS autofill index behavior fix Co-authored-by: Chad Scharf <cscharf@users.noreply.github.com> Co-authored-by: Chad Scharf <cscharf@users.noreply.github.com>
This commit is contained in:
parent
4b9a036e5e
commit
ce965ba5e1
25 changed files with 378 additions and 40 deletions
|
@ -489,7 +489,8 @@ namespace Bit.App.Pages
|
|||
AppResources.InternetConnectionRequiredTitle);
|
||||
return false;
|
||||
}
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.DoYouReallyWantToDelete,
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(
|
||||
AppResources.DoYouReallyWantToSoftDeleteCipher,
|
||||
null, AppResources.Yes, AppResources.Cancel);
|
||||
if (!confirmed)
|
||||
{
|
||||
|
@ -497,11 +498,11 @@ namespace Bit.App.Pages
|
|||
}
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Deleting);
|
||||
await _cipherService.DeleteWithServerAsync(Cipher.Id);
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.SoftDeleting);
|
||||
await _cipherService.SoftDeleteWithServerAsync(Cipher.Id);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.ItemDeleted);
|
||||
_messagingService.Send("deletedCipher", Cipher);
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.ItemSoftDeleted);
|
||||
_messagingService.Send("softDeletedCipher", Cipher);
|
||||
return true;
|
||||
}
|
||||
catch (ApiException e)
|
||||
|
|
|
@ -16,14 +16,19 @@ namespace Bit.App.Pages
|
|||
private bool _hasFocused;
|
||||
|
||||
public CiphersPage(Func<CipherView, bool> filter, bool folder = false, bool collection = false,
|
||||
bool type = false, string autofillUrl = null)
|
||||
bool type = false, string autofillUrl = null, bool deleted = false)
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as CiphersPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.Filter = filter;
|
||||
_vm.AutofillUrl = _autofillUrl = autofillUrl;
|
||||
if (folder)
|
||||
_vm.Deleted = deleted;
|
||||
if (deleted)
|
||||
{
|
||||
_vm.PageTitle = AppResources.SearchTrash;
|
||||
}
|
||||
else if (folder)
|
||||
{
|
||||
_vm.PageTitle = AppResources.SearchFolder;
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ namespace Bit.App.Pages
|
|||
public ExtendedObservableCollection<CipherView> Ciphers { get; set; }
|
||||
public Func<CipherView, bool> Filter { get; set; }
|
||||
public string AutofillUrl { get; set; }
|
||||
public bool Deleted { get; set; }
|
||||
|
||||
public bool ShowNoData
|
||||
{
|
||||
|
@ -105,7 +106,8 @@ namespace Bit.App.Pages
|
|||
}
|
||||
try
|
||||
{
|
||||
ciphers = await _searchService.SearchCiphersAsync(searchText, Filter, null, cts.Token);
|
||||
ciphers = await _searchService.SearchCiphersAsync(searchText,
|
||||
Filter ?? (c => c.IsDeleted == Deleted), null, cts.Token);
|
||||
cts.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
|
|
@ -28,7 +28,8 @@ namespace Bit.App.Pages
|
|||
private PreviousPageInfo _previousPage;
|
||||
|
||||
public GroupingsPage(bool mainPage, CipherType? type = null, string folderId = null,
|
||||
string collectionId = null, string pageTitle = null, PreviousPageInfo previousPage = null)
|
||||
string collectionId = null, string pageTitle = null, PreviousPageInfo previousPage = null,
|
||||
bool deleted = false)
|
||||
{
|
||||
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
|
||||
InitializeComponent();
|
||||
|
@ -47,6 +48,7 @@ namespace Bit.App.Pages
|
|||
_vm.Type = type;
|
||||
_vm.FolderId = folderId;
|
||||
_vm.CollectionId = collectionId;
|
||||
_vm.Deleted = deleted;
|
||||
_previousPage = previousPage;
|
||||
if (pageTitle != null)
|
||||
{
|
||||
|
@ -64,6 +66,11 @@ namespace Bit.App.Pages
|
|||
ToolbarItems.Add(_lockItem);
|
||||
ToolbarItems.Add(_exitItem);
|
||||
}
|
||||
if (deleted)
|
||||
{
|
||||
_absLayout.Children.Remove(_fab);
|
||||
ToolbarItems.Remove(_addItem);
|
||||
}
|
||||
}
|
||||
|
||||
public ExtendedListView ListView { get; set; }
|
||||
|
@ -132,6 +139,7 @@ namespace Bit.App.Pages
|
|||
}
|
||||
}
|
||||
await ShowPreviousPageAsync();
|
||||
AdjustToolbar();
|
||||
}, _mainContent);
|
||||
|
||||
if (!_vm.MainPage)
|
||||
|
@ -200,7 +208,11 @@ namespace Bit.App.Pages
|
|||
return;
|
||||
}
|
||||
|
||||
if (item.Cipher != null)
|
||||
if (item.IsTrash)
|
||||
{
|
||||
await _vm.SelectTrashAsync();
|
||||
}
|
||||
else if (item.Cipher != null)
|
||||
{
|
||||
await _vm.SelectCipherAsync(item.Cipher);
|
||||
}
|
||||
|
@ -223,7 +235,7 @@ namespace Bit.App.Pages
|
|||
if (DoOnce())
|
||||
{
|
||||
var page = new CiphersPage(_vm.Filter, _vm.FolderId != null, _vm.CollectionId != null,
|
||||
_vm.Type != null);
|
||||
_vm.Type != null, deleted: _vm.Deleted);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page), false);
|
||||
}
|
||||
}
|
||||
|
@ -245,7 +257,7 @@ namespace Bit.App.Pages
|
|||
|
||||
private async void AddButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
if (!_vm.Deleted && DoOnce())
|
||||
{
|
||||
var page = new AddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
|
@ -268,5 +280,11 @@ namespace Bit.App.Pages
|
|||
}
|
||||
_previousPage = null;
|
||||
}
|
||||
|
||||
private void AdjustToolbar()
|
||||
{
|
||||
_addItem.IsEnabled = !_vm.Deleted;
|
||||
_addItem.IconImageSource = _vm.Deleted ? null : "plus.png";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ namespace Bit.App.Pages
|
|||
public CipherType? Type { get; set; }
|
||||
public string ItemCount { get; set; }
|
||||
public bool FuzzyAutofill { get; set; }
|
||||
public bool IsTrash { get; set; }
|
||||
|
||||
public string Name
|
||||
{
|
||||
|
@ -24,7 +25,11 @@ namespace Bit.App.Pages
|
|||
{
|
||||
return _name;
|
||||
}
|
||||
if (Folder != null)
|
||||
if (IsTrash)
|
||||
{
|
||||
_name = AppResources.Trash;
|
||||
}
|
||||
else if (Folder != null)
|
||||
{
|
||||
_name = Folder.Name;
|
||||
}
|
||||
|
@ -64,7 +69,11 @@ namespace Bit.App.Pages
|
|||
{
|
||||
return _icon;
|
||||
}
|
||||
if (Folder != null)
|
||||
if (IsTrash)
|
||||
{
|
||||
_icon = "\uf014"; // fa-trash-o
|
||||
}
|
||||
else if (Folder != null)
|
||||
{
|
||||
_icon = Folder.Id == null ? "" : "";
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ namespace Bit.App.Pages
|
|||
private Dictionary<string, int> _folderCounts = new Dictionary<string, int>();
|
||||
private Dictionary<string, int> _collectionCounts = new Dictionary<string, int>();
|
||||
private Dictionary<CipherType, int> _typeCounts = new Dictionary<CipherType, int>();
|
||||
private int _deletedCount = 0;
|
||||
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IFolderService _folderService;
|
||||
|
@ -73,6 +74,7 @@ namespace Bit.App.Pages
|
|||
public string FolderId { get; set; }
|
||||
public string CollectionId { get; set; }
|
||||
public Func<CipherView, bool> Filter { get; set; }
|
||||
public bool Deleted { get; set; }
|
||||
|
||||
public bool HasCiphers { get; set; }
|
||||
public bool HasFolders { get; set; }
|
||||
|
@ -152,7 +154,7 @@ namespace Bit.App.Pages
|
|||
ShowNoData = false;
|
||||
Loading = true;
|
||||
ShowList = false;
|
||||
ShowAddCipherButton = true;
|
||||
ShowAddCipherButton = !Deleted;
|
||||
var groupedItems = new List<GroupingsPageListGroup>();
|
||||
var page = Page as GroupingsPage;
|
||||
|
||||
|
@ -233,7 +235,8 @@ namespace Bit.App.Pages
|
|||
}
|
||||
if (Ciphers?.Any() ?? false)
|
||||
{
|
||||
var ciphersListItems = Ciphers.Select(c => new GroupingsPageListItem { Cipher = c }).ToList();
|
||||
var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted)
|
||||
.Select(c => new GroupingsPageListItem { Cipher = c }).ToList();
|
||||
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
|
||||
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
|
||||
}
|
||||
|
@ -244,6 +247,18 @@ namespace Bit.App.Pages
|
|||
groupedItems.Add(new GroupingsPageListGroup(noFolderCiphersListItems, AppResources.FolderNone,
|
||||
noFolderCiphersListItems.Count, uppercaseGroupNames, false));
|
||||
}
|
||||
// Ensure this is last in the list (appears at the bottom)
|
||||
if (MainPage && !Deleted)
|
||||
{
|
||||
groupedItems.Add(new GroupingsPageListGroup(new List<GroupingsPageListItem>()
|
||||
{
|
||||
new GroupingsPageListItem()
|
||||
{
|
||||
IsTrash = true,
|
||||
ItemCount = _deletedCount.ToString("N0")
|
||||
}
|
||||
}, AppResources.Trash, _deletedCount, uppercaseGroupNames, false));
|
||||
}
|
||||
GroupedItems.ResetWithRange(groupedItems);
|
||||
}
|
||||
finally
|
||||
|
@ -299,6 +314,12 @@ namespace Bit.App.Pages
|
|||
await Page.Navigation.PushAsync(page);
|
||||
}
|
||||
|
||||
public async Task SelectTrashAsync()
|
||||
{
|
||||
var page = new GroupingsPage(false, null, null, null, AppResources.Trash, null, true);
|
||||
await Page.Navigation.PushAsync(page);
|
||||
}
|
||||
|
||||
public async Task ExitAsync()
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.ExitConfirmation,
|
||||
|
@ -344,6 +365,7 @@ namespace Bit.App.Pages
|
|||
HasFolders = false;
|
||||
HasCollections = false;
|
||||
Filter = null;
|
||||
_deletedCount = 0;
|
||||
|
||||
if (MainPage)
|
||||
{
|
||||
|
@ -356,9 +378,14 @@ namespace Bit.App.Pages
|
|||
}
|
||||
else
|
||||
{
|
||||
if (Type != null)
|
||||
if (Deleted)
|
||||
{
|
||||
Filter = c => c.Type == Type.Value;
|
||||
Filter = c => c.IsDeleted;
|
||||
NoDataText = AppResources.NoItemsTrash;
|
||||
}
|
||||
else if (Type != null)
|
||||
{
|
||||
Filter = c => c.Type == Type.Value && !c.IsDeleted;
|
||||
}
|
||||
else if (FolderId != null)
|
||||
{
|
||||
|
@ -377,7 +404,7 @@ namespace Bit.App.Pages
|
|||
{
|
||||
PageTitle = AppResources.FolderNone;
|
||||
}
|
||||
Filter = c => c.FolderId == folderId;
|
||||
Filter = c => c.FolderId == folderId && !c.IsDeleted;
|
||||
}
|
||||
else if (CollectionId != null)
|
||||
{
|
||||
|
@ -389,7 +416,7 @@ namespace Bit.App.Pages
|
|||
PageTitle = collectionNode.Node.Name;
|
||||
NestedCollections = (collectionNode.Children?.Count ?? 0) > 0 ? collectionNode.Children : null;
|
||||
}
|
||||
Filter = c => c.CollectionIds?.Contains(CollectionId) ?? false;
|
||||
Filter = c => c.CollectionIds?.Contains(CollectionId) ?? false && !c.IsDeleted;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -402,6 +429,12 @@ namespace Bit.App.Pages
|
|||
{
|
||||
if (MainPage)
|
||||
{
|
||||
if (c.IsDeleted)
|
||||
{
|
||||
_deletedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c.Favorite)
|
||||
{
|
||||
if (FavoriteCiphers == null)
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
Order="Secondary" />
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_closeItem" x:Key="closeItem" />
|
||||
<ToolbarItem Text="{u:I18n Edit}" Clicked="EditToolbarItem_Clicked" Order="Primary"
|
||||
<ToolbarItem Clicked="EditToolbarItem_Clicked" Order="Primary"
|
||||
x:Name="_editItem" x:Key="editItem" />
|
||||
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary"
|
||||
x:Name="_moreItem" x:Key="moreItem"
|
||||
|
@ -670,7 +670,8 @@
|
|||
AbsoluteLayout.LayoutFlags="PositionProportional"
|
||||
AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n EditItem}">
|
||||
AutomationProperties.Name="{u:I18n EditItem}"
|
||||
IsVisible="{Binding CanEdit}">
|
||||
<Button.Effects>
|
||||
<effects:FabShadowEffect />
|
||||
</Button.Effects>
|
||||
|
|
|
@ -118,10 +118,20 @@ namespace Bit.App.Pages
|
|||
private async void EditToolbarItem_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
if (_vm.IsDeleted)
|
||||
{
|
||||
if (await _vm.RestoreAsync())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_vm.CipherId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EditButton_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
|
@ -234,7 +244,17 @@ namespace Bit.App.Pages
|
|||
|
||||
private void AdjustToolbar()
|
||||
{
|
||||
if (Device.RuntimePlatform != Device.Android || _vm.Cipher == null)
|
||||
if (_vm.Cipher == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_editItem.Text = _vm.Cipher.IsDeleted ? AppResources.Restore :
|
||||
AppResources.Edit;
|
||||
if (_vm.Cipher.IsDeleted)
|
||||
{
|
||||
_absLayout.Children.Remove(_fab);
|
||||
}
|
||||
if (Device.RuntimePlatform != Device.Android)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -268,6 +288,10 @@ namespace Bit.App.Pages
|
|||
ToolbarItems.Insert(1, _collectionsItem);
|
||||
}
|
||||
}
|
||||
if (_vm.Cipher.IsDeleted && !ToolbarItems.Contains(_editItem))
|
||||
{
|
||||
ToolbarItems.Insert(1, _editItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,6 +86,8 @@ namespace Bit.App.Pages
|
|||
nameof(PasswordUpdatedText),
|
||||
nameof(PasswordHistoryText),
|
||||
nameof(ShowIdentityAddress),
|
||||
nameof(IsDeleted),
|
||||
nameof(CanEdit),
|
||||
});
|
||||
}
|
||||
public List<ViewPageFieldViewModel> Fields
|
||||
|
@ -210,6 +212,8 @@ namespace Bit.App.Pages
|
|||
Page.Resources["textTotp"] = Application.Current.Resources[value ? "text-danger" : "text-default"];
|
||||
}
|
||||
}
|
||||
public bool IsDeleted => Cipher.IsDeleted;
|
||||
public bool CanEdit => !Cipher.IsDeleted;
|
||||
|
||||
public async Task<bool> LoadAsync(Action finishedLoadingAction = null)
|
||||
{
|
||||
|
@ -282,7 +286,8 @@ namespace Bit.App.Pages
|
|||
AppResources.InternetConnectionRequiredTitle);
|
||||
return false;
|
||||
}
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.DoYouReallyWantToDelete,
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(
|
||||
Cipher.IsDeleted ? AppResources.DoYouReallyWantToPermanentlyDeleteCipher : AppResources.DoYouReallyWantToSoftDeleteCipher,
|
||||
null, AppResources.Yes, AppResources.Cancel);
|
||||
if (!confirmed)
|
||||
{
|
||||
|
@ -290,11 +295,58 @@ namespace Bit.App.Pages
|
|||
}
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Deleting);
|
||||
await _deviceActionService.ShowLoadingAsync(Cipher.IsDeleted ? AppResources.Deleting : AppResources.SoftDeleting);
|
||||
if (Cipher.IsDeleted)
|
||||
{
|
||||
await _cipherService.DeleteWithServerAsync(Cipher.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _cipherService.SoftDeleteWithServerAsync(Cipher.Id);
|
||||
}
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.ItemDeleted);
|
||||
_messagingService.Send("deletedCipher", Cipher);
|
||||
_platformUtilsService.ShowToast("success", null,
|
||||
Cipher.IsDeleted ? AppResources.ItemDeleted : AppResources.ItemSoftDeleted);
|
||||
_messagingService.Send(Cipher.IsDeleted ? "deletedCipher" : "softDeletedCipher", Cipher);
|
||||
return true;
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> RestoreAsync()
|
||||
{
|
||||
if (!IsDeleted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return false;
|
||||
}
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.DoYouReallyWantToRestoreCipher,
|
||||
null, AppResources.Yes, AppResources.Cancel);
|
||||
if (!confirmed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Restoring);
|
||||
await _cipherService.RestoreWithServerAsync(Cipher.Id);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.ItemRestored);
|
||||
_messagingService.Send("restoredCipher", Cipher);
|
||||
return true;
|
||||
}
|
||||
catch (ApiException e)
|
||||
|
|
67
src/App/Resources/AppResources.Designer.cs
generated
67
src/App/Resources/AppResources.Designer.cs
generated
|
@ -10,6 +10,7 @@
|
|||
|
||||
namespace Bit.App.Resources {
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
|
@ -1924,6 +1925,12 @@ namespace Bit.App.Resources {
|
|||
}
|
||||
}
|
||||
|
||||
public static string NoItemsTrash {
|
||||
get {
|
||||
return ResourceManager.GetString("NoItemsTrash", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string AutofillAccessibilityService {
|
||||
get {
|
||||
return ResourceManager.GetString("AutofillAccessibilityService", resourceCulture);
|
||||
|
@ -2871,5 +2878,65 @@ namespace Bit.App.Resources {
|
|||
return ResourceManager.GetString("AutofillTileUriNotFound", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string SoftDeleting {
|
||||
get {
|
||||
return ResourceManager.GetString("SoftDeleting", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ItemSoftDeleted {
|
||||
get {
|
||||
return ResourceManager.GetString("ItemSoftDeleted", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Restore {
|
||||
get {
|
||||
return ResourceManager.GetString("Restore", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Restoring {
|
||||
get {
|
||||
return ResourceManager.GetString("Restoring", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ItemRestored {
|
||||
get {
|
||||
return ResourceManager.GetString("ItemRestored", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Trash {
|
||||
get {
|
||||
return ResourceManager.GetString("Trash", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string SearchTrash {
|
||||
get {
|
||||
return ResourceManager.GetString("SearchTrash", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string DoYouReallyWantToPermanentlyDeleteCipher {
|
||||
get {
|
||||
return ResourceManager.GetString("DoYouReallyWantToPermanentlyDeleteCipher", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string DoYouReallyWantToRestoreCipher {
|
||||
get {
|
||||
return ResourceManager.GetString("DoYouReallyWantToRestoreCipher", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string DoYouReallyWantToSoftDeleteCipher {
|
||||
get {
|
||||
return ResourceManager.GetString("DoYouReallyWantToSoftDeleteCipher", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1131,6 +1131,9 @@
|
|||
<data name="NoItemsFolder" xml:space="preserve">
|
||||
<value>There are no items in this folder.</value>
|
||||
</data>
|
||||
<data name="NoItemsTrash" xml:space="preserve">
|
||||
<value>There are no items in the trash.</value>
|
||||
</data>
|
||||
<data name="AutofillAccessibilityService" xml:space="preserve">
|
||||
<value>Auto-fill Accessibility Service</value>
|
||||
</data>
|
||||
|
@ -1631,4 +1634,44 @@
|
|||
<data name="AutofillTileUriNotFound" xml:space="preserve">
|
||||
<value>No password fields detected</value>
|
||||
</data>
|
||||
<data name="SoftDeleting" xml:space="preserve">
|
||||
<value>Sending to trash...</value>
|
||||
<comment>Message shown when interacting with the server</comment>
|
||||
</data>
|
||||
<data name="ItemSoftDeleted" xml:space="preserve">
|
||||
<value>Item has been sent to trash.</value>
|
||||
<comment>Confirmation message after successfully soft-deleting a login</comment>
|
||||
</data>
|
||||
<data name="Restore" xml:space="preserve">
|
||||
<value>Restore</value>
|
||||
<comment>Restores an entity (verb).</comment>
|
||||
</data>
|
||||
<data name="Restoring" xml:space="preserve">
|
||||
<value>Restoring...</value>
|
||||
<comment>Message shown when interacting with the server</comment>
|
||||
</data>
|
||||
<data name="ItemRestored" xml:space="preserve">
|
||||
<value>Item has been restored.</value>
|
||||
<comment>Confirmation message after successfully restoring a soft-deleted item</comment>
|
||||
</data>
|
||||
<data name="Trash" xml:space="preserve">
|
||||
<value>Trash</value>
|
||||
<comment>(noun) Location of deleted items which have not yet been permanently deleted</comment>
|
||||
</data>
|
||||
<data name="SearchTrash" xml:space="preserve">
|
||||
<value>Search trash</value>
|
||||
<comment>(action prompt) Label for the search text field when viewing the trash folder</comment>
|
||||
</data>
|
||||
<data name="DoYouReallyWantToPermanentlyDeleteCipher" xml:space="preserve">
|
||||
<value>Do you really want to permanently delete? This cannot be undone.</value>
|
||||
<comment>Confirmation alert message when permanently deleteing a cipher.</comment>
|
||||
</data>
|
||||
<data name="DoYouReallyWantToRestoreCipher" xml:space="preserve">
|
||||
<value>Do you really want to restore this item?</value>
|
||||
<comment>Confirmation alert message when restoring a soft-deleted cipher.</comment>
|
||||
</data>
|
||||
<data name="DoYouReallyWantToSoftDeleteCipher" xml:space="preserve">
|
||||
<value>Do you really want to send to the trash?</value>
|
||||
<comment>Confirmation alert message when soft-deleting a cipher.</comment>
|
||||
</data>
|
||||
</root>
|
|
@ -19,7 +19,11 @@ namespace Bit.App.Utilities
|
|||
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
var lockService = ServiceContainer.Resolve<ILockService>("lockService");
|
||||
var options = new List<string> { AppResources.View, AppResources.Edit };
|
||||
var options = new List<string> { AppResources.View };
|
||||
if (!cipher.IsDeleted)
|
||||
{
|
||||
options.Add(AppResources.Edit);
|
||||
}
|
||||
if (cipher.Type == Core.Enums.CipherType.Login)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(cipher.Login.Username))
|
||||
|
|
|
@ -37,6 +37,8 @@ namespace Bit.Core.Abstractions
|
|||
Task PutCipherCollectionsAsync(string id, CipherCollectionsRequest request);
|
||||
Task<FolderResponse> PutFolderAsync(string id, FolderRequest request);
|
||||
Task<CipherResponse> PutShareCipherAsync(string id, CipherShareRequest request);
|
||||
Task PutDeleteCipherAsync(string id);
|
||||
Task PutRestoreCipherAsync(string id);
|
||||
Task RefreshIdentityTokenAsync();
|
||||
Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path,
|
||||
TRequest body, bool authed, bool hasResponse);
|
||||
|
|
|
@ -36,5 +36,7 @@ namespace Bit.Core.Abstractions
|
|||
Task UpsertAsync(CipherData cipher);
|
||||
Task UpsertAsync(List<CipherData> cipher);
|
||||
Task<byte[]> DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId);
|
||||
Task SoftDeleteWithServerAsync(string id);
|
||||
Task RestoreWithServerAsync(string id);
|
||||
}
|
||||
}
|
|
@ -12,8 +12,8 @@ namespace Bit.Core.Abstractions
|
|||
Task IndexCiphersAsync();
|
||||
bool IsSearchable(string query);
|
||||
Task<List<CipherView>> SearchCiphersAsync(string query, Func<CipherView, bool> filter = null,
|
||||
List<CipherView> ciphers = null, CancellationToken ct = default(CancellationToken));
|
||||
List<CipherView> ciphers = null, CancellationToken ct = default);
|
||||
List<CipherView> SearchCiphersBasic(List<CipherView> ciphers, string query,
|
||||
CancellationToken ct = default(CancellationToken));
|
||||
CancellationToken ct = default, bool deleted = false);
|
||||
}
|
||||
}
|
|
@ -26,6 +26,8 @@
|
|||
Cipher_ClientCopiedHiddenField = 1112,
|
||||
Cipher_ClientCopiedCardCode = 1113,
|
||||
Cipher_ClientAutofilled = 1114,
|
||||
Cipher_SoftDeleted = 1115,
|
||||
Cipher_Restored = 1116,
|
||||
|
||||
Collection_Created = 1300,
|
||||
Collection_Updated = 1301,
|
||||
|
|
|
@ -45,6 +45,7 @@ namespace Bit.Core.Models.Data
|
|||
Fields = response.Fields?.Select(f => new FieldData(f)).ToList();
|
||||
Attachments = response.Attachments?.Select(a => new AttachmentData(a)).ToList();
|
||||
PasswordHistory = response.PasswordHistory?.Select(ph => new PasswordHistoryData(ph)).ToList();
|
||||
DeletedDate = response.DeletedDate;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
|
@ -66,5 +67,6 @@ namespace Bit.Core.Models.Data
|
|||
public List<AttachmentData> Attachments { get; set; }
|
||||
public List<PasswordHistoryData> PasswordHistory { get; set; }
|
||||
public List<string> CollectionIds { get; set; }
|
||||
public DateTime? DeletedDate { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ namespace Bit.Core.Models.Domain
|
|||
Attachments = obj.Attachments?.Select(a => new Attachment(a, alreadyEncrypted)).ToList();
|
||||
Fields = obj.Fields?.Select(f => new Field(f, alreadyEncrypted)).ToList();
|
||||
PasswordHistory = obj.PasswordHistory?.Select(ph => new PasswordHistory(ph, alreadyEncrypted)).ToList();
|
||||
DeletedDate = obj.DeletedDate;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
|
@ -73,6 +74,8 @@ namespace Bit.Core.Models.Domain
|
|||
public List<PasswordHistory> PasswordHistory { get; set; }
|
||||
public HashSet<string> CollectionIds { get; set; }
|
||||
|
||||
public DateTime? DeletedDate { get; set; }
|
||||
|
||||
public async Task<CipherView> DecryptAsync()
|
||||
{
|
||||
var model = new CipherView(this);
|
||||
|
@ -161,7 +164,8 @@ namespace Bit.Core.Models.Domain
|
|||
Favorite = Favorite,
|
||||
RevisionDate = RevisionDate,
|
||||
Type = Type,
|
||||
CollectionIds = CollectionIds.ToList()
|
||||
CollectionIds = CollectionIds.ToList(),
|
||||
DeletedDate = DeletedDate
|
||||
};
|
||||
BuildDataModel(this, c, new HashSet<string>
|
||||
{
|
||||
|
|
|
@ -24,5 +24,6 @@ namespace Bit.Core.Models.Response
|
|||
public List<AttachmentResponse> Attachments { get; set; }
|
||||
public List<PasswordHistoryResponse> PasswordHistory { get; set; }
|
||||
public List<string> CollectionIds { get; set; }
|
||||
public DateTime? DeletedDate { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ namespace Bit.Core.Models.View
|
|||
LocalData = c.LocalData;
|
||||
CollectionIds = c.CollectionIds;
|
||||
RevisionDate = c.RevisionDate;
|
||||
DeletedDate = c.DeletedDate;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
|
@ -43,6 +44,7 @@ namespace Bit.Core.Models.View
|
|||
public List<PasswordHistoryView> PasswordHistory { get; set; }
|
||||
public HashSet<string> CollectionIds { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
public DateTime? DeletedDate { get; set; }
|
||||
|
||||
|
||||
public string SubTitle
|
||||
|
@ -96,5 +98,6 @@ namespace Bit.Core.Models.View
|
|||
return Login.PasswordRevisionDate;
|
||||
}
|
||||
}
|
||||
public bool IsDeleted => DeletedDate.HasValue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -245,6 +245,16 @@ namespace Bit.Core.Services
|
|||
return SendAsync<object, object>(HttpMethod.Delete, string.Concat("/ciphers/", id), null, true, false);
|
||||
}
|
||||
|
||||
public Task PutDeleteCipherAsync(string id)
|
||||
{
|
||||
return SendAsync<object, object>(HttpMethod.Put, string.Concat("/ciphers/", id, "/delete"), null, true, false);
|
||||
}
|
||||
|
||||
public Task PutRestoreCipherAsync(string id)
|
||||
{
|
||||
return SendAsync<object, object>(HttpMethod.Put, string.Concat("/ciphers/", id, "/restore"), null, true, false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Attachments APIs
|
||||
|
|
|
@ -265,6 +265,10 @@ namespace Bit.Core.Services
|
|||
var ciphers = await GetAllDecryptedAsync();
|
||||
return ciphers.Where(cipher =>
|
||||
{
|
||||
if (cipher.IsDeleted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (folder && cipher.FolderId == groupingId)
|
||||
{
|
||||
return true;
|
||||
|
@ -324,6 +328,11 @@ namespace Bit.Core.Services
|
|||
|
||||
foreach (var cipher in ciphers)
|
||||
{
|
||||
if (cipher.IsDeleted)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cipher.Type != CipherType.Login && (includeOtherTypes?.Any(t => t == cipher.Type) ?? false))
|
||||
{
|
||||
others.Add(cipher);
|
||||
|
@ -695,6 +704,45 @@ namespace Bit.Core.Services
|
|||
return null;
|
||||
}
|
||||
|
||||
public async Task SoftDeleteWithServerAsync(string id)
|
||||
{
|
||||
var userId = await _userService.GetUserIdAsync();
|
||||
var cipherKey = string.Format(Keys_CiphersFormat, userId);
|
||||
var ciphers = await _storageService.GetAsync<Dictionary<string, CipherData>>(cipherKey);
|
||||
if (ciphers == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!ciphers.ContainsKey(id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _apiService.PutDeleteCipherAsync(id);
|
||||
ciphers[id].DeletedDate = DateTime.UtcNow;
|
||||
await _storageService.SaveAsync(cipherKey, ciphers);
|
||||
DecryptedCipherCache = null;
|
||||
}
|
||||
|
||||
public async Task RestoreWithServerAsync(string id)
|
||||
{
|
||||
var userId = await _userService.GetUserIdAsync();
|
||||
var cipherKey = string.Format(Keys_CiphersFormat, userId);
|
||||
var ciphers = await _storageService.GetAsync<Dictionary<string, CipherData>>(cipherKey);
|
||||
if (ciphers == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!ciphers.ContainsKey(id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _apiService.PutRestoreCipherAsync(id);
|
||||
ciphers[id].DeletedDate = null;
|
||||
await _storageService.SaveAsync(cipherKey, ciphers);
|
||||
DecryptedCipherCache = null;
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
private async Task ShareAttachmentWithServerAsync(AttachmentView attachmentView, string cipherId,
|
||||
|
|
|
@ -35,7 +35,7 @@ namespace Bit.Core.Services
|
|||
}
|
||||
|
||||
public async Task<List<CipherView>> SearchCiphersAsync(string query, Func<CipherView, bool> filter = null,
|
||||
List<CipherView> ciphers = null, CancellationToken ct = default(CancellationToken))
|
||||
List<CipherView> ciphers = null, CancellationToken ct = default)
|
||||
{
|
||||
var results = new List<CipherView>();
|
||||
if (query != null)
|
||||
|
@ -68,7 +68,7 @@ namespace Bit.Core.Services
|
|||
}
|
||||
|
||||
public List<CipherView> SearchCiphersBasic(List<CipherView> ciphers, string query,
|
||||
CancellationToken ct = default(CancellationToken))
|
||||
CancellationToken ct = default, bool deleted = false)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
query = query.Trim().ToLower();
|
||||
|
|
|
@ -63,7 +63,7 @@ namespace Bit.iOS.Core.Views
|
|||
}
|
||||
|
||||
_allItems = combinedLogins
|
||||
.Where(c => c.Type == Bit.Core.Enums.CipherType.Login)
|
||||
.Where(c => c.Type == Bit.Core.Enums.CipherType.Login && !c.IsDeleted)
|
||||
.Select(s => new CipherViewModel(s))
|
||||
.ToList() ?? new List<CipherViewModel>();
|
||||
FilterResults(searchFilter, new CancellationToken());
|
||||
|
|
|
@ -120,7 +120,7 @@ namespace Bit.iOS
|
|||
}
|
||||
}
|
||||
}
|
||||
else if (message.Command == "addedCipher" || message.Command == "editedCipher")
|
||||
else if (message.Command == "addedCipher" || message.Command == "editedCipher" || message.Command == "restoredCipher")
|
||||
{
|
||||
if (_deviceActionService.SystemMajorVersion() >= 12)
|
||||
{
|
||||
|
@ -142,7 +142,7 @@ namespace Bit.iOS
|
|||
await ASHelpers.ReplaceAllIdentities();
|
||||
}
|
||||
}
|
||||
else if (message.Command == "deletedCipher")
|
||||
else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher")
|
||||
{
|
||||
if (_deviceActionService.SystemMajorVersion() >= 12)
|
||||
{
|
||||
|
@ -168,6 +168,11 @@ namespace Bit.iOS
|
|||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher")
|
||||
&& _deviceActionService.SystemMajorVersion() >= 12)
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
}
|
||||
});
|
||||
|
||||
return base.FinishedLaunching(app, options);
|
||||
|
|
Loading…
Reference in a new issue