Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tooltip for unsigned accounts needs to provide more information #5569

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ public String getDisplayString() {
return String.format(displayString, daysUntilLimitLifted);
}

public boolean isLimitLifted() {
return this == PEER_LIMIT_LIFTED || this == PEER_SIGNER || this == ARBITRATOR;
}

}

private final KeyRing keyRing;
Expand Down
8 changes: 8 additions & 0 deletions core/src/main/resources/i18n/displayStrings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,14 @@ offerbook.timeSinceSigning.info.signer=signed by peer and can sign peer accounts
offerbook.timeSinceSigning.info.banned=account was banned
offerbook.timeSinceSigning.daysSinceSigning={0} days
offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing
offerbook.timeSinceSigning.tooltip.accountLimit=Account limit: {0}
offerbook.timeSinceSigning.tooltip.accountLimitLifted=Account limit lifted
offerbook.timeSinceSigning.tooltip.info.unsigned=This account hasn't been signed yet
offerbook.timeSinceSigning.tooltip.info.signed=This account has been signed
offerbook.timeSinceSigning.tooltip.info.signedAndLifted=This account has been signed and can sign peer accounts
offerbook.timeSinceSigning.tooltip.checkmark.buyBtc=but BTC from a signed account
This conversation was marked as resolved.
Show resolved Hide resolved
offerbook.timeSinceSigning.tooltip.checkmark.wait=wait a minimum of {0} days
offerbook.timeSinceSigning.tooltip.learnMore=Learn more
offerbook.xmrAutoConf=Is auto-confirm enabled

offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\
Expand Down
9 changes: 9 additions & 0 deletions desktop/src/main/java/bisq/desktop/bisq.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
-fx-text-fill: -bs-rd-error-red;
}

.text-gray-ddd {
This conversation was marked as resolved.
Show resolved Hide resolved
-fx-text-fill: -bs-color-gray-ddd;
}

.error {
-fx-accent: -bs-rd-error-red;
}
Expand Down Expand Up @@ -609,6 +613,11 @@ tree-table-view:focused {
-fx-font-size: 13;
}

.bold-text,
.bold-text .text {
-fx-font-weight: bold;
}

/* Splash */
#splash {
-fx-background-color: -bs-background-color;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/

package bisq.desktop.components;

import bisq.desktop.components.controlsfx.control.PopOver;
import bisq.desktop.main.offer.offerbook.OfferBookListItem;
import bisq.desktop.util.FormBuilder;
import bisq.desktop.util.GUIUtil;

import bisq.core.account.sign.SignedWitnessService;
import bisq.core.locale.Res;
import bisq.core.offer.OfferRestrictions;
import bisq.core.util.coin.CoinFormatter;

import de.jensd.fx.fontawesome.AwesomeIcon;

import javafx.scene.Node;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;

import javafx.geometry.Insets;
import javafx.geometry.Pos;


public class AccountStatusTooltipLabel extends AutoTooltipLabel {

public static final int DEFAULT_WIDTH = 300;
private final Node textIcon;
private final PopOverWrapper popoverWrapper = new PopOverWrapper();
private final OfferBookListItem.WitnessAgeData witnessAgeData;
private final String popupTitle;

public AccountStatusTooltipLabel(OfferBookListItem.WitnessAgeData witnessAgeData,
CoinFormatter formatter) {
super(witnessAgeData.getDisplayString());
this.witnessAgeData = witnessAgeData;
this.textIcon = FormBuilder.getIcon(witnessAgeData.getIcon());
this.popupTitle = witnessAgeData.isLimitLifted()
? Res.get("offerbook.timeSinceSigning.tooltip.accountLimitLifted")
: Res.get("offerbook.timeSinceSigning.tooltip.accountLimit", formatter.formatCoinWithCode(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT));

positionAndActivateIcon();
}

private void positionAndActivateIcon() {
textIcon.setOpacity(0.4);
textIcon.getStyleClass().add("tooltip-icon");
textIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createPopOver()));
This conversation was marked as resolved.
Show resolved Hide resolved
textIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver());

setGraphic(textIcon);
setContentDisplay(ContentDisplay.RIGHT);
}

private PopOver createPopOver() {
Label titleLabel = new Label(popupTitle);
titleLabel.setMaxWidth(DEFAULT_WIDTH);
titleLabel.setWrapText(true);
titleLabel.setPadding(new Insets(10, 10, 2, 10));
titleLabel.getStyleClass().add("bold-text");
titleLabel.getStyleClass().add("default-text");

Label infoLabel = new Label(witnessAgeData.getInfo());
infoLabel.setMaxWidth(DEFAULT_WIDTH);
infoLabel.setWrapText(true);
infoLabel.setPadding(new Insets(2, 10, 2, 10));
infoLabel.getStyleClass().add("default-text");

Label buyLabel = createDetailsItem(
Res.get("offerbook.timeSinceSigning.tooltip.checkmark.buyBtc"),
witnessAgeData.isAccountSigned()
);
Label waitLabel = createDetailsItem(
Res.get("offerbook.timeSinceSigning.tooltip.checkmark.wait", SignedWitnessService.SIGNER_AGE_DAYS),
witnessAgeData.isLimitLifted()
);

Hyperlink learnMoreLink = new Hyperlink(Res.get("offerbook.timeSinceSigning.tooltip.learnMore"));
learnMoreLink.setMaxWidth(DEFAULT_WIDTH);
learnMoreLink.setWrapText(true);
learnMoreLink.setPadding(new Insets(10, 10, 2, 10));
learnMoreLink.getStyleClass().add("very-small-text");
learnMoreLink.setOnAction((e) -> {
GUIUtil.openWebPage("https://bisq.wiki/Account_limits");
});
This conversation was marked as resolved.
Show resolved Hide resolved

VBox vBox = new VBox(2, titleLabel, infoLabel, buyLabel, waitLabel, learnMoreLink);
vBox.setPadding(new Insets(2, 0, 2, 0));
vBox.setAlignment(Pos.CENTER_LEFT);

PopOver popOver = new PopOver(vBox);
if (textIcon.getScene() != null) {
popOver.setDetachable(false);
popOver.setArrowLocation(PopOver.ArrowLocation.LEFT_CENTER);
popOver.show(textIcon, -10);
}
return popOver;
}

private Label createDetailsItem(String text, boolean active) {
Label icon = FormBuilder.getIcon(active ? AwesomeIcon.OK_SIGN : AwesomeIcon.REMOVE_SIGN);
icon.setLayoutY(4);
icon.getStyleClass().add("icon");
if (active) {
icon.getStyleClass().add("highlight");
} else {
icon.getStyleClass().add("text-gray-ddd");
}

Label label = new Label(text, icon);
label.setMaxWidth(DEFAULT_WIDTH);
label.setWrapText(true);
label.setPadding(new Insets(0, 10, 0, 10));
label.getStyleClass().addAll("small-text");
if (active) {
label.getStyleClass().add("success-text");
} else {
label.getStyleClass().add("text-gray-ddd");
}
return label;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
import lombok.Value;
import lombok.extern.slf4j.Slf4j;

import org.jetbrains.annotations.NotNull;

@Slf4j

public class OfferBookListItem {
Expand All @@ -55,102 +57,112 @@ public OfferBookListItem(Offer offer) {
public WitnessAgeData getWitnessAgeData(AccountAgeWitnessService accountAgeWitnessService,
SignedWitnessService signedWitnessService) {
if (witnessAgeData == null) {
long ageInMs;
long daysSinceSignedAsLong = -1;
long accountAgeDaysAsLong = -1;
long accountAgeDaysNotYetSignedAsLong = -1;
String displayString;
String info;
GlyphIcons icon;

if (CurrencyUtil.isCryptoCurrency(offer.getCurrencyCode())) {
// Altcoins
displayString = Res.get("offerbook.timeSinceSigning.notSigned.noNeed");
info = Res.get("shared.notSigned.noNeedAlts");
icon = MaterialDesignIcon.INFORMATION_OUTLINE;
witnessAgeData = new WitnessAgeData(WitnessAgeData.TYPE_ALTCOINS);
} else if (PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode())) {
// Fiat and signed witness required
Optional<AccountAgeWitness> optionalWitness = accountAgeWitnessService.findWitness(offer);
AccountAgeWitnessService.SignState signState = optionalWitness.map(accountAgeWitnessService::getSignState)
AccountAgeWitnessService.SignState signState = optionalWitness
.map(accountAgeWitnessService::getSignState)
.orElse(AccountAgeWitnessService.SignState.UNSIGNED);
boolean isSignedAccountAgeWitness = optionalWitness.map(signedWitnessService::isSignedAccountAgeWitness)

boolean isSignedAccountAgeWitness = optionalWitness
.map(signedWitnessService::isSignedAccountAgeWitness)
.orElse(false);

if (isSignedAccountAgeWitness || !signState.equals(AccountAgeWitnessService.SignState.UNSIGNED)) {
// either signed & limits lifted, or waiting for limits to be lifted
// Or banned
daysSinceSignedAsLong = TimeUnit.MILLISECONDS.toDays(optionalWitness.map(witness ->
accountAgeWitnessService.getWitnessSignAge(witness, new Date()))
.orElse(0L));
displayString = Res.get("offerbook.timeSinceSigning.daysSinceSigning", daysSinceSignedAsLong);
info = Res.get("offerbook.timeSinceSigning.info", signState.getDisplayString());
witnessAgeData = new WitnessAgeData(
signState.isLimitLifted() ? WitnessAgeData.TYPE_SIGNED_AND_LIMIT_LIFTED : WitnessAgeData.TYPE_SIGNED_OR_BANNED,
optionalWitness.map(witness -> accountAgeWitnessService.getWitnessSignAge(witness, new Date())).orElse(0L),
signState);
} else {
// Unsigned case
ageInMs = optionalWitness.map(e -> accountAgeWitnessService.getAccountAge(e, new Date()))
.orElse(-1L);
accountAgeDaysNotYetSignedAsLong = ageInMs > -1 ? TimeUnit.MILLISECONDS.toDays(ageInMs) : 0;
displayString = Res.get("offerbook.timeSinceSigning.notSigned");
info = Res.get("shared.notSigned", accountAgeDaysNotYetSignedAsLong);
witnessAgeData = new WitnessAgeData(
WitnessAgeData.TYPE_NOT_SIGNED,
optionalWitness.map(e -> accountAgeWitnessService.getAccountAge(e, new Date())).orElse(0L),
signState
);
}

icon = GUIUtil.getIconForSignState(signState);
} else {
// Fiat, no signed witness required, we show account age
ageInMs = accountAgeWitnessService.getAccountAge(offer);
accountAgeDaysAsLong = ageInMs > -1 ? TimeUnit.MILLISECONDS.toDays(ageInMs) : 0;
displayString = Res.get("offerbook.timeSinceSigning.notSigned.ageDays", accountAgeDaysAsLong);
info = Res.get("shared.notSigned.noNeedDays", accountAgeDaysAsLong);
icon = MaterialDesignIcon.CHECKBOX_MARKED_OUTLINE;
witnessAgeData = new WitnessAgeData(
WitnessAgeData.TYPE_NOT_SIGNING_REQUIRED,
accountAgeWitnessService.getAccountAge(offer)
);
}

witnessAgeData = new WitnessAgeData(displayString, info, icon, daysSinceSignedAsLong, accountAgeDaysNotYetSignedAsLong, accountAgeDaysAsLong);
}
return witnessAgeData;
}

@Value
public static class WitnessAgeData {
private final String displayString;
private final String info;
private final GlyphIcons icon;
private final Long daysSinceSignedAsLong;
private final long accountAgeDaysNotYetSignedAsLong;
private final Long accountAgeDaysAsLong;
public static class WitnessAgeData implements Comparable<WitnessAgeData> {
String displayString;
String info;
GlyphIcons icon;
// Used for sorting
private final Long type;
Long type;
// Used for sorting
private final Long days;

public WitnessAgeData(String displayString,
String info,
GlyphIcons icon,
long daysSinceSignedAsLong,
long accountAgeDaysNotYetSignedAsLong,
long accountAgeDaysAsLong) {
this.displayString = displayString;
this.info = info;
this.icon = icon;
this.daysSinceSignedAsLong = daysSinceSignedAsLong;
this.accountAgeDaysNotYetSignedAsLong = accountAgeDaysNotYetSignedAsLong;
this.accountAgeDaysAsLong = accountAgeDaysAsLong;

if (daysSinceSignedAsLong > -1) {
// First we show signed accounts sorted by days
this.type = 3L;
this.days = daysSinceSignedAsLong;
} else if (accountAgeDaysNotYetSignedAsLong > -1) {
// Next group is not yet signed accounts sorted by account age
this.type = 2L;
this.days = accountAgeDaysNotYetSignedAsLong;
} else if (accountAgeDaysAsLong > -1) {
// Next group is not signing required accounts sorted by account age
this.type = 1L;
this.days = accountAgeDaysAsLong;
Long days;

public static final long TYPE_SIGNED_AND_LIMIT_LIFTED = 4L;
public static final long TYPE_SIGNED_OR_BANNED = 3L;
public static final long TYPE_NOT_SIGNED = 2L;
public static final long TYPE_NOT_SIGNING_REQUIRED = 1L;
public static final long TYPE_ALTCOINS = 0L;

public WitnessAgeData(long type) {
this(type, 0, null);
}

public WitnessAgeData(long type, long days) {
this(type, days, null);
}

public WitnessAgeData(long type, long age, AccountAgeWitnessService.SignState signState) {
this.type = type;
long days = age > -1 ? TimeUnit.MILLISECONDS.toDays(age) : 0;
this.days = days;

if (type == TYPE_SIGNED_AND_LIMIT_LIFTED) {
this.displayString = Res.get("offerbook.timeSinceSigning.daysSinceSigning", days);
this.info = Res.get("offerbook.timeSinceSigning.tooltip.info.signedAndLifted");
this.icon = GUIUtil.getIconForSignState(signState);
} else if (type == TYPE_SIGNED_OR_BANNED) {
this.displayString = Res.get("offerbook.timeSinceSigning.daysSinceSigning", days);
this.info = Res.get("offerbook.timeSinceSigning.tooltip.info.signed");
this.icon = GUIUtil.getIconForSignState(signState);
} else if (type == TYPE_NOT_SIGNED) {
this.displayString = Res.get("offerbook.timeSinceSigning.notSigned");
this.info = Res.get("offerbook.timeSinceSigning.tooltip.info.unsigned");
this.icon = GUIUtil.getIconForSignState(signState);
} else if (type == TYPE_NOT_SIGNING_REQUIRED) {
this.displayString = Res.get("offerbook.timeSinceSigning.notSigned.ageDays", days);
this.info = Res.get("shared.notSigned.noNeedDays", days);
this.icon = MaterialDesignIcon.CHECKBOX_MARKED_OUTLINE;
} else {
// No signing and age required (altcoins)
this.type = 0L;
this.days = 0L;
this.displayString = Res.get("offerbook.timeSinceSigning.notSigned.noNeed");
this.info = Res.get("shared.notSigned.noNeedAlts");
this.icon = MaterialDesignIcon.INFORMATION_OUTLINE;
}
}

public boolean isAccountSigned() {
return this.type == TYPE_SIGNED_AND_LIMIT_LIFTED || this.type == TYPE_SIGNED_OR_BANNED;
}

public boolean isLimitLifted() {
return this.type == TYPE_SIGNED_AND_LIMIT_LIFTED;
}

public boolean isSigningRequired() {
return this.type != TYPE_NOT_SIGNING_REQUIRED && this.type != TYPE_ALTCOINS;
}

@Override
public int compareTo(@NotNull WitnessAgeData o) {
return (int) (this.type.equals(o.getType()) ? this.days - o.getDays() : this.type - o.getType());
}
}
}

Loading