io.bitsquare.gui.main.disputes.trader.TraderDisputeView.java Source code

Java tutorial

Introduction

Here is the source code for io.bitsquare.gui.main.disputes.trader.TraderDisputeView.java

Source

/*
 * This file is part of Bitsquare.
 *
 * Bitsquare 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.
 *
 * Bitsquare 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 Bitsquare. If not, see <http://www.gnu.org/licenses/>.
 */

package io.bitsquare.gui.main.disputes.trader;

import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import io.bitsquare.alert.PrivateNotificationManager;
import io.bitsquare.app.Version;
import io.bitsquare.arbitration.Dispute;
import io.bitsquare.arbitration.DisputeManager;
import io.bitsquare.arbitration.messages.DisputeCommunicationMessage;
import io.bitsquare.arbitration.payload.Attachment;
import io.bitsquare.common.Timer;
import io.bitsquare.common.UserThread;
import io.bitsquare.common.crypto.KeyRing;
import io.bitsquare.common.crypto.PubKeyRing;
import io.bitsquare.common.util.Utilities;
import io.bitsquare.gui.common.view.ActivatableView;
import io.bitsquare.gui.common.view.FxmlView;
import io.bitsquare.gui.components.BusyAnimation;
import io.bitsquare.gui.components.HyperlinkWithIcon;
import io.bitsquare.gui.components.InputTextField;
import io.bitsquare.gui.components.TableGroupHeadline;
import io.bitsquare.gui.main.overlays.popups.Popup;
import io.bitsquare.gui.main.overlays.windows.ContractWindow;
import io.bitsquare.gui.main.overlays.windows.DisputeSummaryWindow;
import io.bitsquare.gui.main.overlays.windows.SendPrivateNotificationWindow;
import io.bitsquare.gui.main.overlays.windows.TradeDetailsWindow;
import io.bitsquare.gui.util.BSFormatter;
import io.bitsquare.gui.util.GUIUtil;
import io.bitsquare.p2p.NodeAddress;
import io.bitsquare.p2p.P2PService;
import io.bitsquare.p2p.network.Connection;
import io.bitsquare.trade.Contract;
import io.bitsquare.trade.Trade;
import io.bitsquare.trade.TradeManager;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Paint;
import javafx.scene.text.TextAlignment;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Callback;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;

import javax.annotation.Nullable;
import javax.inject.Inject;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;

// will be probably only used for arbitration communication, will be renamed and the icon changed
@FxmlView
public class TraderDisputeView extends ActivatableView<VBox, Void> {

    private final DisputeManager disputeManager;
    protected final KeyRing keyRing;
    private final TradeManager tradeManager;
    private final Stage stage;
    protected final BSFormatter formatter;
    private final DisputeSummaryWindow disputeSummaryWindow;
    private PrivateNotificationManager privateNotificationManager;
    private final ContractWindow contractWindow;
    private final TradeDetailsWindow tradeDetailsWindow;
    private P2PService p2PService;

    private final List<Attachment> tempAttachments = new ArrayList<>();

    private TableView<Dispute> tableView;
    private SortedList<Dispute> sortedList;

    private Dispute selectedDispute;
    private ListView<DisputeCommunicationMessage> messageListView;
    private TextArea inputTextArea;
    private AnchorPane messagesAnchorPane;
    private VBox messagesInputBox;
    private BusyAnimation sendMsgBusyAnimation;
    private Label sendMsgInfoLabel;
    private ChangeListener<Boolean> arrivedPropertyListener;
    private ChangeListener<Boolean> storedInMailboxPropertyListener;
    @Nullable
    private DisputeCommunicationMessage disputeCommunicationMessage;
    private ListChangeListener<DisputeCommunicationMessage> disputeDirectMessageListListener;
    private ChangeListener<Boolean> selectedDisputeClosedPropertyListener;
    private Subscription selectedDisputeSubscription;
    private TableGroupHeadline tableGroupHeadline;
    private ObservableList<DisputeCommunicationMessage> disputeCommunicationMessages;
    private Button sendButton;
    private Subscription inputTextAreaTextSubscription;
    private EventHandler<KeyEvent> keyEventEventHandler;
    private Scene scene;
    protected FilteredList<Dispute> filteredList;
    private InputTextField filterTextField;
    private ChangeListener<String> filterTextFieldListener;
    protected HBox filterBox;

    ///////////////////////////////////////////////////////////////////////////////////////////
    // Constructor, lifecycle
    ///////////////////////////////////////////////////////////////////////////////////////////

    @Inject
    public TraderDisputeView(DisputeManager disputeManager, KeyRing keyRing, TradeManager tradeManager, Stage stage,
            BSFormatter formatter, DisputeSummaryWindow disputeSummaryWindow,
            PrivateNotificationManager privateNotificationManager, ContractWindow contractWindow,
            TradeDetailsWindow tradeDetailsWindow, P2PService p2PService) {
        this.disputeManager = disputeManager;
        this.keyRing = keyRing;
        this.tradeManager = tradeManager;
        this.stage = stage;
        this.formatter = formatter;
        this.disputeSummaryWindow = disputeSummaryWindow;
        this.privateNotificationManager = privateNotificationManager;
        this.contractWindow = contractWindow;
        this.tradeDetailsWindow = tradeDetailsWindow;
        this.p2PService = p2PService;
    }

    @Override
    public void initialize() {
        Label label = new Label("Filter list:");
        HBox.setMargin(label, new Insets(5, 0, 0, 0));
        filterTextField = new InputTextField();
        filterTextField.setText("open");
        filterTextFieldListener = (observable, oldValue,
                newValue) -> applyFilteredListPredicate(filterTextField.getText());

        filterBox = new HBox();
        filterBox.setSpacing(5);
        filterBox.getChildren().addAll(label, filterTextField);
        VBox.setVgrow(filterBox, Priority.NEVER);
        filterBox.setVisible(false);
        filterBox.setManaged(false);

        tableView = new TableView<>();
        VBox.setVgrow(tableView, Priority.SOMETIMES);
        tableView.setMinHeight(150);

        root.getChildren().addAll(filterBox, tableView);

        tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
        Label placeholder = new Label("There are no open tickets");
        placeholder.setWrapText(true);
        tableView.setPlaceholder(placeholder);
        tableView.getSelectionModel().clearSelection();

        tableView.getColumns().add(getSelectColumn());

        TableColumn<Dispute, Dispute> contractColumn = getContractColumn();
        tableView.getColumns().add(contractColumn);

        TableColumn<Dispute, Dispute> dateColumn = getDateColumn();
        tableView.getColumns().add(dateColumn);

        TableColumn<Dispute, Dispute> tradeIdColumn = getTradeIdColumn();
        tableView.getColumns().add(tradeIdColumn);

        TableColumn<Dispute, Dispute> buyerOnionAddressColumn = getBuyerOnionAddressColumn();
        tableView.getColumns().add(buyerOnionAddressColumn);

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

        TableColumn<Dispute, Dispute> stateColumn = getStateColumn();
        tableView.getColumns().add(stateColumn);

        tradeIdColumn.setComparator((o1, o2) -> o1.getTradeId().compareTo(o2.getTradeId()));
        dateColumn.setComparator((o1, o2) -> o1.getOpeningDate().compareTo(o2.getOpeningDate()));
        buyerOnionAddressColumn.setComparator(
                (o1, o2) -> getBuyerOnionAddressColumnLabel(o1).compareTo(getBuyerOnionAddressColumnLabel(o2)));
        sellerOnionAddressColumn.setComparator(
                (o1, o2) -> getSellerOnionAddressColumnLabel(o1).compareTo(getSellerOnionAddressColumnLabel(o2)));
        marketColumn.setComparator((o1, o2) -> formatter.getCurrencyPair(o1.getContract().offer.getCurrencyCode())
                .compareTo(o2.getContract().offer.getCurrencyCode()));

        dateColumn.setSortType(TableColumn.SortType.DESCENDING);
        tableView.getSortOrder().add(dateColumn);

        /*inputTextAreaListener = (observable, oldValue, newValue) ->
            sendButton.setDisable(newValue.length() == 0
                    && tempAttachments.size() == 0 &&
                    selectedDispute.disputeResultProperty().get() == null);*/

        selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> {
            messagesInputBox.setVisible(!newValue);
            messagesInputBox.setManaged(!newValue);
            AnchorPane.setBottomAnchor(messageListView, newValue ? 0d : 120d);
        };

        disputeDirectMessageListListener = c -> scrollToBottom();

        keyEventEventHandler = event -> {
            if (new KeyCodeCombination(KeyCode.L, KeyCombination.ALT_DOWN).match(event)) {
                Map<String, List<Dispute>> map = new HashMap<>();
                disputeManager.getDisputesAsObservableList().stream().forEach(dispute -> {
                    String tradeId = dispute.getTradeId();
                    List<Dispute> list;
                    if (!map.containsKey(tradeId))
                        map.put(tradeId, new ArrayList<>());

                    list = map.get(tradeId);
                    list.add(dispute);
                });
                List<List<Dispute>> disputeGroups = new ArrayList<>();
                map.entrySet().stream().forEach(entry -> {
                    disputeGroups.add(entry.getValue());
                });
                disputeGroups.sort((o1, o2) -> !o1.isEmpty() && !o2.isEmpty()
                        ? o1.get(0).getOpeningDate().compareTo(o2.get(0).getOpeningDate())
                        : 0);
                StringBuilder stringBuilder = new StringBuilder();
                stringBuilder.append("Summary of all disputes (No. of disputes: " + disputeGroups.size() + ")\n\n");
                disputeGroups.stream().forEach(disputeGroup -> {
                    Dispute dispute0 = disputeGroup.get(0);
                    stringBuilder.append(
                            "##########################################################################################/\n")
                            .append("## Trade ID: ").append(dispute0.getTradeId()).append("\n").append("## Date: ")
                            .append(formatter.formatDateTime(dispute0.getOpeningDate())).append("\n")
                            .append("## Is support ticket: ").append(dispute0.isSupportTicket()).append("\n");
                    if (dispute0.disputeResultProperty().get() != null
                            && dispute0.disputeResultProperty().get().getReason() != null) {
                        stringBuilder.append("## Reason: ")
                                .append(dispute0.disputeResultProperty().get().getReason()).append("\n");
                    }
                    stringBuilder.append(
                            "##########################################################################################/\n")
                            .append("\n");
                    disputeGroup.stream().forEach(dispute -> {
                        stringBuilder.append(
                                "*******************************************************************************************\n")
                                .append("** Trader's ID: ").append(dispute.getTraderId())
                                .append("\n*******************************************************************************************\n")
                                .append("\n");
                        dispute.getDisputeCommunicationMessagesAsObservableList().stream().forEach(m -> {
                            String role = m.isSenderIsTrader() ? ">> Trader's msg: " : "<< Arbitrator's msg: ";
                            stringBuilder.append(role).append(m.getMessage()).append("\n");
                        });
                        stringBuilder.append("\n");
                    });
                    stringBuilder.append("\n");
                });
                String message = stringBuilder.toString();
                new Popup().headLine("All disputes (" + disputeGroups.size() + ")").information(message).width(1000)
                        .actionButtonText("Copy").onAction(() -> Utilities.copyToClipboard(message)).show();
            } else if (new KeyCodeCombination(KeyCode.U, KeyCombination.ALT_DOWN).match(event)) {
                // Hidden shortcut to re-open a dispute. Allow it also for traders not only arbitrator.
                if (selectedDispute != null) {
                    if (selectedDisputeClosedPropertyListener != null)
                        selectedDispute.isClosedProperty().removeListener(selectedDisputeClosedPropertyListener);
                    selectedDispute.setIsClosed(false);
                }
            } else if (new KeyCodeCombination(KeyCode.R, KeyCombination.ALT_DOWN).match(event)) {
                if (selectedDispute != null) {
                    PubKeyRing pubKeyRing = selectedDispute.getTraderPubKeyRing();
                    NodeAddress nodeAddress;
                    if (pubKeyRing.equals(selectedDispute.getContract().getBuyerPubKeyRing()))
                        nodeAddress = selectedDispute.getContract().getBuyerNodeAddress();
                    else
                        nodeAddress = selectedDispute.getContract().getSellerNodeAddress();

                    new SendPrivateNotificationWindow(pubKeyRing, nodeAddress).onAddAlertMessage(
                            privateNotificationManager::sendPrivateNotificationMessageIfKeyIsValid).show();
                }
            }
        };
    }

    @Override
    protected void activate() {
        filterTextField.textProperty().addListener(filterTextFieldListener);
        disputeManager.cleanupDisputes();

        filteredList = new FilteredList<>(disputeManager.getDisputesAsObservableList());
        applyFilteredListPredicate(filterTextField.getText());

        sortedList = new SortedList<>(filteredList);
        sortedList.comparatorProperty().bind(tableView.comparatorProperty());
        tableView.setItems(sortedList);

        // sortedList.setComparator((o1, o2) -> o2.getOpeningDate().compareTo(o1.getOpeningDate()));
        selectedDisputeSubscription = EasyBind.subscribe(tableView.getSelectionModel().selectedItemProperty(),
                this::onSelectDispute);

        Dispute selectedItem = tableView.getSelectionModel().getSelectedItem();
        if (selectedItem != null)
            tableView.getSelectionModel().select(selectedItem);

        scrollToBottom();

        scene = root.getScene();
        if (scene != null)
            scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler);

        // If doPrint=true we print out a html page which opens tabs with all deposit txs 
        // (firefox needs about:config change to allow > 20 tabs)
        // Useful to check if there any funds in not finished trades (no payout tx done).
        // Last check 10.02.2017 found 8 trades and we contacted all traders as far as possible (email if available 
        // otherwise in-app private notification)
        boolean doPrint = false;
        if (doPrint) {
            try {
                DateFormat formatter = new SimpleDateFormat("dd/MM/yy");
                Date startDate = formatter.parse("10/02/17");
                startDate = new Date(0); // print all from start

                HashMap<String, Dispute> map = new HashMap<>();
                disputeManager.getDisputesAsObservableList().stream().forEach(dispute -> {
                    map.put(dispute.getDepositTxId(), dispute);
                });

                final Date finalStartDate = startDate;
                List<Dispute> disputes = new ArrayList<>(map.values());
                disputes.sort((o1, o2) -> o1.getOpeningDate().compareTo(o2.getOpeningDate()));
                List<List<Dispute>> subLists = Lists.partition(disputes, 1000);
                StringBuilder sb = new StringBuilder();
                subLists.stream().forEach(list -> {
                    StringBuilder sb1 = new StringBuilder(
                            "\n<html><head><script type=\"text/javascript\">function load(){\n");
                    StringBuilder sb2 = new StringBuilder("\n}</script></head><body onload=\"load()\">\n");
                    list.stream().forEach(dispute -> {
                        if (dispute.getOpeningDate().after(finalStartDate)) {
                            String txId = dispute.getDepositTxId();
                            sb1.append("window.open(\"https://blockchain.info/tx/").append(txId)
                                    .append("\", '_blank');\n");

                            sb2.append("Dispute ID: ").append(dispute.getId()).append(" Tx ID: ")
                                    .append("<a href=\"https://blockchain.info/tx/").append(txId).append("\">")
                                    .append(txId).append("</a> ").append("Opening date: ")
                                    .append(formatter.format(dispute.getOpeningDate())).append("<br/>\n");
                        }
                    });
                    sb2.append("</body></html>");
                    String res = sb1.toString() + sb2.toString();

                    sb.append(res).append("\n\n\n");
                });
                log.info(sb.toString());
            } catch (ParseException ignore) {
            }
        }
    }

    @Override
    protected void deactivate() {
        filterTextField.textProperty().removeListener(filterTextFieldListener);
        sortedList.comparatorProperty().unbind();
        selectedDisputeSubscription.unsubscribe();
        removeListenersOnSelectDispute();

        if (scene != null)
            scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler);
    }

    protected void applyFilteredListPredicate(String filterString) {
        // If in trader view we must not display arbitrators own disputes as trader (must not happen anyway)
        filteredList.setPredicate(dispute -> !dispute.getArbitratorPubKeyRing().equals(keyRing.getPubKeyRing()));
    }

    ///////////////////////////////////////////////////////////////////////////////////////////
    // UI actions
    ///////////////////////////////////////////////////////////////////////////////////////////

    private void onOpenContract(Dispute dispute) {
        contractWindow.show(dispute);
    }

    private void onSendMessage(String inputText, Dispute dispute) {
        if (disputeCommunicationMessage != null) {
            disputeCommunicationMessage.arrivedProperty().removeListener(arrivedPropertyListener);
            disputeCommunicationMessage.storedInMailboxProperty().removeListener(storedInMailboxPropertyListener);
        }

        disputeCommunicationMessage = disputeManager.sendDisputeDirectMessage(dispute, inputText,
                new ArrayList<>(tempAttachments));
        tempAttachments.clear();
        scrollToBottom();

        inputTextArea.setDisable(true);
        inputTextArea.clear();

        Timer timer = UserThread.runAfter(() -> {
            sendMsgInfoLabel.setVisible(true);
            sendMsgInfoLabel.setManaged(true);
            sendMsgInfoLabel.setText("Sending Message...");

            sendMsgBusyAnimation.play();
        }, 500, TimeUnit.MILLISECONDS);

        arrivedPropertyListener = (observable, oldValue, newValue) -> {
            if (newValue) {
                hideSendMsgInfo(timer);
            }
        };
        if (disputeCommunicationMessage != null && disputeCommunicationMessage.arrivedProperty() != null)
            disputeCommunicationMessage.arrivedProperty().addListener(arrivedPropertyListener);
        storedInMailboxPropertyListener = (observable, oldValue, newValue) -> {
            if (newValue) {
                sendMsgInfoLabel.setVisible(true);
                sendMsgInfoLabel.setManaged(true);
                sendMsgInfoLabel.setText("Receiver is not online. Message is saved to his mailbox.");
                hideSendMsgInfo(timer);
            }
        };
        if (disputeCommunicationMessage != null)
            disputeCommunicationMessage.storedInMailboxProperty().addListener(storedInMailboxPropertyListener);
    }

    private void hideSendMsgInfo(Timer timer) {
        timer.stop();
        inputTextArea.setDisable(false);

        UserThread.runAfter(() -> {
            sendMsgInfoLabel.setVisible(false);
            sendMsgInfoLabel.setManaged(false);
        }, 5);
        sendMsgBusyAnimation.stop();
    }

    private void onCloseDispute(Dispute dispute) {
        long protocolVersion = dispute.getContract().offer.getProtocolVersion();
        if (protocolVersion == Version.TRADE_PROTOCOL_VERSION) {
            disputeSummaryWindow.onFinalizeDispute(() -> messagesAnchorPane.getChildren().remove(messagesInputBox))
                    .show(dispute);
        } else {
            new Popup<>().warning("The offer in that dispute has been created with an older version of Bitsquare.\n"
                    + "You cannot close that dispute with your version of the application.\n\n"
                    + "Please use an older version with protocol version " + protocolVersion).show();
        }
    }

    private void onRequestUpload() {
        int totalSize = tempAttachments.stream().mapToInt(a -> a.getBytes().length).sum();
        if (tempAttachments.size() < 3) {
            FileChooser fileChooser = new FileChooser();
            int maxMsgSize = Connection.getMaxMsgSize();
            int maxSizeInKB = maxMsgSize / 1024;
            fileChooser.setTitle("Open file to attach (max. file size: " + maxSizeInKB + " kb)");
            /* if (Utilities.isUnix())
            fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));*/
            File result = fileChooser.showOpenDialog(stage);
            if (result != null) {
                try {
                    URL url = result.toURI().toURL();
                    try (InputStream inputStream = url.openStream()) {
                        byte[] filesAsBytes = ByteStreams.toByteArray(inputStream);
                        int size = filesAsBytes.length;
                        int newSize = totalSize + size;
                        if (newSize > maxMsgSize) {
                            new Popup().warning("The total size of your attachments is " + (newSize / 1024)
                                    + " kb and is exceeding the max. allowed " + "message size of " + maxSizeInKB
                                    + " kB.").show();
                        } else if (size > maxMsgSize) {
                            new Popup().warning("The max. allowed file size is " + maxSizeInKB + " kB.").show();
                        } else {
                            tempAttachments.add(new Attachment(result.getName(), filesAsBytes));
                            inputTextArea
                                    .setText(inputTextArea.getText() + "\n[Attachment " + result.getName() + "]");
                        }
                    } catch (java.io.IOException e) {
                        e.printStackTrace();
                        log.error(e.getMessage());
                    }
                } catch (MalformedURLException e2) {
                    e2.printStackTrace();
                    log.error(e2.getMessage());
                }
            }
        } else {
            new Popup().warning("You cannot send more then 3 attachments in one message.").show();
        }
    }

    private void onOpenAttachment(Attachment attachment) {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Save file to disk");
        fileChooser.setInitialFileName(attachment.getFileName());
        /* if (Utilities.isUnix())
        fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));*/
        File file = fileChooser.showSaveDialog(stage);
        if (file != null) {
            try (FileOutputStream fileOutputStream = new FileOutputStream(file.getAbsolutePath())) {
                fileOutputStream.write(attachment.getBytes());
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println(e.getMessage());
            }
        }
    }

    private void removeListenersOnSelectDispute() {
        if (selectedDispute != null) {
            if (selectedDisputeClosedPropertyListener != null)
                selectedDispute.isClosedProperty().removeListener(selectedDisputeClosedPropertyListener);

            if (disputeCommunicationMessages != null && disputeDirectMessageListListener != null)
                disputeCommunicationMessages.removeListener(disputeDirectMessageListListener);
        }

        if (disputeCommunicationMessage != null) {
            if (arrivedPropertyListener != null)
                disputeCommunicationMessage.arrivedProperty().removeListener(arrivedPropertyListener);
            if (storedInMailboxPropertyListener != null)
                disputeCommunicationMessage.storedInMailboxProperty()
                        .removeListener(storedInMailboxPropertyListener);
        }

        if (messageListView != null)
            messageListView.prefWidthProperty().unbind();

        if (tableGroupHeadline != null)
            tableGroupHeadline.prefWidthProperty().unbind();

        if (messagesAnchorPane != null)
            messagesAnchorPane.prefWidthProperty().unbind();

        if (inputTextAreaTextSubscription != null)
            inputTextAreaTextSubscription.unsubscribe();
    }

    private void addListenersOnSelectDispute() {
        if (tableGroupHeadline != null) {
            tableGroupHeadline.prefWidthProperty().bind(root.widthProperty());
            messageListView.prefWidthProperty().bind(root.widthProperty());
            messagesAnchorPane.prefWidthProperty().bind(root.widthProperty());
            disputeCommunicationMessages.addListener(disputeDirectMessageListListener);
            if (selectedDispute != null)
                selectedDispute.isClosedProperty().addListener(selectedDisputeClosedPropertyListener);
            inputTextAreaTextSubscription = EasyBind.subscribe(inputTextArea.textProperty(),
                    t -> sendButton.setDisable(t.isEmpty()));
        }
    }

    private void onSelectDispute(Dispute dispute) {
        removeListenersOnSelectDispute();
        if (dispute == null) {
            if (root.getChildren().size() > 2)
                root.getChildren().remove(2);

            selectedDispute = null;
        } else if (selectedDispute != dispute) {
            this.selectedDispute = dispute;

            boolean isTrader = disputeManager.isTrader(selectedDispute);

            tableGroupHeadline = new TableGroupHeadline();
            tableGroupHeadline.setText("Messages");

            AnchorPane.setTopAnchor(tableGroupHeadline, 10d);
            AnchorPane.setRightAnchor(tableGroupHeadline, 0d);
            AnchorPane.setBottomAnchor(tableGroupHeadline, 0d);
            AnchorPane.setLeftAnchor(tableGroupHeadline, 0d);

            disputeCommunicationMessages = selectedDispute.getDisputeCommunicationMessagesAsObservableList();
            SortedList<DisputeCommunicationMessage> sortedList = new SortedList<>(disputeCommunicationMessages);
            sortedList.setComparator((o1, o2) -> o1.getDate().compareTo(o2.getDate()));
            messageListView = new ListView<>(sortedList);
            messageListView.setId("message-list-view");

            messageListView.setMinHeight(150);
            AnchorPane.setTopAnchor(messageListView, 30d);
            AnchorPane.setRightAnchor(messageListView, 0d);
            AnchorPane.setLeftAnchor(messageListView, 0d);

            messagesAnchorPane = new AnchorPane();
            VBox.setVgrow(messagesAnchorPane, Priority.ALWAYS);

            inputTextArea = new TextArea();
            inputTextArea.setPrefHeight(70);
            inputTextArea.setWrapText(true);

            sendButton = new Button("Send");
            sendButton.setDefaultButton(true);
            sendButton.setOnAction(e -> {
                if (p2PService.isBootstrapped()) {
                    String text = inputTextArea.getText();
                    if (!text.isEmpty())
                        onSendMessage(text, selectedDispute);
                } else {
                    new Popup().information("You need to wait until you are fully connected to the network.\n"
                            + "That might take up to about 2 minutes at startup.").show();
                }
            });
            inputTextAreaTextSubscription = EasyBind.subscribe(inputTextArea.textProperty(),
                    t -> sendButton.setDisable(t.isEmpty()));

            Button uploadButton = new Button("Add attachments");
            uploadButton.setOnAction(e -> onRequestUpload());

            sendMsgInfoLabel = new Label();
            sendMsgInfoLabel.setVisible(false);
            sendMsgInfoLabel.setManaged(false);
            sendMsgInfoLabel.setPadding(new Insets(5, 0, 0, 0));

            sendMsgBusyAnimation = new BusyAnimation(false);

            if (!selectedDispute.isClosed()) {
                HBox buttonBox = new HBox();
                buttonBox.setSpacing(10);
                buttonBox.getChildren().addAll(sendButton, uploadButton, sendMsgBusyAnimation, sendMsgInfoLabel);

                if (!isTrader) {
                    Button closeDisputeButton = new Button("Close ticket");
                    closeDisputeButton.setOnAction(e -> onCloseDispute(selectedDispute));
                    closeDisputeButton.setDefaultButton(true);
                    Pane spacer = new Pane();
                    HBox.setHgrow(spacer, Priority.ALWAYS);
                    buttonBox.getChildren().addAll(spacer, closeDisputeButton);
                }

                messagesInputBox = new VBox();
                messagesInputBox.setSpacing(10);
                messagesInputBox.getChildren().addAll(inputTextArea, buttonBox);
                VBox.setVgrow(buttonBox, Priority.ALWAYS);

                AnchorPane.setRightAnchor(messagesInputBox, 0d);
                AnchorPane.setBottomAnchor(messagesInputBox, 5d);
                AnchorPane.setLeftAnchor(messagesInputBox, 0d);

                AnchorPane.setBottomAnchor(messageListView, 120d);

                messagesAnchorPane.getChildren().addAll(tableGroupHeadline, messageListView, messagesInputBox);
            } else {
                AnchorPane.setBottomAnchor(messageListView, 0d);
                messagesAnchorPane.getChildren().addAll(tableGroupHeadline, messageListView);
            }

            messageListView.setCellFactory(
                    new Callback<ListView<DisputeCommunicationMessage>, ListCell<DisputeCommunicationMessage>>() {
                        @Override
                        public ListCell<DisputeCommunicationMessage> call(
                                ListView<DisputeCommunicationMessage> list) {
                            return new ListCell<DisputeCommunicationMessage>() {
                                public ChangeListener<Boolean> sendMsgBusyAnimationListener;
                                final Pane bg = new Pane();
                                final ImageView arrow = new ImageView();
                                final Label headerLabel = new Label();
                                final Label messageLabel = new Label();
                                final Label copyIcon = new Label();
                                final HBox attachmentsBox = new HBox();
                                final AnchorPane messageAnchorPane = new AnchorPane();
                                final Label statusIcon = new Label();
                                final double arrowWidth = 15d;
                                final double attachmentsBoxHeight = 20d;
                                final double border = 10d;
                                final double bottomBorder = 25d;
                                final double padding = border + 10d;
                                final double msgLabelPaddingRight = padding + 20d;

                                {
                                    bg.setMinHeight(30);
                                    messageLabel.setWrapText(true);
                                    headerLabel.setTextAlignment(TextAlignment.CENTER);
                                    attachmentsBox.setSpacing(5);
                                    statusIcon.setStyle("-fx-font-size: 10;");
                                    Tooltip.install(copyIcon, new Tooltip("Copy to clipboard"));
                                    messageAnchorPane.getChildren().addAll(bg, arrow, headerLabel, messageLabel,
                                            copyIcon, attachmentsBox, statusIcon);
                                }

                                @Override
                                public void updateItem(final DisputeCommunicationMessage item, boolean empty) {
                                    super.updateItem(item, empty);

                                    if (item != null && !empty) {
                                        copyIcon.setOnMouseClicked(
                                                e -> Utilities.copyToClipboard(messageLabel.getText()));

                                        /* messageAnchorPane.prefWidthProperty().bind(EasyBind.map(messageListView.widthProperty(),
                                            w -> (double) w - padding - GUIUtil.getScrollbarWidth(messageListView)));*/
                                        if (!messageAnchorPane.prefWidthProperty().isBound())
                                            messageAnchorPane.prefWidthProperty()
                                                    .bind(messageListView.widthProperty().subtract(
                                                            padding + GUIUtil.getScrollbarWidth(messageListView)));

                                        AnchorPane.setTopAnchor(bg, 15d);
                                        AnchorPane.setBottomAnchor(bg, bottomBorder);
                                        AnchorPane.setTopAnchor(headerLabel, 0d);
                                        AnchorPane.setBottomAnchor(arrow, bottomBorder + 5d);
                                        AnchorPane.setTopAnchor(messageLabel, 25d);
                                        AnchorPane.setTopAnchor(copyIcon, 25d);
                                        AnchorPane.setBottomAnchor(attachmentsBox, bottomBorder + 10);

                                        boolean senderIsTrader = item.isSenderIsTrader();
                                        boolean isMyMsg = isTrader ? senderIsTrader : !senderIsTrader;

                                        arrow.setVisible(!item.isSystemMessage());
                                        arrow.setManaged(!item.isSystemMessage());
                                        statusIcon.setVisible(false);
                                        if (item.isSystemMessage()) {
                                            headerLabel.setStyle("-fx-text-fill: -bs-green; -fx-font-size: 11;");
                                            bg.setId("message-bubble-green");
                                            messageLabel.setStyle("-fx-text-fill: white;");
                                            copyIcon.setStyle("-fx-text-fill: white;");
                                        } else if (isMyMsg) {
                                            headerLabel.setStyle("-fx-text-fill: -fx-accent; -fx-font-size: 11;");
                                            bg.setId("message-bubble-blue");
                                            messageLabel.setStyle("-fx-text-fill: white;");
                                            copyIcon.setStyle("-fx-text-fill: white;");
                                            if (isTrader)
                                                arrow.setId("bubble_arrow_blue_left");
                                            else
                                                arrow.setId("bubble_arrow_blue_right");

                                            if (sendMsgBusyAnimationListener != null)
                                                sendMsgBusyAnimation.isRunningProperty()
                                                        .removeListener(sendMsgBusyAnimationListener);

                                            sendMsgBusyAnimationListener = (observable, oldValue, newValue) -> {
                                                if (!newValue) {
                                                    if (item.arrivedProperty().get())
                                                        showArrivedIcon();
                                                    else if (item.storedInMailboxProperty().get())
                                                        showMailboxIcon();
                                                }
                                            };
                                            sendMsgBusyAnimation.isRunningProperty()
                                                    .addListener(sendMsgBusyAnimationListener);

                                            if (item.arrivedProperty().get())
                                                showArrivedIcon();
                                            else if (item.storedInMailboxProperty().get())
                                                showMailboxIcon();
                                            //TODO show that icon on error
                                            /*else if (sendMsgProgressIndicator.getProgress() == 0)
                                            showNotArrivedIcon();*/
                                        } else {
                                            headerLabel
                                                    .setStyle("-fx-text-fill: -bs-light-grey; -fx-font-size: 11;");
                                            bg.setId("message-bubble-grey");
                                            messageLabel.setStyle("-fx-text-fill: black;");
                                            copyIcon.setStyle("-fx-text-fill: black;");
                                            if (isTrader)
                                                arrow.setId("bubble_arrow_grey_right");
                                            else
                                                arrow.setId("bubble_arrow_grey_left");
                                        }

                                        if (item.isSystemMessage()) {
                                            AnchorPane.setLeftAnchor(headerLabel, padding);
                                            AnchorPane.setRightAnchor(headerLabel, padding);
                                            AnchorPane.setLeftAnchor(bg, border);
                                            AnchorPane.setRightAnchor(bg, border);
                                            AnchorPane.setLeftAnchor(messageLabel, padding);
                                            AnchorPane.setRightAnchor(messageLabel, msgLabelPaddingRight);
                                            AnchorPane.setRightAnchor(copyIcon, padding);
                                            AnchorPane.setLeftAnchor(attachmentsBox, padding);
                                            AnchorPane.setRightAnchor(attachmentsBox, padding);
                                        } else if (senderIsTrader) {
                                            AnchorPane.setLeftAnchor(headerLabel, padding + arrowWidth);
                                            AnchorPane.setLeftAnchor(bg, border + arrowWidth);
                                            AnchorPane.setRightAnchor(bg, border);
                                            AnchorPane.setLeftAnchor(arrow, border);
                                            AnchorPane.setLeftAnchor(messageLabel, padding + arrowWidth);
                                            AnchorPane.setRightAnchor(messageLabel, msgLabelPaddingRight);
                                            AnchorPane.setRightAnchor(copyIcon, padding);
                                            AnchorPane.setLeftAnchor(attachmentsBox, padding + arrowWidth);
                                            AnchorPane.setRightAnchor(attachmentsBox, padding);
                                            AnchorPane.setRightAnchor(statusIcon, padding);
                                        } else {
                                            AnchorPane.setRightAnchor(headerLabel, padding + arrowWidth);
                                            AnchorPane.setLeftAnchor(bg, border);
                                            AnchorPane.setRightAnchor(bg, border + arrowWidth);
                                            AnchorPane.setRightAnchor(arrow, border);
                                            AnchorPane.setLeftAnchor(messageLabel, padding);
                                            AnchorPane.setRightAnchor(messageLabel,
                                                    msgLabelPaddingRight + arrowWidth);
                                            AnchorPane.setRightAnchor(copyIcon, padding + arrowWidth);
                                            AnchorPane.setLeftAnchor(attachmentsBox, padding);
                                            AnchorPane.setRightAnchor(attachmentsBox, padding + arrowWidth);
                                            AnchorPane.setLeftAnchor(statusIcon, padding);
                                        }

                                        AnchorPane.setBottomAnchor(statusIcon, 7d);
                                        headerLabel.setText(formatter.formatDateTime(item.getDate()));
                                        messageLabel.setText(item.getMessage());
                                        attachmentsBox.getChildren().clear();
                                        if (item.getAttachments().size() > 0) {
                                            AnchorPane.setBottomAnchor(messageLabel,
                                                    bottomBorder + attachmentsBoxHeight + 10);
                                            attachmentsBox.getChildren().add(new Label("Attachments: ") {
                                                {
                                                    setPadding(new Insets(0, 0, 3, 0));
                                                    if (isMyMsg)
                                                        setStyle("-fx-text-fill: white;");
                                                    else
                                                        setStyle("-fx-text-fill: black;");
                                                }
                                            });

                                            item.getAttachments().stream().forEach(attachment -> {
                                                final Label icon = new Label();
                                                setPadding(new Insets(0, 0, 3, 0));
                                                if (isMyMsg)
                                                    icon.getStyleClass().add("attachment-icon");
                                                else
                                                    icon.getStyleClass().add("attachment-icon-black");

                                                AwesomeDude.setIcon(icon, AwesomeIcon.FILE_TEXT);
                                                icon.setPadding(new Insets(-2, 0, 0, 0));
                                                icon.setTooltip(new Tooltip(attachment.getFileName()));
                                                icon.setOnMouseClicked(event -> onOpenAttachment(attachment));
                                                attachmentsBox.getChildren().add(icon);
                                            });
                                        } else {
                                            AnchorPane.setBottomAnchor(messageLabel, bottomBorder + 10);
                                        }

                                        // Need to set it here otherwise style is not correct
                                        AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY, "16.0");
                                        copyIcon.getStyleClass().add("copy-icon-disputes");

                                        // TODO There are still some cell rendering issues on updates
                                        setGraphic(messageAnchorPane);
                                    } else {
                                        if (sendMsgBusyAnimation != null && sendMsgBusyAnimationListener != null)
                                            sendMsgBusyAnimation.isRunningProperty()
                                                    .removeListener(sendMsgBusyAnimationListener);

                                        messageAnchorPane.prefWidthProperty().unbind();

                                        AnchorPane.clearConstraints(bg);
                                        AnchorPane.clearConstraints(headerLabel);
                                        AnchorPane.clearConstraints(arrow);
                                        AnchorPane.clearConstraints(messageLabel);
                                        AnchorPane.clearConstraints(copyIcon);
                                        AnchorPane.clearConstraints(statusIcon);
                                        AnchorPane.clearConstraints(attachmentsBox);

                                        copyIcon.setOnMouseClicked(null);
                                        setGraphic(null);
                                    }
                                }

                                /*  private void showNotArrivedIcon() {
                                statusIcon.setVisible(true);
                                AwesomeDude.setIcon(statusIcon, AwesomeIcon.WARNING_SIGN, "14");
                                Tooltip.install(statusIcon, new Tooltip("Message did not arrive. Please try to send again."));
                                statusIcon.setTextFill(Paint.valueOf("#dd0000"));
                                  }*/

                                private void showMailboxIcon() {
                                    statusIcon.setVisible(true);
                                    AwesomeDude.setIcon(statusIcon, AwesomeIcon.ENVELOPE_ALT, "14");
                                    Tooltip.install(statusIcon, new Tooltip("Message saved in receiver's mailbox"));
                                    statusIcon.setTextFill(Paint.valueOf("#0f87c3"));
                                }

                                private void showArrivedIcon() {
                                    statusIcon.setVisible(true);
                                    AwesomeDude.setIcon(statusIcon, AwesomeIcon.OK, "14");
                                    Tooltip.install(statusIcon, new Tooltip("Message arrived at receiver"));
                                    statusIcon.setTextFill(Paint.valueOf("#0f87c3"));
                                }
                            };
                        }
                    });

            if (root.getChildren().size() > 2)
                root.getChildren().remove(2);
            root.getChildren().add(2, messagesAnchorPane);

            scrollToBottom();
        }

        addListenersOnSelectDispute();
    }

    ///////////////////////////////////////////////////////////////////////////////////////////
    // Table
    ///////////////////////////////////////////////////////////////////////////////////////////

    private TableColumn<Dispute, Dispute> getSelectColumn() {
        TableColumn<Dispute, Dispute> column = new TableColumn<Dispute, Dispute>("Select") {
            {
                setMinWidth(80);
                setMaxWidth(80);
                setSortable(false);
            }
        };
        column.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
        column.setCellFactory(new Callback<TableColumn<Dispute, Dispute>, TableCell<Dispute, Dispute>>() {

            @Override
            public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
                return new TableCell<Dispute, Dispute>() {

                    Button button;

                    @Override
                    public void updateItem(final Dispute item, boolean empty) {
                        super.updateItem(item, empty);
                        if (item != null && !empty) {
                            if (button == null) {
                                button = new Button("Select");
                                button.setOnAction(e -> tableView.getSelectionModel().select(item));
                                setGraphic(button);
                            }
                        } else {
                            setGraphic(null);
                            if (button != null) {
                                button.setOnAction(null);
                                button = null;
                            }
                        }
                    }
                };
            }
        });
        return column;
    }

    private TableColumn<Dispute, Dispute> getContractColumn() {
        TableColumn<Dispute, Dispute> column = new TableColumn<Dispute, Dispute>("Details") {
            {
                setMinWidth(80);
                setSortable(false);
            }
        };
        column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
        column.setCellFactory(new Callback<TableColumn<Dispute, Dispute>, TableCell<Dispute, Dispute>>() {

            @Override
            public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
                return new TableCell<Dispute, Dispute>() {
                    final Button button = new Button("Details");

                    {

                    }

                    @Override
                    public void updateItem(final Dispute item, boolean empty) {
                        super.updateItem(item, empty);

                        if (item != null && !empty) {
                            button.setOnAction(e -> onOpenContract(item));
                            setGraphic(button);
                        } else {
                            setGraphic(null);
                            button.setOnAction(null);
                        }
                    }
                };
            }
        });
        return column;
    }

    private TableColumn<Dispute, Dispute> getDateColumn() {
        TableColumn<Dispute, Dispute> column = new TableColumn<Dispute, Dispute>("Date") {
            {
                setMinWidth(180);
            }
        };
        column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
        column.setCellFactory(new Callback<TableColumn<Dispute, Dispute>, TableCell<Dispute, Dispute>>() {
            @Override
            public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
                return new TableCell<Dispute, Dispute>() {
                    @Override
                    public void updateItem(final Dispute item, boolean empty) {
                        super.updateItem(item, empty);
                        if (item != null && !empty)
                            setText(formatter.formatDateTime(item.getOpeningDate()));
                        else
                            setText("");
                    }
                };
            }
        });
        return column;
    }

    private TableColumn<Dispute, Dispute> getTradeIdColumn() {
        TableColumn<Dispute, Dispute> column = new TableColumn<Dispute, Dispute>("Trade ID") {
            {
                setMinWidth(110);
            }
        };
        column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
        column.setCellFactory(new Callback<TableColumn<Dispute, Dispute>, TableCell<Dispute, Dispute>>() {
            @Override
            public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
                return new TableCell<Dispute, Dispute>() {
                    private HyperlinkWithIcon field;

                    @Override
                    public void updateItem(final Dispute item, boolean empty) {
                        super.updateItem(item, empty);

                        if (item != null && !empty) {
                            field = new HyperlinkWithIcon(item.getShortTradeId(), true);
                            Optional<Trade> tradeOptional = tradeManager.getTradeById(item.getTradeId());
                            if (tradeOptional.isPresent()) {
                                field.setMouseTransparent(false);
                                field.setTooltip(new Tooltip("Open popup for details"));
                                field.setOnAction(event -> tradeDetailsWindow.show(tradeOptional.get()));
                            } else {
                                field.setMouseTransparent(true);
                            }
                            setGraphic(field);
                        } else {
                            setGraphic(null);
                            if (field != null)
                                field.setOnAction(null);
                        }
                    }
                };
            }
        });
        return column;
    }

    private TableColumn<Dispute, Dispute> getBuyerOnionAddressColumn() {
        TableColumn<Dispute, Dispute> column = new TableColumn<Dispute, Dispute>("BTC buyer address") {
            {
                setMinWidth(170);
            }
        };
        column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
        column.setCellFactory(new Callback<TableColumn<Dispute, Dispute>, TableCell<Dispute, Dispute>>() {
            @Override
            public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
                return new TableCell<Dispute, Dispute>() {
                    @Override
                    public void updateItem(final Dispute item, boolean empty) {
                        super.updateItem(item, empty);
                        if (item != null && !empty)
                            setText(getBuyerOnionAddressColumnLabel(item));
                        else
                            setText("");
                    }
                };
            }
        });
        return column;
    }

    private TableColumn<Dispute, Dispute> getSellerOnionAddressColumn() {
        TableColumn<Dispute, Dispute> column = new TableColumn<Dispute, Dispute>("BTC seller address") {
            {
                setMinWidth(170);
            }
        };
        column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
        column.setCellFactory(new Callback<TableColumn<Dispute, Dispute>, TableCell<Dispute, Dispute>>() {
            @Override
            public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
                return new TableCell<Dispute, Dispute>() {
                    @Override
                    public void updateItem(final Dispute item, boolean empty) {
                        super.updateItem(item, empty);
                        if (item != null && !empty)
                            setText(getSellerOnionAddressColumnLabel(item));
                        else
                            setText("");
                    }
                };
            }
        });
        return column;
    }

    protected String getBuyerOnionAddressColumnLabel(Dispute item) {
        Contract contract = item.getContract();
        if (contract != null) {
            NodeAddress buyerNodeAddress = contract.getBuyerNodeAddress();
            if (buyerNodeAddress != null)
                return buyerNodeAddress.getHostNameWithoutPostFix() + " ("
                        + disputeManager.getNrOfDisputes(true, contract) + ")";
            else
                return "N/A";
        } else {
            return "N/A";
        }
    }

    protected String getSellerOnionAddressColumnLabel(Dispute item) {
        Contract contract = item.getContract();
        if (contract != null) {
            NodeAddress sellerNodeAddress = contract.getSellerNodeAddress();
            if (sellerNodeAddress != null)
                return sellerNodeAddress.getHostNameWithoutPostFix() + " ("
                        + disputeManager.getNrOfDisputes(false, contract) + ")";
            else
                return "N/A";
        } else {
            return "N/A";
        }
    }

    private TableColumn<Dispute, Dispute> getMarketColumn() {
        TableColumn<Dispute, Dispute> column = new TableColumn<Dispute, Dispute>("Market") {
            {
                setMinWidth(130);
            }
        };
        column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
        column.setCellFactory(new Callback<TableColumn<Dispute, Dispute>, TableCell<Dispute, Dispute>>() {
            @Override
            public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
                return new TableCell<Dispute, Dispute>() {
                    @Override
                    public void updateItem(final Dispute item, boolean empty) {
                        super.updateItem(item, empty);
                        if (item != null && !empty)
                            setText(formatter.getCurrencyPair(item.getContract().offer.getCurrencyCode()));
                        else
                            setText("");
                    }
                };
            }
        });
        return column;
    }

    private TableColumn<Dispute, Dispute> getRoleColumn() {
        TableColumn<Dispute, Dispute> column = new TableColumn<Dispute, Dispute>("Role") {
            {
                setMinWidth(130);
            }
        };
        column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
        column.setCellFactory(new Callback<TableColumn<Dispute, Dispute>, TableCell<Dispute, Dispute>>() {
            @Override
            public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
                return new TableCell<Dispute, Dispute>() {
                    @Override
                    public void updateItem(final Dispute item, boolean empty) {
                        super.updateItem(item, empty);
                        if (item != null && !empty) {
                            if (item.isDisputeOpenerIsOfferer())
                                setText(item.isDisputeOpenerIsBuyer() ? "BTC buyer/Offerer" : "BTC seller/Offerer");
                            else
                                setText(item.isDisputeOpenerIsBuyer() ? "BTC buyer/Taker" : "BTC seller/Taker");
                        } else {
                            setText("");
                        }
                    }
                };
            }
        });
        return column;
    }

    private TableColumn<Dispute, Dispute> getStateColumn() {
        TableColumn<Dispute, Dispute> column = new TableColumn<Dispute, Dispute>("State") {
            {
                setMinWidth(50);
            }
        };
        column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
        column.setCellFactory(new Callback<TableColumn<Dispute, Dispute>, TableCell<Dispute, Dispute>>() {
            @Override
            public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
                return new TableCell<Dispute, Dispute>() {

                    public ReadOnlyBooleanProperty closedProperty;
                    public ChangeListener<Boolean> listener;

                    @Override
                    public void updateItem(final Dispute item, boolean empty) {
                        super.updateItem(item, empty);
                        if (item != null && !empty) {
                            listener = (observable, oldValue, newValue) -> {
                                setText(newValue ? "Closed" : "Open");
                                getTableRow().setOpacity(newValue ? 0.4 : 1);
                            };
                            closedProperty = item.isClosedProperty();
                            closedProperty.addListener(listener);
                            boolean isClosed = item.isClosed();
                            setText(isClosed ? "Closed" : "Open");
                            getTableRow().setOpacity(isClosed ? 0.4 : 1);
                        } else {
                            if (closedProperty != null) {
                                closedProperty.removeListener(listener);
                                closedProperty = null;
                            }
                            setText("");
                        }
                    }
                };
            }
        });
        return column;
    }

    private void scrollToBottom() {
        if (messageListView != null)
            UserThread.execute(() -> messageListView.scrollTo(Integer.MAX_VALUE));
    }

}