Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve chat functionality of mediation/arbitration #5207

Merged
merged 5 commits into from Mar 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ private void onChatMessage(ChatMessage chatMessage, Dispute dispute) {
// If last message is not a result message we re-open as we might have received a new message from the
// trader/mediator/arbitrator who has reopened the case
if (dispute.isClosed() && !chatMessages.isEmpty() && !chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute)) {
dispute.setIsClosed(false);
dispute.reOpen();
if (dispute.getSupportType() == SupportType.MEDIATION) {
mediationManager.requestPersistence();
} else if (dispute.getSupportType() == SupportType.REFUND) {
Expand Down
149 changes: 142 additions & 7 deletions core/src/main/java/bisq/core/support/dispute/Dispute.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,23 +27,30 @@
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;
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;
Expand All @@ -58,6 +66,23 @@
@EqualsAndHashCode
@Getter
public final class Dispute implements NetworkPayload, PersistablePayload {

public enum State {
NEEDS_UPGRADE,
NEW,
OPEN,
REOPENED,
CLOSED;

public static Dispute.State fromProto(protobuf.Dispute.State state) {
return ProtoUtil.enumFromProto(Dispute.State.class, state.name());
}

public static protobuf.Dispute.State toProtoMessage(Dispute.State state) {
return protobuf.Dispute.State.valueOf(state.name());
}
}

private final String tradeId;
private final String id;
private final int traderId;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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();


///////////////////////////////////////////////////////////////////////////////////////////
Expand All @@ -124,6 +164,7 @@ public Dispute(long openingDate,
boolean disputeOpenerIsMaker,
PubKeyRing traderPubKeyRing,
long tradeDate,
long tradePeriodEnd,
Contract contract,
@Nullable byte[] contractHash,
@Nullable byte[] depositTxSerialized,
Expand All @@ -143,6 +184,7 @@ public Dispute(long openingDate,
this.disputeOpenerIsMaker = disputeOpenerIsMaker;
this.traderPubKeyRing = traderPubKeyRing;
this.tradeDate = tradeDate;
this.tradePeriodEnd = tradePeriodEnd;
this.contract = contract;
this.contractHash = contractHash;
this.depositTxSerialized = depositTxSerialized;
Expand All @@ -158,6 +200,7 @@ public Dispute(long openingDate,

id = tradeId + "_" + traderId;
uid = UUID.randomUUID().toString();
refreshAlertLevel(true);
}


Expand All @@ -176,15 +219,17 @@ public protobuf.Dispute toProtoMessage() {
.setDisputeOpenerIsMaker(disputeOpenerIsMaker)
.setTraderPubKeyRing(traderPubKeyRing.toProtoMessage())
.setTradeDate(tradeDate)
.setTradePeriodEnd(tradePeriodEnd)
.setContract(contract.toProtoMessage())
.setContractAsJson(contractAsJson)
.setAgentPubKeyRing(agentPubKeyRing.toProtoMessage())
.setIsSupportTicket(isSupportTicket)
.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)));
Expand All @@ -200,6 +245,7 @@ public protobuf.Dispute toProtoMessage() {
Optional.ofNullable(mediatorsDisputeResult).ifPresent(result -> builder.setMediatorsDisputeResult(mediatorsDisputeResult));
Optional.ofNullable(delayedPayoutTxId).ifPresent(result -> builder.setDelayedPayoutTxId(delayedPayoutTxId));
Optional.ofNullable(donationAddressOfDelayedPayoutTx).ifPresent(result -> builder.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx));
Optional.ofNullable(getExtraDataMap()).ifPresent(builder::putAllExtraData);
return builder.build();
}

Expand All @@ -211,6 +257,7 @@ public static Dispute fromProto(protobuf.Dispute proto, CoreProtoResolver corePr
proto.getDisputeOpenerIsMaker(),
PubKeyRing.fromProto(proto.getTraderPubKeyRing()),
proto.getTradeDate(),
proto.getTradePeriodEnd(),
Contract.fromProto(proto.getContract(), coreProtoResolver),
ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash()),
ProtoUtil.byteArrayOrNullFromProto(proto.getDepositTxSerialized()),
Expand All @@ -224,11 +271,13 @@ public static Dispute fromProto(protobuf.Dispute proto, CoreProtoResolver corePr
proto.getIsSupportTicket(),
SupportType.fromProto(proto.getSupportType()));

dispute.setExtraDataMap(CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
null : ExtraDataMapValidator.getValidatedExtraDataMap(proto.getExtraDataMap()));

dispute.chatMessages.addAll(proto.getChatMessageList().stream()
.map(ChatMessage::fromPayloadProto)
.collect(Collectors.toList()));

dispute.isClosedProperty.set(proto.getIsClosed());
if (proto.hasDisputeResult())
dispute.disputeResultProperty.set(DisputeResult.fromProto(proto.getDisputeResult()));
dispute.disputePayoutTxId = ProtoUtil.stringOrNullFromProto(proto.getDisputePayoutTxId());
Expand All @@ -248,6 +297,20 @@ public static Dispute fromProto(protobuf.Dispute proto, CoreProtoResolver corePr
dispute.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx);
}

if (Dispute.State.fromProto(proto.getState()) == State.NEEDS_UPGRADE) {
// old disputes did not have a state field, so choose an appropriate state:
dispute.setState(proto.getIsClosed() ? State.CLOSED : State.OPEN);
if (dispute.getDisputeState() == State.CLOSED) {
// mark chat messages as read for pre-existing CLOSED disputes
// otherwise at upgrade, all old disputes would have 1 unread chat message
// because currently when a dispute is closed, the last chat message is not marked read
dispute.getChatMessages().forEach(m -> m.setWasDisplayed(true));
}
} else {
dispute.setState(Dispute.State.fromProto(proto.getState()));
}

dispute.refreshAlertLevel(true);
return dispute;
}

Expand All @@ -269,14 +332,32 @@ public void addAndPersistChatMessage(ChatMessage chatMessage) {
// Setters
///////////////////////////////////////////////////////////////////////////////////////////

public void setIsClosed(boolean isClosed) {
this.isClosedProperty.set(isClosed);
public void setIsClosed() {
setState(State.CLOSED);
}

public void reOpen() {
setState(State.REOPENED);
}

public void setState(Dispute.State disputeState) {
this.disputeState = disputeState;
this.isClosedProperty.set(disputeState == State.CLOSED);
}

public void setDisputeResult(DisputeResult disputeResult) {
disputeResultProperty.set(disputeResult);
}

public void setExtraData(String key, String value) {
if (key == null || value == null) {
return;
}
if (extraDataMap == null) {
extraDataMap = new HashMap<>();
}
extraDataMap.put(key, value);
}

///////////////////////////////////////////////////////////////////////////////////////////
// Getters
Expand All @@ -289,7 +370,9 @@ public String getShortTradeId() {
public ReadOnlyBooleanProperty isClosedProperty() {
return isClosedProperty;
}

public ReadOnlyIntegerProperty getBadgeCountProperty() {
return badgeCountProperty;
}
public ReadOnlyObjectProperty<DisputeResult> disputeResultProperty() {
return disputeResultProperty;
}
Expand All @@ -298,26 +381,78 @@ public Date getTradeDate() {
return new Date(tradeDate);
}

public Date getTradePeriodEnd() {
return new Date(tradePeriodEnd);
}

public Date getOpeningDate() {
return new Date(openingDate);
}

public boolean isNew() {
return this.disputeState == State.NEW;
}

public boolean isClosed() {
return isClosedProperty.get();
return this.disputeState == State.CLOSED;
}

public void refreshAlertLevel(boolean senderFlag) {
// if the dispute is "new" that is 1 alert that has to be propagated upstream
// or if there are unread messages that is 1 alert that has to be propagated upstream
if (isNew() || unreadMessageCount(senderFlag) > 0) {
badgeCountProperty.setValue(1);
} else {
badgeCountProperty.setValue(0);
}
}

public long unreadMessageCount(boolean senderFlag) {
return chatMessages.stream()
.filter(m -> m.isSenderIsTrader() == senderFlag)
.filter(m -> !m.isSystemMessage())
.filter(m -> !m.isWasDisplayed())
.count();
}

public void setDisputeSeen(boolean senderFlag) {
if (this.disputeState == State.NEW)
setState(State.OPEN);
refreshAlertLevel(senderFlag);
}

public void setChatMessagesSeen(boolean senderFlag) {
getChatMessages().forEach(m -> m.setWasDisplayed(true));
refreshAlertLevel(senderFlag);
}

public String getRoleString() {
if (disputeOpenerIsMaker) {
if (disputeOpenerIsBuyer)
return Res.get("support.buyerOfferer");
else
return Res.get("support.sellerOfferer");
} else {
if (disputeOpenerIsBuyer)
return Res.get("support.buyerTaker");
else
return Res.get("support.sellerTaker");
}
}

@Override
public String toString() {
return "Dispute{" +
"\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) +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,14 @@
import bisq.common.proto.persistable.PersistedDataHost;

import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

import javafx.collections.ObservableList;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
Expand All @@ -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
Expand Down Expand Up @@ -153,26 +149,21 @@ private void onDisputesChangeListener(List<? extends Dispute> addedList,
@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());
});
}
Expand Down
Loading