From bffd094263c97124befe1063af6df786c42af66e Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 13 Sep 2024 13:57:47 +0700 Subject: [PATCH 1/5] Add getQuantityKey method --- i18n/src/main/java/bisq/i18n/Res.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/i18n/src/main/java/bisq/i18n/Res.java b/i18n/src/main/java/bisq/i18n/Res.java index 9c15fbd7dd..4942d58d67 100644 --- a/i18n/src/main/java/bisq/i18n/Res.java +++ b/i18n/src/main/java/bisq/i18n/Res.java @@ -63,6 +63,16 @@ public static void setLanguage(String languageCode) { ); } + public static String getQuantityKey(String key, double number) { + if (number == 0) { + return get(key + ".0"); + } else if (number == 1) { + return get(key + ".1"); + } else { + return get(key + ".*", number); + } + } + public static String get(String key, Object... arguments) { return MessageFormat.format(get(key), arguments); } From f296c7f73c0d772c82328807a9a844a80f588140 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 13 Sep 2024 13:58:03 +0700 Subject: [PATCH 2/5] Improve trade wizard --- .../bisq_easy/components/AmountComponent.java | 2 +- .../bisq_easy/components/AmountInput.java | 12 +- .../amount/TradeWizardAmountController.java | 329 ++++++++++++++---- .../amount/TradeWizardAmountModel.java | 4 + .../amount/TradeWizardAmountView.java | 29 +- .../TradeWizardSelectOfferController.java | 16 +- .../bisq_easy/BisqEasyTradeAmountLimits.java | 22 ++ i18n/src/main/resources/bisq_easy.properties | 18 +- 8 files changed, 326 insertions(+), 106 deletions(-) diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountComponent.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountComponent.java index 845be0c131..649ffaae10 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountComponent.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountComponent.java @@ -286,7 +286,7 @@ public void onActivate() { quoteSideAmountInput.setAmount(exactAmount.round(0)); } else { log.warn("price.quoteProperty().get() is null. We use a fiat value of 100 as default value."); - Fiat defaultQuoteSideAmount = Fiat.fromValue(1000000, model.getMarket().getQuoteCurrencyCode()); + Fiat defaultQuoteSideAmount = Fiat.fromFaceValue(100, model.getMarket().getQuoteCurrencyCode()); quoteSideAmountInput.setAmount(defaultQuoteSideAmount); } } else { diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountInput.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountInput.java index fb2524c138..bba97d8900 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountInput.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountInput.java @@ -20,6 +20,7 @@ import bisq.common.currency.Market; import bisq.common.monetary.Monetary; import bisq.common.util.MathUtils; +import bisq.common.util.StringUtils; import bisq.desktop.components.controls.validator.NumberValidator; import bisq.presentation.formatters.AmountFormatter; import bisq.presentation.parser.AmountParser; @@ -79,7 +80,7 @@ public void requestFocus() { protected static class Controller implements bisq.desktop.common.view.Controller { @Setter - protected Model model; + protected Model model; @Getter @Setter protected View view; @@ -111,7 +112,7 @@ private void onFocusChange(boolean hasFocus) { } private void onAmount(String value) { - if (isValueOrCodeNull(value, model.code.get())) { + if (StringUtils.isEmpty(value) || StringUtils.isEmpty(model.code.get())) { setAmountInvalid(); return; } @@ -124,9 +125,6 @@ private void onAmount(String value) { updateAmountIfNotFocused(value); } - private boolean isValueOrCodeNull(String value, String code) { - return value == null || code == null; - } private void handleInvalidValue() { if (!model.hasFocus) { @@ -219,7 +217,9 @@ private void onFocusChanged(ObservableValue observable, Boole } } - private void onAmountChanged(ObservableValue observable, Monetary oldValue, Monetary newValue) { + private void onAmountChanged(ObservableValue observable, + Monetary oldValue, + Monetary newValue) { applyAmount(newValue); } diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountController.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountController.java index 151da357a6..67ad0a528d 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountController.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountController.java @@ -190,37 +190,7 @@ public void updateQuoteSideAmountSpecWithPriceSpec(PriceSpec priceSpec) { maxOrFixAmountComponent.setQuote(priceQuote.get()); OfferAmountUtil.updateQuoteSideAmountSpecWithPriceSpec(marketPriceService, amountSpec, priceSpec, market) - .ifPresent(quoteSideAmountSpec -> { - // TODO we do not change min/max amounts if price has changed as its confusing to user. - // we prefer that users could breach the min/max defined in the AmountComponent - // the min/max as btc amount is anyway not great. better would be a market specific min/max. - //todo Move AmountComponent.MIN_RANGE_BASE_SIDE_VALUE to config - /* long minQuoteSideValueValue = PriceUtil.findQuote(marketPriceService, priceSpec, market) - .map(priceQuote -> priceQuote.toQuoteSideMonetary(AmountComponent.MIN_RANGE_BASE_SIDE_VALUE)) - .map(Monetary::getValue) - .orElseThrow(); - long maxQuoteSideValueValue = PriceUtil.findQuote(marketPriceService, priceSpec, market) - .map(priceQuote -> priceQuote.toQuoteSideMonetary(AmountComponent.MAX_RANGE_BASE_SIDE_VALUE)) - .map(Monetary::getValue) - .orElseThrow(); - if (quoteSideAmountSpec instanceof QuoteSideFixedAmountSpec) { - QuoteSideFixedAmountSpec fixedAmountSpec = (QuoteSideFixedAmountSpec) quoteSideAmountSpec; - if (fixedAmountSpec.getAmount() < minQuoteSideValueValue) { - quoteSideAmountSpec = new QuoteSideFixedAmountSpec(minQuoteSideValueValue); - } else if (fixedAmountSpec.getAmount() > maxQuoteSideValueValue) { - quoteSideAmountSpec = new QuoteSideFixedAmountSpec(maxQuoteSideValueValue); - } - } else if (quoteSideAmountSpec instanceof QuoteSideRangeAmountSpec) { - QuoteSideRangeAmountSpec rangeAmountSpec = (QuoteSideRangeAmountSpec) quoteSideAmountSpec; - long minAmount = Math.max(minQuoteSideValueValue, rangeAmountSpec.getMinAmount()); - long maxAmount = Math.min(maxQuoteSideValueValue, rangeAmountSpec.getMaxAmount()); - checkArgument(minAmount <= maxAmount); - quoteSideAmountSpec = new QuoteSideRangeAmountSpec(minAmount, maxAmount); - } else { - throw new RuntimeException("Unsupported amountSpec: {}" + quoteSideAmountSpec); - }*/ - model.getQuoteSideAmountSpec().set(quoteSideAmountSpec); - }); + .ifPresent(quoteSideAmountSpec -> model.getQuoteSideAmountSpec().set(quoteSideAmountSpec)); } public void reset() { @@ -349,6 +319,8 @@ public void onDeactivate() { priceTooltipPin.unsubscribe(); view.getRoot().setOnKeyPressed(null); model.getIsAmountLimitInfoOverlayVisible().set(false); + //todo + // maxOrFixAmountComponent.reset(); } void onSetReputationBasedAmount() { @@ -458,8 +430,7 @@ private Optional findBestOfferQuote() { return bestOffersPrice; } - // Used for finding best price quote of available matching offers - private boolean filterOffers(BisqEasyOffer peersOffer) { + private boolean filterOffersByAmounts(BisqEasyOffer peersOffer) { try { Optional optionalMakersUserProfile = userProfileService.findUserProfile(peersOffer.getMakersUserProfileId()); if (optionalMakersUserProfile.isEmpty()) { @@ -530,7 +501,10 @@ private Optional getMarketPriceQuote() { private void quoteSideAmountsChanged(boolean maxAmountChanged) { Monetary minQuoteSideAmount = minAmountComponent.getQuoteSideAmount().get(); Monetary maxOrFixedQuoteSideAmount = maxOrFixAmountComponent.getQuoteSideAmount().get(); - boolean insecureValue = maxOrFixedQuoteSideAmount.isGreaterThan(maxOrFixAmountComponent.getReputationBasedQuoteSideAmount()); + + model.getIsAmountHyperLinkDisabled().set(false); + boolean insecureValue = maxOrFixAmountComponent.getReputationBasedQuoteSideAmount() != null && + maxOrFixedQuoteSideAmount.isGreaterThan(maxOrFixAmountComponent.getReputationBasedQuoteSideAmount()); model.getIsWarningIconVisible().set(insecureValue); long highestScore = reputationService.getScoreByUserProfileId().entrySet().stream() @@ -554,6 +528,7 @@ private void quoteSideAmountsChanged(boolean maxAmountChanged) { .orElseThrow(); boolean noReputationNeededForMaxOrFixedAmount = maxOrFixedQuoteSideAmount.isLessThanOrEqual(amountWithoutReputationNeeded); if (model.getDirection().isBuy()) { + // Buyer String formattedAmountWithoutReputationNeeded = formatAmountWithCode(amountWithoutReputationNeeded); String formattedMaxOrFixedAmount = formatAmountWithCode(maxOrFixedQuoteSideAmount); if (model.getIsMinAmountEnabled().get()) { @@ -568,7 +543,7 @@ private void quoteSideAmountsChanged(boolean maxAmountChanged) { // min < 25 USD, max > 25 USD model.getAmountLimitInfoLeadLine().set(Res.get("bisqEasy.tradeWizard.amount.buyer.noReputationNeededForMinAmount.limitInfo.leadLine", formattedAmountWithoutReputationNeeded)); - String numSellers = Res.get("bisqEasy.tradeWizard.amount.buyer.numSellers." + Math.min(numPotentialTakersForMaxOrFixedAmount, 2), numPotentialTakersForMaxOrFixedAmount); + String numSellers = Res.getQuantityKey("bisqEasy.tradeWizard.amount.buyer.numSellers", numPotentialTakersForMaxOrFixedAmount); model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.noReputationNeededForMinAmount.limitInfo", formattedMaxOrFixedAmount, numSellers)); model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.noReputationNeededForMinAmount.limitInfo.overlay.info", @@ -577,50 +552,108 @@ private void quoteSideAmountsChanged(boolean maxAmountChanged) { // min > 25 USD model.getIsWarningIconVisible().set(numPotentialTakersForMaxOrFixedAmount == 0); if (maxAmountChanged) { - String numSellers = Res.get("bisqEasy.tradeWizard.amount.buyer.numSellers." + Math.min(numPotentialTakersForMaxOrFixedAmount, 2), numPotentialTakersForMaxOrFixedAmount); + String numSellers = Res.getQuantityKey("bisqEasy.tradeWizard.amount.buyer.numSellers", numPotentialTakersForMaxOrFixedAmount); model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo", numSellers, formattedMaxOrFixedAmount)); model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.overlay.info", formattedMaxOrFixedAmount, requiredReputationScoreForMaxOrFixedAmount) + "\n\n"); } else { - String numSellers = Res.get("bisqEasy.tradeWizard.amount.buyer.numSellers." + Math.min(numPotentialTakersForMinAmount, 2), numPotentialTakersForMinAmount); + String numSellers = Res.getQuantityKey("bisqEasy.tradeWizard.amount.buyer.numSellers", numPotentialTakersForMinAmount); model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo", numSellers, formattedMinAmount)); model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.overlay.info", formattedMinAmount, requiredReputationScoreForMinAmount) + "\n\n"); } } } else { - if (noReputationNeededForMaxOrFixedAmount) { - // max < 25 USD (inherently also min < 25 USD) - model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.noReputationNeededForMaxOrFixedAmount", formattedAmountWithoutReputationNeeded)); - model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.overlay.noReputationNeededForMaxOrFixedAmount.info", formattedMaxOrFixedAmount, formattedAmountWithoutReputationNeeded) + "\n\n"); - model.getIsWarningIconVisible().set(true); + if (model.isCreateOfferMode()) { + if (noReputationNeededForMaxOrFixedAmount) { + // max < 25 USD (inherently also min < 25 USD) + model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.noReputationNeededForMaxOrFixedAmount", formattedAmountWithoutReputationNeeded)); + model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.overlay.noReputationNeededForMaxOrFixedAmount.info", formattedMaxOrFixedAmount, formattedAmountWithoutReputationNeeded) + "\n\n"); + model.getIsWarningIconVisible().set(true); + } else { + // min > 25 USD + String numSellers = Res.getQuantityKey("bisqEasy.tradeWizard.amount.buyer.numSellers", numPotentialTakersForMaxOrFixedAmount); + model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo", numSellers, formattedMaxOrFixedAmount)); + model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.overlay.info", formattedMaxOrFixedAmount, requiredReputationScoreForMaxOrFixedAmount) + "\n\n"); + model.getIsWarningIconVisible().set(numPotentialTakersForMaxOrFixedAmount == 0); + } } else { - // min > 25 USD - String numSellers = Res.get("bisqEasy.tradeWizard.amount.buyer.numSellers." + Math.min(numPotentialTakersForMaxOrFixedAmount, 2), numPotentialTakersForMaxOrFixedAmount); - model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo", numSellers, formattedMaxOrFixedAmount)); - model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.overlay.info", formattedMaxOrFixedAmount, requiredReputationScoreForMaxOrFixedAmount) + "\n\n"); - model.getIsWarningIconVisible().set(numPotentialTakersForMaxOrFixedAmount == 0); + long numMatchingOffers = getNumMatchingOffers(maxOrFixedQuoteSideAmount); + String numOffers = Res.get("bisqEasy.tradeWizard.amount.numOffers." + Math.min(numMatchingOffers, 2), numMatchingOffers); + model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.numMatchingOffers.info", numOffers)); + model.getIsAmountLimitInfoOverlayVisible().set(false); + model.getAmountLimitInfoAmount().set(null); + model.getIsWarningIconVisible().set(numMatchingOffers == 0); } } } else { - long myReputationScore = model.getMyReputationScore(); - String formattedReputationBasedMaxAmount = formatAmountWithCode(model.getReputationBasedMaxAmount()); - String formattedAmountWithoutReputationNeeded = formatAmountWithCode(amountWithoutReputationNeeded); - if (noReputationNeededForMaxOrFixedAmount) { - model.getAmountLimitInfoAmount().set(""); - model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.noReputationNeededForMaxOrFixedAmount.overlay.info", - formattedAmountWithoutReputationNeeded, myReputationScore, formattedReputationBasedMaxAmount)); - model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.noReputationNeededForMaxOrFixedAmount", formattedAmountWithoutReputationNeeded)); + // Seller + if (model.isCreateOfferMode()) { + long myReputationScore = model.getMyReputationScore(); + String formattedReputationBasedMaxAmount = formatAmountWithCode(model.getReputationBasedMaxAmount()); + String formattedAmountWithoutReputationNeeded = formatAmountWithCode(amountWithoutReputationNeeded); + if (noReputationNeededForMaxOrFixedAmount) { + model.getAmountLimitInfoAmount().set(""); + model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.noReputationNeededForMaxOrFixedAmount.overlay.info", + formattedAmountWithoutReputationNeeded, myReputationScore, formattedReputationBasedMaxAmount)); + model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.noReputationNeededForMaxOrFixedAmount", formattedAmountWithoutReputationNeeded)); + } else { + model.getAmountLimitInfoAmount().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfoAmount", formattedReputationBasedMaxAmount)); + model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.overlay.info", myReputationScore, formattedReputationBasedMaxAmount)); + model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo", myReputationScore)); + } } else { + // Wizard + long numMatchingOffers = getNumMatchingOffers(maxOrFixedQuoteSideAmount); + String numOffers = Res.get("bisqEasy.tradeWizard.amount.numOffers." + Math.min(numMatchingOffers, 2), numMatchingOffers); + model.getAmountLimitInfoLeadLine().set(Res.get("bisqEasy.tradeWizard.amount.seller.numMatchingOffers.info", numOffers)); + model.getIsWarningIconVisible().set(numMatchingOffers == 0); + + long myReputationScore = model.getMyReputationScore(); + String formattedReputationBasedMaxAmount = formatAmountWithCode(model.getReputationBasedMaxAmount()); + model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.wizard.limitInfo", myReputationScore)); + model.getIsAmountHyperLinkDisabled().set(true); model.getAmountLimitInfoAmount().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfoAmount", formattedReputationBasedMaxAmount)); - model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.overlay.info", myReputationScore, formattedReputationBasedMaxAmount)); - model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo", myReputationScore)); + model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.wizard.overlay.info", myReputationScore, formattedReputationBasedMaxAmount)); } } } + private long getNumMatchingOffers(Monetary quoteSideAmount) { + return bisqEasyOfferbookChannelService.findChannel(model.getMarket()).orElseThrow().getChatMessages().stream() + .filter(chatMessage -> chatMessage.getBisqEasyOffer().isPresent()) + .map(chatMessage -> chatMessage.getBisqEasyOffer().get()) + .filter(offer -> { + if (!isValidDirection(offer)) { + return false; + } + if (!isValidMarket(offer)) { + return false; + } + if (!isValidMakerProfile(offer)) { + return false; + } + if (!isValidAmountRange(offer)) { + return false; + } + + Optional result = BisqEasyTradeAmountLimits.checkOfferAmountLimitForMinAmount(reputationService, + userIdentityService, + userProfileService, + marketPriceService, + model.getMarket(), + quoteSideAmount, + offer); + if (!result.map(Result::isValid).orElse(false)) { + return false; + } + return true; + }) + .count(); + } + private void applyQuoteSideMinMaxRange() { - Monetary maxRangeValue = BisqEasyTradeAmountLimits.usdToFiat(marketPriceService, model.getMarket(), BisqEasyTradeAmountLimits.MAX_USD_TRADE_AMOUNT) + Monetary maxRangeValue = BisqEasyTradeAmountLimits.usdToFiat(marketPriceService, model.getMarket(), MAX_USD_TRADE_AMOUNT) .orElseThrow().round(0); - Monetary minRangeValue = BisqEasyTradeAmountLimits.usdToFiat(marketPriceService, model.getMarket(), BisqEasyTradeAmountLimits.DEFAULT_MIN_USD_TRADE_AMOUNT) + Monetary minRangeValue = BisqEasyTradeAmountLimits.usdToFiat(marketPriceService, model.getMarket(), DEFAULT_MIN_USD_TRADE_AMOUNT) .orElseThrow().round(0); if (model.getReputationBasedMaxAmount() == null) { @@ -632,8 +665,27 @@ private void applyQuoteSideMinMaxRange() { ); } - minAmountComponent.setMinMaxRange(minRangeValue, maxRangeValue); - maxOrFixAmountComponent.setMinMaxRange(minRangeValue, maxRangeValue); + boolean isCreateOfferMode = model.isCreateOfferMode(); + if (isCreateOfferMode) { + model.getIsLearnMoreVisible().set(true); + minAmountComponent.setMinMaxRange(minRangeValue, maxRangeValue); + maxOrFixAmountComponent.setMinMaxRange(minRangeValue, maxRangeValue); + } else { + if (model.getDirection().isBuy()) { + model.getIsLearnMoreVisible().set(false); + Monetary recommendedValue = BisqEasyTradeAmountLimits.usdToFiat(marketPriceService, model.getMarket(), Fiat.fromFaceValue(100, "USD")) + .orElseThrow().round(0); + maxOrFixAmountComponent.setQuoteSideAmount(recommendedValue); + maxOrFixAmountComponent.setMinMaxRange(minRangeValue, maxRangeValue); + } else { + model.getIsLearnMoreVisible().set(true); + Monetary recommendedValue = BisqEasyTradeAmountLimits.usdToFiat(marketPriceService, model.getMarket(), MAX_USD_TRADE_AMOUNT_WITHOUT_REPUTATION) + .orElseThrow().round(0); + maxOrFixAmountComponent.setQuoteSideAmount(recommendedValue); + maxOrFixAmountComponent.setMinMaxRange(minRangeValue, model.getReputationBasedMaxAmount()); + } + } + if (model.getDirection().isBuy()) { model.setAmountLimitInfoLink(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.learnMore")); @@ -647,36 +699,161 @@ private void applyQuoteSideMinMaxRange() { .orElse(0L); Monetary highestPossibleUsdAmount = BisqEasyTradeAmountLimits.getUsdAmountFromReputationScore(highestScore); - minAmountComponent.setReputationBasedQuoteSideAmount(highestPossibleUsdAmount); - maxOrFixAmountComponent.setReputationBasedQuoteSideAmount(highestPossibleUsdAmount); + if (isCreateOfferMode) { + minAmountComponent.setReputationBasedQuoteSideAmount(highestPossibleUsdAmount); + maxOrFixAmountComponent.setReputationBasedQuoteSideAmount(highestPossibleUsdAmount); + } long rangeMidValue = minRangeValue.getValue() + (maxRangeValue.getValue() - minRangeValue.getValue()) / 2; // For buyers, we show the mid-range amount if there is a highestPossibleUsdAmount > rangeMidValue - if (highestPossibleUsdAmount.getValue() > rangeMidValue && + if (isCreateOfferMode && highestPossibleUsdAmount.getValue() > rangeMidValue && rangeMidValue > MAX_USD_TRADE_AMOUNT_WITHOUT_REPUTATION.getValue()) { maxOrFixAmountComponent.setQuoteSideAmount(Fiat.fromValue(rangeMidValue, "USD")); } else { applyReputationBasedQuoteSideAmount(); } } else { - model.setLinkToWikiText(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.overlay.linkToWikiText")); - model.setAmountLimitInfoLink(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.link")); + if (isCreateOfferMode) { + model.setLinkToWikiText(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.overlay.linkToWikiText")); + model.setAmountLimitInfoLink(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.link")); + + Monetary reputationBasedQuoteSideAmount = model.getReputationBasedMaxAmount(); + long myReputationScore = model.getMyReputationScore(); + model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo", myReputationScore)); + minAmountComponent.setReputationBasedQuoteSideAmount(reputationBasedQuoteSideAmount); + maxOrFixAmountComponent.setReputationBasedQuoteSideAmount(reputationBasedQuoteSideAmount); + String formattedAmount = formatAmountWithCode(reputationBasedQuoteSideAmount); - Monetary reputationBasedQuoteSideAmount = model.getReputationBasedMaxAmount(); - long myReputationScore = model.getMyReputationScore(); - model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo", myReputationScore)); + model.getAmountLimitInfoAmount().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfoAmount", formattedAmount)); + model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.overlay.info", myReputationScore, formattedAmount)); - minAmountComponent.setReputationBasedQuoteSideAmount(reputationBasedQuoteSideAmount); - maxOrFixAmountComponent.setReputationBasedQuoteSideAmount(reputationBasedQuoteSideAmount); - String formattedAmount = formatAmountWithCode(reputationBasedQuoteSideAmount); + applyReputationBasedQuoteSideAmount(); + } else { + // Wizard + //todo + model.setLinkToWikiText(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.overlay.linkToWikiText")); + model.setAmountLimitInfoLink(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.link")); + + Monetary reputationBasedQuoteSideAmount = model.getReputationBasedMaxAmount(); + maxOrFixAmountComponent.setQuoteSideAmount(reputationBasedQuoteSideAmount); + long myReputationScore = model.getMyReputationScore(); + model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo", myReputationScore)); + String formattedAmount = formatAmountWithCode(reputationBasedQuoteSideAmount); - model.getAmountLimitInfoAmount().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfoAmount", formattedAmount)); - model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.overlay.info", myReputationScore, formattedAmount)); + model.getAmountLimitInfoAmount().set(Res.get("bisqEasy.tradeWizard.amount.seller.wizard.limitInfo", formattedAmount)); + model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.overlay.info", myReputationScore, formattedAmount)); - applyReputationBasedQuoteSideAmount(); + applyReputationBasedQuoteSideAmount(); + } } } private void applyReputationBasedQuoteSideAmount() { - maxOrFixAmountComponent.setQuoteSideAmount(maxOrFixAmountComponent.getReputationBasedQuoteSideAmount()); + if (model.isCreateOfferMode()) { + maxOrFixAmountComponent.setQuoteSideAmount(maxOrFixAmountComponent.getReputationBasedQuoteSideAmount()); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////////////// + // Filter + /////////////////////////////////////////////////////////////////////////////////////////////////// + + private boolean isValidDirection(BisqEasyOffer peersOffer) { + return peersOffer.getTakersDirection().equals(model.getDirection()); + } + + private boolean isValidMarket(BisqEasyOffer peersOffer) { + return peersOffer.getMarket().equals(model.getMarket()); + } + + private boolean isValidMakerProfile(BisqEasyOffer peersOffer) { + Optional optionalMakersUserProfile = userProfileService.findUserProfile(peersOffer.getMakersUserProfileId()); + if (optionalMakersUserProfile.isEmpty()) { + return false; + } + UserProfile makersUserProfile = optionalMakersUserProfile.get(); + if (userProfileService.isChatUserIgnored(makersUserProfile)) { + return false; + } + if (userIdentityService.getUserIdentities().stream() + .map(userIdentity -> userIdentity.getUserProfile().getId()) + .anyMatch(userProfileId -> userProfileId.equals(optionalMakersUserProfile.get().getId()))) { + return false; + } + + return true; + } + + private boolean isValidPaymentMethods(BisqEasyOffer peersOffer) { + List bitcoinPaymentMethodNames = PaymentMethodSpecUtil.getPaymentMethodNames(peersOffer.getBaseSidePaymentMethodSpecs()); + List baseSidePaymentMethodSpecs = PaymentMethodSpecUtil.createBitcoinPaymentMethodSpecs(model.getBitcoinPaymentMethods()); + List baseSidePaymentMethodNames = PaymentMethodSpecUtil.getPaymentMethodNames(baseSidePaymentMethodSpecs); + if (baseSidePaymentMethodNames.stream().noneMatch(bitcoinPaymentMethodNames::contains)) { + return false; + } + + List fiatPaymentMethodNames = PaymentMethodSpecUtil.getPaymentMethodNames(peersOffer.getQuoteSidePaymentMethodSpecs()); + List quoteSidePaymentMethodSpecs = PaymentMethodSpecUtil.createFiatPaymentMethodSpecs(model.getFiatPaymentMethods()); + List quoteSidePaymentMethodNames = PaymentMethodSpecUtil.getPaymentMethodNames(quoteSidePaymentMethodSpecs); + if (quoteSidePaymentMethodNames.stream().noneMatch(fiatPaymentMethodNames::contains)) { + return false; + } + + return true; + } + + private boolean isValidAmountRange(BisqEasyOffer peersOffer) { + Optional myQuoteSideMinOrFixedAmount = OfferAmountUtil.findQuoteSideMinOrFixedAmount(marketPriceService, model.getQuoteSideAmountSpec().get(), MARKET_PRICE_SPEC, model.getMarket()); + Optional peersQuoteSideMaxOrFixedAmount = OfferAmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, peersOffer); + if (myQuoteSideMinOrFixedAmount.orElseThrow().round(0).getValue() > peersQuoteSideMaxOrFixedAmount.orElseThrow().round(0).getValue()) { + return false; + } + + Optional myQuoteSideMaxOrFixedAmount = OfferAmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, model.getQuoteSideAmountSpec().get(), MARKET_PRICE_SPEC, model.getMarket()); + Optional peersQuoteSideMinOrFixedAmount = OfferAmountUtil.findQuoteSideMinOrFixedAmount(marketPriceService, peersOffer); + if (myQuoteSideMaxOrFixedAmount.orElseThrow().round(0).getValue() < peersQuoteSideMinOrFixedAmount.orElseThrow().round(0).getValue()) { + return false; + } + + return true; + } + + + private boolean isValidAmountLimit(BisqEasyOffer peersOffer) { + if (!BisqEasyTradeAmountLimits.checkOfferAmountLimitForMaxOrFixedAmount(reputationService, + bisqEasyService, + userIdentityService, + userProfileService, + marketPriceService, + peersOffer) + .map(BisqEasyTradeAmountLimits.Result::isValid) + .orElse(false)) { + return false; + } + + return true; + } + + // Used for finding best price quote of available matching offers + private boolean filterOffers(BisqEasyOffer peersOffer) { + if (!isValidDirection(peersOffer)) { + return false; + } + if (!isValidMarket(peersOffer)) { + return false; + } + if (!isValidPaymentMethods(peersOffer)) { + return false; + } + if (!isValidMakerProfile(peersOffer)) { + return false; + } + if (!isValidAmountRange(peersOffer)) { + return false; + } + if (!isValidAmountLimit(peersOffer)) { + return false; + } + return true; } } diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountModel.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountModel.java index 140f217db2..e3fb78f4ba 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountModel.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountModel.java @@ -50,6 +50,7 @@ public class TradeWizardAmountModel implements Model { private final StringProperty amountLimitInfoLeadLine = new SimpleStringProperty(); private final StringProperty amountLimitInfoAmount = new SimpleStringProperty(); private final StringProperty amountLimitInfoOverlayInfo = new SimpleStringProperty(); + private final BooleanProperty isAmountHyperLinkDisabled = new SimpleBooleanProperty(); @Setter private String amountLimitInfoLink; @Setter @@ -66,6 +67,7 @@ public class TradeWizardAmountModel implements Model { private final BooleanProperty isMinAmountEnabled = new SimpleBooleanProperty(); private final BooleanProperty isAmountLimitInfoOverlayVisible = new SimpleBooleanProperty(); private final BooleanProperty isWarningIconVisible = new SimpleBooleanProperty(); + private final BooleanProperty isLearnMoreVisible = new SimpleBooleanProperty(); private final StringProperty toggleButtonText = new SimpleStringProperty(); private final StringProperty priceTooltip = new SimpleStringProperty(); private final ObjectProperty quoteSideAmountSpec = new SimpleObjectProperty<>(); @@ -81,6 +83,7 @@ public void reset() { amountLimitInfo.set(null); amountLimitInfoAmount.set(null); amountLimitInfoOverlayInfo.set(null); + isAmountHyperLinkDisabled.set(false); amountLimitInfoLink = null; linkToWikiText = null; isCreateOfferMode = false; @@ -89,6 +92,7 @@ public void reset() { isMinAmountEnabled.set(false); isAmountLimitInfoOverlayVisible.set(false); isWarningIconVisible.set(false); + isLearnMoreVisible.set(false); toggleButtonText.set(null); priceTooltip.set(null); quoteSideAmountSpec.set(null); diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountView.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountView.java index 5ab696e5fc..46c63236d1 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountView.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountView.java @@ -43,7 +43,7 @@ @Slf4j public class TradeWizardAmountView extends View { private final Label headlineLabel, amountLimitInfo, amountLimitInfoLeadLine, amountLimitInfoOverlayInfo, linkToWikiText, warningIcon; - private final Hyperlink amountLimitInfoAmount, showOverlayHyperLink, linkToWiki; + private final Hyperlink amountLimitInfoAmount, learnMoreHyperLink, linkToWiki; private final VBox minAmountRoot, content, amountLimitInfoOverlay; private final Button toggleButton, closeOverlayButton; private final HBox amountLimitInfoHBox; @@ -73,10 +73,11 @@ public TradeWizardAmountView(TradeWizardAmountModel model, amountLimitInfoAmount = new Hyperlink(); amountLimitInfoAmount.getStyleClass().add("trade-wizard-amount-limit-info-overlay-link"); - showOverlayHyperLink = new Hyperlink(); - showOverlayHyperLink.getStyleClass().add("trade-wizard-amount-limit-info-overlay-link"); + learnMoreHyperLink = new Hyperlink(); + learnMoreHyperLink.getStyleClass().add("trade-wizard-amount-limit-info-overlay-link"); - amountLimitInfoHBox = new HBox(5, amountLimitInfo, amountLimitInfoAmount, showOverlayHyperLink); + amountLimitInfoHBox = new HBox(5, amountLimitInfo, amountLimitInfoAmount, learnMoreHyperLink); + amountLimitInfoHBox.setAlignment(Pos.BASELINE_CENTER); amountLimitInfoLeadLine = new Label(); amountLimitInfoLeadLine.getStyleClass().add("trade-wizard-amount-limit-info"); @@ -87,6 +88,7 @@ public TradeWizardAmountView(TradeWizardAmountModel model, warningIcon.getStyleClass().add("overlay-icon-warning"); amountLimitInfoWithWarnIcon = new HBox(10, warningIcon, amountLimitInfoVBox); + amountLimitInfoWithWarnIcon.setAlignment(Pos.CENTER); toggleButton = new Button(Res.get("bisqEasy.tradeWizard.amount.addMinAmountOption")); toggleButton.getStyleClass().add("outlined-button"); @@ -109,7 +111,7 @@ public TradeWizardAmountView(TradeWizardAmountModel model, @Override protected void onViewAttached() { headlineLabel.setText(model.getHeadline()); - showOverlayHyperLink.setText(model.getAmountLimitInfoLink()); + learnMoreHyperLink.setText(model.getAmountLimitInfoLink()); linkToWikiText.setText(model.getLinkToWikiText()); amountLimitInfo.textProperty().bind(model.getAmountLimitInfo()); @@ -117,7 +119,10 @@ protected void onViewAttached() { amountLimitInfoAmount.textProperty().bind(model.getAmountLimitInfoAmount()); amountLimitInfoOverlayInfo.textProperty().bind(model.getAmountLimitInfoOverlayInfo()); toggleButton.textProperty().bind(model.getToggleButtonText()); + amountLimitInfoAmount.disableProperty().bind(model.getIsAmountHyperLinkDisabled()); + learnMoreHyperLink.visibleProperty().bind(model.getIsLearnMoreVisible()); + learnMoreHyperLink.managedProperty().bind(model.getIsLearnMoreVisible()); warningIcon.visibleProperty().bind(model.getIsWarningIconVisible()); amountLimitInfoAmount.visibleProperty().bind(model.getAmountLimitInfoAmount().isEmpty().not()); amountLimitInfoAmount.managedProperty().bind(model.getAmountLimitInfoAmount().isEmpty().not()); @@ -132,13 +137,6 @@ protected void onViewAttached() { amountLimitInfoLeadLine.setManaged(!isEmpty); double top = isEmpty ? 0 : -22.5; HBox.setMargin(warningIcon, new Insets(top, 0, 0, 0)); - if (isEmpty) { - amountLimitInfoHBox.setAlignment(Pos.BASELINE_CENTER); - amountLimitInfoWithWarnIcon.setAlignment(Pos.CENTER); - } else { - amountLimitInfoHBox.setAlignment(Pos.BASELINE_LEFT); - amountLimitInfoWithWarnIcon.setAlignment(Pos.CENTER_LEFT); - } }); isAmountLimitInfoVisiblePin = EasyBind.subscribe(model.getIsAmountLimitInfoOverlayVisible(), @@ -158,7 +156,7 @@ protected void onViewAttached() { }); amountLimitInfoAmount.setOnAction(e -> controller.onSetReputationBasedAmount()); - showOverlayHyperLink.setOnAction(e -> controller.onShowAmountLimitInfoOverlay()); + learnMoreHyperLink.setOnAction(e -> controller.onShowAmountLimitInfoOverlay()); linkToWiki.setOnAction(e -> controller.onOpenWiki(linkToWiki.getText())); closeOverlayButton.setOnAction(e -> controller.onCloseAmountLimitInfoOverlay()); toggleButton.setOnAction(e -> controller.onToggleMinAmountVisibility()); @@ -171,7 +169,10 @@ protected void onViewDetached() { amountLimitInfoLeadLine.textProperty().unbind(); amountLimitInfoAmount.textProperty().unbind(); amountLimitInfoOverlayInfo.textProperty().unbind(); + amountLimitInfoAmount.disableProperty().unbind(); + learnMoreHyperLink.visibleProperty().unbind(); + learnMoreHyperLink.managedProperty().unbind(); warningIcon.visibleProperty().unbind(); minAmountRoot.visibleProperty().unbind(); minAmountRoot.managedProperty().unbind(); @@ -182,7 +183,7 @@ protected void onViewDetached() { isAmountLimitInfoVisiblePin.unsubscribe(); amountLimitInfoAmount.setOnAction(null); - showOverlayHyperLink.setOnAction(null); + learnMoreHyperLink.setOnAction(null); linkToWiki.setOnAction(null); closeOverlayButton.setOnAction(null); toggleButton.setOnAction(null); diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/select_offer/TradeWizardSelectOfferController.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/select_offer/TradeWizardSelectOfferController.java index 53e99060ab..646f8aee43 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/select_offer/TradeWizardSelectOfferController.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/select_offer/TradeWizardSelectOfferController.java @@ -314,14 +314,14 @@ private Predicate getPredicate() { return false; } - if (!BisqEasyTradeAmountLimits.checkOfferAmountLimitForMaxOrFixedAmount(reputationService, - bisqEasyService, - userIdentityService, - userProfileService, - marketPriceService, - peersOffer) - .map(BisqEasyTradeAmountLimits.Result::isValid) - .orElse(false)) { + Optional result = BisqEasyTradeAmountLimits.checkOfferAmountLimitForMinAmount(reputationService, + userIdentityService, + userProfileService, + marketPriceService, + model.getMarket(), + myQuoteSideMaxOrFixedAmount, + peersOffer); + if (!result.map(BisqEasyTradeAmountLimits.Result::isValid).orElse(false)) { return false; } diff --git a/bisq-easy/src/main/java/bisq/bisq_easy/BisqEasyTradeAmountLimits.java b/bisq-easy/src/main/java/bisq/bisq_easy/BisqEasyTradeAmountLimits.java index 223c06740f..e217beafd3 100644 --- a/bisq-easy/src/main/java/bisq/bisq_easy/BisqEasyTradeAmountLimits.java +++ b/bisq-easy/src/main/java/bisq/bisq_easy/BisqEasyTradeAmountLimits.java @@ -79,6 +79,28 @@ private static Fiat getMaxUsdTradeAmount(long totalScore) { } + public static Optional checkOfferAmountLimitForMinAmount(ReputationService reputationService, + UserIdentityService userIdentityService, + UserProfileService userProfileService, + MarketPriceService marketPriceService, + Market market, + Monetary fiatAmount, + BisqEasyOffer peersOffer) { + return findRequiredReputationScoreByFiatAmount(marketPriceService, market, fiatAmount) + .map(requiredReputationScore -> { + long sellersReputationScore = getSellersReputationScore(reputationService, userIdentityService, userProfileService, peersOffer); + return getResult(sellersReputationScore, requiredReputationScore); + }); + } + + public static Optional findRequiredReputationScoreByFiatAmount1(MarketPriceService marketPriceService, + Market market, + Monetary fiatAmount) { + return fiatToBtc(marketPriceService, market, fiatAmount) + .flatMap(btc -> btcToUsd(marketPriceService, btc)) + .map(BisqEasyTradeAmountLimits::getRequiredReputationScoreByUsdAmount); + } + public static Optional checkOfferAmountLimitForMinAmount(ReputationService reputationService, BisqEasyService bisqEasyService, UserIdentityService userIdentityService, diff --git a/i18n/src/main/resources/bisq_easy.properties b/i18n/src/main/resources/bisq_easy.properties index e3dff3ab90..431b84ea72 100644 --- a/i18n/src/main/resources/bisq_easy.properties +++ b/i18n/src/main/resources/bisq_easy.properties @@ -156,6 +156,11 @@ bisqEasy.component.amount.baseSide.tooltip.taker.offerPrice=with the offer price bisqEasy.tradeWizard.amount.limitInfo.overlay.headline=Reputation-based trade amount limits bisqEasy.tradeWizard.amount.limitInfo.overlay.close=Close overlay +bisqEasy.tradeWizard.amount.seller.numMatchingOffers.info=There {0} matching the chosen trade amount. +bisqEasy.tradeWizard.amount.seller.wizard.limitInfo=With your reputation score of {0} you can trade up to +bisqEasy.tradeWizard.amount.seller.limitInfo.wizard.overlay.info=With a reputation score of {0}, you provide security for trades up to {1}.\n\n\ + You can find information on how to increase your reputation at ''Reputation/Build Reputation''. + bisqEasy.tradeWizard.amount.seller.limitInfo=Your reputation score of {0} provides security for offers up to bisqEasy.tradeWizard.amount.seller.limitInfoAmount={0}. bisqEasy.tradeWizard.amount.seller.limitInfo.link=Learn more @@ -170,16 +175,27 @@ bisqEasy.tradeWizard.amount.seller.limitInfo.noReputationNeededForMaxOrFixedAmou For offers exceeding this amount, buyers will receive a warning about potential risks when taking your offer.\n\n\ You can find information on how to increase your reputation at ''Reputation/Build Reputation''. bisqEasy.tradeWizard.amount.buyer.limitInfo.learnMore=Learn more + # suppress inspection "UnusedProperty" bisqEasy.tradeWizard.amount.buyer.numSellers.0=is no seller # suppress inspection "UnusedProperty" bisqEasy.tradeWizard.amount.buyer.numSellers.1=is one seller # suppress inspection "UnusedProperty" -bisqEasy.tradeWizard.amount.buyer.numSellers.2=are {0} sellers +bisqEasy.tradeWizard.amount.buyer.numSellers.*=are {0} sellers + +# suppress inspection "UnusedProperty" +bisqEasy.tradeWizard.amount.numOffers.0=is no offer +# suppress inspection "UnusedProperty" +bisqEasy.tradeWizard.amount.numOffers.1=is one offer +# suppress inspection "UnusedProperty" +bisqEasy.tradeWizard.amount.numOffers.*=are {0} offers + bisqEasy.tradeWizard.amount.buyer.limitInfo=There {0} in the network with sufficient reputation to take an offer of {1}. bisqEasy.tradeWizard.amount.buyer.limitInfo.overlay.info=A seller who wants to take your offer of {0}, must have a reputation score of at least {1}.\n\ By reducing the maximum trade amount, you make your offer accessible to more sellers. +bisqEasy.tradeWizard.amount.buyer.numMatchingOffers.info=There {0} matching the chosen trade amount. + bisqEasy.tradeWizard.amount.buyer.limitInfo.noReputationNeededForMaxOrFixedAmount=For amounts up to {0}, reputation requirements are relaxed, allowing anyone to take your offer. bisqEasy.tradeWizard.amount.buyer.limitInfo.overlay.noReputationNeededForMaxOrFixedAmount.info=Given the low amount of {0}, reputation requirements are relaxed.\n\ For amounts up to {1}, sellers with insufficient or no reputation can take the offer. From 07179b5ea6c4abec058103b6dcd62f1e4e68f70c Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 13 Sep 2024 16:42:17 +0700 Subject: [PATCH 3/5] Use getPluralization instead of getAsSingularOrPlural --- i18n/src/main/java/bisq/i18n/Res.java | 41 +++++-------------- i18n/src/main/resources/bisq_easy.properties | 16 +++++--- i18n/src/main/resources/default.properties | 11 +++-- i18n/src/main/resources/default_cs.properties | 11 +++-- i18n/src/main/resources/default_de.properties | 11 +++-- i18n/src/main/resources/default_es.properties | 11 +++-- i18n/src/main/resources/default_it.properties | 11 +++-- .../src/main/resources/default_pcm.properties | 11 +++-- .../main/resources/default_pt_BR.properties | 11 +++-- .../formatters/TimeFormatter.java | 6 +-- 10 files changed, 72 insertions(+), 68 deletions(-) diff --git a/i18n/src/main/java/bisq/i18n/Res.java b/i18n/src/main/java/bisq/i18n/Res.java index 4942d58d67..2fffc1504d 100644 --- a/i18n/src/main/java/bisq/i18n/Res.java +++ b/i18n/src/main/java/bisq/i18n/Res.java @@ -63,16 +63,6 @@ public static void setLanguage(String languageCode) { ); } - public static String getQuantityKey(String key, double number) { - if (number == 0) { - return get(key + ".0"); - } else if (number == 1) { - return get(key + ".1"); - } else { - return get(key + ".*", number); - } - } - public static String get(String key, Object... arguments) { return MessageFormat.format(get(key), arguments); } @@ -98,30 +88,19 @@ public static String get(String key) { } } - // Convenience method for supporting format `{0} get(key)` where key is expected to support singular and plural - // cases where the plural key adds a `s` to the singular key. Argument is expected to be integer or long - /** - * @param key Key which supports singular and plural. By convention, we expect the plural key to have - * a `s` as postfix to the singular key. - * @param value The long value - * @return The value separated with a space to the postfix which is either singular or plural form + * Expecting to have i18n keys with '.1' and '.*' postfix for singular and plural handling. + * Additionally, a '.0' postfix handles 0 values. */ - public static String getAsSingularOrPlural(String key, long value) { - if (Math.abs(value) != 1) { - key = key + "s"; + public static String getPluralization(String key, double number) { + if (number == 1) { + return get(key + ".1"); + } else { + if (number == 0 && has(key + ".0")) { + return get(key + ".0"); + } else + return get(key + ".*", number); } - return value + " " + get(key); - } - - /** - * @param key Key which supports singular and plural. By convention, we expect the plural key to have - * a `s` as postfix to the singular key. - * @param value The integer value - * @return The value separated with a space to the postfix which is either singular or plural form - */ - public static String getAsSingularOrPlural(String key, int value) { - return getAsSingularOrPlural(key, (long) value); } public static boolean has(String key) { diff --git a/i18n/src/main/resources/bisq_easy.properties b/i18n/src/main/resources/bisq_easy.properties index 431b84ea72..cfc3cceffd 100644 --- a/i18n/src/main/resources/bisq_easy.properties +++ b/i18n/src/main/resources/bisq_easy.properties @@ -78,11 +78,11 @@ bisqEasy.tradeWizard.direction.feedback.subTitle1=You haven't established any re This reputation-building process is better suited for experienced Bisq users, and you can find detailed information \ about it in the 'Reputation' section. bisqEasy.tradeWizard.direction.feedback.gainReputation=Learn how to build up reputation -bisqEasy.tradeWizard.direction.feedback.subTitle2=While it's possible for sellers to trade without a reputation, \ +bisqEasy.tradeWizard.direction.feedback.subTitle2=While it's possible for sellers to trade without (or insufficient) reputation for low amounts (below amount equivalent to 25.00 USD), \ the likelihood of finding a trading partner is considerably reduced. This is because buyers tend to avoid \ - engaging with sellers who lack a reputation, primarily due to security risks. -bisqEasy.tradeWizard.direction.feedback.tradeWithoutReputation=Trade without reputation -bisqEasy.tradeWizard.direction.feedback.backToBuy=Back to Buy Bitcoin + engaging with sellers who lack a reputation due to security risks. +bisqEasy.tradeWizard.direction.feedback.tradeWithoutReputation=Continue without reputation +bisqEasy.tradeWizard.direction.feedback.backToBuy=Back ################################################################################ @@ -194,7 +194,11 @@ bisqEasy.tradeWizard.amount.buyer.limitInfo=There {0} in the network with suffic bisqEasy.tradeWizard.amount.buyer.limitInfo.overlay.info=A seller who wants to take your offer of {0}, must have a reputation score of at least {1}.\n\ By reducing the maximum trade amount, you make your offer accessible to more sellers. -bisqEasy.tradeWizard.amount.buyer.numMatchingOffers.info=There {0} matching the chosen trade amount. +bisqEasy.tradeWizard.amount.buyer.limitInfo.wizard.info.leadLine=There {0} matching the chosen trade amount. +bisqEasy.tradeWizard.amount.buyer.limitInfo.wizard.info=For offers up to {0}, reputation requirements are relaxed. +bisqEasy.tradeWizard.amount.buyer.limitInfo.wizard.overlay.info=Given the low min. amount of {0}, the reputation requirements are relaxed. \ + For amounts up to {1}, sellers do not need reputation.\n\n\ + At the 'SELECT OFFER' screen it is recommended to choose sellers with higher reputation. bisqEasy.tradeWizard.amount.buyer.limitInfo.noReputationNeededForMaxOrFixedAmount=For amounts up to {0}, reputation requirements are relaxed, allowing anyone to take your offer. bisqEasy.tradeWizard.amount.buyer.limitInfo.overlay.noReputationNeededForMaxOrFixedAmount.info=Given the low amount of {0}, reputation requirements are relaxed.\n\ @@ -463,7 +467,7 @@ bisqEasy.walletGuide.intro.content=In Bisq Easy, the Bitcoin you receive goes st bisqEasy.walletGuide.download.headline=Downloading your wallet bisqEasy.walletGuide.download.content=There are many wallets out there that you can use. In this guide, we will show you how to use Bluewallet. Bluewallet is great and, at the same time, very simple, and you can use it to receive your bitcoin from Bisq Easy.\n\n\ You can download Bluewallet on your phone, regardless of whether you have an Android or iOS device. To do so, you can visit the official webpage at 'bluewallet.io'. Once you are there, click on App Store or Google Play depending on the device you are using.\n\n\ - Important note: for your safety, make sure that you download the app from the official app store of your device. The official app is provided by 'Bluewallet Services, S.R.L.', and you should be able to see this in your app store. Downloading a malicious copycat could put your funds at risk.\n\n\ + Important note: for your safety, make sure that you download the app from the official app store of your device. The official app is provided by 'Bluewallet Services, S.R.L.', and you should be able to see this in your app store. Downloading a malicious wallet could put your funds at risk.\n\n\ Finally, a quick note: Bisq is not affiliated with Bluewallet in any way. We suggest using Bluewallet due to its quality and simplicity, but there are many other options on the market. You should feel absolutely free to compare, try and choose whichever wallet fits your needs best. bisqEasy.walletGuide.download.link=Click here to visit Bluewallet's page diff --git a/i18n/src/main/resources/default.properties b/i18n/src/main/resources/default.properties index 5f6987a973..8a27b9711d 100644 --- a/i18n/src/main/resources/default.properties +++ b/i18n/src/main/resources/default.properties @@ -80,11 +80,14 @@ offer.amount=Amount temporal.date=Date temporal.age=Age -temporal.day=day -temporal.days=days -temporal.year=year # suppress inspection "UnusedProperty" -temporal.years=years +temporal.day.1=day +# suppress inspection "UnusedProperty" +temporal.day.*=days +# suppress inspection "UnusedProperty" +temporal.year.1=year +# suppress inspection "UnusedProperty" +temporal.year.*=years temporal.at=at diff --git a/i18n/src/main/resources/default_cs.properties b/i18n/src/main/resources/default_cs.properties index 72dc910556..5ce7974108 100644 --- a/i18n/src/main/resources/default_cs.properties +++ b/i18n/src/main/resources/default_cs.properties @@ -80,11 +80,14 @@ offer.amount=Množství temporal.date=Datum temporal.age=Stáří -temporal.day=den -temporal.days=dny -temporal.year=rok # suppress inspection "UnusedProperty" -temporal.years=roky +temporal.day.1=den +# suppress inspection "UnusedProperty" +temporal.day.*=dny +# suppress inspection "UnusedProperty" +temporal.year.1=rok +# suppress inspection "UnusedProperty" +temporal.year.*=roky temporal.at=v diff --git a/i18n/src/main/resources/default_de.properties b/i18n/src/main/resources/default_de.properties index f491ccc993..eaa890cd86 100644 --- a/i18n/src/main/resources/default_de.properties +++ b/i18n/src/main/resources/default_de.properties @@ -80,11 +80,14 @@ offer.amount=Menge temporal.date=Datum temporal.age=Alter -temporal.day=Tag -temporal.days=Tage -temporal.year=Jahr # suppress inspection "UnusedProperty" -temporal.years=Jahre +temporal.day.1=Tag +# suppress inspection "UnusedProperty" +temporal.day.*=Tage +# suppress inspection "UnusedProperty" +temporal.year.1=Jahr +# suppress inspection "UnusedProperty" +temporal.year.*=Jahre temporal.at=bei diff --git a/i18n/src/main/resources/default_es.properties b/i18n/src/main/resources/default_es.properties index 5c79ae3f24..3d70b8b6cf 100644 --- a/i18n/src/main/resources/default_es.properties +++ b/i18n/src/main/resources/default_es.properties @@ -80,11 +80,14 @@ offer.amount=Cantidad temporal.date=Fecha temporal.age=Edad -temporal.day=día -temporal.days=días -temporal.year=año # suppress inspection "UnusedProperty" -temporal.years=años +temporal.day.1=día +# suppress inspection "UnusedProperty" +temporal.day.*=días +# suppress inspection "UnusedProperty" +temporal.year.1=año +# suppress inspection "UnusedProperty" +temporal.year.*=años temporal.at=en diff --git a/i18n/src/main/resources/default_it.properties b/i18n/src/main/resources/default_it.properties index d3375678e5..42be585a13 100644 --- a/i18n/src/main/resources/default_it.properties +++ b/i18n/src/main/resources/default_it.properties @@ -80,11 +80,14 @@ offer.amount=Quantità temporal.date=Data temporal.age=Età -temporal.day=giorno -temporal.days=giorni -temporal.year=anno # suppress inspection "UnusedProperty" -temporal.years=anni +temporal.day.1=giorno +# suppress inspection "UnusedProperty" +temporal.day.*=giorni +# suppress inspection "UnusedProperty" +temporal.year.1=anno +# suppress inspection "UnusedProperty" +temporal.year.*=anni temporal.at=a diff --git a/i18n/src/main/resources/default_pcm.properties b/i18n/src/main/resources/default_pcm.properties index 1cf40b5a35..3d68a9f738 100644 --- a/i18n/src/main/resources/default_pcm.properties +++ b/i18n/src/main/resources/default_pcm.properties @@ -80,11 +80,14 @@ offer.amount=Amount temporal.date=Date temporal.age=Age -temporal.day=day -temporal.days=days -temporal.year=year # suppress inspection "UnusedProperty" -temporal.years=years +temporal.day.1=day +# suppress inspection "UnusedProperty" +temporal.day.*=days +# suppress inspection "UnusedProperty" +temporal.year.1=year +# suppress inspection "UnusedProperty" +temporal.year.*=years temporal.at=at diff --git a/i18n/src/main/resources/default_pt_BR.properties b/i18n/src/main/resources/default_pt_BR.properties index c64ddcee18..e6333453d4 100644 --- a/i18n/src/main/resources/default_pt_BR.properties +++ b/i18n/src/main/resources/default_pt_BR.properties @@ -80,11 +80,14 @@ offer.amount=Quantidade temporal.date=Data temporal.age=Idade -temporal.day=dia -temporal.days=dias -temporal.year=ano # suppress inspection "UnusedProperty" -temporal.years=anos +temporal.day.1=dia +# suppress inspection "UnusedProperty" +temporal.day.*=dias +# suppress inspection "UnusedProperty" +temporal.year.1=ano +# suppress inspection "UnusedProperty" +temporal.year.*=anos temporal.at=às diff --git a/presentation/src/main/java/bisq/presentation/formatters/TimeFormatter.java b/presentation/src/main/java/bisq/presentation/formatters/TimeFormatter.java index 627d6e74d8..746a643c3d 100644 --- a/presentation/src/main/java/bisq/presentation/formatters/TimeFormatter.java +++ b/presentation/src/main/java/bisq/presentation/formatters/TimeFormatter.java @@ -69,7 +69,7 @@ public static String formatAge(long duration) { long days = hours / 24; hours = hours % 24; if (days > 0) { - String dayString = Res.getAsSingularOrPlural("temporal.day", days); + String dayString = Res.getPluralization("temporal.day", days); return String.format("%s, %d hours, %d min, %d sec", dayString, hours, min, sec); } else if (hours > 0) { return String.format("%d hours, %d min, %d sec", hours, min, sec); @@ -82,9 +82,9 @@ public static String formatAgeInDays(long date) { long totalDays = getAgeInDays(date); long years = totalDays / 365; long days = totalDays - years * 365; - String dayString = Res.getAsSingularOrPlural("temporal.day", days); + String dayString = Res.getPluralization("temporal.day", days); if (years > 0) { - String yearString = Res.getAsSingularOrPlural("temporal.year", years); + String yearString = Res.getPluralization("temporal.year", years); return yearString + ", " + dayString; } else { return dayString; From 8bd539de55624842776361d9f4267083ba919ecc Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 13 Sep 2024 16:42:23 +0700 Subject: [PATCH 4/5] Improve trade wizard --- .../bisq_easy/components/AmountComponent.java | 65 ++++----- .../amount/TradeWizardAmountController.java | 131 +++++++++--------- 2 files changed, 97 insertions(+), 99 deletions(-) diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountComponent.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountComponent.java index 649ffaae10..0506e07e18 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountComponent.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountComponent.java @@ -372,47 +372,40 @@ private void applyInitialRangeValues() { boolean isMinRangeMonetaryFiat = FiatCurrencyRepository.getCurrencyByCodeMap().containsKey(minRangeMonetary.getCode()); boolean isMaxRangeMonetaryFiat = FiatCurrencyRepository.getCurrencyByCodeMap().containsKey(maxRangeMonetary.getCode()); - if (model.getMinRangeBaseSideValue().get() == null) { - Monetary minRangeMonetaryAsCoin = !isMinRangeMonetaryFiat ? - minRangeMonetary : - priceQuote.toBaseSideMonetary(minRangeMonetary); - model.getMinRangeBaseSideValue().set(minRangeMonetaryAsCoin); - if (!model.useQuoteCurrencyForMinMaxRange) { - model.getMinRangeValueAsString().set(Res.get("bisqEasy.component.amount.minRangeValue", - AmountFormatter.formatAmountWithCode(minRangeMonetaryAsCoin))); - } + Monetary minRangeMonetaryAsCoin = !isMinRangeMonetaryFiat ? + minRangeMonetary : + priceQuote.toBaseSideMonetary(minRangeMonetary); + model.getMinRangeBaseSideValue().set(minRangeMonetaryAsCoin); + if (!model.useQuoteCurrencyForMinMaxRange) { + model.getMinRangeValueAsString().set(Res.get("bisqEasy.component.amount.minRangeValue", + AmountFormatter.formatAmountWithCode(minRangeMonetaryAsCoin))); } - if (model.getMaxRangeBaseSideValue().get() == null) { - Monetary maxRangeMonetaryAsCoin = !isMaxRangeMonetaryFiat ? - maxRangeMonetary : - priceQuote.toBaseSideMonetary(maxRangeMonetary); - model.getMaxRangeBaseSideValue().set(maxRangeMonetaryAsCoin); - if (!model.useQuoteCurrencyForMinMaxRange) { - model.getMaxRangeValueAsString().set(Res.get("bisqEasy.component.amount.maxRangeValue", - AmountFormatter.formatAmountWithCode(maxRangeMonetaryAsCoin))); - } + + Monetary maxRangeMonetaryAsCoin = !isMaxRangeMonetaryFiat ? + maxRangeMonetary : + priceQuote.toBaseSideMonetary(maxRangeMonetary); + model.getMaxRangeBaseSideValue().set(maxRangeMonetaryAsCoin); + if (!model.useQuoteCurrencyForMinMaxRange) { + model.getMaxRangeValueAsString().set(Res.get("bisqEasy.component.amount.maxRangeValue", + AmountFormatter.formatAmountWithCode(maxRangeMonetaryAsCoin))); } - if (model.getMinRangeQuoteSideValue().get() == null) { - Monetary minRangeMonetaryAsFiat = isMinRangeMonetaryFiat ? - minRangeMonetary : - priceQuote.toQuoteSideMonetary(minRangeMonetary).round(0); - model.getMinRangeQuoteSideValue().set(minRangeMonetaryAsFiat); - if (model.useQuoteCurrencyForMinMaxRange) { - model.getMinRangeValueAsString().set(Res.get("bisqEasy.component.amount.minRangeValue", - AmountFormatter.formatAmountWithCode(minRangeMonetaryAsFiat))); - } + Monetary minRangeMonetaryAsFiat = isMinRangeMonetaryFiat ? + minRangeMonetary : + priceQuote.toQuoteSideMonetary(minRangeMonetary).round(0); + model.getMinRangeQuoteSideValue().set(minRangeMonetaryAsFiat); + if (model.useQuoteCurrencyForMinMaxRange) { + model.getMinRangeValueAsString().set(Res.get("bisqEasy.component.amount.minRangeValue", + AmountFormatter.formatAmountWithCode(minRangeMonetaryAsFiat))); } - if (model.getMaxRangeQuoteSideValue().get() == null) { - Monetary maxRangeMonetaryAsFiat = isMaxRangeMonetaryFiat ? - maxRangeMonetary : - priceQuote.toQuoteSideMonetary(maxRangeMonetary).round(0); - model.getMaxRangeQuoteSideValue().set(maxRangeMonetaryAsFiat); - if (model.useQuoteCurrencyForMinMaxRange) { - model.getMaxRangeValueAsString().set(Res.get("bisqEasy.component.amount.maxRangeValue", - AmountFormatter.formatAmountWithCode(maxRangeMonetaryAsFiat))); - } + Monetary maxRangeMonetaryAsFiat = isMaxRangeMonetaryFiat ? + maxRangeMonetary : + priceQuote.toQuoteSideMonetary(maxRangeMonetary).round(0); + model.getMaxRangeQuoteSideValue().set(maxRangeMonetaryAsFiat); + if (model.useQuoteCurrencyForMinMaxRange) { + model.getMaxRangeValueAsString().set(Res.get("bisqEasy.component.amount.maxRangeValue", + AmountFormatter.formatAmountWithCode(maxRangeMonetaryAsFiat))); } applySliderTrackStyle(); diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountController.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountController.java index 67ad0a528d..dc96629241 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountController.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountController.java @@ -319,8 +319,6 @@ public void onDeactivate() { priceTooltipPin.unsubscribe(); view.getRoot().setOnKeyPressed(null); model.getIsAmountLimitInfoOverlayVisible().set(false); - //todo - // maxOrFixAmountComponent.reset(); } void onSetReputationBasedAmount() { @@ -532,6 +530,7 @@ private void quoteSideAmountsChanged(boolean maxAmountChanged) { String formattedAmountWithoutReputationNeeded = formatAmountWithCode(amountWithoutReputationNeeded); String formattedMaxOrFixedAmount = formatAmountWithCode(maxOrFixedQuoteSideAmount); if (model.getIsMinAmountEnabled().get()) { + // Amount range String formattedMinAmount = formatAmountWithCode(minQuoteSideAmount); boolean noReputationNeededForMinAmount = minQuoteSideAmount.isLessThanOrEqual(amountWithoutReputationNeeded); model.getIsWarningIconVisible().set(noReputationNeededForMaxOrFixedAmount || noReputationNeededForMinAmount); @@ -543,7 +542,7 @@ private void quoteSideAmountsChanged(boolean maxAmountChanged) { // min < 25 USD, max > 25 USD model.getAmountLimitInfoLeadLine().set(Res.get("bisqEasy.tradeWizard.amount.buyer.noReputationNeededForMinAmount.limitInfo.leadLine", formattedAmountWithoutReputationNeeded)); - String numSellers = Res.getQuantityKey("bisqEasy.tradeWizard.amount.buyer.numSellers", numPotentialTakersForMaxOrFixedAmount); + String numSellers = Res.getPluralization("bisqEasy.tradeWizard.amount.buyer.numSellers", numPotentialTakersForMaxOrFixedAmount); model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.noReputationNeededForMinAmount.limitInfo", formattedMaxOrFixedAmount, numSellers)); model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.noReputationNeededForMinAmount.limitInfo.overlay.info", @@ -552,17 +551,19 @@ private void quoteSideAmountsChanged(boolean maxAmountChanged) { // min > 25 USD model.getIsWarningIconVisible().set(numPotentialTakersForMaxOrFixedAmount == 0); if (maxAmountChanged) { - String numSellers = Res.getQuantityKey("bisqEasy.tradeWizard.amount.buyer.numSellers", numPotentialTakersForMaxOrFixedAmount); + String numSellers = Res.getPluralization("bisqEasy.tradeWizard.amount.buyer.numSellers", numPotentialTakersForMaxOrFixedAmount); model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo", numSellers, formattedMaxOrFixedAmount)); model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.overlay.info", formattedMaxOrFixedAmount, requiredReputationScoreForMaxOrFixedAmount) + "\n\n"); } else { - String numSellers = Res.getQuantityKey("bisqEasy.tradeWizard.amount.buyer.numSellers", numPotentialTakersForMinAmount); + String numSellers = Res.getPluralization("bisqEasy.tradeWizard.amount.buyer.numSellers", numPotentialTakersForMinAmount); model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo", numSellers, formattedMinAmount)); model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.overlay.info", formattedMinAmount, requiredReputationScoreForMinAmount) + "\n\n"); } } } else { + // Fixed amount if (model.isCreateOfferMode()) { + // Create offer if (noReputationNeededForMaxOrFixedAmount) { // max < 25 USD (inherently also min < 25 USD) model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.noReputationNeededForMaxOrFixedAmount", formattedAmountWithoutReputationNeeded)); @@ -570,23 +571,34 @@ private void quoteSideAmountsChanged(boolean maxAmountChanged) { model.getIsWarningIconVisible().set(true); } else { // min > 25 USD - String numSellers = Res.getQuantityKey("bisqEasy.tradeWizard.amount.buyer.numSellers", numPotentialTakersForMaxOrFixedAmount); + String numSellers = Res.getPluralization("bisqEasy.tradeWizard.amount.buyer.numSellers", numPotentialTakersForMaxOrFixedAmount); model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo", numSellers, formattedMaxOrFixedAmount)); model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.overlay.info", formattedMaxOrFixedAmount, requiredReputationScoreForMaxOrFixedAmount) + "\n\n"); model.getIsWarningIconVisible().set(numPotentialTakersForMaxOrFixedAmount == 0); } } else { + // Wizard long numMatchingOffers = getNumMatchingOffers(maxOrFixedQuoteSideAmount); - String numOffers = Res.get("bisqEasy.tradeWizard.amount.numOffers." + Math.min(numMatchingOffers, 2), numMatchingOffers); - model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.numMatchingOffers.info", numOffers)); - model.getIsAmountLimitInfoOverlayVisible().set(false); + String numOffers = Res.getPluralization("bisqEasy.tradeWizard.amount.numOffers", numMatchingOffers); + + model.getAmountLimitInfoLeadLine().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.wizard.info.leadLine",numOffers)); + boolean weakSecurity = maxOrFixedQuoteSideAmount.isLessThanOrEqual(MAX_USD_TRADE_AMOUNT_WITHOUT_REPUTATION); + String formatted = formatAmountWithCode(MAX_USD_TRADE_AMOUNT_WITHOUT_REPUTATION); + if (weakSecurity) { + model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.wizard.info", formatted)); + } else { + model.getAmountLimitInfo().set(null); + } + model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.wizard.overlay.info", formattedMaxOrFixedAmount, formatted) + "\n\n"); + model.getIsLearnMoreVisible().set(weakSecurity); + model.getIsWarningIconVisible().set(weakSecurity || numMatchingOffers == 0); model.getAmountLimitInfoAmount().set(null); - model.getIsWarningIconVisible().set(numMatchingOffers == 0); } } } else { // Seller if (model.isCreateOfferMode()) { + // Create offer long myReputationScore = model.getMyReputationScore(); String formattedReputationBasedMaxAmount = formatAmountWithCode(model.getReputationBasedMaxAmount()); String formattedAmountWithoutReputationNeeded = formatAmountWithCode(amountWithoutReputationNeeded); @@ -603,7 +615,7 @@ private void quoteSideAmountsChanged(boolean maxAmountChanged) { } else { // Wizard long numMatchingOffers = getNumMatchingOffers(maxOrFixedQuoteSideAmount); - String numOffers = Res.get("bisqEasy.tradeWizard.amount.numOffers." + Math.min(numMatchingOffers, 2), numMatchingOffers); + String numOffers = Res.getPluralization("bisqEasy.tradeWizard.amount.numOffers", numMatchingOffers); model.getAmountLimitInfoLeadLine().set(Res.get("bisqEasy.tradeWizard.amount.seller.numMatchingOffers.info", numOffers)); model.getIsWarningIconVisible().set(numMatchingOffers == 0); @@ -617,39 +629,6 @@ private void quoteSideAmountsChanged(boolean maxAmountChanged) { } } - private long getNumMatchingOffers(Monetary quoteSideAmount) { - return bisqEasyOfferbookChannelService.findChannel(model.getMarket()).orElseThrow().getChatMessages().stream() - .filter(chatMessage -> chatMessage.getBisqEasyOffer().isPresent()) - .map(chatMessage -> chatMessage.getBisqEasyOffer().get()) - .filter(offer -> { - if (!isValidDirection(offer)) { - return false; - } - if (!isValidMarket(offer)) { - return false; - } - if (!isValidMakerProfile(offer)) { - return false; - } - if (!isValidAmountRange(offer)) { - return false; - } - - Optional result = BisqEasyTradeAmountLimits.checkOfferAmountLimitForMinAmount(reputationService, - userIdentityService, - userProfileService, - marketPriceService, - model.getMarket(), - quoteSideAmount, - offer); - if (!result.map(Result::isValid).orElse(false)) { - return false; - } - return true; - }) - .count(); - } - private void applyQuoteSideMinMaxRange() { Monetary maxRangeValue = BisqEasyTradeAmountLimits.usdToFiat(marketPriceService, model.getMarket(), MAX_USD_TRADE_AMOUNT) .orElseThrow().round(0); @@ -664,30 +643,29 @@ private void applyQuoteSideMinMaxRange() { .orElse(Fiat.fromValue(0, model.getMarket().getQuoteCurrencyCode())) ); } - + Fiat defaultUsdAmount = MAX_USD_TRADE_AMOUNT_WITHOUT_REPUTATION.multiply(2); + Monetary defaultFiatAmount = BisqEasyTradeAmountLimits.usdToFiat(marketPriceService, model.getMarket(), defaultUsdAmount) + .orElseThrow().round(0); boolean isCreateOfferMode = model.isCreateOfferMode(); if (isCreateOfferMode) { model.getIsLearnMoreVisible().set(true); minAmountComponent.setMinMaxRange(minRangeValue, maxRangeValue); maxOrFixAmountComponent.setMinMaxRange(minRangeValue, maxRangeValue); } else { + model.getIsLearnMoreVisible().set(model.getDirection().isSell()); if (model.getDirection().isBuy()) { - model.getIsLearnMoreVisible().set(false); - Monetary recommendedValue = BisqEasyTradeAmountLimits.usdToFiat(marketPriceService, model.getMarket(), Fiat.fromFaceValue(100, "USD")) - .orElseThrow().round(0); - maxOrFixAmountComponent.setQuoteSideAmount(recommendedValue); maxOrFixAmountComponent.setMinMaxRange(minRangeValue, maxRangeValue); } else { - model.getIsLearnMoreVisible().set(true); - Monetary recommendedValue = BisqEasyTradeAmountLimits.usdToFiat(marketPriceService, model.getMarket(), MAX_USD_TRADE_AMOUNT_WITHOUT_REPUTATION) - .orElseThrow().round(0); - maxOrFixAmountComponent.setQuoteSideAmount(recommendedValue); - maxOrFixAmountComponent.setMinMaxRange(minRangeValue, model.getReputationBasedMaxAmount()); + maxOrFixAmountComponent.setMinMaxRange(minRangeValue, model.getReputationBasedMaxAmount().round(0)); } - } + if (maxOrFixAmountComponent.getQuoteSideAmount().get() == null) { + maxOrFixAmountComponent.setQuoteSideAmount(defaultFiatAmount); + } + } if (model.getDirection().isBuy()) { + // Buyer case model.setAmountLimitInfoLink(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.learnMore")); model.setLinkToWikiText(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.overlay.linkToWikiText")); model.getAmountLimitInfoAmount().set(""); @@ -703,15 +681,11 @@ private void applyQuoteSideMinMaxRange() { minAmountComponent.setReputationBasedQuoteSideAmount(highestPossibleUsdAmount); maxOrFixAmountComponent.setReputationBasedQuoteSideAmount(highestPossibleUsdAmount); } - long rangeMidValue = minRangeValue.getValue() + (maxRangeValue.getValue() - minRangeValue.getValue()) / 2; - // For buyers, we show the mid-range amount if there is a highestPossibleUsdAmount > rangeMidValue - if (isCreateOfferMode && highestPossibleUsdAmount.getValue() > rangeMidValue && - rangeMidValue > MAX_USD_TRADE_AMOUNT_WITHOUT_REPUTATION.getValue()) { - maxOrFixAmountComponent.setQuoteSideAmount(Fiat.fromValue(rangeMidValue, "USD")); - } else { - applyReputationBasedQuoteSideAmount(); + if (maxOrFixAmountComponent.getQuoteSideAmount().get() == null) { + maxOrFixAmountComponent.setQuoteSideAmount(defaultFiatAmount); } } else { + // Seller case if (isCreateOfferMode) { model.setLinkToWikiText(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.overlay.linkToWikiText")); model.setAmountLimitInfoLink(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.link")); @@ -729,7 +703,6 @@ private void applyQuoteSideMinMaxRange() { applyReputationBasedQuoteSideAmount(); } else { // Wizard - //todo model.setLinkToWikiText(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.overlay.linkToWikiText")); model.setAmountLimitInfoLink(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.link")); @@ -753,6 +726,38 @@ private void applyReputationBasedQuoteSideAmount() { } } + private long getNumMatchingOffers(Monetary quoteSideAmount) { + return bisqEasyOfferbookChannelService.findChannel(model.getMarket()).orElseThrow().getChatMessages().stream() + .filter(chatMessage -> chatMessage.getBisqEasyOffer().isPresent()) + .map(chatMessage -> chatMessage.getBisqEasyOffer().get()) + .filter(offer -> { + if (!isValidDirection(offer)) { + return false; + } + if (!isValidMarket(offer)) { + return false; + } + if (!isValidMakerProfile(offer)) { + return false; + } + if (!isValidAmountRange(offer)) { + return false; + } + + Optional result = BisqEasyTradeAmountLimits.checkOfferAmountLimitForMinAmount(reputationService, + userIdentityService, + userProfileService, + marketPriceService, + model.getMarket(), + quoteSideAmount, + offer); + if (!result.map(Result::isValid).orElse(false)) { + return false; + } + return true; + }) + .count(); + } /////////////////////////////////////////////////////////////////////////////////////////////////// // Filter From cd1eb7150897efe6de81ccbf85ca8ac0d3f28f22 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 13 Sep 2024 18:37:26 +0700 Subject: [PATCH 5/5] Add range marker to slider --- .../bisq_easy/components/AmountComponent.java | 79 +++++++++---- .../amount/TakeOfferAmountController.java | 6 +- .../amount/TradeWizardAmountController.java | 104 ++++++++++++++---- .../list/ChatMessagesListController.java | 2 - .../bisq_easy/BisqEasyTradeAmountLimits.java | 10 -- i18n/src/main/resources/bisq_easy.properties | 4 +- 6 files changed, 147 insertions(+), 58 deletions(-) diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountComponent.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountComponent.java index 0506e07e18..7ec7e26f44 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountComponent.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/components/AmountComponent.java @@ -50,6 +50,8 @@ import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; +import java.util.Optional; + import static com.google.common.base.Preconditions.checkArgument; @Slf4j @@ -93,12 +95,16 @@ public void setMinMaxRange(Monetary minRangeValue, Monetary maxRangeValue) { controller.setMinMaxRange(minRangeValue, maxRangeValue); } - public void setReputationBasedQuoteSideAmount(Monetary reputationBasedQuoteSideAmount) { - controller.setReputationBasedQuoteSideAmount(reputationBasedQuoteSideAmount); + public void setLeftMarkerQuoteSideValue(Monetary quoteSideAmount) { + controller.setLeftMarkerQuoteSideValue(quoteSideAmount); + } + + public void setRightMarkerQuoteSideValue(Monetary quoteSideAmount) { + controller.setRightMarkerQuoteSideValue(quoteSideAmount); } public Monetary getReputationBasedQuoteSideAmount() { - return controller.model.getReputationBasedQuoteSideAmount(); + return controller.model.getRightMarkerQuoteSideValue(); } public void applyReputationBasedQuoteSideAmount() { @@ -128,6 +134,9 @@ public void setDescription(String description) { } private static class Controller implements bisq.desktop.common.view.Controller { + private static final String SLIDER_TRACK_DEFAULT_COLOR = "-bisq-dark-grey-50"; + private static final String SLIDER_TRACK_MARKER_COLOR = "-bisq2-green"; + private final Model model; @Getter private final View view; @@ -243,13 +252,18 @@ public void setMinMaxRange(Monetary minRangeValue, Monetary maxRangeValue) { applyInitialRangeValues(); } - public void setReputationBasedQuoteSideAmount(Monetary reputationBasedQuoteSideAmount) { - model.setReputationBasedQuoteSideAmount(reputationBasedQuoteSideAmount); + public void setLeftMarkerQuoteSideValue(Monetary quoteSideAmount) { + model.setLeftMarkerQuoteSideValue(quoteSideAmount); + applySliderTrackStyle(); + } + + public void setRightMarkerQuoteSideValue(Monetary quoteSideAmount) { + model.setRightMarkerQuoteSideValue(quoteSideAmount); applySliderTrackStyle(); } public void applyReputationBasedQuoteSideAmount() { - quoteSideAmountInput.setAmount(model.getReputationBasedQuoteSideAmount()); + quoteSideAmountInput.setAmount(model.getRightMarkerQuoteSideValue()); } public void setQuote(PriceQuote priceQuote) { @@ -414,22 +428,37 @@ private void applyInitialRangeValues() { private void applySliderTrackStyle() { Monetary minRangeMonetary = model.getMinRangeQuoteSideValue().get(); Monetary maxRangeMonetary = model.getMaxRangeQuoteSideValue().get(); - Monetary reputationBasedQuoteSideAmount = model.getReputationBasedQuoteSideAmount(); - if (reputationBasedQuoteSideAmount != null && - minRangeMonetary != null && - maxRangeMonetary != null) { - double repAmount = reputationBasedQuoteSideAmount.getValue() - minRangeMonetary.getValue(); - double range = model.getMaxRangeMonetary().get().getValue() - minRangeMonetary.getValue(); - double reputationBasedAmountOnSlider = range != 0 ? repAmount / range : 0; - String rightSideColor = "-bisq-dark-grey-50"; - model.getSliderTrackStyle().set(String.format( - "-track-color: linear-gradient(to right, " + - "-bisq2-green 0%%, " + - "-bisq2-green %1$.1f%%, " + - rightSideColor + " %1$.1f%%, " + - rightSideColor + " 100%%);", - 100 * reputationBasedAmountOnSlider)); + if (minRangeMonetary == null || maxRangeMonetary == null) { + return; } + long minRangeMonetaryValue = minRangeMonetary.getValue(); + long maxRangeMonetaryValue = maxRangeMonetary.getValue(); + double range = maxRangeMonetaryValue - minRangeMonetaryValue; + + // If left value is not set we use minRange + // If left value is set but right value not set we don't show any marker + Monetary markerQuoteSideValue = model.getLeftMarkerQuoteSideValue(); + long leftMarkerQuoteSideValue = Optional.ofNullable(markerQuoteSideValue).orElse(minRangeMonetary).getValue(); + double left = leftMarkerQuoteSideValue - minRangeMonetaryValue; + double leftPercentage = range != 0 ? 100 * left / range : 0; + + long rightMarkerQuoteSideValue = Optional.ofNullable(model.getRightMarkerQuoteSideValue()).orElse(minRangeMonetary).getValue(); + double right = rightMarkerQuoteSideValue - minRangeMonetaryValue; + double rightPercentage = range != 0 ? 100 * right / range : 0; + + // E.g.: -bisq-dark-grey-50 0%, -bisq-dark-grey-50 30.0%, -bisq2-green 30.0%, -bisq2-green 60.0%, -bisq-dark-grey-50 60.0%, -bisq-dark-grey-50 100%) + String segments = String.format( + SLIDER_TRACK_DEFAULT_COLOR + " 0%%, " + + SLIDER_TRACK_DEFAULT_COLOR + " %1$.1f%%, " + + + SLIDER_TRACK_MARKER_COLOR + " %1$.1f%%, " + + SLIDER_TRACK_MARKER_COLOR + " %2$.1f%%, " + + + SLIDER_TRACK_DEFAULT_COLOR + " %2$.1f%%, " + + SLIDER_TRACK_DEFAULT_COLOR + " 100%%)", + leftPercentage, rightPercentage); + String style = "-track-color: linear-gradient(to right, " + segments + ";"; + model.getSliderTrackStyle().set(style); } @Override @@ -446,6 +475,8 @@ public void onDeactivate() { maxRangeCustomValuePin.unsubscribe(); baseSideAmountValidPin.unsubscribe(); quoteSideAmountValidPin.unsubscribe(); + model.setLeftMarkerQuoteSideValue(null); + model.setRightMarkerQuoteSideValue(null); } private void setQuoteFromBase() { @@ -500,7 +531,9 @@ private static class Model implements bisq.desktop.common.view.Model { @Setter private ObjectProperty maxRangeQuoteSideValue = new SimpleObjectProperty<>(); @Setter - private Monetary reputationBasedQuoteSideAmount; + private Monetary leftMarkerQuoteSideValue; + @Setter + private Monetary rightMarkerQuoteSideValue; private final StringProperty sliderTrackStyle = new SimpleStringProperty(); @Setter private Market market = MarketRepository.getDefault(); @@ -522,6 +555,8 @@ void reset() { sliderFocus.set(false); market = MarketRepository.getDefault(); direction = Direction.BUY; + leftMarkerQuoteSideValue = null; + rightMarkerQuoteSideValue = null; } } diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/take_offer/amount/TakeOfferAmountController.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/take_offer/amount/TakeOfferAmountController.java index 835eb58dda..76ef0314ec 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/take_offer/amount/TakeOfferAmountController.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/take_offer/amount/TakeOfferAmountController.java @@ -87,7 +87,7 @@ public void init(BisqEasyOffer bisqEasyOffer, Optional optionalReputat maxAmount = reputationBasedQuoteSideAmount.isGreaterThan(offersQuoteSideMaxOrFixedAmount) ? offersQuoteSideMaxOrFixedAmount : reputationBasedQuoteSideAmount; - amountComponent.setReputationBasedQuoteSideAmount(maxAmount); + amountComponent.setRightMarkerQuoteSideValue(maxAmount); applyQuoteSideMinMaxRange(quoteSideMinAmount, maxAmount); long sellersScore = reputationService.getReputationScore(userIdentityService.getSelectedUserIdentity().getUserProfile()).getTotalScore(); @@ -186,7 +186,7 @@ private void applyQuoteSideMinMaxRange(Monetary minRangeValue, Monetary maxRange if (reputationBasedQuoteSideAmount.isLessThan(maxRangeValue)) { model.getIsAmountLimitInfoVisible().set(true); - amountComponent.setReputationBasedQuoteSideAmount(reputationBasedQuoteSideAmount); + amountComponent.setRightMarkerQuoteSideValue(reputationBasedQuoteSideAmount); amountComponent.setQuoteSideAmount(reputationBasedQuoteSideAmount); String formattedAmount = AmountFormatter.formatAmountWithCode(reputationBasedQuoteSideAmount); model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.takeOffer.amount.buyer.limitInfo.overlay.info", sellersReputationScore, formattedAmount) + "\n\n"); @@ -209,7 +209,7 @@ private void applyQuoteSideMinMaxRange(Monetary minRangeValue, Monetary maxRange BisqEasyTradeAmountLimits.getReputationBasedQuoteSideAmount(marketPriceService, bisqEasyOffer.getMarket(), myReputationScore) .ifPresent(myReputationBasedQuoteSideAmount -> { model.getIsAmountHyperLinkDisabled().set(myReputationBasedQuoteSideAmount.isGreaterThan(maxRangeValue)); - amountComponent.setReputationBasedQuoteSideAmount(myReputationBasedQuoteSideAmount); + amountComponent.setRightMarkerQuoteSideValue(myReputationBasedQuoteSideAmount); String formattedAmount = AmountFormatter.formatAmountWithCode(myReputationBasedQuoteSideAmount); model.getIsAmountLimitInfoVisible().set(true); model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo", myReputationScore)); diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountController.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountController.java index dc96629241..4034b7adaa 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountController.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/trade_wizard/amount/TradeWizardAmountController.java @@ -26,6 +26,7 @@ import bisq.chat.bisqeasy.offerbook.BisqEasyOfferbookChannelService; import bisq.common.currency.Market; import bisq.common.currency.MarketRepository; +import bisq.common.data.Pair; import bisq.common.monetary.Fiat; import bisq.common.monetary.Monetary; import bisq.common.monetary.PriceQuote; @@ -479,7 +480,6 @@ private boolean filterOffersByAmounts(BisqEasyOffer peersOffer) { } return BisqEasyTradeAmountLimits.checkOfferAmountLimitForMaxOrFixedAmount(reputationService, - bisqEasyService, userIdentityService, userProfileService, marketPriceService, @@ -578,10 +578,12 @@ private void quoteSideAmountsChanged(boolean maxAmountChanged) { } } else { // Wizard + applyMarkerRange(); + long numMatchingOffers = getNumMatchingOffers(maxOrFixedQuoteSideAmount); String numOffers = Res.getPluralization("bisqEasy.tradeWizard.amount.numOffers", numMatchingOffers); - model.getAmountLimitInfoLeadLine().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.wizard.info.leadLine",numOffers)); + model.getAmountLimitInfoLeadLine().set(Res.get("bisqEasy.tradeWizard.amount.buyer.limitInfo.wizard.info.leadLine", numOffers)); boolean weakSecurity = maxOrFixedQuoteSideAmount.isLessThanOrEqual(MAX_USD_TRADE_AMOUNT_WITHOUT_REPUTATION); String formatted = formatAmountWithCode(MAX_USD_TRADE_AMOUNT_WITHOUT_REPUTATION); if (weakSecurity) { @@ -614,9 +616,11 @@ private void quoteSideAmountsChanged(boolean maxAmountChanged) { } } else { // Wizard + applyMarkerRange(); + long numMatchingOffers = getNumMatchingOffers(maxOrFixedQuoteSideAmount); String numOffers = Res.getPluralization("bisqEasy.tradeWizard.amount.numOffers", numMatchingOffers); - model.getAmountLimitInfoLeadLine().set(Res.get("bisqEasy.tradeWizard.amount.seller.numMatchingOffers.info", numOffers)); + model.getAmountLimitInfoLeadLine().set(Res.get("bisqEasy.tradeWizard.amount.seller.wizard.numMatchingOffers.info", numOffers)); model.getIsWarningIconVisible().set(numMatchingOffers == 0); long myReputationScore = model.getMyReputationScore(); @@ -624,7 +628,7 @@ private void quoteSideAmountsChanged(boolean maxAmountChanged) { model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.wizard.limitInfo", myReputationScore)); model.getIsAmountHyperLinkDisabled().set(true); model.getAmountLimitInfoAmount().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfoAmount", formattedReputationBasedMaxAmount)); - model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo.wizard.overlay.info", myReputationScore, formattedReputationBasedMaxAmount)); + model.getAmountLimitInfoOverlayInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.wizard.limitInfo.overlay.info", myReputationScore, formattedReputationBasedMaxAmount)); } } } @@ -652,6 +656,9 @@ private void applyQuoteSideMinMaxRange() { minAmountComponent.setMinMaxRange(minRangeValue, maxRangeValue); maxOrFixAmountComponent.setMinMaxRange(minRangeValue, maxRangeValue); } else { + // Wizard + applyMarkerRange(); + model.getIsLearnMoreVisible().set(model.getDirection().isSell()); if (model.getDirection().isBuy()) { maxOrFixAmountComponent.setMinMaxRange(minRangeValue, maxRangeValue); @@ -678,8 +685,8 @@ private void applyQuoteSideMinMaxRange() { Monetary highestPossibleUsdAmount = BisqEasyTradeAmountLimits.getUsdAmountFromReputationScore(highestScore); if (isCreateOfferMode) { - minAmountComponent.setReputationBasedQuoteSideAmount(highestPossibleUsdAmount); - maxOrFixAmountComponent.setReputationBasedQuoteSideAmount(highestPossibleUsdAmount); + minAmountComponent.setRightMarkerQuoteSideValue(highestPossibleUsdAmount); + maxOrFixAmountComponent.setRightMarkerQuoteSideValue(highestPossibleUsdAmount); } if (maxOrFixAmountComponent.getQuoteSideAmount().get() == null) { maxOrFixAmountComponent.setQuoteSideAmount(defaultFiatAmount); @@ -693,8 +700,8 @@ private void applyQuoteSideMinMaxRange() { Monetary reputationBasedQuoteSideAmount = model.getReputationBasedMaxAmount(); long myReputationScore = model.getMyReputationScore(); model.getAmountLimitInfo().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfo", myReputationScore)); - minAmountComponent.setReputationBasedQuoteSideAmount(reputationBasedQuoteSideAmount); - maxOrFixAmountComponent.setReputationBasedQuoteSideAmount(reputationBasedQuoteSideAmount); + minAmountComponent.setRightMarkerQuoteSideValue(reputationBasedQuoteSideAmount); + maxOrFixAmountComponent.setRightMarkerQuoteSideValue(reputationBasedQuoteSideAmount); String formattedAmount = formatAmountWithCode(reputationBasedQuoteSideAmount); model.getAmountLimitInfoAmount().set(Res.get("bisqEasy.tradeWizard.amount.seller.limitInfoAmount", formattedAmount)); @@ -720,14 +727,20 @@ private void applyQuoteSideMinMaxRange() { } } + private void applyMarkerRange() { + Pair, Optional> availableOfferAmountRange = getLowestAndHighestAmountInAvailableOffers(); + maxOrFixAmountComponent.setLeftMarkerQuoteSideValue(availableOfferAmountRange.getFirst().orElse(null)); + maxOrFixAmountComponent.setRightMarkerQuoteSideValue(availableOfferAmountRange.getSecond().orElse(null)); + } + private void applyReputationBasedQuoteSideAmount() { if (model.isCreateOfferMode()) { maxOrFixAmountComponent.setQuoteSideAmount(maxOrFixAmountComponent.getReputationBasedQuoteSideAmount()); } } - private long getNumMatchingOffers(Monetary quoteSideAmount) { - return bisqEasyOfferbookChannelService.findChannel(model.getMarket()).orElseThrow().getChatMessages().stream() + private Pair, Optional> getLowestAndHighestAmountInAvailableOffers() { + List filteredOffers = bisqEasyOfferbookChannelService.findChannel(model.getMarket()).orElseThrow().getChatMessages().stream() .filter(chatMessage -> chatMessage.getBisqEasyOffer().isPresent()) .map(chatMessage -> chatMessage.getBisqEasyOffer().get()) .filter(offer -> { @@ -740,20 +753,54 @@ private long getNumMatchingOffers(Monetary quoteSideAmount) { if (!isValidMakerProfile(offer)) { return false; } - if (!isValidAmountRange(offer)) { - return false; - } - Optional result = BisqEasyTradeAmountLimits.checkOfferAmountLimitForMinAmount(reputationService, + Optional result = checkOfferAmountLimitForMinAmount(reputationService, userIdentityService, userProfileService, marketPriceService, - model.getMarket(), - quoteSideAmount, offer); if (!result.map(Result::isValid).orElse(false)) { return false; } + + return true; + }) + .toList(); + Optional lowest = filteredOffers.stream() + .map(offer -> OfferAmountUtil.findQuoteSideMinOrFixedAmount(marketPriceService, offer)) + .filter(Optional::isPresent) + .map(Optional::get) + .min(Monetary::compareTo); + Optional highest = filteredOffers.stream() + .map(offer -> OfferAmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, offer)) + .filter(Optional::isPresent) + .map(Optional::get) + .max(Monetary::compareTo); + return new Pair<>(lowest, highest); + } + + private long getNumMatchingOffers(Monetary quoteSideAmount) { + return bisqEasyOfferbookChannelService.findChannel(model.getMarket()).orElseThrow().getChatMessages().stream() + .filter(chatMessage -> chatMessage.getBisqEasyOffer().isPresent()) + .map(chatMessage -> chatMessage.getBisqEasyOffer().get()) + .filter(offer -> { + if (!isValidDirection(offer)) { + return false; + } + if (!isValidMarket(offer)) { + return false; + } + if (!isValidMakerProfile(offer)) { + return false; + } + if (!isValidAmountRange(offer)) { + return false; + } + + if (!isValidAmountLimit(offer, quoteSideAmount)) { + return false; + } + return true; }) .count(); @@ -810,23 +857,42 @@ private boolean isValidPaymentMethods(BisqEasyOffer peersOffer) { private boolean isValidAmountRange(BisqEasyOffer peersOffer) { Optional myQuoteSideMinOrFixedAmount = OfferAmountUtil.findQuoteSideMinOrFixedAmount(marketPriceService, model.getQuoteSideAmountSpec().get(), MARKET_PRICE_SPEC, model.getMarket()); Optional peersQuoteSideMaxOrFixedAmount = OfferAmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, peersOffer); - if (myQuoteSideMinOrFixedAmount.orElseThrow().round(0).getValue() > peersQuoteSideMaxOrFixedAmount.orElseThrow().round(0).getValue()) { + if (myQuoteSideMinOrFixedAmount.isEmpty() || peersQuoteSideMaxOrFixedAmount.isEmpty()) { + return false; + } + if (myQuoteSideMinOrFixedAmount.get().round(0).getValue() > peersQuoteSideMaxOrFixedAmount.get().round(0).getValue()) { return false; } Optional myQuoteSideMaxOrFixedAmount = OfferAmountUtil.findQuoteSideMaxOrFixedAmount(marketPriceService, model.getQuoteSideAmountSpec().get(), MARKET_PRICE_SPEC, model.getMarket()); Optional peersQuoteSideMinOrFixedAmount = OfferAmountUtil.findQuoteSideMinOrFixedAmount(marketPriceService, peersOffer); - if (myQuoteSideMaxOrFixedAmount.orElseThrow().round(0).getValue() < peersQuoteSideMinOrFixedAmount.orElseThrow().round(0).getValue()) { + if (myQuoteSideMaxOrFixedAmount.isEmpty() || peersQuoteSideMinOrFixedAmount.isEmpty()) { + return false; + } + if (myQuoteSideMaxOrFixedAmount.get().round(0).getValue() < peersQuoteSideMinOrFixedAmount.get().round(0).getValue()) { return false; } return true; } + private boolean isValidAmountLimit(BisqEasyOffer peersOffer, Monetary quoteSideAmount) { + Optional result = BisqEasyTradeAmountLimits.checkOfferAmountLimitForMinAmount(reputationService, + userIdentityService, + userProfileService, + marketPriceService, + model.getMarket(), + quoteSideAmount, + peersOffer); + if (!result.map(Result::isValid).orElse(false)) { + return false; + } + return true; + } + private boolean isValidAmountLimit(BisqEasyOffer peersOffer) { if (!BisqEasyTradeAmountLimits.checkOfferAmountLimitForMaxOrFixedAmount(reputationService, - bisqEasyService, userIdentityService, userProfileService, marketPriceService, diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/chat/message_container/list/ChatMessagesListController.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/chat/message_container/list/ChatMessagesListController.java index ed7a931c63..b8d059db02 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/chat/message_container/list/ChatMessagesListController.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/chat/message_container/list/ChatMessagesListController.java @@ -293,13 +293,11 @@ public void onTakeOffer(BisqEasyOfferbookMessage bisqEasyOfferbookMessage) { } Optional limitForMinAmount = BisqEasyTradeAmountLimits.checkOfferAmountLimitForMinAmount(reputationService, - bisqEasyService, userIdentityService, userProfileService, marketPriceService, bisqEasyOffer); Optional limitForMaxAmount = BisqEasyTradeAmountLimits.checkOfferAmountLimitForMaxOrFixedAmount(reputationService, - bisqEasyService, userIdentityService, userProfileService, marketPriceService, diff --git a/bisq-easy/src/main/java/bisq/bisq_easy/BisqEasyTradeAmountLimits.java b/bisq-easy/src/main/java/bisq/bisq_easy/BisqEasyTradeAmountLimits.java index e217beafd3..2efa7edaab 100644 --- a/bisq-easy/src/main/java/bisq/bisq_easy/BisqEasyTradeAmountLimits.java +++ b/bisq-easy/src/main/java/bisq/bisq_easy/BisqEasyTradeAmountLimits.java @@ -93,16 +93,7 @@ public static Optional checkOfferAmountLimitForMinAmount(ReputationServi }); } - public static Optional findRequiredReputationScoreByFiatAmount1(MarketPriceService marketPriceService, - Market market, - Monetary fiatAmount) { - return fiatToBtc(marketPriceService, market, fiatAmount) - .flatMap(btc -> btcToUsd(marketPriceService, btc)) - .map(BisqEasyTradeAmountLimits::getRequiredReputationScoreByUsdAmount); - } - public static Optional checkOfferAmountLimitForMinAmount(ReputationService reputationService, - BisqEasyService bisqEasyService, UserIdentityService userIdentityService, UserProfileService userProfileService, MarketPriceService marketPriceService, @@ -115,7 +106,6 @@ public static Optional checkOfferAmountLimitForMinAmount(ReputationServi } public static Optional checkOfferAmountLimitForMaxOrFixedAmount(ReputationService reputationService, - BisqEasyService bisqEasyService, UserIdentityService userIdentityService, UserProfileService userProfileService, MarketPriceService marketPriceService, diff --git a/i18n/src/main/resources/bisq_easy.properties b/i18n/src/main/resources/bisq_easy.properties index cfc3cceffd..e4582216c8 100644 --- a/i18n/src/main/resources/bisq_easy.properties +++ b/i18n/src/main/resources/bisq_easy.properties @@ -156,9 +156,9 @@ bisqEasy.component.amount.baseSide.tooltip.taker.offerPrice=with the offer price bisqEasy.tradeWizard.amount.limitInfo.overlay.headline=Reputation-based trade amount limits bisqEasy.tradeWizard.amount.limitInfo.overlay.close=Close overlay -bisqEasy.tradeWizard.amount.seller.numMatchingOffers.info=There {0} matching the chosen trade amount. +bisqEasy.tradeWizard.amount.seller.wizard.numMatchingOffers.info=There {0} matching the chosen trade amount. bisqEasy.tradeWizard.amount.seller.wizard.limitInfo=With your reputation score of {0} you can trade up to -bisqEasy.tradeWizard.amount.seller.limitInfo.wizard.overlay.info=With a reputation score of {0}, you provide security for trades up to {1}.\n\n\ +bisqEasy.tradeWizard.amount.seller.wizard.limitInfo.overlay.info=With a reputation score of {0}, you provide security for trades up to {1}.\n\n\ You can find information on how to increase your reputation at ''Reputation/Build Reputation''. bisqEasy.tradeWizard.amount.seller.limitInfo=Your reputation score of {0} provides security for offers up to