refactor pocket selection to handle labels
MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/Send/PrivacyControlViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/Send/PrivacyControlViewModel.cs
@@ -2,10 +2,10 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
-using DynamicData;
-using DynamicData.Aggregation;
-using NBitcoin;
+using System.Reactive.Linq;
+using Avalonia.Threading;
using ReactiveUI;
+using WalletWasabi.Blockchain.Analysis.Clustering;
using WalletWasabi.Blockchain.TransactionOutputs;
using WalletWasabi.Fluent.Helpers;
using WalletWasabi.Fluent.ViewModels.Dialogs.Base;
@@ -18,14 +18,11 @@ public partial class PrivacyControlViewModel : DialogViewModelBase<IEnumerable<S
{
private readonly Wallet _wallet;
private readonly TransactionInfo _transactionInfo;
+
private readonly bool _isSilent;
- private readonly SourceList<PocketViewModel> _pocketSource;
- private readonly ReadOnlyObservableCollection<PocketViewModel> _pockets;
- private readonly IObservableList<PocketViewModel> _selectedPockets;
- [AutoNotify] private decimal _stillNeeded;
- [AutoNotify] private bool _enoughSelected;
- [AutoNotify] private bool _isWarningOpen;
+ private List<PocketViewModel> _usedPockets;
+ private bool _isUpdating;
public PrivacyControlViewModel(Wallet wallet, TransactionInfo transactionInfo, bool isSilent)
{
@@ -33,71 +30,89 @@ public partial class PrivacyControlViewModel : DialogViewModelBase<IEnumerable<S
_transactionInfo = transactionInfo;
_isSilent = isSilent;
- _pocketSource = new SourceList<PocketViewModel>();
+ _usedPockets = _wallet.Coins.GetPockets(_wallet.ServiceConfiguration.MinAnonScoreTarget).Select(x => new PocketViewModel(x)).ToList();
+ Labels = new ObservableCollection<string>();
+ MustHaveLabels = new ObservableCollection<string>();
- _pocketSource.Connect()
- .Bind(out _pockets)
- .Subscribe();
+ SetupCancel(enableCancel: false, enableCancelOnEscape: true, enableCancelOnPressed: false);
+ EnableBack = true;
- var selected = _pocketSource.Connect()
- .AutoRefresh()
- .Filter(x => x.IsSelected);
+ NextCommand = ReactiveCommand.Create(Complete);
- _selectedPockets = selected.AsObservableList();
+ EnableAutoBusyOn(NextCommand);
- selected.Sum(x => x.TotalBtc)
- .Subscribe(x =>
+ Observable
+ .FromEventPattern(Labels, nameof(Labels.CollectionChanged))
+ .Subscribe(_ =>
{
- IsWarningOpen = _selectedPockets.Count > 1 && _selectedPockets.Items.Any(x => x.Labels == CoinPocketHelper.PrivateFundsText);
-
- StillNeeded = transactionInfo.Amount.ToDecimal(MoneyUnit.BTC) - x;
- EnoughSelected = StillNeeded <= 0;
- });
+ if (_isUpdating)
+ {
+ return;
+ }
- StillNeeded = transactionInfo.Amount.ToDecimal(MoneyUnit.BTC);
+ var pocketsToRemove = _usedPockets
+ .Where(x => x.Labels.Any(y => !Labels.Contains(y) && x.Labels.Any(y => !MustHaveLabels.Contains(y))))
+ .ToArray();
- SetupCancel(enableCancel: false, enableCancelOnEscape: true, enableCancelOnPressed: false);
- EnableBack = true;
+ Dispatcher.UIThread.Post(() =>
+ {
+ _isUpdating = true;
- NextCommand = ReactiveCommand.Create(Complete, this.WhenAnyValue(x => x.EnoughSelected));
+ _usedPockets = _usedPockets.Except(pocketsToRemove).ToList();
+ Labels.Clear();
+ UpdateLabels();
- EnableAutoBusyOn(NextCommand);
+ _isUpdating = false;
+ });
+ });
}
- public ReadOnlyObservableCollection<PocketViewModel> Pockets => _pockets;
+ public ObservableCollection<string> Labels { get; set; }
+
+ public ObservableCollection<string> MustHaveLabels { get; }
private void Complete()
{
- Close(DialogResultKind.Normal, _selectedPockets.Items.SelectMany(x => x.Coins).ToArray());
+ Close(DialogResultKind.Normal, _usedPockets.SelectMany(x => x.Coins).ToArray());
}
- protected override void OnNavigatedTo(bool isInHistory, CompositeDisposable disposables)
+ private void UpdateLabels()
{
- base.OnNavigatedTo(isInHistory, disposables);
-
- if (!isInHistory)
+ foreach (var label in SmartLabel.Merge(_usedPockets.Select(x => x.Labels)))
{
- var pockets = _wallet.Coins.GetPockets(_wallet.ServiceConfiguration.MinAnonScoreTarget).Select(x => new PocketViewModel(x));
+ var coinsWithSameLabel = _usedPockets.Where(x => x.Labels.Contains(label));
- _pocketSource.AddRange(pockets);
+ if (_usedPockets.Sum(x => x.Coins.TotalAmount()) - coinsWithSameLabel.Sum(x => x.Coins.TotalAmount()) >= _transactionInfo.Amount)
+ {
+ Labels.Add(label);
}
-
- foreach (var pocket in _pockets)
+ else if (!MustHaveLabels.Contains(label))
{
- pocket.IsSelected = false;
+ MustHaveLabels.Add(label);
+ }
+ }
}
- if (_pocketSource.Count == 1)
+ protected override void OnNavigatedTo(bool isInHistory, CompositeDisposable disposables)
{
- _pocketSource.Items.First().IsSelected = true;
+ base.OnNavigatedTo(isInHistory, disposables);
+ if (!isInHistory)
+ {
+ _isUpdating = true;
+ UpdateLabels();
+ _isUpdating = false;
+ }
+
+ if (_usedPockets.Count == 1)
+ {
Complete();
}
else if (_isSilent &&
- _pocketSource.Items.FirstOrDefault(x => x.Labels == CoinPocketHelper.PrivateFundsText) is { } privatePocket &&
+ _usedPockets.FirstOrDefault(x => x.Labels == CoinPocketHelper.PrivateFundsText) is { } privatePocket &&
privatePocket.Coins.TotalAmount() >= _transactionInfo.Amount)
{
- privatePocket.IsSelected = true;
+ _usedPockets = _usedPockets.Where(x => x.Labels == CoinPocketHelper.PrivateFundsText).ToList();
Complete();
}
}

MODIFY
WalletWasabi.Fluent/Views/Wallets/Send/PrivacyControlView.axaml
WalletWasabi.Fluent/Views/Wallets/Send/PrivacyControlView.axaml
IsBusy="{Binding IsBusy}"
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<DockPanel>
-
- <!-- Warning messages -->
- <StackPanel Margin="0 10 0 0" DockPanel.Dock="Bottom" Spacing="5">
- <Panel.Styles>
- <Style Selector="c|InfoMessage[IsVisible=False]">
- <Setter Property="Opacity" Value="0" />
- </Style>
- <Style Selector="c|InfoMessage">
- <Setter Property="HorizontalAlignment" Value="Center" />
- </Style>
- </Panel.Styles>
-
- <c:InfoMessage Foreground="{DynamicResource WarningMessageForeground}"
- IsVisible="{Binding !EnoughSelected}">
- <StackPanel Orientation="Horizontal">
- <TextBlock Text="Insufficient funds selected. Select at least another " />
- <TextBlock Text="{Binding StillNeeded}" />
- <TextBlock Text=" BTC" />
- </StackPanel>
- </c:InfoMessage>
-
- <c:InfoMessage Content="Warning, using both private and non-private funds in the same transaction can destroy your privacy."
- IsVisible="{Binding IsWarningOpen}" Foreground="{DynamicResource SystemErrorTextColor}" />
-
- <c:InfoMessage Content="Not in a hurry? Consider making more of your funds private by CoinJoining before making this transaction.">
- <c:InfoMessage.IsVisible>
- <MultiBinding Converter="{x:Static BoolConverters.And}">
- <Binding Path="EnoughSelected" />
- <Binding Path="IsWarningOpen" Converter="{x:Static BoolConverters.Not}" />
- </MultiBinding>
- </c:InfoMessage.IsVisible>
- </c:InfoMessage>
- </StackPanel>
-
- <!-- List -->
- <DataBox Items="{Binding Pockets}">
- <DataBox.Resources>
- <SolidColorBrush x:Key="DataGridRowIndicatorBrush" Color="Transparent" />
- <SolidColorBrush x:Key="DataGridRowSelectedBackgroundColor" Color="Transparent" />
- <StaticResource x:Key="DataGridRowSelectedUnfocusedBackgroundColor"
- ResourceKey="DataGridRowSelectedBackgroundColor" />
- </DataBox.Resources>
- <DataBox.Columns>
- <DataBoxTemplateColumn CanUserSort="False" CanUserResize="False" CanUserReorder="False">
- <DataBoxTemplateColumn.CellTemplate>
- <DataTemplate x:DataType="vm:PocketViewModel">
- <CheckBox HorizontalAlignment="Center" IsChecked="{Binding IsSelected}" Focusable="False" />
- </DataTemplate>
- </DataBoxTemplateColumn.CellTemplate>
- </DataBoxTemplateColumn>
-
- <DataBoxTextColumn x:DataType="vm:PocketViewModel"
- CanUserSort="False"
- Header="Total (BTC)"
- Binding="{Binding TotalBtc}" />
-
- <DataBoxTemplateColumn CanUserSort="False" Width="*" Header="Labels">
- <DataBoxTemplateColumn.CellTemplate>
- <DataTemplate x:DataType="vm:PocketViewModel">
- <c:TagsBox Items="{Binding Labels}" IsReadOnly="True" HorizontalAlignment="Left" />
- </DataTemplate>
- </DataBoxTemplateColumn.CellTemplate>
- </DataBoxTemplateColumn>
- </DataBox.Columns>
- </DataBox>
+ <c:TagsBox Items="{Binding MustHaveLabels}" IsReadOnly="True" DockPanel.Dock="Bottom" />
+ <TextBlock Text="Must have:" DockPanel.Dock="Bottom" />
+ <c:TagsBox Items="{Binding Labels}" EnableDelete="True" />
</DockPanel>
</c:ContentArea>
</UserControl>

refactor pocket selection to handle labels