mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-27 12:26:31 +01: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
@ -489,7 +489,8 @@ namespace Bit.App.Pages
|
|||||||
AppResources.InternetConnectionRequiredTitle);
|
AppResources.InternetConnectionRequiredTitle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.DoYouReallyWantToDelete,
|
var confirmed = await _platformUtilsService.ShowDialogAsync(
|
||||||
|
AppResources.DoYouReallyWantToSoftDeleteCipher,
|
||||||
null, AppResources.Yes, AppResources.Cancel);
|
null, AppResources.Yes, AppResources.Cancel);
|
||||||
if (!confirmed)
|
if (!confirmed)
|
||||||
{
|
{
|
||||||
@ -497,11 +498,11 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.Deleting);
|
await _deviceActionService.ShowLoadingAsync(AppResources.SoftDeleting);
|
||||||
await _cipherService.DeleteWithServerAsync(Cipher.Id);
|
await _cipherService.SoftDeleteWithServerAsync(Cipher.Id);
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
_platformUtilsService.ShowToast("success", null, AppResources.ItemDeleted);
|
_platformUtilsService.ShowToast("success", null, AppResources.ItemSoftDeleted);
|
||||||
_messagingService.Send("deletedCipher", Cipher);
|
_messagingService.Send("softDeletedCipher", Cipher);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (ApiException e)
|
catch (ApiException e)
|
||||||
|
@ -16,14 +16,19 @@ namespace Bit.App.Pages
|
|||||||
private bool _hasFocused;
|
private bool _hasFocused;
|
||||||
|
|
||||||
public CiphersPage(Func<CipherView, bool> filter, bool folder = false, bool collection = false,
|
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();
|
InitializeComponent();
|
||||||
_vm = BindingContext as CiphersPageViewModel;
|
_vm = BindingContext as CiphersPageViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.Filter = filter;
|
_vm.Filter = filter;
|
||||||
_vm.AutofillUrl = _autofillUrl = autofillUrl;
|
_vm.AutofillUrl = _autofillUrl = autofillUrl;
|
||||||
if (folder)
|
_vm.Deleted = deleted;
|
||||||
|
if (deleted)
|
||||||
|
{
|
||||||
|
_vm.PageTitle = AppResources.SearchTrash;
|
||||||
|
}
|
||||||
|
else if (folder)
|
||||||
{
|
{
|
||||||
_vm.PageTitle = AppResources.SearchFolder;
|
_vm.PageTitle = AppResources.SearchFolder;
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@ namespace Bit.App.Pages
|
|||||||
public ExtendedObservableCollection<CipherView> Ciphers { get; set; }
|
public ExtendedObservableCollection<CipherView> Ciphers { get; set; }
|
||||||
public Func<CipherView, bool> Filter { get; set; }
|
public Func<CipherView, bool> Filter { get; set; }
|
||||||
public string AutofillUrl { get; set; }
|
public string AutofillUrl { get; set; }
|
||||||
|
public bool Deleted { get; set; }
|
||||||
|
|
||||||
public bool ShowNoData
|
public bool ShowNoData
|
||||||
{
|
{
|
||||||
@ -105,7 +106,8 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
try
|
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();
|
cts.Token.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
|
@ -28,7 +28,8 @@ namespace Bit.App.Pages
|
|||||||
private PreviousPageInfo _previousPage;
|
private PreviousPageInfo _previousPage;
|
||||||
|
|
||||||
public GroupingsPage(bool mainPage, CipherType? type = null, string folderId = null,
|
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);
|
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@ -47,6 +48,7 @@ namespace Bit.App.Pages
|
|||||||
_vm.Type = type;
|
_vm.Type = type;
|
||||||
_vm.FolderId = folderId;
|
_vm.FolderId = folderId;
|
||||||
_vm.CollectionId = collectionId;
|
_vm.CollectionId = collectionId;
|
||||||
|
_vm.Deleted = deleted;
|
||||||
_previousPage = previousPage;
|
_previousPage = previousPage;
|
||||||
if (pageTitle != null)
|
if (pageTitle != null)
|
||||||
{
|
{
|
||||||
@ -64,6 +66,11 @@ namespace Bit.App.Pages
|
|||||||
ToolbarItems.Add(_lockItem);
|
ToolbarItems.Add(_lockItem);
|
||||||
ToolbarItems.Add(_exitItem);
|
ToolbarItems.Add(_exitItem);
|
||||||
}
|
}
|
||||||
|
if (deleted)
|
||||||
|
{
|
||||||
|
_absLayout.Children.Remove(_fab);
|
||||||
|
ToolbarItems.Remove(_addItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExtendedListView ListView { get; set; }
|
public ExtendedListView ListView { get; set; }
|
||||||
@ -132,6 +139,7 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await ShowPreviousPageAsync();
|
await ShowPreviousPageAsync();
|
||||||
|
AdjustToolbar();
|
||||||
}, _mainContent);
|
}, _mainContent);
|
||||||
|
|
||||||
if (!_vm.MainPage)
|
if (!_vm.MainPage)
|
||||||
@ -200,7 +208,11 @@ namespace Bit.App.Pages
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Cipher != null)
|
if (item.IsTrash)
|
||||||
|
{
|
||||||
|
await _vm.SelectTrashAsync();
|
||||||
|
}
|
||||||
|
else if (item.Cipher != null)
|
||||||
{
|
{
|
||||||
await _vm.SelectCipherAsync(item.Cipher);
|
await _vm.SelectCipherAsync(item.Cipher);
|
||||||
}
|
}
|
||||||
@ -223,7 +235,7 @@ namespace Bit.App.Pages
|
|||||||
if (DoOnce())
|
if (DoOnce())
|
||||||
{
|
{
|
||||||
var page = new CiphersPage(_vm.Filter, _vm.FolderId != null, _vm.CollectionId != null,
|
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);
|
await Navigation.PushModalAsync(new NavigationPage(page), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -245,7 +257,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private async void AddButton_Clicked(object sender, EventArgs e)
|
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);
|
var page = new AddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId);
|
||||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
@ -268,5 +280,11 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
_previousPage = null;
|
_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 CipherType? Type { get; set; }
|
||||||
public string ItemCount { get; set; }
|
public string ItemCount { get; set; }
|
||||||
public bool FuzzyAutofill { get; set; }
|
public bool FuzzyAutofill { get; set; }
|
||||||
|
public bool IsTrash { get; set; }
|
||||||
|
|
||||||
public string Name
|
public string Name
|
||||||
{
|
{
|
||||||
@ -24,7 +25,11 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
return _name;
|
return _name;
|
||||||
}
|
}
|
||||||
if (Folder != null)
|
if (IsTrash)
|
||||||
|
{
|
||||||
|
_name = AppResources.Trash;
|
||||||
|
}
|
||||||
|
else if (Folder != null)
|
||||||
{
|
{
|
||||||
_name = Folder.Name;
|
_name = Folder.Name;
|
||||||
}
|
}
|
||||||
@ -64,7 +69,11 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
return _icon;
|
return _icon;
|
||||||
}
|
}
|
||||||
if (Folder != null)
|
if (IsTrash)
|
||||||
|
{
|
||||||
|
_icon = "\uf014"; // fa-trash-o
|
||||||
|
}
|
||||||
|
else if (Folder != null)
|
||||||
{
|
{
|
||||||
_icon = Folder.Id == 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> _folderCounts = new Dictionary<string, int>();
|
||||||
private Dictionary<string, int> _collectionCounts = new Dictionary<string, int>();
|
private Dictionary<string, int> _collectionCounts = new Dictionary<string, int>();
|
||||||
private Dictionary<CipherType, int> _typeCounts = new Dictionary<CipherType, int>();
|
private Dictionary<CipherType, int> _typeCounts = new Dictionary<CipherType, int>();
|
||||||
|
private int _deletedCount = 0;
|
||||||
|
|
||||||
private readonly ICipherService _cipherService;
|
private readonly ICipherService _cipherService;
|
||||||
private readonly IFolderService _folderService;
|
private readonly IFolderService _folderService;
|
||||||
@ -73,6 +74,7 @@ namespace Bit.App.Pages
|
|||||||
public string FolderId { get; set; }
|
public string FolderId { get; set; }
|
||||||
public string CollectionId { get; set; }
|
public string CollectionId { get; set; }
|
||||||
public Func<CipherView, bool> Filter { get; set; }
|
public Func<CipherView, bool> Filter { get; set; }
|
||||||
|
public bool Deleted { get; set; }
|
||||||
|
|
||||||
public bool HasCiphers { get; set; }
|
public bool HasCiphers { get; set; }
|
||||||
public bool HasFolders { get; set; }
|
public bool HasFolders { get; set; }
|
||||||
@ -152,7 +154,7 @@ namespace Bit.App.Pages
|
|||||||
ShowNoData = false;
|
ShowNoData = false;
|
||||||
Loading = true;
|
Loading = true;
|
||||||
ShowList = false;
|
ShowList = false;
|
||||||
ShowAddCipherButton = true;
|
ShowAddCipherButton = !Deleted;
|
||||||
var groupedItems = new List<GroupingsPageListGroup>();
|
var groupedItems = new List<GroupingsPageListGroup>();
|
||||||
var page = Page as GroupingsPage;
|
var page = Page as GroupingsPage;
|
||||||
|
|
||||||
@ -233,7 +235,8 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
if (Ciphers?.Any() ?? false)
|
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,
|
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
|
||||||
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
|
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
|
||||||
}
|
}
|
||||||
@ -244,6 +247,18 @@ namespace Bit.App.Pages
|
|||||||
groupedItems.Add(new GroupingsPageListGroup(noFolderCiphersListItems, AppResources.FolderNone,
|
groupedItems.Add(new GroupingsPageListGroup(noFolderCiphersListItems, AppResources.FolderNone,
|
||||||
noFolderCiphersListItems.Count, uppercaseGroupNames, false));
|
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);
|
GroupedItems.ResetWithRange(groupedItems);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -299,6 +314,12 @@ namespace Bit.App.Pages
|
|||||||
await Page.Navigation.PushAsync(page);
|
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()
|
public async Task ExitAsync()
|
||||||
{
|
{
|
||||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.ExitConfirmation,
|
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.ExitConfirmation,
|
||||||
@ -344,6 +365,7 @@ namespace Bit.App.Pages
|
|||||||
HasFolders = false;
|
HasFolders = false;
|
||||||
HasCollections = false;
|
HasCollections = false;
|
||||||
Filter = null;
|
Filter = null;
|
||||||
|
_deletedCount = 0;
|
||||||
|
|
||||||
if (MainPage)
|
if (MainPage)
|
||||||
{
|
{
|
||||||
@ -356,9 +378,14 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
else
|
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)
|
else if (FolderId != null)
|
||||||
{
|
{
|
||||||
@ -377,7 +404,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
PageTitle = AppResources.FolderNone;
|
PageTitle = AppResources.FolderNone;
|
||||||
}
|
}
|
||||||
Filter = c => c.FolderId == folderId;
|
Filter = c => c.FolderId == folderId && !c.IsDeleted;
|
||||||
}
|
}
|
||||||
else if (CollectionId != null)
|
else if (CollectionId != null)
|
||||||
{
|
{
|
||||||
@ -389,7 +416,7 @@ namespace Bit.App.Pages
|
|||||||
PageTitle = collectionNode.Node.Name;
|
PageTitle = collectionNode.Node.Name;
|
||||||
NestedCollections = (collectionNode.Children?.Count ?? 0) > 0 ? collectionNode.Children : null;
|
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
|
else
|
||||||
{
|
{
|
||||||
@ -402,6 +429,12 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
if (MainPage)
|
if (MainPage)
|
||||||
{
|
{
|
||||||
|
if (c.IsDeleted)
|
||||||
|
{
|
||||||
|
_deletedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (c.Favorite)
|
if (c.Favorite)
|
||||||
{
|
{
|
||||||
if (FavoriteCiphers == null)
|
if (FavoriteCiphers == null)
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
Order="Secondary" />
|
Order="Secondary" />
|
||||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||||
x:Name="_closeItem" x:Key="closeItem" />
|
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" />
|
x:Name="_editItem" x:Key="editItem" />
|
||||||
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary"
|
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary"
|
||||||
x:Name="_moreItem" x:Key="moreItem"
|
x:Name="_moreItem" x:Key="moreItem"
|
||||||
@ -670,7 +670,8 @@
|
|||||||
AbsoluteLayout.LayoutFlags="PositionProportional"
|
AbsoluteLayout.LayoutFlags="PositionProportional"
|
||||||
AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize"
|
AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n EditItem}">
|
AutomationProperties.Name="{u:I18n EditItem}"
|
||||||
|
IsVisible="{Binding CanEdit}">
|
||||||
<Button.Effects>
|
<Button.Effects>
|
||||||
<effects:FabShadowEffect />
|
<effects:FabShadowEffect />
|
||||||
</Button.Effects>
|
</Button.Effects>
|
||||||
|
@ -118,10 +118,20 @@ namespace Bit.App.Pages
|
|||||||
private async void EditToolbarItem_Clicked(object sender, System.EventArgs e)
|
private async void EditToolbarItem_Clicked(object sender, System.EventArgs e)
|
||||||
{
|
{
|
||||||
if (DoOnce())
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
if (_vm.IsDeleted)
|
||||||
|
{
|
||||||
|
if (await _vm.RestoreAsync())
|
||||||
|
{
|
||||||
|
await Navigation.PopModalAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_vm.CipherId)));
|
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_vm.CipherId)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void EditButton_Clicked(object sender, System.EventArgs e)
|
private void EditButton_Clicked(object sender, System.EventArgs e)
|
||||||
{
|
{
|
||||||
@ -234,7 +244,17 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private void AdjustToolbar()
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@ -268,6 +288,10 @@ namespace Bit.App.Pages
|
|||||||
ToolbarItems.Insert(1, _collectionsItem);
|
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(PasswordUpdatedText),
|
||||||
nameof(PasswordHistoryText),
|
nameof(PasswordHistoryText),
|
||||||
nameof(ShowIdentityAddress),
|
nameof(ShowIdentityAddress),
|
||||||
|
nameof(IsDeleted),
|
||||||
|
nameof(CanEdit),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public List<ViewPageFieldViewModel> Fields
|
public List<ViewPageFieldViewModel> Fields
|
||||||
@ -210,6 +212,8 @@ namespace Bit.App.Pages
|
|||||||
Page.Resources["textTotp"] = Application.Current.Resources[value ? "text-danger" : "text-default"];
|
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)
|
public async Task<bool> LoadAsync(Action finishedLoadingAction = null)
|
||||||
{
|
{
|
||||||
@ -282,7 +286,8 @@ namespace Bit.App.Pages
|
|||||||
AppResources.InternetConnectionRequiredTitle);
|
AppResources.InternetConnectionRequiredTitle);
|
||||||
return false;
|
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);
|
null, AppResources.Yes, AppResources.Cancel);
|
||||||
if (!confirmed)
|
if (!confirmed)
|
||||||
{
|
{
|
||||||
@ -290,11 +295,58 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.Deleting);
|
await _deviceActionService.ShowLoadingAsync(Cipher.IsDeleted ? AppResources.Deleting : AppResources.SoftDeleting);
|
||||||
|
if (Cipher.IsDeleted)
|
||||||
|
{
|
||||||
await _cipherService.DeleteWithServerAsync(Cipher.Id);
|
await _cipherService.DeleteWithServerAsync(Cipher.Id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _cipherService.SoftDeleteWithServerAsync(Cipher.Id);
|
||||||
|
}
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
_platformUtilsService.ShowToast("success", null, AppResources.ItemDeleted);
|
_platformUtilsService.ShowToast("success", null,
|
||||||
_messagingService.Send("deletedCipher", Cipher);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
catch (ApiException e)
|
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 {
|
namespace Bit.App.Resources {
|
||||||
using System;
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
|
||||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
[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 {
|
public static string AutofillAccessibilityService {
|
||||||
get {
|
get {
|
||||||
return ResourceManager.GetString("AutofillAccessibilityService", resourceCulture);
|
return ResourceManager.GetString("AutofillAccessibilityService", resourceCulture);
|
||||||
@ -2871,5 +2878,65 @@ namespace Bit.App.Resources {
|
|||||||
return ResourceManager.GetString("AutofillTileUriNotFound", resourceCulture);
|
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">
|
<data name="NoItemsFolder" xml:space="preserve">
|
||||||
<value>There are no items in this folder.</value>
|
<value>There are no items in this folder.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="NoItemsTrash" xml:space="preserve">
|
||||||
|
<value>There are no items in the trash.</value>
|
||||||
|
</data>
|
||||||
<data name="AutofillAccessibilityService" xml:space="preserve">
|
<data name="AutofillAccessibilityService" xml:space="preserve">
|
||||||
<value>Auto-fill Accessibility Service</value>
|
<value>Auto-fill Accessibility Service</value>
|
||||||
</data>
|
</data>
|
||||||
@ -1631,4 +1634,44 @@
|
|||||||
<data name="AutofillTileUriNotFound" xml:space="preserve">
|
<data name="AutofillTileUriNotFound" xml:space="preserve">
|
||||||
<value>No password fields detected</value>
|
<value>No password fields detected</value>
|
||||||
</data>
|
</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>
|
</root>
|
@ -19,7 +19,11 @@ namespace Bit.App.Utilities
|
|||||||
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||||
var lockService = ServiceContainer.Resolve<ILockService>("lockService");
|
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 (cipher.Type == Core.Enums.CipherType.Login)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(cipher.Login.Username))
|
if (!string.IsNullOrWhiteSpace(cipher.Login.Username))
|
||||||
|
@ -37,6 +37,8 @@ namespace Bit.Core.Abstractions
|
|||||||
Task PutCipherCollectionsAsync(string id, CipherCollectionsRequest request);
|
Task PutCipherCollectionsAsync(string id, CipherCollectionsRequest request);
|
||||||
Task<FolderResponse> PutFolderAsync(string id, FolderRequest request);
|
Task<FolderResponse> PutFolderAsync(string id, FolderRequest request);
|
||||||
Task<CipherResponse> PutShareCipherAsync(string id, CipherShareRequest request);
|
Task<CipherResponse> PutShareCipherAsync(string id, CipherShareRequest request);
|
||||||
|
Task PutDeleteCipherAsync(string id);
|
||||||
|
Task PutRestoreCipherAsync(string id);
|
||||||
Task RefreshIdentityTokenAsync();
|
Task RefreshIdentityTokenAsync();
|
||||||
Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path,
|
Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path,
|
||||||
TRequest body, bool authed, bool hasResponse);
|
TRequest body, bool authed, bool hasResponse);
|
||||||
|
@ -36,5 +36,7 @@ namespace Bit.Core.Abstractions
|
|||||||
Task UpsertAsync(CipherData cipher);
|
Task UpsertAsync(CipherData cipher);
|
||||||
Task UpsertAsync(List<CipherData> cipher);
|
Task UpsertAsync(List<CipherData> cipher);
|
||||||
Task<byte[]> DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId);
|
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();
|
Task IndexCiphersAsync();
|
||||||
bool IsSearchable(string query);
|
bool IsSearchable(string query);
|
||||||
Task<List<CipherView>> SearchCiphersAsync(string query, Func<CipherView, bool> filter = null,
|
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,
|
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_ClientCopiedHiddenField = 1112,
|
||||||
Cipher_ClientCopiedCardCode = 1113,
|
Cipher_ClientCopiedCardCode = 1113,
|
||||||
Cipher_ClientAutofilled = 1114,
|
Cipher_ClientAutofilled = 1114,
|
||||||
|
Cipher_SoftDeleted = 1115,
|
||||||
|
Cipher_Restored = 1116,
|
||||||
|
|
||||||
Collection_Created = 1300,
|
Collection_Created = 1300,
|
||||||
Collection_Updated = 1301,
|
Collection_Updated = 1301,
|
||||||
|
@ -45,6 +45,7 @@ namespace Bit.Core.Models.Data
|
|||||||
Fields = response.Fields?.Select(f => new FieldData(f)).ToList();
|
Fields = response.Fields?.Select(f => new FieldData(f)).ToList();
|
||||||
Attachments = response.Attachments?.Select(a => new AttachmentData(a)).ToList();
|
Attachments = response.Attachments?.Select(a => new AttachmentData(a)).ToList();
|
||||||
PasswordHistory = response.PasswordHistory?.Select(ph => new PasswordHistoryData(ph)).ToList();
|
PasswordHistory = response.PasswordHistory?.Select(ph => new PasswordHistoryData(ph)).ToList();
|
||||||
|
DeletedDate = response.DeletedDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
@ -66,5 +67,6 @@ namespace Bit.Core.Models.Data
|
|||||||
public List<AttachmentData> Attachments { get; set; }
|
public List<AttachmentData> Attachments { get; set; }
|
||||||
public List<PasswordHistoryData> PasswordHistory { get; set; }
|
public List<PasswordHistoryData> PasswordHistory { get; set; }
|
||||||
public List<string> CollectionIds { 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();
|
Attachments = obj.Attachments?.Select(a => new Attachment(a, alreadyEncrypted)).ToList();
|
||||||
Fields = obj.Fields?.Select(f => new Field(f, alreadyEncrypted)).ToList();
|
Fields = obj.Fields?.Select(f => new Field(f, alreadyEncrypted)).ToList();
|
||||||
PasswordHistory = obj.PasswordHistory?.Select(ph => new PasswordHistory(ph, alreadyEncrypted)).ToList();
|
PasswordHistory = obj.PasswordHistory?.Select(ph => new PasswordHistory(ph, alreadyEncrypted)).ToList();
|
||||||
|
DeletedDate = obj.DeletedDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
@ -73,6 +74,8 @@ namespace Bit.Core.Models.Domain
|
|||||||
public List<PasswordHistory> PasswordHistory { get; set; }
|
public List<PasswordHistory> PasswordHistory { get; set; }
|
||||||
public HashSet<string> CollectionIds { get; set; }
|
public HashSet<string> CollectionIds { get; set; }
|
||||||
|
|
||||||
|
public DateTime? DeletedDate { get; set; }
|
||||||
|
|
||||||
public async Task<CipherView> DecryptAsync()
|
public async Task<CipherView> DecryptAsync()
|
||||||
{
|
{
|
||||||
var model = new CipherView(this);
|
var model = new CipherView(this);
|
||||||
@ -161,7 +164,8 @@ namespace Bit.Core.Models.Domain
|
|||||||
Favorite = Favorite,
|
Favorite = Favorite,
|
||||||
RevisionDate = RevisionDate,
|
RevisionDate = RevisionDate,
|
||||||
Type = Type,
|
Type = Type,
|
||||||
CollectionIds = CollectionIds.ToList()
|
CollectionIds = CollectionIds.ToList(),
|
||||||
|
DeletedDate = DeletedDate
|
||||||
};
|
};
|
||||||
BuildDataModel(this, c, new HashSet<string>
|
BuildDataModel(this, c, new HashSet<string>
|
||||||
{
|
{
|
||||||
|
@ -24,5 +24,6 @@ namespace Bit.Core.Models.Response
|
|||||||
public List<AttachmentResponse> Attachments { get; set; }
|
public List<AttachmentResponse> Attachments { get; set; }
|
||||||
public List<PasswordHistoryResponse> PasswordHistory { get; set; }
|
public List<PasswordHistoryResponse> PasswordHistory { get; set; }
|
||||||
public List<string> CollectionIds { 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;
|
LocalData = c.LocalData;
|
||||||
CollectionIds = c.CollectionIds;
|
CollectionIds = c.CollectionIds;
|
||||||
RevisionDate = c.RevisionDate;
|
RevisionDate = c.RevisionDate;
|
||||||
|
DeletedDate = c.DeletedDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
@ -43,6 +44,7 @@ namespace Bit.Core.Models.View
|
|||||||
public List<PasswordHistoryView> PasswordHistory { get; set; }
|
public List<PasswordHistoryView> PasswordHistory { get; set; }
|
||||||
public HashSet<string> CollectionIds { get; set; }
|
public HashSet<string> CollectionIds { get; set; }
|
||||||
public DateTime RevisionDate { get; set; }
|
public DateTime RevisionDate { get; set; }
|
||||||
|
public DateTime? DeletedDate { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public string SubTitle
|
public string SubTitle
|
||||||
@ -96,5 +98,6 @@ namespace Bit.Core.Models.View
|
|||||||
return Login.PasswordRevisionDate;
|
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);
|
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
|
#endregion
|
||||||
|
|
||||||
#region Attachments APIs
|
#region Attachments APIs
|
||||||
|
@ -265,6 +265,10 @@ namespace Bit.Core.Services
|
|||||||
var ciphers = await GetAllDecryptedAsync();
|
var ciphers = await GetAllDecryptedAsync();
|
||||||
return ciphers.Where(cipher =>
|
return ciphers.Where(cipher =>
|
||||||
{
|
{
|
||||||
|
if (cipher.IsDeleted)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (folder && cipher.FolderId == groupingId)
|
if (folder && cipher.FolderId == groupingId)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
@ -324,6 +328,11 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
foreach (var cipher in ciphers)
|
foreach (var cipher in ciphers)
|
||||||
{
|
{
|
||||||
|
if (cipher.IsDeleted)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (cipher.Type != CipherType.Login && (includeOtherTypes?.Any(t => t == cipher.Type) ?? false))
|
if (cipher.Type != CipherType.Login && (includeOtherTypes?.Any(t => t == cipher.Type) ?? false))
|
||||||
{
|
{
|
||||||
others.Add(cipher);
|
others.Add(cipher);
|
||||||
@ -695,6 +704,45 @@ namespace Bit.Core.Services
|
|||||||
return null;
|
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
|
// Helpers
|
||||||
|
|
||||||
private async Task ShareAttachmentWithServerAsync(AttachmentView attachmentView, string cipherId,
|
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,
|
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>();
|
var results = new List<CipherView>();
|
||||||
if (query != null)
|
if (query != null)
|
||||||
@ -68,7 +68,7 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<CipherView> SearchCiphersBasic(List<CipherView> ciphers, string query,
|
public List<CipherView> SearchCiphersBasic(List<CipherView> ciphers, string query,
|
||||||
CancellationToken ct = default(CancellationToken))
|
CancellationToken ct = default, bool deleted = false)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
query = query.Trim().ToLower();
|
query = query.Trim().ToLower();
|
||||||
|
@ -63,7 +63,7 @@ namespace Bit.iOS.Core.Views
|
|||||||
}
|
}
|
||||||
|
|
||||||
_allItems = combinedLogins
|
_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))
|
.Select(s => new CipherViewModel(s))
|
||||||
.ToList() ?? new List<CipherViewModel>();
|
.ToList() ?? new List<CipherViewModel>();
|
||||||
FilterResults(searchFilter, new CancellationToken());
|
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)
|
if (_deviceActionService.SystemMajorVersion() >= 12)
|
||||||
{
|
{
|
||||||
@ -142,7 +142,7 @@ namespace Bit.iOS
|
|||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentities();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (message.Command == "deletedCipher")
|
else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher")
|
||||||
{
|
{
|
||||||
if (_deviceActionService.SystemMajorVersion() >= 12)
|
if (_deviceActionService.SystemMajorVersion() >= 12)
|
||||||
{
|
{
|
||||||
@ -168,6 +168,11 @@ namespace Bit.iOS
|
|||||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher")
|
||||||
|
&& _deviceActionService.SystemMajorVersion() >= 12)
|
||||||
|
{
|
||||||
|
await ASHelpers.ReplaceAllIdentities();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return base.FinishedLaunching(app, options);
|
return base.FinishedLaunching(app, options);
|
||||||
|
Loading…
Reference in New Issue
Block a user