Merge pull request #5207 from jmacxx/improve_dispute_chat

Improve chat functionality of mediation/arbitration
This commit is contained in:
Christoph Atteneder 2021-03-17 09:38:35 +01:00 committed by GitHub
commit 9270398fb3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 676 additions and 195 deletions

View file

@ -123,7 +123,7 @@ public class DisputeMsgEvents {
// If last message is not a result message we re-open as we might have received a new message from the // 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 // trader/mediator/arbitrator who has reopened the case
if (dispute.isClosed() && !chatMessages.isEmpty() && !chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute)) { if (dispute.isClosed() && !chatMessages.isEmpty() && !chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute)) {
dispute.setIsClosed(false); dispute.reOpen();
if (dispute.getSupportType() == SupportType.MEDIATION) { if (dispute.getSupportType() == SupportType.MEDIATION) {
mediationManager.requestPersistence(); mediationManager.requestPersistence();
} else if (dispute.getSupportType() == SupportType.REFUND) { } else if (dispute.getSupportType() == SupportType.REFUND) {

View file

@ -17,6 +17,7 @@
package bisq.core.support.dispute; package bisq.core.support.dispute;
import bisq.core.locale.Res;
import bisq.core.proto.CoreProtoResolver; import bisq.core.proto.CoreProtoResolver;
import bisq.core.support.SupportType; import bisq.core.support.SupportType;
import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.ChatMessage;
@ -26,15 +27,20 @@ import bisq.common.crypto.PubKeyRing;
import bisq.common.proto.ProtoUtil; import bisq.common.proto.ProtoUtil;
import bisq.common.proto.network.NetworkPayload; import bisq.common.proto.network.NetworkPayload;
import bisq.common.proto.persistable.PersistablePayload; import bisq.common.proto.persistable.PersistablePayload;
import bisq.common.util.CollectionUtils;
import bisq.common.util.ExtraDataMapValidator;
import bisq.common.util.Utilities; import bisq.common.util.Utilities;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
@ -42,7 +48,9 @@ import javafx.collections.ObservableList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -58,6 +66,23 @@ import javax.annotation.Nullable;
@EqualsAndHashCode @EqualsAndHashCode
@Getter @Getter
public final class Dispute implements NetworkPayload, PersistablePayload { 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 tradeId;
private final String id; private final String id;
private final int traderId; private final int traderId;
@ -66,6 +91,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
// PubKeyRing of trader who opened the dispute // PubKeyRing of trader who opened the dispute
private final PubKeyRing traderPubKeyRing; private final PubKeyRing traderPubKeyRing;
private final long tradeDate; private final long tradeDate;
private final long tradePeriodEnd;
private final Contract contract; private final Contract contract;
@Nullable @Nullable
private final byte[] contractHash; private final byte[] contractHash;
@ -85,7 +111,6 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
private final PubKeyRing agentPubKeyRing; // dispute agent private final PubKeyRing agentPubKeyRing; // dispute agent
private final boolean isSupportTicket; private final boolean isSupportTicket;
private final ObservableList<ChatMessage> chatMessages = FXCollections.observableArrayList(); private final ObservableList<ChatMessage> chatMessages = FXCollections.observableArrayList();
private final BooleanProperty isClosedProperty = new SimpleBooleanProperty();
// disputeResultProperty.get is Nullable! // disputeResultProperty.get is Nullable!
private final ObjectProperty<DisputeResult> disputeResultProperty = new SimpleObjectProperty<>(); private final ObjectProperty<DisputeResult> disputeResultProperty = new SimpleObjectProperty<>();
private final long openingDate; private final long openingDate;
@ -107,10 +132,25 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
@Setter @Setter
@Nullable @Nullable
private String donationAddressOfDelayedPayoutTx; 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<String, String> extraDataMap;
// We do not persist uid, it is only used by dispute agents to guarantee an uid. // We do not persist uid, it is only used by dispute agents to guarantee an uid.
@Setter @Setter
@Nullable @Nullable
private transient String uid; 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 final class Dispute implements NetworkPayload, PersistablePayload {
boolean disputeOpenerIsMaker, boolean disputeOpenerIsMaker,
PubKeyRing traderPubKeyRing, PubKeyRing traderPubKeyRing,
long tradeDate, long tradeDate,
long tradePeriodEnd,
Contract contract, Contract contract,
@Nullable byte[] contractHash, @Nullable byte[] contractHash,
@Nullable byte[] depositTxSerialized, @Nullable byte[] depositTxSerialized,
@ -143,6 +184,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
this.disputeOpenerIsMaker = disputeOpenerIsMaker; this.disputeOpenerIsMaker = disputeOpenerIsMaker;
this.traderPubKeyRing = traderPubKeyRing; this.traderPubKeyRing = traderPubKeyRing;
this.tradeDate = tradeDate; this.tradeDate = tradeDate;
this.tradePeriodEnd = tradePeriodEnd;
this.contract = contract; this.contract = contract;
this.contractHash = contractHash; this.contractHash = contractHash;
this.depositTxSerialized = depositTxSerialized; this.depositTxSerialized = depositTxSerialized;
@ -158,6 +200,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
id = tradeId + "_" + traderId; id = tradeId + "_" + traderId;
uid = UUID.randomUUID().toString(); uid = UUID.randomUUID().toString();
refreshAlertLevel(true);
} }
@ -176,6 +219,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
.setDisputeOpenerIsMaker(disputeOpenerIsMaker) .setDisputeOpenerIsMaker(disputeOpenerIsMaker)
.setTraderPubKeyRing(traderPubKeyRing.toProtoMessage()) .setTraderPubKeyRing(traderPubKeyRing.toProtoMessage())
.setTradeDate(tradeDate) .setTradeDate(tradeDate)
.setTradePeriodEnd(tradePeriodEnd)
.setContract(contract.toProtoMessage()) .setContract(contract.toProtoMessage())
.setContractAsJson(contractAsJson) .setContractAsJson(contractAsJson)
.setAgentPubKeyRing(agentPubKeyRing.toProtoMessage()) .setAgentPubKeyRing(agentPubKeyRing.toProtoMessage())
@ -183,8 +227,9 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
.addAllChatMessage(clonedChatMessages.stream() .addAllChatMessage(clonedChatMessages.stream()
.map(msg -> msg.toProtoNetworkEnvelope().getChatMessage()) .map(msg -> msg.toProtoNetworkEnvelope().getChatMessage())
.collect(Collectors.toList())) .collect(Collectors.toList()))
.setIsClosed(isClosedProperty.get()) .setIsClosed(this.isClosed())
.setOpeningDate(openingDate) .setOpeningDate(openingDate)
.setState(Dispute.State.toProtoMessage(disputeState))
.setId(id); .setId(id);
Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(e))); Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(e)));
@ -200,6 +245,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
Optional.ofNullable(mediatorsDisputeResult).ifPresent(result -> builder.setMediatorsDisputeResult(mediatorsDisputeResult)); Optional.ofNullable(mediatorsDisputeResult).ifPresent(result -> builder.setMediatorsDisputeResult(mediatorsDisputeResult));
Optional.ofNullable(delayedPayoutTxId).ifPresent(result -> builder.setDelayedPayoutTxId(delayedPayoutTxId)); Optional.ofNullable(delayedPayoutTxId).ifPresent(result -> builder.setDelayedPayoutTxId(delayedPayoutTxId));
Optional.ofNullable(donationAddressOfDelayedPayoutTx).ifPresent(result -> builder.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx)); Optional.ofNullable(donationAddressOfDelayedPayoutTx).ifPresent(result -> builder.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx));
Optional.ofNullable(getExtraDataMap()).ifPresent(builder::putAllExtraData);
return builder.build(); return builder.build();
} }
@ -211,6 +257,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
proto.getDisputeOpenerIsMaker(), proto.getDisputeOpenerIsMaker(),
PubKeyRing.fromProto(proto.getTraderPubKeyRing()), PubKeyRing.fromProto(proto.getTraderPubKeyRing()),
proto.getTradeDate(), proto.getTradeDate(),
proto.getTradePeriodEnd(),
Contract.fromProto(proto.getContract(), coreProtoResolver), Contract.fromProto(proto.getContract(), coreProtoResolver),
ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash()), ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash()),
ProtoUtil.byteArrayOrNullFromProto(proto.getDepositTxSerialized()), ProtoUtil.byteArrayOrNullFromProto(proto.getDepositTxSerialized()),
@ -224,11 +271,13 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
proto.getIsSupportTicket(), proto.getIsSupportTicket(),
SupportType.fromProto(proto.getSupportType())); SupportType.fromProto(proto.getSupportType()));
dispute.setExtraDataMap(CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
null : ExtraDataMapValidator.getValidatedExtraDataMap(proto.getExtraDataMap()));
dispute.chatMessages.addAll(proto.getChatMessageList().stream() dispute.chatMessages.addAll(proto.getChatMessageList().stream()
.map(ChatMessage::fromPayloadProto) .map(ChatMessage::fromPayloadProto)
.collect(Collectors.toList())); .collect(Collectors.toList()));
dispute.isClosedProperty.set(proto.getIsClosed());
if (proto.hasDisputeResult()) if (proto.hasDisputeResult())
dispute.disputeResultProperty.set(DisputeResult.fromProto(proto.getDisputeResult())); dispute.disputeResultProperty.set(DisputeResult.fromProto(proto.getDisputeResult()));
dispute.disputePayoutTxId = ProtoUtil.stringOrNullFromProto(proto.getDisputePayoutTxId()); dispute.disputePayoutTxId = ProtoUtil.stringOrNullFromProto(proto.getDisputePayoutTxId());
@ -248,6 +297,20 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
dispute.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx); 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; return dispute;
} }
@ -269,14 +332,32 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
// Setters // Setters
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void setIsClosed(boolean isClosed) { public void setIsClosed() {
this.isClosedProperty.set(isClosed); 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) { public void setDisputeResult(DisputeResult disputeResult) {
disputeResultProperty.set(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 // Getters
@ -289,7 +370,9 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
public ReadOnlyBooleanProperty isClosedProperty() { public ReadOnlyBooleanProperty isClosedProperty() {
return isClosedProperty; return isClosedProperty;
} }
public ReadOnlyIntegerProperty getBadgeCountProperty() {
return badgeCountProperty;
}
public ReadOnlyObjectProperty<DisputeResult> disputeResultProperty() { public ReadOnlyObjectProperty<DisputeResult> disputeResultProperty() {
return disputeResultProperty; return disputeResultProperty;
} }
@ -298,14 +381,64 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
return new Date(tradeDate); return new Date(tradeDate);
} }
public Date getTradePeriodEnd() {
return new Date(tradePeriodEnd);
}
public Date getOpeningDate() { public Date getOpeningDate() {
return new Date(openingDate); return new Date(openingDate);
} }
public boolean isClosed() { public boolean isNew() {
return isClosedProperty.get(); return this.disputeState == State.NEW;
} }
public boolean isClosed() {
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 @Override
public String toString() { public String toString() {
@ -313,11 +446,13 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
"\n tradeId='" + tradeId + '\'' + "\n tradeId='" + tradeId + '\'' +
",\n id='" + id + '\'' + ",\n id='" + id + '\'' +
",\n uid='" + uid + '\'' + ",\n uid='" + uid + '\'' +
",\n state=" + disputeState +
",\n traderId=" + traderId + ",\n traderId=" + traderId +
",\n disputeOpenerIsBuyer=" + disputeOpenerIsBuyer + ",\n disputeOpenerIsBuyer=" + disputeOpenerIsBuyer +
",\n disputeOpenerIsMaker=" + disputeOpenerIsMaker + ",\n disputeOpenerIsMaker=" + disputeOpenerIsMaker +
",\n traderPubKeyRing=" + traderPubKeyRing + ",\n traderPubKeyRing=" + traderPubKeyRing +
",\n tradeDate=" + tradeDate + ",\n tradeDate=" + tradeDate +
",\n tradePeriodEnd=" + tradePeriodEnd +
",\n contract=" + contract + ",\n contract=" + contract +
",\n contractHash=" + Utilities.bytesAsHexString(contractHash) + ",\n contractHash=" + Utilities.bytesAsHexString(contractHash) +
",\n depositTxSerialized=" + Utilities.bytesAsHexString(depositTxSerialized) + ",\n depositTxSerialized=" + Utilities.bytesAsHexString(depositTxSerialized) +

View file

@ -26,17 +26,14 @@ import bisq.common.persistence.PersistenceManager;
import bisq.common.proto.persistable.PersistedDataHost; import bisq.common.proto.persistable.PersistedDataHost;
import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import javafx.beans.property.IntegerProperty; import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -52,7 +49,6 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
protected final PersistenceManager<T> persistenceManager; protected final PersistenceManager<T> persistenceManager;
@Getter @Getter
private final T disputeList; private final T disputeList;
private final Map<String, Subscription> disputeIsClosedSubscriptionsMap = new HashMap<>();
@Getter @Getter
private final IntegerProperty numOpenDisputes = new SimpleIntegerProperty(); private final IntegerProperty numOpenDisputes = new SimpleIntegerProperty();
@Getter @Getter
@ -153,26 +149,21 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
@Nullable List<? extends Dispute> removedList) { @Nullable List<? extends Dispute> removedList) {
if (removedList != null) { if (removedList != null) {
removedList.forEach(dispute -> { removedList.forEach(dispute -> {
String id = dispute.getId();
if (disputeIsClosedSubscriptionsMap.containsKey(id)) {
disputeIsClosedSubscriptionsMap.get(id).unsubscribe();
disputeIsClosedSubscriptionsMap.remove(id);
}
disputedTradeIds.remove(dispute.getTradeId()); disputedTradeIds.remove(dispute.getTradeId());
}); });
} }
addedList.forEach(dispute -> { addedList.forEach(dispute -> {
String id = dispute.getId(); // for each dispute added, keep track of its "BadgeCountProperty"
Subscription disputeStateSubscription = EasyBind.subscribe(dispute.isClosedProperty(), EasyBind.subscribe(dispute.getBadgeCountProperty(),
isClosed -> { isAlerting -> {
// We get the event before the list gets updated, so we execute on next frame // We get the event before the list gets updated, so we execute on next frame
UserThread.execute(() -> { UserThread.execute(() -> {
int openDisputes = (int) disputeList.getList().stream() int numAlerts = (int) disputeList.getList().stream()
.filter(e -> !e.isClosed()).count(); .mapToLong(x -> x.getBadgeCountProperty().getValue())
numOpenDisputes.set(openDisputes); .sum();
numOpenDisputes.set(numAlerts);
}); });
}); });
disputeIsClosedSubscriptionsMap.put(id, disputeStateSubscription);
disputedTradeIds.add(dispute.getTradeId()); disputedTradeIds.add(dispute.getTradeId());
}); });
} }

View file

@ -314,6 +314,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
Dispute dispute = openNewDisputeMessage.getDispute(); Dispute dispute = openNewDisputeMessage.getDispute();
// Disputes from clients < 1.2.0 always have support type ARBITRATION in dispute as the field didn't exist before // Disputes from clients < 1.2.0 always have support type ARBITRATION in dispute as the field didn't exist before
dispute.setSupportType(openNewDisputeMessage.getSupportType()); 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(); Contract contract = dispute.getContract();
addPriceInfoMessage(dispute, 0); addPriceInfoMessage(dispute, 0);
@ -577,6 +579,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
!disputeFromOpener.isDisputeOpenerIsMaker(), !disputeFromOpener.isDisputeOpenerIsMaker(),
pubKeyRing, pubKeyRing,
disputeFromOpener.getTradeDate().getTime(), disputeFromOpener.getTradeDate().getTime(),
disputeFromOpener.getTradePeriodEnd().getTime(),
contractFromOpener, contractFromOpener,
disputeFromOpener.getContractHash(), disputeFromOpener.getContractHash(),
disputeFromOpener.getDepositTxSerialized(), disputeFromOpener.getDepositTxSerialized(),
@ -589,6 +592,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
disputeFromOpener.getAgentPubKeyRing(), disputeFromOpener.getAgentPubKeyRing(),
disputeFromOpener.isSupportTicket(), disputeFromOpener.isSupportTicket(),
disputeFromOpener.getSupportType()); disputeFromOpener.getSupportType());
dispute.setExtraDataMap(disputeFromOpener.getExtraDataMap());
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId()); dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx()); dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx());
@ -829,6 +833,14 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
.findAny(); .findAny();
} }
public Optional<Trade> findTrade(Dispute dispute) {
Optional<Trade> 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) { private void addMediationResultMessage(Dispute dispute) {
// In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent. // In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent.
if (dispute.getMediatorsDisputeResult() != null) { if (dispute.getMediatorsDisputeResult() != null) {
@ -846,6 +858,20 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
} }
} }
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 // 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 // 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 // security deposit (in mediation we will always get back 0.003 BTC to keep some incentive to accept mediated

View file

@ -217,7 +217,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
} else { } else {
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId()); 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) { if (dispute.disputeResultProperty().get() != null) {
log.warn("We already got a dispute result. That should only happen if a dispute needs to be closed " + log.warn("We already got a dispute result. That should only happen if a dispute needs to be closed " +

View file

@ -193,7 +193,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
} else { } else {
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId()); log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId());
} }
dispute.setIsClosed(true); dispute.setIsClosed();
dispute.setDisputeResult(disputeResult); dispute.setDisputeResult(disputeResult);

View file

@ -190,7 +190,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
} else { } else {
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId()); 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) { if (dispute.disputeResultProperty().get() != null) {
log.warn("We got already a dispute result. That should only happen if a dispute needs to be closed " + log.warn("We got already a dispute result. That should only happen if a dispute needs to be closed " +

View file

@ -1129,8 +1129,10 @@ support.sellerAddress=BTC seller address
support.role=Role support.role=Role
support.agent=Support agent support.agent=Support agent
support.state=State support.state=State
support.chat=Chat
support.closed=Closed support.closed=Closed
support.open=Open support.open=Open
support.process=Process
support.buyerOfferer=BTC buyer/Maker support.buyerOfferer=BTC buyer/Maker
support.sellerOfferer=BTC seller/Maker support.sellerOfferer=BTC seller/Maker
support.buyerTaker=BTC buyer/Taker support.buyerTaker=BTC buyer/Taker
@ -1180,7 +1182,8 @@ support.warning.disputesWithInvalidDonationAddress=The delayed payout transactio
{3} {3}
support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? 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.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 # Settings
@ -2536,6 +2539,9 @@ disputeSummaryWindow.payoutAmount.buyer=Buyer's payout amount
disputeSummaryWindow.payoutAmount.seller=Seller's payout amount disputeSummaryWindow.payoutAmount.seller=Seller's payout amount
disputeSummaryWindow.payoutAmount.invert=Use loser as publisher disputeSummaryWindow.payoutAmount.invert=Use loser as publisher
disputeSummaryWindow.reason=Reason of dispute 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 # dynamic values are not recognized by IntelliJ
# suppress inspection "UnusedProperty" # suppress inspection "UnusedProperty"

View file

@ -184,6 +184,7 @@ public class AccountAgeWitnessServiceTest {
true, true,
buyerPubKeyRing, buyerPubKeyRing,
now - 1, now - 1,
now - 1,
contract, contract,
null, null,
null, null,
@ -196,7 +197,7 @@ public class AccountAgeWitnessServiceTest {
null, null,
true, true,
SupportType.ARBITRATION)); SupportType.ARBITRATION));
disputes.get(0).getIsClosedProperty().set(true); disputes.get(0).setIsClosed();
disputes.get(0).getDisputeResultProperty().set(new DisputeResult( disputes.get(0).getDisputeResultProperty().set(new DisputeResult(
"trade1", "trade1",
1, 1,

View file

@ -84,16 +84,16 @@ import javafx.geometry.Insets;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import java.time.Instant;
import java.util.Date; import java.util.Date;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static bisq.desktop.util.FormBuilder.add2ButtonsWithBox; import static bisq.desktop.util.FormBuilder.*;
import static bisq.desktop.util.FormBuilder.addConfirmationLabelLabel;
import static bisq.desktop.util.FormBuilder.addTitledGroupBg;
import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j @Slf4j
@ -170,12 +170,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
} }
} }
public DisputeSummaryWindow onFinalizeDispute(Runnable finalizeDisputeHandler) {
this.finalizeDisputeHandlerOptional = Optional.of(finalizeDisputeHandler);
return this;
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Protected // Protected
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -288,17 +282,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
addConfirmationLabelLabel(gridPane, rowIndex, Res.get("shared.tradeId"), dispute.getShortTradeId(), addConfirmationLabelLabel(gridPane, rowIndex, Res.get("shared.tradeId"), dispute.getShortTradeId(),
Layout.TWICE_FIRST_ROW_DISTANCE); Layout.TWICE_FIRST_ROW_DISTANCE);
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.openDate"), DisplayUtils.formatDateTime(dispute.getOpeningDate())); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.openDate"), DisplayUtils.formatDateTime(dispute.getOpeningDate()));
if (dispute.isDisputeOpenerIsMaker()) { role = dispute.getRoleString();
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");
}
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.role"), role); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.role"), role);
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.tradeAmount"), addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.tradeAmount"),
formatter.formatCoinWithCode(contract.getTradeAmount())); formatter.formatCoinWithCode(contract.getTradeAmount()));
@ -314,6 +298,24 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
" " + " " +
formatter.formatCoinWithCode(contract.getOfferPayload().getSellerSecurityDeposit()); formatter.formatCoinWithCode(contract.getOfferPayload().getSellerSecurityDeposit());
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); 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<String, String> entry : dispute.getExtraDataMap().entrySet()) {
extraDataSummary += "[" + entry.getKey() + ":" + entry.getValue() + "] ";
}
addConfirmationLabelLabelWithCopyIcon(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.extraInfo"), extraDataSummary);
}
}
} }
private void addTradeAmountPayoutControls() { private void addTradeAmountPayoutControls() {
@ -812,7 +814,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected()); disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected());
disputeResult.setCloseDate(new Date()); disputeResult.setCloseDate(new Date());
dispute.setDisputeResult(disputeResult); dispute.setDisputeResult(disputeResult);
dispute.setIsClosed(true); dispute.setIsClosed();
DisputeResult.Reason reason = disputeResult.getReason(); DisputeResult.Reason reason = disputeResult.getReason();
summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty()); summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty());

View file

@ -537,6 +537,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
isMaker, isMaker,
pubKeyRing, pubKeyRing,
trade.getDate().getTime(), trade.getDate().getTime(),
trade.getMaxTradePeriodDate().getTime(),
trade.getContract(), trade.getContract(),
trade.getContractHash(), trade.getContractHash(),
depositTxSerialized, depositTxSerialized,
@ -549,6 +550,8 @@ public class PendingTradesDataModel extends ActivatableDataModel {
mediatorPubKeyRing, mediatorPubKeyRing,
isSupportTicket, isSupportTicket,
SupportType.MEDIATION); SupportType.MEDIATION);
dispute.setExtraData("counterCurrencyTxId", trade.getCounterCurrencyTxId());
dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData());
dispute.setDonationAddressOfDelayedPayoutTx(donationAddressString.get()); dispute.setDonationAddressOfDelayedPayoutTx(donationAddressString.get());
if (delayedPayoutTx != null) { if (delayedPayoutTx != null) {
@ -598,6 +601,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
isMaker, isMaker,
pubKeyRing, pubKeyRing,
trade.getDate().getTime(), trade.getDate().getTime(),
trade.getMaxTradePeriodDate().getTime(),
trade.getContract(), trade.getContract(),
trade.getContractHash(), trade.getContractHash(),
depositTxSerialized, depositTxSerialized,
@ -610,6 +614,8 @@ public class PendingTradesDataModel extends ActivatableDataModel {
refundAgentPubKeyRing, refundAgentPubKeyRing,
isSupportTicket, isSupportTicket,
SupportType.REFUND); SupportType.REFUND);
dispute.setExtraData("counterCurrencyTxId", trade.getCounterCurrencyTxId());
dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData());
String tradeId = dispute.getTradeId(); String tradeId = dispute.getTradeId();
mediationManager.findDispute(tradeId) mediationManager.findDispute(tradeId)

View file

@ -403,7 +403,7 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
model.dataModel.getTradeManager().requestPersistence(); model.dataModel.getTradeManager().requestPersistence();
tradeIdOfOpenChat = trade.getId(); tradeIdOfOpenChat = trade.getId();
ChatView chatView = new ChatView(traderChatManager, formatter); ChatView chatView = new ChatView(traderChatManager, formatter, Res.get("offerbook.trader"));
chatView.setAllowAttachments(false); chatView.setAllowAttachments(false);
chatView.setDisplayHeader(false); chatView.setDisplayHeader(false);
chatView.initialize(); chatView.initialize();

View file

@ -137,10 +137,12 @@ public class ChatView extends AnchorPane {
private EventHandler<KeyEvent> keyEventEventHandler; private EventHandler<KeyEvent> keyEventEventHandler;
private SupportManager supportManager; private SupportManager supportManager;
private Optional<SupportSession> optionalSupportSession = Optional.empty(); private Optional<SupportSession> optionalSupportSession = Optional.empty();
private String counterpartyName;
public ChatView(SupportManager supportManager, CoinFormatter formatter) { public ChatView(SupportManager supportManager, CoinFormatter formatter, String counterpartyName) {
this.supportManager = supportManager; this.supportManager = supportManager;
this.formatter = formatter; this.formatter = formatter;
this.counterpartyName = counterpartyName;
allowAttachments = true; allowAttachments = true;
displayHeader = true; displayHeader = true;
} }
@ -414,7 +416,11 @@ public class ChatView extends AnchorPane {
AnchorPane.setLeftAnchor(statusHBox, padding); AnchorPane.setLeftAnchor(statusHBox, padding);
} }
AnchorPane.setBottomAnchor(statusHBox, 7d); 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()); messageLabel.setText(message.getMessage());
attachmentsBox.getChildren().clear(); attachmentsBox.getChildren().clear();
if (allowAttachments && if (allowAttachments &&

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<? extends DisputeList<Dispute>> disputeManager;
protected final CoinFormatter formatter;
protected final Preferences preferences;
private ChatCallback chatCallback;
private double chatPopupStageXPosition = -1;
private double chatPopupStageYPosition = -1;
private ChangeListener<Number> xPositionListener;
private ChangeListener<Number> yPositionListener;
DisputeChatPopup(DisputeManager<? extends DisputeList<Dispute>> 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));
}
}

View file

@ -29,8 +29,8 @@ import bisq.desktop.main.overlays.windows.DisputeSummaryWindow;
import bisq.desktop.main.overlays.windows.SendPrivateNotificationWindow; import bisq.desktop.main.overlays.windows.SendPrivateNotificationWindow;
import bisq.desktop.main.overlays.windows.TradeDetailsWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow;
import bisq.desktop.main.overlays.windows.VerifyDisputeResultSignatureWindow; import bisq.desktop.main.overlays.windows.VerifyDisputeResultSignatureWindow;
import bisq.desktop.main.shared.ChatView;
import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.FormBuilder;
import bisq.desktop.util.GUIUtil; import bisq.desktop.util.GUIUtil;
import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.account.witness.AccountAgeWitnessService;
@ -45,17 +45,20 @@ import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.DisputeSession; import bisq.core.support.dispute.DisputeSession;
import bisq.core.support.dispute.agent.DisputeAgentLookupMap; 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.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.ChatMessage;
import bisq.core.trade.Contract; import bisq.core.trade.Contract;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.common.UserThread;
import bisq.common.crypto.KeyRing; import bisq.common.crypto.KeyRing;
import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.PubKeyRing;
import bisq.common.util.Utilities; import bisq.common.util.Utilities;
@ -64,10 +67,13 @@ import org.bitcoinj.core.Coin;
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
import com.jfoenix.controls.JFXBadge;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.TableCell; import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip; import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
@ -77,6 +83,7 @@ import javafx.scene.layout.VBox;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos;
import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription; import org.fxmisc.easybind.Subscription;
@ -110,6 +117,7 @@ import lombok.Getter;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import static bisq.desktop.util.FormBuilder.getIconForLabel; import static bisq.desktop.util.FormBuilder.getIconForLabel;
import static bisq.desktop.util.FormBuilder.getRegularIconButton;
public abstract class DisputeView extends ActivatableView<VBox, Void> { public abstract class DisputeView extends ActivatableView<VBox, Void> {
public enum FilterResult { public enum FilterResult {
@ -143,6 +151,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
protected final KeyRing keyRing; protected final KeyRing keyRing;
private final TradeManager tradeManager; private final TradeManager tradeManager;
protected final CoinFormatter formatter; protected final CoinFormatter formatter;
protected final Preferences preferences;
protected final DisputeSummaryWindow disputeSummaryWindow; protected final DisputeSummaryWindow disputeSummaryWindow;
private final PrivateNotificationManager privateNotificationManager; private final PrivateNotificationManager privateNotificationManager;
private final ContractWindow contractWindow; private final ContractWindow contractWindow;
@ -160,19 +169,21 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
@Getter @Getter
protected Dispute selectedDispute; protected Dispute selectedDispute;
protected ChatView chatView;
private ChangeListener<Boolean> selectedDisputeClosedPropertyListener;
private Subscription selectedDisputeSubscription; private Subscription selectedDisputeSubscription;
protected FilteredList<Dispute> filteredList; protected FilteredList<Dispute> filteredList;
protected InputTextField filterTextField; protected InputTextField filterTextField;
private ChangeListener<String> filterTextFieldListener; private ChangeListener<String> filterTextFieldListener;
protected AutoTooltipButton sigCheckButton, reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton; protected AutoTooltipButton sigCheckButton, reOpenButton, closeButton, sendPrivateNotificationButton, reportButton, fullReportButton;
private final Map<String, ListChangeListener<ChatMessage>> disputeChatMessagesListeners = new HashMap<>(); private final Map<String, ListChangeListener<ChatMessage>> disputeChatMessagesListeners = new HashMap<>();
@Nullable @Nullable
private ListChangeListener<Dispute> disputesListener; // Only set in mediation cases private ListChangeListener<Dispute> disputesListener; // Only set in mediation cases
protected Label alertIconLabel; protected Label alertIconLabel;
protected TableColumn<Dispute, Dispute> stateColumn; protected TableColumn<Dispute, Dispute> stateColumn;
private Map<String, ListChangeListener<ChatMessage>> listenerByDispute = new HashMap<>();
private Map<String, Button> chatButtonByDispute = new HashMap<>();
private Map<String, JFXBadge> chatBadgeByDispute = new HashMap<>();
private Map<String, JFXBadge> newBadgeByDispute = new HashMap<>();
protected DisputeChatPopup chatPopup;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -183,6 +194,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
KeyRing keyRing, KeyRing keyRing,
TradeManager tradeManager, TradeManager tradeManager,
CoinFormatter formatter, CoinFormatter formatter,
Preferences preferences,
DisputeSummaryWindow disputeSummaryWindow, DisputeSummaryWindow disputeSummaryWindow,
PrivateNotificationManager privateNotificationManager, PrivateNotificationManager privateNotificationManager,
ContractWindow contractWindow, ContractWindow contractWindow,
@ -196,6 +208,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
this.keyRing = keyRing; this.keyRing = keyRing;
this.tradeManager = tradeManager; this.tradeManager = tradeManager;
this.formatter = formatter; this.formatter = formatter;
this.preferences = preferences;
this.disputeSummaryWindow = disputeSummaryWindow; this.disputeSummaryWindow = disputeSummaryWindow;
this.privateNotificationManager = privateNotificationManager; this.privateNotificationManager = privateNotificationManager;
this.contractWindow = contractWindow; this.contractWindow = contractWindow;
@ -205,6 +218,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
this.refundAgentManager = refundAgentManager; this.refundAgentManager = refundAgentManager;
this.daoFacade = daoFacade; this.daoFacade = daoFacade;
this.useDevPrivilegeKeys = useDevPrivilegeKeys; this.useDevPrivilegeKeys = useDevPrivilegeKeys;
DisputeChatPopup.ChatCallback chatCallback = this::handleOnProcessDispute;
chatPopup = new DisputeChatPopup(disputeManager, formatter, preferences, chatCallback);
} }
@Override @Override
@ -238,6 +253,15 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
reOpenDisputeFromButton(); 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 = new AutoTooltipButton(Res.get("support.sendNotificationButton.label"));
sendPrivateNotificationButton.setDisable(true); sendPrivateNotificationButton.setDisable(true);
sendPrivateNotificationButton.setVisible(false); sendPrivateNotificationButton.setVisible(false);
@ -279,6 +303,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
alertIconLabel, alertIconLabel,
spacer, spacer,
reOpenButton, reOpenButton,
closeButton,
sendPrivateNotificationButton, sendPrivateNotificationButton,
reportButton, reportButton,
fullReportButton, fullReportButton,
@ -292,11 +317,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
root.getChildren().addAll(filterBox, tableView); root.getChildren().addAll(filterBox, tableView);
setupTable(); setupTable();
selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> chatView.setInputBoxVisible(!newValue);
chatView = new ChatView(disputeManager, formatter);
chatView.initialize();
} }
@Override @Override
@ -311,7 +331,17 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
sortedList.comparatorProperty().bind(tableView.comparatorProperty()); sortedList.comparatorProperty().bind(tableView.comparatorProperty());
tableView.setItems(sortedList); tableView.setItems(sortedList);
// sortedList.setComparator((o1, o2) -> o2.getOpeningDate().compareTo(o1.getOpeningDate())); // double-click on a row opens chat window
tableView.setRowFactory( tv -> {
TableRow<Dispute> 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); selectedDisputeSubscription = EasyBind.subscribe(tableView.getSelectionModel().selectedItemProperty(), this::onSelectDispute);
Dispute selectedItem = tableView.getSelectionModel().getSelectedItem(); Dispute selectedItem = tableView.getSelectionModel().getSelectedItem();
@ -320,11 +350,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
else if (sortedList.size() > 0) else if (sortedList.size() > 0)
tableView.getSelectionModel().select(0); tableView.getSelectionModel().select(0);
if (chatView != null) {
chatView.activate();
chatView.scrollToBottom();
}
GUIUtil.requestFocus(filterTextField); GUIUtil.requestFocus(filterTextField);
} }
@ -333,10 +358,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
filterTextField.textProperty().removeListener(filterTextFieldListener); filterTextField.textProperty().removeListener(filterTextFieldListener);
sortedList.comparatorProperty().unbind(); sortedList.comparatorProperty().unbind();
selectedDisputeSubscription.unsubscribe(); selectedDisputeSubscription.unsubscribe();
removeListenersOnSelectDispute();
if (chatView != null)
chatView.deactivate();
} }
@ -385,6 +406,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
protected abstract DisputeSession getConcreteDisputeChatSession(Dispute dispute); protected abstract DisputeSession getConcreteDisputeChatSession(Dispute dispute);
protected abstract boolean senderFlag(); // implemented in the agent / client views
protected void applyFilteredListPredicate(String filterString) { protected void applyFilteredListPredicate(String filterString) {
AtomicReference<FilterResult> filterResult = new AtomicReference<>(FilterResult.NO_FILTER); AtomicReference<FilterResult> filterResult = new AtomicReference<>(FilterResult.NO_FILTER);
filteredList.setPredicate(dispute -> { filteredList.setPredicate(dispute -> {
@ -468,18 +491,37 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
return FilterResult.NO_MATCH; 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() { protected void reOpenDisputeFromButton() {
reOpenDispute(); 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<Trade> 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() { protected void reOpenDispute() {
if (selectedDispute != null) { if (selectedDispute != null && selectedDispute.isClosed()) {
selectedDispute.setIsClosed(false); selectedDispute.reOpen();
handleOnSelectDispute(selectedDispute); handleOnProcessDispute(selectedDispute);
disputeManager.requestPersistence(); disputeManager.requestPersistence();
onSelectDispute(selectedDispute);
} }
} }
@ -488,46 +530,21 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
// UI actions // UI actions
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private void onOpenContract(Dispute dispute) { protected void onOpenContract(Dispute dispute) {
dispute.setDisputeSeen(senderFlag());
contractWindow.show(dispute); contractWindow.show(dispute);
} }
private void removeListenersOnSelectDispute() {
if (selectedDispute != null) {
if (selectedDisputeClosedPropertyListener != null)
selectedDispute.isClosedProperty().removeListener(selectedDisputeClosedPropertyListener);
}
}
private void addListenersOnSelectDispute() {
if (selectedDispute != null)
selectedDispute.isClosedProperty().addListener(selectedDisputeClosedPropertyListener);
}
private void onSelectDispute(Dispute dispute) { private void onSelectDispute(Dispute dispute) {
removeListenersOnSelectDispute();
if (dispute == null) { if (dispute == null) {
if (root.getChildren().size() > 2) {
root.getChildren().remove(2);
}
selectedDispute = null; selectedDispute = null;
} else if (selectedDispute != dispute) { } else if (selectedDispute != dispute) {
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()); reOpenButton.setDisable(selectedDispute == null || !selectedDispute.isClosed());
closeButton.setDisable(selectedDispute == null || selectedDispute.isClosed());
sendPrivateNotificationButton.setDisable(selectedDispute == null); sendPrivateNotificationButton.setDisable(selectedDispute == null);
addListenersOnSelectDispute();
} }
@ -887,10 +904,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
tableView.setPlaceholder(placeholder); tableView.setPlaceholder(placeholder);
tableView.getSelectionModel().clearSelection(); tableView.getSelectionModel().clearSelection();
tableView.getColumns().add(getSelectColumn()); tableView.getColumns().add(getContractColumn());
TableColumn<Dispute, Dispute> contractColumn = getContractColumn();
tableView.getColumns().add(contractColumn);
TableColumn<Dispute, Dispute> dateColumn = getDateColumn(); TableColumn<Dispute, Dispute> dateColumn = getDateColumn();
tableView.getColumns().add(dateColumn); tableView.getColumns().add(dateColumn);
@ -904,18 +918,19 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
TableColumn<Dispute, Dispute> sellerOnionAddressColumn = getSellerOnionAddressColumn(); TableColumn<Dispute, Dispute> sellerOnionAddressColumn = getSellerOnionAddressColumn();
tableView.getColumns().add(sellerOnionAddressColumn); tableView.getColumns().add(sellerOnionAddressColumn);
TableColumn<Dispute, Dispute> marketColumn = getMarketColumn(); TableColumn<Dispute, Dispute> marketColumn = getMarketColumn();
tableView.getColumns().add(marketColumn); tableView.getColumns().add(marketColumn);
TableColumn<Dispute, Dispute> roleColumn = getRoleColumn(); tableView.getColumns().add(getRoleColumn());
tableView.getColumns().add(roleColumn);
maybeAddAgentColumn(); maybeAddAgentColumn();
stateColumn = getStateColumn(); stateColumn = getStateColumn();
tableView.getColumns().add(stateColumn); tableView.getColumns().add(stateColumn);
maybeAddProcessColumn();
tableView.getColumns().add(getChatColumn());
tradeIdColumn.setComparator(Comparator.comparing(Dispute::getTradeId)); tradeIdColumn.setComparator(Comparator.comparing(Dispute::getTradeId));
dateColumn.setComparator(Comparator.comparing(Dispute::getOpeningDate)); dateColumn.setComparator(Comparator.comparing(Dispute::getOpeningDate));
buyerOnionAddressColumn.setComparator(Comparator.comparing(this::getBuyerOnionAddressColumnLabel)); buyerOnionAddressColumn.setComparator(Comparator.comparing(this::getBuyerOnionAddressColumnLabel));
@ -926,6 +941,10 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
tableView.getSortOrder().add(dateColumn); tableView.getSortOrder().add(dateColumn);
} }
protected void maybeAddProcessColumn() {
// Only relevant client views will impl it
}
protected void maybeAddAgentColumn() { protected void maybeAddAgentColumn() {
// Only relevant client views will impl it // Only relevant client views will impl it
} }
@ -935,41 +954,42 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
return null; return null;
} }
private TableColumn<Dispute, Dispute> getSelectColumn() { private TableColumn<Dispute, Dispute> getContractColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("shared.select")); TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("shared.details")) {
column.setMinWidth(80); {
column.setMaxWidth(80); setMaxWidth(150);
column.setSortable(false); setMinWidth(80);
column.getStyleClass().add("first-column"); getStyleClass().addAll("first-column", "avatar-column");
setSortable(false);
column.setCellValueFactory((addressListItem) -> }
new ReadOnlyObjectWrapper<>(addressListItem.getValue())); };
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
column.setCellFactory( column.setCellFactory(
new Callback<>() { new Callback<>() {
@Override @Override
public TableCell<Dispute, Dispute> call(TableColumn<Dispute, public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
Dispute> column) {
return new TableCell<>() { return new TableCell<>() {
Button button;
@Override @Override
public void updateItem(final Dispute item, boolean empty) { public void updateItem(final Dispute item, boolean empty) {
super.updateItem(item, empty); super.updateItem(item, empty);
if (item != null && !empty) { if (item != null && !empty) {
if (button == null) { Button button = getRegularIconButton(MaterialDesignIcon.INFORMATION_OUTLINE);
button = new AutoTooltipButton(Res.get("shared.select")); JFXBadge badge = new JFXBadge(new Label(""), Pos.BASELINE_RIGHT);
setGraphic(button); badge.setPosition(Pos.TOP_RIGHT);
} badge.setVisible(item.isNew());
button.setOnAction(e -> tableView.getSelectionModel().select(item)); 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 { } else {
setGraphic(null); setGraphic(null);
if (button != null) {
button.setOnAction(null);
button = null;
}
} }
} }
}; };
@ -978,38 +998,94 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
return column; return column;
} }
private TableColumn<Dispute, Dispute> getContractColumn() { protected TableColumn<Dispute, Dispute> getProcessColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("shared.details")) { TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("support.process")) {
{ {
setMinWidth(80); setMaxWidth(50);
setMinWidth(50);
getStyleClass().addAll("avatar-column");
setSortable(false); setSortable(false);
} }
}; };
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
column.setCellFactory( column.setCellFactory(
new Callback<>() { new Callback<>() {
@Override @Override
public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) { public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
return new TableCell<>() { return new TableCell<>() {
Button button;
@Override @Override
public void updateItem(final Dispute item, boolean empty) { public void updateItem(final Dispute item, boolean empty) {
super.updateItem(item, empty); super.updateItem(item, empty);
if (item != null && !empty) { if (item != null && !empty) {
if (button == null) { Button button = getRegularIconButton(MaterialDesignIcon.GAVEL);
button = new AutoTooltipButton(Res.get("shared.details")); button.setOnAction(e -> {
setGraphic(button); tableView.getSelectionModel().select(this.getIndex());
} handleOnProcessDispute(item);
button.setOnAction(e -> onOpenContract(item)); item.setDisputeSeen(senderFlag());
newBadgeByDispute.get(item.getId()).setVisible(item.isNew());
});
HBox hBox = new HBox(button);
hBox.setAlignment(Pos.CENTER);
setGraphic(hBox);
} else { } else {
setGraphic(null); setGraphic(null);
if (button != null) { }
button.setOnAction(null); }
button = null; };
}
});
return column;
}
private TableColumn<Dispute, Dispute> getChatColumn() {
TableColumn<Dispute, Dispute> 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<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
return new TableCell<>() {
@Override
public void updateItem(final Dispute item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
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<ChatMessage> listener = c -> updateChatMessageCount(item, chatBadge);
listenerByDispute.put(id, listener);
item.getChatMessages().addListener(listener);
}
updateChatMessageCount(item, chatBadge);
setGraphic(chatBadge);
} else {
setGraphic(null);
} }
} }
}; };
@ -1214,10 +1290,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
public void updateItem(final Dispute item, boolean empty) { public void updateItem(final Dispute item, boolean empty) {
super.updateItem(item, empty); super.updateItem(item, empty);
if (item != null && !empty) { if (item != null && !empty) {
if (item.isDisputeOpenerIsMaker()) setText(item.getRoleString());
setText(item.isDisputeOpenerIsBuyer() ? Res.get("support.buyerOfferer") : Res.get("support.sellerOfferer"));
else
setText(item.isDisputeOpenerIsBuyer() ? Res.get("support.buyerTaker") : Res.get("support.sellerTaker"));
} else { } else {
setText(""); setText("");
} }
@ -1312,6 +1385,43 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
}); });
return column; 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");
}
}
} }

View file

@ -17,7 +17,6 @@
package bisq.desktop.main.support.dispute.agent; package bisq.desktop.main.support.dispute.agent;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.AutoTooltipTableColumn; import bisq.desktop.components.AutoTooltipTableColumn;
import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.ContractWindow; import bisq.desktop.main.overlays.windows.ContractWindow;
@ -32,13 +31,13 @@ import bisq.core.locale.Res;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeList; import bisq.core.support.dispute.DisputeList;
import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.DisputeSession;
import bisq.core.support.dispute.agent.MultipleHolderNameDetection; import bisq.core.support.dispute.agent.MultipleHolderNameDetection;
import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.TradeDataValidation; import bisq.core.trade.TradeDataValidation;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.user.DontShowAgainLookup; import bisq.core.user.DontShowAgainLookup;
import bisq.core.user.Preferences;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
import bisq.common.crypto.KeyRing; import bisq.common.crypto.KeyRing;
@ -46,7 +45,6 @@ import bisq.common.util.Utilities;
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.TableCell; import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn;
@ -76,6 +74,7 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
KeyRing keyRing, KeyRing keyRing,
TradeManager tradeManager, TradeManager tradeManager,
CoinFormatter formatter, CoinFormatter formatter,
Preferences preferences,
DisputeSummaryWindow disputeSummaryWindow, DisputeSummaryWindow disputeSummaryWindow,
PrivateNotificationManager privateNotificationManager, PrivateNotificationManager privateNotificationManager,
ContractWindow contractWindow, ContractWindow contractWindow,
@ -89,6 +88,7 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
keyRing, keyRing,
tradeManager, tradeManager,
formatter, formatter,
preferences,
disputeSummaryWindow, disputeSummaryWindow,
privateNotificationManager, privateNotificationManager,
contractWindow, contractWindow,
@ -216,14 +216,8 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
} }
@Override @Override
protected void handleOnSelectDispute(Dispute dispute) { protected void handleOnProcessDispute(Dispute dispute) {
Button closeDisputeButton = null; onCloseDispute(dispute);
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());
} }
@Override @Override
@ -352,6 +346,16 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
"to them so they can ban those traders.\n\n" + "to them so they can ban those traders.\n\n" +
Utilities.toTruncatedString(report, 700, false); Utilities.toTruncatedString(report, 700, false);
} }
@Override
protected void maybeAddProcessColumn() {
tableView.getColumns().add(getProcessColumn());
}
@Override
protected boolean senderFlag() {
return true;
}
} }

View file

@ -36,6 +36,7 @@ import bisq.core.support.dispute.arbitration.ArbitrationSession;
import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
@ -53,6 +54,7 @@ public class ArbitratorView extends DisputeAgentView {
KeyRing keyRing, KeyRing keyRing,
TradeManager tradeManager, TradeManager tradeManager,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
Preferences preferences,
DisputeSummaryWindow disputeSummaryWindow, DisputeSummaryWindow disputeSummaryWindow,
PrivateNotificationManager privateNotificationManager, PrivateNotificationManager privateNotificationManager,
ContractWindow contractWindow, ContractWindow contractWindow,
@ -66,6 +68,7 @@ public class ArbitratorView extends DisputeAgentView {
keyRing, keyRing,
tradeManager, tradeManager,
formatter, formatter,
preferences,
disputeSummaryWindow, disputeSummaryWindow,
privateNotificationManager, privateNotificationManager,
contractWindow, contractWindow,
@ -94,7 +97,8 @@ public class ArbitratorView extends DisputeAgentView {
// This code path is not tested and it is not assumed that it is still be used as old arbitrators would use // 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. // their old Bisq version if still cases are pending.
if (protocolVersion == 1) { if (protocolVersion == 1) {
disputeSummaryWindow.onFinalizeDispute(() -> chatView.removeInputBox()).show(dispute); chatPopup.closeChat();
disputeSummaryWindow.show(dispute);
} else { } else {
new Popup().warning(Res.get("support.wrongVersion", protocolVersion)).show(); new Popup().warning(Res.get("support.wrongVersion", protocolVersion)).show();
} }

View file

@ -34,6 +34,7 @@ import bisq.core.support.dispute.mediation.MediationSession;
import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
@ -51,6 +52,7 @@ public class MediatorView extends DisputeAgentView {
KeyRing keyRing, KeyRing keyRing,
TradeManager tradeManager, TradeManager tradeManager,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
Preferences preferences,
DisputeSummaryWindow disputeSummaryWindow, DisputeSummaryWindow disputeSummaryWindow,
PrivateNotificationManager privateNotificationManager, PrivateNotificationManager privateNotificationManager,
ContractWindow contractWindow, ContractWindow contractWindow,
@ -64,6 +66,7 @@ public class MediatorView extends DisputeAgentView {
keyRing, keyRing,
tradeManager, tradeManager,
formatter, formatter,
preferences,
disputeSummaryWindow, disputeSummaryWindow,
privateNotificationManager, privateNotificationManager,
contractWindow, contractWindow,
@ -112,6 +115,7 @@ public class MediatorView extends DisputeAgentView {
@Override @Override
protected void onCloseDispute(Dispute dispute) { protected void onCloseDispute(Dispute dispute) {
disputeSummaryWindow.onFinalizeDispute(() -> chatView.removeInputBox()).show(dispute); chatPopup.closeChat();
disputeSummaryWindow.show(dispute);
} }
} }

View file

@ -36,6 +36,7 @@ import bisq.core.support.dispute.refund.RefundManager;
import bisq.core.support.dispute.refund.RefundSession; import bisq.core.support.dispute.refund.RefundSession;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
@ -53,6 +54,7 @@ public class RefundAgentView extends DisputeAgentView {
KeyRing keyRing, KeyRing keyRing,
TradeManager tradeManager, TradeManager tradeManager,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
Preferences preferences,
DisputeSummaryWindow disputeSummaryWindow, DisputeSummaryWindow disputeSummaryWindow,
PrivateNotificationManager privateNotificationManager, PrivateNotificationManager privateNotificationManager,
ContractWindow contractWindow, ContractWindow contractWindow,
@ -66,6 +68,7 @@ public class RefundAgentView extends DisputeAgentView {
keyRing, keyRing,
tradeManager, tradeManager,
formatter, formatter,
preferences,
disputeSummaryWindow, disputeSummaryWindow,
privateNotificationManager, privateNotificationManager,
contractWindow, contractWindow,
@ -92,7 +95,8 @@ public class RefundAgentView extends DisputeAgentView {
long protocolVersion = dispute.getContract().getOfferPayload().getProtocolVersion(); long protocolVersion = dispute.getContract().getOfferPayload().getProtocolVersion();
// Refund agent was introduced with protocolVersion version 2. We do not support old trade protocol cases. // Refund agent was introduced with protocolVersion version 2. We do not support old trade protocol cases.
if (protocolVersion >= 2) { if (protocolVersion >= 2) {
disputeSummaryWindow.onFinalizeDispute(() -> chatView.removeInputBox()).show(dispute); chatPopup.closeChat();
disputeSummaryWindow.show(dispute);
} else { } else {
new Popup().warning(Res.get("support.wrongVersion", protocolVersion)).show(); new Popup().warning(Res.get("support.wrongVersion", protocolVersion)).show();
} }

View file

@ -28,10 +28,10 @@ import bisq.core.dao.DaoFacade;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeList; import bisq.core.support.dispute.DisputeList;
import bisq.core.support.dispute.DisputeManager; 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.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.user.Preferences;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
import bisq.common.crypto.KeyRing; import bisq.common.crypto.KeyRing;
@ -41,6 +41,7 @@ public abstract class DisputeClientView extends DisputeView {
KeyRing keyRing, KeyRing keyRing,
TradeManager tradeManager, TradeManager tradeManager,
CoinFormatter formatter, CoinFormatter formatter,
Preferences preferences,
DisputeSummaryWindow disputeSummaryWindow, DisputeSummaryWindow disputeSummaryWindow,
PrivateNotificationManager privateNotificationManager, PrivateNotificationManager privateNotificationManager,
ContractWindow contractWindow, ContractWindow contractWindow,
@ -50,17 +51,11 @@ public abstract class DisputeClientView extends DisputeView {
RefundAgentManager refundAgentManager, RefundAgentManager refundAgentManager,
DaoFacade daoFacade, DaoFacade daoFacade,
boolean useDevPrivilegeKeys) { boolean useDevPrivilegeKeys) {
super(DisputeManager, keyRing, tradeManager, formatter, disputeSummaryWindow, privateNotificationManager, super(DisputeManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow, privateNotificationManager,
contractWindow, tradeDetailsWindow, accountAgeWitnessService, contractWindow, tradeDetailsWindow, accountAgeWitnessService,
mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys); mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
} }
@Override
protected void handleOnSelectDispute(Dispute dispute) {
DisputeSession chatSession = getConcreteDisputeChatSession(dispute);
chatView.display(chatSession, root.widthProperty());
}
@Override @Override
protected DisputeView.FilterResult getFilterResult(Dispute dispute, String filterString) { protected DisputeView.FilterResult getFilterResult(Dispute dispute, String filterString) {
// As we are in the client view we hide disputes where we are the agent // As we are in the client view we hide disputes where we are the agent
@ -70,4 +65,9 @@ public abstract class DisputeClientView extends DisputeView {
return super.getFilterResult(dispute, filterString); return super.getFilterResult(dispute, filterString);
} }
@Override
protected boolean senderFlag() {
return false;
}
} }

View file

@ -34,6 +34,7 @@ import bisq.core.support.dispute.arbitration.ArbitrationSession;
import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
@ -50,6 +51,7 @@ public class ArbitrationClientView extends DisputeClientView {
KeyRing keyRing, KeyRing keyRing,
TradeManager tradeManager, TradeManager tradeManager,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
Preferences preferences,
DisputeSummaryWindow disputeSummaryWindow, DisputeSummaryWindow disputeSummaryWindow,
PrivateNotificationManager privateNotificationManager, PrivateNotificationManager privateNotificationManager,
ContractWindow contractWindow, ContractWindow contractWindow,
@ -59,7 +61,7 @@ public class ArbitrationClientView extends DisputeClientView {
RefundAgentManager refundAgentManager, RefundAgentManager refundAgentManager,
DaoFacade daoFacade, DaoFacade daoFacade,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { @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, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService,
mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys); mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
} }

View file

@ -37,6 +37,7 @@ import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.Contract; import bisq.core.trade.Contract;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
@ -48,8 +49,6 @@ import bisq.common.crypto.KeyRing;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javafx.scene.control.TableColumn;
@FxmlView @FxmlView
public class MediationClientView extends DisputeClientView { public class MediationClientView extends DisputeClientView {
@Inject @Inject
@ -57,6 +56,7 @@ public class MediationClientView extends DisputeClientView {
KeyRing keyRing, KeyRing keyRing,
TradeManager tradeManager, TradeManager tradeManager,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
Preferences preferences,
DisputeSummaryWindow disputeSummaryWindow, DisputeSummaryWindow disputeSummaryWindow,
PrivateNotificationManager privateNotificationManager, PrivateNotificationManager privateNotificationManager,
ContractWindow contractWindow, ContractWindow contractWindow,
@ -66,7 +66,7 @@ public class MediationClientView extends DisputeClientView {
RefundAgentManager refundAgentManager, RefundAgentManager refundAgentManager,
DaoFacade daoFacade, DaoFacade daoFacade,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { @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, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService,
mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys); mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
} }
@ -76,6 +76,8 @@ public class MediationClientView extends DisputeClientView {
super.initialize(); super.initialize();
reOpenButton.setVisible(true); reOpenButton.setVisible(true);
reOpenButton.setManaged(true); reOpenButton.setManaged(true);
closeButton.setVisible(true);
closeButton.setManaged(true);
setupReOpenDisputeListener(); setupReOpenDisputeListener();
} }
@ -105,7 +107,7 @@ public class MediationClientView extends DisputeClientView {
protected void reOpenDisputeFromButton() { protected void reOpenDisputeFromButton() {
new Popup().attention(Res.get("support.reOpenByTrader.prompt")) new Popup().attention(Res.get("support.reOpenByTrader.prompt"))
.actionButtonText(Res.get("shared.yes")) .actionButtonText(Res.get("shared.yes"))
.onAction(this::reOpenDispute) .onAction(() -> reOpenDispute())
.show(); .show();
} }
@ -116,7 +118,6 @@ public class MediationClientView extends DisputeClientView {
@Override @Override
protected void maybeAddAgentColumn() { protected void maybeAddAgentColumn() {
TableColumn<Dispute, Dispute> agentColumn = getAgentColumn(); tableView.getColumns().add(getAgentColumn());
tableView.getColumns().add(agentColumn);
} }
} }

View file

@ -35,6 +35,7 @@ import bisq.core.support.dispute.refund.RefundSession;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.Contract; import bisq.core.trade.Contract;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
@ -46,8 +47,6 @@ import bisq.common.crypto.KeyRing;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javafx.scene.control.TableColumn;
@FxmlView @FxmlView
public class RefundClientView extends DisputeClientView { public class RefundClientView extends DisputeClientView {
@Inject @Inject
@ -55,6 +54,7 @@ public class RefundClientView extends DisputeClientView {
KeyRing keyRing, KeyRing keyRing,
TradeManager tradeManager, TradeManager tradeManager,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
Preferences preferences,
DisputeSummaryWindow disputeSummaryWindow, DisputeSummaryWindow disputeSummaryWindow,
PrivateNotificationManager privateNotificationManager, PrivateNotificationManager privateNotificationManager,
ContractWindow contractWindow, ContractWindow contractWindow,
@ -64,7 +64,7 @@ public class RefundClientView extends DisputeClientView {
RefundAgentManager refundAgentManager, RefundAgentManager refundAgentManager,
DaoFacade daoFacade, DaoFacade daoFacade,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { @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, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService,
mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys); mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
} }
@ -86,7 +86,6 @@ public class RefundClientView extends DisputeClientView {
@Override @Override
protected void maybeAddAgentColumn() { protected void maybeAddAgentColumn() {
TableColumn<Dispute, Dispute> agentColumn = getAgentColumn(); tableView.getColumns().add(getAgentColumn());
tableView.getColumns().add(agentColumn);
} }
} }

View file

@ -392,6 +392,21 @@ public class FormBuilder {
return new Tuple2<>(label1, label2); return new Tuple2<>(label1, label2);
} }
public static Tuple2<Label, TextFieldWithCopyIcon> 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<Label, TextArea> addConfirmationLabelTextArea(GridPane gridPane, public static Tuple2<Label, TextArea> addConfirmationLabelTextArea(GridPane gridPane,
int rowIndex, int rowIndex,
String title1, String title1,

View file

@ -804,6 +804,13 @@ message SignedWitness {
message Dispute { message Dispute {
enum State {
NEEDS_UPGRADE = 0;
NEW = 1;
OPEN = 2;
REOPENED = 3;
CLOSED = 4;
}
string trade_id = 1; string trade_id = 1;
string id = 2; string id = 2;
int32 trader_id = 3; int32 trader_id = 3;
@ -831,6 +838,9 @@ message Dispute {
string mediators_dispute_result = 25; string mediators_dispute_result = 25;
string delayed_payout_tx_id = 26; string delayed_payout_tx_id = 26;
string donation_address_of_delayed_payout_tx = 27; string donation_address_of_delayed_payout_tx = 27;
State state = 28;
int64 trade_period_end = 29;
map<string, string> extra_data = 30;
} }
message Attachment { message Attachment {