mirror of
https://github.com/bisq-network/bisq.git
synced 2025-02-24 07:07:43 +01:00
Merge pull request #5207 from jmacxx/improve_dispute_chat
Improve chat functionality of mediation/arbitration
This commit is contained in:
commit
9270398fb3
25 changed files with 676 additions and 195 deletions
|
@ -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
|
||||
// trader/mediator/arbitrator who has reopened the case
|
||||
if (dispute.isClosed() && !chatMessages.isEmpty() && !chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute)) {
|
||||
dispute.setIsClosed(false);
|
||||
dispute.reOpen();
|
||||
if (dispute.getSupportType() == SupportType.MEDIATION) {
|
||||
mediationManager.requestPersistence();
|
||||
} else if (dispute.getSupportType() == SupportType.REFUND) {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package bisq.core.support.dispute;
|
||||
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.proto.CoreProtoResolver;
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
|
@ -26,15 +27,20 @@ import bisq.common.crypto.PubKeyRing;
|
|||
import bisq.common.proto.ProtoUtil;
|
||||
import bisq.common.proto.network.NetworkPayload;
|
||||
import bisq.common.proto.persistable.PersistablePayload;
|
||||
import bisq.common.util.CollectionUtils;
|
||||
import bisq.common.util.ExtraDataMapValidator;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyIntegerProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
|
@ -42,7 +48,9 @@ import javafx.collections.ObservableList;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -58,6 +66,23 @@ import javax.annotation.Nullable;
|
|||
@EqualsAndHashCode
|
||||
@Getter
|
||||
public final class Dispute implements NetworkPayload, PersistablePayload {
|
||||
|
||||
public enum State {
|
||||
NEEDS_UPGRADE,
|
||||
NEW,
|
||||
OPEN,
|
||||
REOPENED,
|
||||
CLOSED;
|
||||
|
||||
public static Dispute.State fromProto(protobuf.Dispute.State state) {
|
||||
return ProtoUtil.enumFromProto(Dispute.State.class, state.name());
|
||||
}
|
||||
|
||||
public static protobuf.Dispute.State toProtoMessage(Dispute.State state) {
|
||||
return protobuf.Dispute.State.valueOf(state.name());
|
||||
}
|
||||
}
|
||||
|
||||
private final String tradeId;
|
||||
private final String id;
|
||||
private final int traderId;
|
||||
|
@ -66,6 +91,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
// PubKeyRing of trader who opened the dispute
|
||||
private final PubKeyRing traderPubKeyRing;
|
||||
private final long tradeDate;
|
||||
private final long tradePeriodEnd;
|
||||
private final Contract contract;
|
||||
@Nullable
|
||||
private final byte[] contractHash;
|
||||
|
@ -85,7 +111,6 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
private final PubKeyRing agentPubKeyRing; // dispute agent
|
||||
private final boolean isSupportTicket;
|
||||
private final ObservableList<ChatMessage> chatMessages = FXCollections.observableArrayList();
|
||||
private final BooleanProperty isClosedProperty = new SimpleBooleanProperty();
|
||||
// disputeResultProperty.get is Nullable!
|
||||
private final ObjectProperty<DisputeResult> disputeResultProperty = new SimpleObjectProperty<>();
|
||||
private final long openingDate;
|
||||
|
@ -107,10 +132,25 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
@Setter
|
||||
@Nullable
|
||||
private String donationAddressOfDelayedPayoutTx;
|
||||
// Added at v1.6.0
|
||||
private Dispute.State disputeState = State.NEW;
|
||||
|
||||
// Should be only used in emergency case if we need to add data but do not want to break backward compatibility
|
||||
// at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new
|
||||
// field in a class would break that hash and therefore break the storage mechanism.
|
||||
@Nullable
|
||||
@Setter
|
||||
private Map<String, String> extraDataMap;
|
||||
|
||||
// We do not persist uid, it is only used by dispute agents to guarantee an uid.
|
||||
@Setter
|
||||
@Nullable
|
||||
private transient String uid;
|
||||
@Setter
|
||||
private transient long payoutTxConfirms = -1;
|
||||
|
||||
private transient final BooleanProperty isClosedProperty = new SimpleBooleanProperty();
|
||||
private transient final IntegerProperty badgeCountProperty = new SimpleIntegerProperty();
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -124,6 +164,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
boolean disputeOpenerIsMaker,
|
||||
PubKeyRing traderPubKeyRing,
|
||||
long tradeDate,
|
||||
long tradePeriodEnd,
|
||||
Contract contract,
|
||||
@Nullable byte[] contractHash,
|
||||
@Nullable byte[] depositTxSerialized,
|
||||
|
@ -143,6 +184,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
this.disputeOpenerIsMaker = disputeOpenerIsMaker;
|
||||
this.traderPubKeyRing = traderPubKeyRing;
|
||||
this.tradeDate = tradeDate;
|
||||
this.tradePeriodEnd = tradePeriodEnd;
|
||||
this.contract = contract;
|
||||
this.contractHash = contractHash;
|
||||
this.depositTxSerialized = depositTxSerialized;
|
||||
|
@ -158,6 +200,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
|
||||
id = tradeId + "_" + traderId;
|
||||
uid = UUID.randomUUID().toString();
|
||||
refreshAlertLevel(true);
|
||||
}
|
||||
|
||||
|
||||
|
@ -176,6 +219,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
.setDisputeOpenerIsMaker(disputeOpenerIsMaker)
|
||||
.setTraderPubKeyRing(traderPubKeyRing.toProtoMessage())
|
||||
.setTradeDate(tradeDate)
|
||||
.setTradePeriodEnd(tradePeriodEnd)
|
||||
.setContract(contract.toProtoMessage())
|
||||
.setContractAsJson(contractAsJson)
|
||||
.setAgentPubKeyRing(agentPubKeyRing.toProtoMessage())
|
||||
|
@ -183,8 +227,9 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
.addAllChatMessage(clonedChatMessages.stream()
|
||||
.map(msg -> msg.toProtoNetworkEnvelope().getChatMessage())
|
||||
.collect(Collectors.toList()))
|
||||
.setIsClosed(isClosedProperty.get())
|
||||
.setIsClosed(this.isClosed())
|
||||
.setOpeningDate(openingDate)
|
||||
.setState(Dispute.State.toProtoMessage(disputeState))
|
||||
.setId(id);
|
||||
|
||||
Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(e)));
|
||||
|
@ -200,6 +245,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
Optional.ofNullable(mediatorsDisputeResult).ifPresent(result -> builder.setMediatorsDisputeResult(mediatorsDisputeResult));
|
||||
Optional.ofNullable(delayedPayoutTxId).ifPresent(result -> builder.setDelayedPayoutTxId(delayedPayoutTxId));
|
||||
Optional.ofNullable(donationAddressOfDelayedPayoutTx).ifPresent(result -> builder.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx));
|
||||
Optional.ofNullable(getExtraDataMap()).ifPresent(builder::putAllExtraData);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
@ -211,6 +257,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
proto.getDisputeOpenerIsMaker(),
|
||||
PubKeyRing.fromProto(proto.getTraderPubKeyRing()),
|
||||
proto.getTradeDate(),
|
||||
proto.getTradePeriodEnd(),
|
||||
Contract.fromProto(proto.getContract(), coreProtoResolver),
|
||||
ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash()),
|
||||
ProtoUtil.byteArrayOrNullFromProto(proto.getDepositTxSerialized()),
|
||||
|
@ -224,11 +271,13 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
proto.getIsSupportTicket(),
|
||||
SupportType.fromProto(proto.getSupportType()));
|
||||
|
||||
dispute.setExtraDataMap(CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
|
||||
null : ExtraDataMapValidator.getValidatedExtraDataMap(proto.getExtraDataMap()));
|
||||
|
||||
dispute.chatMessages.addAll(proto.getChatMessageList().stream()
|
||||
.map(ChatMessage::fromPayloadProto)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
dispute.isClosedProperty.set(proto.getIsClosed());
|
||||
if (proto.hasDisputeResult())
|
||||
dispute.disputeResultProperty.set(DisputeResult.fromProto(proto.getDisputeResult()));
|
||||
dispute.disputePayoutTxId = ProtoUtil.stringOrNullFromProto(proto.getDisputePayoutTxId());
|
||||
|
@ -248,6 +297,20 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
dispute.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx);
|
||||
}
|
||||
|
||||
if (Dispute.State.fromProto(proto.getState()) == State.NEEDS_UPGRADE) {
|
||||
// old disputes did not have a state field, so choose an appropriate state:
|
||||
dispute.setState(proto.getIsClosed() ? State.CLOSED : State.OPEN);
|
||||
if (dispute.getDisputeState() == State.CLOSED) {
|
||||
// mark chat messages as read for pre-existing CLOSED disputes
|
||||
// otherwise at upgrade, all old disputes would have 1 unread chat message
|
||||
// because currently when a dispute is closed, the last chat message is not marked read
|
||||
dispute.getChatMessages().forEach(m -> m.setWasDisplayed(true));
|
||||
}
|
||||
} else {
|
||||
dispute.setState(Dispute.State.fromProto(proto.getState()));
|
||||
}
|
||||
|
||||
dispute.refreshAlertLevel(true);
|
||||
return dispute;
|
||||
}
|
||||
|
||||
|
@ -269,14 +332,32 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
// Setters
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void setIsClosed(boolean isClosed) {
|
||||
this.isClosedProperty.set(isClosed);
|
||||
public void setIsClosed() {
|
||||
setState(State.CLOSED);
|
||||
}
|
||||
|
||||
public void reOpen() {
|
||||
setState(State.REOPENED);
|
||||
}
|
||||
|
||||
public void setState(Dispute.State disputeState) {
|
||||
this.disputeState = disputeState;
|
||||
this.isClosedProperty.set(disputeState == State.CLOSED);
|
||||
}
|
||||
|
||||
public void setDisputeResult(DisputeResult disputeResult) {
|
||||
disputeResultProperty.set(disputeResult);
|
||||
}
|
||||
|
||||
public void setExtraData(String key, String value) {
|
||||
if (key == null || value == null) {
|
||||
return;
|
||||
}
|
||||
if (extraDataMap == null) {
|
||||
extraDataMap = new HashMap<>();
|
||||
}
|
||||
extraDataMap.put(key, value);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
|
@ -289,7 +370,9 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
public ReadOnlyBooleanProperty isClosedProperty() {
|
||||
return isClosedProperty;
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty getBadgeCountProperty() {
|
||||
return badgeCountProperty;
|
||||
}
|
||||
public ReadOnlyObjectProperty<DisputeResult> disputeResultProperty() {
|
||||
return disputeResultProperty;
|
||||
}
|
||||
|
@ -298,14 +381,64 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
return new Date(tradeDate);
|
||||
}
|
||||
|
||||
public Date getTradePeriodEnd() {
|
||||
return new Date(tradePeriodEnd);
|
||||
}
|
||||
|
||||
public Date getOpeningDate() {
|
||||
return new Date(openingDate);
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
return isClosedProperty.get();
|
||||
public boolean isNew() {
|
||||
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
|
||||
public String toString() {
|
||||
|
@ -313,11 +446,13 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
"\n tradeId='" + tradeId + '\'' +
|
||||
",\n id='" + id + '\'' +
|
||||
",\n uid='" + uid + '\'' +
|
||||
",\n state=" + disputeState +
|
||||
",\n traderId=" + traderId +
|
||||
",\n disputeOpenerIsBuyer=" + disputeOpenerIsBuyer +
|
||||
",\n disputeOpenerIsMaker=" + disputeOpenerIsMaker +
|
||||
",\n traderPubKeyRing=" + traderPubKeyRing +
|
||||
",\n tradeDate=" + tradeDate +
|
||||
",\n tradePeriodEnd=" + tradePeriodEnd +
|
||||
",\n contract=" + contract +
|
||||
",\n contractHash=" + Utilities.bytesAsHexString(contractHash) +
|
||||
",\n depositTxSerialized=" + Utilities.bytesAsHexString(depositTxSerialized) +
|
||||
|
|
|
@ -26,17 +26,14 @@ import bisq.common.persistence.PersistenceManager;
|
|||
import bisq.common.proto.persistable.PersistedDataHost;
|
||||
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.Subscription;
|
||||
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -52,7 +49,6 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
|
|||
protected final PersistenceManager<T> persistenceManager;
|
||||
@Getter
|
||||
private final T disputeList;
|
||||
private final Map<String, Subscription> disputeIsClosedSubscriptionsMap = new HashMap<>();
|
||||
@Getter
|
||||
private final IntegerProperty numOpenDisputes = new SimpleIntegerProperty();
|
||||
@Getter
|
||||
|
@ -153,26 +149,21 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
|
|||
@Nullable List<? extends Dispute> removedList) {
|
||||
if (removedList != null) {
|
||||
removedList.forEach(dispute -> {
|
||||
String id = dispute.getId();
|
||||
if (disputeIsClosedSubscriptionsMap.containsKey(id)) {
|
||||
disputeIsClosedSubscriptionsMap.get(id).unsubscribe();
|
||||
disputeIsClosedSubscriptionsMap.remove(id);
|
||||
}
|
||||
disputedTradeIds.remove(dispute.getTradeId());
|
||||
});
|
||||
}
|
||||
addedList.forEach(dispute -> {
|
||||
String id = dispute.getId();
|
||||
Subscription disputeStateSubscription = EasyBind.subscribe(dispute.isClosedProperty(),
|
||||
isClosed -> {
|
||||
// for each dispute added, keep track of its "BadgeCountProperty"
|
||||
EasyBind.subscribe(dispute.getBadgeCountProperty(),
|
||||
isAlerting -> {
|
||||
// We get the event before the list gets updated, so we execute on next frame
|
||||
UserThread.execute(() -> {
|
||||
int openDisputes = (int) disputeList.getList().stream()
|
||||
.filter(e -> !e.isClosed()).count();
|
||||
numOpenDisputes.set(openDisputes);
|
||||
int numAlerts = (int) disputeList.getList().stream()
|
||||
.mapToLong(x -> x.getBadgeCountProperty().getValue())
|
||||
.sum();
|
||||
numOpenDisputes.set(numAlerts);
|
||||
});
|
||||
});
|
||||
disputeIsClosedSubscriptionsMap.put(id, disputeStateSubscription);
|
||||
disputedTradeIds.add(dispute.getTradeId());
|
||||
});
|
||||
}
|
||||
|
|
|
@ -314,6 +314,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
Dispute dispute = openNewDisputeMessage.getDispute();
|
||||
// Disputes from clients < 1.2.0 always have support type ARBITRATION in dispute as the field didn't exist before
|
||||
dispute.setSupportType(openNewDisputeMessage.getSupportType());
|
||||
// disputes from clients < 1.6.0 have state not set as the field didn't exist before
|
||||
dispute.setState(Dispute.State.NEW); // this can be removed a few months after 1.6.0 release
|
||||
|
||||
Contract contract = dispute.getContract();
|
||||
addPriceInfoMessage(dispute, 0);
|
||||
|
@ -577,6 +579,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
!disputeFromOpener.isDisputeOpenerIsMaker(),
|
||||
pubKeyRing,
|
||||
disputeFromOpener.getTradeDate().getTime(),
|
||||
disputeFromOpener.getTradePeriodEnd().getTime(),
|
||||
contractFromOpener,
|
||||
disputeFromOpener.getContractHash(),
|
||||
disputeFromOpener.getDepositTxSerialized(),
|
||||
|
@ -589,6 +592,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
disputeFromOpener.getAgentPubKeyRing(),
|
||||
disputeFromOpener.isSupportTicket(),
|
||||
disputeFromOpener.getSupportType());
|
||||
dispute.setExtraDataMap(disputeFromOpener.getExtraDataMap());
|
||||
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
|
||||
dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx());
|
||||
|
||||
|
@ -829,6 +833,14 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
.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) {
|
||||
// In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent.
|
||||
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
|
||||
// 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
|
||||
|
|
|
@ -217,7 +217,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
} else {
|
||||
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId());
|
||||
}
|
||||
dispute.setIsClosed(true);
|
||||
dispute.setIsClosed();
|
||||
|
||||
if (dispute.disputeResultProperty().get() != null) {
|
||||
log.warn("We already got a dispute result. That should only happen if a dispute needs to be closed " +
|
||||
|
|
|
@ -193,7 +193,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
|||
} else {
|
||||
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId());
|
||||
}
|
||||
dispute.setIsClosed(true);
|
||||
dispute.setIsClosed();
|
||||
|
||||
dispute.setDisputeResult(disputeResult);
|
||||
|
||||
|
|
|
@ -190,7 +190,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
|||
} else {
|
||||
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId());
|
||||
}
|
||||
dispute.setIsClosed(true);
|
||||
dispute.setIsClosed();
|
||||
|
||||
if (dispute.disputeResultProperty().get() != null) {
|
||||
log.warn("We got already a dispute result. That should only happen if a dispute needs to be closed " +
|
||||
|
|
|
@ -1129,8 +1129,10 @@ support.sellerAddress=BTC seller address
|
|||
support.role=Role
|
||||
support.agent=Support agent
|
||||
support.state=State
|
||||
support.chat=Chat
|
||||
support.closed=Closed
|
||||
support.open=Open
|
||||
support.process=Process
|
||||
support.buyerOfferer=BTC buyer/Maker
|
||||
support.sellerOfferer=BTC seller/Maker
|
||||
support.buyerTaker=BTC buyer/Taker
|
||||
|
@ -1180,7 +1182,8 @@ support.warning.disputesWithInvalidDonationAddress=The delayed payout transactio
|
|||
{3}
|
||||
support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute?
|
||||
support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout.
|
||||
|
||||
support.warning.traderCloseOwnDisputeWarning=Traders can only self-close their support tickets when the trade has been paid out.
|
||||
support.info.disputeReOpened=Dispute ticket has been re-opened.
|
||||
|
||||
####################################################################
|
||||
# Settings
|
||||
|
@ -2536,6 +2539,9 @@ disputeSummaryWindow.payoutAmount.buyer=Buyer's payout amount
|
|||
disputeSummaryWindow.payoutAmount.seller=Seller's payout amount
|
||||
disputeSummaryWindow.payoutAmount.invert=Use loser as publisher
|
||||
disputeSummaryWindow.reason=Reason of dispute
|
||||
disputeSummaryWindow.tradePeriodEnd=Trade period end
|
||||
disputeSummaryWindow.extraInfo=Extra information
|
||||
disputeSummaryWindow.delayedPayoutStatus=Delayed Payout Status
|
||||
|
||||
# dynamic values are not recognized by IntelliJ
|
||||
# suppress inspection "UnusedProperty"
|
||||
|
|
|
@ -184,6 +184,7 @@ public class AccountAgeWitnessServiceTest {
|
|||
true,
|
||||
buyerPubKeyRing,
|
||||
now - 1,
|
||||
now - 1,
|
||||
contract,
|
||||
null,
|
||||
null,
|
||||
|
@ -196,7 +197,7 @@ public class AccountAgeWitnessServiceTest {
|
|||
null,
|
||||
true,
|
||||
SupportType.ARBITRATION));
|
||||
disputes.get(0).getIsClosedProperty().set(true);
|
||||
disputes.get(0).setIsClosed();
|
||||
disputes.get(0).getDisputeResultProperty().set(new DisputeResult(
|
||||
"trade1",
|
||||
1,
|
||||
|
|
|
@ -84,16 +84,16 @@ import javafx.geometry.Insets;
|
|||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static bisq.desktop.util.FormBuilder.add2ButtonsWithBox;
|
||||
import static bisq.desktop.util.FormBuilder.addConfirmationLabelLabel;
|
||||
import static bisq.desktop.util.FormBuilder.addTitledGroupBg;
|
||||
import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox;
|
||||
import static bisq.desktop.util.FormBuilder.*;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
@Slf4j
|
||||
|
@ -170,12 +170,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
}
|
||||
}
|
||||
|
||||
public DisputeSummaryWindow onFinalizeDispute(Runnable finalizeDisputeHandler) {
|
||||
this.finalizeDisputeHandlerOptional = Optional.of(finalizeDisputeHandler);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Protected
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -288,17 +282,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
addConfirmationLabelLabel(gridPane, rowIndex, Res.get("shared.tradeId"), dispute.getShortTradeId(),
|
||||
Layout.TWICE_FIRST_ROW_DISTANCE);
|
||||
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.openDate"), DisplayUtils.formatDateTime(dispute.getOpeningDate()));
|
||||
if (dispute.isDisputeOpenerIsMaker()) {
|
||||
if (dispute.isDisputeOpenerIsBuyer())
|
||||
role = Res.get("support.buyerOfferer");
|
||||
else
|
||||
role = Res.get("support.sellerOfferer");
|
||||
} else {
|
||||
if (dispute.isDisputeOpenerIsBuyer())
|
||||
role = Res.get("support.buyerTaker");
|
||||
else
|
||||
role = Res.get("support.sellerTaker");
|
||||
}
|
||||
role = dispute.getRoleString();
|
||||
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.role"), role);
|
||||
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.tradeAmount"),
|
||||
formatter.formatCoinWithCode(contract.getTradeAmount()));
|
||||
|
@ -314,6 +298,24 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
" " +
|
||||
formatter.formatCoinWithCode(contract.getOfferPayload().getSellerSecurityDeposit());
|
||||
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit);
|
||||
|
||||
boolean isMediationDispute = getDisputeManager(dispute) instanceof MediationManager;
|
||||
if (isMediationDispute) {
|
||||
if (dispute.getTradePeriodEnd().getTime() > 0) {
|
||||
String status = DisplayUtils.formatDateTime(dispute.getTradePeriodEnd());
|
||||
Label tradePeriodEnd = addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.tradePeriodEnd"), status).second;
|
||||
if (dispute.getTradePeriodEnd().toInstant().isAfter(Instant.now())) {
|
||||
tradePeriodEnd.getStyleClass().add("version-new"); // highlight field when the trade period is still active
|
||||
}
|
||||
}
|
||||
if (dispute.getExtraDataMap() != null && dispute.getExtraDataMap().size() > 0) {
|
||||
String extraDataSummary = "";
|
||||
for (Map.Entry<String, String> entry : dispute.getExtraDataMap().entrySet()) {
|
||||
extraDataSummary += "[" + entry.getKey() + ":" + entry.getValue() + "] ";
|
||||
}
|
||||
addConfirmationLabelLabelWithCopyIcon(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.extraInfo"), extraDataSummary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addTradeAmountPayoutControls() {
|
||||
|
@ -812,7 +814,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected());
|
||||
disputeResult.setCloseDate(new Date());
|
||||
dispute.setDisputeResult(disputeResult);
|
||||
dispute.setIsClosed(true);
|
||||
dispute.setIsClosed();
|
||||
DisputeResult.Reason reason = disputeResult.getReason();
|
||||
|
||||
summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty());
|
||||
|
|
|
@ -537,6 +537,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
|||
isMaker,
|
||||
pubKeyRing,
|
||||
trade.getDate().getTime(),
|
||||
trade.getMaxTradePeriodDate().getTime(),
|
||||
trade.getContract(),
|
||||
trade.getContractHash(),
|
||||
depositTxSerialized,
|
||||
|
@ -549,6 +550,8 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
|||
mediatorPubKeyRing,
|
||||
isSupportTicket,
|
||||
SupportType.MEDIATION);
|
||||
dispute.setExtraData("counterCurrencyTxId", trade.getCounterCurrencyTxId());
|
||||
dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData());
|
||||
|
||||
dispute.setDonationAddressOfDelayedPayoutTx(donationAddressString.get());
|
||||
if (delayedPayoutTx != null) {
|
||||
|
@ -598,6 +601,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
|||
isMaker,
|
||||
pubKeyRing,
|
||||
trade.getDate().getTime(),
|
||||
trade.getMaxTradePeriodDate().getTime(),
|
||||
trade.getContract(),
|
||||
trade.getContractHash(),
|
||||
depositTxSerialized,
|
||||
|
@ -610,6 +614,8 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
|||
refundAgentPubKeyRing,
|
||||
isSupportTicket,
|
||||
SupportType.REFUND);
|
||||
dispute.setExtraData("counterCurrencyTxId", trade.getCounterCurrencyTxId());
|
||||
dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData());
|
||||
|
||||
String tradeId = dispute.getTradeId();
|
||||
mediationManager.findDispute(tradeId)
|
||||
|
|
|
@ -403,7 +403,7 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
|
|||
model.dataModel.getTradeManager().requestPersistence();
|
||||
tradeIdOfOpenChat = trade.getId();
|
||||
|
||||
ChatView chatView = new ChatView(traderChatManager, formatter);
|
||||
ChatView chatView = new ChatView(traderChatManager, formatter, Res.get("offerbook.trader"));
|
||||
chatView.setAllowAttachments(false);
|
||||
chatView.setDisplayHeader(false);
|
||||
chatView.initialize();
|
||||
|
|
|
@ -137,10 +137,12 @@ public class ChatView extends AnchorPane {
|
|||
private EventHandler<KeyEvent> keyEventEventHandler;
|
||||
private SupportManager supportManager;
|
||||
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.formatter = formatter;
|
||||
this.counterpartyName = counterpartyName;
|
||||
allowAttachments = true;
|
||||
displayHeader = true;
|
||||
}
|
||||
|
@ -414,7 +416,11 @@ public class ChatView extends AnchorPane {
|
|||
AnchorPane.setLeftAnchor(statusHBox, padding);
|
||||
}
|
||||
AnchorPane.setBottomAnchor(statusHBox, 7d);
|
||||
headerLabel.setText(DisplayUtils.formatDateTime(new Date(message.getDate())));
|
||||
String metaData = DisplayUtils.formatDateTime(new Date(message.getDate()));
|
||||
if (!message.isSystemMessage())
|
||||
metaData = (isMyMsg ? "Sent " : "Received ") + metaData
|
||||
+ (isMyMsg ? "" : " from " + counterpartyName);
|
||||
headerLabel.setText(metaData);
|
||||
messageLabel.setText(message.getMessage());
|
||||
attachmentsBox.getChildren().clear();
|
||||
if (allowAttachments &&
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -29,8 +29,8 @@ import bisq.desktop.main.overlays.windows.DisputeSummaryWindow;
|
|||
import bisq.desktop.main.overlays.windows.SendPrivateNotificationWindow;
|
||||
import bisq.desktop.main.overlays.windows.TradeDetailsWindow;
|
||||
import bisq.desktop.main.overlays.windows.VerifyDisputeResultSignatureWindow;
|
||||
import bisq.desktop.main.shared.ChatView;
|
||||
import bisq.desktop.util.DisplayUtils;
|
||||
import bisq.desktop.util.FormBuilder;
|
||||
import bisq.desktop.util.GUIUtil;
|
||||
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
|
@ -45,17 +45,20 @@ import bisq.core.support.dispute.DisputeManager;
|
|||
import bisq.core.support.dispute.DisputeResult;
|
||||
import bisq.core.support.dispute.DisputeSession;
|
||||
import bisq.core.support.dispute.agent.DisputeAgentLookupMap;
|
||||
import bisq.core.support.dispute.mediation.MediationManager;
|
||||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
import bisq.core.trade.Contract;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.util.Utilities;
|
||||
|
@ -64,10 +67,13 @@ import org.bitcoinj.core.Coin;
|
|||
|
||||
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
|
||||
|
||||
import com.jfoenix.controls.JFXBadge;
|
||||
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableRow;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
@ -77,6 +83,7 @@ import javafx.scene.layout.VBox;
|
|||
import javafx.scene.text.Text;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.Subscription;
|
||||
|
@ -110,6 +117,7 @@ import lombok.Getter;
|
|||
import javax.annotation.Nullable;
|
||||
|
||||
import static bisq.desktop.util.FormBuilder.getIconForLabel;
|
||||
import static bisq.desktop.util.FormBuilder.getRegularIconButton;
|
||||
|
||||
public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
||||
public enum FilterResult {
|
||||
|
@ -143,6 +151,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
protected final KeyRing keyRing;
|
||||
private final TradeManager tradeManager;
|
||||
protected final CoinFormatter formatter;
|
||||
protected final Preferences preferences;
|
||||
protected final DisputeSummaryWindow disputeSummaryWindow;
|
||||
private final PrivateNotificationManager privateNotificationManager;
|
||||
private final ContractWindow contractWindow;
|
||||
|
@ -160,19 +169,21 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
@Getter
|
||||
protected Dispute selectedDispute;
|
||||
|
||||
protected ChatView chatView;
|
||||
|
||||
private ChangeListener<Boolean> selectedDisputeClosedPropertyListener;
|
||||
private Subscription selectedDisputeSubscription;
|
||||
protected FilteredList<Dispute> filteredList;
|
||||
protected InputTextField filterTextField;
|
||||
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<>();
|
||||
@Nullable
|
||||
private ListChangeListener<Dispute> disputesListener; // Only set in mediation cases
|
||||
protected Label alertIconLabel;
|
||||
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,
|
||||
TradeManager tradeManager,
|
||||
CoinFormatter formatter,
|
||||
Preferences preferences,
|
||||
DisputeSummaryWindow disputeSummaryWindow,
|
||||
PrivateNotificationManager privateNotificationManager,
|
||||
ContractWindow contractWindow,
|
||||
|
@ -196,6 +208,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
this.keyRing = keyRing;
|
||||
this.tradeManager = tradeManager;
|
||||
this.formatter = formatter;
|
||||
this.preferences = preferences;
|
||||
this.disputeSummaryWindow = disputeSummaryWindow;
|
||||
this.privateNotificationManager = privateNotificationManager;
|
||||
this.contractWindow = contractWindow;
|
||||
|
@ -205,6 +218,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
this.refundAgentManager = refundAgentManager;
|
||||
this.daoFacade = daoFacade;
|
||||
this.useDevPrivilegeKeys = useDevPrivilegeKeys;
|
||||
DisputeChatPopup.ChatCallback chatCallback = this::handleOnProcessDispute;
|
||||
chatPopup = new DisputeChatPopup(disputeManager, formatter, preferences, chatCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -238,6 +253,15 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
reOpenDisputeFromButton();
|
||||
});
|
||||
|
||||
closeButton = new AutoTooltipButton(Res.get("support.closeTicket"));
|
||||
closeButton.setDisable(true);
|
||||
closeButton.setVisible(false);
|
||||
closeButton.setManaged(false);
|
||||
HBox.setHgrow(closeButton, Priority.NEVER);
|
||||
closeButton.setOnAction(e -> {
|
||||
closeDisputeFromButton();
|
||||
});
|
||||
|
||||
sendPrivateNotificationButton = new AutoTooltipButton(Res.get("support.sendNotificationButton.label"));
|
||||
sendPrivateNotificationButton.setDisable(true);
|
||||
sendPrivateNotificationButton.setVisible(false);
|
||||
|
@ -279,6 +303,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
alertIconLabel,
|
||||
spacer,
|
||||
reOpenButton,
|
||||
closeButton,
|
||||
sendPrivateNotificationButton,
|
||||
reportButton,
|
||||
fullReportButton,
|
||||
|
@ -292,11 +317,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
root.getChildren().addAll(filterBox, tableView);
|
||||
|
||||
setupTable();
|
||||
|
||||
selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> chatView.setInputBoxVisible(!newValue);
|
||||
|
||||
chatView = new ChatView(disputeManager, formatter);
|
||||
chatView.initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -311,7 +331,17 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
sortedList.comparatorProperty().bind(tableView.comparatorProperty());
|
||||
tableView.setItems(sortedList);
|
||||
|
||||
// sortedList.setComparator((o1, o2) -> o2.getOpeningDate().compareTo(o1.getOpeningDate()));
|
||||
// double-click on a row opens chat window
|
||||
tableView.setRowFactory( tv -> {
|
||||
TableRow<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);
|
||||
|
||||
Dispute selectedItem = tableView.getSelectionModel().getSelectedItem();
|
||||
|
@ -320,11 +350,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
else if (sortedList.size() > 0)
|
||||
tableView.getSelectionModel().select(0);
|
||||
|
||||
if (chatView != null) {
|
||||
chatView.activate();
|
||||
chatView.scrollToBottom();
|
||||
}
|
||||
|
||||
GUIUtil.requestFocus(filterTextField);
|
||||
}
|
||||
|
||||
|
@ -333,10 +358,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
filterTextField.textProperty().removeListener(filterTextFieldListener);
|
||||
sortedList.comparatorProperty().unbind();
|
||||
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 boolean senderFlag(); // implemented in the agent / client views
|
||||
|
||||
protected void applyFilteredListPredicate(String filterString) {
|
||||
AtomicReference<FilterResult> filterResult = new AtomicReference<>(FilterResult.NO_FILTER);
|
||||
filteredList.setPredicate(dispute -> {
|
||||
|
@ -468,18 +491,37 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
return FilterResult.NO_MATCH;
|
||||
}
|
||||
|
||||
|
||||
// a derived version in the ClientView for users pops up an "Are you sure" box first.
|
||||
// this version includes the sending of an automatic message to the user, see addMediationReOpenedMessage
|
||||
protected void reOpenDisputeFromButton() {
|
||||
reOpenDispute();
|
||||
disputeManager.addMediationReOpenedMessage(selectedDispute, false);
|
||||
}
|
||||
|
||||
protected abstract void handleOnSelectDispute(Dispute dispute);
|
||||
// only applicable to traders
|
||||
// only allow them to close the dispute if the trade is paid out
|
||||
// the reason for having this is that sometimes traders end up with closed disputes that are not "closed" @pazza
|
||||
protected void closeDisputeFromButton() {
|
||||
Optional<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() {
|
||||
if (selectedDispute != null) {
|
||||
selectedDispute.setIsClosed(false);
|
||||
handleOnSelectDispute(selectedDispute);
|
||||
if (selectedDispute != null && selectedDispute.isClosed()) {
|
||||
selectedDispute.reOpen();
|
||||
handleOnProcessDispute(selectedDispute);
|
||||
disputeManager.requestPersistence();
|
||||
onSelectDispute(selectedDispute);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -488,46 +530,21 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
// UI actions
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void onOpenContract(Dispute dispute) {
|
||||
protected void onOpenContract(Dispute dispute) {
|
||||
dispute.setDisputeSeen(senderFlag());
|
||||
contractWindow.show(dispute);
|
||||
}
|
||||
|
||||
private void removeListenersOnSelectDispute() {
|
||||
if (selectedDispute != null) {
|
||||
if (selectedDisputeClosedPropertyListener != null)
|
||||
selectedDispute.isClosedProperty().removeListener(selectedDisputeClosedPropertyListener);
|
||||
}
|
||||
}
|
||||
|
||||
private void addListenersOnSelectDispute() {
|
||||
if (selectedDispute != null)
|
||||
selectedDispute.isClosedProperty().addListener(selectedDisputeClosedPropertyListener);
|
||||
}
|
||||
|
||||
private void onSelectDispute(Dispute dispute) {
|
||||
removeListenersOnSelectDispute();
|
||||
if (dispute == null) {
|
||||
if (root.getChildren().size() > 2) {
|
||||
root.getChildren().remove(2);
|
||||
}
|
||||
|
||||
selectedDispute = null;
|
||||
} else if (selectedDispute != dispute) {
|
||||
selectedDispute = dispute;
|
||||
if (chatView != null) {
|
||||
handleOnSelectDispute(dispute);
|
||||
}
|
||||
|
||||
if (root.getChildren().size() > 2) {
|
||||
root.getChildren().remove(2);
|
||||
}
|
||||
root.getChildren().add(2, chatView);
|
||||
}
|
||||
|
||||
reOpenButton.setDisable(selectedDispute == null || !selectedDispute.isClosed());
|
||||
closeButton.setDisable(selectedDispute == null || selectedDispute.isClosed());
|
||||
sendPrivateNotificationButton.setDisable(selectedDispute == null);
|
||||
|
||||
addListenersOnSelectDispute();
|
||||
}
|
||||
|
||||
|
||||
|
@ -887,10 +904,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
tableView.setPlaceholder(placeholder);
|
||||
tableView.getSelectionModel().clearSelection();
|
||||
|
||||
tableView.getColumns().add(getSelectColumn());
|
||||
|
||||
TableColumn<Dispute, Dispute> contractColumn = getContractColumn();
|
||||
tableView.getColumns().add(contractColumn);
|
||||
tableView.getColumns().add(getContractColumn());
|
||||
|
||||
TableColumn<Dispute, Dispute> dateColumn = getDateColumn();
|
||||
tableView.getColumns().add(dateColumn);
|
||||
|
@ -904,18 +918,19 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
TableColumn<Dispute, Dispute> sellerOnionAddressColumn = getSellerOnionAddressColumn();
|
||||
tableView.getColumns().add(sellerOnionAddressColumn);
|
||||
|
||||
|
||||
TableColumn<Dispute, Dispute> marketColumn = getMarketColumn();
|
||||
tableView.getColumns().add(marketColumn);
|
||||
|
||||
TableColumn<Dispute, Dispute> roleColumn = getRoleColumn();
|
||||
tableView.getColumns().add(roleColumn);
|
||||
tableView.getColumns().add(getRoleColumn());
|
||||
|
||||
maybeAddAgentColumn();
|
||||
|
||||
stateColumn = getStateColumn();
|
||||
tableView.getColumns().add(stateColumn);
|
||||
|
||||
maybeAddProcessColumn();
|
||||
tableView.getColumns().add(getChatColumn());
|
||||
|
||||
tradeIdColumn.setComparator(Comparator.comparing(Dispute::getTradeId));
|
||||
dateColumn.setComparator(Comparator.comparing(Dispute::getOpeningDate));
|
||||
buyerOnionAddressColumn.setComparator(Comparator.comparing(this::getBuyerOnionAddressColumnLabel));
|
||||
|
@ -926,6 +941,10 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
tableView.getSortOrder().add(dateColumn);
|
||||
}
|
||||
|
||||
protected void maybeAddProcessColumn() {
|
||||
// Only relevant client views will impl it
|
||||
}
|
||||
|
||||
protected void maybeAddAgentColumn() {
|
||||
// Only relevant client views will impl it
|
||||
}
|
||||
|
@ -935,41 +954,42 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
return null;
|
||||
}
|
||||
|
||||
private TableColumn<Dispute, Dispute> getSelectColumn() {
|
||||
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("shared.select"));
|
||||
column.setMinWidth(80);
|
||||
column.setMaxWidth(80);
|
||||
column.setSortable(false);
|
||||
column.getStyleClass().add("first-column");
|
||||
|
||||
column.setCellValueFactory((addressListItem) ->
|
||||
new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
|
||||
private TableColumn<Dispute, Dispute> getContractColumn() {
|
||||
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("shared.details")) {
|
||||
{
|
||||
setMaxWidth(150);
|
||||
setMinWidth(80);
|
||||
getStyleClass().addAll("first-column", "avatar-column");
|
||||
setSortable(false);
|
||||
}
|
||||
};
|
||||
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
|
||||
column.setCellFactory(
|
||||
new Callback<>() {
|
||||
|
||||
@Override
|
||||
public TableCell<Dispute, Dispute> call(TableColumn<Dispute,
|
||||
Dispute> column) {
|
||||
public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
|
||||
return new TableCell<>() {
|
||||
|
||||
Button button;
|
||||
|
||||
@Override
|
||||
public void updateItem(final Dispute item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (item != null && !empty) {
|
||||
if (button == null) {
|
||||
button = new AutoTooltipButton(Res.get("shared.select"));
|
||||
setGraphic(button);
|
||||
}
|
||||
button.setOnAction(e -> tableView.getSelectionModel().select(item));
|
||||
Button button = getRegularIconButton(MaterialDesignIcon.INFORMATION_OUTLINE);
|
||||
JFXBadge badge = new JFXBadge(new Label(""), Pos.BASELINE_RIGHT);
|
||||
badge.setPosition(Pos.TOP_RIGHT);
|
||||
badge.setVisible(item.isNew());
|
||||
badge.setText("New");
|
||||
badge.getStyleClass().add("new");
|
||||
newBadgeByDispute.put(item.getId(), badge);
|
||||
HBox hBox = new HBox(button, badge);
|
||||
setGraphic(hBox);
|
||||
button.setOnAction(e -> {
|
||||
tableView.getSelectionModel().select(this.getIndex());
|
||||
onOpenContract(item);
|
||||
item.setDisputeSeen(senderFlag());
|
||||
badge.setVisible(item.isNew());
|
||||
});
|
||||
} else {
|
||||
setGraphic(null);
|
||||
if (button != null) {
|
||||
button.setOnAction(null);
|
||||
button = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -978,38 +998,94 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
return column;
|
||||
}
|
||||
|
||||
private TableColumn<Dispute, Dispute> getContractColumn() {
|
||||
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("shared.details")) {
|
||||
protected TableColumn<Dispute, Dispute> getProcessColumn() {
|
||||
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("support.process")) {
|
||||
{
|
||||
setMinWidth(80);
|
||||
setMaxWidth(50);
|
||||
setMinWidth(50);
|
||||
getStyleClass().addAll("avatar-column");
|
||||
setSortable(false);
|
||||
}
|
||||
};
|
||||
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
|
||||
column.setCellFactory(
|
||||
new Callback<>() {
|
||||
|
||||
@Override
|
||||
public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
|
||||
return new TableCell<>() {
|
||||
Button button;
|
||||
|
||||
@Override
|
||||
public void updateItem(final Dispute item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (item != null && !empty) {
|
||||
if (button == null) {
|
||||
button = new AutoTooltipButton(Res.get("shared.details"));
|
||||
setGraphic(button);
|
||||
}
|
||||
button.setOnAction(e -> onOpenContract(item));
|
||||
Button button = getRegularIconButton(MaterialDesignIcon.GAVEL);
|
||||
button.setOnAction(e -> {
|
||||
tableView.getSelectionModel().select(this.getIndex());
|
||||
handleOnProcessDispute(item);
|
||||
item.setDisputeSeen(senderFlag());
|
||||
newBadgeByDispute.get(item.getId()).setVisible(item.isNew());
|
||||
});
|
||||
HBox hBox = new HBox(button);
|
||||
hBox.setAlignment(Pos.CENTER);
|
||||
setGraphic(hBox);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
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) {
|
||||
super.updateItem(item, empty);
|
||||
if (item != null && !empty) {
|
||||
if (item.isDisputeOpenerIsMaker())
|
||||
setText(item.isDisputeOpenerIsBuyer() ? Res.get("support.buyerOfferer") : Res.get("support.sellerOfferer"));
|
||||
else
|
||||
setText(item.isDisputeOpenerIsBuyer() ? Res.get("support.buyerTaker") : Res.get("support.sellerTaker"));
|
||||
setText(item.getRoleString());
|
||||
} else {
|
||||
setText("");
|
||||
}
|
||||
|
@ -1312,6 +1385,43 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
});
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
|
||||
package bisq.desktop.main.support.dispute.agent;
|
||||
|
||||
import bisq.desktop.components.AutoTooltipButton;
|
||||
import bisq.desktop.components.AutoTooltipTableColumn;
|
||||
import bisq.desktop.main.overlays.popups.Popup;
|
||||
import bisq.desktop.main.overlays.windows.ContractWindow;
|
||||
|
@ -32,13 +31,13 @@ import bisq.core.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.support.dispute.agent.MultipleHolderNameDetection;
|
||||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
|
||||
import bisq.core.trade.TradeDataValidation;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.user.DontShowAgainLookup;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
||||
import bisq.common.crypto.KeyRing;
|
||||
|
@ -46,7 +45,6 @@ import bisq.common.util.Utilities;
|
|||
|
||||
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
|
||||
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
|
@ -76,6 +74,7 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
|
|||
KeyRing keyRing,
|
||||
TradeManager tradeManager,
|
||||
CoinFormatter formatter,
|
||||
Preferences preferences,
|
||||
DisputeSummaryWindow disputeSummaryWindow,
|
||||
PrivateNotificationManager privateNotificationManager,
|
||||
ContractWindow contractWindow,
|
||||
|
@ -89,6 +88,7 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
|
|||
keyRing,
|
||||
tradeManager,
|
||||
formatter,
|
||||
preferences,
|
||||
disputeSummaryWindow,
|
||||
privateNotificationManager,
|
||||
contractWindow,
|
||||
|
@ -216,14 +216,8 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void handleOnSelectDispute(Dispute dispute) {
|
||||
Button closeDisputeButton = null;
|
||||
if (!dispute.isClosed() && !disputeManager.isTrader(dispute)) {
|
||||
closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket"));
|
||||
closeDisputeButton.setOnAction(e -> onCloseDispute(getSelectedDispute()));
|
||||
}
|
||||
DisputeSession chatSession = getConcreteDisputeChatSession(dispute);
|
||||
chatView.display(chatSession, closeDisputeButton, root.widthProperty());
|
||||
protected void handleOnProcessDispute(Dispute dispute) {
|
||||
onCloseDispute(dispute);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -352,6 +346,16 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
|
|||
"to them so they can ban those traders.\n\n" +
|
||||
Utilities.toTruncatedString(report, 700, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void maybeAddProcessColumn() {
|
||||
tableView.getColumns().add(getProcessColumn());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean senderFlag() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ import bisq.core.support.dispute.arbitration.ArbitrationSession;
|
|||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
||||
|
@ -53,6 +54,7 @@ public class ArbitratorView extends DisputeAgentView {
|
|||
KeyRing keyRing,
|
||||
TradeManager tradeManager,
|
||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
|
||||
Preferences preferences,
|
||||
DisputeSummaryWindow disputeSummaryWindow,
|
||||
PrivateNotificationManager privateNotificationManager,
|
||||
ContractWindow contractWindow,
|
||||
|
@ -66,6 +68,7 @@ public class ArbitratorView extends DisputeAgentView {
|
|||
keyRing,
|
||||
tradeManager,
|
||||
formatter,
|
||||
preferences,
|
||||
disputeSummaryWindow,
|
||||
privateNotificationManager,
|
||||
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
|
||||
// their old Bisq version if still cases are pending.
|
||||
if (protocolVersion == 1) {
|
||||
disputeSummaryWindow.onFinalizeDispute(() -> chatView.removeInputBox()).show(dispute);
|
||||
chatPopup.closeChat();
|
||||
disputeSummaryWindow.show(dispute);
|
||||
} else {
|
||||
new Popup().warning(Res.get("support.wrongVersion", protocolVersion)).show();
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import bisq.core.support.dispute.mediation.MediationSession;
|
|||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
||||
|
@ -51,6 +52,7 @@ public class MediatorView extends DisputeAgentView {
|
|||
KeyRing keyRing,
|
||||
TradeManager tradeManager,
|
||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
|
||||
Preferences preferences,
|
||||
DisputeSummaryWindow disputeSummaryWindow,
|
||||
PrivateNotificationManager privateNotificationManager,
|
||||
ContractWindow contractWindow,
|
||||
|
@ -64,6 +66,7 @@ public class MediatorView extends DisputeAgentView {
|
|||
keyRing,
|
||||
tradeManager,
|
||||
formatter,
|
||||
preferences,
|
||||
disputeSummaryWindow,
|
||||
privateNotificationManager,
|
||||
contractWindow,
|
||||
|
@ -112,6 +115,7 @@ public class MediatorView extends DisputeAgentView {
|
|||
|
||||
@Override
|
||||
protected void onCloseDispute(Dispute dispute) {
|
||||
disputeSummaryWindow.onFinalizeDispute(() -> chatView.removeInputBox()).show(dispute);
|
||||
chatPopup.closeChat();
|
||||
disputeSummaryWindow.show(dispute);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import bisq.core.support.dispute.refund.RefundManager;
|
|||
import bisq.core.support.dispute.refund.RefundSession;
|
||||
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
||||
|
@ -53,6 +54,7 @@ public class RefundAgentView extends DisputeAgentView {
|
|||
KeyRing keyRing,
|
||||
TradeManager tradeManager,
|
||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
|
||||
Preferences preferences,
|
||||
DisputeSummaryWindow disputeSummaryWindow,
|
||||
PrivateNotificationManager privateNotificationManager,
|
||||
ContractWindow contractWindow,
|
||||
|
@ -66,6 +68,7 @@ public class RefundAgentView extends DisputeAgentView {
|
|||
keyRing,
|
||||
tradeManager,
|
||||
formatter,
|
||||
preferences,
|
||||
disputeSummaryWindow,
|
||||
privateNotificationManager,
|
||||
contractWindow,
|
||||
|
@ -92,7 +95,8 @@ public class RefundAgentView extends DisputeAgentView {
|
|||
long protocolVersion = dispute.getContract().getOfferPayload().getProtocolVersion();
|
||||
// Refund agent was introduced with protocolVersion version 2. We do not support old trade protocol cases.
|
||||
if (protocolVersion >= 2) {
|
||||
disputeSummaryWindow.onFinalizeDispute(() -> chatView.removeInputBox()).show(dispute);
|
||||
chatPopup.closeChat();
|
||||
disputeSummaryWindow.show(dispute);
|
||||
} else {
|
||||
new Popup().warning(Res.get("support.wrongVersion", protocolVersion)).show();
|
||||
}
|
||||
|
|
|
@ -28,10 +28,10 @@ import bisq.core.dao.DaoFacade;
|
|||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.DisputeList;
|
||||
import bisq.core.support.dispute.DisputeManager;
|
||||
import bisq.core.support.dispute.DisputeSession;
|
||||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
||||
import bisq.common.crypto.KeyRing;
|
||||
|
@ -41,6 +41,7 @@ public abstract class DisputeClientView extends DisputeView {
|
|||
KeyRing keyRing,
|
||||
TradeManager tradeManager,
|
||||
CoinFormatter formatter,
|
||||
Preferences preferences,
|
||||
DisputeSummaryWindow disputeSummaryWindow,
|
||||
PrivateNotificationManager privateNotificationManager,
|
||||
ContractWindow contractWindow,
|
||||
|
@ -50,17 +51,11 @@ public abstract class DisputeClientView extends DisputeView {
|
|||
RefundAgentManager refundAgentManager,
|
||||
DaoFacade daoFacade,
|
||||
boolean useDevPrivilegeKeys) {
|
||||
super(DisputeManager, keyRing, tradeManager, formatter, disputeSummaryWindow, privateNotificationManager,
|
||||
super(DisputeManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow, privateNotificationManager,
|
||||
contractWindow, tradeDetailsWindow, accountAgeWitnessService,
|
||||
mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleOnSelectDispute(Dispute dispute) {
|
||||
DisputeSession chatSession = getConcreteDisputeChatSession(dispute);
|
||||
chatView.display(chatSession, root.widthProperty());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DisputeView.FilterResult getFilterResult(Dispute dispute, String filterString) {
|
||||
// As we are in the client view we hide disputes where we are the agent
|
||||
|
@ -70,4 +65,9 @@ public abstract class DisputeClientView extends DisputeView {
|
|||
|
||||
return super.getFilterResult(dispute, filterString);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean senderFlag() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import bisq.core.support.dispute.arbitration.ArbitrationSession;
|
|||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
||||
|
@ -50,6 +51,7 @@ public class ArbitrationClientView extends DisputeClientView {
|
|||
KeyRing keyRing,
|
||||
TradeManager tradeManager,
|
||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
|
||||
Preferences preferences,
|
||||
DisputeSummaryWindow disputeSummaryWindow,
|
||||
PrivateNotificationManager privateNotificationManager,
|
||||
ContractWindow contractWindow,
|
||||
|
@ -59,7 +61,7 @@ public class ArbitrationClientView extends DisputeClientView {
|
|||
RefundAgentManager refundAgentManager,
|
||||
DaoFacade daoFacade,
|
||||
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
|
||||
super(arbitrationManager, keyRing, tradeManager, formatter, disputeSummaryWindow,
|
||||
super(arbitrationManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow,
|
||||
privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService,
|
||||
mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
|||
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
|
||||
import bisq.core.trade.Contract;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
||||
|
@ -48,8 +49,6 @@ import bisq.common.crypto.KeyRing;
|
|||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import javafx.scene.control.TableColumn;
|
||||
|
||||
@FxmlView
|
||||
public class MediationClientView extends DisputeClientView {
|
||||
@Inject
|
||||
|
@ -57,6 +56,7 @@ public class MediationClientView extends DisputeClientView {
|
|||
KeyRing keyRing,
|
||||
TradeManager tradeManager,
|
||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
|
||||
Preferences preferences,
|
||||
DisputeSummaryWindow disputeSummaryWindow,
|
||||
PrivateNotificationManager privateNotificationManager,
|
||||
ContractWindow contractWindow,
|
||||
|
@ -66,7 +66,7 @@ public class MediationClientView extends DisputeClientView {
|
|||
RefundAgentManager refundAgentManager,
|
||||
DaoFacade daoFacade,
|
||||
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
|
||||
super(mediationManager, keyRing, tradeManager, formatter, disputeSummaryWindow,
|
||||
super(mediationManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow,
|
||||
privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService,
|
||||
mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
|
||||
}
|
||||
|
@ -76,6 +76,8 @@ public class MediationClientView extends DisputeClientView {
|
|||
super.initialize();
|
||||
reOpenButton.setVisible(true);
|
||||
reOpenButton.setManaged(true);
|
||||
closeButton.setVisible(true);
|
||||
closeButton.setManaged(true);
|
||||
setupReOpenDisputeListener();
|
||||
}
|
||||
|
||||
|
@ -105,7 +107,7 @@ public class MediationClientView extends DisputeClientView {
|
|||
protected void reOpenDisputeFromButton() {
|
||||
new Popup().attention(Res.get("support.reOpenByTrader.prompt"))
|
||||
.actionButtonText(Res.get("shared.yes"))
|
||||
.onAction(this::reOpenDispute)
|
||||
.onAction(() -> reOpenDispute())
|
||||
.show();
|
||||
}
|
||||
|
||||
|
@ -116,7 +118,6 @@ public class MediationClientView extends DisputeClientView {
|
|||
|
||||
@Override
|
||||
protected void maybeAddAgentColumn() {
|
||||
TableColumn<Dispute, Dispute> agentColumn = getAgentColumn();
|
||||
tableView.getColumns().add(agentColumn);
|
||||
tableView.getColumns().add(getAgentColumn());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import bisq.core.support.dispute.refund.RefundSession;
|
|||
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
|
||||
import bisq.core.trade.Contract;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
||||
|
@ -46,8 +47,6 @@ import bisq.common.crypto.KeyRing;
|
|||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import javafx.scene.control.TableColumn;
|
||||
|
||||
@FxmlView
|
||||
public class RefundClientView extends DisputeClientView {
|
||||
@Inject
|
||||
|
@ -55,6 +54,7 @@ public class RefundClientView extends DisputeClientView {
|
|||
KeyRing keyRing,
|
||||
TradeManager tradeManager,
|
||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
|
||||
Preferences preferences,
|
||||
DisputeSummaryWindow disputeSummaryWindow,
|
||||
PrivateNotificationManager privateNotificationManager,
|
||||
ContractWindow contractWindow,
|
||||
|
@ -64,7 +64,7 @@ public class RefundClientView extends DisputeClientView {
|
|||
RefundAgentManager refundAgentManager,
|
||||
DaoFacade daoFacade,
|
||||
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
|
||||
super(refundManager, keyRing, tradeManager, formatter, disputeSummaryWindow,
|
||||
super(refundManager, keyRing, tradeManager, formatter, preferences, disputeSummaryWindow,
|
||||
privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService,
|
||||
mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
|
||||
}
|
||||
|
@ -86,7 +86,6 @@ public class RefundClientView extends DisputeClientView {
|
|||
|
||||
@Override
|
||||
protected void maybeAddAgentColumn() {
|
||||
TableColumn<Dispute, Dispute> agentColumn = getAgentColumn();
|
||||
tableView.getColumns().add(agentColumn);
|
||||
tableView.getColumns().add(getAgentColumn());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -392,6 +392,21 @@ public class FormBuilder {
|
|||
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,
|
||||
int rowIndex,
|
||||
String title1,
|
||||
|
|
|
@ -804,6 +804,13 @@ message SignedWitness {
|
|||
|
||||
|
||||
message Dispute {
|
||||
enum State {
|
||||
NEEDS_UPGRADE = 0;
|
||||
NEW = 1;
|
||||
OPEN = 2;
|
||||
REOPENED = 3;
|
||||
CLOSED = 4;
|
||||
}
|
||||
string trade_id = 1;
|
||||
string id = 2;
|
||||
int32 trader_id = 3;
|
||||
|
@ -831,6 +838,9 @@ message Dispute {
|
|||
string mediators_dispute_result = 25;
|
||||
string delayed_payout_tx_id = 26;
|
||||
string donation_address_of_delayed_payout_tx = 27;
|
||||
State state = 28;
|
||||
int64 trade_period_end = 29;
|
||||
map<string, string> extra_data = 30;
|
||||
}
|
||||
|
||||
message Attachment {
|
||||
|
|
Loading…
Add table
Reference in a new issue