/* * 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)); } }