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 db11d29ef03..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,10 +56,15 @@ public enum Reason { OTHER, BUG, USABILITY, - SCAM, - PROTOCOL_VIOLATION, - NO_REPLY, - BANK_PROBLEMS + SCAM, // Not used anymore + PROTOCOL_VIOLATION, // Not used anymore + NO_REPLY, // Not used anymore + BANK_PROBLEMS, + OPTION_TRADE, + SELLER_NOT_RESPONDING, + 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/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)); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 8b9d6fb6f43..dcdf17d0d98 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -965,8 +965,13 @@ 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 support.sendingMessage=Sending Message... support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. @@ -2376,6 +2381,12 @@ 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.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 03374fd3938..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 @@ -113,7 +113,10 @@ public class DisputeSummaryWindow extends Overlay { buyerGetsAllRadioButton, sellerGetsAllRadioButton, customRadioButton; private RadioButton reasonWasBugRadioButton, reasonWasUsabilityIssueRadioButton, reasonProtocolViolationRadioButton, reasonNoReplyRadioButton, reasonWasScamRadioButton, - reasonWasOtherRadioButton, reasonWasBankRadioButton; + reasonWasOtherRadioButton, reasonWasBankRadioButton, reasonWasOptionTradeRadioButton, + 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; private String role; @@ -155,7 +158,7 @@ public void show(Dispute dispute) { this.dispute = dispute; rowIndex = -1; - width = 700; + width = 1150; createGridPane(); addContent(); display(); @@ -209,7 +212,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 +261,11 @@ private void addContent() { reasonWasScamRadioButton.setDisable(true); reasonWasOtherRadioButton.setDisable(true); reasonWasBankRadioButton.setDisable(true); + reasonWasOptionTradeRadioButton.setDisable(true); + reasonWasSellerNotRespondingRadioButton.setDisable(true); + reasonWasWrongSenderAccountRadioButton.setDisable(true); + reasonWasPeerWasLateRadioButton.setDisable(true); + reasonWasTradeAlreadySettledRadioButton.setDisable(true); isLoserPublisherCheckBox.setDisable(true); isLoserPublisherCheckBox.setSelected(peersDisputeResult.isLoserPublisher()); @@ -485,12 +493,27 @@ 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")); + reasonWasPeerWasLateRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.peerWasLate")); + reasonWasTradeAlreadySettledRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.reason.tradeAlreadySettled")); 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( + reasonWasTradeAlreadySettledRadioButton, + reasonWasPeerWasLateRadioButton, + reasonWasOptionTradeRadioButton, + reasonWasSellerNotRespondingRadioButton, + reasonWasWrongSenderAccountRadioButton, + reasonWasBugRadioButton, + reasonWasUsabilityIssueRadioButton, + reasonWasBankRadioButton, + reasonWasOtherRadioButton + ); VBox vBox = addTopLabelWithVBox(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.reason"), @@ -505,22 +528,38 @@ private void addReasonControls() { reasonWasScamRadioButton.setToggleGroup(reasonToggleGroup); reasonWasOtherRadioButton.setToggleGroup(reasonToggleGroup); reasonWasBankRadioButton.setToggleGroup(reasonToggleGroup); + reasonWasOptionTradeRadioButton.setToggleGroup(reasonToggleGroup); + reasonWasSellerNotRespondingRadioButton.setToggleGroup(reasonToggleGroup); + reasonWasWrongSenderAccountRadioButton.setToggleGroup(reasonToggleGroup); + reasonWasPeerWasLateRadioButton.setToggleGroup(reasonToggleGroup); + reasonWasTradeAlreadySettledRadioButton.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); + } 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); } @@ -549,6 +588,21 @@ 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; + 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/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..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; @@ -55,6 +56,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; @@ -64,9 +67,9 @@ 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; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; @@ -81,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; @@ -101,6 +106,8 @@ import lombok.Getter; +import javax.annotation.Nullable; + public abstract class DisputeView extends ActivatableView { protected final DisputeManager> disputeManager; @@ -125,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 /////////////////////////////////////////////////////////////////////////////////////////// @@ -163,16 +174,53 @@ 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); + + 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(); + }); + + fullReportButton = new AutoTooltipButton(Res.get("support.fullReportButton.label")); + fullReportButton.setVisible(false); + fullReportButton.setManaged(false); + 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, reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton); VBox.setVgrow(filterBox, Priority.NEVER); - filterBox.setVisible(false); - filterBox.setManaged(false); tableView = new TableView<>(); VBox.setVgrow(tableView, Priority.SOMETIMES); @@ -180,85 +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(); + + 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()); + } + }; + } - keyEventEventHandler = event -> { - if (Utilities.isAltOrCtrlPressed(KeyCode.L, event)) { - showFullReport(); - } else if (Utilities.isAltOrCtrlPressed(KeyCode.K, event)) { - showCompactReport(); - } else 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); + // 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() { @@ -274,15 +539,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 +564,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"); + } + + 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 = disputeResult0.getSummaryNotesProperty().get(); + 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 +633,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 +660,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,180 +698,64 @@ 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(); } - @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 117dbb4cb53..b581cf4009f 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -810,6 +810,11 @@ message DisputeResult { PROTOCOL_VIOLATION = 5; NO_REPLY = 6; BANK_PROBLEMS = 7; + OPTION_TRADE = 8; + SELLER_NOT_RESPONDING = 9; + WRONG_SENDER_ACCOUNT = 10; + TRADE_ALREADY_SETTLED = 11; + PEER_WAS_LATE = 12; } string trade_id = 1;