diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index c07a912cb5b..c7080614830 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -772,6 +772,11 @@ public void setIgnoreDustThreshold(int value) { requestPersistence(); } + public void setShowOffersMatchingMyAccounts(boolean value) { + prefPayload.setShowOffersMatchingMyAccounts(value); + requestPersistence(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Getter @@ -1081,5 +1086,7 @@ private interface ExcludesDelegateMethods { void setAutoConfirmSettings(AutoConfirmSettings autoConfirmSettings); void setHideNonAccountPaymentMethods(boolean hideNonAccountPaymentMethods); + + void setShowOffersMatchingMyAccounts(boolean value); } } diff --git a/core/src/main/java/bisq/core/user/PreferencesPayload.java b/core/src/main/java/bisq/core/user/PreferencesPayload.java index 82b800f13eb..03628791f36 100644 --- a/core/src/main/java/bisq/core/user/PreferencesPayload.java +++ b/core/src/main/java/bisq/core/user/PreferencesPayload.java @@ -131,6 +131,8 @@ public final class PreferencesPayload implements PersistableEnvelope { // Added in 1.5.5 private boolean hideNonAccountPaymentMethods; + private boolean showOffersMatchingMyAccounts; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -195,7 +197,8 @@ public Message toProtoMessage() { .addAllAutoConfirmSettings(autoConfirmSettingsList.stream() .map(autoConfirmSettings -> ((protobuf.AutoConfirmSettings) autoConfirmSettings.toProtoMessage())) .collect(Collectors.toList())) - .setHideNonAccountPaymentMethods(hideNonAccountPaymentMethods); + .setHideNonAccountPaymentMethods(hideNonAccountPaymentMethods) + .setShowOffersMatchingMyAccounts(showOffersMatchingMyAccounts); Optional.ofNullable(backupDirectory).ifPresent(builder::setBackupDirectory); Optional.ofNullable(preferredTradeCurrency).ifPresent(e -> builder.setPreferredTradeCurrency((protobuf.TradeCurrency) e.toProtoMessage())); @@ -290,7 +293,8 @@ public static PreferencesPayload fromProto(protobuf.PreferencesPayload proto, Co new ArrayList<>(proto.getAutoConfirmSettingsList().stream() .map(AutoConfirmSettings::fromProto) .collect(Collectors.toList())), - proto.getHideNonAccountPaymentMethods() + proto.getHideNonAccountPaymentMethods(), + proto.getShowOffersMatchingMyAccounts() ); } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index fec863f6746..4af5a97364c 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -342,6 +342,7 @@ offerbook.offerersAcceptedBankSeats=Accepted seat of bank countries (taker):\n { offerbook.availableOffers=Available offers offerbook.filterByCurrency=Filter by currency offerbook.filterByPaymentMethod=Filter by payment method +offerbook.matchingOffers=Offers matching my accounts offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=This account was verified and {0} offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java index 417fe20730d..6c3fa3d6c24 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java @@ -22,6 +22,7 @@ import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.AutoTooltipSlideToggleButton; import bisq.desktop.components.AutoTooltipTableColumn; import bisq.desktop.components.AutocompleteComboBox; import bisq.desktop.components.ColoredDecimalPlacesWithZerosText; @@ -126,6 +127,7 @@ public class OfferBookView extends ActivatableViewAndModel currencyComboBox; private AutocompleteComboBox paymentMethodComboBox; private AutoTooltipButton createOfferButton; + private AutoTooltipSlideToggleButton matchingOffersToggle; private AutoTooltipTableColumn amountColumn, volumeColumn, marketColumn, priceColumn, paymentMethodColumn, depositColumn, signingStateColumn, avatarColumn; private TableView tableView; @@ -174,10 +176,25 @@ public void initialize() { hBox.setSpacing(35); hBox.setPadding(new Insets(10, 0, 0, 0)); - final Tuple3> currencyBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( + Tuple3> currencyBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( Res.get("offerbook.filterByCurrency")); - final Tuple3> paymentBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( + currencyComboBox = currencyBoxTuple.third; + currencyComboBox.setPrefWidth(270); + + Tuple3> paymentBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( Res.get("offerbook.filterByPaymentMethod")); + paymentMethodComboBox = paymentBoxTuple.third; + paymentMethodComboBox.setCellFactory(GUIUtil.getPaymentMethodCellFactory()); + paymentMethodComboBox.setPrefWidth(270); + + matchingOffersToggle = new AutoTooltipSlideToggleButton(); + matchingOffersToggle.setText(Res.get("offerbook.matchingOffers")); + HBox.setMargin(matchingOffersToggle, new Insets(7, 0, -9, -15)); + + hBox.getChildren().addAll(currencyBoxTuple.first, paymentBoxTuple.first, matchingOffersToggle); + AnchorPane.setLeftAnchor(hBox, 0d); + AnchorPane.setTopAnchor(hBox, 0d); + AnchorPane.setBottomAnchor(hBox, 0d); createOfferButton = new AutoTooltipButton(); createOfferButton.setMinHeight(40); @@ -185,11 +202,6 @@ public void initialize() { AnchorPane.setRightAnchor(createOfferButton, 0d); AnchorPane.setBottomAnchor(createOfferButton, 0d); - hBox.getChildren().addAll(currencyBoxTuple.first, paymentBoxTuple.first, createOfferButton); - AnchorPane.setLeftAnchor(hBox, 0d); - AnchorPane.setTopAnchor(hBox, 0d); - AnchorPane.setBottomAnchor(hBox, 0d); - AnchorPane anchorPane = new AnchorPane(); anchorPane.getChildren().addAll(hBox, createOfferButton); @@ -199,11 +211,6 @@ public void initialize() { GridPane.setMargin(anchorPane, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 0)); root.getChildren().add(anchorPane); - currencyComboBox = currencyBoxTuple.third; - - paymentMethodComboBox = paymentBoxTuple.third; - paymentMethodComboBox.setCellFactory(GUIUtil.getPaymentMethodCellFactory()); - tableView = new TableView<>(); GridPane.setRowIndex(tableView, ++gridRow); @@ -328,6 +335,10 @@ protected void activate() { currencyComboBox.getEditor().setText(new CurrencyStringConverter(currencyComboBox).toString(currencyComboBox.getSelectionModel().getSelectedItem())); + matchingOffersToggle.setSelected(model.useOffersMatchingMyAccountsFilter); + matchingOffersToggle.disableProperty().bind(model.disableMatchToggle); + matchingOffersToggle.setOnAction(e -> model.onShowOffersMatchingMyAccounts(matchingOffersToggle.isSelected())); + volumeColumn.sortableProperty().bind(model.showAllTradeCurrenciesProperty.not()); model.getOfferList().comparatorProperty().bind(tableView.comparatorProperty()); @@ -424,6 +435,8 @@ private void updateSigningStateColumn() { @Override protected void deactivate() { createOfferButton.setOnAction(null); + matchingOffersToggle.setOnAction(null); + matchingOffersToggle.disableProperty().unbind(); model.getOfferList().comparatorProperty().unbind(); volumeColumn.sortableProperty().unbind(); @@ -1024,6 +1037,10 @@ public void updateItem(final OfferBookListItem item, boolean empty) { final Offer offer = item.getOffer(); boolean myOffer = model.isMyOffer(offer); if (tableRow != null) { + // this code is duplicated in model.getOffersMatchingMyAccountsPredicate but as + // we want to pass the results for displaying relevant info in popups we + // cannot simply replace it with the predicate. If there are any changes we + // need to maintain both. isPaymentAccountValidForOffer = model.isAnyPaymentAccountValidForOffer(offer); isInsufficientCounterpartyTradeLimit = model.isInsufficientCounterpartyTradeLimit(offer); hasSameProtocolVersion = model.hasSameProtocolVersion(offer); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java index 92b97e1e29b..2a69f863fca 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -78,16 +78,19 @@ import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import javafx.collections.SetChangeListener; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import java.text.DecimalFormat; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -122,15 +125,19 @@ class OfferBookViewModel extends ActivatableViewModel { // If id is empty string we ignore filter (display all methods) - PaymentMethod selectedPaymentMethod = PaymentMethod.getDummyPaymentMethod(GUIUtil.SHOW_ALL_FLAG); + PaymentMethod selectedPaymentMethod = getShowAllEntryForPaymentMethod(); private boolean isTabSelected; final BooleanProperty showAllTradeCurrenciesProperty = new SimpleBooleanProperty(true); + final BooleanProperty disableMatchToggle = new SimpleBooleanProperty(); final IntegerProperty maxPlacesForAmount = new SimpleIntegerProperty(); final IntegerProperty maxPlacesForVolume = new SimpleIntegerProperty(); final IntegerProperty maxPlacesForPrice = new SimpleIntegerProperty(); final IntegerProperty maxPlacesForMarketPriceMargin = new SimpleIntegerProperty(); boolean showAllPaymentMethods = true; + boolean useOffersMatchingMyAccountsFilter; + private final Map myInsufficientTradeLimitCache = new HashMap<>(); + private final Map insufficientCounterpartyTradeLimitCache = new HashMap<>(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -206,6 +213,12 @@ public OfferBookViewModel(User user, highestMarketPriceMarginOffer.ifPresent(offerBookListItem -> maxPlacesForMarketPriceMargin.set(formatMarketPriceMargin(offerBookListItem.getOffer(), false).length())); }; + + // If our accounts have changed we reset our myInsufficientTradeLimitCache as it depends on account data + if (user != null) { + user.getPaymentAccountsAsObservable().addListener((SetChangeListener) c -> + myInsufficientTradeLimitCache.clear()); + } } @Override @@ -213,7 +226,7 @@ protected void activate() { filteredItems.addListener(filterItemsListener); String code = direction == OfferPayload.Direction.BUY ? preferences.getBuyScreenCurrencyCode() : preferences.getSellScreenCurrencyCode(); - if (code != null && !code.equals(GUIUtil.SHOW_ALL_FLAG) && !code.isEmpty() && + if (code != null && !code.isEmpty() && !isShowAllEntry(code) && CurrencyUtil.getTradeCurrency(code).isPresent()) { showAllTradeCurrenciesProperty.set(false); selectedTradeCurrency = CurrencyUtil.getTradeCurrency(code).get(); @@ -223,10 +236,15 @@ protected void activate() { } tradeCurrencyCode.set(selectedTradeCurrency.getCode()); + if (user != null) { + disableMatchToggle.set(user.getPaymentAccounts() == null || user.getPaymentAccounts().isEmpty()); + } + useOffersMatchingMyAccountsFilter = !disableMatchToggle.get() && isShowOffersMatchingMyAccounts(); + fillAllTradeCurrencies(); preferences.getTradeCurrenciesAsObservable().addListener(tradeCurrencyListChangeListener); offerBook.fillOfferBookListItems(); - applyFilterPredicate(); + filterOffers(); setMarketPriceFeedCurrency(); priceUtil.recalculateBsq30DayAveragePrice(); @@ -238,6 +256,7 @@ protected void deactivate() { preferences.getTradeCurrenciesAsObservable().removeListener(tradeCurrencyListChangeListener); } + /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @@ -268,7 +287,7 @@ else if (!showAllEntry) { } setMarketPriceFeedCurrency(); - applyFilterPredicate(); + filterOffers(); if (direction == OfferPayload.Direction.BUY) preferences.setBuyScreenCurrencyCode(code); @@ -288,23 +307,34 @@ void onSetPaymentMethod(PaymentMethod paymentMethod) { // If we select TransferWise we switch to show all currencies as TransferWise supports // sending to most currencies. if (paymentMethod.getId().equals(PaymentMethod.TRANSFERWISE_ID)) { - onSetTradeCurrency(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); + onSetTradeCurrency(getShowAllEntryForCurrency()); } } else { - this.selectedPaymentMethod = PaymentMethod.getDummyPaymentMethod(GUIUtil.SHOW_ALL_FLAG); + this.selectedPaymentMethod = getShowAllEntryForPaymentMethod(); } - applyFilterPredicate(); + filterOffers(); } void onRemoveOpenOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { openOfferManager.removeOffer(offer, resultHandler, errorMessageHandler); } + void onShowOffersMatchingMyAccounts(boolean isSelected) { + useOffersMatchingMyAccountsFilter = isSelected; + preferences.setShowOffersMatchingMyAccounts(useOffersMatchingMyAccountsFilter); + filterOffers(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// + boolean isShowOffersMatchingMyAccounts() { + return preferences.isShowOffersMatchingMyAccounts(); + } + SortedList getOfferList() { return sortedItems; } @@ -348,7 +378,7 @@ ObservableList getPaymentMethods() { } list.sort(Comparator.naturalOrder()); - list.add(0, PaymentMethod.getDummyPaymentMethod(GUIUtil.SHOW_ALL_FLAG)); + list.add(0, getShowAllEntryForPaymentMethod()); return list; } @@ -528,11 +558,12 @@ private void setMarketPriceFeedCurrency() { private void fillAllTradeCurrencies() { allTradeCurrencies.clear(); // Used for ignoring filter (show all) - allTradeCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); + allTradeCurrencies.add(getShowAllEntryForCurrency()); allTradeCurrencies.addAll(preferences.getTradeCurrenciesAsObservable()); - allTradeCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, "")); + allTradeCurrencies.add(getEditEntryForCurrency()); } + /////////////////////////////////////////////////////////////////////////////////////////// // Checks /////////////////////////////////////////////////////////////////////////////////////////// @@ -560,8 +591,15 @@ boolean canCreateOrTakeOffer() { // Filters /////////////////////////////////////////////////////////////////////////////////////////// - private void applyFilterPredicate() { - filteredItems.setPredicate(offerBookListItem -> { + private void filterOffers() { + Predicate predicate = useOffersMatchingMyAccountsFilter ? + getCurrencyAndMethodPredicate().and(getOffersMatchingMyAccountsPredicate()) : + getCurrencyAndMethodPredicate(); + filteredItems.setPredicate(predicate); + } + + private Predicate getCurrencyAndMethodPredicate() { + return offerBookListItem -> { Offer offer = offerBookListItem.getOffer(); boolean directionResult = offer.getDirection() != direction; boolean currencyResult = (showAllTradeCurrenciesProperty.get()) || @@ -570,7 +608,37 @@ private void applyFilterPredicate() { offer.getPaymentMethod().equals(selectedPaymentMethod); boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook(); return directionResult && currencyResult && paymentMethodResult && notMyOfferOrShowMyOffersActivated; - }); + }; + } + + private Predicate getOffersMatchingMyAccountsPredicate() { + // This code duplicates code in the view at the button column. We need there the different results for + // display in popups so we cannot replace that with the predicate. Any change need to be applied in both + // places. + return offerBookListItem -> { + Offer offer = offerBookListItem.getOffer(); + boolean isPaymentAccountValidForOffer = isAnyPaymentAccountValidForOffer(offer); + boolean isInsufficientCounterpartyTradeLimit = isInsufficientCounterpartyTradeLimit(offer); + boolean hasSameProtocolVersion = hasSameProtocolVersion(offer); + boolean isIgnored = isIgnored(offer); + boolean isOfferBanned = isOfferBanned(offer); + boolean isCurrencyBanned = isCurrencyBanned(offer); + boolean isPaymentMethodBanned = isPaymentMethodBanned(offer); + boolean isNodeAddressBanned = isNodeAddressBanned(offer); + boolean requireUpdateToNewVersion = requireUpdateToNewVersion(); + boolean isMyInsufficientTradeLimit = isMyInsufficientTradeLimit(offer); + boolean isTradable = isPaymentAccountValidForOffer && + !isInsufficientCounterpartyTradeLimit && + hasSameProtocolVersion && + !isIgnored && + !isOfferBanned && + !isCurrencyBanned && + !isPaymentMethodBanned && + !isNodeAddressBanned && + !requireUpdateToNewVersion && + !isMyInsufficientTradeLimit; + return isTradable; + }; } boolean isIgnored(Offer offer) { @@ -598,13 +666,28 @@ boolean requireUpdateToNewVersion() { return filterManager.requireUpdateToNewVersionForTrading(); } + // This call is a bit expensive so we cache results boolean isInsufficientCounterpartyTradeLimit(Offer offer) { - return CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && - !accountAgeWitnessService.verifyPeersTradeAmount(offer, offer.getAmount(), errorMessage -> { - }); + String offerId = offer.getId(); + if (insufficientCounterpartyTradeLimitCache.containsKey(offerId)) { + return insufficientCounterpartyTradeLimitCache.get(offerId); + } + + boolean result = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && + !accountAgeWitnessService.verifyPeersTradeAmount(offer, offer.getAmount(), + errorMessage -> { + }); + insufficientCounterpartyTradeLimitCache.put(offerId, result); + return result; } + // This call is a bit expensive so we cache results boolean isMyInsufficientTradeLimit(Offer offer) { + String offerId = offer.getId(); + if (myInsufficientTradeLimitCache.containsKey(offerId)) { + return myInsufficientTradeLimitCache.get(offerId); + } + Optional accountOptional = getMostMaturePaymentAccountForOffer(offer); long myTradeLimit = accountOptional .map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount, @@ -615,9 +698,11 @@ boolean isMyInsufficientTradeLimit(Offer offer) { accountOptional.isPresent() ? accountOptional.get().getAccountName() : "null", Coin.valueOf(myTradeLimit).toFriendlyString(), Coin.valueOf(offerMinAmount).toFriendlyString()); - return CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && + boolean result = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && accountOptional.isPresent() && myTradeLimit < offerMinAmount; + myInsufficientTradeLimitCache.put(offerId, result); + return result; } boolean hasSameProtocolVersion(Offer offer) { @@ -645,11 +730,11 @@ int getNumTrades(Offer offer) { public boolean hasSelectionAccountSigning() { if (showAllTradeCurrenciesProperty.get()) { - if (!selectedPaymentMethod.getId().equals(GUIUtil.SHOW_ALL_FLAG)) { + if (!isShowAllEntry(selectedPaymentMethod.getId())) { return PaymentMethod.hasChargebackRisk(selectedPaymentMethod); } } else { - if (selectedPaymentMethod.getId().equals(GUIUtil.SHOW_ALL_FLAG)) + if (isShowAllEntry(selectedPaymentMethod.getId())) return CurrencyUtil.getMatureMarketCurrencies().stream() .anyMatch(c -> c.getCode().equals(selectedTradeCurrency.getCode())); else @@ -675,4 +760,16 @@ public String formatDepositString(Coin deposit, long amount) { var percentage = FormattingUtils.formatToRoundedPercentWithSymbol(deposit.getValue() / (double) amount); return btcFormatter.formatCoin(deposit) + " (" + percentage + ")"; } + + private TradeCurrency getShowAllEntryForCurrency() { + return new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, ""); + } + + private TradeCurrency getEditEntryForCurrency() { + return new CryptoCurrency(GUIUtil.EDIT_FLAG, ""); + } + + private PaymentMethod getShowAllEntryForPaymentMethod() { + return PaymentMethod.getDummyPaymentMethod(GUIUtil.SHOW_ALL_FLAG); + } } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 65c2ea8ae09..0a3a44364d7 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1616,6 +1616,7 @@ message PreferencesPayload { repeated AutoConfirmSettings auto_confirm_settings = 56; double bsq_average_trim_threshold = 57; bool hide_non_account_payment_methods = 58; + bool show_offers_matching_my_accounts = 59; } message AutoConfirmSettings {