Support UI improvements (per bisq project 67).

This commit is contained in:
jmacxx 2023-03-04 15:29:37 -06:00
parent cfeb597cf1
commit c404eb1e91
No known key found for this signature in database
GPG Key ID: 155297BABFE94A1B
18 changed files with 331 additions and 149 deletions

View File

@ -94,12 +94,12 @@ public class DisputeMsgEvents {
log.debug("We got a ChatMessage added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId());
c.next();
if (c.wasAdded()) {
c.getAddedSubList().forEach(chatMessage -> onChatMessage(chatMessage, dispute));
c.getAddedSubList().forEach(this::onChatMessage);
}
});
}
private void onChatMessage(ChatMessage chatMessage, Dispute dispute) {
private void onChatMessage(ChatMessage chatMessage) {
if (chatMessage.getSenderNodeAddress().equals(p2PService.getAddress())) {
return;
}
@ -116,22 +116,5 @@ public class DisputeMsgEvents {
log.error(e.toString());
e.printStackTrace();
}
// We check at every new message if it might be a message sent after the dispute had been closed. If that is the
// case we revert the isClosed flag so that the UI can reopen the dispute and indicate that a new dispute
// message arrived.
Optional<ChatMessage> newestChatMessage = dispute.getChatMessages().stream().
sorted(Comparator.comparingLong(ChatMessage::getDate).reversed()).findFirst();
// If last message is not a result message we re-open as we might have received a new message from the
// trader/mediator/arbitrator who has reopened the case
if (dispute.isClosed() && newestChatMessage.isPresent() && !newestChatMessage.get().isResultMessage(dispute)) {
log.info("Reopening dispute {} due to new chat message received {}", dispute.getTradeId(), newestChatMessage.get().getUid());
dispute.reOpen();
if (dispute.getSupportType() == SupportType.MEDIATION) {
mediationManager.requestPersistence();
} else if (dispute.getSupportType() == SupportType.REFUND) {
refundManager.requestPersistence();
}
}
}
}

View File

@ -42,15 +42,14 @@ import com.google.protobuf.ByteString;
import org.bitcoinj.core.Transaction;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@ -82,7 +81,8 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
NEW,
OPEN,
REOPENED,
CLOSED;
CLOSED,
RESULT_PROPOSED;
public static Dispute.State fromProto(protobuf.Dispute.State state) {
return ProtoUtil.enumFromProto(Dispute.State.class, state.name());
@ -168,7 +168,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
@Setter
private transient boolean payoutDone = false;
private transient final BooleanProperty isClosedProperty = new SimpleBooleanProperty();
private transient final StringProperty disputeStateProperty = new SimpleStringProperty();
private transient final IntegerProperty badgeCountProperty = new SimpleIntegerProperty();
private transient FileTransferReceiver fileTransferSession = null;
@ -239,6 +239,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
id = tradeId + "_" + traderId;
uid = UUID.randomUUID().toString();
setState(State.NEW);
refreshAlertLevel(true);
}
@ -413,7 +414,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
public void setState(Dispute.State disputeState) {
this.disputeState = disputeState;
this.isClosedProperty.set(disputeState == State.CLOSED);
this.disputeStateProperty.set(disputeState.toString());
}
public void setDisputeResult(DisputeResult disputeResult) {
@ -438,10 +439,6 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
return Utilities.getShortId(tradeId);
}
public ReadOnlyBooleanProperty isClosedProperty() {
return isClosedProperty;
}
public ReadOnlyIntegerProperty getBadgeCountProperty() {
return badgeCountProperty;
}
@ -470,6 +467,10 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
return this.disputeState == State.CLOSED;
}
public boolean isResultProposed() {
return this.disputeState == State.RESULT_PROPOSED;
}
public void refreshAlertLevel(boolean senderFlag) {
// if the dispute is "new" that is 1 alert that has to be propagated upstream
// or if there are unread messages that is 1 alert that has to be propagated upstream
@ -559,7 +560,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
",\n agentPubKeyRing=" + agentPubKeyRing +
",\n isSupportTicket=" + isSupportTicket +
",\n chatMessages=" + chatMessages +
",\n isClosedProperty=" + isClosedProperty +
",\n disputeStateProperty=" + disputeStateProperty +
",\n disputeResultProperty=" + disputeResultProperty +
",\n disputePayoutTxId='" + disputePayoutTxId + '\'' +
",\n openingDate=" + openingDate +

View File

@ -99,7 +99,7 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
public void cleanupDisputes(@Nullable Consumer<String> closedDisputeHandler) {
disputeList.stream().forEach(dispute -> {
String tradeId = dispute.getTradeId();
if (dispute.isClosed() && closedDisputeHandler != null) {
if (dispute.isResultProposed() && closedDisputeHandler != null) {
closedDisputeHandler.accept(tradeId);
}
});

View File

@ -31,6 +31,7 @@ import bisq.core.offer.bisq_v1.OfferPayload;
import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.support.SupportManager;
import bisq.core.support.dispute.mediation.MediationResultState;
import bisq.core.support.dispute.messages.DisputeResultMessage;
import bisq.core.support.dispute.messages.OpenNewDisputeMessage;
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
@ -254,6 +255,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
@Override
public void onUpdatedDataReceived() {
tryApplyMessages();
checkDisputesForUpdates();
}
});
@ -263,8 +265,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
});
walletsSetup.numPeersProperty().addListener((observable, oldValue, newValue) -> {
if (walletsSetup.hasSufficientPeersForBroadcast())
if (walletsSetup.hasSufficientPeersForBroadcast()) {
tryApplyMessages();
}
});
tryApplyMessages();
@ -292,6 +295,46 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
maybeClearSensitiveData();
}
private void checkDisputesForUpdates() {
List<Dispute> disputes = getDisputeList().getList();
disputes.forEach(dispute -> {
if (dispute.isResultProposed()) {
// an open dispute where the mediator has proposed a result. has the trade moved on?
// if so, dispute can close and the mediator needs to be informed so they can close their ticket.
tradeManager.getTradeById(dispute.getTradeId()).ifPresentOrElse(
t -> checkForMediatedTradePayout(t, dispute),
() -> closedTradableManager.getTradableById(dispute.getTradeId()).ifPresent(
t -> checkForMediatedTradePayout((Trade) t, dispute)));
}
});
}
protected void checkForMediatedTradePayout(Trade trade, Dispute dispute) {
if (trade.disputeStateProperty().get().isArbitrated() || trade.getTradePhase() == Trade.Phase.PAYOUT_PUBLISHED) {
disputedTradeUpdate(trade.getDisputeState().toString(), dispute, true);
} else {
// user accepted/rejected mediation proposal (before lockup period has expired)
trade.mediationResultStateProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == MediationResultState.MEDIATION_RESULT_ACCEPTED ||
newValue == MediationResultState.MEDIATION_RESULT_REJECTED) {
disputedTradeUpdate(newValue.toString(), dispute, false);
}
});
// user rejected mediation after lockup period: opening arbitration
trade.disputeStateProperty().addListener((observable, oldValue, newValue) -> {
if (newValue.isArbitrated()) {
disputedTradeUpdate(newValue.toString(), dispute, true);
}
});
// trade paid out through mediation
trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == Trade.Phase.PAYOUT_PUBLISHED) {
disputedTradeUpdate(newValue.toString(), dispute, true);
}
});
}
}
public boolean isTrader(Dispute dispute) {
return pubKeyRing.equals(dispute.getTraderPubKeyRing());
}
@ -650,8 +693,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
chatMessage.setSystemMessage(true);
dispute.addAndPersistChatMessage(chatMessage);
addPriceInfoMessage(dispute, 0);
disputeList.add(dispute);
// We mirrored dispute already!
@ -719,6 +760,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
}
}
);
addPriceInfoMessage(dispute, 0);
requestPersistence();
}
@ -897,17 +939,23 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
}
}
public void addMediationReOpenedMessage(Dispute dispute, boolean senderIsTrader) {
// when a mediated trade changes, send a system message informing the mediator, so they can maybe close their ticket.
public void disputedTradeUpdate(String message, Dispute dispute, boolean close) {
if (dispute.isClosed()) {
return;
}
ChatMessage chatMessage = new ChatMessage(
getSupportType(),
dispute.getTradeId(),
dispute.getTraderId(),
senderIsTrader,
Res.get("support.info.disputeReOpened"),
true,
Res.get("support.info.disputedTradeUpdate", message),
p2PService.getAddress());
chatMessage.setSystemMessage(false);
dispute.addAndPersistChatMessage(chatMessage);
this.sendChatMessage(chatMessage);
chatMessage.setSystemMessage(true);
this.sendChatMessage(chatMessage); // inform the mediator
if (close) {
dispute.setIsClosed(); // close the trader's ticket
}
requestPersistence();
}

View File

@ -288,6 +288,47 @@ public final class DisputeResult implements NetworkPayload {
return payoutSuggestion.toString();
}
public String getPayoutSuggestionCustomizedToBuyerOrSeller(boolean isBuyer) {
// see github.com/bisq-network/proposals/issues/407
if (isBuyer) {
switch (payoutSuggestion) {
case BUYER_GETS_TRADE_AMOUNT:
return Res.get("disputeSummaryWindow.result.buyerGetsTradeAmount");
case BUYER_GETS_TRADE_AMOUNT_MINUS_PENALTY:
return Res.get("disputeSummaryWindow.result.buyerGetsTradeAmountMinusPenalty");
case BUYER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION:
return Res.get("disputeSummaryWindow.result.buyerGetsTradeAmountPlusCompensation");
case SELLER_GETS_TRADE_AMOUNT:
return Res.get("disputeSummaryWindow.result.buyerGetsHisDeposit");
case SELLER_GETS_TRADE_AMOUNT_MINUS_PENALTY:
return Res.get("disputeSummaryWindow.result.buyerGetsHisDepositPlusPenaltyFromSeller");
case SELLER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION:
return Res.get("disputeSummaryWindow.result.buyerGetsHisDepositMinusPenalty");
case CUSTOM_PAYOUT:
return Res.get("disputeSummaryWindow.result.customPayout");
default:
}
} else {
switch (payoutSuggestion) {
case SELLER_GETS_TRADE_AMOUNT:
return Res.get("disputeSummaryWindow.result.sellerGetsTradeAmount");
case SELLER_GETS_TRADE_AMOUNT_MINUS_PENALTY:
return Res.get("disputeSummaryWindow.result.sellerGetsTradeAmountMinusPenalty");
case SELLER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION:
return Res.get("disputeSummaryWindow.result.sellerGetsTradeAmountPlusCompensation");
case BUYER_GETS_TRADE_AMOUNT:
return Res.get("disputeSummaryWindow.result.sellerGetsHisDeposit");
case BUYER_GETS_TRADE_AMOUNT_MINUS_PENALTY:
return Res.get("disputeSummaryWindow.result.sellerGetsHisDepositPlusPenaltyFromBuyer");
case BUYER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION:
return Res.get("disputeSummaryWindow.result.sellerGetsHisDepositMinusPenalty");
case CUSTOM_PAYOUT:
return Res.get("disputeSummaryWindow.result.customPayout");
default:
}
}
return Res.get("popup.headline.error");
}
@Override
public String toString() {

View File

@ -141,8 +141,12 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
@Override
public void cleanupDisputes() {
// closes any trades/disputes which paid out while Bisq was not in use
disputeListService.cleanupDisputes(tradeId -> tradeManager.getTradeById(tradeId).filter(trade -> trade.getPayoutTx() != null)
.ifPresent(trade -> tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED)));
.ifPresent(trade -> {
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED);
findOwnDispute(tradeId).ifPresent(Dispute::setIsClosed);
}));
}
@Override
@ -201,7 +205,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
} else {
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId());
}
dispute.setIsClosed();
dispute.setState(Dispute.State.RESULT_PROPOSED);
dispute.setDisputeResult(disputeResult);
@ -216,6 +220,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
trade.setDisputeState(Trade.DisputeState.MEDIATION_CLOSED);
tradeManager.requestPersistence();
checkForMediatedTradePayout(trade, dispute);
}
} else {
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);

View File

@ -158,7 +158,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
String roleContextMsg = Res.get("support.initialArbitratorMsg",
DisputeAgentLookupMap.getMatrixLinkForAgent(getAgentNodeAddress(dispute).getFullAddress()));
String link = "https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration";
return Res.get("support.initialInfo", role, roleContextMsg, role, link);
return Res.get("support.initialInfoRefundAgent", role, roleContextMsg, role, link);
}
@Override

View File

@ -975,8 +975,9 @@ portfolio.pending.mediationResult.popup.info=The mediator has suggested the foll
You can accept or reject this suggested payout.\n\n\
By accepting, you sign the proposed payout transaction. \
Mediation is expected to be the optimal resolution for both traders. \
If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\n\
If one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to reject mediation suggestion, \
If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed. \
Please inform the mediator if the trade is not paid out in the next 48h.\n\n\
If agreement is not possible, you will have to wait until {2} (block {3}) to Send to Arbitration,\
which will open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\n\
If the trade goes to arbitration the arbitrator will pay out the trade amount plus one peer's security deposit. \
This means the total arbitration payout will be less than the mediation payout. \
@ -984,12 +985,14 @@ portfolio.pending.mediationResult.popup.info=The mediator has suggested the foll
one peer not responding, or disputing the mediator made a fair payout suggestion. \n\n\
More details about the arbitration model: [HYPERLINK:https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration]
portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout \
but it seems that your trading peer has not accepted it.\n\n\
but it seems that your trading peer has not yet accepted it. \
Inform your mediator that you have accepted mediation if your peer has not accepted the mediation suggestion in 48h.\n\n\
Once the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will \
investigate the case again and do a payout based on their findings.\n\n\
You can find more details about the arbitration model at:\
You can find more details about the arbitration model at: \
[HYPERLINK:https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration]
portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration
portfolio.pending.mediationResult.popup.reject=Reject
portfolio.pending.mediationResult.popup.openArbitration=Send to arbitration
portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted
portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\n\
@ -1208,7 +1211,7 @@ support.save=Save file to disk
support.messages=Messages
support.input.prompt=Enter message...
support.send=Send
support.addAttachments=Add attachments
support.addAttachments=Attach Files
support.closeTicket=Close ticket
support.attachments=Attachments:
support.savedInMailbox=Message saved in receiver's mailbox
@ -1274,6 +1277,20 @@ support.initialInfo=Please enter a description of your problem in the text field
\t● You need to cooperate with the {2} and provide the information they request to make your case.\n\
\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\n\
You can read more about the dispute process at: {3}
support.initialInfoRefundAgent=Please describe why have you opened arbitration, or why do you think your peer did so. \
Add as much information as possible to speed up dispute resolution time. Mediation and trading chats are not shared with the arbitrator.\n\n\
Here is a check list for information you should provide:\n\
\t● If you are the BTC buyer: Did you make the Fiat or Altcoin transfer? If so, did you click the 'payment started' \
button in the application? Did you accept mediator's suggestion?\n\
\t● If you are the BTC seller: Did you receive the Fiat or Altcoin payment? If so, did you click the 'payment received' \
button in the application? Did you accept mediator's suggestion?\n\
Please make yourself familiar with the basic rules for the dispute process:\n\
\t● You need to respond to the {0}''s requests within 2 days.\n\
\t● {1}\n\
\t● The maximum period for a dispute is 14 days.\n\
\t● You need to cooperate with the {2} and provide the information they request to make your case.\n\
\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\n\
You can read more about the dispute process at: {3}
support.initialMediatorMsg=Mediators will generally reply to you within 24 hours.\n\
\t If you have not had a reply after 48 hours please feel free to reach out to your mediator on Matrix.\n\
\t Mediators usernames on Matrix are the same as their usernames within the Bisq app.\n\
@ -1302,7 +1319,7 @@ support.warning.disputesWithInvalidDonationAddress=The delayed payout transactio
support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute?
support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout.
support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out.
support.info.disputeReOpened=Dispute ticket has been re-opened.
support.info.disputedTradeUpdate=Disputed trade update: {0}
####################################################################
# Settings
@ -2854,10 +2871,22 @@ disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account
disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late
# suppress inspection "UnusedProperty"
disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled
disputeSummaryWindow.result.buyerGetsTradeAmount=(BTC buyer receives trade amount + his deposit)
disputeSummaryWindow.result.buyerGetsTradeAmountMinusPenalty=(BTC buyer receives trade amount + his deposit - a penalty)
disputeSummaryWindow.result.buyerGetsTradeAmountPlusCompensation=(BTC buyer receives trade amount + his deposit + compensation from seller)
disputeSummaryWindow.result.buyerGetsHisDeposit=(BTC buyer receives his deposit)
disputeSummaryWindow.result.buyerGetsHisDepositPlusPenaltyFromSeller=(BTC buyer receives his deposit + compensation from seller)
disputeSummaryWindow.result.buyerGetsHisDepositMinusPenalty=(BTC buyer receives a penalty on his deposit)
disputeSummaryWindow.result.sellerGetsTradeAmount=(BTC seller receives trade amount + his deposit)
disputeSummaryWindow.result.sellerGetsTradeAmountMinusPenalty=(BTC seller receives trade amount + his deposit - a penalty)
disputeSummaryWindow.result.sellerGetsTradeAmountPlusCompensation=(BTC seller receives trade amount + his deposit + compensation from buyer)
disputeSummaryWindow.result.sellerGetsHisDeposit=(BTC seller receives his deposit)
disputeSummaryWindow.result.sellerGetsHisDepositPlusPenaltyFromBuyer=(BTC seller receives his deposit + compensation from buyer)
disputeSummaryWindow.result.sellerGetsHisDepositMinusPenalty=(BTC seller receives a penalty on his deposit)
disputeSummaryWindow.result.customPayout=(a custom payout)
disputeSummaryWindow.summaryNotes=Summary notes
disputeSummaryWindow.addSummaryNotes=Add summary notes
disputeSummaryWindow.close.button=Close ticket
disputeSummaryWindow.close.button=Apply
# Do no change any line break or order of tokens as the structure is used for signature verification
# suppress inspection "TrailingSpacesInProperty"
@ -2877,7 +2906,11 @@ disputeSummaryWindow.close.msg=Ticket closed on {0}\n\
disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3}
disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\n\
Open trade and accept or reject suggestion from mediator
Go to Open trades to Accept the mediation proposal and wait for your peer's acceptance, if necessary.\n\
Click Reject if you disagree and explain your reasons on the mediation ticket.\n\
If the trade has not been paid out in the next 48h, please inform your mediator on this ticket.\n\
Keep in mind that the arbitrator can only make the payout of the trade amount + 1 security deposit. The other security \
deposit will be left unpaid.
disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\n\
No further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions
disputeSummaryWindow.close.closePeer=You need to close also the trading peers ticket!
@ -3316,6 +3349,7 @@ notification.trade.confirmed=Your trade has at least one blockchain confirmation
notification.trade.paymentStarted=The BTC buyer has started the payment.
notification.trade.selectTrade=Select trade
notification.trade.peerOpenedDispute=Your trading peer has opened a {0}.
notification.trade.disputeResolved=A proposed solution has been issued for the {0}.
notification.trade.disputeClosed=The {0} has been closed.
notification.walletUpdate.headline=Trading wallet update
notification.walletUpdate.msg=Your trading wallet is sufficiently funded.\nAmount: {0}

View File

@ -163,6 +163,7 @@ public abstract class Overlay<T extends Overlay<T>> {
protected boolean hideCloseButton;
protected boolean isDisplayed;
protected boolean disableActionButton;
protected boolean disableTertiaryActionButton;
@Getter
protected BooleanProperty isHiddenProperty = new SimpleBooleanProperty();
@ -176,11 +177,11 @@ public abstract class Overlay<T extends Overlay<T>> {
protected Label headlineIcon, copyIcon, headLineLabel, messageLabel;
protected String headLine, message, closeButtonText, actionButtonText,
secondaryActionButtonText, dontShowAgainId, dontShowAgainText,
secondaryActionButtonText, tertiaryActionButtonText, dontShowAgainId, dontShowAgainText,
truncatedMessage;
private ArrayList<String> messageHyperlinks;
private String headlineStyle;
protected Button actionButton, secondaryActionButton;
protected Button actionButton, secondaryActionButton, tertiaryActionButton;
private HBox buttonBox;
protected AutoTooltipButton closeButton;
@ -189,6 +190,7 @@ public abstract class Overlay<T extends Overlay<T>> {
protected Optional<Runnable> closeHandlerOptional = Optional.<Runnable>empty();
protected Optional<Runnable> actionHandlerOptional = Optional.empty();
protected Optional<Runnable> secondaryActionHandlerOptional = Optional.<Runnable>empty();
protected Optional<Runnable> tertiaryActionHandlerOptional = Optional.<Runnable>empty();
protected ChangeListener<Number> positionListener;
protected Timer centerTime;
@ -301,6 +303,11 @@ public abstract class Overlay<T extends Overlay<T>> {
return cast();
}
public T onTertiaryAction(Runnable actionHandlerOptional) {
this.tertiaryActionHandlerOptional = Optional.of(actionHandlerOptional);
return cast();
}
public T headLine(String headLine) {
this.headLine = headLine;
return cast();
@ -433,6 +440,11 @@ public abstract class Overlay<T extends Overlay<T>> {
return cast();
}
public T tertiaryActionButtonText(String text) {
this.tertiaryActionButtonText = text;
return cast();
}
public T useShutDownButton() {
this.actionButtonText = Res.get("shared.shutDown");
this.actionHandlerOptional = Optional.ofNullable(BisqApp.getShutDownHandler());
@ -489,6 +501,10 @@ public abstract class Overlay<T extends Overlay<T>> {
return cast();
}
public T setTertiaryButtonDisabledState(boolean disableState) {
this.disableTertiaryActionButton = disableState;
return cast();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Protected
@ -1006,6 +1022,17 @@ public abstract class Overlay<T extends Overlay<T>> {
buttonBox.getChildren().add(secondaryActionButton);
}
if (tertiaryActionButtonText != null && tertiaryActionHandlerOptional.isPresent()) {
tertiaryActionButton = new AutoTooltipButton(tertiaryActionButtonText);
tertiaryActionButton.setOnAction(event -> {
hide();
tertiaryActionHandlerOptional.ifPresent(Runnable::run);
});
buttonBox.getChildren().add(tertiaryActionButton);
tertiaryActionButton.setDisable(disableTertiaryActionButton);
}
if (!hideCloseButton)
buttonBox.getChildren().add(closeButton);
} else if (!hideCloseButton) {

View File

@ -265,7 +265,7 @@ public class NotificationCenter {
message = Res.get("notification.trade.peerOpenedDispute", disputeOrTicket);
break;
case MEDIATION_CLOSED:
message = Res.get("notification.trade.disputeClosed", disputeOrTicket);
message = Res.get("notification.trade.disputeResolved", disputeOrTicket);
break;
default:
// if (DevEnv.isDevMode()) {

View File

@ -240,8 +240,9 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
addPayoutAmountTextFields();
addReasonControls();
applyDisputeResultToUiControls();
boolean applyPeersDisputeResult = peersDisputeOptional.isPresent() && peersDisputeOptional.get().isClosed();
boolean applyPeersDisputeResult = peersDisputeOptional.isPresent() && (
peersDisputeOptional.get().getDisputeState() == Dispute.State.RESULT_PROPOSED ||
peersDisputeOptional.get().getDisputeState() == Dispute.State.CLOSED);
if (applyPeersDisputeResult) {
// If the other peers dispute has been closed we apply the result to ourselves
DisputeResult peersDisputeResult = peersDisputeOptional.get().getDisputeResultProperty().get();
@ -697,7 +698,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
}
if (peersDisputeOptional.isPresent() && peersDisputeOptional.get().isClosed()) {
closeTicket(closeTicketButton); // all checks done already on peers ticket
applyDisputeResult(closeTicketButton); // all checks done already on peers ticket
} else {
maybeCheckTransactions().thenAccept(continue1 -> {
if (continue1) {
@ -705,7 +706,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
if (continue2) {
maybeMakePayout().thenAccept(continue3 -> {
if (continue3) {
closeTicket(closeTicketButton);
applyDisputeResult(closeTicketButton);
}
});
}
@ -964,17 +965,16 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
return asyncStatus;
}
private void closeTicket(Button closeTicketButton) {
private void applyDisputeResult(Button closeTicketButton) {
DisputeManager<? extends DisputeList<Dispute>> disputeManager = getDisputeManager(dispute);
if (disputeManager == null) {
return;
}
boolean isRefundAgent = disputeManager instanceof RefundManager;
disputeResult.setLoserPublisher(false); // field no longer used per pazza / leo816
disputeResult.setCloseDate(new Date());
dispute.setDisputeResult(disputeResult);
dispute.setIsClosed();
dispute.setState(isRefundAgent ? Dispute.State.CLOSED : Dispute.State.RESULT_PROPOSED);
DisputeResult.Reason reason = disputeResult.getReason();
summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty());
@ -992,8 +992,10 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
Res.get("disputeSummaryWindow.reason." + reason.name()),
disputeResult.getPayoutSuggestionText(),
amount,
formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()),
formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()),
formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()) +
(isRefundAgent ? "" : " " + disputeResult.getPayoutSuggestionCustomizedToBuyerOrSeller(true)),
formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()) +
(isRefundAgent ? "" : " " + disputeResult.getPayoutSuggestionCustomizedToBuyerOrSeller(false)),
disputeResult.summaryNotesProperty().get()
);
@ -1013,12 +1015,14 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
disputeManager.sendDisputeResultMessage(disputeResult, dispute, summaryText);
if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) {
UserThread.runAfter(() -> new Popup()
.attention(Res.get("disputeSummaryWindow.close.closePeer"))
.show(),
200, TimeUnit.MILLISECONDS);
}
peersDisputeOptional.ifPresent(peersDispute -> {
if (!peersDispute.isResultProposed() && !peersDispute.isClosed()) {
UserThread.runAfter(() -> new Popup()
.attention(Res.get("disputeSummaryWindow.close.closePeer"))
.show(),
200, TimeUnit.MILLISECONDS);
}
});
finalizeDisputeHandlerOptional.ifPresent(Runnable::run);

View File

@ -644,35 +644,43 @@ public abstract class TradeStepView extends AnchorPane {
.headLine(headLine)
.instruction(message)
.actionButtonText(actionButtonText)
.onAction(() -> {
model.dataModel.mediationManager.onAcceptMediationResult(trade,
() -> {
log.info("onAcceptMediationResult completed");
acceptMediationResultPopup = null;
},
errorMessage -> {
UserThread.execute(() -> {
new Popup().error(errorMessage).show();
if (acceptMediationResultPopup != null) {
acceptMediationResultPopup.hide();
acceptMediationResultPopup = null;
}
});
});
})
.secondaryActionButtonText(Res.get("portfolio.pending.mediationResult.popup.openArbitration"))
.onSecondaryAction(() -> {
model.dataModel.mediationManager.rejectMediationResult(trade);
model.dataModel.onOpenDispute();
acceptMediationResultPopup = null;
})
.onClose(() -> {
acceptMediationResultPopup = null;
});
.onAction(this::acceptProposal)
.secondaryActionButtonText(Res.get("portfolio.pending.mediationResult.popup.reject"))
.onSecondaryAction(this::rejectProposal)
.tertiaryActionButtonText(Res.get("portfolio.pending.mediationResult.popup.openArbitration"))
.onTertiaryAction(this::startArbitration)
.setTertiaryButtonDisabledState(remaining > 0)
.onClose(() -> acceptMediationResultPopup = null);
acceptMediationResultPopup.show();
}
private void acceptProposal() {
model.dataModel.mediationManager.onAcceptMediationResult(trade,
() -> {
log.info("onAcceptMediationResult completed");
acceptMediationResultPopup = null;
},
errorMessage -> {
UserThread.execute(() -> {
new Popup().error(errorMessage).show();
if (acceptMediationResultPopup != null) {
acceptMediationResultPopup.hide();
acceptMediationResultPopup = null;
}
});
});
}
private void rejectProposal() {
model.dataModel.mediationManager.rejectMediationResult(trade);
acceptMediationResultPopup = null;
}
private void startArbitration() {
model.dataModel.onOpenDispute();
acceptMediationResultPopup = null;
}
protected String getCurrencyName(Trade trade) {
return CurrencyUtil.getNameByCode(getCurrencyCode(trade));
}

View File

@ -22,6 +22,7 @@ import bisq.desktop.components.AutoTooltipLabel;
import bisq.desktop.components.BisqTextArea;
import bisq.desktop.components.BusyAnimation;
import bisq.desktop.components.TableGroupHeadline;
import bisq.desktop.main.overlays.notifications.Notification;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.GUIUtil;
@ -214,6 +215,10 @@ public class ChatView extends AnchorPane {
Button uploadButton = new AutoTooltipButton(Res.get("support.addAttachments"));
uploadButton.setOnAction(e -> onRequestUpload());
Button clipboardButton = new AutoTooltipButton(Res.get("shared.copyToClipboard"));
clipboardButton.setOnAction(e -> copyChatMessagesToClipboard(clipboardButton));
uploadButton.setStyle("-fx-pref-width: 66; -fx-padding: 3 3 3 3;");
clipboardButton.setStyle("-fx-pref-width: 50; -fx-padding: 3 3 3 3;");
sendMsgInfoLabel = new AutoTooltipLabel();
sendMsgInfoLabel.setVisible(false);
@ -229,7 +234,7 @@ public class ChatView extends AnchorPane {
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
if (allowAttachments)
buttonBox.getChildren().addAll(sendButton, uploadButton, sendMsgBusyAnimation, sendMsgInfoLabel);
buttonBox.getChildren().addAll(sendButton, uploadButton, clipboardButton, sendMsgBusyAnimation, sendMsgInfoLabel);
else
buttonBox.getChildren().addAll(sendButton, sendMsgBusyAnimation, sendMsgInfoLabel);
@ -591,6 +596,24 @@ public class ChatView extends AnchorPane {
}
}
private void copyChatMessagesToClipboard(Button sourceBtn) {
optionalSupportSession.ifPresent(session -> {
StringBuilder stringBuilder = new StringBuilder();
chatMessages.forEach(i -> {
String metaData = DisplayUtils.formatDateTime(new Date(i.getDate()));
metaData = metaData + (i.isSystemMessage() ? " (System message)" :
(i.isSenderIsTrader() ? " (from Trader)" : " (from Agent)"));
stringBuilder.append(metaData).append("\n").append(i.getMessage()).append("\n\n");
});
Utilities.copyToClipboard(stringBuilder.toString());
new Notification()
.notification(Res.get("shared.copiedToClipboard"))
.hideCloseButton()
.autoClose()
.show();
});
}
private void onOpenAttachment(Attachment attachment) {
if (!allowAttachments)
return;

View File

@ -94,8 +94,8 @@ import javafx.geometry.Pos;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
@ -181,7 +181,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
protected FilteredList<Dispute> filteredList;
protected InputTextField filterTextField;
private ChangeListener<String> filterTextFieldListener;
protected AutoTooltipButton sigCheckButton, reOpenButton, closeButton, sendPrivateNotificationButton, reportButton, fullReportButton;
protected AutoTooltipButton sigCheckButton, openOrCloseButton, sendPrivateNotificationButton, reportButton, fullReportButton;
private final Map<String, ListChangeListener<ChatMessage>> disputeChatMessagesListeners = new HashMap<>();
@Nullable
private ListChangeListener<Dispute> disputesListener; // Only set in mediation cases
@ -256,22 +256,17 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
alertIconLabel.setVisible(false);
alertIconLabel.setManaged(false);
reOpenButton = new AutoTooltipButton(Res.get("support.reOpenButton.label"));
reOpenButton.setDisable(true);
reOpenButton.setVisible(false);
reOpenButton.setManaged(false);
HBox.setHgrow(reOpenButton, Priority.NEVER);
reOpenButton.setOnAction(e -> {
reOpenDisputeFromButton();
});
closeButton = new AutoTooltipButton(Res.get("support.closeTicket"));
closeButton.setDisable(true);
closeButton.setVisible(false);
closeButton.setManaged(false);
HBox.setHgrow(closeButton, Priority.NEVER);
closeButton.setOnAction(e -> {
closeDisputeFromButton();
openOrCloseButton = new AutoTooltipButton(Res.get("support.reOpenButton.label"));
openOrCloseButton.setDisable(true);
openOrCloseButton.setVisible(false);
openOrCloseButton.setManaged(false);
HBox.setHgrow(openOrCloseButton, Priority.NEVER);
openOrCloseButton.setOnAction(e -> {
if (openOrCloseButton.getText().equalsIgnoreCase(Res.get("support.closeTicket"))) {
closeDisputeFromButton();
} else {
reOpenDisputeFromButton();
}
});
sendPrivateNotificationButton = new AutoTooltipButton(Res.get("support.sendNotificationButton.label"));
@ -314,8 +309,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
filterTextField,
alertIconLabel,
spacer,
reOpenButton,
closeButton,
openOrCloseButton,
sendPrivateNotificationButton,
reportButton,
fullReportButton,
@ -449,7 +443,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
// For open filter we do not want to continue further as json data would cause a match
if (filter.equalsIgnoreCase("open")) {
return !dispute.isClosed() ? FilterResult.OPEN_DISPUTES : FilterResult.NO_MATCH;
return !dispute.isClosed() || dispute.unreadMessageCount(senderFlag()) > 0 ?
FilterResult.OPEN_DISPUTES : FilterResult.NO_MATCH;
}
if (dispute.getTradeId().toLowerCase().contains(filter)) {
@ -509,18 +504,14 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
}
// a derived version in the ClientView for users pops up an "Are you sure" box first.
// this version includes the sending of an automatic message to the user, see addMediationReOpenedMessage
protected void reOpenDisputeFromButton() {
if (reOpenDispute()) {
disputeManager.addMediationReOpenedMessage(selectedDispute, false);
}
reOpenDispute();
}
// only applicable to traders
// only allow them to close the dispute if the trade is paid out
// traders -> only allow them to close the dispute if the trade is paid out
// the reason for having this is that sometimes traders end up with closed disputes that are not "closed" @pazza
protected void closeDisputeFromButton() {
disputeManager.findTrade(selectedDispute).ifPresent(
disputeManager.findTrade(selectedDispute).ifPresentOrElse(
(trade) -> {
if (trade.isFundsLockedIn()) {
new Popup().warning(Res.get("support.warning.traderCloseOwnDisputeWarning")).show();
@ -529,6 +520,11 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
disputeManager.requestPersistence();
onSelectDispute(selectedDispute);
}
},
() -> {
selectedDispute.setIsClosed();
disputeManager.requestPersistence();
onSelectDispute(selectedDispute);
});
}
@ -542,7 +538,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
isNodeAddressOk(selectedDispute,
!disputeManager.isTrader(selectedDispute))) {
selectedDispute.reOpen();
handleOnProcessDispute(selectedDispute);
disputeManager.requestPersistence();
onSelectDispute(selectedDispute);
return true;
@ -580,9 +575,15 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
selectedDispute = dispute;
}
reOpenButton.setDisable(selectedDispute == null || !selectedDispute.isClosed());
closeButton.setDisable(selectedDispute == null || selectedDispute.isClosed());
sendPrivateNotificationButton.setDisable(selectedDispute == null);
if (selectedDispute == null) {
openOrCloseButton.setDisable(true);
sendPrivateNotificationButton.setDisable(true);
} else {
openOrCloseButton.setDisable(false);
sendPrivateNotificationButton.setDisable(false);
openOrCloseButton.setText(selectedDispute.isClosed() ?
Res.get("support.reOpenButton.label").toUpperCase() : Res.get("support.closeTicket").toUpperCase());
}
}
@ -1140,7 +1141,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
private TableColumn<Dispute, Dispute> getDateColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("shared.date")) {
{
setMinWidth(180);
setMinWidth(100);
setPrefWidth(150);
}
};
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
@ -1166,7 +1168,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
private TableColumn<Dispute, Dispute> getTradeIdColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("shared.tradeId")) {
{
setMinWidth(110);
setMinWidth(50);
setPrefWidth(100);
}
};
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
@ -1303,7 +1306,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
private TableColumn<Dispute, Dispute> getMarketColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("shared.market")) {
{
setMinWidth(80);
setMinWidth(60);
}
};
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
@ -1329,7 +1332,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
private TableColumn<Dispute, Dispute> getRoleColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("support.role")) {
{
setMinWidth(130);
setMinWidth(80);
}
};
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
@ -1390,7 +1393,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
private TableColumn<Dispute, Dispute> getStateColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("support.state")) {
{
setMinWidth(50);
setMinWidth(75);
setPrefWidth(100);
}
};
column.getStyleClass().add("last-column");
@ -1402,8 +1406,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
return new TableCell<>() {
ReadOnlyBooleanProperty closedProperty;
ChangeListener<Boolean> listener;
ReadOnlyStringProperty closedProperty;
ChangeListener<String> listener;
@Override
public void updateItem(final Dispute item, boolean empty) {
@ -1414,16 +1418,16 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
}
listener = (observable, oldValue, newValue) -> {
setText(newValue ? Res.get("support.closed") : Res.get("support.open"));
setText(newValue);
if (getTableRow() != null)
getTableRow().setOpacity(newValue && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1);
getTableRow().setOpacity(newValue.equalsIgnoreCase("CLOSED") && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1);
if (item.isClosed() && item == chatPopup.getSelectedDispute())
chatPopup.closeChat(); // close the chat popup when the associated ticket is closed
};
closedProperty = item.isClosedProperty();
closedProperty = item.getDisputeStateProperty();
closedProperty.addListener(listener);
boolean isClosed = item.isClosed();
setText(isClosed ? Res.get("support.closed") : Res.get("support.open"));
setText(closedProperty.get());
if (getTableRow() != null)
getTableRow().setOpacity(isClosed && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1);
} else {
@ -1496,7 +1500,11 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
@Override
public void onCloseDisputeFromChatWindow(Dispute dispute) {
handleOnProcessDispute(dispute);
if (dispute.getDisputeState() == Dispute.State.NEW || dispute.getDisputeState() == Dispute.State.OPEN) {
handleOnProcessDispute(dispute);
} else {
closeDisputeFromButton();
}
}
@Override

View File

@ -257,7 +257,8 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
private TableColumn<Dispute, Dispute> getAlertColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>("Alert") {
{
setMinWidth(50);
setMinWidth(20);
setPrefWidth(20);
}
};
column.getStyleClass().add("last-column");

View File

@ -85,8 +85,8 @@ public class MediatorView extends DisputeAgentView {
@Override
public void initialize() {
super.initialize();
reOpenButton.setVisible(true);
reOpenButton.setManaged(true);
openOrCloseButton.setVisible(true);
openOrCloseButton.setManaged(true);
setupReOpenDisputeListener();
}

View File

@ -76,10 +76,8 @@ public class MediationClientView extends DisputeClientView {
@Override
public void initialize() {
super.initialize();
reOpenButton.setVisible(true);
reOpenButton.setManaged(true);
closeButton.setVisible(true);
closeButton.setManaged(true);
openOrCloseButton.setVisible(true);
openOrCloseButton.setManaged(true);
setupReOpenDisputeListener();
}

View File

@ -909,6 +909,7 @@ message Dispute {
OPEN = 2;
REOPENED = 3;
CLOSED = 4;
RESULT_PROPOSED = 5;
}
string trade_id = 1;
string id = 2;