From 2cc0e4134bc37e7210c1ea67ee2340ce9b48629a Mon Sep 17 00:00:00 2001 From: sqrrm Date: Wed, 26 Aug 2020 13:40:49 +0200 Subject: [PATCH 1/4] Reload dispute layout on reopen When a dispute is reopened it now shows the chat text entry field. --- .../main/java/bisq/desktop/main/support/dispute/DisputeView.java | 1 + 1 file changed, 1 insertion(+) diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java index 88bafcd3cb2..35edd7cef63 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java @@ -235,6 +235,7 @@ public void initialize() { if (selectedDisputeClosedPropertyListener != null) selectedDispute.isClosedProperty().removeListener(selectedDisputeClosedPropertyListener); selectedDispute.setIsClosed(false); + handleOnSelectDispute(selectedDispute); } } else if (Utilities.isAltOrCtrlPressed(KeyCode.R, event)) { if (selectedDispute != null) { From fa9f7991865e8a067cf34277356178debbff49e6 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 28 Aug 2020 13:48:28 -0500 Subject: [PATCH 2/4] Improve dispute views Add report button: Generates report text of all disputes with more detailed information which should be used for reporting. Add "Dump text of all disputes" button for getting a text representation of all disputes including the messages. This is not used for report but might be useful for search, etc. Do not show legacy arbitration tabs (for traders as well for arbitrators) if no cases available. Add 3 new enum entries for Reason for dispute: OPTION_TRADE SELLER_NOT_RESPONDING WRONG_SENDER_ACCOUNT Hide NO_REPLY and PROTOCOL_VIOLATION in UI (still in code if we still need it and re-activate them) --- .../core/support/dispute/DisputeResult.java | 9 +- .../resources/i18n/displayStrings.properties | 5 + .../windows/DisputeSummaryWindow.java | 62 +++++++-- .../desktop/main/support/SupportView.fxml | 6 - .../desktop/main/support/SupportView.java | 61 ++++++--- .../main/support/dispute/DisputeView.java | 119 ++++++++++++------ proto/src/main/proto/pb.proto | 3 + 7 files changed, 188 insertions(+), 77 deletions(-) diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeResult.java b/core/src/main/java/bisq/core/support/dispute/DisputeResult.java index db11d29ef03..268ed0ded02 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeResult.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeResult.java @@ -57,9 +57,12 @@ public enum Reason { BUG, USABILITY, SCAM, - PROTOCOL_VIOLATION, - NO_REPLY, - BANK_PROBLEMS + PROTOCOL_VIOLATION, // Not used anymore + NO_REPLY, // Not used anymore + BANK_PROBLEMS, + OPTION_TRADE, + SELLER_NOT_RESPONDING, + WRONG_SENDER_ACCOUNT } private final String tradeId; diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 8b9d6fb6f43..038facc0f36 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -967,6 +967,8 @@ support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets support.filter=Filter list support.filter.prompt=Enter trade ID, date, onion address or account data +support.reportButton.label=Generate report +support.fullReportButton.label=Get text dump of all disputes support.noTickets=There are no open tickets support.sendingMessage=Sending Message... support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. @@ -2376,6 +2378,9 @@ disputeSummaryWindow.reason.noReply=No reply disputeSummaryWindow.reason.scam=Scam disputeSummaryWindow.reason.other=Other disputeSummaryWindow.reason.bank=Bank +disputeSummaryWindow.reason.optionTrade=Option trade +disputeSummaryWindow.reason.sellerNotResponding=Seller not responding +disputeSummaryWindow.reason.wrongSenderAccount=Wrong sender account disputeSummaryWindow.summaryNotes=Summary notes disputeSummaryWindow.addSummaryNotes=Add summary notes disputeSummaryWindow.close.button=Close ticket diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index 03374fd3938..d680c1a2efc 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -113,7 +113,9 @@ public class DisputeSummaryWindow extends Overlay { buyerGetsAllRadioButton, sellerGetsAllRadioButton, customRadioButton; private RadioButton reasonWasBugRadioButton, reasonWasUsabilityIssueRadioButton, reasonProtocolViolationRadioButton, reasonNoReplyRadioButton, reasonWasScamRadioButton, - reasonWasOtherRadioButton, reasonWasBankRadioButton; + reasonWasOtherRadioButton, reasonWasBankRadioButton, reasonWasOptionTradeRadioButton, + reasonWasSellerNotRespondingRadioButton, reasonWasWrongSenderAccountRadioButton; + // Dispute object of other trade peer. The dispute field is the one from which we opened the close dispute window. private Optional peersDisputeOptional; private String role; @@ -155,7 +157,7 @@ public void show(Dispute dispute) { this.dispute = dispute; rowIndex = -1; - width = 700; + width = 1150; createGridPane(); addContent(); display(); @@ -209,7 +211,7 @@ protected void createGridPane() { gridPane.setPadding(new Insets(35, 40, 30, 40)); gridPane.getStyleClass().add("grid-pane"); gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT); - gridPane.setPrefWidth(900); + gridPane.setPrefWidth(width); } private void addContent() { @@ -258,6 +260,9 @@ private void addContent() { reasonWasScamRadioButton.setDisable(true); reasonWasOtherRadioButton.setDisable(true); reasonWasBankRadioButton.setDisable(true); + reasonWasOptionTradeRadioButton.setDisable(true); + reasonWasSellerNotRespondingRadioButton.setDisable(true); + reasonWasWrongSenderAccountRadioButton.setDisable(true); isLoserPublisherCheckBox.setDisable(true); isLoserPublisherCheckBox.setSelected(peersDisputeResult.isLoserPublisher()); @@ -485,12 +490,24 @@ private void addReasonControls() { reasonWasScamRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.scam")); reasonWasBankRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.bank")); reasonWasOtherRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.other")); + reasonWasOptionTradeRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.optionTrade")); + reasonWasSellerNotRespondingRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.sellerNotResponding")); + reasonWasWrongSenderAccountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.wrongSenderAccount")); HBox feeRadioButtonPane = new HBox(); feeRadioButtonPane.setSpacing(20); - feeRadioButtonPane.getChildren().addAll(reasonWasBugRadioButton, reasonWasUsabilityIssueRadioButton, - reasonProtocolViolationRadioButton, reasonNoReplyRadioButton, - reasonWasBankRadioButton, reasonWasScamRadioButton, reasonWasOtherRadioButton); + // We don't show no reply and protocol violation as those should be covered by more specific ones. We still leave + // the code to enable it if it turns out it is still requested by mediators. + feeRadioButtonPane.getChildren().addAll( + reasonWasOptionTradeRadioButton, + reasonWasSellerNotRespondingRadioButton, + reasonWasWrongSenderAccountRadioButton, + reasonWasBugRadioButton, + reasonWasUsabilityIssueRadioButton, + reasonWasBankRadioButton, + reasonWasScamRadioButton, + reasonWasOtherRadioButton + ); VBox vBox = addTopLabelWithVBox(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.reason"), @@ -505,22 +522,32 @@ private void addReasonControls() { reasonWasScamRadioButton.setToggleGroup(reasonToggleGroup); reasonWasOtherRadioButton.setToggleGroup(reasonToggleGroup); reasonWasBankRadioButton.setToggleGroup(reasonToggleGroup); + reasonWasOptionTradeRadioButton.setToggleGroup(reasonToggleGroup); + reasonWasSellerNotRespondingRadioButton.setToggleGroup(reasonToggleGroup); + reasonWasWrongSenderAccountRadioButton.setToggleGroup(reasonToggleGroup); reasonToggleSelectionListener = (observable, oldValue, newValue) -> { - if (newValue == reasonWasBugRadioButton) + if (newValue == reasonWasBugRadioButton) { disputeResult.setReason(DisputeResult.Reason.BUG); - else if (newValue == reasonWasUsabilityIssueRadioButton) + } else if (newValue == reasonWasUsabilityIssueRadioButton) { disputeResult.setReason(DisputeResult.Reason.USABILITY); - else if (newValue == reasonProtocolViolationRadioButton) + } else if (newValue == reasonProtocolViolationRadioButton) { disputeResult.setReason(DisputeResult.Reason.PROTOCOL_VIOLATION); - else if (newValue == reasonNoReplyRadioButton) + } else if (newValue == reasonNoReplyRadioButton) { disputeResult.setReason(DisputeResult.Reason.NO_REPLY); - else if (newValue == reasonWasScamRadioButton) + } else if (newValue == reasonWasScamRadioButton) { disputeResult.setReason(DisputeResult.Reason.SCAM); - else if (newValue == reasonWasBankRadioButton) + } else if (newValue == reasonWasBankRadioButton) { disputeResult.setReason(DisputeResult.Reason.BANK_PROBLEMS); - else if (newValue == reasonWasOtherRadioButton) + } else if (newValue == reasonWasOtherRadioButton) { disputeResult.setReason(DisputeResult.Reason.OTHER); + } else if (newValue == reasonWasOptionTradeRadioButton) { + disputeResult.setReason(DisputeResult.Reason.OPTION_TRADE); + } else if (newValue == reasonWasSellerNotRespondingRadioButton) { + disputeResult.setReason(DisputeResult.Reason.SELLER_NOT_RESPONDING); + } else if (newValue == reasonWasWrongSenderAccountRadioButton) { + disputeResult.setReason(DisputeResult.Reason.WRONG_SENDER_ACCOUNT); + } }; reasonToggleGroup.selectedToggleProperty().addListener(reasonToggleSelectionListener); } @@ -549,6 +576,15 @@ private void setReasonRadioButtonState() { case OTHER: reasonToggleGroup.selectToggle(reasonWasOtherRadioButton); break; + case OPTION_TRADE: + reasonToggleGroup.selectToggle(reasonWasOptionTradeRadioButton); + break; + case SELLER_NOT_RESPONDING: + reasonToggleGroup.selectToggle(reasonWasSellerNotRespondingRadioButton); + break; + case WRONG_SENDER_ACCOUNT: + reasonToggleGroup.selectToggle(reasonWasWrongSenderAccountRadioButton); + break; } } } diff --git a/desktop/src/main/java/bisq/desktop/main/support/SupportView.fxml b/desktop/src/main/java/bisq/desktop/main/support/SupportView.fxml index b26ee6db59a..1492cb1a093 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/SupportView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/support/SupportView.fxml @@ -18,15 +18,9 @@ --> - - - - - - diff --git a/desktop/src/main/java/bisq/desktop/main/support/SupportView.java b/desktop/src/main/java/bisq/desktop/main/support/SupportView.java index 56e2859283b..078bbaede39 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/SupportView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/SupportView.java @@ -51,8 +51,6 @@ import javax.inject.Inject; -import javafx.fxml.FXML; - import javafx.scene.control.Tab; import javafx.scene.control.TabPane; @@ -60,13 +58,17 @@ import javafx.collections.MapChangeListener; +import javax.annotation.Nullable; + @FxmlView public class SupportView extends ActivatableView { - @FXML - Tab tradersMediationDisputesTab, tradersRefundDisputesTab, tradersArbitrationDisputesTab; - - private Tab arbitratorTab, mediatorTab, refundAgentTab; + private Tab tradersMediationDisputesTab, tradersRefundDisputesTab; + @Nullable + private Tab tradersArbitrationDisputesTab; + private Tab mediatorTab, refundAgentTab; + @Nullable + private Tab arbitratorTab; private final Navigation navigation; private final ArbitratorManager arbitratorManager; @@ -108,12 +110,29 @@ public SupportView(CachingViewLoader viewLoader, @Override public void initialize() { - // has to be called before loadView + tradersMediationDisputesTab = new Tab(); + tradersMediationDisputesTab.setClosable(false); + root.getTabs().add(tradersMediationDisputesTab); + + tradersRefundDisputesTab = new Tab(); + tradersRefundDisputesTab.setClosable(false); + root.getTabs().add(tradersRefundDisputesTab); + + // We only show tradersArbitrationDisputesTab if we have cases + if (!arbitrationManager.getDisputesAsObservableList().isEmpty()) { + tradersArbitrationDisputesTab = new Tab(); + tradersArbitrationDisputesTab.setClosable(false); + root.getTabs().add(tradersArbitrationDisputesTab); + } + + // Has to be called before loadView updateAgentTabs(); tradersMediationDisputesTab.setText(Res.get("support.tab.mediation.support").toUpperCase()); tradersRefundDisputesTab.setText(Res.get("support.tab.arbitration.support").toUpperCase()); - tradersArbitrationDisputesTab.setText(Res.get("support.tab.legacyArbitration.support").toUpperCase()); + if (tradersArbitrationDisputesTab != null) { + tradersArbitrationDisputesTab.setText(Res.get("support.tab.legacyArbitration.support").toUpperCase()); + } navigationListener = viewPath -> { if (viewPath.size() == 3 && viewPath.indexOf(SupportView.class) == 1) loadView(viewPath.tip()); @@ -142,16 +161,19 @@ else if (newValue == refundAgentTab) private void updateAgentTabs() { PubKeyRing myPubKeyRing = keyRing.getPubKeyRing(); - boolean isActiveArbitrator = arbitratorManager.getObservableMap().values().stream() - .anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(myPubKeyRing)); - if (arbitratorTab == null) { - // In case a arbitrator has become inactive he still might get disputes from pending trades - boolean hasDisputesAsArbitrator = arbitrationManager.getDisputesAsObservableList().stream() - .anyMatch(d -> d.getAgentPubKeyRing().equals(myPubKeyRing)); - if (isActiveArbitrator || hasDisputesAsArbitrator) { - arbitratorTab = new Tab(); - arbitratorTab.setClosable(false); - root.getTabs().add(arbitratorTab); + boolean hasArbitrationCases = !arbitrationManager.getDisputesAsObservableList().isEmpty(); + if (hasArbitrationCases) { + boolean isActiveArbitrator = arbitratorManager.getObservableMap().values().stream() + .anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(myPubKeyRing)); + if (arbitratorTab == null) { + // In case a arbitrator has become inactive he still might get disputes from pending trades + boolean hasDisputesAsArbitrator = arbitrationManager.getDisputesAsObservableList().stream() + .anyMatch(d -> d.getAgentPubKeyRing().equals(myPubKeyRing)); + if (isActiveArbitrator || hasDisputesAsArbitrator) { + arbitratorTab = new Tab(); + arbitratorTab.setClosable(false); + root.getTabs().add(arbitratorTab); + } } } @@ -224,11 +246,12 @@ protected void activate() { } String key = "supportInfo"; - if (!DevEnv.isDevMode()) + if (!DevEnv.isDevMode()) { new Popup().backgroundInfo(Res.get("support.backgroundInfo")) .width(900) .dontShowAgainId(key) .show(); + } } @Override diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java index 88bafcd3cb2..5d56b5273bb 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java @@ -55,6 +55,8 @@ import bisq.common.crypto.PubKeyRing; import bisq.common.util.Utilities; +import org.bitcoinj.core.Coin; + import com.google.common.collect.Lists; import javafx.scene.Scene; @@ -67,6 +69,7 @@ import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; @@ -163,13 +166,31 @@ public DisputeView(DisputeManager> public void initialize() { Label label = new AutoTooltipLabel(Res.get("support.filter")); HBox.setMargin(label, new Insets(5, 0, 0, 0)); + HBox.setHgrow(label, Priority.NEVER); + filterTextField = new InputTextField(); filterTextField.setText("open"); filterTextFieldListener = (observable, oldValue, newValue) -> applyFilteredListPredicate(filterTextField.getText()); + HBox.setHgrow(filterTextField, Priority.NEVER); + + Button reportButton = new AutoTooltipButton(Res.get("support.reportButton.label")); + HBox.setHgrow(reportButton, Priority.NEVER); + reportButton.setOnAction(e -> { + showCompactReport(); + }); + + Button fullReportButton = new AutoTooltipButton(Res.get("support.fullReportButton.label")); + HBox.setHgrow(fullReportButton, Priority.NEVER); + fullReportButton.setOnAction(e -> { + showFullReport(); + }); + + Pane spacer = new Pane(); + HBox.setHgrow(spacer, Priority.ALWAYS); filterBox = new HBox(); filterBox.setSpacing(5); - filterBox.getChildren().addAll(label, filterTextField); + filterBox.getChildren().addAll(label, filterTextField, spacer, reportButton, fullReportButton); VBox.setVgrow(filterBox, Priority.NEVER); filterBox.setVisible(false); filterBox.setManaged(false); @@ -225,11 +246,7 @@ public void initialize() { selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> chatView.setInputBoxVisible(!newValue); keyEventEventHandler = event -> { - if (Utilities.isAltOrCtrlPressed(KeyCode.L, event)) { - showFullReport(); - } else if (Utilities.isAltOrCtrlPressed(KeyCode.K, event)) { - showCompactReport(); - } else if (Utilities.isAltOrCtrlPressed(KeyCode.U, event)) { + if (Utilities.isAltOrCtrlPressed(KeyCode.U, event)) { // Hidden shortcut to re-open a dispute. Allow it also for traders not only arbitrator. if (selectedDispute != null) { if (selectedDisputeClosedPropertyListener != null) @@ -274,15 +291,24 @@ private void showCompactReport() { list.add(dispute); }); - List> disputeGroups = new ArrayList<>(); - map.forEach((key, value) -> disputeGroups.add(value)); - disputeGroups.sort(Comparator.comparing(o -> !o.isEmpty() ? o.get(0).getOpeningDate() : new Date(0))); + List> allDisputes = new ArrayList<>(); + map.forEach((key, value) -> allDisputes.add(value)); + allDisputes.sort(Comparator.comparing(o -> !o.isEmpty() ? o.get(0).getOpeningDate() : new Date(0))); StringBuilder stringBuilder = new StringBuilder(); AtomicInteger disputeIndex = new AtomicInteger(); - disputeGroups.forEach(disputeGroup -> { - if (disputeGroup.size() > 0) { - Dispute dispute0 = disputeGroup.get(0); - Date openingDate = dispute0.getOpeningDate(); + allDisputes.forEach(disputesPerTrade -> { + if (disputesPerTrade.size() > 0) { + Dispute firstDispute = disputesPerTrade.get(0); + Date openingDate = firstDispute.getOpeningDate(); + Contract contract = firstDispute.getContract(); + String buyersRole = contract.isBuyerMakerAndSellerTaker() ? "Buyer as maker" : "Buyer as taker"; + String sellersRole = contract.isBuyerMakerAndSellerTaker() ? "Seller as taker" : "Seller as maker"; + String opener = firstDispute.isDisputeOpenerIsBuyer() ? buyersRole : sellersRole; + DisputeResult disputeResult = firstDispute.getDisputeResultProperty().get(); + String winner = disputeResult != null && + disputeResult.getWinner() == DisputeResult.Winner.BUYER ? "Buyer" : "Seller"; + String buyerPayoutAmount = disputeResult != null ? disputeResult.getBuyerPayoutAmount().toFriendlyString() : ""; + String sellerPayoutAmount = disputeResult != null ? disputeResult.getSellerPayoutAmount().toFriendlyString() : ""; stringBuilder.append("\n") .append("Dispute nr. ") .append(disputeIndex.incrementAndGet()) @@ -290,45 +316,67 @@ private void showCompactReport() { .append("Opening date: ") .append(DisplayUtils.formatDateTime(openingDate)) .append("\n"); - DisputeResult disputeResult0 = dispute0.getDisputeResultProperty().get(); String summaryNotes0 = ""; - if (disputeResult0 != null) { - Date closeDate = disputeResult0.getCloseDate(); + if (disputeResult != null) { + Date closeDate = disputeResult.getCloseDate(); long duration = closeDate.getTime() - openingDate.getTime(); stringBuilder.append("Close date: ") .append(DisplayUtils.formatDateTime(closeDate)) .append("\n") - .append("Duration: ") + .append("Dispute duration: ") .append(FormattingUtils.formatDurationAsWords(duration)) .append("\n"); + } - summaryNotes0 = disputeResult0.getSummaryNotesProperty().get(); + stringBuilder.append("Payment method: ") + .append(Res.get(contract.getPaymentMethodId())) + .append("\n") + .append("Currency: ") + .append(CurrencyUtil.getNameAndCode(contract.getOfferPayload().getCurrencyCode())) + .append("\n") + .append("Trade amount: ") + .append(contract.getTradeAmount().toFriendlyString()) + .append("\n") + .append("Buyer/seller security deposit: ") + .append(Coin.valueOf(contract.getOfferPayload().getBuyerSecurityDeposit()).toFriendlyString()) + .append("/") + .append(Coin.valueOf(contract.getOfferPayload().getSellerSecurityDeposit()).toFriendlyString()) + .append("\n") + .append("Dispute opened by: ") + .append(opener) + .append("\n") + .append("Payout to buyer/seller (winner): ") + .append(buyerPayoutAmount).append("/") + .append(sellerPayoutAmount).append(" (") + .append(winner) + .append(")\n"); + + if (disputeResult != null) { + DisputeResult.Reason reason = disputeResult.getReason(); + if (firstDispute.disputeResultProperty().get().getReason() != null) { + disputesByReason.putIfAbsent(reason.name(), new ArrayList<>()); + disputesByReason.get(reason.name()).add(firstDispute); + stringBuilder.append("Reason: ") + .append(reason.name()) + .append("\n"); + } + + summaryNotes0 = disputeResult.getSummaryNotesProperty().get(); stringBuilder.append("Summary notes: ").append(summaryNotes0).append("\n"); } // We might have a different summary notes at second trader. Only if it // is different we show it. - if (disputeGroup.size() > 1) { - Dispute dispute1 = disputeGroup.get(1); + if (disputesPerTrade.size() > 1) { + Dispute dispute1 = disputesPerTrade.get(1); DisputeResult disputeResult1 = dispute1.getDisputeResultProperty().get(); if (disputeResult1 != null) { String summaryNotes1 = disputeResult1.getSummaryNotesProperty().get(); if (!summaryNotes1.equals(summaryNotes0)) { - stringBuilder.append("Summary notes (trader 2): ").append(summaryNotes1).append("\n"); + stringBuilder.append("Summary notes (different message to other trader was used): ").append(summaryNotes1).append("\n"); } } } - - if (dispute0.disputeResultProperty().get() != null) { - DisputeResult.Reason reason = dispute0.disputeResultProperty().get().getReason(); - if (dispute0.disputeResultProperty().get().getReason() != null) { - disputesByReason.putIfAbsent(reason.name(), new ArrayList<>()); - disputesByReason.get(reason.name()).add(dispute0); - stringBuilder.append("Reason: ") - .append(reason.name()) - .append("\n"); - } - } } }); stringBuilder.append("\n").append("Summary of reasons for disputes: ").append("\n"); @@ -337,7 +385,7 @@ private void showCompactReport() { }); String message = stringBuilder.toString(); - new Popup().headLine("Compact summary of all disputes (" + disputeGroups.size() + ")") + new Popup().headLine("Report for " + allDisputes.size() + " disputes") .maxMessageLength(500) .information(message) .width(1200) @@ -364,7 +412,6 @@ private void showFullReport() { StringBuilder stringBuilder = new StringBuilder(); // We don't translate that as it is not intended for the public - stringBuilder.append("Summary of all disputes (No. of disputes: ").append(disputeGroups.size()).append(")\n\n"); disputeGroups.forEach(disputeGroup -> { Dispute dispute0 = disputeGroup.get(0); stringBuilder.append("##########################################################################################/\n") @@ -403,11 +450,11 @@ private void showFullReport() { }); String message = stringBuilder.toString(); // We don't translate that as it is not intended for the public - new Popup().headLine("All disputes (" + disputeGroups.size() + ")") + new Popup().headLine("Detailed text dump for " + disputeGroups.size() + " disputes") .maxMessageLength(1000) .information(message) .width(1200) - .actionButtonText("Copy") + .actionButtonText("Copy to clipboard") .onAction(() -> Utilities.copyToClipboard(message)) .show(); } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 117dbb4cb53..6b3e068f3da 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -810,6 +810,9 @@ message DisputeResult { PROTOCOL_VIOLATION = 5; NO_REPLY = 6; BANK_PROBLEMS = 7; + OPTION_TRADE = 8; + SELLER_NOT_RESPONDING = 9; + WRONG_SENDER_ACCOUNT = 10; } string trade_id = 1; From f52beadc476f2cb58ff9b4b4a051156cb70c3b7f Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 28 Aug 2020 22:15:11 -0500 Subject: [PATCH 3/4] Improve dispute views Add re-open dispute button to mediation views (not refund agent as there it might be dangerous if he would close 2 times and pay out twice. If a dispute was reopened and a message was sent afterwards the receiver will auto-reopen the dispute as well and the UI shows the number indicator. Add search field to traders dispute views. Fix DisputeMsgEvents which was handling only legacy arbitration cases and now uses mediation and refund agent. Used to update state in case the UI is not selected. Add "send private notification" button for mediators and refund agents. Add report button for mediators and refund agents Generates report text of all disputes with more detailed information which should be used for reporting. Add "Dump text of all disputes" button for getting a text representation of all disputes including the messages for mediators and refund agents. This is not used for report but might be useful for search, etc. Do not show legacy arbitration tabs (for traders as well for arbitrators) if no cases available. Add 5 new enum entries for Reason for dispute: OPTION_TRADE SELLER_NOT_RESPONDING WRONG_SENDER_ACCOUNT, TRADE_ALREADY_SETTLED, PEER_WAS_LATE Hide NO_REPLY, SCAM and PROTOCOL_VIOLATION in UI (still in code if we still need it and re-activate them). --- .../alerts/DisputeMsgEvents.java | 48 +- .../core/support/dispute/DisputeManager.java | 2 +- .../core/support/dispute/DisputeResult.java | 8 +- .../dispute/mediation/MediationManager.java | 5 - .../core/support/messages/ChatMessage.java | 13 +- .../resources/i18n/displayStrings.properties | 8 +- .../windows/DisputeSummaryWindow.java | 22 +- .../SendPrivateNotificationWindow.java | 36 +- .../bisq/desktop/main/shared/ChatView.java | 8 +- .../main/support/dispute/DisputeView.java | 563 +++++++++++------- .../dispute/agent/DisputeAgentView.java | 35 +- .../dispute/agent/mediation/MediatorView.java | 27 +- .../dispute/client/DisputeClientView.java | 13 +- .../client/mediation/MediationClientView.java | 33 +- proto/src/main/proto/pb.proto | 2 + 15 files changed, 547 insertions(+), 276 deletions(-) diff --git a/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java b/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java index 6559d27c922..1c2b9390ffb 100644 --- a/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java +++ b/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java @@ -22,7 +22,8 @@ import bisq.core.notifications.MobileMessageType; import bisq.core.notifications.MobileNotificationService; import bisq.core.support.dispute.Dispute; -import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.refund.RefundManager; import bisq.core.support.messages.ChatMessage; import bisq.network.p2p.P2PService; @@ -31,6 +32,7 @@ import javax.inject.Singleton; import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; import java.util.UUID; @@ -39,28 +41,38 @@ @Slf4j @Singleton public class DisputeMsgEvents { + private final RefundManager refundManager; + private final MediationManager mediationManager; private final P2PService p2PService; private final MobileNotificationService mobileNotificationService; @Inject - public DisputeMsgEvents(ArbitrationManager arbitrationManager, + public DisputeMsgEvents(RefundManager refundManager, + MediationManager mediationManager, P2PService p2PService, MobileNotificationService mobileNotificationService) { + this.refundManager = refundManager; + this.mediationManager = mediationManager; this.p2PService = p2PService; this.mobileNotificationService = mobileNotificationService; + } - // We need to handle it here in the constructor otherwise we get repeated the messages sent. - arbitrationManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> { + public void onAllServicesInitialized() { + refundManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> { c.next(); if (c.wasAdded()) { c.getAddedSubList().forEach(this::setDisputeListener); } }); - arbitrationManager.getDisputesAsObservableList().forEach(this::setDisputeListener); - } + refundManager.getDisputesAsObservableList().forEach(this::setDisputeListener); - // We ignore that onAllServicesInitialized here - public void onAllServicesInitialized() { + mediationManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> { + c.next(); + if (c.wasAdded()) { + c.getAddedSubList().forEach(this::setDisputeListener); + } + }); + mediationManager.getDisputesAsObservableList().forEach(this::setDisputeListener); } private void setDisputeListener(Dispute dispute) { @@ -70,24 +82,24 @@ private void setDisputeListener(Dispute dispute) { log.debug("We got a ChatMessage added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId()); c.next(); if (c.wasAdded()) { - c.getAddedSubList().forEach(this::setChatMessage); + c.getAddedSubList().forEach(chatMessage -> onChatMessage(chatMessage, dispute)); } }); //TODO test if (!dispute.getChatMessages().isEmpty()) - setChatMessage(dispute.getChatMessages().get(0)); + onChatMessage(dispute.getChatMessages().get(0), dispute); } - private void setChatMessage(ChatMessage disputeMsg) { + private void onChatMessage(ChatMessage chatMessage, Dispute dispute) { // TODO we need to prevent to send msg for old dispute messages again at restart // Maybe we need a new property in ChatMessage // As key is not set in initial iterations it seems we don't need an extra handling. // the mailbox msg is set a bit later so that triggers a notification, but not the old messages. // We only send msg in case we are not the sender - if (!disputeMsg.getSenderNodeAddress().equals(p2PService.getAddress())) { - String shortId = disputeMsg.getShortId(); + if (!chatMessage.getSenderNodeAddress().equals(p2PService.getAddress())) { + String shortId = chatMessage.getShortId(); MobileMessage message = new MobileMessage(Res.get("account.notifications.dispute.message.title"), Res.get("account.notifications.dispute.message.msg", shortId), shortId, @@ -99,6 +111,16 @@ private void setChatMessage(ChatMessage disputeMsg) { 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. + ObservableList chatMessages = dispute.getChatMessages(); + // 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() && !chatMessages.isEmpty() && !chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute)) { + dispute.setIsClosed(false); + } } public static MobileMessage getTestMsg() { diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index 6a4036a18aa..4e13adb1fac 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -603,8 +603,8 @@ public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute disput text, p2PService.getAddress()); - dispute.addAndPersistChatMessage(chatMessage); disputeResult.setChatMessage(chatMessage); + dispute.addAndPersistChatMessage(chatMessage); NodeAddress peersNodeAddress; Contract contract = dispute.getContract(); diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeResult.java b/core/src/main/java/bisq/core/support/dispute/DisputeResult.java index 268ed0ded02..d046a16c982 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeResult.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeResult.java @@ -56,13 +56,15 @@ public enum Reason { OTHER, BUG, USABILITY, - SCAM, + SCAM, // Not used anymore PROTOCOL_VIOLATION, // Not used anymore - NO_REPLY, // Not used anymore + NO_REPLY, // Not used anymore BANK_PROBLEMS, OPTION_TRADE, SELLER_NOT_RESPONDING, - WRONG_SENDER_ACCOUNT + WRONG_SENDER_ACCOUNT, + TRADE_ALREADY_SETTLED, + PEER_WAS_LATE } private final String tradeId; diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java index ea6b12bd997..44c5a309d7f 100644 --- a/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java @@ -188,11 +188,6 @@ public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { } dispute.setIsClosed(true); - if (dispute.disputeResultProperty().get() != null) { - log.warn("We got already a dispute result. That should only happen if a dispute needs to be closed " + - "again because the first close did not succeed. TradeId = " + tradeId); - } - dispute.setDisputeResult(disputeResult); Optional tradeOptional = tradeManager.getTradeById(tradeId); diff --git a/core/src/main/java/bisq/core/support/messages/ChatMessage.java b/core/src/main/java/bisq/core/support/messages/ChatMessage.java index 51c4f2a06d5..3b38c8a244b 100644 --- a/core/src/main/java/bisq/core/support/messages/ChatMessage.java +++ b/core/src/main/java/bisq/core/support/messages/ChatMessage.java @@ -19,6 +19,8 @@ import bisq.core.support.SupportType; import bisq.core.support.dispute.Attachment; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeResult; import bisq.network.p2p.NodeAddress; @@ -59,7 +61,6 @@ @Getter @Slf4j public final class ChatMessage extends SupportMessage { - public interface Listener { void onMessageStateChanged(); } @@ -328,6 +329,16 @@ public void addWeakMessageStateListener(Listener listener) { this.listener = new WeakReference<>(listener); } + public boolean isResultMessage(Dispute dispute) { + DisputeResult disputeResult = dispute.getDisputeResultProperty().get(); + if (disputeResult == null) { + return false; + } + + ChatMessage resultChatMessage = disputeResult.getChatMessage(); + return resultChatMessage != null && resultChatMessage.getUid().equals(uid); + } + private void notifyChangeListener() { if (listener != null) { Listener listener = this.listener.get(); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 038facc0f36..dcdf17d0d98 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -965,8 +965,11 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -support.filter=Filter list +support.filter=Search disputes support.filter.prompt=Enter trade ID, date, onion address or account data +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open dispute +support.sendNotificationButton.label=Send private notification support.reportButton.label=Generate report support.fullReportButton.label=Get text dump of all disputes support.noTickets=There are no open tickets @@ -2381,6 +2384,9 @@ disputeSummaryWindow.reason.bank=Bank disputeSummaryWindow.reason.optionTrade=Option trade disputeSummaryWindow.reason.sellerNotResponding=Seller not responding disputeSummaryWindow.reason.wrongSenderAccount=Wrong sender account +disputeSummaryWindow.reason.peerWasLate=Peer was late +disputeSummaryWindow.reason.tradeAlreadySettled=Trade already settled + disputeSummaryWindow.summaryNotes=Summary notes disputeSummaryWindow.addSummaryNotes=Add summary notes disputeSummaryWindow.close.button=Close ticket diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index d680c1a2efc..b55aa07e081 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -114,7 +114,8 @@ public class DisputeSummaryWindow extends Overlay { private RadioButton reasonWasBugRadioButton, reasonWasUsabilityIssueRadioButton, reasonProtocolViolationRadioButton, reasonNoReplyRadioButton, reasonWasScamRadioButton, reasonWasOtherRadioButton, reasonWasBankRadioButton, reasonWasOptionTradeRadioButton, - reasonWasSellerNotRespondingRadioButton, reasonWasWrongSenderAccountRadioButton; + reasonWasSellerNotRespondingRadioButton, reasonWasWrongSenderAccountRadioButton, + reasonWasPeerWasLateRadioButton, reasonWasTradeAlreadySettledRadioButton; // Dispute object of other trade peer. The dispute field is the one from which we opened the close dispute window. private Optional peersDisputeOptional; @@ -263,6 +264,8 @@ private void addContent() { reasonWasOptionTradeRadioButton.setDisable(true); reasonWasSellerNotRespondingRadioButton.setDisable(true); reasonWasWrongSenderAccountRadioButton.setDisable(true); + reasonWasPeerWasLateRadioButton.setDisable(true); + reasonWasTradeAlreadySettledRadioButton.setDisable(true); isLoserPublisherCheckBox.setDisable(true); isLoserPublisherCheckBox.setSelected(peersDisputeResult.isLoserPublisher()); @@ -493,19 +496,22 @@ private void addReasonControls() { reasonWasOptionTradeRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.optionTrade")); reasonWasSellerNotRespondingRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.sellerNotResponding")); reasonWasWrongSenderAccountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.wrongSenderAccount")); + reasonWasPeerWasLateRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.peerWasLate")); + reasonWasTradeAlreadySettledRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.tradeAlreadySettled")); HBox feeRadioButtonPane = new HBox(); feeRadioButtonPane.setSpacing(20); // We don't show no reply and protocol violation as those should be covered by more specific ones. We still leave // the code to enable it if it turns out it is still requested by mediators. feeRadioButtonPane.getChildren().addAll( + reasonWasTradeAlreadySettledRadioButton, + reasonWasPeerWasLateRadioButton, reasonWasOptionTradeRadioButton, reasonWasSellerNotRespondingRadioButton, reasonWasWrongSenderAccountRadioButton, reasonWasBugRadioButton, reasonWasUsabilityIssueRadioButton, reasonWasBankRadioButton, - reasonWasScamRadioButton, reasonWasOtherRadioButton ); @@ -525,6 +531,8 @@ private void addReasonControls() { reasonWasOptionTradeRadioButton.setToggleGroup(reasonToggleGroup); reasonWasSellerNotRespondingRadioButton.setToggleGroup(reasonToggleGroup); reasonWasWrongSenderAccountRadioButton.setToggleGroup(reasonToggleGroup); + reasonWasPeerWasLateRadioButton.setToggleGroup(reasonToggleGroup); + reasonWasTradeAlreadySettledRadioButton.setToggleGroup(reasonToggleGroup); reasonToggleSelectionListener = (observable, oldValue, newValue) -> { if (newValue == reasonWasBugRadioButton) { @@ -547,6 +555,10 @@ private void addReasonControls() { disputeResult.setReason(DisputeResult.Reason.SELLER_NOT_RESPONDING); } else if (newValue == reasonWasWrongSenderAccountRadioButton) { disputeResult.setReason(DisputeResult.Reason.WRONG_SENDER_ACCOUNT); + } else if (newValue == reasonWasTradeAlreadySettledRadioButton) { + disputeResult.setReason(DisputeResult.Reason.TRADE_ALREADY_SETTLED); + } else if (newValue == reasonWasPeerWasLateRadioButton) { + disputeResult.setReason(DisputeResult.Reason.PEER_WAS_LATE); } }; reasonToggleGroup.selectedToggleProperty().addListener(reasonToggleSelectionListener); @@ -585,6 +597,12 @@ private void setReasonRadioButtonState() { case WRONG_SENDER_ACCOUNT: reasonToggleGroup.selectToggle(reasonWasWrongSenderAccountRadioButton); break; + case PEER_WAS_LATE: + reasonToggleGroup.selectToggle(reasonWasPeerWasLateRadioButton); + break; + case TRADE_ALREADY_SETTLED: + reasonToggleGroup.selectToggle(reasonWasTradeAlreadySettledRadioButton); + break; } } } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/SendPrivateNotificationWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SendPrivateNotificationWindow.java index c47ad17bb45..236015e11e0 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/SendPrivateNotificationWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SendPrivateNotificationWindow.java @@ -29,6 +29,7 @@ import bisq.network.p2p.NodeAddress; import bisq.network.p2p.SendMailboxMessageListener; +import bisq.common.UserThread; import bisq.common.app.DevEnv; import bisq.common.crypto.PubKeyRing; import bisq.common.util.Tuple2; @@ -44,6 +45,8 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; +import java.util.concurrent.TimeUnit; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,7 +61,10 @@ public class SendPrivateNotificationWindow extends Overlay { if (alertMessageTextArea.getText().length() > 0 && keyInputTextField.getText().length() > 0) { PrivateNotificationPayload privateNotification = new PrivateNotificationPayload(alertMessageTextArea.getText()); - if (!privateNotificationManager.sendPrivateNotificationMessageIfKeyIsValid( + boolean wasKeyValid = privateNotificationManager.sendPrivateNotificationMessageIfKeyIsValid( privateNotification, pubKeyRing, nodeAddress, @@ -115,26 +121,36 @@ private void addContent() { @Override public void onArrived() { log.info("PrivateNotificationPayload arrived at peer {}.", nodeAddress); - new Popup().feedback(Res.get("shared.messageArrived")) - .onClose(SendPrivateNotificationWindow.this::hide).show(); + UserThread.runAfter(() -> new Popup().feedback(Res.get("shared.messageArrived")) + .onClose(SendPrivateNotificationWindow.this::hide) + .show(), 100, TimeUnit.MILLISECONDS); } @Override public void onStoredInMailbox() { log.info("PrivateNotificationPayload stored in mailbox for peer {}.", nodeAddress); - new Popup().feedback(Res.get("shared.messageStoredInMailbox")) - .onClose(SendPrivateNotificationWindow.this::hide).show(); + UserThread.runAfter(() -> new Popup().feedback(Res.get("shared.messageStoredInMailbox")) + .onClose(SendPrivateNotificationWindow.this::hide) + .show(), 100, TimeUnit.MILLISECONDS); } @Override public void onFault(String errorMessage) { log.error("PrivateNotificationPayload failed: Peer {}, errorMessage={}", nodeAddress, errorMessage); - new Popup().feedback(Res.get("shared.messageSendingFailed", errorMessage)) - .onClose(SendPrivateNotificationWindow.this::hide).show(); + UserThread.runAfter(() -> new Popup().feedback(Res.get("shared.messageSendingFailed", errorMessage)) + .onClose(SendPrivateNotificationWindow.this::hide) + .show(), 100, TimeUnit.MILLISECONDS); } - })) - new Popup().warning(Res.get("shared.invalidKey")).width(300).onClose(this::blurAgain).show(); + }); + if (wasKeyValid) { + doClose(); + } else { + UserThread.runAfter(() -> new Popup().warning(Res.get("shared.invalidKey")) + .width(300) + .onClose(this::blurAgain) + .show(), 100, TimeUnit.MILLISECONDS); + } } }); diff --git a/desktop/src/main/java/bisq/desktop/main/shared/ChatView.java b/desktop/src/main/java/bisq/desktop/main/shared/ChatView.java index 985872e4225..60d49a067fa 100644 --- a/desktop/src/main/java/bisq/desktop/main/shared/ChatView.java +++ b/desktop/src/main/java/bisq/desktop/main/shared/ChatView.java @@ -680,9 +680,11 @@ public void scrollToBottom() { } public void setInputBoxVisible(boolean visible) { - messagesInputBox.setVisible(visible); - messagesInputBox.setManaged(visible); - AnchorPane.setBottomAnchor(messageListView, visible ? 120d : 0d); + if (messagesInputBox != null) { + messagesInputBox.setVisible(visible); + messagesInputBox.setManaged(visible); + AnchorPane.setBottomAnchor(messageListView, visible ? 120d : 0d); + } } public void removeInputBox() { diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java index 1ecfd2dbbdb..6bf28f446cb 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java @@ -42,6 +42,7 @@ import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.DisputeSession; +import bisq.core.support.messages.ChatMessage; import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; @@ -66,7 +67,6 @@ import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; -import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; @@ -84,6 +84,8 @@ import javafx.event.EventHandler; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; @@ -104,6 +106,8 @@ import lombok.Getter; +import javax.annotation.Nullable; + public abstract class DisputeView extends ActivatableView { protected final DisputeManager> disputeManager; @@ -128,12 +132,16 @@ public abstract class DisputeView extends ActivatableView { private ChangeListener selectedDisputeClosedPropertyListener; private Subscription selectedDisputeSubscription; - protected EventHandler keyEventEventHandler; + private EventHandler keyEventEventHandler; private Scene scene; protected FilteredList filteredList; - private InputTextField filterTextField; + protected InputTextField filterTextField; private ChangeListener filterTextFieldListener; - protected HBox filterBox; + private HBox filterBox; + protected AutoTooltipButton reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton; + private Map> disputeChatMessagesListeners = new HashMap<>(); + @Nullable + private ListChangeListener disputesListener; // Only set in mediation cases /////////////////////////////////////////////////////////////////////////////////////////// @@ -169,17 +177,38 @@ public void initialize() { HBox.setHgrow(label, Priority.NEVER); filterTextField = new InputTextField(); - filterTextField.setText("open"); filterTextFieldListener = (observable, oldValue, newValue) -> applyFilteredListPredicate(filterTextField.getText()); HBox.setHgrow(filterTextField, Priority.NEVER); - Button reportButton = new AutoTooltipButton(Res.get("support.reportButton.label")); + 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(); + }); + + sendPrivateNotificationButton = new AutoTooltipButton(Res.get("support.sendNotificationButton.label")); + sendPrivateNotificationButton.setDisable(true); + sendPrivateNotificationButton.setVisible(false); + sendPrivateNotificationButton.setManaged(false); + HBox.setHgrow(sendPrivateNotificationButton, Priority.NEVER); + sendPrivateNotificationButton.setOnAction(e -> { + sendPrivateNotification(); + }); + + reportButton = new AutoTooltipButton(Res.get("support.reportButton.label")); + reportButton.setVisible(false); + reportButton.setManaged(false); HBox.setHgrow(reportButton, Priority.NEVER); reportButton.setOnAction(e -> { showCompactReport(); }); - Button fullReportButton = new AutoTooltipButton(Res.get("support.fullReportButton.label")); + fullReportButton = new AutoTooltipButton(Res.get("support.fullReportButton.label")); + fullReportButton.setVisible(false); + fullReportButton.setManaged(false); HBox.setHgrow(fullReportButton, Priority.NEVER); fullReportButton.setOnAction(e -> { showFullReport(); @@ -190,10 +219,8 @@ public void initialize() { filterBox = new HBox(); filterBox.setSpacing(5); - filterBox.getChildren().addAll(label, filterTextField, spacer, reportButton, fullReportButton); + filterBox.getChildren().addAll(label, filterTextField, spacer, reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton); VBox.setVgrow(filterBox, Priority.NEVER); - filterBox.setVisible(false); - filterBox.setManaged(false); tableView = new TableView<>(); VBox.setVgrow(tableView, Priority.SOMETIMES); @@ -201,82 +228,302 @@ public void initialize() { root.getChildren().addAll(filterBox, tableView); - tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); - Label placeholder = new AutoTooltipLabel(Res.get("support.noTickets")); - placeholder.setWrapText(true); - tableView.setPlaceholder(placeholder); - tableView.getSelectionModel().clearSelection(); + setupTable(); - tableView.getColumns().add(getSelectColumn()); + selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> chatView.setInputBoxVisible(!newValue); - TableColumn contractColumn = getContractColumn(); - tableView.getColumns().add(contractColumn); + keyEventEventHandler = this::handleKeyPressed; - TableColumn dateColumn = getDateColumn(); - tableView.getColumns().add(dateColumn); + chatView = new ChatView(disputeManager, formatter); + chatView.initialize(); + } - TableColumn tradeIdColumn = getTradeIdColumn(); - tableView.getColumns().add(tradeIdColumn); + @Override + protected void activate() { + filterTextField.textProperty().addListener(filterTextFieldListener); - TableColumn buyerOnionAddressColumn = getBuyerOnionAddressColumn(); - tableView.getColumns().add(buyerOnionAddressColumn); + filteredList = new FilteredList<>(disputeManager.getDisputesAsObservableList()); + applyFilteredListPredicate(filterTextField.getText()); - TableColumn sellerOnionAddressColumn = getSellerOnionAddressColumn(); - tableView.getColumns().add(sellerOnionAddressColumn); + sortedList = new SortedList<>(filteredList); + sortedList.comparatorProperty().bind(tableView.comparatorProperty()); + tableView.setItems(sortedList); + // sortedList.setComparator((o1, o2) -> o2.getOpeningDate().compareTo(o1.getOpeningDate())); + selectedDisputeSubscription = EasyBind.subscribe(tableView.getSelectionModel().selectedItemProperty(), this::onSelectDispute); - TableColumn marketColumn = getMarketColumn(); - tableView.getColumns().add(marketColumn); + Dispute selectedItem = tableView.getSelectionModel().getSelectedItem(); + if (selectedItem != null) + tableView.getSelectionModel().select(selectedItem); + else if (sortedList.size() > 0) + tableView.getSelectionModel().select(0); - TableColumn roleColumn = getRoleColumn(); - tableView.getColumns().add(roleColumn); + if (chatView != null) { + chatView.activate(); + chatView.scrollToBottom(); + } - TableColumn stateColumn = getStateColumn(); - tableView.getColumns().add(stateColumn); + scene = root.getScene(); + if (scene != null) + scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); - tradeIdColumn.setComparator(Comparator.comparing(Dispute::getTradeId)); - dateColumn.setComparator(Comparator.comparing(Dispute::getOpeningDate)); - buyerOnionAddressColumn.setComparator(Comparator.comparing(this::getBuyerOnionAddressColumnLabel)); - sellerOnionAddressColumn.setComparator(Comparator.comparing(this::getSellerOnionAddressColumnLabel)); - marketColumn.setComparator((o1, o2) -> CurrencyUtil.getCurrencyPair(o1.getContract().getOfferPayload().getCurrencyCode()).compareTo(o2.getContract().getOfferPayload().getCurrencyCode())); + // If doPrint=true we print out a html page which opens tabs with all deposit txs + // (firefox needs about:config change to allow > 20 tabs) + // Useful to check if there any funds in not finished trades (no payout tx done). + // Last check 10.02.2017 found 8 trades and we contacted all traders as far as possible (email if available + // otherwise in-app private notification) + boolean doPrint = false; + //noinspection ConstantConditions + if (doPrint) { + try { + DateFormat formatter = new SimpleDateFormat("dd/MM/yy"); + //noinspection UnusedAssignment + Date startDate = formatter.parse("10/02/17"); + startDate = new Date(0); // print all from start - dateColumn.setSortType(TableColumn.SortType.DESCENDING); - tableView.getSortOrder().add(dateColumn); + HashMap map = new HashMap<>(); + disputeManager.getDisputesAsObservableList().forEach(dispute -> map.put(dispute.getDepositTxId(), dispute)); - selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> chatView.setInputBoxVisible(!newValue); + final Date finalStartDate = startDate; + List disputes = new ArrayList<>(map.values()); + disputes.sort(Comparator.comparing(Dispute::getOpeningDate)); + List> subLists = Lists.partition(disputes, 1000); + StringBuilder sb = new StringBuilder(); + // We don't translate that as it is not intended for the public + subLists.forEach(list -> { + StringBuilder sb1 = new StringBuilder("\n\n"); + list.forEach(dispute -> { + if (dispute.getOpeningDate().after(finalStartDate)) { + String txId = dispute.getDepositTxId(); + sb1.append("window.open(\"https://blockchain.info/tx/").append(txId).append("\", '_blank');\n"); + + sb2.append("Dispute ID: ").append(dispute.getId()). + append(" Tx ID: "). + append(""). + append(txId).append(" "). + append("Opening date: ").append(formatter.format(dispute.getOpeningDate())).append("
\n"); + } + }); + sb2.append(""); + String res = sb1.toString() + sb2.toString(); - keyEventEventHandler = event -> { - if (Utilities.isAltOrCtrlPressed(KeyCode.U, event)) { - // Hidden shortcut to re-open a dispute. Allow it also for traders not only arbitrator. - if (selectedDispute != null) { - if (selectedDisputeClosedPropertyListener != null) - selectedDispute.isClosedProperty().removeListener(selectedDisputeClosedPropertyListener); - selectedDispute.setIsClosed(false); - handleOnSelectDispute(selectedDispute); + sb.append(res).append("\n\n\n"); + }); + log.info(sb.toString()); + } catch (ParseException ignore) { + } + } + GUIUtil.requestFocus(filterTextField); + } + + @Override + protected void deactivate() { + filterTextField.textProperty().removeListener(filterTextFieldListener); + sortedList.comparatorProperty().unbind(); + selectedDisputeSubscription.unsubscribe(); + removeListenersOnSelectDispute(); + + if (scene != null) + scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + + if (chatView != null) + chatView.deactivate(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + // Reopen feature is only use in mediation from both mediator and traders + protected void setupReOpenDisputeListener() { + disputesListener = c -> { + c.next(); + if (c.wasAdded()) { + onDisputesAdded(c.getAddedSubList()); + } else if (c.wasRemoved()) { + onDisputesRemoved(c.getRemoved()); + } + }; + } + + // Reopen feature is only use in mediation from both mediator and traders + protected void activateReOpenDisputeListener() { + // Register listeners on all disputes for potential re-opening + onDisputesAdded(disputeManager.getDisputesAsObservableList()); + disputeManager.getDisputesAsObservableList().addListener(disputesListener); + + disputeManager.getDisputesAsObservableList().forEach(dispute -> { + if (dispute.isClosed()) { + ObservableList chatMessages = dispute.getChatMessages(); + // 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 (!chatMessages.isEmpty() && !chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute)) { + onSelectDispute(dispute); + reOpenDispute(); } - } else if (Utilities.isAltOrCtrlPressed(KeyCode.R, event)) { - if (selectedDispute != null) { - PubKeyRing pubKeyRing = selectedDispute.getTraderPubKeyRing(); - NodeAddress nodeAddress; - if (pubKeyRing.equals(selectedDispute.getContract().getBuyerPubKeyRing())) - nodeAddress = selectedDispute.getContract().getBuyerNodeAddress(); - else - nodeAddress = selectedDispute.getContract().getSellerNodeAddress(); - - new SendPrivateNotificationWindow( - privateNotificationManager, - pubKeyRing, - nodeAddress, - useDevPrivilegeKeys - ).show(); + } + }); + } + + // Reopen feature is only use in mediation from both mediator and traders + protected void deactivateReOpenDisputeListener() { + onDisputesRemoved(disputeManager.getDisputesAsObservableList()); + disputeManager.getDisputesAsObservableList().removeListener(disputesListener); + } + + protected abstract SupportType getType(); + + protected abstract DisputeSession getConcreteDisputeChatSession(Dispute dispute); + + protected void applyFilteredListPredicate(String filterString) { + // If in trader view we must not display arbitrators own disputes as trader (must not happen anyway) + filteredList.setPredicate(dispute -> !dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())); + } + + protected void reOpenDisputeFromButton() { + reOpenDispute(); + } + + protected abstract void handleOnSelectDispute(Dispute dispute); + + protected void onCloseDispute(Dispute dispute) { + long protocolVersion = dispute.getContract().getOfferPayload().getProtocolVersion(); + if (protocolVersion == Version.TRADE_PROTOCOL_VERSION) { + disputeSummaryWindow.onFinalizeDispute(() -> chatView.removeInputBox()) + .show(dispute); + } else { + new Popup().warning(Res.get("support.wrongVersion", protocolVersion)).show(); + } + } + + protected void handleKeyPressed(KeyEvent event) { + } + + protected void reOpenDispute() { + if (selectedDispute != null) { + selectedDispute.setIsClosed(false); + handleOnSelectDispute(selectedDispute); + } + } + + protected boolean anyMatchOfFilterString(Dispute dispute, String filterString) { + boolean matchesTradeId = dispute.getTradeId().contains(filterString); + boolean matchesDate = DisplayUtils.formatDate(dispute.getOpeningDate()).contains(filterString); + boolean isBuyerOnion = dispute.getContract().getBuyerNodeAddress().getFullAddress().contains(filterString); + boolean isSellerOnion = dispute.getContract().getSellerNodeAddress().getFullAddress().contains(filterString); + boolean matchesBuyersPaymentAccountData = dispute.getContract().getBuyerPaymentAccountPayload().getPaymentDetails().contains(filterString); + boolean matchesSellersPaymentAccountData = dispute.getContract().getSellerPaymentAccountPayload().getPaymentDetails().contains(filterString); + return matchesTradeId || matchesDate || isBuyerOnion || isSellerOnion || + matchesBuyersPaymentAccountData || matchesSellersPaymentAccountData; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onOpenContract(Dispute dispute) { + contractWindow.show(dispute); + } + + private void removeListenersOnSelectDispute() { + if (selectedDispute != null) { + if (selectedDisputeClosedPropertyListener != null) + selectedDispute.isClosedProperty().removeListener(selectedDisputeClosedPropertyListener); + } + } + + private void addListenersOnSelectDispute() { + if (selectedDispute != null) + selectedDispute.isClosedProperty().addListener(selectedDisputeClosedPropertyListener); + } + + private void onSelectDispute(Dispute dispute) { + removeListenersOnSelectDispute(); + if (dispute == null) { + if (root.getChildren().size() > 2) { + root.getChildren().remove(2); + } + + selectedDispute = null; + } else if (selectedDispute != dispute) { + selectedDispute = dispute; + if (chatView != null) { + handleOnSelectDispute(dispute); + } + + if (root.getChildren().size() > 2) { + root.getChildren().remove(2); + } + root.getChildren().add(2, chatView); + } + + reOpenButton.setDisable(selectedDispute == null || !selectedDispute.isClosed()); + sendPrivateNotificationButton.setDisable(selectedDispute == null); + + addListenersOnSelectDispute(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + // Reopen feature is only use in mediation from both mediator and traders + private void onDisputesAdded(List addedDisputes) { + addedDisputes.forEach(dispute -> { + ListChangeListener listener = c -> { + c.next(); + if (c.wasAdded()) { + c.getAddedSubList().forEach(chatMessage -> { + if (dispute.isClosed()) { + if (chatMessage.isResultMessage(dispute)) { + onSelectDispute(null); + } else { + onSelectDispute(dispute); + reOpenDispute(); + } + } + }); } + // We never remove chat messages so no remove listener + }; + dispute.getChatMessages().addListener(listener); + disputeChatMessagesListeners.put(dispute.getId(), listener); + }); + } + + // Reopen feature is only use in mediation from both mediator and traders + private void onDisputesRemoved(List removedDisputes) { + removedDisputes.forEach(dispute -> { + String id = dispute.getId(); + if (disputeChatMessagesListeners.containsKey(id)) { + ListChangeListener listener = disputeChatMessagesListeners.get(id); + dispute.getChatMessages().removeListener(listener); + disputeChatMessagesListeners.remove(id); + } + }); + } + + private void sendPrivateNotification() { + if (selectedDispute != null) { + PubKeyRing pubKeyRing = selectedDispute.getTraderPubKeyRing(); + NodeAddress nodeAddress; + Contract contract = selectedDispute.getContract(); + if (pubKeyRing.equals(contract.getBuyerPubKeyRing())) { + nodeAddress = contract.getBuyerNodeAddress(); } else { - handleKeyPressed(event); + nodeAddress = contract.getSellerNodeAddress(); } - }; - chatView = new ChatView(disputeManager, formatter); - chatView.initialize(); + new SendPrivateNotificationWindow( + privateNotificationManager, + pubKeyRing, + nodeAddress, + useDevPrivilegeKeys + ).show(); + } } private void showCompactReport() { @@ -460,171 +707,55 @@ private void showFullReport() { .show(); } - @Override - protected void activate() { - filterTextField.textProperty().addListener(filterTextFieldListener); - - filteredList = new FilteredList<>(disputeManager.getDisputesAsObservableList()); - applyFilteredListPredicate(filterTextField.getText()); - - sortedList = new SortedList<>(filteredList); - sortedList.comparatorProperty().bind(tableView.comparatorProperty()); - tableView.setItems(sortedList); - - // sortedList.setComparator((o1, o2) -> o2.getOpeningDate().compareTo(o1.getOpeningDate())); - selectedDisputeSubscription = EasyBind.subscribe(tableView.getSelectionModel().selectedItemProperty(), this::onSelectDispute); - - Dispute selectedItem = tableView.getSelectionModel().getSelectedItem(); - if (selectedItem != null) - tableView.getSelectionModel().select(selectedItem); - else if (sortedList.size() > 0) - tableView.getSelectionModel().select(0); - - if (chatView != null) { - chatView.activate(); - chatView.scrollToBottom(); - } - - scene = root.getScene(); - if (scene != null) - scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); - - // If doPrint=true we print out a html page which opens tabs with all deposit txs - // (firefox needs about:config change to allow > 20 tabs) - // Useful to check if there any funds in not finished trades (no payout tx done). - // Last check 10.02.2017 found 8 trades and we contacted all traders as far as possible (email if available - // otherwise in-app private notification) - boolean doPrint = false; - //noinspection ConstantConditions - if (doPrint) { - try { - DateFormat formatter = new SimpleDateFormat("dd/MM/yy"); - //noinspection UnusedAssignment - Date startDate = formatter.parse("10/02/17"); - startDate = new Date(0); // print all from start - - HashMap map = new HashMap<>(); - disputeManager.getDisputesAsObservableList().forEach(dispute -> map.put(dispute.getDepositTxId(), dispute)); - - final Date finalStartDate = startDate; - List disputes = new ArrayList<>(map.values()); - disputes.sort(Comparator.comparing(Dispute::getOpeningDate)); - List> subLists = Lists.partition(disputes, 1000); - StringBuilder sb = new StringBuilder(); - // We don't translate that as it is not intended for the public - subLists.forEach(list -> { - StringBuilder sb1 = new StringBuilder("\n\n"); - list.forEach(dispute -> { - if (dispute.getOpeningDate().after(finalStartDate)) { - String txId = dispute.getDepositTxId(); - sb1.append("window.open(\"https://blockchain.info/tx/").append(txId).append("\", '_blank');\n"); - - sb2.append("Dispute ID: ").append(dispute.getId()). - append(" Tx ID: "). - append(""). - append(txId).append(" "). - append("Opening date: ").append(formatter.format(dispute.getOpeningDate())).append("
\n"); - } - }); - sb2.append(""); - String res = sb1.toString() + sb2.toString(); - sb.append(res).append("\n\n\n"); - }); - log.info(sb.toString()); - } catch (ParseException ignore) { - } - } - GUIUtil.requestFocus(filterTextField); - } - - @Override - protected void deactivate() { - filterTextField.textProperty().removeListener(filterTextFieldListener); - sortedList.comparatorProperty().unbind(); - selectedDisputeSubscription.unsubscribe(); - removeListenersOnSelectDispute(); - - if (scene != null) - scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); - - if (chatView != null) - chatView.deactivate(); - } - - protected abstract SupportType getType(); - - protected abstract DisputeSession getConcreteDisputeChatSession(Dispute dispute); + /////////////////////////////////////////////////////////////////////////////////////////// + // Table + /////////////////////////////////////////////////////////////////////////////////////////// - protected void applyFilteredListPredicate(String filterString) { - // If in trader view we must not display arbitrators own disputes as trader (must not happen anyway) - filteredList.setPredicate(dispute -> !dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())); - } + private void setupTable() { + tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + Label placeholder = new AutoTooltipLabel(Res.get("support.noTickets")); + placeholder.setWrapText(true); + tableView.setPlaceholder(placeholder); + tableView.getSelectionModel().clearSelection(); + tableView.getColumns().add(getSelectColumn()); - /////////////////////////////////////////////////////////////////////////////////////////// - // UI actions - /////////////////////////////////////////////////////////////////////////////////////////// + TableColumn contractColumn = getContractColumn(); + tableView.getColumns().add(contractColumn); - private void onOpenContract(Dispute dispute) { - contractWindow.show(dispute); - } + TableColumn dateColumn = getDateColumn(); + tableView.getColumns().add(dateColumn); - protected void removeListenersOnSelectDispute() { - if (selectedDispute != null) { - if (selectedDisputeClosedPropertyListener != null) - selectedDispute.isClosedProperty().removeListener(selectedDisputeClosedPropertyListener); - } - } + TableColumn tradeIdColumn = getTradeIdColumn(); + tableView.getColumns().add(tradeIdColumn); - protected void addListenersOnSelectDispute() { - if (selectedDispute != null) - selectedDispute.isClosedProperty().addListener(selectedDisputeClosedPropertyListener); - } + TableColumn buyerOnionAddressColumn = getBuyerOnionAddressColumn(); + tableView.getColumns().add(buyerOnionAddressColumn); - protected void onSelectDispute(Dispute dispute) { - removeListenersOnSelectDispute(); - if (dispute == null) { - if (root.getChildren().size() > 2) - root.getChildren().remove(2); + TableColumn sellerOnionAddressColumn = getSellerOnionAddressColumn(); + tableView.getColumns().add(sellerOnionAddressColumn); - selectedDispute = null; - } else if (selectedDispute != dispute) { - this.selectedDispute = dispute; - if (chatView != null) { - handleOnSelectDispute(dispute); - } - if (root.getChildren().size() > 2) - root.getChildren().remove(2); - root.getChildren().add(2, chatView); - } + TableColumn marketColumn = getMarketColumn(); + tableView.getColumns().add(marketColumn); - addListenersOnSelectDispute(); - } + TableColumn roleColumn = getRoleColumn(); + tableView.getColumns().add(roleColumn); - protected abstract void handleOnSelectDispute(Dispute dispute); + TableColumn stateColumn = getStateColumn(); + tableView.getColumns().add(stateColumn); - protected void onCloseDispute(Dispute dispute) { - long protocolVersion = dispute.getContract().getOfferPayload().getProtocolVersion(); - if (protocolVersion == Version.TRADE_PROTOCOL_VERSION) { - disputeSummaryWindow.onFinalizeDispute(() -> chatView.removeInputBox()) - .show(dispute); - } else { - new Popup() - .warning(Res.get("support.wrongVersion", protocolVersion)) - .show(); - } - } + tradeIdColumn.setComparator(Comparator.comparing(Dispute::getTradeId)); + dateColumn.setComparator(Comparator.comparing(Dispute::getOpeningDate)); + buyerOnionAddressColumn.setComparator(Comparator.comparing(this::getBuyerOnionAddressColumnLabel)); + sellerOnionAddressColumn.setComparator(Comparator.comparing(this::getSellerOnionAddressColumnLabel)); + marketColumn.setComparator((o1, o2) -> CurrencyUtil.getCurrencyPair(o1.getContract().getOfferPayload().getCurrencyCode()).compareTo(o2.getContract().getOfferPayload().getCurrencyCode())); - protected void handleKeyPressed(KeyEvent event) { + dateColumn.setSortType(TableColumn.SortType.DESCENDING); + tableView.getSortOrder().add(dateColumn); } - /////////////////////////////////////////////////////////////////////////////////////////// - // Table - /////////////////////////////////////////////////////////////////////////////////////////// - private TableColumn getSelectColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.select")); column.setMinWidth(80); diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/DisputeAgentView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/DisputeAgentView.java index 864e97148f8..582c461b087 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/DisputeAgentView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/DisputeAgentView.java @@ -22,7 +22,6 @@ import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; import bisq.desktop.main.support.dispute.DisputeView; -import bisq.desktop.util.DisplayUtils; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; @@ -66,27 +65,29 @@ public DisputeAgentView(DisputeManager { - boolean matchesTradeId = dispute.getTradeId().contains(filterString); - boolean matchesDate = DisplayUtils.formatDate(dispute.getOpeningDate()).contains(filterString); - boolean isBuyerOnion = dispute.getContract().getBuyerNodeAddress().getFullAddress().contains(filterString); - boolean isSellerOnion = dispute.getContract().getSellerNodeAddress().getFullAddress().contains(filterString); - boolean matchesBuyersPaymentAccountData = dispute.getContract().getBuyerPaymentAccountPayload().getPaymentDetails().contains(filterString); - boolean matchesSellersPaymentAccountData = dispute.getContract().getSellerPaymentAccountPayload().getPaymentDetails().contains(filterString); - - boolean anyMatch = matchesTradeId || matchesDate || isBuyerOnion || isSellerOnion || - matchesBuyersPaymentAccountData || matchesSellersPaymentAccountData; - - boolean open = !dispute.isClosed() && filterString.toLowerCase().equals("open"); - boolean isMyCase = dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing()); - return isMyCase && (open || filterString.isEmpty() || anyMatch); + // If in arbitrator view we must only display disputes where we are selected as arbitrator (must not receive others anyway) + if (!dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())) { + return false; + } + boolean isOpen = !dispute.isClosed() && filterString.toLowerCase().equals("open"); + return filterString.isEmpty() || + isOpen || + anyMatchOfFilterString(dispute, filterString); }); } diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/mediation/MediatorView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/mediation/MediatorView.java index 44601a2d249..ba939c34d23 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/mediation/MediatorView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/mediation/MediatorView.java @@ -37,8 +37,8 @@ import bisq.common.config.Config; import bisq.common.crypto.KeyRing; -import javax.inject.Named; import javax.inject.Inject; +import javax.inject.Named; @FxmlView public class MediatorView extends DisputeAgentView { @@ -66,6 +66,31 @@ public MediatorView(MediationManager mediationManager, useDevPrivilegeKeys); } + @Override + public void initialize() { + super.initialize(); + reOpenButton.setVisible(true); + reOpenButton.setManaged(true); + setupReOpenDisputeListener(); + } + + @Override + protected void activate() { + super.activate(); + activateReOpenDisputeListener(); + + // We need to call applyFilteredListPredicate after we called activateReOpenDisputeListener as we use the + // "open" string by default and it was applied in the super call but the disputes got set in + // activateReOpenDisputeListener + applyFilteredListPredicate(filterTextField.getText()); + } + + @Override + protected void deactivate() { + super.deactivate(); + deactivateReOpenDisputeListener(); + } + @Override protected SupportType getType() { return SupportType.MEDIATION; diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/DisputeClientView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/DisputeClientView.java index c6fe9ce3cbe..f06875202e7 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/DisputeClientView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/DisputeClientView.java @@ -17,7 +17,6 @@ package bisq.desktop.main.support.dispute.client; -import bisq.desktop.common.view.FxmlView; import bisq.desktop.main.overlays.windows.ContractWindow; import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; @@ -54,4 +53,16 @@ protected void handleOnSelectDispute(Dispute dispute) { DisputeSession chatSession = getConcreteDisputeChatSession(dispute); chatView.display(chatSession, root.widthProperty()); } + + @Override + protected void applyFilteredListPredicate(String filterString) { + filteredList.setPredicate(dispute -> { + // As we are in the client view we hide disputes where we are the agent + if (dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())) { + return false; + } + + return filterString.isEmpty() || anyMatchOfFilterString(dispute, filterString); + }); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/mediation/MediationClientView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/mediation/MediationClientView.java index d88bd541112..eb60178e5f7 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/mediation/MediationClientView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/mediation/MediationClientView.java @@ -18,6 +18,7 @@ package bisq.desktop.main.support.dispute.client.mediation; import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.ContractWindow; import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; @@ -25,6 +26,7 @@ import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; +import bisq.core.locale.Res; import bisq.core.support.SupportType; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeSession; @@ -37,9 +39,8 @@ import bisq.common.config.Config; import bisq.common.crypto.KeyRing; -import javax.inject.Named; - import javax.inject.Inject; +import javax.inject.Named; @FxmlView public class MediationClientView extends DisputeClientView { @@ -59,6 +60,26 @@ public MediationClientView(MediationManager mediationManager, useDevPrivilegeKeys); } + @Override + public void initialize() { + super.initialize(); + reOpenButton.setVisible(true); + reOpenButton.setManaged(true); + setupReOpenDisputeListener(); + } + + @Override + protected void activate() { + super.activate(); + activateReOpenDisputeListener(); + } + + @Override + protected void deactivate() { + super.deactivate(); + deactivateReOpenDisputeListener(); + } + @Override protected SupportType getType() { return SupportType.MEDIATION; @@ -68,4 +89,12 @@ protected SupportType getType() { protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { return new MediationSession(dispute, disputeManager.isTrader(dispute)); } + + @Override + protected void reOpenDisputeFromButton() { + new Popup().attention(Res.get("support.reOpenByTrader.prompt")) + .actionButtonText(Res.get("shared.yes")) + .onAction(this::reOpenDispute) + .show(); + } } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 6b3e068f3da..b581cf4009f 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -813,6 +813,8 @@ message DisputeResult { OPTION_TRADE = 8; SELLER_NOT_RESPONDING = 9; WRONG_SENDER_ACCOUNT = 10; + TRADE_ALREADY_SETTLED = 11; + PEER_WAS_LATE = 12; } string trade_id = 1; From b5c8bae1fae3883e42f63020ed35a706cbc2049e Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 28 Aug 2020 22:16:04 -0500 Subject: [PATCH 4/4] Enable check for lock time on mainnet and add a 3 block tolerance. --- .../TakerProcessesInputsForDepositTxResponse.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessesInputsForDepositTxResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessesInputsForDepositTxResponse.java index ba1b80b5308..4eec9e57eec 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessesInputsForDepositTxResponse.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessesInputsForDepositTxResponse.java @@ -17,11 +17,13 @@ package bisq.core.trade.protocol.tasks.taker; +import bisq.core.btc.wallet.Restrictions; import bisq.core.trade.Trade; import bisq.core.trade.messages.InputsForDepositTxResponse; import bisq.core.trade.protocol.TradingPeer; import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.common.config.Config; import bisq.common.taskrunner.TaskRunner; import lombok.extern.slf4j.Slf4j; @@ -58,8 +60,15 @@ protected void run() { byte[] preparedDepositTx = inputsForDepositTxResponse.getPreparedDepositTx(); processModel.setPreparedDepositTx(checkNotNull(preparedDepositTx)); long lockTime = inputsForDepositTxResponse.getLockTime(); - //todo for dev testing deactivated - //checkArgument(lockTime >= processModel.getBtcWalletService().getBestChainHeight() + 144 * 20); + if (Config.baseCurrencyNetwork().isMainnet()) { + int myLockTime = processModel.getBtcWalletService().getBestChainHeight() + + Restrictions.getLockTime(processModel.getOffer().getPaymentMethod().isAsset()); + // We allow a tolerance of 3 blocks as BestChainHeight might be a bit different on maker and taker in case a new + // block was just found + checkArgument(Math.abs(lockTime - myLockTime) <= 3, + "Lock time of maker is more than 3 blocks different to the locktime I " + + "calculated. Makers lockTime= " + lockTime + ", myLockTime=" + myLockTime); + } trade.setLockTime(lockTime); log.info("lockTime={}, delay={}", lockTime, (processModel.getBtcWalletService().getBestChainHeight() - lockTime));