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 3aa011a64de..b92ffe1c92f 100644 --- a/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java +++ b/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java @@ -94,12 +94,12 @@ 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(chatMessage -> onChatMessage(chatMessage, dispute)); + c.getAddedSubList().forEach(this::onChatMessage); } }); } - private void onChatMessage(ChatMessage chatMessage, Dispute dispute) { + private void onChatMessage(ChatMessage chatMessage) { if (chatMessage.getSenderNodeAddress().equals(p2PService.getAddress())) { return; } @@ -116,22 +116,5 @@ private void onChatMessage(ChatMessage chatMessage, Dispute dispute) { log.error(e.toString()); e.printStackTrace(); } - - // We check at every new message if it might be a message sent after the dispute had been closed. If that is the - // case we revert the isClosed flag so that the UI can reopen the dispute and indicate that a new dispute - // message arrived. - Optional newestChatMessage = dispute.getChatMessages().stream(). - sorted(Comparator.comparingLong(ChatMessage::getDate).reversed()).findFirst(); - // If last message is not a result message we re-open as we might have received a new message from the - // trader/mediator/arbitrator who has reopened the case - if (dispute.isClosed() && newestChatMessage.isPresent() && !newestChatMessage.get().isResultMessage(dispute)) { - log.info("Reopening dispute {} due to new chat message received {}", dispute.getTradeId(), newestChatMessage.get().getUid()); - dispute.reOpen(); - if (dispute.getSupportType() == SupportType.MEDIATION) { - mediationManager.requestPersistence(); - } else if (dispute.getSupportType() == SupportType.REFUND) { - refundManager.requestPersistence(); - } - } } } diff --git a/core/src/main/java/bisq/core/support/dispute/Dispute.java b/core/src/main/java/bisq/core/support/dispute/Dispute.java index dfd676ca2e7..ec8a3e74564 100644 --- a/core/src/main/java/bisq/core/support/dispute/Dispute.java +++ b/core/src/main/java/bisq/core/support/dispute/Dispute.java @@ -42,15 +42,14 @@ import org.bitcoinj.core.Transaction; -import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; -import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -82,7 +81,8 @@ public enum State { NEW, OPEN, REOPENED, - CLOSED; + CLOSED, + RESULT_PROPOSED; public static Dispute.State fromProto(protobuf.Dispute.State state) { return ProtoUtil.enumFromProto(Dispute.State.class, state.name()); @@ -168,7 +168,7 @@ public static protobuf.Dispute.State toProtoMessage(Dispute.State state) { @Setter private transient boolean payoutDone = false; - private transient final BooleanProperty isClosedProperty = new SimpleBooleanProperty(); + private transient final StringProperty disputeStateProperty = new SimpleStringProperty(); private transient final IntegerProperty badgeCountProperty = new SimpleIntegerProperty(); private transient FileTransferReceiver fileTransferSession = null; @@ -239,6 +239,7 @@ public Dispute(long openingDate, id = tradeId + "_" + traderId; uid = UUID.randomUUID().toString(); + setState(State.NEW); refreshAlertLevel(true); } @@ -413,7 +414,7 @@ public void reOpen() { public void setState(Dispute.State disputeState) { this.disputeState = disputeState; - this.isClosedProperty.set(disputeState == State.CLOSED); + this.disputeStateProperty.set(disputeState.toString()); } public void setDisputeResult(DisputeResult disputeResult) { @@ -438,10 +439,6 @@ public String getShortTradeId() { return Utilities.getShortId(tradeId); } - public ReadOnlyBooleanProperty isClosedProperty() { - return isClosedProperty; - } - public ReadOnlyIntegerProperty getBadgeCountProperty() { return badgeCountProperty; } @@ -470,6 +467,10 @@ public boolean isClosed() { return this.disputeState == State.CLOSED; } + public boolean isResultProposed() { + return this.disputeState == State.RESULT_PROPOSED; + } + public void refreshAlertLevel(boolean senderFlag) { // if the dispute is "new" that is 1 alert that has to be propagated upstream // or if there are unread messages that is 1 alert that has to be propagated upstream @@ -559,7 +560,7 @@ public String toString() { ",\n agentPubKeyRing=" + agentPubKeyRing + ",\n isSupportTicket=" + isSupportTicket + ",\n chatMessages=" + chatMessages + - ",\n isClosedProperty=" + isClosedProperty + + ",\n disputeStateProperty=" + disputeStateProperty + ",\n disputeResultProperty=" + disputeResultProperty + ",\n disputePayoutTxId='" + disputePayoutTxId + '\'' + ",\n openingDate=" + openingDate + diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeListService.java b/core/src/main/java/bisq/core/support/dispute/DisputeListService.java index d1a1a20ba10..ebc2d6fad76 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeListService.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeListService.java @@ -99,7 +99,7 @@ protected String getFileName() { public void cleanupDisputes(@Nullable Consumer closedDisputeHandler) { disputeList.stream().forEach(dispute -> { String tradeId = dispute.getTradeId(); - if (dispute.isClosed() && closedDisputeHandler != null) { + if (dispute.isResultProposed() && closedDisputeHandler != null) { closedDisputeHandler.accept(tradeId); } }); 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 e64ac9a78dc..9ebe8eaf575 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -31,6 +31,7 @@ import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.PriceFeedService; import bisq.core.support.SupportManager; +import bisq.core.support.dispute.mediation.MediationResultState; import bisq.core.support.dispute.messages.DisputeResultMessage; import bisq.core.support.dispute.messages.OpenNewDisputeMessage; import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; @@ -254,6 +255,7 @@ public void onAllServicesInitialized() { @Override public void onUpdatedDataReceived() { tryApplyMessages(); + checkDisputesForUpdates(); } }); @@ -263,8 +265,9 @@ public void onUpdatedDataReceived() { }); walletsSetup.numPeersProperty().addListener((observable, oldValue, newValue) -> { - if (walletsSetup.hasSufficientPeersForBroadcast()) + if (walletsSetup.hasSufficientPeersForBroadcast()) { tryApplyMessages(); + } }); tryApplyMessages(); @@ -292,6 +295,46 @@ public void onUpdatedDataReceived() { maybeClearSensitiveData(); } + private void checkDisputesForUpdates() { + List disputes = getDisputeList().getList(); + disputes.forEach(dispute -> { + if (dispute.isResultProposed()) { + // an open dispute where the mediator has proposed a result. has the trade moved on? + // if so, dispute can close and the mediator needs to be informed so they can close their ticket. + tradeManager.getTradeById(dispute.getTradeId()).ifPresentOrElse( + t -> checkForMediatedTradePayout(t, dispute), + () -> closedTradableManager.getTradableById(dispute.getTradeId()).ifPresent( + t -> checkForMediatedTradePayout((Trade) t, dispute))); + } + }); + } + + protected void checkForMediatedTradePayout(Trade trade, Dispute dispute) { + if (trade.disputeStateProperty().get().isArbitrated() || trade.getTradePhase() == Trade.Phase.PAYOUT_PUBLISHED) { + disputedTradeUpdate(trade.getDisputeState().toString(), dispute, true); + } else { + // user accepted/rejected mediation proposal (before lockup period has expired) + trade.mediationResultStateProperty().addListener((observable, oldValue, newValue) -> { + if (newValue == MediationResultState.MEDIATION_RESULT_ACCEPTED || + newValue == MediationResultState.MEDIATION_RESULT_REJECTED) { + disputedTradeUpdate(newValue.toString(), dispute, false); + } + }); + // user rejected mediation after lockup period: opening arbitration + trade.disputeStateProperty().addListener((observable, oldValue, newValue) -> { + if (newValue.isArbitrated()) { + disputedTradeUpdate(newValue.toString(), dispute, true); + } + }); + // trade paid out through mediation + trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> { + if (newValue == Trade.Phase.PAYOUT_PUBLISHED) { + disputedTradeUpdate(newValue.toString(), dispute, true); + } + }); + } + } + public boolean isTrader(Dispute dispute) { return pubKeyRing.equals(dispute.getTraderPubKeyRing()); } @@ -650,8 +693,6 @@ private void doSendPeerOpenedDisputeMessage(Dispute disputeFromOpener, chatMessage.setSystemMessage(true); dispute.addAndPersistChatMessage(chatMessage); - addPriceInfoMessage(dispute, 0); - disputeList.add(dispute); // We mirrored dispute already! @@ -719,6 +760,7 @@ public void onFault(String errorMessage) { } } ); + addPriceInfoMessage(dispute, 0); requestPersistence(); } @@ -897,17 +939,23 @@ private void addMediationResultMessage(Dispute dispute) { } } - public void addMediationReOpenedMessage(Dispute dispute, boolean senderIsTrader) { + // when a mediated trade changes, send a system message informing the mediator, so they can maybe close their ticket. + public void disputedTradeUpdate(String message, Dispute dispute, boolean close) { + if (dispute.isClosed()) { + return; + } ChatMessage chatMessage = new ChatMessage( getSupportType(), dispute.getTradeId(), dispute.getTraderId(), - senderIsTrader, - Res.get("support.info.disputeReOpened"), + true, + Res.get("support.info.disputedTradeUpdate", message), p2PService.getAddress()); - chatMessage.setSystemMessage(false); - dispute.addAndPersistChatMessage(chatMessage); - this.sendChatMessage(chatMessage); + chatMessage.setSystemMessage(true); + this.sendChatMessage(chatMessage); // inform the mediator + if (close) { + dispute.setIsClosed(); // close the trader's ticket + } requestPersistence(); } 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 8959413c821..2bb84b8c685 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeResult.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeResult.java @@ -288,6 +288,47 @@ public String getPayoutSuggestionText() { return payoutSuggestion.toString(); } + public String getPayoutSuggestionCustomizedToBuyerOrSeller(boolean isBuyer) { + // see github.com/bisq-network/proposals/issues/407 + if (isBuyer) { + switch (payoutSuggestion) { + case BUYER_GETS_TRADE_AMOUNT: + return Res.get("disputeSummaryWindow.result.buyerGetsTradeAmount"); + case BUYER_GETS_TRADE_AMOUNT_MINUS_PENALTY: + return Res.get("disputeSummaryWindow.result.buyerGetsTradeAmountMinusPenalty"); + case BUYER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION: + return Res.get("disputeSummaryWindow.result.buyerGetsTradeAmountPlusCompensation"); + case SELLER_GETS_TRADE_AMOUNT: + return Res.get("disputeSummaryWindow.result.buyerGetsHisDeposit"); + case SELLER_GETS_TRADE_AMOUNT_MINUS_PENALTY: + return Res.get("disputeSummaryWindow.result.buyerGetsHisDepositPlusPenaltyFromSeller"); + case SELLER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION: + return Res.get("disputeSummaryWindow.result.buyerGetsHisDepositMinusPenalty"); + case CUSTOM_PAYOUT: + return Res.get("disputeSummaryWindow.result.customPayout"); + default: + } + } else { + switch (payoutSuggestion) { + case SELLER_GETS_TRADE_AMOUNT: + return Res.get("disputeSummaryWindow.result.sellerGetsTradeAmount"); + case SELLER_GETS_TRADE_AMOUNT_MINUS_PENALTY: + return Res.get("disputeSummaryWindow.result.sellerGetsTradeAmountMinusPenalty"); + case SELLER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION: + return Res.get("disputeSummaryWindow.result.sellerGetsTradeAmountPlusCompensation"); + case BUYER_GETS_TRADE_AMOUNT: + return Res.get("disputeSummaryWindow.result.sellerGetsHisDeposit"); + case BUYER_GETS_TRADE_AMOUNT_MINUS_PENALTY: + return Res.get("disputeSummaryWindow.result.sellerGetsHisDepositPlusPenaltyFromBuyer"); + case BUYER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION: + return Res.get("disputeSummaryWindow.result.sellerGetsHisDepositMinusPenalty"); + case CUSTOM_PAYOUT: + return Res.get("disputeSummaryWindow.result.customPayout"); + default: + } + } + return Res.get("popup.headline.error"); + } @Override public String toString() { 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 f7be4ce4f79..8e640588925 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 @@ -141,8 +141,12 @@ protected AckMessageSourceType getAckMessageSourceType() { @Override public void cleanupDisputes() { + // closes any trades/disputes which paid out while Bisq was not in use disputeListService.cleanupDisputes(tradeId -> tradeManager.getTradeById(tradeId).filter(trade -> trade.getPayoutTx() != null) - .ifPresent(trade -> tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED))); + .ifPresent(trade -> { + tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED); + findOwnDispute(tradeId).ifPresent(Dispute::setIsClosed); + })); } @Override @@ -201,7 +205,7 @@ public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { } else { log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId()); } - dispute.setIsClosed(); + dispute.setState(Dispute.State.RESULT_PROPOSED); dispute.setDisputeResult(disputeResult); @@ -216,6 +220,7 @@ public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { trade.setDisputeState(Trade.DisputeState.MEDIATION_CLOSED); tradeManager.requestPersistence(); + checkForMediatedTradePayout(trade, dispute); } } else { Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java index 346a0dc5b20..494ec90fad7 100644 --- a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java @@ -158,7 +158,7 @@ protected String getDisputeInfo(Dispute dispute) { String roleContextMsg = Res.get("support.initialArbitratorMsg", DisputeAgentLookupMap.getMatrixLinkForAgent(getAgentNodeAddress(dispute).getFullAddress())); String link = "https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration"; - return Res.get("support.initialInfo", role, roleContextMsg, role, link); + return Res.get("support.initialInfoRefundAgent", role, roleContextMsg, role, link); } @Override diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index f41c0c93798..cb6eb8726bd 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -977,8 +977,9 @@ portfolio.pending.mediationResult.popup.info=The mediator has suggested the foll You can accept or reject this suggested payout.\n\n\ By accepting, you sign the proposed payout transaction. \ Mediation is expected to be the optimal resolution for both traders. \ - If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\n\ - If one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to reject mediation suggestion, \ + If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed. \ + Please inform the mediator if the trade is not paid out in the next 48h.\n\n\ + If agreement is not possible, you will have to wait until {2} (block {3}) to Send to Arbitration,\ which will open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\n\ If the trade goes to arbitration the arbitrator will pay out the trade amount plus one peer's security deposit. \ This means the total arbitration payout will be less than the mediation payout. \ @@ -986,12 +987,14 @@ portfolio.pending.mediationResult.popup.info=The mediator has suggested the foll one peer not responding, or disputing the mediator made a fair payout suggestion. \n\n\ More details about the arbitration model: [HYPERLINK:https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration] portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout \ - but it seems that your trading peer has not accepted it.\n\n\ + but it seems that your trading peer has not yet accepted it. \ + Inform your mediator that you have accepted mediation if your peer has not accepted the mediation suggestion in 48h.\n\n\ Once the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will \ investigate the case again and do a payout based on their findings.\n\n\ - You can find more details about the arbitration model at:\ + You can find more details about the arbitration model at: \ [HYPERLINK:https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration] -portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration +portfolio.pending.mediationResult.popup.reject=Reject +portfolio.pending.mediationResult.popup.openArbitration=Send to arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\n\ @@ -1210,7 +1213,7 @@ support.save=Save file to disk support.messages=Messages support.input.prompt=Enter message... support.send=Send -support.addAttachments=Add attachments +support.addAttachments=Attach Files support.closeTicket=Close ticket support.attachments=Attachments: support.savedInMailbox=Message saved in receiver's mailbox @@ -1276,6 +1279,20 @@ support.initialInfo=Please enter a description of your problem in the text field \t● You need to cooperate with the {2} and provide the information they request to make your case.\n\ \t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\n\ You can read more about the dispute process at: {3} +support.initialInfoRefundAgent=Please describe why have you opened arbitration, or why do you think your peer did so. \ + Add as much information as possible to speed up dispute resolution time. Mediation and trading chats are not shared with the arbitrator.\n\n\ + Here is a check list for information you should provide:\n\ + \t● If you are the BTC buyer: Did you make the Fiat or Altcoin transfer? If so, did you click the 'payment started' \ + button in the application? Did you accept mediator's suggestion?\n\ + \t● If you are the BTC seller: Did you receive the Fiat or Altcoin payment? If so, did you click the 'payment received' \ + button in the application? Did you accept mediator's suggestion?\n\ + Please make yourself familiar with the basic rules for the dispute process:\n\ +\t● You need to respond to the {0}''s requests within 2 days.\n\ +\t● {1}\n\ +\t● The maximum period for a dispute is 14 days.\n\ +\t● You need to cooperate with the {2} and provide the information they request to make your case.\n\ +\t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\n\ +You can read more about the dispute process at: {3} support.initialMediatorMsg=Mediators will generally reply to you within 24 hours.\n\ \t If you have not had a reply after 48 hours please feel free to reach out to your mediator on Matrix.\n\ \t Mediators usernames on Matrix are the same as their usernames within the Bisq app.\n\ @@ -1304,7 +1321,7 @@ support.warning.disputesWithInvalidDonationAddress=The delayed payout transactio support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out. -support.info.disputeReOpened=Dispute ticket has been re-opened. +support.info.disputedTradeUpdate=Disputed trade update: {0} #################################################################### # Settings @@ -2856,10 +2873,22 @@ disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late # suppress inspection "UnusedProperty" disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled - +disputeSummaryWindow.result.buyerGetsTradeAmount=(BTC buyer receives trade amount + his deposit) +disputeSummaryWindow.result.buyerGetsTradeAmountMinusPenalty=(BTC buyer receives trade amount + his deposit - a penalty) +disputeSummaryWindow.result.buyerGetsTradeAmountPlusCompensation=(BTC buyer receives trade amount + his deposit + compensation from seller) +disputeSummaryWindow.result.buyerGetsHisDeposit=(BTC buyer receives his deposit) +disputeSummaryWindow.result.buyerGetsHisDepositPlusPenaltyFromSeller=(BTC buyer receives his deposit + compensation from seller) +disputeSummaryWindow.result.buyerGetsHisDepositMinusPenalty=(BTC buyer receives a penalty on his deposit) +disputeSummaryWindow.result.sellerGetsTradeAmount=(BTC seller receives trade amount + his deposit) +disputeSummaryWindow.result.sellerGetsTradeAmountMinusPenalty=(BTC seller receives trade amount + his deposit - a penalty) +disputeSummaryWindow.result.sellerGetsTradeAmountPlusCompensation=(BTC seller receives trade amount + his deposit + compensation from buyer) +disputeSummaryWindow.result.sellerGetsHisDeposit=(BTC seller receives his deposit) +disputeSummaryWindow.result.sellerGetsHisDepositPlusPenaltyFromBuyer=(BTC seller receives his deposit + compensation from buyer) +disputeSummaryWindow.result.sellerGetsHisDepositMinusPenalty=(BTC seller receives a penalty on his deposit) +disputeSummaryWindow.result.customPayout=(a custom payout) disputeSummaryWindow.summaryNotes=Summary notes disputeSummaryWindow.addSummaryNotes=Add summary notes -disputeSummaryWindow.close.button=Close ticket +disputeSummaryWindow.close.button=Apply # Do no change any line break or order of tokens as the structure is used for signature verification # suppress inspection "TrailingSpacesInProperty" @@ -2879,7 +2908,11 @@ disputeSummaryWindow.close.msg=Ticket closed on {0}\n\ disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\n\ -Open trade and accept or reject suggestion from mediator + Go to Open trades to Accept the mediation proposal and wait for your peer's acceptance, if necessary.\n\ + Click Reject if you disagree and explain your reasons on the mediation ticket.\n\ + If the trade has not been paid out in the next 48h, please inform your mediator on this ticket.\n\ + Keep in mind that the arbitrator can only make the payout of the trade amount + 1 security deposit. The other security \ + deposit will be left unpaid. disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\n\ No further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions disputeSummaryWindow.close.closePeer=You need to close also the trading peers ticket! @@ -3318,6 +3351,7 @@ notification.trade.confirmed=Your trade has at least one blockchain confirmation notification.trade.paymentStarted=The BTC buyer has started the payment. notification.trade.selectTrade=Select trade notification.trade.peerOpenedDispute=Your trading peer has opened a {0}. +notification.trade.disputeResolved=A proposed solution has been issued for the {0}. notification.trade.disputeClosed=The {0} has been closed. notification.walletUpdate.headline=Trading wallet update notification.walletUpdate.msg=Your trading wallet is sufficiently funded.\nAmount: {0} diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java b/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java index 77796da6b65..b7fb94f8a04 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java @@ -163,6 +163,7 @@ protected enum Type { protected boolean hideCloseButton; protected boolean isDisplayed; protected boolean disableActionButton; + protected boolean disableTertiaryActionButton; @Getter protected BooleanProperty isHiddenProperty = new SimpleBooleanProperty(); @@ -176,11 +177,11 @@ protected enum Type { protected Label headlineIcon, copyIcon, headLineLabel, messageLabel; protected String headLine, message, closeButtonText, actionButtonText, - secondaryActionButtonText, dontShowAgainId, dontShowAgainText, + secondaryActionButtonText, tertiaryActionButtonText, dontShowAgainId, dontShowAgainText, truncatedMessage; private ArrayList messageHyperlinks; private String headlineStyle; - protected Button actionButton, secondaryActionButton; + protected Button actionButton, secondaryActionButton, tertiaryActionButton; private HBox buttonBox; protected AutoTooltipButton closeButton; @@ -189,6 +190,7 @@ protected enum Type { protected Optional closeHandlerOptional = Optional.empty(); protected Optional actionHandlerOptional = Optional.empty(); protected Optional secondaryActionHandlerOptional = Optional.empty(); + protected Optional tertiaryActionHandlerOptional = Optional.empty(); protected ChangeListener positionListener; protected Timer centerTime; @@ -301,6 +303,11 @@ public T onSecondaryAction(Runnable secondaryActionHandlerOptional) { return cast(); } + public T onTertiaryAction(Runnable actionHandlerOptional) { + this.tertiaryActionHandlerOptional = Optional.of(actionHandlerOptional); + return cast(); + } + public T headLine(String headLine) { this.headLine = headLine; return cast(); @@ -433,6 +440,11 @@ public T secondaryActionButtonText(String secondaryActionButtonText) { return cast(); } + public T tertiaryActionButtonText(String text) { + this.tertiaryActionButtonText = text; + return cast(); + } + public T useShutDownButton() { this.actionButtonText = Res.get("shared.shutDown"); this.actionHandlerOptional = Optional.ofNullable(BisqApp.getShutDownHandler()); @@ -489,6 +501,10 @@ public T disableActionButton() { return cast(); } + public T setTertiaryButtonDisabledState(boolean disableState) { + this.disableTertiaryActionButton = disableState; + return cast(); + } /////////////////////////////////////////////////////////////////////////////////////////// // Protected @@ -1006,6 +1022,17 @@ protected void addButtons() { buttonBox.getChildren().add(secondaryActionButton); } + if (tertiaryActionButtonText != null && tertiaryActionHandlerOptional.isPresent()) { + tertiaryActionButton = new AutoTooltipButton(tertiaryActionButtonText); + tertiaryActionButton.setOnAction(event -> { + hide(); + tertiaryActionHandlerOptional.ifPresent(Runnable::run); + }); + + buttonBox.getChildren().add(tertiaryActionButton); + tertiaryActionButton.setDisable(disableTertiaryActionButton); + } + if (!hideCloseButton) buttonBox.getChildren().add(closeButton); } else if (!hideCloseButton) { diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java b/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java index b86f87a82da..7b162550544 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java @@ -265,7 +265,7 @@ private void onDisputeStateChanged(Trade trade, Trade.DisputeState disputeState) message = Res.get("notification.trade.peerOpenedDispute", disputeOrTicket); break; case MEDIATION_CLOSED: - message = Res.get("notification.trade.disputeClosed", disputeOrTicket); + message = Res.get("notification.trade.disputeResolved", disputeOrTicket); break; default: // if (DevEnv.isDevMode()) { 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 56e59948c53..b7a6b54423c 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 @@ -240,8 +240,9 @@ private void addContent() { addPayoutAmountTextFields(); addReasonControls(); applyDisputeResultToUiControls(); - - boolean applyPeersDisputeResult = peersDisputeOptional.isPresent() && peersDisputeOptional.get().isClosed(); + boolean applyPeersDisputeResult = peersDisputeOptional.isPresent() && ( + peersDisputeOptional.get().getDisputeState() == Dispute.State.RESULT_PROPOSED || + peersDisputeOptional.get().getDisputeState() == Dispute.State.CLOSED); if (applyPeersDisputeResult) { // If the other peers dispute has been closed we apply the result to ourselves DisputeResult peersDisputeResult = peersDisputeOptional.get().getDisputeResultProperty().get(); @@ -697,7 +698,7 @@ protected void addButtons() { } if (peersDisputeOptional.isPresent() && peersDisputeOptional.get().isClosed()) { - closeTicket(closeTicketButton); // all checks done already on peers ticket + applyDisputeResult(closeTicketButton); // all checks done already on peers ticket } else { maybeCheckTransactions().thenAccept(continue1 -> { if (continue1) { @@ -705,7 +706,7 @@ protected void addButtons() { if (continue2) { maybeMakePayout().thenAccept(continue3 -> { if (continue3) { - closeTicket(closeTicketButton); + applyDisputeResult(closeTicketButton); } }); } @@ -964,17 +965,16 @@ private CompletableFuture checkGeneralValidity() { return asyncStatus; } - private void closeTicket(Button closeTicketButton) { + private void applyDisputeResult(Button closeTicketButton) { DisputeManager> disputeManager = getDisputeManager(dispute); if (disputeManager == null) { return; } - boolean isRefundAgent = disputeManager instanceof RefundManager; disputeResult.setLoserPublisher(false); // field no longer used per pazza / leo816 disputeResult.setCloseDate(new Date()); dispute.setDisputeResult(disputeResult); - dispute.setIsClosed(); + dispute.setState(isRefundAgent ? Dispute.State.CLOSED : Dispute.State.RESULT_PROPOSED); DisputeResult.Reason reason = disputeResult.getReason(); summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty()); @@ -992,8 +992,10 @@ private void closeTicket(Button closeTicketButton) { Res.get("disputeSummaryWindow.reason." + reason.name()), disputeResult.getPayoutSuggestionText(), amount, - formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()), - formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()), + formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()) + + (isRefundAgent ? "" : " " + disputeResult.getPayoutSuggestionCustomizedToBuyerOrSeller(true)), + formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()) + + (isRefundAgent ? "" : " " + disputeResult.getPayoutSuggestionCustomizedToBuyerOrSeller(false)), disputeResult.summaryNotesProperty().get() ); @@ -1013,12 +1015,14 @@ private void closeTicket(Button closeTicketButton) { disputeManager.sendDisputeResultMessage(disputeResult, dispute, summaryText); - if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) { - UserThread.runAfter(() -> new Popup() - .attention(Res.get("disputeSummaryWindow.close.closePeer")) - .show(), - 200, TimeUnit.MILLISECONDS); - } + peersDisputeOptional.ifPresent(peersDispute -> { + if (!peersDispute.isResultProposed() && !peersDispute.isClosed()) { + UserThread.runAfter(() -> new Popup() + .attention(Res.get("disputeSummaryWindow.close.closePeer")) + .show(), + 200, TimeUnit.MILLISECONDS); + } + }); finalizeDisputeHandlerOptional.ifPresent(Runnable::run); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java index 6ad032edd79..8979eaaaefe 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java @@ -644,33 +644,41 @@ private void openMediationResultPopup(String headLine) { .headLine(headLine) .instruction(message) .actionButtonText(actionButtonText) - .onAction(() -> { - model.dataModel.mediationManager.onAcceptMediationResult(trade, - () -> { - log.info("onAcceptMediationResult completed"); - acceptMediationResultPopup = null; - }, - errorMessage -> { - UserThread.execute(() -> { - new Popup().error(errorMessage).show(); - if (acceptMediationResultPopup != null) { - acceptMediationResultPopup.hide(); - acceptMediationResultPopup = null; - } - }); - }); - }) - .secondaryActionButtonText(Res.get("portfolio.pending.mediationResult.popup.openArbitration")) - .onSecondaryAction(() -> { - model.dataModel.mediationManager.rejectMediationResult(trade); - model.dataModel.onOpenDispute(); - acceptMediationResultPopup = null; - }) - .onClose(() -> { + .onAction(this::acceptProposal) + .secondaryActionButtonText(Res.get("portfolio.pending.mediationResult.popup.reject")) + .onSecondaryAction(this::rejectProposal) + .tertiaryActionButtonText(Res.get("portfolio.pending.mediationResult.popup.openArbitration")) + .onTertiaryAction(this::startArbitration) + .setTertiaryButtonDisabledState(remaining > 0) + .onClose(() -> acceptMediationResultPopup = null); + acceptMediationResultPopup.show(); + } + + private void acceptProposal() { + model.dataModel.mediationManager.onAcceptMediationResult(trade, + () -> { + log.info("onAcceptMediationResult completed"); acceptMediationResultPopup = null; + }, + errorMessage -> { + UserThread.execute(() -> { + new Popup().error(errorMessage).show(); + if (acceptMediationResultPopup != null) { + acceptMediationResultPopup.hide(); + acceptMediationResultPopup = null; + } + }); }); + } - acceptMediationResultPopup.show(); + private void rejectProposal() { + model.dataModel.mediationManager.rejectMediationResult(trade); + acceptMediationResultPopup = null; + } + + private void startArbitration() { + model.dataModel.onOpenDispute(); + acceptMediationResultPopup = null; } protected String getCurrencyName(Trade trade) { 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 d2ed8071af7..a9bd065a9d8 100644 --- a/desktop/src/main/java/bisq/desktop/main/shared/ChatView.java +++ b/desktop/src/main/java/bisq/desktop/main/shared/ChatView.java @@ -22,6 +22,7 @@ import bisq.desktop.components.BisqTextArea; import bisq.desktop.components.BusyAnimation; import bisq.desktop.components.TableGroupHeadline; +import bisq.desktop.main.overlays.notifications.Notification; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.GUIUtil; @@ -214,6 +215,10 @@ public void display(SupportSession supportSession, Button uploadButton = new AutoTooltipButton(Res.get("support.addAttachments")); uploadButton.setOnAction(e -> onRequestUpload()); + Button clipboardButton = new AutoTooltipButton(Res.get("shared.copyToClipboard")); + clipboardButton.setOnAction(e -> copyChatMessagesToClipboard(clipboardButton)); + uploadButton.setStyle("-fx-pref-width: 66; -fx-padding: 3 3 3 3;"); + clipboardButton.setStyle("-fx-pref-width: 50; -fx-padding: 3 3 3 3;"); sendMsgInfoLabel = new AutoTooltipLabel(); sendMsgInfoLabel.setVisible(false); @@ -229,7 +234,7 @@ public void display(SupportSession supportSession, HBox buttonBox = new HBox(); buttonBox.setSpacing(10); if (allowAttachments) - buttonBox.getChildren().addAll(sendButton, uploadButton, sendMsgBusyAnimation, sendMsgInfoLabel); + buttonBox.getChildren().addAll(sendButton, uploadButton, clipboardButton, sendMsgBusyAnimation, sendMsgInfoLabel); else buttonBox.getChildren().addAll(sendButton, sendMsgBusyAnimation, sendMsgInfoLabel); @@ -591,6 +596,24 @@ public void onAttachText(String textAttachment, String name) { } } + private void copyChatMessagesToClipboard(Button sourceBtn) { + optionalSupportSession.ifPresent(session -> { + StringBuilder stringBuilder = new StringBuilder(); + chatMessages.forEach(i -> { + String metaData = DisplayUtils.formatDateTime(new Date(i.getDate())); + metaData = metaData + (i.isSystemMessage() ? " (System message)" : + (i.isSenderIsTrader() ? " (from Trader)" : " (from Agent)")); + stringBuilder.append(metaData).append("\n").append(i.getMessage()).append("\n\n"); + }); + Utilities.copyToClipboard(stringBuilder.toString()); + new Notification() + .notification(Res.get("shared.copiedToClipboard")) + .hideCloseButton() + .autoClose() + .show(); + }); + } + private void onOpenAttachment(Attachment attachment) { if (!allowAttachments) return; 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 f0d8a81fed5..dfd602c8408 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 @@ -94,8 +94,8 @@ import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; -import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; @@ -181,7 +181,7 @@ public enum FilterResult { protected FilteredList filteredList; protected InputTextField filterTextField; private ChangeListener filterTextFieldListener; - protected AutoTooltipButton sigCheckButton, reOpenButton, closeButton, sendPrivateNotificationButton, reportButton, fullReportButton; + protected AutoTooltipButton sigCheckButton, openOrCloseButton, sendPrivateNotificationButton, reportButton, fullReportButton; private final Map> disputeChatMessagesListeners = new HashMap<>(); @Nullable private ListChangeListener disputesListener; // Only set in mediation cases @@ -256,22 +256,17 @@ public void initialize() { alertIconLabel.setVisible(false); alertIconLabel.setManaged(false); - reOpenButton = new AutoTooltipButton(Res.get("support.reOpenButton.label")); - reOpenButton.setDisable(true); - reOpenButton.setVisible(false); - reOpenButton.setManaged(false); - HBox.setHgrow(reOpenButton, Priority.NEVER); - reOpenButton.setOnAction(e -> { - reOpenDisputeFromButton(); - }); - - closeButton = new AutoTooltipButton(Res.get("support.closeTicket")); - closeButton.setDisable(true); - closeButton.setVisible(false); - closeButton.setManaged(false); - HBox.setHgrow(closeButton, Priority.NEVER); - closeButton.setOnAction(e -> { - closeDisputeFromButton(); + openOrCloseButton = new AutoTooltipButton(Res.get("support.reOpenButton.label")); + openOrCloseButton.setDisable(true); + openOrCloseButton.setVisible(false); + openOrCloseButton.setManaged(false); + HBox.setHgrow(openOrCloseButton, Priority.NEVER); + openOrCloseButton.setOnAction(e -> { + if (openOrCloseButton.getText().equalsIgnoreCase(Res.get("support.closeTicket"))) { + closeDisputeFromButton(); + } else { + reOpenDisputeFromButton(); + } }); sendPrivateNotificationButton = new AutoTooltipButton(Res.get("support.sendNotificationButton.label")); @@ -314,8 +309,7 @@ public void initialize() { filterTextField, alertIconLabel, spacer, - reOpenButton, - closeButton, + openOrCloseButton, sendPrivateNotificationButton, reportButton, fullReportButton, @@ -449,7 +443,8 @@ protected FilterResult getFilterResult(Dispute dispute, String filterTerm) { // For open filter we do not want to continue further as json data would cause a match if (filter.equalsIgnoreCase("open")) { - return !dispute.isClosed() ? FilterResult.OPEN_DISPUTES : FilterResult.NO_MATCH; + return !dispute.isClosed() || dispute.unreadMessageCount(senderFlag()) > 0 ? + FilterResult.OPEN_DISPUTES : FilterResult.NO_MATCH; } if (dispute.getTradeId().toLowerCase().contains(filter)) { @@ -509,18 +504,14 @@ protected FilterResult getFilterResult(Dispute dispute, String filterTerm) { } // a derived version in the ClientView for users pops up an "Are you sure" box first. - // this version includes the sending of an automatic message to the user, see addMediationReOpenedMessage protected void reOpenDisputeFromButton() { - if (reOpenDispute()) { - disputeManager.addMediationReOpenedMessage(selectedDispute, false); - } + reOpenDispute(); } - // only applicable to traders - // only allow them to close the dispute if the trade is paid out + // traders -> only allow them to close the dispute if the trade is paid out // the reason for having this is that sometimes traders end up with closed disputes that are not "closed" @pazza protected void closeDisputeFromButton() { - disputeManager.findTrade(selectedDispute).ifPresent( + disputeManager.findTrade(selectedDispute).ifPresentOrElse( (trade) -> { if (trade.isFundsLockedIn()) { new Popup().warning(Res.get("support.warning.traderCloseOwnDisputeWarning")).show(); @@ -529,6 +520,11 @@ protected void closeDisputeFromButton() { disputeManager.requestPersistence(); onSelectDispute(selectedDispute); } + }, + () -> { + selectedDispute.setIsClosed(); + disputeManager.requestPersistence(); + onSelectDispute(selectedDispute); }); } @@ -542,7 +538,6 @@ protected boolean reOpenDispute() { isNodeAddressOk(selectedDispute, !disputeManager.isTrader(selectedDispute))) { selectedDispute.reOpen(); - handleOnProcessDispute(selectedDispute); disputeManager.requestPersistence(); onSelectDispute(selectedDispute); return true; @@ -580,9 +575,15 @@ private void onSelectDispute(Dispute dispute) { selectedDispute = dispute; } - reOpenButton.setDisable(selectedDispute == null || !selectedDispute.isClosed()); - closeButton.setDisable(selectedDispute == null || selectedDispute.isClosed()); - sendPrivateNotificationButton.setDisable(selectedDispute == null); + if (selectedDispute == null) { + openOrCloseButton.setDisable(true); + sendPrivateNotificationButton.setDisable(true); + } else { + openOrCloseButton.setDisable(false); + sendPrivateNotificationButton.setDisable(false); + openOrCloseButton.setText(selectedDispute.isClosed() ? + Res.get("support.reOpenButton.label").toUpperCase() : Res.get("support.closeTicket").toUpperCase()); + } } @@ -1140,7 +1141,8 @@ public void updateItem(final Dispute item, boolean empty) { private TableColumn getDateColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.date")) { { - setMinWidth(180); + setMinWidth(100); + setPrefWidth(150); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); @@ -1166,7 +1168,8 @@ public void updateItem(final Dispute item, boolean empty) { private TableColumn getTradeIdColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.tradeId")) { { - setMinWidth(110); + setMinWidth(50); + setPrefWidth(100); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); @@ -1303,7 +1306,7 @@ private String getSellerOnionAddressColumnLabel(Dispute item) { private TableColumn getMarketColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.market")) { { - setMinWidth(80); + setMinWidth(60); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); @@ -1329,7 +1332,7 @@ public void updateItem(final Dispute item, boolean empty) { private TableColumn getRoleColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.role")) { { - setMinWidth(130); + setMinWidth(80); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); @@ -1390,7 +1393,8 @@ public void updateItem(final Dispute item, boolean empty) { private TableColumn getStateColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.state")) { { - setMinWidth(50); + setMinWidth(75); + setPrefWidth(100); } }; column.getStyleClass().add("last-column"); @@ -1402,8 +1406,8 @@ public TableCell call(TableColumn column) { return new TableCell<>() { - ReadOnlyBooleanProperty closedProperty; - ChangeListener listener; + ReadOnlyStringProperty closedProperty; + ChangeListener listener; @Override public void updateItem(final Dispute item, boolean empty) { @@ -1414,16 +1418,16 @@ public void updateItem(final Dispute item, boolean empty) { } listener = (observable, oldValue, newValue) -> { - setText(newValue ? Res.get("support.closed") : Res.get("support.open")); + setText(newValue); if (getTableRow() != null) - getTableRow().setOpacity(newValue && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1); + getTableRow().setOpacity(newValue.equalsIgnoreCase("CLOSED") && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1); if (item.isClosed() && item == chatPopup.getSelectedDispute()) chatPopup.closeChat(); // close the chat popup when the associated ticket is closed }; - closedProperty = item.isClosedProperty(); + closedProperty = item.getDisputeStateProperty(); closedProperty.addListener(listener); boolean isClosed = item.isClosed(); - setText(isClosed ? Res.get("support.closed") : Res.get("support.open")); + setText(closedProperty.get()); if (getTableRow() != null) getTableRow().setOpacity(isClosed && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1); } else { @@ -1496,7 +1500,11 @@ private PeerInfoIconDispute createAvatar(Integer tableRowId, Contract contract, @Override public void onCloseDisputeFromChatWindow(Dispute dispute) { - handleOnProcessDispute(dispute); + if (dispute.getDisputeState() == Dispute.State.NEW || dispute.getDisputeState() == Dispute.State.OPEN) { + handleOnProcessDispute(dispute); + } else { + closeDisputeFromButton(); + } } @Override 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 62217a98d2c..7a398bfc07f 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 @@ -257,7 +257,8 @@ private void suspiciousDisputeDetected() { private TableColumn getAlertColumn() { TableColumn column = new AutoTooltipTableColumn<>("Alert") { { - setMinWidth(50); + setMinWidth(20); + setPrefWidth(20); } }; column.getStyleClass().add("last-column"); 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 9892ee7d73e..f82852a8b97 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 @@ -85,8 +85,8 @@ public MediatorView(MediationManager mediationManager, @Override public void initialize() { super.initialize(); - reOpenButton.setVisible(true); - reOpenButton.setManaged(true); + openOrCloseButton.setVisible(true); + openOrCloseButton.setManaged(true); setupReOpenDisputeListener(); } 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 f967623e833..9c6db9432fd 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 @@ -76,10 +76,8 @@ public MediationClientView(MediationManager mediationManager, @Override public void initialize() { super.initialize(); - reOpenButton.setVisible(true); - reOpenButton.setManaged(true); - closeButton.setVisible(true); - closeButton.setManaged(true); + openOrCloseButton.setVisible(true); + openOrCloseButton.setManaged(true); setupReOpenDisputeListener(); } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 3e0a3471a80..d56eebae434 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -909,6 +909,7 @@ message Dispute { OPEN = 2; REOPENED = 3; CLOSED = 4; + RESULT_PROPOSED = 5; } string trade_id = 1; string id = 2;