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 ca4322f3e16..6f201a0aa30 100644 --- a/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java +++ b/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java @@ -123,7 +123,7 @@ private void onChatMessage(ChatMessage chatMessage, Dispute dispute) { // 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); + dispute.reOpen(); if (dispute.getSupportType() == SupportType.MEDIATION) { mediationManager.requestPersistence(); } else if (dispute.getSupportType() == SupportType.REFUND) { 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 a9ac1ae1293..7edb0bb5c43 100644 --- a/core/src/main/java/bisq/core/support/dispute/Dispute.java +++ b/core/src/main/java/bisq/core/support/dispute/Dispute.java @@ -17,6 +17,7 @@ package bisq.core.support.dispute; +import bisq.core.locale.Res; import bisq.core.proto.CoreProtoResolver; import bisq.core.support.SupportType; import bisq.core.support.messages.ChatMessage; @@ -26,15 +27,20 @@ import bisq.common.proto.ProtoUtil; import bisq.common.proto.network.NetworkPayload; import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.CollectionUtils; +import bisq.common.util.ExtraDataMapValidator; import bisq.common.util.Utilities; import com.google.protobuf.ByteString; 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.collections.FXCollections; @@ -42,7 +48,9 @@ import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @@ -58,6 +66,23 @@ @EqualsAndHashCode @Getter public final class Dispute implements NetworkPayload, PersistablePayload { + + public enum State { + NEEDS_UPGRADE, + NEW, + OPEN, + REOPENED, + CLOSED; + + public static Dispute.State fromProto(protobuf.Dispute.State state) { + return ProtoUtil.enumFromProto(Dispute.State.class, state.name()); + } + + public static protobuf.Dispute.State toProtoMessage(Dispute.State state) { + return protobuf.Dispute.State.valueOf(state.name()); + } + } + private final String tradeId; private final String id; private final int traderId; @@ -66,6 +91,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload { // PubKeyRing of trader who opened the dispute private final PubKeyRing traderPubKeyRing; private final long tradeDate; + private final long tradePeriodEnd; private final Contract contract; @Nullable private final byte[] contractHash; @@ -85,7 +111,6 @@ public final class Dispute implements NetworkPayload, PersistablePayload { private final PubKeyRing agentPubKeyRing; // dispute agent private final boolean isSupportTicket; private final ObservableList chatMessages = FXCollections.observableArrayList(); - private final BooleanProperty isClosedProperty = new SimpleBooleanProperty(); // disputeResultProperty.get is Nullable! private final ObjectProperty disputeResultProperty = new SimpleObjectProperty<>(); private final long openingDate; @@ -107,10 +132,25 @@ public final class Dispute implements NetworkPayload, PersistablePayload { @Setter @Nullable private String donationAddressOfDelayedPayoutTx; + // Added at v1.6.0 + private Dispute.State disputeState = State.NEW; + + // Should be only used in emergency case if we need to add data but do not want to break backward compatibility + // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new + // field in a class would break that hash and therefore break the storage mechanism. + @Nullable + @Setter + private Map extraDataMap; + // We do not persist uid, it is only used by dispute agents to guarantee an uid. @Setter @Nullable private transient String uid; + @Setter + private transient long payoutTxConfirms = -1; + + private transient final BooleanProperty isClosedProperty = new SimpleBooleanProperty(); + private transient final IntegerProperty badgeCountProperty = new SimpleIntegerProperty(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -124,6 +164,7 @@ public Dispute(long openingDate, boolean disputeOpenerIsMaker, PubKeyRing traderPubKeyRing, long tradeDate, + long tradePeriodEnd, Contract contract, @Nullable byte[] contractHash, @Nullable byte[] depositTxSerialized, @@ -143,6 +184,7 @@ public Dispute(long openingDate, this.disputeOpenerIsMaker = disputeOpenerIsMaker; this.traderPubKeyRing = traderPubKeyRing; this.tradeDate = tradeDate; + this.tradePeriodEnd = tradePeriodEnd; this.contract = contract; this.contractHash = contractHash; this.depositTxSerialized = depositTxSerialized; @@ -158,6 +200,7 @@ public Dispute(long openingDate, id = tradeId + "_" + traderId; uid = UUID.randomUUID().toString(); + refreshAlertLevel(true); } @@ -176,6 +219,7 @@ public protobuf.Dispute toProtoMessage() { .setDisputeOpenerIsMaker(disputeOpenerIsMaker) .setTraderPubKeyRing(traderPubKeyRing.toProtoMessage()) .setTradeDate(tradeDate) + .setTradePeriodEnd(tradePeriodEnd) .setContract(contract.toProtoMessage()) .setContractAsJson(contractAsJson) .setAgentPubKeyRing(agentPubKeyRing.toProtoMessage()) @@ -183,8 +227,9 @@ public protobuf.Dispute toProtoMessage() { .addAllChatMessage(clonedChatMessages.stream() .map(msg -> msg.toProtoNetworkEnvelope().getChatMessage()) .collect(Collectors.toList())) - .setIsClosed(isClosedProperty.get()) + .setIsClosed(this.isClosed()) .setOpeningDate(openingDate) + .setState(Dispute.State.toProtoMessage(disputeState)) .setId(id); Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(e))); @@ -200,6 +245,7 @@ public protobuf.Dispute toProtoMessage() { Optional.ofNullable(mediatorsDisputeResult).ifPresent(result -> builder.setMediatorsDisputeResult(mediatorsDisputeResult)); Optional.ofNullable(delayedPayoutTxId).ifPresent(result -> builder.setDelayedPayoutTxId(delayedPayoutTxId)); Optional.ofNullable(donationAddressOfDelayedPayoutTx).ifPresent(result -> builder.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx)); + Optional.ofNullable(getExtraDataMap()).ifPresent(builder::putAllExtraData); return builder.build(); } @@ -211,6 +257,7 @@ public static Dispute fromProto(protobuf.Dispute proto, CoreProtoResolver corePr proto.getDisputeOpenerIsMaker(), PubKeyRing.fromProto(proto.getTraderPubKeyRing()), proto.getTradeDate(), + proto.getTradePeriodEnd(), Contract.fromProto(proto.getContract(), coreProtoResolver), ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash()), ProtoUtil.byteArrayOrNullFromProto(proto.getDepositTxSerialized()), @@ -224,11 +271,13 @@ public static Dispute fromProto(protobuf.Dispute proto, CoreProtoResolver corePr proto.getIsSupportTicket(), SupportType.fromProto(proto.getSupportType())); + dispute.setExtraDataMap(CollectionUtils.isEmpty(proto.getExtraDataMap()) ? + null : ExtraDataMapValidator.getValidatedExtraDataMap(proto.getExtraDataMap())); + dispute.chatMessages.addAll(proto.getChatMessageList().stream() .map(ChatMessage::fromPayloadProto) .collect(Collectors.toList())); - dispute.isClosedProperty.set(proto.getIsClosed()); if (proto.hasDisputeResult()) dispute.disputeResultProperty.set(DisputeResult.fromProto(proto.getDisputeResult())); dispute.disputePayoutTxId = ProtoUtil.stringOrNullFromProto(proto.getDisputePayoutTxId()); @@ -248,6 +297,20 @@ public static Dispute fromProto(protobuf.Dispute proto, CoreProtoResolver corePr dispute.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx); } + if (Dispute.State.fromProto(proto.getState()) == State.NEEDS_UPGRADE) { + // old disputes did not have a state field, so choose an appropriate state: + dispute.setState(proto.getIsClosed() ? State.CLOSED : State.OPEN); + if (dispute.getDisputeState() == State.CLOSED) { + // mark chat messages as read for pre-existing CLOSED disputes + // otherwise at upgrade, all old disputes would have 1 unread chat message + // because currently when a dispute is closed, the last chat message is not marked read + dispute.getChatMessages().forEach(m -> m.setWasDisplayed(true)); + } + } else { + dispute.setState(Dispute.State.fromProto(proto.getState())); + } + + dispute.refreshAlertLevel(true); return dispute; } @@ -269,14 +332,32 @@ public void addAndPersistChatMessage(ChatMessage chatMessage) { // Setters /////////////////////////////////////////////////////////////////////////////////////////// - public void setIsClosed(boolean isClosed) { - this.isClosedProperty.set(isClosed); + public void setIsClosed() { + setState(State.CLOSED); + } + + public void reOpen() { + setState(State.REOPENED); + } + + public void setState(Dispute.State disputeState) { + this.disputeState = disputeState; + this.isClosedProperty.set(disputeState == State.CLOSED); } public void setDisputeResult(DisputeResult disputeResult) { disputeResultProperty.set(disputeResult); } + public void setExtraData(String key, String value) { + if (key == null || value == null) { + return; + } + if (extraDataMap == null) { + extraDataMap = new HashMap<>(); + } + extraDataMap.put(key, value); + } /////////////////////////////////////////////////////////////////////////////////////////// // Getters @@ -289,7 +370,9 @@ public String getShortTradeId() { public ReadOnlyBooleanProperty isClosedProperty() { return isClosedProperty; } - + public ReadOnlyIntegerProperty getBadgeCountProperty() { + return badgeCountProperty; + } public ReadOnlyObjectProperty disputeResultProperty() { return disputeResultProperty; } @@ -298,14 +381,64 @@ public Date getTradeDate() { return new Date(tradeDate); } + public Date getTradePeriodEnd() { + return new Date(tradePeriodEnd); + } + public Date getOpeningDate() { return new Date(openingDate); } + public boolean isNew() { + return this.disputeState == State.NEW; + } + public boolean isClosed() { - return isClosedProperty.get(); + return this.disputeState == State.CLOSED; } + 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 + if (isNew() || unreadMessageCount(senderFlag) > 0) { + badgeCountProperty.setValue(1); + } else { + badgeCountProperty.setValue(0); + } + } + + public long unreadMessageCount(boolean senderFlag) { + return chatMessages.stream() + .filter(m -> m.isSenderIsTrader() == senderFlag) + .filter(m -> !m.isSystemMessage()) + .filter(m -> !m.isWasDisplayed()) + .count(); + } + + public void setDisputeSeen(boolean senderFlag) { + if (this.disputeState == State.NEW) + setState(State.OPEN); + refreshAlertLevel(senderFlag); + } + + public void setChatMessagesSeen(boolean senderFlag) { + getChatMessages().forEach(m -> m.setWasDisplayed(true)); + refreshAlertLevel(senderFlag); + } + + public String getRoleString() { + if (disputeOpenerIsMaker) { + if (disputeOpenerIsBuyer) + return Res.get("support.buyerOfferer"); + else + return Res.get("support.sellerOfferer"); + } else { + if (disputeOpenerIsBuyer) + return Res.get("support.buyerTaker"); + else + return Res.get("support.sellerTaker"); + } + } @Override public String toString() { @@ -313,11 +446,13 @@ public String toString() { "\n tradeId='" + tradeId + '\'' + ",\n id='" + id + '\'' + ",\n uid='" + uid + '\'' + + ",\n state=" + disputeState + ",\n traderId=" + traderId + ",\n disputeOpenerIsBuyer=" + disputeOpenerIsBuyer + ",\n disputeOpenerIsMaker=" + disputeOpenerIsMaker + ",\n traderPubKeyRing=" + traderPubKeyRing + ",\n tradeDate=" + tradeDate + + ",\n tradePeriodEnd=" + tradePeriodEnd + ",\n contract=" + contract + ",\n contractHash=" + Utilities.bytesAsHexString(contractHash) + ",\n depositTxSerialized=" + Utilities.bytesAsHexString(depositTxSerialized) + 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 f722f2dbdab..b38cf7fa4ff 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeListService.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeListService.java @@ -26,17 +26,14 @@ import bisq.common.proto.persistable.PersistedDataHost; import org.fxmisc.easybind.EasyBind; -import org.fxmisc.easybind.Subscription; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.collections.ObservableList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -52,7 +49,6 @@ public abstract class DisputeListService> impleme protected final PersistenceManager persistenceManager; @Getter private final T disputeList; - private final Map disputeIsClosedSubscriptionsMap = new HashMap<>(); @Getter private final IntegerProperty numOpenDisputes = new SimpleIntegerProperty(); @Getter @@ -153,26 +149,21 @@ private void onDisputesChangeListener(List addedList, @Nullable List removedList) { if (removedList != null) { removedList.forEach(dispute -> { - String id = dispute.getId(); - if (disputeIsClosedSubscriptionsMap.containsKey(id)) { - disputeIsClosedSubscriptionsMap.get(id).unsubscribe(); - disputeIsClosedSubscriptionsMap.remove(id); - } disputedTradeIds.remove(dispute.getTradeId()); }); } addedList.forEach(dispute -> { - String id = dispute.getId(); - Subscription disputeStateSubscription = EasyBind.subscribe(dispute.isClosedProperty(), - isClosed -> { + // for each dispute added, keep track of its "BadgeCountProperty" + EasyBind.subscribe(dispute.getBadgeCountProperty(), + isAlerting -> { // We get the event before the list gets updated, so we execute on next frame UserThread.execute(() -> { - int openDisputes = (int) disputeList.getList().stream() - .filter(e -> !e.isClosed()).count(); - numOpenDisputes.set(openDisputes); + int numAlerts = (int) disputeList.getList().stream() + .mapToLong(x -> x.getBadgeCountProperty().getValue()) + .sum(); + numOpenDisputes.set(numAlerts); }); }); - disputeIsClosedSubscriptionsMap.put(id, disputeStateSubscription); disputedTradeIds.add(dispute.getTradeId()); }); } 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 bf78a565c0a..6abb55ffcbc 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -314,6 +314,8 @@ protected void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessa Dispute dispute = openNewDisputeMessage.getDispute(); // Disputes from clients < 1.2.0 always have support type ARBITRATION in dispute as the field didn't exist before dispute.setSupportType(openNewDisputeMessage.getSupportType()); + // disputes from clients < 1.6.0 have state not set as the field didn't exist before + dispute.setState(Dispute.State.NEW); // this can be removed a few months after 1.6.0 release Contract contract = dispute.getContract(); addPriceInfoMessage(dispute, 0); @@ -577,6 +579,7 @@ private void doSendPeerOpenedDisputeMessage(Dispute disputeFromOpener, !disputeFromOpener.isDisputeOpenerIsMaker(), pubKeyRing, disputeFromOpener.getTradeDate().getTime(), + disputeFromOpener.getTradePeriodEnd().getTime(), contractFromOpener, disputeFromOpener.getContractHash(), disputeFromOpener.getDepositTxSerialized(), @@ -589,6 +592,7 @@ private void doSendPeerOpenedDisputeMessage(Dispute disputeFromOpener, disputeFromOpener.getAgentPubKeyRing(), disputeFromOpener.isSupportTicket(), disputeFromOpener.getSupportType()); + dispute.setExtraDataMap(disputeFromOpener.getExtraDataMap()); dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId()); dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx()); @@ -829,6 +833,14 @@ public Optional findDispute(String tradeId) { .findAny(); } + public Optional findTrade(Dispute dispute) { + Optional retVal = tradeManager.getTradeById(dispute.getTradeId()); + if (!retVal.isPresent()) { + retVal = closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(dispute.getTradeId())).findFirst(); + } + return retVal; + } + private void addMediationResultMessage(Dispute dispute) { // In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent. if (dispute.getMediatorsDisputeResult() != null) { @@ -846,6 +858,20 @@ private void addMediationResultMessage(Dispute dispute) { } } + public void addMediationReOpenedMessage(Dispute dispute, boolean senderIsTrader) { + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + dispute.getTraderId(), + senderIsTrader, + Res.get("support.info.disputeReOpened"), + p2PService.getAddress()); + chatMessage.setSystemMessage(false); + dispute.addAndPersistChatMessage(chatMessage); + this.sendChatMessage(chatMessage); + requestPersistence(); + } + // If price was going down between take offer time and open dispute time the buyer has an incentive to // not send the payment but to try to make a new trade with the better price. We risks to lose part of the // security deposit (in mediation we will always get back 0.003 BTC to keep some incentive to accept mediated diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java index bc63510f18b..a2c53a29f52 100644 --- a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java @@ -217,7 +217,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(true); + dispute.setIsClosed(); if (dispute.disputeResultProperty().get() != null) { log.warn("We already got a dispute result. That should only happen if a dispute needs to be closed " + 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 85753951f7b..776e2191ed3 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 @@ -193,7 +193,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(true); + dispute.setIsClosed(); dispute.setDisputeResult(disputeResult); 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 d9b5fe502db..deb1537b624 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 @@ -190,7 +190,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(true); + dispute.setIsClosed(); if (dispute.disputeResultProperty().get() != null) { log.warn("We got already a dispute result. That should only happen if a dispute needs to be closed " + diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 2681b539cd0..145502bc70b 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1129,8 +1129,10 @@ support.sellerAddress=BTC seller address support.role=Role support.agent=Support agent support.state=State +support.chat=Chat support.closed=Closed support.open=Open +support.process=Process support.buyerOfferer=BTC buyer/Maker support.sellerOfferer=BTC seller/Maker support.buyerTaker=BTC buyer/Taker @@ -1180,7 +1182,8 @@ support.warning.disputesWithInvalidDonationAddress=The delayed payout transactio {3} 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. #################################################################### # Settings @@ -2536,6 +2539,9 @@ disputeSummaryWindow.payoutAmount.buyer=Buyer's payout amount disputeSummaryWindow.payoutAmount.seller=Seller's payout amount disputeSummaryWindow.payoutAmount.invert=Use loser as publisher disputeSummaryWindow.reason=Reason of dispute +disputeSummaryWindow.tradePeriodEnd=Trade period end +disputeSummaryWindow.extraInfo=Extra information +disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status # dynamic values are not recognized by IntelliJ # suppress inspection "UnusedProperty" diff --git a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java index 9782e37ce13..cf47c712d4b 100644 --- a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java +++ b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java @@ -184,6 +184,7 @@ public void testArbitratorSignWitness() { true, buyerPubKeyRing, now - 1, + now - 1, contract, null, null, @@ -196,7 +197,7 @@ public void testArbitratorSignWitness() { null, true, SupportType.ARBITRATION)); - disputes.get(0).getIsClosedProperty().set(true); + disputes.get(0).setIsClosed(); disputes.get(0).getDisputeResultProperty().set(new DisputeResult( "trade1", 1, 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 24582555ab2..d2298d39bb7 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 @@ -84,16 +84,16 @@ import javafx.beans.binding.Bindings; import javafx.beans.value.ChangeListener; +import java.time.Instant; + import java.util.Date; +import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; -import static bisq.desktop.util.FormBuilder.add2ButtonsWithBox; -import static bisq.desktop.util.FormBuilder.addConfirmationLabelLabel; -import static bisq.desktop.util.FormBuilder.addTitledGroupBg; -import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox; +import static bisq.desktop.util.FormBuilder.*; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @@ -170,12 +170,6 @@ public void show(Dispute dispute) { } } - public DisputeSummaryWindow onFinalizeDispute(Runnable finalizeDisputeHandler) { - this.finalizeDisputeHandlerOptional = Optional.of(finalizeDisputeHandler); - return this; - } - - /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @@ -288,17 +282,7 @@ private void addInfoPane() { addConfirmationLabelLabel(gridPane, rowIndex, Res.get("shared.tradeId"), dispute.getShortTradeId(), Layout.TWICE_FIRST_ROW_DISTANCE); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.openDate"), DisplayUtils.formatDateTime(dispute.getOpeningDate())); - if (dispute.isDisputeOpenerIsMaker()) { - if (dispute.isDisputeOpenerIsBuyer()) - role = Res.get("support.buyerOfferer"); - else - role = Res.get("support.sellerOfferer"); - } else { - if (dispute.isDisputeOpenerIsBuyer()) - role = Res.get("support.buyerTaker"); - else - role = Res.get("support.sellerTaker"); - } + role = dispute.getRoleString(); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.role"), role); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.tradeAmount"), formatter.formatCoinWithCode(contract.getTradeAmount())); @@ -314,6 +298,24 @@ private void addInfoPane() { " " + formatter.formatCoinWithCode(contract.getOfferPayload().getSellerSecurityDeposit()); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); + + boolean isMediationDispute = getDisputeManager(dispute) instanceof MediationManager; + if (isMediationDispute) { + if (dispute.getTradePeriodEnd().getTime() > 0) { + String status = DisplayUtils.formatDateTime(dispute.getTradePeriodEnd()); + Label tradePeriodEnd = addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.tradePeriodEnd"), status).second; + if (dispute.getTradePeriodEnd().toInstant().isAfter(Instant.now())) { + tradePeriodEnd.getStyleClass().add("version-new"); // highlight field when the trade period is still active + } + } + if (dispute.getExtraDataMap() != null && dispute.getExtraDataMap().size() > 0) { + String extraDataSummary = ""; + for (Map.Entry entry : dispute.getExtraDataMap().entrySet()) { + extraDataSummary += "[" + entry.getKey() + ":" + entry.getValue() + "] "; + } + addConfirmationLabelLabelWithCopyIcon(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.extraInfo"), extraDataSummary); + } + } } private void addTradeAmountPayoutControls() { @@ -812,7 +814,7 @@ private void doClose(Button closeTicketButton) { disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected()); disputeResult.setCloseDate(new Date()); dispute.setDisputeResult(disputeResult); - dispute.setIsClosed(true); + dispute.setIsClosed(); DisputeResult.Reason reason = disputeResult.getReason(); summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty()); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index b3e75bf15fe..62548c91963 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -537,6 +537,7 @@ private void doOpenDispute(boolean isSupportTicket, Transaction depositTx) { isMaker, pubKeyRing, trade.getDate().getTime(), + trade.getMaxTradePeriodDate().getTime(), trade.getContract(), trade.getContractHash(), depositTxSerialized, @@ -549,6 +550,8 @@ private void doOpenDispute(boolean isSupportTicket, Transaction depositTx) { mediatorPubKeyRing, isSupportTicket, SupportType.MEDIATION); + dispute.setExtraData("counterCurrencyTxId", trade.getCounterCurrencyTxId()); + dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData()); dispute.setDonationAddressOfDelayedPayoutTx(donationAddressString.get()); if (delayedPayoutTx != null) { @@ -598,6 +601,7 @@ private void doOpenDispute(boolean isSupportTicket, Transaction depositTx) { isMaker, pubKeyRing, trade.getDate().getTime(), + trade.getMaxTradePeriodDate().getTime(), trade.getContract(), trade.getContractHash(), depositTxSerialized, @@ -610,6 +614,8 @@ private void doOpenDispute(boolean isSupportTicket, Transaction depositTx) { refundAgentPubKeyRing, isSupportTicket, SupportType.REFUND); + dispute.setExtraData("counterCurrencyTxId", trade.getCounterCurrencyTxId()); + dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData()); String tradeId = dispute.getTradeId(); mediationManager.findDispute(tradeId) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 81ef8590f68..2864435029b 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -403,7 +403,7 @@ private void openChat(Trade trade) { model.dataModel.getTradeManager().requestPersistence(); tradeIdOfOpenChat = trade.getId(); - ChatView chatView = new ChatView(traderChatManager, formatter); + ChatView chatView = new ChatView(traderChatManager, formatter, Res.get("offerbook.trader")); chatView.setAllowAttachments(false); chatView.setDisplayHeader(false); chatView.initialize(); 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 6c53d843c83..c71b18520e9 100644 --- a/desktop/src/main/java/bisq/desktop/main/shared/ChatView.java +++ b/desktop/src/main/java/bisq/desktop/main/shared/ChatView.java @@ -137,10 +137,12 @@ public class ChatView extends AnchorPane { private EventHandler keyEventEventHandler; private SupportManager supportManager; private Optional optionalSupportSession = Optional.empty(); + private String counterpartyName; - public ChatView(SupportManager supportManager, CoinFormatter formatter) { + public ChatView(SupportManager supportManager, CoinFormatter formatter, String counterpartyName) { this.supportManager = supportManager; this.formatter = formatter; + this.counterpartyName = counterpartyName; allowAttachments = true; displayHeader = true; } @@ -414,7 +416,11 @@ protected void updateItem(ChatMessage message, boolean empty) { AnchorPane.setLeftAnchor(statusHBox, padding); } AnchorPane.setBottomAnchor(statusHBox, 7d); - headerLabel.setText(DisplayUtils.formatDateTime(new Date(message.getDate()))); + String metaData = DisplayUtils.formatDateTime(new Date(message.getDate())); + if (!message.isSystemMessage()) + metaData = (isMyMsg ? "Sent " : "Received ") + metaData + + (isMyMsg ? "" : " from " + counterpartyName); + headerLabel.setText(metaData); messageLabel.setText(message.getMessage()); attachmentsBox.getChildren().clear(); if (allowAttachments && diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeChatPopup.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeChatPopup.java new file mode 100644 index 00000000000..796f86ecfe1 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeChatPopup.java @@ -0,0 +1,155 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.support.dispute; + +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.main.MainView; +import bisq.desktop.main.shared.ChatView; +import bisq.desktop.util.CssTheme; + +import bisq.core.locale.Res; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeList; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeSession; +import bisq.core.user.Preferences; +import bisq.core.util.coin.CoinFormatter; + +import bisq.common.UserThread; + +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.stage.Window; + +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.StackPane; + +import javafx.beans.value.ChangeListener; + +public class DisputeChatPopup { + public interface ChatCallback { + void onCloseDisputeFromChatWindow(Dispute dispute); + } + + private Stage chatPopupStage; + protected final DisputeManager> disputeManager; + protected final CoinFormatter formatter; + protected final Preferences preferences; + private ChatCallback chatCallback; + private double chatPopupStageXPosition = -1; + private double chatPopupStageYPosition = -1; + private ChangeListener xPositionListener; + private ChangeListener yPositionListener; + + DisputeChatPopup(DisputeManager> disputeManager, + CoinFormatter formatter, + Preferences preferences, + ChatCallback chatCallback) { + this.disputeManager = disputeManager; + this.formatter = formatter; + this.preferences = preferences; + this.chatCallback = chatCallback; + } + + public boolean isChatShown() { + return chatPopupStage != null; + } + + public void closeChat() { + if (chatPopupStage != null) + chatPopupStage.close(); + } + + public void openChat(Dispute selectedDispute, DisputeSession concreteDisputeSession, String counterpartyName) { + closeChat(); + selectedDispute.getChatMessages().forEach(m -> m.setWasDisplayed(true)); + disputeManager.requestPersistence(); + + ChatView chatView = new ChatView(disputeManager, formatter, counterpartyName); + chatView.setAllowAttachments(true); + chatView.setDisplayHeader(false); + chatView.initialize(); + + AnchorPane pane = new AnchorPane(chatView); + pane.setPrefSize(760, 500); + AnchorPane.setLeftAnchor(chatView, 10d); + AnchorPane.setRightAnchor(chatView, 10d); + AnchorPane.setTopAnchor(chatView, -20d); + AnchorPane.setBottomAnchor(chatView, 10d); + + Button closeDisputeButton = null; + if (!selectedDispute.isClosed() && !disputeManager.isTrader(selectedDispute)) { + closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); + closeDisputeButton.setOnAction(e -> chatCallback.onCloseDisputeFromChatWindow(selectedDispute)); + } + chatView.display(concreteDisputeSession, closeDisputeButton, pane.widthProperty()); + chatView.activate(); + chatView.scrollToBottom(); + chatPopupStage = new Stage(); + chatPopupStage.setTitle(Res.get("tradeChat.chatWindowTitle", selectedDispute.getShortTradeId()) + + " " + selectedDispute.getRoleString()); + StackPane owner = MainView.getRootContainer(); + Scene rootScene = owner.getScene(); + chatPopupStage.initOwner(rootScene.getWindow()); + chatPopupStage.initModality(Modality.NONE); + chatPopupStage.initStyle(StageStyle.DECORATED); + chatPopupStage.setOnHiding(event -> { + chatView.deactivate(); + // at close we set all as displayed. While open we ignore updates of the numNewMsg in the list icon. + selectedDispute.getChatMessages().forEach(m -> m.setWasDisplayed(true)); + disputeManager.requestPersistence(); + chatPopupStage = null; + }); + + Scene scene = new Scene(pane); + CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), false); + scene.addEventHandler(KeyEvent.KEY_RELEASED, ev -> { + if (ev.getCode() == KeyCode.ESCAPE) { + ev.consume(); + chatPopupStage.hide(); + } + }); + chatPopupStage.setScene(scene); + chatPopupStage.setOpacity(0); + chatPopupStage.show(); + + xPositionListener = (observable, oldValue, newValue) -> chatPopupStageXPosition = (double) newValue; + chatPopupStage.xProperty().addListener(xPositionListener); + yPositionListener = (observable, oldValue, newValue) -> chatPopupStageYPosition = (double) newValue; + chatPopupStage.yProperty().addListener(yPositionListener); + + if (chatPopupStageXPosition == -1) { + Window rootSceneWindow = rootScene.getWindow(); + double titleBarHeight = rootSceneWindow.getHeight() - rootScene.getHeight(); + chatPopupStage.setX(Math.round(rootSceneWindow.getX() + (owner.getWidth() - chatPopupStage.getWidth() / 4 * 3))); + chatPopupStage.setY(Math.round(rootSceneWindow.getY() + titleBarHeight + (owner.getHeight() - chatPopupStage.getHeight() / 4 * 3))); + } else { + chatPopupStage.setX(chatPopupStageXPosition); + chatPopupStage.setY(chatPopupStageYPosition); + } + + // Delay display to next render frame to avoid that the popup is first quickly displayed in default position + // and after a short moment in the correct position + UserThread.execute(() -> chatPopupStage.setOpacity(1)); + } +} 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 3e66d49b640..e640ee000a6 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 @@ -29,8 +29,8 @@ import bisq.desktop.main.overlays.windows.SendPrivateNotificationWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; import bisq.desktop.main.overlays.windows.VerifyDisputeResultSignatureWindow; -import bisq.desktop.main.shared.ChatView; import bisq.desktop.util.DisplayUtils; +import bisq.desktop.util.FormBuilder; import bisq.desktop.util.GUIUtil; import bisq.core.account.witness.AccountAgeWitnessService; @@ -45,17 +45,20 @@ import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.DisputeSession; import bisq.core.support.dispute.agent.DisputeAgentLookupMap; +import bisq.core.support.dispute.mediation.MediationManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.messages.ChatMessage; import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; +import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; import bisq.network.p2p.NodeAddress; +import bisq.common.UserThread; import bisq.common.crypto.KeyRing; import bisq.common.crypto.PubKeyRing; import bisq.common.util.Utilities; @@ -64,10 +67,13 @@ import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; +import com.jfoenix.controls.JFXBadge; + import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; +import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.layout.HBox; @@ -77,6 +83,7 @@ import javafx.scene.text.Text; import javafx.geometry.Insets; +import javafx.geometry.Pos; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; @@ -110,6 +117,7 @@ import javax.annotation.Nullable; import static bisq.desktop.util.FormBuilder.getIconForLabel; +import static bisq.desktop.util.FormBuilder.getRegularIconButton; public abstract class DisputeView extends ActivatableView { public enum FilterResult { @@ -143,6 +151,7 @@ public enum FilterResult { protected final KeyRing keyRing; private final TradeManager tradeManager; protected final CoinFormatter formatter; + protected final Preferences preferences; protected final DisputeSummaryWindow disputeSummaryWindow; private final PrivateNotificationManager privateNotificationManager; private final ContractWindow contractWindow; @@ -160,19 +169,21 @@ public enum FilterResult { @Getter protected Dispute selectedDispute; - protected ChatView chatView; - - private ChangeListener selectedDisputeClosedPropertyListener; private Subscription selectedDisputeSubscription; protected FilteredList filteredList; protected InputTextField filterTextField; private ChangeListener filterTextFieldListener; - protected AutoTooltipButton sigCheckButton, reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton; + protected AutoTooltipButton sigCheckButton, reOpenButton, closeButton, sendPrivateNotificationButton, reportButton, fullReportButton; private final Map> disputeChatMessagesListeners = new HashMap<>(); @Nullable private ListChangeListener disputesListener; // Only set in mediation cases protected Label alertIconLabel; protected TableColumn stateColumn; + private Map> listenerByDispute = new HashMap<>(); + private Map chatButtonByDispute = new HashMap<>(); + private Map chatBadgeByDispute = new HashMap<>(); + private Map newBadgeByDispute = new HashMap<>(); + protected DisputeChatPopup chatPopup; /////////////////////////////////////////////////////////////////////////////////////////// @@ -183,6 +194,7 @@ public DisputeView(DisputeManager> disputeManager KeyRing keyRing, TradeManager tradeManager, CoinFormatter formatter, + Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, @@ -196,6 +208,7 @@ public DisputeView(DisputeManager> disputeManager this.keyRing = keyRing; this.tradeManager = tradeManager; this.formatter = formatter; + this.preferences = preferences; this.disputeSummaryWindow = disputeSummaryWindow; this.privateNotificationManager = privateNotificationManager; this.contractWindow = contractWindow; @@ -205,6 +218,8 @@ public DisputeView(DisputeManager> disputeManager this.refundAgentManager = refundAgentManager; this.daoFacade = daoFacade; this.useDevPrivilegeKeys = useDevPrivilegeKeys; + DisputeChatPopup.ChatCallback chatCallback = this::handleOnProcessDispute; + chatPopup = new DisputeChatPopup(disputeManager, formatter, preferences, chatCallback); } @Override @@ -238,6 +253,15 @@ public void initialize() { 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(); + }); + sendPrivateNotificationButton = new AutoTooltipButton(Res.get("support.sendNotificationButton.label")); sendPrivateNotificationButton.setDisable(true); sendPrivateNotificationButton.setVisible(false); @@ -279,6 +303,7 @@ public void initialize() { alertIconLabel, spacer, reOpenButton, + closeButton, sendPrivateNotificationButton, reportButton, fullReportButton, @@ -292,11 +317,6 @@ public void initialize() { root.getChildren().addAll(filterBox, tableView); setupTable(); - - selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> chatView.setInputBoxVisible(!newValue); - - chatView = new ChatView(disputeManager, formatter); - chatView.initialize(); } @Override @@ -311,7 +331,17 @@ protected void activate() { sortedList.comparatorProperty().bind(tableView.comparatorProperty()); tableView.setItems(sortedList); - // sortedList.setComparator((o1, o2) -> o2.getOpeningDate().compareTo(o1.getOpeningDate())); + // double-click on a row opens chat window + tableView.setRowFactory( tv -> { + TableRow row = new TableRow<>(); + row.setOnMouseClicked(event -> { + if (event.getClickCount() == 2 && (!row.isEmpty())) { + openChat(row.getItem()); + } + }); + return row; + }); + selectedDisputeSubscription = EasyBind.subscribe(tableView.getSelectionModel().selectedItemProperty(), this::onSelectDispute); Dispute selectedItem = tableView.getSelectionModel().getSelectedItem(); @@ -320,11 +350,6 @@ protected void activate() { else if (sortedList.size() > 0) tableView.getSelectionModel().select(0); - if (chatView != null) { - chatView.activate(); - chatView.scrollToBottom(); - } - GUIUtil.requestFocus(filterTextField); } @@ -333,10 +358,6 @@ protected void deactivate() { filterTextField.textProperty().removeListener(filterTextFieldListener); sortedList.comparatorProperty().unbind(); selectedDisputeSubscription.unsubscribe(); - removeListenersOnSelectDispute(); - - if (chatView != null) - chatView.deactivate(); } @@ -385,6 +406,8 @@ protected void deactivateReOpenDisputeListener() { protected abstract DisputeSession getConcreteDisputeChatSession(Dispute dispute); + protected abstract boolean senderFlag(); // implemented in the agent / client views + protected void applyFilteredListPredicate(String filterString) { AtomicReference filterResult = new AtomicReference<>(FilterResult.NO_FILTER); filteredList.setPredicate(dispute -> { @@ -468,18 +491,37 @@ protected FilterResult getFilterResult(Dispute dispute, String filterTerm) { return FilterResult.NO_MATCH; } - + // 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() { reOpenDispute(); + disputeManager.addMediationReOpenedMessage(selectedDispute, false); } - protected abstract void handleOnSelectDispute(Dispute dispute); + // only applicable to 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() { + Optional tradeOptional = disputeManager.findTrade(selectedDispute); + if (tradeOptional.isPresent() && tradeOptional.get().getPayoutTxId() != null && tradeOptional.get().getPayoutTxId().length() > 0) { + selectedDispute.setIsClosed(); + disputeManager.requestPersistence(); + onSelectDispute(selectedDispute); + } else { + new Popup().warning(Res.get("support.warning.traderCloseOwnDisputeWarning")).show(); + } + } + + protected void handleOnProcessDispute(Dispute dispute) { + // overridden by clients that use it (dispute agents) + } protected void reOpenDispute() { - if (selectedDispute != null) { - selectedDispute.setIsClosed(false); - handleOnSelectDispute(selectedDispute); + if (selectedDispute != null && selectedDispute.isClosed()) { + selectedDispute.reOpen(); + handleOnProcessDispute(selectedDispute); disputeManager.requestPersistence(); + onSelectDispute(selectedDispute); } } @@ -488,46 +530,21 @@ protected void reOpenDispute() { // UI actions /////////////////////////////////////////////////////////////////////////////////////////// - private void onOpenContract(Dispute dispute) { + protected void onOpenContract(Dispute dispute) { + dispute.setDisputeSeen(senderFlag()); 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()); + closeButton.setDisable(selectedDispute == null || selectedDispute.isClosed()); sendPrivateNotificationButton.setDisable(selectedDispute == null); - - addListenersOnSelectDispute(); } @@ -887,10 +904,7 @@ protected void setupTable() { tableView.setPlaceholder(placeholder); tableView.getSelectionModel().clearSelection(); - tableView.getColumns().add(getSelectColumn()); - - TableColumn contractColumn = getContractColumn(); - tableView.getColumns().add(contractColumn); + tableView.getColumns().add(getContractColumn()); TableColumn dateColumn = getDateColumn(); tableView.getColumns().add(dateColumn); @@ -904,18 +918,19 @@ protected void setupTable() { TableColumn sellerOnionAddressColumn = getSellerOnionAddressColumn(); tableView.getColumns().add(sellerOnionAddressColumn); - TableColumn marketColumn = getMarketColumn(); tableView.getColumns().add(marketColumn); - TableColumn roleColumn = getRoleColumn(); - tableView.getColumns().add(roleColumn); + tableView.getColumns().add(getRoleColumn()); maybeAddAgentColumn(); stateColumn = getStateColumn(); tableView.getColumns().add(stateColumn); + maybeAddProcessColumn(); + tableView.getColumns().add(getChatColumn()); + tradeIdColumn.setComparator(Comparator.comparing(Dispute::getTradeId)); dateColumn.setComparator(Comparator.comparing(Dispute::getOpeningDate)); buyerOnionAddressColumn.setComparator(Comparator.comparing(this::getBuyerOnionAddressColumnLabel)); @@ -926,6 +941,10 @@ protected void setupTable() { tableView.getSortOrder().add(dateColumn); } + protected void maybeAddProcessColumn() { + // Only relevant client views will impl it + } + protected void maybeAddAgentColumn() { // Only relevant client views will impl it } @@ -935,41 +954,42 @@ protected NodeAddress getAgentNodeAddress(Contract contract) { return null; } - private TableColumn getSelectColumn() { - TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.select")); - column.setMinWidth(80); - column.setMaxWidth(80); - column.setSortable(false); - column.getStyleClass().add("first-column"); - - column.setCellValueFactory((addressListItem) -> - new ReadOnlyObjectWrapper<>(addressListItem.getValue())); + private TableColumn getContractColumn() { + TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.details")) { + { + setMaxWidth(150); + setMinWidth(80); + getStyleClass().addAll("first-column", "avatar-column"); + setSortable(false); + } + }; + column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( new Callback<>() { - @Override - public TableCell call(TableColumn column) { + public TableCell call(TableColumn column) { return new TableCell<>() { - - Button button; - @Override public void updateItem(final Dispute item, boolean empty) { super.updateItem(item, empty); - if (item != null && !empty) { - if (button == null) { - button = new AutoTooltipButton(Res.get("shared.select")); - setGraphic(button); - } - button.setOnAction(e -> tableView.getSelectionModel().select(item)); + Button button = getRegularIconButton(MaterialDesignIcon.INFORMATION_OUTLINE); + JFXBadge badge = new JFXBadge(new Label(""), Pos.BASELINE_RIGHT); + badge.setPosition(Pos.TOP_RIGHT); + badge.setVisible(item.isNew()); + badge.setText("New"); + badge.getStyleClass().add("new"); + newBadgeByDispute.put(item.getId(), badge); + HBox hBox = new HBox(button, badge); + setGraphic(hBox); + button.setOnAction(e -> { + tableView.getSelectionModel().select(this.getIndex()); + onOpenContract(item); + item.setDisputeSeen(senderFlag()); + badge.setVisible(item.isNew()); + }); } else { setGraphic(null); - if (button != null) { - button.setOnAction(null); - button = null; - } } } }; @@ -978,38 +998,94 @@ public void updateItem(final Dispute item, boolean empty) { return column; } - private TableColumn getContractColumn() { - TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.details")) { + protected TableColumn getProcessColumn() { + TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.process")) { { - setMinWidth(80); + setMaxWidth(50); + setMinWidth(50); + getStyleClass().addAll("avatar-column"); setSortable(false); } }; column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellFactory( new Callback<>() { - @Override public TableCell call(TableColumn column) { return new TableCell<>() { - Button button; - @Override public void updateItem(final Dispute item, boolean empty) { super.updateItem(item, empty); + if (item != null && !empty) { + Button button = getRegularIconButton(MaterialDesignIcon.GAVEL); + button.setOnAction(e -> { + tableView.getSelectionModel().select(this.getIndex()); + handleOnProcessDispute(item); + item.setDisputeSeen(senderFlag()); + newBadgeByDispute.get(item.getId()).setVisible(item.isNew()); + }); + HBox hBox = new HBox(button); + hBox.setAlignment(Pos.CENTER); + setGraphic(hBox); + } else { + setGraphic(null); + } + } + }; + } + }); + return column; + } + private TableColumn getChatColumn() { + TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.chat")) { + { + setMaxWidth(40); + setMinWidth(40); + getStyleClass().addAll("avatar-column"); + setSortable(false); + } + }; + column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final Dispute item, boolean empty) { + super.updateItem(item, empty); if (item != null && !empty) { - if (button == null) { - button = new AutoTooltipButton(Res.get("shared.details")); - setGraphic(button); + String id = item.getId(); + Button button; + if (!chatButtonByDispute.containsKey(id)) { + button = FormBuilder.getIconButton(MaterialDesignIcon.COMMENT_MULTIPLE_OUTLINE); + chatButtonByDispute.put(id, button); + button.setTooltip(new Tooltip(Res.get("tradeChat.openChat"))); + } else { + button = chatButtonByDispute.get(id); + } + JFXBadge chatBadge; + if (!chatBadgeByDispute.containsKey(id)) { + chatBadge = new JFXBadge(button); + chatBadgeByDispute.put(id, chatBadge); + chatBadge.setPosition(Pos.TOP_RIGHT); + } else { + chatBadge = chatBadgeByDispute.get(id); + } + button.setOnAction(e -> { + tableView.getSelectionModel().select(this.getIndex()); + openChat(item); + }); + if (!listenerByDispute.containsKey(id)) { + ListChangeListener listener = c -> updateChatMessageCount(item, chatBadge); + listenerByDispute.put(id, listener); + item.getChatMessages().addListener(listener); } - button.setOnAction(e -> onOpenContract(item)); + updateChatMessageCount(item, chatBadge); + setGraphic(chatBadge); } else { setGraphic(null); - if (button != null) { - button.setOnAction(null); - button = null; - } } } }; @@ -1214,10 +1290,7 @@ public TableCell call(TableColumn column) { public void updateItem(final Dispute item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { - if (item.isDisputeOpenerIsMaker()) - setText(item.isDisputeOpenerIsBuyer() ? Res.get("support.buyerOfferer") : Res.get("support.sellerOfferer")); - else - setText(item.isDisputeOpenerIsBuyer() ? Res.get("support.buyerTaker") : Res.get("support.sellerTaker")); + setText(item.getRoleString()); } else { setText(""); } @@ -1312,6 +1385,43 @@ public void updateItem(final Dispute item, boolean empty) { }); return column; } -} + private void openChat(Dispute dispute) { + chatPopup.openChat(dispute, getConcreteDisputeChatSession(dispute), getCounterpartyName()); + dispute.setDisputeSeen(senderFlag()); + newBadgeByDispute.get(dispute.getId()).setVisible(dispute.isNew()); + updateChatMessageCount(dispute, chatBadgeByDispute.get(dispute.getId())); + } + + private void updateChatMessageCount(Dispute dispute, JFXBadge chatBadge) { + if (chatBadge == null) + return; + // when the chat popup is active, we do not display new message count indicator for that item + if (chatPopup.isChatShown() && selectedDispute != null && dispute.getId().equals(selectedDispute.getId())) { + chatBadge.setText(""); + chatBadge.setEnabled(false); + chatBadge.refreshBadge(); + // have to UserThread.execute or the new message will be sent to peer as "read" + UserThread.execute(() -> dispute.setChatMessagesSeen(senderFlag())); + return; + } + + if (dispute.unreadMessageCount(senderFlag()) > 0) { + chatBadge.setText(String.valueOf(dispute.unreadMessageCount(senderFlag()))); + chatBadge.setEnabled(true); + } else { + chatBadge.setText(""); + chatBadge.setEnabled(false); + } + chatBadge.refreshBadge(); + dispute.refreshAlertLevel(senderFlag()); + } + private String getCounterpartyName() { + if (senderFlag()) { + return Res.get("offerbook.trader"); + } else { + return (disputeManager instanceof MediationManager) ? Res.get("shared.mediator") : Res.get("shared.refundAgent"); + } + } +} 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 bf4113e50f9..f3eb2da28b1 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 @@ -17,7 +17,6 @@ package bisq.desktop.main.support.dispute.agent; -import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipTableColumn; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.ContractWindow; @@ -32,13 +31,13 @@ import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeList; import bisq.core.support.dispute.DisputeManager; -import bisq.core.support.dispute.DisputeSession; import bisq.core.support.dispute.agent.MultipleHolderNameDetection; import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.TradeDataValidation; import bisq.core.trade.TradeManager; import bisq.core.user.DontShowAgainLookup; +import bisq.core.user.Preferences; import bisq.core.util.coin.CoinFormatter; import bisq.common.crypto.KeyRing; @@ -46,7 +45,6 @@ import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; -import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; @@ -76,6 +74,7 @@ public DisputeAgentView(DisputeManager> disputeMa KeyRing keyRing, TradeManager tradeManager, CoinFormatter formatter, + Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, @@ -89,6 +88,7 @@ public DisputeAgentView(DisputeManager> disputeMa keyRing, tradeManager, formatter, + preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, @@ -216,14 +216,8 @@ protected DisputeView.FilterResult getFilterResult(Dispute dispute, String filte } @Override - protected void handleOnSelectDispute(Dispute dispute) { - Button closeDisputeButton = null; - if (!dispute.isClosed() && !disputeManager.isTrader(dispute)) { - closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); - closeDisputeButton.setOnAction(e -> onCloseDispute(getSelectedDispute())); - } - DisputeSession chatSession = getConcreteDisputeChatSession(dispute); - chatView.display(chatSession, closeDisputeButton, root.widthProperty()); + protected void handleOnProcessDispute(Dispute dispute) { + onCloseDispute(dispute); } @Override @@ -352,6 +346,16 @@ private String getReportMessage(String report, String subString) { "to them so they can ban those traders.\n\n" + Utilities.toTruncatedString(report, 700, false); } + + @Override + protected void maybeAddProcessColumn() { + tableView.getColumns().add(getProcessColumn()); + } + + @Override + protected boolean senderFlag() { + return true; + } } diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/arbitration/ArbitratorView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/arbitration/ArbitratorView.java index ba6f4b19952..08b169734a4 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/arbitration/ArbitratorView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/arbitration/ArbitratorView.java @@ -36,6 +36,7 @@ import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.TradeManager; +import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; @@ -53,6 +54,7 @@ public ArbitratorView(ArbitrationManager arbitrationManager, KeyRing keyRing, TradeManager tradeManager, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, + Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, @@ -66,6 +68,7 @@ public ArbitratorView(ArbitrationManager arbitrationManager, keyRing, tradeManager, formatter, + preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, @@ -94,7 +97,8 @@ protected void onCloseDispute(Dispute dispute) { // This code path is not tested and it is not assumed that it is still be used as old arbitrators would use // their old Bisq version if still cases are pending. if (protocolVersion == 1) { - disputeSummaryWindow.onFinalizeDispute(() -> chatView.removeInputBox()).show(dispute); + chatPopup.closeChat(); + disputeSummaryWindow.show(dispute); } else { new Popup().warning(Res.get("support.wrongVersion", protocolVersion)).show(); } 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 835a1034fcb..95723a1a743 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 @@ -34,6 +34,7 @@ import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.TradeManager; +import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; @@ -51,6 +52,7 @@ public MediatorView(MediationManager mediationManager, KeyRing keyRing, TradeManager tradeManager, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, + Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, @@ -64,6 +66,7 @@ public MediatorView(MediationManager mediationManager, keyRing, tradeManager, formatter, + preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, @@ -112,6 +115,7 @@ protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { @Override protected void onCloseDispute(Dispute dispute) { - disputeSummaryWindow.onFinalizeDispute(() -> chatView.removeInputBox()).show(dispute); + chatPopup.closeChat(); + disputeSummaryWindow.show(dispute); } } diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/refund/RefundAgentView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/refund/RefundAgentView.java index ef5ed5c1487..ca17b18526f 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/refund/RefundAgentView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/refund/RefundAgentView.java @@ -36,6 +36,7 @@ import bisq.core.support.dispute.refund.RefundSession; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.TradeManager; +import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; @@ -53,6 +54,7 @@ public RefundAgentView(RefundManager refundManager, KeyRing keyRing, TradeManager tradeManager, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, + Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, @@ -66,6 +68,7 @@ public RefundAgentView(RefundManager refundManager, keyRing, tradeManager, formatter, + preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, @@ -92,7 +95,8 @@ protected void onCloseDispute(Dispute dispute) { long protocolVersion = dispute.getContract().getOfferPayload().getProtocolVersion(); // Refund agent was introduced with protocolVersion version 2. We do not support old trade protocol cases. if (protocolVersion >= 2) { - disputeSummaryWindow.onFinalizeDispute(() -> chatView.removeInputBox()).show(dispute); + chatPopup.closeChat(); + disputeSummaryWindow.show(dispute); } else { new Popup().warning(Res.get("support.wrongVersion", protocolVersion)).show(); } 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 7414b0b15fe..7c73df56dc0 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 @@ -28,10 +28,10 @@ import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeList; import bisq.core.support.dispute.DisputeManager; -import bisq.core.support.dispute.DisputeSession; import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.TradeManager; +import bisq.core.user.Preferences; import bisq.core.util.coin.CoinFormatter; import bisq.common.crypto.KeyRing; @@ -41,6 +41,7 @@ public DisputeClientView(DisputeManager> DisputeM KeyRing keyRing, TradeManager tradeManager, CoinFormatter formatter, + Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, @@ -50,17 +51,11 @@ public DisputeClientView(DisputeManager> DisputeM RefundAgentManager refundAgentManager, DaoFacade daoFacade, boolean useDevPrivilegeKeys) { - super(DisputeManager, keyRing, tradeManager, formatter, disputeSummaryWindow, privateNotificationManager, + super(DisputeManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys); } - @Override - protected void handleOnSelectDispute(Dispute dispute) { - DisputeSession chatSession = getConcreteDisputeChatSession(dispute); - chatView.display(chatSession, root.widthProperty()); - } - @Override protected DisputeView.FilterResult getFilterResult(Dispute dispute, String filterString) { // As we are in the client view we hide disputes where we are the agent @@ -70,4 +65,9 @@ protected DisputeView.FilterResult getFilterResult(Dispute dispute, String filte return super.getFilterResult(dispute, filterString); } + + @Override + protected boolean senderFlag() { + return false; + } } diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/arbitration/ArbitrationClientView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/arbitration/ArbitrationClientView.java index 699463b84ff..250886b77d6 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/arbitration/ArbitrationClientView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/arbitration/ArbitrationClientView.java @@ -34,6 +34,7 @@ import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.TradeManager; +import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; @@ -50,6 +51,7 @@ public ArbitrationClientView(ArbitrationManager arbitrationManager, KeyRing keyRing, TradeManager tradeManager, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, + Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, @@ -59,7 +61,7 @@ public ArbitrationClientView(ArbitrationManager arbitrationManager, RefundAgentManager refundAgentManager, DaoFacade daoFacade, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { - super(arbitrationManager, keyRing, tradeManager, formatter, disputeSummaryWindow, + super(arbitrationManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys); } 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 9c9a99b38e0..7f55c5175b0 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 @@ -37,6 +37,7 @@ import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.Contract; import bisq.core.trade.TradeManager; +import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; @@ -48,8 +49,6 @@ import javax.inject.Inject; import javax.inject.Named; -import javafx.scene.control.TableColumn; - @FxmlView public class MediationClientView extends DisputeClientView { @Inject @@ -57,6 +56,7 @@ public MediationClientView(MediationManager mediationManager, KeyRing keyRing, TradeManager tradeManager, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, + Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, @@ -66,7 +66,7 @@ public MediationClientView(MediationManager mediationManager, RefundAgentManager refundAgentManager, DaoFacade daoFacade, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { - super(mediationManager, keyRing, tradeManager, formatter, disputeSummaryWindow, + super(mediationManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys); } @@ -76,6 +76,8 @@ public void initialize() { super.initialize(); reOpenButton.setVisible(true); reOpenButton.setManaged(true); + closeButton.setVisible(true); + closeButton.setManaged(true); setupReOpenDisputeListener(); } @@ -105,7 +107,7 @@ protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { protected void reOpenDisputeFromButton() { new Popup().attention(Res.get("support.reOpenByTrader.prompt")) .actionButtonText(Res.get("shared.yes")) - .onAction(this::reOpenDispute) + .onAction(() -> reOpenDispute()) .show(); } @@ -116,7 +118,6 @@ protected NodeAddress getAgentNodeAddress(Contract contract) { @Override protected void maybeAddAgentColumn() { - TableColumn agentColumn = getAgentColumn(); - tableView.getColumns().add(agentColumn); + tableView.getColumns().add(getAgentColumn()); } } diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/refund/RefundClientView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/refund/RefundClientView.java index f1bd6d6da17..b622f25bef5 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/refund/RefundClientView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/refund/RefundClientView.java @@ -35,6 +35,7 @@ import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.Contract; import bisq.core.trade.TradeManager; +import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; @@ -46,8 +47,6 @@ import javax.inject.Inject; import javax.inject.Named; -import javafx.scene.control.TableColumn; - @FxmlView public class RefundClientView extends DisputeClientView { @Inject @@ -55,6 +54,7 @@ public RefundClientView(RefundManager refundManager, KeyRing keyRing, TradeManager tradeManager, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, + Preferences preferences, DisputeSummaryWindow disputeSummaryWindow, PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow, @@ -64,7 +64,7 @@ public RefundClientView(RefundManager refundManager, RefundAgentManager refundAgentManager, DaoFacade daoFacade, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { - super(refundManager, keyRing, tradeManager, formatter, disputeSummaryWindow, + super(refundManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys); } @@ -86,7 +86,6 @@ protected NodeAddress getAgentNodeAddress(Contract contract) { @Override protected void maybeAddAgentColumn() { - TableColumn agentColumn = getAgentColumn(); - tableView.getColumns().add(agentColumn); + tableView.getColumns().add(getAgentColumn()); } } diff --git a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java index 5eabd281dc6..9dc747b11cd 100644 --- a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java @@ -392,6 +392,21 @@ public static Tuple2 addConfirmationLabelLabel(GridPane gridPane, return new Tuple2<>(label1, label2); } + public static Tuple2 addConfirmationLabelLabelWithCopyIcon(GridPane gridPane, + int rowIndex, + String title1, + String title2) { + Label label1 = addLabel(gridPane, rowIndex, title1); + label1.getStyleClass().add("confirmation-label"); + TextFieldWithCopyIcon label2 = new TextFieldWithCopyIcon("confirmation-value"); + label2.setText(title2); + GridPane.setRowIndex(label2, rowIndex); + gridPane.getChildren().add(label2); + GridPane.setColumnIndex(label2, 1); + GridPane.setHalignment(label1, HPos.LEFT); + return new Tuple2<>(label1, label2); + } + public static Tuple2 addConfirmationLabelTextArea(GridPane gridPane, int rowIndex, String title1, diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 3889328f9ba..db08d57f4b8 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -804,6 +804,13 @@ message SignedWitness { message Dispute { + enum State { + NEEDS_UPGRADE = 0; + NEW = 1; + OPEN = 2; + REOPENED = 3; + CLOSED = 4; + } string trade_id = 1; string id = 2; int32 trader_id = 3; @@ -831,6 +838,9 @@ message Dispute { string mediators_dispute_result = 25; string delayed_payout_tx_id = 26; string donation_address_of_delayed_payout_tx = 27; + State state = 28; + int64 trade_period_end = 29; + map extra_data = 30; } message Attachment {