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
// 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) {

View file

@ -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) +

View file

@ -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());
});
}

View file

@ -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

View file

@ -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 " +

View file

@ -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);

View file

@ -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 " +

View file

@ -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"

View file

@ -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,

View file

@ -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());

View file

@ -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)

View file

@ -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();

View file

@ -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 &&

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.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,39 +998,95 @@ 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");
}
}
}

View file

@ -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;
}
}

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.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();
}

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.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);
}
}

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.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();
}

View file

@ -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;
}
}

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.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);
}

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.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());
}
}

View file

@ -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());
}
}

View file

@ -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,

View file

@ -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 {