From b5b126ceb4bbce996eadf7b3a3dec29ad30f0c26 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Fri, 24 May 2019 00:33:31 +0200 Subject: [PATCH 01/33] Refactor Chat out of TraderDisputeView On the way to adding chat for traders this is a first step. Mainly just moving functionality out of TraderDisputeView to Chat class. There are still remaining dispute functionality that needs to be factored away. --- .../java/bisq/desktop/main/Chat/Chat.java | 705 ++++++++++++++++++ .../disputes/trader/TraderDisputeView.java | 603 +-------------- 2 files changed, 733 insertions(+), 575 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/main/Chat/Chat.java diff --git a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java new file mode 100644 index 00000000000..d2324e763da --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java @@ -0,0 +1,705 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.Chat; + +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.BisqTextArea; +import bisq.desktop.components.BusyAnimation; +import bisq.desktop.components.TableGroupHeadline; +import bisq.desktop.components.TextFieldWithIcon; +import bisq.desktop.main.disputes.trader.TraderDisputeView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; +import bisq.desktop.util.GUIUtil; + +import bisq.core.arbitration.Attachment; +import bisq.core.arbitration.Dispute; +import bisq.core.arbitration.DisputeManager; +import bisq.core.arbitration.messages.DisputeCommunicationMessage; +import bisq.core.locale.Res; +import bisq.core.util.BSFormatter; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.network.Connection; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.Version; +import bisq.common.util.Utilities; + +import com.google.common.io.ByteStreams; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.stage.FileChooser; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.TextArea; +import javafx.scene.control.Tooltip; +import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Paint; +import javafx.scene.text.TextAlignment; + +import javafx.geometry.Insets; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; + +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.value.ChangeListener; + +import javafx.event.EventHandler; + +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; + +import javafx.util.Callback; + +import java.net.MalformedURLException; +import java.net.URL; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import lombok.Setter; + +public class Chat extends AnchorPane { + public static final Logger log = LoggerFactory.getLogger(TextFieldWithIcon.class); + + // UI + private TextArea inputTextArea; + private Button sendButton; + private ListView messageListView; + private Label sendMsgInfoLabel; + private BusyAnimation sendMsgBusyAnimation; + private TableGroupHeadline tableGroupHeadline; + private VBox messagesInputBox; + + // TODO Dispute stuff - need to go and generic functionality added + private final DisputeSummaryWindow disputeSummaryWindow; + private TraderDisputeView traderDisputeView; + private final DisputeManager disputeManager; + + // Communication stuff, to be renamed to something more generic + private final P2PService p2PService; + private DisputeCommunicationMessage disputeCommunicationMessage; + private ObservableList disputeCommunicationMessages; + private ListChangeListener disputeDirectMessageListListener; + private Subscription inputTextAreaTextSubscription; + private final List tempAttachments = new ArrayList<>(); + private ChangeListener storedInMailboxPropertyListener, arrivedPropertyListener; + private ChangeListener sendMessageErrorPropertyListener; + + @Setter + private boolean isTrader; + + protected final BSFormatter formatter; + private EventHandler keyEventEventHandler; + + public Chat(TraderDisputeView traderDisputeView, + DisputeSummaryWindow disputeSummaryWindow, + DisputeManager disputeManager, + P2PService p2PService, + BSFormatter formatter + ) { + this.traderDisputeView = traderDisputeView; + this.disputeSummaryWindow = disputeSummaryWindow; + this.disputeManager = disputeManager; + this.p2PService = p2PService; + this.formatter = formatter; + } + + public void initialize() { + disputeDirectMessageListListener = c -> scrollToBottom(); + + keyEventEventHandler = event -> { + if (Utilities.isAltOrCtrlPressed(KeyCode.ENTER, event)) { + if (traderDisputeView.getSelectedDispute() != null && + messagesInputBox.isVisible() && inputTextArea.isFocused()) + onTrySendMessage(); + } + }; + } + + public void activate() { + if (getScene() != null) + getScene().addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } + + public void deactivate() { + if (getScene() != null) + getScene().removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } + + public void display() { + if (traderDisputeView.getSelectedDispute() == null) + return; + this.getChildren().clear(); + + tableGroupHeadline = new TableGroupHeadline(); + tableGroupHeadline.setText(Res.get("support.messages")); + + AnchorPane.setTopAnchor(tableGroupHeadline, 10d); + AnchorPane.setRightAnchor(tableGroupHeadline, 0d); + AnchorPane.setBottomAnchor(tableGroupHeadline, 0d); + AnchorPane.setLeftAnchor(tableGroupHeadline, 0d); + + disputeCommunicationMessages = traderDisputeView.getSelectedDispute().getDisputeCommunicationMessages(); + SortedList sortedList = new SortedList<>(disputeCommunicationMessages); + sortedList.setComparator(Comparator.comparing(o -> new Date(o.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); + + VBox.setVgrow(this, Priority.ALWAYS); + + inputTextArea = new BisqTextArea(); + inputTextArea.setPrefHeight(70); + inputTextArea.setWrapText(true); + if (isTrader) + inputTextArea.setPromptText(Res.get("support.input.prompt")); + + sendButton = new AutoTooltipButton(Res.get("support.send")); + sendButton.setDefaultButton(true); + sendButton.setOnAction(e -> onTrySendMessage()); + inputTextAreaTextSubscription = EasyBind.subscribe(inputTextArea.textProperty(), t -> sendButton.setDisable(t.isEmpty())); + + Button uploadButton = new AutoTooltipButton(Res.get("support.addAttachments")); + uploadButton.setOnAction(e -> onRequestUpload()); + + sendMsgInfoLabel = new AutoTooltipLabel(); + sendMsgInfoLabel.setVisible(false); + sendMsgInfoLabel.setManaged(false); + sendMsgInfoLabel.setPadding(new Insets(5, 0, 0, 0)); + + sendMsgBusyAnimation = new BusyAnimation(false); + + if (!traderDisputeView.getSelectedDispute().isClosed()) { + HBox buttonBox = new HBox(); + buttonBox.setSpacing(10); + buttonBox.getChildren().addAll(sendButton, uploadButton, sendMsgBusyAnimation, sendMsgInfoLabel); + + if (!isTrader) { + Button closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); + closeDisputeButton.setOnAction(e -> onCloseDispute(traderDisputeView.getSelectedDispute())); + 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); + + this.getChildren().addAll(tableGroupHeadline, messageListView, messagesInputBox); + } else { + AnchorPane.setBottomAnchor(messageListView, 0d); + this.getChildren().addAll(tableGroupHeadline, messageListView); + } + + messageListView.setCellFactory(new Callback<>() { + @Override + public ListCell call(ListView list) { + return new ListCell<>() { + ChangeListener sendMsgBusyAnimationListener; + final Pane bg = new Pane(); + final ImageView arrow = new ImageView(); + final Label headerLabel = new AutoTooltipLabel(); + final Label messageLabel = new AutoTooltipLabel(); + final Label copyIcon = new Label(); + final HBox attachmentsBox = new HBox(); + final AnchorPane messageAnchorPane = new AnchorPane(); + final Label statusIcon = new Label(); + final Label statusInfoLabel = new Label(); + final HBox statusHBox = new HBox(); + 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.getStyleClass().add("small-text"); + statusInfoLabel.getStyleClass().add("small-text"); + statusInfoLabel.setPadding(new Insets(3, 0, 0, 0)); + copyIcon.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); + statusHBox.setSpacing(5); + statusHBox.getChildren().addAll(statusIcon, statusInfoLabel); + messageAnchorPane.getChildren().addAll(bg, arrow, headerLabel, messageLabel, copyIcon, attachmentsBox, statusHBox); + } + + @Override + public void updateItem(final DisputeCommunicationMessage message, boolean empty) { + super.updateItem(message, empty); + if (message != null && !empty) { + copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(messageLabel.getText())); + messageLabel.setOnMouseClicked(event -> { + if (2 > event.getClickCount()) { + return; + } + GUIUtil.showSelectableTextModal(headerLabel.getText(), messageLabel.getText()); + }); + + 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 = message.isSenderIsTrader(); + boolean isMyMsg = isTrader == senderIsTrader; + + arrow.setVisible(!message.isSystemMessage()); + arrow.setManaged(!message.isSystemMessage()); + statusHBox.setVisible(false); + + headerLabel.getStyleClass().removeAll("message-header", "my-message-header", "success-text", + "highlight-static"); + messageLabel.getStyleClass().removeAll("my-message", "message"); + copyIcon.getStyleClass().removeAll("my-message", "message"); + + if (message.isSystemMessage()) { + headerLabel.getStyleClass().addAll("message-header", "success-text"); + bg.setId("message-bubble-green"); + messageLabel.getStyleClass().add("my-message"); + copyIcon.getStyleClass().add("my-message"); + message.addWeakMessageStateListener(() -> updateMsgState(message)); + updateMsgState(message); + } else if (isMyMsg) { + headerLabel.getStyleClass().add("my-message-header"); + bg.setId("message-bubble-blue"); + messageLabel.getStyleClass().add("my-message"); + copyIcon.getStyleClass().add("my-message"); + 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) + updateMsgState(message); + }; + + sendMsgBusyAnimation.isRunningProperty().addListener(sendMsgBusyAnimationListener); + message.addWeakMessageStateListener(() -> updateMsgState(message)); + updateMsgState(message); + } else { + headerLabel.getStyleClass().add("message-header"); + bg.setId("message-bubble-grey"); + messageLabel.getStyleClass().add("message"); + copyIcon.getStyleClass().add("message"); + if (isTrader) + arrow.setId("bubble_arrow_grey_right"); + else + arrow.setId("bubble_arrow_grey_left"); + } + + if (message.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); + AnchorPane.clearConstraints(statusHBox); + AnchorPane.setLeftAnchor(statusHBox, 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.clearConstraints(statusHBox); + AnchorPane.setRightAnchor(statusHBox, 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.clearConstraints(statusHBox); + AnchorPane.setLeftAnchor(statusHBox, padding); + } + AnchorPane.setBottomAnchor(statusHBox, 7d); + headerLabel.setText(formatter.formatDateTime(new Date(message.getDate()))); + messageLabel.setText(message.getMessage()); + attachmentsBox.getChildren().clear(); + if (message.getAttachments() != null && message.getAttachments().size() > 0) { + AnchorPane.setBottomAnchor(messageLabel, bottomBorder + attachmentsBoxHeight + 10); + attachmentsBox.getChildren().add(new AutoTooltipLabel(Res.get("support.attachments") + " ") {{ + setPadding(new Insets(0, 0, 3, 0)); + if (isMyMsg) + getStyleClass().add("my-message"); + else + getStyleClass().add("message"); + }}); + message.getAttachments().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().addAll("icon", "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(statusHBox); + AnchorPane.clearConstraints(attachmentsBox); + + copyIcon.setOnMouseClicked(null); + messageLabel.setOnMouseClicked(null); + setGraphic(null); + } + } + + private void updateMsgState(DisputeCommunicationMessage message) { + boolean visible; + AwesomeIcon icon = null; + String text = null; + statusIcon.setTextFill(Paint.valueOf("#0f87c3")); + statusInfoLabel.setTextFill(Paint.valueOf("#0f87c3")); + statusHBox.setOpacity(1); + log.debug("updateMsgState msg-{}, ack={}, arrived={}", message.getMessage(), + message.acknowledgedProperty().get(), message.arrivedProperty().get()); + if (message.acknowledgedProperty().get()) { + visible = true; + icon = AwesomeIcon.OK_SIGN; + text = Res.get("support.acknowledged"); + + } else if (message.ackErrorProperty().get() != null) { + visible = true; + icon = AwesomeIcon.EXCLAMATION_SIGN; + text = Res.get("support.error", message.ackErrorProperty().get()); + statusIcon.setTextFill(Paint.valueOf("#dd0000")); + statusInfoLabel.setTextFill(Paint.valueOf("#dd0000")); + } else if (message.arrivedProperty().get()) { + visible = true; + icon = AwesomeIcon.OK; + text = Res.get("support.arrived"); + statusHBox.setOpacity(0.5); + } else if (message.storedInMailboxProperty().get()) { + visible = true; + icon = AwesomeIcon.ENVELOPE; + text = Res.get("support.savedInMailbox"); + statusHBox.setOpacity(0.5); + } else { + visible = false; + log.debug("updateMsgState called but no msg state available. message={}", message); + } + + statusHBox.setVisible(visible); + if (visible) { + AwesomeDude.setIcon(statusIcon, icon, "14"); + statusIcon.setTooltip(new Tooltip(text)); + statusInfoLabel.setText(text); + } + } + }; + } + }); + + scrollToBottom(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Actions + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onTrySendMessage() { + if (p2PService.isBootstrapped()) { + String text = inputTextArea.getText(); + if (!text.isEmpty()) { + if (text.length() < 5_000) { + onSendMessage(text, traderDisputeView.getSelectedDispute()); + } else { + new Popup<>().information(Res.get("popup.warning.messageTooLong")).show(); + } + } + } else { + new Popup<>().information(Res.get("popup.warning.notFullyConnected")).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.getPermittedMessageSize(); + int maxSizeInKB = maxMsgSize / 1024; + fileChooser.setTitle(Res.get("support.openFile", maxSizeInKB)); + /* if (Utilities.isUnix()) + fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));*/ + File result = fileChooser.showOpenDialog(getScene().getWindow()); + 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(Res.get("support.attachmentTooLarge", (newSize / 1024), maxSizeInKB)).show(); + } else if (size > maxMsgSize) { + new Popup<>().warning(Res.get("support.maxSize", maxSizeInKB)).show(); + } else { + tempAttachments.add(new Attachment(result.getName(), filesAsBytes)); + inputTextArea.setText(inputTextArea.getText() + "\n[" + Res.get("support.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(Res.get("support.tooManyAttachments")).show(); + } + } + + private void onCloseDispute(Dispute dispute) { + long protocolVersion = dispute.getContract().getOfferPayload().getProtocolVersion(); + if (protocolVersion == Version.TRADE_PROTOCOL_VERSION) { + disputeSummaryWindow.onFinalizeDispute(() -> this.getChildren().remove(messagesInputBox)) + .show(dispute); + } else { + new Popup<>() + .warning(Res.get("support.wrongVersion", protocolVersion)) + .show(); + } + } + + private void onOpenAttachment(Attachment attachment) { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(Res.get("support.save")); + fileChooser.setInitialFileName(attachment.getFileName()); + /* if (Utilities.isUnix()) + fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));*/ + File file = fileChooser.showSaveDialog(getScene().getWindow()); + 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 onSendMessage(String inputText, Dispute dispute) { + if (disputeCommunicationMessage != null) { + disputeCommunicationMessage.arrivedProperty().removeListener(arrivedPropertyListener); + disputeCommunicationMessage.storedInMailboxProperty().removeListener(storedInMailboxPropertyListener); + disputeCommunicationMessage.sendMessageErrorProperty().removeListener(sendMessageErrorPropertyListener); + } + + 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(Res.get("support.sendingMessage")); + + sendMsgBusyAnimation.play(); + }, 500, TimeUnit.MILLISECONDS); + + arrivedPropertyListener = (observable, oldValue, newValue) -> { + if (newValue) { + hideSendMsgInfo(timer); + } + }; + storedInMailboxPropertyListener = (observable, oldValue, newValue) -> { + if (newValue) { + sendMsgInfoLabel.setVisible(true); + sendMsgInfoLabel.setManaged(true); + sendMsgInfoLabel.setText(Res.get("support.receiverNotOnline")); + hideSendMsgInfo(timer); + } + }; + sendMessageErrorPropertyListener = (observable, oldValue, newValue) -> { + if (newValue != null) { + sendMsgInfoLabel.setVisible(true); + sendMsgInfoLabel.setManaged(true); + sendMsgInfoLabel.setText(Res.get("support.sendMessageError", newValue)); + hideSendMsgInfo(timer); + } + }; + if (disputeCommunicationMessage != null) { + disputeCommunicationMessage.arrivedProperty().addListener(arrivedPropertyListener); + disputeCommunicationMessage.storedInMailboxProperty().addListener(storedInMailboxPropertyListener); + disputeCommunicationMessage.sendMessageErrorProperty().addListener(sendMessageErrorPropertyListener); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Helpers + /////////////////////////////////////////////////////////////////////////////////////////// + + private void hideSendMsgInfo(Timer timer) { + timer.stop(); + inputTextArea.setDisable(false); + + UserThread.runAfter(() -> { + sendMsgInfoLabel.setVisible(false); + sendMsgInfoLabel.setManaged(false); + }, 5); + sendMsgBusyAnimation.stop(); + } + + public void scrollToBottom() { + if (messageListView != null) + UserThread.execute(() -> messageListView.scrollTo(Integer.MAX_VALUE)); + } + + public void setInputBoxVisible(boolean visible) { + messagesInputBox.setVisible(visible); + messagesInputBox.setManaged(visible); + AnchorPane.setBottomAnchor(messageListView, visible ? 120d : 0d); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Bindings + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addListenersOnSelectDispute(ReadOnlyDoubleProperty widthProperty) { + if (tableGroupHeadline != null) { + tableGroupHeadline.prefWidthProperty().bind(widthProperty); + messageListView.prefWidthProperty().bind(widthProperty); + this.prefWidthProperty().bind(widthProperty); + disputeCommunicationMessages.addListener(disputeDirectMessageListListener); + inputTextAreaTextSubscription = EasyBind.subscribe(inputTextArea.textProperty(), t -> sendButton.setDisable(t.isEmpty())); + } + } + + public void removeListenersOnSelectDispute() { + 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(); + + this.prefWidthProperty().unbind(); + + if (inputTextAreaTextSubscription != null) + inputTextAreaTextSubscription.unsubscribe(); + } + +} diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java b/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java index 753c7956384..96af973dacf 100644 --- a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java @@ -22,12 +22,9 @@ import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.AutoTooltipTableColumn; -import bisq.desktop.components.BisqTextArea; -import bisq.desktop.components.BusyAnimation; import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.components.InputTextField; -import bisq.desktop.components.TableGroupHeadline; -import bisq.desktop.main.disputes.arbitrator.ArbitratorDisputeView; +import bisq.desktop.main.Chat.Chat; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.ContractWindow; import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; @@ -37,10 +34,8 @@ import bisq.core.alert.PrivateNotificationManager; import bisq.core.app.AppOptionKeys; -import bisq.core.arbitration.Attachment; import bisq.core.arbitration.Dispute; import bisq.core.arbitration.DisputeManager; -import bisq.core.arbitration.messages.DisputeCommunicationMessage; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.payment.AccountAgeWitnessService; @@ -51,11 +46,7 @@ import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; -import bisq.network.p2p.network.Connection; -import bisq.common.Timer; -import bisq.common.UserThread; -import bisq.common.app.Version; import bisq.common.crypto.KeyRing; import bisq.common.crypto.PubKeyRing; import bisq.common.util.Utilities; @@ -65,33 +56,19 @@ import javax.inject.Inject; 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 javafx.stage.FileChooser; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; -import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; -import javafx.scene.control.TextArea; import javafx.scene.control.Tooltip; -import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; -import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; -import javafx.scene.paint.Paint; -import javafx.scene.text.TextAlignment; import javafx.geometry.Insets; @@ -104,8 +81,6 @@ import javafx.event.EventHandler; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; @@ -115,14 +90,6 @@ import java.text.ParseException; import java.text.SimpleDateFormat; -import java.net.MalformedURLException; -import java.net.URL; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; - import java.util.ArrayList; import java.util.Comparator; import java.util.Date; @@ -130,9 +97,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; +import lombok.Getter; // will be probably only used for arbitration communication, will be renamed and the icon changed @FxmlView @@ -148,31 +114,19 @@ public class TraderDisputeView extends ActivatableView { private final TradeDetailsWindow tradeDetailsWindow; private final P2PService p2PService; - private final List tempAttachments = new ArrayList<>(); private final AccountAgeWitnessService accountAgeWitnessService; private final boolean useDevPrivilegeKeys; private TableView tableView; private SortedList sortedList; + @Getter private Dispute selectedDispute; - private ListView messageListView; - private TextArea inputTextArea; - private AnchorPane messagesAnchorPane; - private VBox messagesInputBox; - private BusyAnimation sendMsgBusyAnimation; - private Label sendMsgInfoLabel; - private ChangeListener storedInMailboxPropertyListener, arrivedPropertyListener; - private ChangeListener sendMessageErrorPropertyListener; - @Nullable - private DisputeCommunicationMessage disputeCommunicationMessage; - private ListChangeListener disputeDirectMessageListListener; + + private Chat disputeChat; + private ChangeListener selectedDisputeClosedPropertyListener; private Subscription selectedDisputeSubscription; - private TableGroupHeadline tableGroupHeadline; - private ObservableList disputeCommunicationMessages; - private Button sendButton; - private Subscription inputTextAreaTextSubscription; private EventHandler keyEventEventHandler; private Scene scene; protected FilteredList filteredList; @@ -273,19 +227,10 @@ public void initialize() { 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); + disputeChat.setInputBoxVisible(!newValue); }; - disputeDirectMessageListListener = c -> scrollToBottom(); - keyEventEventHandler = event -> { if (Utilities.isAltOrCtrlPressed(KeyCode.L, event)) { Map> map = new HashMap<>(); @@ -369,15 +314,18 @@ public void initialize() { .onAddAlertMessage(privateNotificationManager::sendPrivateNotificationMessageIfKeyIsValid) .show(); } - } else if (Utilities.isAltOrCtrlPressed(KeyCode.ENTER, event)) { - if (selectedDispute != null && messagesInputBox.isVisible() && inputTextArea.isFocused()) - onTrySendMessage(); } }; + + disputeChat = new Chat(this, disputeSummaryWindow, disputeManager, p2PService, formatter); + disputeChat.initialize(); } @Override protected void activate() { + if (disputeChat != null) + disputeChat.activate(); + filterTextField.textProperty().addListener(filterTextFieldListener); disputeManager.cleanupDisputes(); @@ -395,7 +343,8 @@ protected void activate() { if (selectedItem != null) tableView.getSelectionModel().select(selectedItem); - scrollToBottom(); + if (disputeChat != null) + disputeChat.scrollToBottom(); scene = root.getScene(); if (scene != null) @@ -460,6 +409,9 @@ protected void deactivate() { if (scene != null) scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + + if (disputeChat != null) + disputeChat.deactivate(); } protected void applyFilteredListPredicate(String filterString) { @@ -476,189 +428,20 @@ private void onOpenContract(Dispute dispute) { contractWindow.show(dispute); } - private void onTrySendMessage() { - if (p2PService.isBootstrapped()) { - String text = inputTextArea.getText(); - if (!text.isEmpty()) { - if (text.length() < 5_000) { - onSendMessage(text, selectedDispute); - } else { - new Popup<>().information(Res.get("popup.warning.messageTooLong")).show(); - } - } - } else { - new Popup<>().information(Res.get("popup.warning.notFullyConnected")).show(); - } - } - - private void onSendMessage(String inputText, Dispute dispute) { - if (disputeCommunicationMessage != null) { - disputeCommunicationMessage.arrivedProperty().removeListener(arrivedPropertyListener); - disputeCommunicationMessage.storedInMailboxProperty().removeListener(storedInMailboxPropertyListener); - disputeCommunicationMessage.sendMessageErrorProperty().removeListener(sendMessageErrorPropertyListener); - } - - 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(Res.get("support.sendingMessage")); - - sendMsgBusyAnimation.play(); - }, 500, TimeUnit.MILLISECONDS); - - arrivedPropertyListener = (observable, oldValue, newValue) -> { - if (newValue) { - hideSendMsgInfo(timer); - } - }; - storedInMailboxPropertyListener = (observable, oldValue, newValue) -> { - if (newValue) { - sendMsgInfoLabel.setVisible(true); - sendMsgInfoLabel.setManaged(true); - sendMsgInfoLabel.setText(Res.get("support.receiverNotOnline")); - hideSendMsgInfo(timer); - } - }; - sendMessageErrorPropertyListener = (observable, oldValue, newValue) -> { - if (newValue != null) { - sendMsgInfoLabel.setVisible(true); - sendMsgInfoLabel.setManaged(true); - sendMsgInfoLabel.setText(Res.get("support.sendMessageError", newValue)); - hideSendMsgInfo(timer); - } - }; - if (disputeCommunicationMessage != null) { - disputeCommunicationMessage.arrivedProperty().addListener(arrivedPropertyListener); - disputeCommunicationMessage.storedInMailboxProperty().addListener(storedInMailboxPropertyListener); - disputeCommunicationMessage.sendMessageErrorProperty().addListener(sendMessageErrorPropertyListener); - } - } - - 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().getOfferPayload().getProtocolVersion(); - if (protocolVersion == Version.TRADE_PROTOCOL_VERSION) { - disputeSummaryWindow.onFinalizeDispute(() -> messagesAnchorPane.getChildren().remove(messagesInputBox)) - .show(dispute); - } else { - new Popup<>() - .warning(Res.get("support.wrongVersion", 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.getPermittedMessageSize(); - int maxSizeInKB = maxMsgSize / 1024; - fileChooser.setTitle(Res.get("support.openFile", maxSizeInKB)); - /* if (Utilities.isUnix()) - fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));*/ - File result = fileChooser.showOpenDialog(root.getScene().getWindow()); - 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(Res.get("support.attachmentTooLarge", (newSize / 1024), maxSizeInKB)).show(); - } else if (size > maxMsgSize) { - new Popup<>().warning(Res.get("support.maxSize", maxSizeInKB)).show(); - } else { - tempAttachments.add(new Attachment(result.getName(), filesAsBytes)); - inputTextArea.setText(inputTextArea.getText() + "\n[" + Res.get("support.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(Res.get("support.tooManyAttachments")).show(); - } - } - - private void onOpenAttachment(Attachment attachment) { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle(Res.get("support.save")); - fileChooser.setInitialFileName(attachment.getFileName()); - /* if (Utilities.isUnix()) - fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));*/ - File file = fileChooser.showSaveDialog(root.getScene().getWindow()); - 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 (disputeChat != null) + disputeChat.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())); - } + if (selectedDispute != null) + selectedDispute.isClosedProperty().addListener(selectedDisputeClosedPropertyListener); + if (disputeChat != null) + disputeChat.addListenersOnSelectDispute(root.widthProperty()); } private void onSelectDispute(Dispute dispute) { @@ -670,339 +453,14 @@ private void onSelectDispute(Dispute dispute) { selectedDispute = null; } else if (selectedDispute != dispute) { this.selectedDispute = dispute; - - boolean isTrader = disputeManager.isTrader(selectedDispute); - - tableGroupHeadline = new TableGroupHeadline(); - tableGroupHeadline.setText(Res.get("support.messages")); - - AnchorPane.setTopAnchor(tableGroupHeadline, 10d); - AnchorPane.setRightAnchor(tableGroupHeadline, 0d); - AnchorPane.setBottomAnchor(tableGroupHeadline, 0d); - AnchorPane.setLeftAnchor(tableGroupHeadline, 0d); - - disputeCommunicationMessages = selectedDispute.getDisputeCommunicationMessages(); - SortedList sortedList = new SortedList<>(disputeCommunicationMessages); - sortedList.setComparator(Comparator.comparing(o -> new Date(o.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 BisqTextArea(); - inputTextArea.setPrefHeight(70); - inputTextArea.setWrapText(true); - if (!(this instanceof ArbitratorDisputeView)) - inputTextArea.setPromptText(Res.get("support.input.prompt")); - - sendButton = new AutoTooltipButton(Res.get("support.send")); - sendButton.setDefaultButton(true); - sendButton.setOnAction(e -> onTrySendMessage()); - inputTextAreaTextSubscription = EasyBind.subscribe(inputTextArea.textProperty(), t -> sendButton.setDisable(t.isEmpty())); - - Button uploadButton = new AutoTooltipButton(Res.get("support.addAttachments")); - uploadButton.setOnAction(e -> onRequestUpload()); - - sendMsgInfoLabel = new AutoTooltipLabel(); - 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 AutoTooltipButton(Res.get("support.closeTicket")); - 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); + if (disputeChat != null) { + disputeChat.setTrader(disputeManager.isTrader(selectedDispute)); + disputeChat.display(); } - messageListView.setCellFactory(new Callback<>() { - @Override - public ListCell call(ListView list) { - return new ListCell<>() { - ChangeListener sendMsgBusyAnimationListener; - final Pane bg = new Pane(); - final ImageView arrow = new ImageView(); - final Label headerLabel = new AutoTooltipLabel(); - final Label messageLabel = new AutoTooltipLabel(); - final Label copyIcon = new Label(); - final HBox attachmentsBox = new HBox(); - final AnchorPane messageAnchorPane = new AnchorPane(); - final Label statusIcon = new Label(); - final Label statusInfoLabel = new Label(); - final HBox statusHBox = new HBox(); - 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.getStyleClass().add("small-text"); - statusInfoLabel.getStyleClass().add("small-text"); - statusInfoLabel.setPadding(new Insets(3, 0, 0, 0)); - copyIcon.setTooltip(new Tooltip(Res.get("shared.copyToClipboard"))); - statusHBox.setSpacing(5); - statusHBox.getChildren().addAll(statusIcon, statusInfoLabel); - messageAnchorPane.getChildren().addAll(bg, arrow, headerLabel, messageLabel, copyIcon, attachmentsBox, statusHBox); - } - - @Override - public void updateItem(final DisputeCommunicationMessage message, boolean empty) { - super.updateItem(message, empty); - if (message != null && !empty) { - copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(messageLabel.getText())); - messageLabel.setOnMouseClicked(event -> { - if (2 > event.getClickCount()) { - return; - } - GUIUtil.showSelectableTextModal(headerLabel.getText(), messageLabel.getText()); - }); - - 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 = message.isSenderIsTrader(); - boolean isMyMsg = isTrader == senderIsTrader; - - arrow.setVisible(!message.isSystemMessage()); - arrow.setManaged(!message.isSystemMessage()); - statusHBox.setVisible(false); - - headerLabel.getStyleClass().removeAll("message-header", "my-message-header", "success-text", - "highlight-static"); - messageLabel.getStyleClass().removeAll("my-message", "message"); - copyIcon.getStyleClass().removeAll("my-message", "message"); - - if (message.isSystemMessage()) { - headerLabel.getStyleClass().addAll("message-header", "success-text"); - bg.setId("message-bubble-green"); - messageLabel.getStyleClass().add("my-message"); - copyIcon.getStyleClass().add("my-message"); - message.addWeakMessageStateListener(() -> updateMsgState(message)); - updateMsgState(message); - } else if (isMyMsg) { - headerLabel.getStyleClass().add("my-message-header"); - bg.setId("message-bubble-blue"); - messageLabel.getStyleClass().add("my-message"); - copyIcon.getStyleClass().add("my-message"); - 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) - updateMsgState(message); - }; - - sendMsgBusyAnimation.isRunningProperty().addListener(sendMsgBusyAnimationListener); - message.addWeakMessageStateListener(() -> updateMsgState(message)); - updateMsgState(message); - } else { - headerLabel.getStyleClass().add("message-header"); - bg.setId("message-bubble-grey"); - messageLabel.getStyleClass().add("message"); - copyIcon.getStyleClass().add("message"); - if (isTrader) - arrow.setId("bubble_arrow_grey_right"); - else - arrow.setId("bubble_arrow_grey_left"); - } - - if (message.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); - AnchorPane.clearConstraints(statusHBox); - AnchorPane.setLeftAnchor(statusHBox, 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.clearConstraints(statusHBox); - AnchorPane.setRightAnchor(statusHBox, 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.clearConstraints(statusHBox); - AnchorPane.setLeftAnchor(statusHBox, padding); - } - AnchorPane.setBottomAnchor(statusHBox, 7d); - headerLabel.setText(formatter.formatDateTime(new Date(message.getDate()))); - messageLabel.setText(message.getMessage()); - attachmentsBox.getChildren().clear(); - if (message.getAttachments() != null && message.getAttachments().size() > 0) { - AnchorPane.setBottomAnchor(messageLabel, bottomBorder + attachmentsBoxHeight + 10); - attachmentsBox.getChildren().add(new AutoTooltipLabel(Res.get("support.attachments") + " ") {{ - setPadding(new Insets(0, 0, 3, 0)); - if (isMyMsg) - getStyleClass().add("my-message"); - else - getStyleClass().add("message"); - }}); - message.getAttachments().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().addAll("icon", "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(statusHBox); - AnchorPane.clearConstraints(attachmentsBox); - - copyIcon.setOnMouseClicked(null); - messageLabel.setOnMouseClicked(null); - setGraphic(null); - } - } - - private void updateMsgState(DisputeCommunicationMessage message) { - boolean visible; - AwesomeIcon icon = null; - String text = null; - statusIcon.setTextFill(Paint.valueOf("#0f87c3")); - statusInfoLabel.setTextFill(Paint.valueOf("#0f87c3")); - statusHBox.setOpacity(1); - log.debug("updateMsgState msg-{}, ack={}, arrived={}", message.getMessage(), - message.acknowledgedProperty().get(), message.arrivedProperty().get()); - if (message.acknowledgedProperty().get()) { - visible = true; - icon = AwesomeIcon.OK_SIGN; - text = Res.get("support.acknowledged"); - - } else if (message.ackErrorProperty().get() != null) { - visible = true; - icon = AwesomeIcon.EXCLAMATION_SIGN; - text = Res.get("support.error", message.ackErrorProperty().get()); - statusIcon.setTextFill(Paint.valueOf("#dd0000")); - statusInfoLabel.setTextFill(Paint.valueOf("#dd0000")); - } else if (message.arrivedProperty().get()) { - visible = true; - icon = AwesomeIcon.OK; - text = Res.get("support.arrived"); - statusHBox.setOpacity(0.5); - } else if (message.storedInMailboxProperty().get()) { - visible = true; - icon = AwesomeIcon.ENVELOPE; - text = Res.get("support.savedInMailbox"); - statusHBox.setOpacity(0.5); - } else { - visible = false; - log.debug("updateMsgState called but no msg state available. message={}", message); - } - - statusHBox.setVisible(visible); - if (visible) { - AwesomeDude.setIcon(statusIcon, icon, "14"); - statusIcon.setTooltip(new Tooltip(text)); - statusInfoLabel.setText(text); - } - } - }; - } - }); - if (root.getChildren().size() > 2) root.getChildren().remove(2); - root.getChildren().add(2, messagesAnchorPane); - - scrollToBottom(); + root.getChildren().add(2, disputeChat); } addListenersOnSelectDispute(); @@ -1353,11 +811,6 @@ public void updateItem(final Dispute item, boolean empty) { return column; } - private void scrollToBottom() { - if (messageListView != null) - UserThread.execute(() -> messageListView.scrollTo(Integer.MAX_VALUE)); - } - } From 390fc444d16eed07679c1a8fdc8c75278451da8b Mon Sep 17 00:00:00 2001 From: sqrrm Date: Sat, 25 May 2019 12:07:01 +0200 Subject: [PATCH 02/33] Send acks when capability found --- p2p/src/main/java/bisq/network/p2p/P2PService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/src/main/java/bisq/network/p2p/P2PService.java b/p2p/src/main/java/bisq/network/p2p/P2PService.java index e3696025238..2534bceabd3 100644 --- a/p2p/src/main/java/bisq/network/p2p/P2PService.java +++ b/p2p/src/main/java/bisq/network/p2p/P2PService.java @@ -628,7 +628,7 @@ private boolean capabilityRequiredAndCapabilityNotSupported(NodeAddress peersNod log.warn("We don't send the message because the peer does not support the required capability. " + "peersNodeAddress={}", peersNodeAddress); - return result; + return !result; } log.warn("We don't have the peer in our persisted peers so we don't know his capabilities. " + From b1362a9a36776a1228153d10afbea46529b7153e Mon Sep 17 00:00:00 2001 From: sqrrm Date: Sat, 25 May 2019 16:22:15 +0200 Subject: [PATCH 03/33] Separate most of Chat and arbitration The naming of DisputeCommunicationMessage has to stay but they otherwise fit what would be more aptly named ChatCommunicationMessage or something in that spririt. --- .../bisq/core/arbitration/DisputeManager.java | 61 +------- .../java/bisq/desktop/main/Chat/Chat.java | 137 +++++++++++------- .../bisq/desktop/main/Chat/ChatSession.java | 46 ++++++ .../main/disputes/DisputeChatSession.java | 87 +++++++++++ .../disputes/trader/TraderDisputeView.java | 33 +++-- 5 files changed, 243 insertions(+), 121 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java create mode 100644 desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java diff --git a/core/src/main/java/bisq/core/arbitration/DisputeManager.java b/core/src/main/java/bisq/core/arbitration/DisputeManager.java index 91e59c4deaf..95091a2a803 100644 --- a/core/src/main/java/bisq/core/arbitration/DisputeManager.java +++ b/core/src/main/java/bisq/core/arbitration/DisputeManager.java @@ -79,7 +79,6 @@ import java.io.File; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -109,6 +108,7 @@ public class DisputeManager implements PersistedDataHost { private final P2PService p2PService; private final KeyRing keyRing; private final Storage disputeStorage; + @Getter private DisputeList disputes; private final String disputeInfo; private final CopyOnWriteArraySet decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>(); @@ -532,63 +532,6 @@ public void onFault(String errorMessage) { } } - // traders send msg to the arbitrator or arbitrator to 1 trader (trader to trader is not allowed) - public DisputeCommunicationMessage sendDisputeDirectMessage(Dispute dispute, String text, ArrayList attachments) { - DisputeCommunicationMessage message = new DisputeCommunicationMessage( - dispute.getTradeId(), - dispute.getTraderPubKeyRing().hashCode(), - isTrader(dispute), - text, - p2PService.getAddress() - ); - - message.addAllAttachments(attachments); - Tuple2 tuple = getNodeAddressPubKeyRingTuple(dispute); - NodeAddress peersNodeAddress = tuple.first; - PubKeyRing receiverPubKeyRing = tuple.second; - - if (isTrader(dispute) || - (isArbitrator(dispute) && !message.isSystemMessage())) - dispute.addDisputeCommunicationMessage(message); - - if (receiverPubKeyRing != null) { - log.info("Send {} to peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - - p2PService.sendEncryptedMailboxMessage(peersNodeAddress, - receiverPubKeyRing, - message, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - message.setArrived(true); - disputes.persist(); - } - - @Override - public void onStoredInMailbox() { - log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - message.setStoredInMailbox(true); - disputes.persist(); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); - message.setSendMessageError(errorMessage); - disputes.persist(); - } - } - ); - } - - return message; - } - // arbitrator send result to trader public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String text) { DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage( @@ -1109,7 +1052,7 @@ public String getNrOfDisputes(boolean isBuyer, Contract contract) { /////////////////////////////////////////////////////////////////////////////////////////// - private Tuple2 getNodeAddressPubKeyRingTuple(Dispute dispute) { + public Tuple2 getNodeAddressPubKeyRingTuple(Dispute dispute) { PubKeyRing receiverPubKeyRing = null; NodeAddress peerNodeAddress = null; if (isTrader(dispute)) { diff --git a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java index d2324e763da..54492035821 100644 --- a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java +++ b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java @@ -25,22 +25,21 @@ import bisq.desktop.components.TextFieldWithIcon; import bisq.desktop.main.disputes.trader.TraderDisputeView; import bisq.desktop.main.overlays.popups.Popup; -import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; import bisq.desktop.util.GUIUtil; import bisq.core.arbitration.Attachment; -import bisq.core.arbitration.Dispute; -import bisq.core.arbitration.DisputeManager; import bisq.core.arbitration.messages.DisputeCommunicationMessage; import bisq.core.locale.Res; import bisq.core.util.BSFormatter; +import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; +import bisq.network.p2p.SendMailboxMessageListener; import bisq.network.p2p.network.Connection; import bisq.common.Timer; import bisq.common.UserThread; -import bisq.common.app.Version; +import bisq.common.crypto.PubKeyRing; import bisq.common.util.Utilities; import com.google.common.io.ByteStreams; @@ -100,8 +99,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import lombok.Setter; - public class Chat extends AnchorPane { public static final Logger log = LoggerFactory.getLogger(TextFieldWithIcon.class); @@ -114,13 +111,12 @@ public class Chat extends AnchorPane { private TableGroupHeadline tableGroupHeadline; private VBox messagesInputBox; - // TODO Dispute stuff - need to go and generic functionality added - private final DisputeSummaryWindow disputeSummaryWindow; + // TODO Can be removed when close ticket button is made generic private TraderDisputeView traderDisputeView; - private final DisputeManager disputeManager; // Communication stuff, to be renamed to something more generic private final P2PService p2PService; + private ChatSession chatSession; private DisputeCommunicationMessage disputeCommunicationMessage; private ObservableList disputeCommunicationMessages; private ListChangeListener disputeDirectMessageListListener; @@ -129,21 +125,14 @@ public class Chat extends AnchorPane { private ChangeListener storedInMailboxPropertyListener, arrivedPropertyListener; private ChangeListener sendMessageErrorPropertyListener; - @Setter - private boolean isTrader; - protected final BSFormatter formatter; private EventHandler keyEventEventHandler; public Chat(TraderDisputeView traderDisputeView, - DisputeSummaryWindow disputeSummaryWindow, - DisputeManager disputeManager, P2PService p2PService, BSFormatter formatter ) { this.traderDisputeView = traderDisputeView; - this.disputeSummaryWindow = disputeSummaryWindow; - this.disputeManager = disputeManager; this.p2PService = p2PService; this.formatter = formatter; } @@ -153,26 +142,22 @@ public void initialize() { keyEventEventHandler = event -> { if (Utilities.isAltOrCtrlPressed(KeyCode.ENTER, event)) { - if (traderDisputeView.getSelectedDispute() != null && - messagesInputBox.isVisible() && inputTextArea.isFocused()) + if (chatSession.chatIsOpen() && inputTextArea.isFocused()) onTrySendMessage(); } }; } public void activate() { - if (getScene() != null) - getScene().addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } public void deactivate() { - if (getScene() != null) - getScene().removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } - public void display() { - if (traderDisputeView.getSelectedDispute() == null) - return; + public void display(ChatSession chatSession) { + this.chatSession = chatSession; this.getChildren().clear(); tableGroupHeadline = new TableGroupHeadline(); @@ -183,7 +168,7 @@ public void display() { AnchorPane.setBottomAnchor(tableGroupHeadline, 0d); AnchorPane.setLeftAnchor(tableGroupHeadline, 0d); - disputeCommunicationMessages = traderDisputeView.getSelectedDispute().getDisputeCommunicationMessages(); + disputeCommunicationMessages = chatSession.getDisputeCommunicationMessages(); SortedList sortedList = new SortedList<>(disputeCommunicationMessages); sortedList.setComparator(Comparator.comparing(o -> new Date(o.getDate()))); messageListView = new ListView<>(sortedList); @@ -199,7 +184,7 @@ public void display() { inputTextArea = new BisqTextArea(); inputTextArea.setPrefHeight(70); inputTextArea.setWrapText(true); - if (isTrader) + if (chatSession.isTrader()) inputTextArea.setPromptText(Res.get("support.input.prompt")); sendButton = new AutoTooltipButton(Res.get("support.send")); @@ -217,14 +202,16 @@ public void display() { sendMsgBusyAnimation = new BusyAnimation(false); - if (!traderDisputeView.getSelectedDispute().isClosed()) { + if (chatSession.chatIsOpen()) { HBox buttonBox = new HBox(); buttonBox.setSpacing(10); buttonBox.getChildren().addAll(sendButton, uploadButton, sendMsgBusyAnimation, sendMsgInfoLabel); - if (!isTrader) { + // TODO handle extra button separately + if (!chatSession.isTrader()) { Button closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); - closeDisputeButton.setOnAction(e -> onCloseDispute(traderDisputeView.getSelectedDispute())); + closeDisputeButton.setOnAction(e -> traderDisputeView.onCloseDispute( + traderDisputeView.getSelectedDispute())); closeDisputeButton.setDefaultButton(true); Pane spacer = new Pane(); HBox.setHgrow(spacer, Priority.ALWAYS); @@ -243,12 +230,16 @@ public void display() { AnchorPane.setBottomAnchor(messageListView, 120d); this.getChildren().addAll(tableGroupHeadline, messageListView, messagesInputBox); - } else { + } else + + { AnchorPane.setBottomAnchor(messageListView, 0d); this.getChildren().addAll(tableGroupHeadline, messageListView); } - messageListView.setCellFactory(new Callback<>() { + messageListView.setCellFactory(new Callback<>() + + { @Override public ListCell call(ListView list) { return new ListCell<>() { @@ -309,7 +300,7 @@ public void updateItem(final DisputeCommunicationMessage message, boolean empty) AnchorPane.setBottomAnchor(attachmentsBox, bottomBorder + 10); boolean senderIsTrader = message.isSenderIsTrader(); - boolean isMyMsg = isTrader == senderIsTrader; + boolean isMyMsg = chatSession.isTrader() == senderIsTrader; arrow.setVisible(!message.isSystemMessage()); arrow.setManaged(!message.isSystemMessage()); @@ -332,7 +323,7 @@ public void updateItem(final DisputeCommunicationMessage message, boolean empty) bg.setId("message-bubble-blue"); messageLabel.getStyleClass().add("my-message"); copyIcon.getStyleClass().add("my-message"); - if (isTrader) + if (chatSession.isTrader()) arrow.setId("bubble_arrow_blue_left"); else arrow.setId("bubble_arrow_blue_right"); @@ -353,7 +344,7 @@ public void updateItem(final DisputeCommunicationMessage message, boolean empty) bg.setId("message-bubble-grey"); messageLabel.getStyleClass().add("message"); copyIcon.getStyleClass().add("message"); - if (isTrader) + if (chatSession.isTrader()) arrow.setId("bubble_arrow_grey_right"); else arrow.setId("bubble_arrow_grey_left"); @@ -500,6 +491,7 @@ private void updateMsgState(DisputeCommunicationMessage message) { }); scrollToBottom(); + } /////////////////////////////////////////////////////////////////////////////////////////// @@ -511,7 +503,7 @@ private void onTrySendMessage() { String text = inputTextArea.getText(); if (!text.isEmpty()) { if (text.length() < 5_000) { - onSendMessage(text, traderDisputeView.getSelectedDispute()); + onSendMessage(text); } else { new Popup<>().information(Res.get("popup.warning.messageTooLong")).show(); } @@ -560,18 +552,6 @@ private void onRequestUpload() { } } - private void onCloseDispute(Dispute dispute) { - long protocolVersion = dispute.getContract().getOfferPayload().getProtocolVersion(); - if (protocolVersion == Version.TRADE_PROTOCOL_VERSION) { - disputeSummaryWindow.onFinalizeDispute(() -> this.getChildren().remove(messagesInputBox)) - .show(dispute); - } else { - new Popup<>() - .warning(Res.get("support.wrongVersion", protocolVersion)) - .show(); - } - } - private void onOpenAttachment(Attachment attachment) { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle(Res.get("support.save")); @@ -589,14 +569,14 @@ private void onOpenAttachment(Attachment attachment) { } } - private void onSendMessage(String inputText, Dispute dispute) { + private void onSendMessage(String inputText) { if (disputeCommunicationMessage != null) { disputeCommunicationMessage.arrivedProperty().removeListener(arrivedPropertyListener); disputeCommunicationMessage.storedInMailboxProperty().removeListener(storedInMailboxPropertyListener); disputeCommunicationMessage.sendMessageErrorProperty().removeListener(sendMessageErrorPropertyListener); } - disputeCommunicationMessage = disputeManager.sendDisputeDirectMessage(dispute, inputText, new ArrayList<>(tempAttachments)); + disputeCommunicationMessage = sendDisputeDirectMessage(inputText, new ArrayList<>(tempAttachments)); tempAttachments.clear(); scrollToBottom(); @@ -639,6 +619,60 @@ private void onSendMessage(String inputText, Dispute dispute) { } } + // traders send msg to the arbitrator or arbitrator to 1 trader (trader to trader is not allowed) + private DisputeCommunicationMessage sendDisputeDirectMessage(String text, ArrayList attachments) { + DisputeCommunicationMessage message = new DisputeCommunicationMessage( + chatSession.getTradeId(), + chatSession.getPubKeyRing().hashCode(), + chatSession.isTrader(), + text, + p2PService.getAddress() + ); + + message.addAllAttachments(attachments); + NodeAddress peersNodeAddress = chatSession.getPeerNodeAddress(); + PubKeyRing receiverPubKeyRing = chatSession.getPeerPubKeyRing(); + + chatSession.addDisputeCommunicationMessage(message); + + if (receiverPubKeyRing != null) { + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + + p2PService.sendEncryptedMailboxMessage(peersNodeAddress, + receiverPubKeyRing, + message, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + message.setArrived(true); + chatSession.persist(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + message.setStoredInMailbox(true); + chatSession.persist(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + message.setSendMessageError(errorMessage); + chatSession.persist(); + } + } + ); + } + + return message; + } + /////////////////////////////////////////////////////////////////////////////////////////// // Helpers /////////////////////////////////////////////////////////////////////////////////////////// @@ -665,6 +699,9 @@ public void setInputBoxVisible(boolean visible) { AnchorPane.setBottomAnchor(messageListView, visible ? 120d : 0d); } + public void removeInputBox() { + this.getChildren().remove(messagesInputBox); + } /////////////////////////////////////////////////////////////////////////////////////////// // Bindings /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java b/desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java new file mode 100644 index 00000000000..742247f5e30 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java @@ -0,0 +1,46 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.Chat; + +import bisq.core.arbitration.messages.DisputeCommunicationMessage; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.PubKeyRing; + +import javafx.collections.ObservableList; + +public interface ChatSession { + boolean isTrader(); + + String getTradeId(); + + PubKeyRing getPubKeyRing(); + + void addDisputeCommunicationMessage(DisputeCommunicationMessage message); + + void persist(); + + ObservableList getDisputeCommunicationMessages(); + + boolean chatIsOpen(); + + NodeAddress getPeerNodeAddress(); + + PubKeyRing getPeerPubKeyRing(); +} diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java b/desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java new file mode 100644 index 00000000000..257493ac9dc --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java @@ -0,0 +1,87 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.disputes; + +import bisq.desktop.main.Chat.ChatSession; + +import bisq.core.arbitration.Dispute; +import bisq.core.arbitration.DisputeManager; +import bisq.core.arbitration.messages.DisputeCommunicationMessage; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.PubKeyRing; + +import javafx.collections.ObservableList; + +public class DisputeChatSession implements ChatSession { + private Dispute dispute; + private DisputeManager disputeManager; + + public DisputeChatSession(Dispute dispute, DisputeManager disputeManager) { + this.dispute = dispute; + this.disputeManager = disputeManager; + } + + @Override + public boolean isTrader() { + return disputeManager.isTrader(dispute); + } + + @Override + public String getTradeId() { + return dispute.getTradeId(); + } + + @Override + public PubKeyRing getPubKeyRing() { + return dispute.getTraderPubKeyRing(); + } + + @Override + public void addDisputeCommunicationMessage(DisputeCommunicationMessage message) { + if (isTrader() || (!isTrader() && !message.isSystemMessage())) + dispute.addDisputeCommunicationMessage(message); + } + + @Override + public void persist() { + disputeManager.getDisputes().persist(); + } + + @Override + public ObservableList getDisputeCommunicationMessages() { + return dispute.getDisputeCommunicationMessages(); + } + + @Override + public boolean chatIsOpen() { + return !dispute.isClosed(); + } + + @Override + public NodeAddress getPeerNodeAddress() { + return disputeManager.getNodeAddressPubKeyRingTuple(dispute).first; + } + + @Override + public PubKeyRing getPeerPubKeyRing() { + return disputeManager.getNodeAddressPubKeyRingTuple(dispute).second; + } + +} diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java b/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java index 96af973dacf..dd744f1de28 100644 --- a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java @@ -25,6 +25,7 @@ import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.components.InputTextField; import bisq.desktop.main.Chat.Chat; +import bisq.desktop.main.disputes.DisputeChatSession; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.ContractWindow; import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; @@ -47,6 +48,7 @@ import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; +import bisq.common.app.Version; import bisq.common.crypto.KeyRing; import bisq.common.crypto.PubKeyRing; import bisq.common.util.Utilities; @@ -227,9 +229,7 @@ public void initialize() { dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); - selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> { - disputeChat.setInputBoxVisible(!newValue); - }; + selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> disputeChat.setInputBoxVisible(!newValue); keyEventEventHandler = event -> { if (Utilities.isAltOrCtrlPressed(KeyCode.L, event)) { @@ -317,15 +317,12 @@ public void initialize() { } }; - disputeChat = new Chat(this, disputeSummaryWindow, disputeManager, p2PService, formatter); + disputeChat = new Chat(this, p2PService, formatter); disputeChat.initialize(); } @Override protected void activate() { - if (disputeChat != null) - disputeChat.activate(); - filterTextField.textProperty().addListener(filterTextFieldListener); disputeManager.cleanupDisputes(); @@ -343,8 +340,10 @@ protected void activate() { if (selectedItem != null) tableView.getSelectionModel().select(selectedItem); - if (disputeChat != null) + if (disputeChat != null) { + disputeChat.activate(); disputeChat.scrollToBottom(); + } scene = root.getScene(); if (scene != null) @@ -454,8 +453,7 @@ private void onSelectDispute(Dispute dispute) { } else if (selectedDispute != dispute) { this.selectedDispute = dispute; if (disputeChat != null) { - disputeChat.setTrader(disputeManager.isTrader(selectedDispute)); - disputeChat.display(); + disputeChat.display(new DisputeChatSession(dispute, disputeManager)); } if (root.getChildren().size() > 2) @@ -466,6 +464,17 @@ private void onSelectDispute(Dispute dispute) { addListenersOnSelectDispute(); } + public void onCloseDispute(Dispute dispute) { + long protocolVersion = dispute.getContract().getOfferPayload().getProtocolVersion(); + if (protocolVersion == Version.TRADE_PROTOCOL_VERSION) { + disputeSummaryWindow.onFinalizeDispute(() -> disputeChat.removeInputBox()) + .show(dispute); + } else { + new Popup<>() + .warning(Res.get("support.wrongVersion", protocolVersion)) + .show(); + } + } /////////////////////////////////////////////////////////////////////////////////////////// // Table @@ -674,7 +683,7 @@ public void updateItem(final Dispute item, boolean empty) { } - protected String getBuyerOnionAddressColumnLabel(Dispute item) { + private String getBuyerOnionAddressColumnLabel(Dispute item) { Contract contract = item.getContract(); if (contract != null) { NodeAddress buyerNodeAddress = contract.getBuyerNodeAddress(); @@ -691,7 +700,7 @@ protected String getBuyerOnionAddressColumnLabel(Dispute item) { } } - protected String getSellerOnionAddressColumnLabel(Dispute item) { + private String getSellerOnionAddressColumnLabel(Dispute item) { Contract contract = item.getContract(); if (contract != null) { NodeAddress sellerNodeAddress = contract.getSellerNodeAddress(); From 1a25678080ffc7700ad9b1e76f0ba9095f17d9e9 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Sat, 25 May 2019 16:39:18 +0200 Subject: [PATCH 04/33] Move close ticket button creation to arbitration Add extra button for generically added button in chat --- .../java/bisq/desktop/main/Chat/Chat.java | 19 ++++--------------- .../bisq/desktop/main/Chat/ChatSession.java | 4 ++++ .../main/disputes/DisputeChatSession.java | 13 ++++++++++++- .../disputes/trader/TraderDisputeView.java | 11 ++++++++--- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java index 54492035821..0652afb162e 100644 --- a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java +++ b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java @@ -23,7 +23,6 @@ import bisq.desktop.components.BusyAnimation; import bisq.desktop.components.TableGroupHeadline; import bisq.desktop.components.TextFieldWithIcon; -import bisq.desktop.main.disputes.trader.TraderDisputeView; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.GUIUtil; @@ -111,9 +110,6 @@ public class Chat extends AnchorPane { private TableGroupHeadline tableGroupHeadline; private VBox messagesInputBox; - // TODO Can be removed when close ticket button is made generic - private TraderDisputeView traderDisputeView; - // Communication stuff, to be renamed to something more generic private final P2PService p2PService; private ChatSession chatSession; @@ -128,11 +124,8 @@ public class Chat extends AnchorPane { protected final BSFormatter formatter; private EventHandler keyEventEventHandler; - public Chat(TraderDisputeView traderDisputeView, - P2PService p2PService, - BSFormatter formatter + public Chat(P2PService p2PService, BSFormatter formatter ) { - this.traderDisputeView = traderDisputeView; this.p2PService = p2PService; this.formatter = formatter; } @@ -207,15 +200,11 @@ public void display(ChatSession chatSession) { buttonBox.setSpacing(10); buttonBox.getChildren().addAll(sendButton, uploadButton, sendMsgBusyAnimation, sendMsgInfoLabel); - // TODO handle extra button separately - if (!chatSession.isTrader()) { - Button closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); - closeDisputeButton.setOnAction(e -> traderDisputeView.onCloseDispute( - traderDisputeView.getSelectedDispute())); - closeDisputeButton.setDefaultButton(true); + if (chatSession.extraButton() != null) { + chatSession.extraButton().setDefaultButton(true); Pane spacer = new Pane(); HBox.setHgrow(spacer, Priority.ALWAYS); - buttonBox.getChildren().addAll(spacer, closeDisputeButton); + buttonBox.getChildren().addAll(spacer, chatSession.extraButton()); } messagesInputBox = new VBox(); diff --git a/desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java b/desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java index 742247f5e30..dc95c399f92 100644 --- a/desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java +++ b/desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java @@ -23,6 +23,8 @@ import bisq.common.crypto.PubKeyRing; +import javafx.scene.control.Button; + import javafx.collections.ObservableList; public interface ChatSession { @@ -43,4 +45,6 @@ public interface ChatSession { NodeAddress getPeerNodeAddress(); PubKeyRing getPeerPubKeyRing(); + + Button extraButton(); } diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java b/desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java index 257493ac9dc..d7abd935250 100644 --- a/desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java +++ b/desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java @@ -27,15 +27,21 @@ import bisq.common.crypto.PubKeyRing; +import javafx.scene.control.Button; + import javafx.collections.ObservableList; +import javax.annotation.Nullable; + public class DisputeChatSession implements ChatSession { private Dispute dispute; private DisputeManager disputeManager; + private Button extraButton; - public DisputeChatSession(Dispute dispute, DisputeManager disputeManager) { + public DisputeChatSession(Dispute dispute, DisputeManager disputeManager, @Nullable Button extraButton) { this.dispute = dispute; this.disputeManager = disputeManager; + this.extraButton = extraButton; } @Override @@ -74,6 +80,11 @@ public boolean chatIsOpen() { return !dispute.isClosed(); } + @Override + public Button extraButton() { + return extraButton; + } + @Override public NodeAddress getPeerNodeAddress() { return disputeManager.getNodeAddressPubKeyRingTuple(dispute).first; diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java b/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java index dd744f1de28..e4bbc8f5e82 100644 --- a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java @@ -317,7 +317,7 @@ public void initialize() { } }; - disputeChat = new Chat(this, p2PService, formatter); + disputeChat = new Chat(p2PService, formatter); disputeChat.initialize(); } @@ -453,7 +453,12 @@ private void onSelectDispute(Dispute dispute) { } else if (selectedDispute != dispute) { this.selectedDispute = dispute; if (disputeChat != null) { - disputeChat.display(new DisputeChatSession(dispute, disputeManager)); + Button closeDisputeButton = null; + if (!dispute.isClosed() && !disputeManager.isTrader(dispute)) { + closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); + closeDisputeButton.setOnAction(e -> onCloseDispute(getSelectedDispute())); + } + disputeChat.display(new DisputeChatSession(dispute, disputeManager, closeDisputeButton)); } if (root.getChildren().size() > 2) @@ -464,7 +469,7 @@ private void onSelectDispute(Dispute dispute) { addListenersOnSelectDispute(); } - public void onCloseDispute(Dispute dispute) { + private void onCloseDispute(Dispute dispute) { long protocolVersion = dispute.getContract().getOfferPayload().getProtocolVersion(); if (protocolVersion == Version.TRADE_PROTOCOL_VERSION) { disputeSummaryWindow.onFinalizeDispute(() -> disputeChat.removeInputBox()) From 2d77f32598de9451c38b2fa51bd2351ac755a949 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Sat, 25 May 2019 18:15:46 +0200 Subject: [PATCH 05/33] Handle Chat message subscription inside Chat --- .../main/java/bisq/desktop/main/Chat/Chat.java | 8 +++++--- .../bisq/desktop/main/Chat/ChatSession.java | 6 +++++- .../main/disputes/DisputeChatSession.java | 17 +++++++++++++++-- .../main/disputes/trader/TraderDisputeView.java | 7 ++----- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java index 0652afb162e..f9563be61ad 100644 --- a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java +++ b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java @@ -147,9 +147,11 @@ public void activate() { public void deactivate() { removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + removeListenersOnSessionChange(); } public void display(ChatSession chatSession) { + removeListenersOnSessionChange(); this.chatSession = chatSession; this.getChildren().clear(); @@ -479,8 +481,8 @@ private void updateMsgState(DisputeCommunicationMessage message) { } }); + addListenersOnSessionChange(chatSession.widthProperty()); scrollToBottom(); - } /////////////////////////////////////////////////////////////////////////////////////////// @@ -695,7 +697,7 @@ public void removeInputBox() { // Bindings /////////////////////////////////////////////////////////////////////////////////////////// - public void addListenersOnSelectDispute(ReadOnlyDoubleProperty widthProperty) { + private void addListenersOnSessionChange(ReadOnlyDoubleProperty widthProperty) { if (tableGroupHeadline != null) { tableGroupHeadline.prefWidthProperty().bind(widthProperty); messageListView.prefWidthProperty().bind(widthProperty); @@ -705,7 +707,7 @@ public void addListenersOnSelectDispute(ReadOnlyDoubleProperty widthProperty) { } } - public void removeListenersOnSelectDispute() { + private void removeListenersOnSessionChange() { if (disputeCommunicationMessages != null && disputeDirectMessageListListener != null) disputeCommunicationMessages.removeListener(disputeDirectMessageListListener); diff --git a/desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java b/desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java index dc95c399f92..c3ee216e1ad 100644 --- a/desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java +++ b/desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java @@ -25,6 +25,8 @@ import javafx.scene.control.Button; +import javafx.beans.property.ReadOnlyDoubleProperty; + import javafx.collections.ObservableList; public interface ChatSession { @@ -42,9 +44,11 @@ public interface ChatSession { boolean chatIsOpen(); + Button extraButton(); + NodeAddress getPeerNodeAddress(); PubKeyRing getPeerPubKeyRing(); - Button extraButton(); + ReadOnlyDoubleProperty widthProperty(); } diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java b/desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java index d7abd935250..f6829d22412 100644 --- a/desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java +++ b/desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java @@ -29,6 +29,8 @@ import javafx.scene.control.Button; +import javafx.beans.property.ReadOnlyDoubleProperty; + import javafx.collections.ObservableList; import javax.annotation.Nullable; @@ -37,11 +39,18 @@ public class DisputeChatSession implements ChatSession { private Dispute dispute; private DisputeManager disputeManager; private Button extraButton; - - public DisputeChatSession(Dispute dispute, DisputeManager disputeManager, @Nullable Button extraButton) { + private ReadOnlyDoubleProperty widthProperty; + + public DisputeChatSession( + Dispute dispute, + DisputeManager disputeManager, + @Nullable Button extraButton, + ReadOnlyDoubleProperty widthProperty + ) { this.dispute = dispute; this.disputeManager = disputeManager; this.extraButton = extraButton; + this.widthProperty = widthProperty; } @Override @@ -95,4 +104,8 @@ public PubKeyRing getPeerPubKeyRing() { return disputeManager.getNodeAddressPubKeyRingTuple(dispute).second; } + @Override + public ReadOnlyDoubleProperty widthProperty() { + return widthProperty; + } } diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java b/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java index e4bbc8f5e82..e4e92598e30 100644 --- a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java @@ -428,8 +428,6 @@ private void onOpenContract(Dispute dispute) { } private void removeListenersOnSelectDispute() { - if (disputeChat != null) - disputeChat.removeListenersOnSelectDispute(); if (selectedDispute != null) { if (selectedDisputeClosedPropertyListener != null) selectedDispute.isClosedProperty().removeListener(selectedDisputeClosedPropertyListener); @@ -439,8 +437,6 @@ private void removeListenersOnSelectDispute() { private void addListenersOnSelectDispute() { if (selectedDispute != null) selectedDispute.isClosedProperty().addListener(selectedDisputeClosedPropertyListener); - if (disputeChat != null) - disputeChat.addListenersOnSelectDispute(root.widthProperty()); } private void onSelectDispute(Dispute dispute) { @@ -458,7 +454,8 @@ private void onSelectDispute(Dispute dispute) { closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); closeDisputeButton.setOnAction(e -> onCloseDispute(getSelectedDispute())); } - disputeChat.display(new DisputeChatSession(dispute, disputeManager, closeDisputeButton)); + disputeChat.display(new DisputeChatSession(dispute, disputeManager, closeDisputeButton, + root.widthProperty())); } if (root.getChildren().size() > 2) From 1b1dce1012beba4a3811d180e689606b4026ee29 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Thu, 30 May 2019 17:35:07 +0200 Subject: [PATCH 06/33] Add ChatManager Move session classes to core. Break out DisputeCommunicationMessage handling from DisputeManager and put in ChatMananger prepare for other uses of ChatManager. Renaming of DisputeCommunicationMessage would be nice but it's representing the protobuf messages so the name has to stay. --- .../core/arbitration/DisputeChatSession.java | 173 ++++++++++++++ .../bisq/core/arbitration/DisputeManager.java | 202 ++--------------- .../main/java/bisq/core/chat/ChatManager.java | 213 ++++++++++++++++++ .../main/java/bisq/core/chat/ChatSession.java | 65 ++++++ .../java/bisq/desktop/main/Chat/Chat.java | 30 ++- .../bisq/desktop/main/Chat/ChatSession.java | 54 ----- .../main/disputes/DisputeChatSession.java | 111 --------- .../disputes/trader/TraderDisputeView.java | 11 +- 8 files changed, 503 insertions(+), 356 deletions(-) create mode 100644 core/src/main/java/bisq/core/arbitration/DisputeChatSession.java create mode 100644 core/src/main/java/bisq/core/chat/ChatManager.java create mode 100644 core/src/main/java/bisq/core/chat/ChatSession.java delete mode 100644 desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java delete mode 100644 desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java diff --git a/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java b/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java new file mode 100644 index 00000000000..5e1407b1fff --- /dev/null +++ b/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java @@ -0,0 +1,173 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.arbitration; + +import bisq.core.chat.ChatManager; +import bisq.core.chat.ChatSession; + +import bisq.core.arbitration.Dispute; +import bisq.core.arbitration.DisputeManager; +import bisq.core.arbitration.messages.DisputeCommunicationMessage; +import bisq.core.arbitration.messages.DisputeMessage; +import bisq.core.arbitration.messages.DisputeResultMessage; +import bisq.core.arbitration.messages.OpenNewDisputeMessage; +import bisq.core.arbitration.messages.PeerOpenedDisputeMessage; +import bisq.core.arbitration.messages.PeerPublishedDisputePayoutTxMessage; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.PubKeyRing; + +import javafx.collections.ObservableList; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +public class DisputeChatSession extends ChatSession { + private static final Logger log = LoggerFactory.getLogger(DisputeManager.class); + + private Dispute dispute; + private DisputeManager disputeManager; + private ChatManager chatManager; + + public DisputeChatSession( + @Nullable Dispute dispute, + DisputeManager disputeManager, + ChatManager chatManager + ) { + this.dispute = dispute; + this.disputeManager = disputeManager; + this.chatManager = chatManager; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Dependent on selected dispute + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public boolean isTrader() { + return disputeManager.isTrader(dispute); + } + + @Override + public String getTradeId() { + return dispute.getTradeId(); + } + + @Override + public PubKeyRing getPubKeyRing() { + return dispute.getTraderPubKeyRing(); + } + + @Override + public void addDisputeCommunicationMessage(DisputeCommunicationMessage message) { + if (isTrader() || (!isTrader() && !message.isSystemMessage())) + dispute.addDisputeCommunicationMessage(message); + } + + @Override + public void persist() { + disputeManager.getDisputes().persist(); + } + + @Override + public ObservableList getDisputeCommunicationMessages() { + return dispute.getDisputeCommunicationMessages(); + } + + @Override + public boolean chatIsOpen() { + return !dispute.isClosed(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Not dependent on selected dispute + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public NodeAddress getPeerNodeAddress(DisputeCommunicationMessage message) { + Optional disputeOptional = disputeManager.findDispute(message.getTradeId(), message.getTraderId()); + if (!disputeOptional.isPresent()) { + log.warn("Could not find dispute for tradeId = {} traderId = {}", + message.getTradeId(), message.getTraderId()); + return null; + } + return disputeManager.getNodeAddressPubKeyRingTuple(disputeOptional.get()).first; + } + + @Override + public PubKeyRing getPeerPubKeyRing(DisputeCommunicationMessage message) { + Optional disputeOptional = disputeManager.findDispute(message.getTradeId(), message.getTraderId()); + if (!disputeOptional.isPresent()) { + log.warn("Could not find dispute for tradeId = {} traderId = {}", + message.getTradeId(), message.getTraderId()); + return null; + } + + return disputeManager.getNodeAddressPubKeyRingTuple(disputeOptional.get()).second; + } + + @Override + public void dispatchMessage(DisputeMessage message) { + log.info("Received {} with tradeId {} and uid {}", + message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); + + if (message instanceof OpenNewDisputeMessage) + disputeManager.onOpenNewDisputeMessage((OpenNewDisputeMessage) message); + else if (message instanceof PeerOpenedDisputeMessage) + disputeManager.onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message); + else if (message instanceof DisputeCommunicationMessage) + chatManager.onDisputeDirectMessage((DisputeCommunicationMessage) message); + else if (message instanceof DisputeResultMessage) + disputeManager.onDisputeResultMessage((DisputeResultMessage) message); + else if (message instanceof PeerPublishedDisputePayoutTxMessage) + disputeManager.onDisputedPayoutTxMessage((PeerPublishedDisputePayoutTxMessage) message); + else + log.warn("Unsupported message at dispatchMessage.\nmessage=" + message); + } + + @Override + public List getChatMessages() { + return disputeManager.getDisputes().getList().stream() + .flatMap(dispute -> dispute.getDisputeCommunicationMessages().stream()) + .collect(Collectors.toList()); + } + + public boolean channelOpen(DisputeCommunicationMessage message) { + return disputeManager.findDispute(message.getTradeId(), message.getTraderId()).isPresent(); + } + + public void storeDisputeCommunicationMessage(DisputeCommunicationMessage message) { + Optional disputeOptional = disputeManager.findDispute(message.getTradeId(), message.getTraderId()); + + if (disputeOptional.isPresent()) { + if (!disputeOptional.get().getDisputeCommunicationMessages().stream() + .anyMatch(m -> m.getUid().equals(message.getUid()))) + disputeOptional.get().addDisputeCommunicationMessage(message); + else + log.warn("We got a disputeCommunicationMessage what we have already stored. UId = {} TradeId = {}", + message.getUid(), message.getTradeId()); + } + } +} diff --git a/core/src/main/java/bisq/core/arbitration/DisputeManager.java b/core/src/main/java/bisq/core/arbitration/DisputeManager.java index 95091a2a803..737458a6f9e 100644 --- a/core/src/main/java/bisq/core/arbitration/DisputeManager.java +++ b/core/src/main/java/bisq/core/arbitration/DisputeManager.java @@ -30,6 +30,7 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.chat.ChatManager; import bisq.core.locale.Res; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; @@ -111,8 +112,8 @@ public class DisputeManager implements PersistedDataHost { @Getter private DisputeList disputes; private final String disputeInfo; - private final CopyOnWriteArraySet decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>(); - private final CopyOnWriteArraySet decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>(); + // private final CopyOnWriteArraySet decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>(); +// private final CopyOnWriteArraySet decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>(); private final Map openDisputes; private final Map closedDisputes; private final Map delayMsgMap = new HashMap<>(); @@ -121,6 +122,8 @@ public class DisputeManager implements PersistedDataHost { @Getter private final IntegerProperty numOpenDisputes = new SimpleIntegerProperty(); + @Getter + private final ChatManager chatManager; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -146,22 +149,15 @@ public DisputeManager(P2PService p2PService, this.openOfferManager = openOfferManager; this.keyRing = keyRing; + chatManager = new ChatManager(p2PService, walletsSetup); + chatManager.setChatSession(new DisputeChatSession(null, this, chatManager)); + disputeStorage = new Storage<>(storageDir, persistenceProtoResolver); openDisputes = new HashMap<>(); closedDisputes = new HashMap<>(); disputeInfo = Res.get("support.initialInfo"); - - // We get first the message handler called then the onBootstrapped - p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> { - decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey); - tryApplyMessages(); - }); - p2PService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> { - decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey); - tryApplyMessages(); - }); } @@ -180,21 +176,21 @@ public void onAllServicesInitialized() { p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onUpdatedDataReceived() { - tryApplyMessages(); + chatManager.tryApplyMessages(); } }); walletsSetup.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> { if (walletsSetup.isDownloadComplete()) - tryApplyMessages(); + chatManager.tryApplyMessages(); }); walletsSetup.numPeersProperty().addListener((observable, oldValue, newValue) -> { if (walletsSetup.hasSufficientPeersForBroadcast()) - tryApplyMessages(); + chatManager.tryApplyMessages(); }); - tryApplyMessages(); + chatManager.tryApplyMessages(); cleanupDisputes(); @@ -254,85 +250,6 @@ public void cleanupDisputes() { }); } - private void tryApplyMessages() { - if (isReadyForTxBroadcast()) - applyMessages(); - } - - private boolean isReadyForTxBroadcast() { - return p2PService.isBootstrapped() && - walletsSetup.isDownloadComplete() && - walletsSetup.hasSufficientPeersForBroadcast(); - } - - private void applyMessages() { - decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> { - NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); - if (networkEnvelope instanceof DisputeMessage) { - dispatchMessage((DisputeMessage) networkEnvelope); - } else if (networkEnvelope instanceof AckMessage) { - processAckMessage((AckMessage) networkEnvelope, null); - } - }); - decryptedDirectMessageWithPubKeys.clear(); - - decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> { - NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); - log.debug("decryptedMessageWithPubKey.message " + networkEnvelope); - if (networkEnvelope instanceof DisputeMessage) { - dispatchMessage((DisputeMessage) networkEnvelope); - p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey); - } else if (networkEnvelope instanceof AckMessage) { - processAckMessage((AckMessage) networkEnvelope, decryptedMessageWithPubKey); - } - }); - decryptedMailboxMessageWithPubKeys.clear(); - } - - private void processAckMessage(AckMessage ackMessage, @Nullable DecryptedMessageWithPubKey decryptedMessageWithPubKey) { - if (ackMessage.getSourceType() == AckMessageSourceType.DISPUTE_MESSAGE) { - if (ackMessage.isSuccess()) { - log.info("Received AckMessage for {} with tradeId {} and uid {}", - ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid()); - } else { - log.warn("Received AckMessage with error state for {} with tradeId {} and errorMessage={}", - ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage()); - } - - disputes.getList().stream() - .flatMap(dispute -> dispute.getDisputeCommunicationMessages().stream()) - .filter(msg -> msg.getUid().equals(ackMessage.getSourceUid())) - .forEach(msg -> { - if (ackMessage.isSuccess()) - msg.setAcknowledged(true); - else - msg.setAckError(ackMessage.getErrorMessage()); - }); - disputes.persist(); - - if (decryptedMessageWithPubKey != null) - p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey); - } - } - - private void dispatchMessage(DisputeMessage message) { - log.info("Received {} with tradeId {} and uid {}", - message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); - - if (message instanceof OpenNewDisputeMessage) - onOpenNewDisputeMessage((OpenNewDisputeMessage) message); - else if (message instanceof PeerOpenedDisputeMessage) - onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message); - else if (message instanceof DisputeCommunicationMessage) - onDisputeDirectMessage((DisputeCommunicationMessage) message); - else if (message instanceof DisputeResultMessage) - onDisputeResultMessage((DisputeResultMessage) message); - else if (message instanceof PeerPublishedDisputePayoutTxMessage) - onDisputedPayoutTxMessage((PeerPublishedDisputePayoutTxMessage) message); - else - log.warn("Unsupported message at dispatchMessage.\nmessage=" + message); - } - public void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen, ResultHandler resultHandler, FaultHandler faultHandler) { if (!disputes.contains(dispute)) { final Optional storedDisputeOptional = findDispute(dispute.getTradeId(), dispute.getTraderId()); @@ -641,53 +558,12 @@ public void onFault(String errorMessage) { ); } - private void sendAckMessage(DisputeMessage disputeMessage, PubKeyRing peersPubKeyRing, - boolean result, @Nullable String errorMessage) { - String tradeId = disputeMessage.getTradeId(); - String uid = disputeMessage.getUid(); - AckMessage ackMessage = new AckMessage(p2PService.getNetworkNode().getNodeAddress(), - AckMessageSourceType.DISPUTE_MESSAGE, - disputeMessage.getClass().getSimpleName(), - uid, - tradeId, - result, - errorMessage); - final NodeAddress peersNodeAddress = disputeMessage.getSenderNodeAddress(); - log.info("Send AckMessage for {} to peer {}. tradeId={}, uid={}", - ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); - p2PService.sendEncryptedMailboxMessage( - peersNodeAddress, - peersPubKeyRing, - ackMessage, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("AckMessage for {} arrived at peer {}. tradeId={}, uid={}", - ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); - } - - @Override - public void onStoredInMailbox() { - log.info("AckMessage for {} stored in mailbox for peer {}. tradeId={}, uid={}", - ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); - } - - @Override - public void onFault(String errorMessage) { - log.error("AckMessage for {} failed. Peer {}. tradeId={}, uid={}, errorMessage={}", - ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid, errorMessage); - } - } - ); - } - - /////////////////////////////////////////////////////////////////////////////////////////// // Incoming message /////////////////////////////////////////////////////////////////////////////////////////// // arbitrator receives that from trader who opens dispute - private void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessage) { + public void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessage) { String errorMessage; Dispute dispute = openNewDisputeMessage.getDispute(); Contract contractFromOpener = dispute.getContract(); @@ -718,12 +594,12 @@ private void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessage if (!messages.isEmpty()) { DisputeCommunicationMessage msg = messages.get(0); PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contractFromOpener.getBuyerPubKeyRing() : contractFromOpener.getSellerPubKeyRing(); - sendAckMessage(msg, sendersPubKeyRing, errorMessage == null, errorMessage); + chatManager.sendAckMessage(msg, sendersPubKeyRing, errorMessage == null, errorMessage); } } // not dispute requester receives that from arbitrator - private void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) { + public void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) { String errorMessage; Dispute dispute = peerOpenedDisputeMessage.getDispute(); if (!isArbitrator(dispute)) { @@ -753,47 +629,13 @@ private void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDispu ObservableList messages = peerOpenedDisputeMessage.getDispute().getDisputeCommunicationMessages(); if (!messages.isEmpty()) { DisputeCommunicationMessage msg = messages.get(0); - sendAckMessage(msg, dispute.getArbitratorPubKeyRing(), errorMessage == null, errorMessage); + chatManager.sendAckMessage(msg, dispute.getArbitratorPubKeyRing(), errorMessage == null, errorMessage); } - sendAckMessage(peerOpenedDisputeMessage, dispute.getArbitratorPubKeyRing(), errorMessage == null, errorMessage); + chatManager.sendAckMessage(peerOpenedDisputeMessage, dispute.getArbitratorPubKeyRing(), errorMessage == null, errorMessage); } - - // A trader can receive a msg from the arbitrator or the arbitrator from a trader. Trader to trader is not allowed. - private void onDisputeDirectMessage(DisputeCommunicationMessage disputeCommunicationMessage) { - final String tradeId = disputeCommunicationMessage.getTradeId(); - final String uid = disputeCommunicationMessage.getUid(); - Optional disputeOptional = findDispute(tradeId, disputeCommunicationMessage.getTraderId()); - if (!disputeOptional.isPresent()) { - log.debug("We got a disputeCommunicationMessage but we don't have a matching dispute. TradeId = " + tradeId); - if (!delayMsgMap.containsKey(uid)) { - Timer timer = UserThread.runAfter(() -> onDisputeDirectMessage(disputeCommunicationMessage), 1); - delayMsgMap.put(uid, timer); - } else { - String msg = "We got a disputeCommunicationMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId; - log.warn(msg); - } - return; - } - - cleanupRetryMap(uid); - Dispute dispute = disputeOptional.get(); - Tuple2 tuple = getNodeAddressPubKeyRingTuple(dispute); - PubKeyRing receiverPubKeyRing = tuple.second; - - if (!dispute.getDisputeCommunicationMessages().contains(disputeCommunicationMessage)) - dispute.addDisputeCommunicationMessage(disputeCommunicationMessage); - else - log.warn("We got a disputeCommunicationMessage what we have already stored. TradeId = " + tradeId); - - // We never get a errorMessage in that method (only if we cannot resolve the receiverPubKeyRing but then we - // cannot send it anyway) - if (receiverPubKeyRing != null) - sendAckMessage(disputeCommunicationMessage, receiverPubKeyRing, true, null); - } - // We get that message at both peers. The dispute object is in context of the trader - private void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { + public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { String errorMessage = null; boolean success = false; PubKeyRing arbitratorsPubKeyRing = null; @@ -965,13 +807,13 @@ public void onFailure(TxBroadcastException exception) { // We use the disputeCommunicationMessage as we only persist those not the disputeResultMessage. // If we would use the disputeResultMessage we could not lookup for the msg when we receive the AckMessage. DisputeCommunicationMessage disputeCommunicationMessage = disputeResultMessage.getDisputeResult().getDisputeCommunicationMessage(); - sendAckMessage(disputeCommunicationMessage, arbitratorsPubKeyRing, success, errorMessage); + chatManager.sendAckMessage(disputeCommunicationMessage, arbitratorsPubKeyRing, success, errorMessage); } } } // Losing trader or in case of 50/50 the seller gets the tx sent from the winner or buyer - private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) { + public void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) { final String uid = peerPublishedDisputePayoutTxMessage.getUid(); final String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId(); Optional disputeOptional = findOwnDispute(tradeId); @@ -1000,7 +842,7 @@ private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerP BtcWalletService.printTx("Disputed payoutTx received from peer", walletTx); // We can only send the ack msg if we have the peersPubKeyRing which requires the dispute - sendAckMessage(peerPublishedDisputePayoutTxMessage, peersPubKeyRing, true, null); + chatManager.sendAckMessage(peerPublishedDisputePayoutTxMessage, peersPubKeyRing, true, null); } @@ -1071,7 +913,7 @@ public Tuple2 getNodeAddressPubKeyRingTuple(Dispute dis return new Tuple2<>(peerNodeAddress, receiverPubKeyRing); } - private Optional findDispute(String tradeId, int traderId) { + public Optional findDispute(String tradeId, int traderId) { return disputes.stream().filter(e -> e.getTradeId().equals(tradeId) && e.getTraderId() == traderId).findAny(); } diff --git a/core/src/main/java/bisq/core/chat/ChatManager.java b/core/src/main/java/bisq/core/chat/ChatManager.java new file mode 100644 index 00000000000..2446d73d25f --- /dev/null +++ b/core/src/main/java/bisq/core/chat/ChatManager.java @@ -0,0 +1,213 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.chat; + +import bisq.core.arbitration.Dispute; +import bisq.core.arbitration.DisputeManager; +import bisq.core.arbitration.messages.DisputeCommunicationMessage; +import bisq.core.arbitration.messages.DisputeMessage; +import bisq.core.btc.setup.WalletsSetup; + +import bisq.network.p2p.AckMessage; +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.DecryptedMessageWithPubKey; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.SendMailboxMessageListener; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.network.NetworkEnvelope; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nullable; + +public class ChatManager { + private static final Logger log = LoggerFactory.getLogger(DisputeManager.class); + + private final P2PService p2PService; + private final WalletsSetup walletsSetup; + @Setter + @Getter + private ChatSession chatSession; + private final Map delayMsgMap = new HashMap<>(); + + private final CopyOnWriteArraySet decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>(); + private final CopyOnWriteArraySet decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>(); + + public ChatManager(P2PService p2PService, + WalletsSetup walletsSetup + ) { + this.p2PService = p2PService; + this.walletsSetup = walletsSetup; + + // We get first the message handler called then the onBootstrapped + p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> { + decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey); + tryApplyMessages(); + }); + p2PService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> { + decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey); + tryApplyMessages(); + }); + } + + public void tryApplyMessages() { + if (isReadyForTxBroadcast()) + applyMessages(); + } + + private boolean isReadyForTxBroadcast() { + return p2PService.isBootstrapped() && + walletsSetup.isDownloadComplete() && + walletsSetup.hasSufficientPeersForBroadcast(); + } + + private void applyMessages() { + decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> { + NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); + if (networkEnvelope instanceof DisputeMessage) { + chatSession.dispatchMessage((DisputeMessage) networkEnvelope); + } else if (networkEnvelope instanceof AckMessage) { + processAckMessage((AckMessage) networkEnvelope, null); + } + }); + decryptedDirectMessageWithPubKeys.clear(); + + decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> { + NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); + log.debug("decryptedMessageWithPubKey.message " + networkEnvelope); + if (networkEnvelope instanceof DisputeMessage) { + chatSession.dispatchMessage((DisputeMessage) networkEnvelope); + p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey); + } else if (networkEnvelope instanceof AckMessage) { + processAckMessage((AckMessage) networkEnvelope, decryptedMessageWithPubKey); + } + }); + decryptedMailboxMessageWithPubKeys.clear(); + } + + public void onDisputeDirectMessage(DisputeCommunicationMessage disputeCommunicationMessage) { + final String tradeId = disputeCommunicationMessage.getTradeId(); + final String uid = disputeCommunicationMessage.getUid(); + boolean channelOpen = chatSession.channelOpen(disputeCommunicationMessage); + if (!channelOpen) { + log.debug("We got a disputeCommunicationMessage but we don't have a matching chat. TradeId = " + tradeId); + if (!delayMsgMap.containsKey(uid)) { + Timer timer = UserThread.runAfter(() -> onDisputeDirectMessage(disputeCommunicationMessage), 1); + delayMsgMap.put(uid, timer); + } else { + String msg = "We got a disputeCommunicationMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId; + log.warn(msg); + } + return; + } + + cleanupRetryMap(uid); + PubKeyRing receiverPubKeyRing = chatSession.getPeerPubKeyRing(disputeCommunicationMessage); + + chatSession.storeDisputeCommunicationMessage(disputeCommunicationMessage); + + // We never get a errorMessage in that method (only if we cannot resolve the receiverPubKeyRing but then we + // cannot send it anyway) + if (receiverPubKeyRing != null) + sendAckMessage(disputeCommunicationMessage, receiverPubKeyRing, true, null); + } + + private void processAckMessage(AckMessage ackMessage, @Nullable DecryptedMessageWithPubKey decryptedMessageWithPubKey) { + if (ackMessage.getSourceType() == AckMessageSourceType.DISPUTE_MESSAGE) { + if (ackMessage.isSuccess()) { + log.info("Received AckMessage for {} with tradeId {} and uid {}", + ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid()); + } else { + log.warn("Received AckMessage with error state for {} with tradeId {} and errorMessage={}", + ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage()); + } + + chatSession.getChatMessages() + .forEach(msg -> { + if (ackMessage.isSuccess()) + msg.setAcknowledged(true); + else + msg.setAckError(ackMessage.getErrorMessage()); + }); + chatSession.persist(); + + if (decryptedMessageWithPubKey != null) + p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey); + } + } + + public void sendAckMessage(DisputeMessage disputeMessage, PubKeyRing peersPubKeyRing, + boolean result, @Nullable String errorMessage) { + String tradeId = disputeMessage.getTradeId(); + String uid = disputeMessage.getUid(); + AckMessage ackMessage = new AckMessage(p2PService.getNetworkNode().getNodeAddress(), + AckMessageSourceType.DISPUTE_MESSAGE, + disputeMessage.getClass().getSimpleName(), + uid, + tradeId, + result, + errorMessage); + final NodeAddress peersNodeAddress = disputeMessage.getSenderNodeAddress(); + log.info("Send AckMessage for {} to peer {}. tradeId={}, uid={}", + ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); + p2PService.sendEncryptedMailboxMessage( + peersNodeAddress, + peersPubKeyRing, + ackMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("AckMessage for {} arrived at peer {}. tradeId={}, uid={}", + ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); + } + + @Override + public void onStoredInMailbox() { + log.info("AckMessage for {} stored in mailbox for peer {}. tradeId={}, uid={}", + ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); + } + + @Override + public void onFault(String errorMessage) { + log.error("AckMessage for {} failed. Peer {}. tradeId={}, uid={}, errorMessage={}", + ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid, errorMessage); + } + } + ); + } + + private void cleanupRetryMap(String uid) { + if (delayMsgMap.containsKey(uid)) { + Timer timer = delayMsgMap.remove(uid); + if (timer != null) + timer.stop(); + } + } +} diff --git a/core/src/main/java/bisq/core/chat/ChatSession.java b/core/src/main/java/bisq/core/chat/ChatSession.java new file mode 100644 index 00000000000..4972f9536a0 --- /dev/null +++ b/core/src/main/java/bisq/core/chat/ChatSession.java @@ -0,0 +1,65 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.chat; + +import bisq.core.arbitration.messages.DisputeCommunicationMessage; +import bisq.core.arbitration.messages.DisputeMessage; + +import bisq.network.p2p.DecryptedMessageWithPubKey; +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.PubKeyRing; + +import javafx.scene.control.Button; + +import javafx.beans.property.ReadOnlyDoubleProperty; + +import javafx.collections.ObservableList; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArraySet; + +import lombok.Getter; + +public abstract class ChatSession { + abstract public boolean isTrader(); + + abstract public String getTradeId(); + + abstract public PubKeyRing getPubKeyRing(); + + abstract public void addDisputeCommunicationMessage(DisputeCommunicationMessage message); + + abstract public void persist(); + + abstract public ObservableList getDisputeCommunicationMessages(); + + abstract public List getChatMessages(); + + abstract public boolean chatIsOpen(); + + abstract public NodeAddress getPeerNodeAddress(DisputeCommunicationMessage message); + + abstract public PubKeyRing getPeerPubKeyRing(DisputeCommunicationMessage message); + + abstract public void dispatchMessage(DisputeMessage message); + + abstract public boolean channelOpen(DisputeCommunicationMessage message); + + abstract public void storeDisputeCommunicationMessage(DisputeCommunicationMessage message); +} diff --git a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java index f9563be61ad..c1cba432fdb 100644 --- a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java +++ b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java @@ -28,6 +28,7 @@ import bisq.core.arbitration.Attachment; import bisq.core.arbitration.messages.DisputeCommunicationMessage; +import bisq.core.chat.ChatSession; import bisq.core.locale.Res; import bisq.core.util.BSFormatter; @@ -98,6 +99,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import lombok.Getter; + +import javax.annotation.Nullable; + public class Chat extends AnchorPane { public static final Logger log = LoggerFactory.getLogger(TextFieldWithIcon.class); @@ -110,6 +115,12 @@ public class Chat extends AnchorPane { private TableGroupHeadline tableGroupHeadline; private VBox messagesInputBox; + // TODO set these on new session or link to session action + @Getter + Button extraButton; + @Getter + private ReadOnlyDoubleProperty widthProperty; + // Communication stuff, to be renamed to something more generic private final P2PService p2PService; private ChatSession chatSession; @@ -150,10 +161,13 @@ public void deactivate() { removeListenersOnSessionChange(); } - public void display(ChatSession chatSession) { + public void display(ChatSession chatSession, @Nullable Button extraButton, + ReadOnlyDoubleProperty widthProperty) { removeListenersOnSessionChange(); - this.chatSession = chatSession; this.getChildren().clear(); + this.chatSession = chatSession; + this.extraButton = extraButton; + this.widthProperty = widthProperty; tableGroupHeadline = new TableGroupHeadline(); tableGroupHeadline.setText(Res.get("support.messages")); @@ -202,11 +216,11 @@ public void display(ChatSession chatSession) { buttonBox.setSpacing(10); buttonBox.getChildren().addAll(sendButton, uploadButton, sendMsgBusyAnimation, sendMsgInfoLabel); - if (chatSession.extraButton() != null) { - chatSession.extraButton().setDefaultButton(true); + if (extraButton != null) { + extraButton.setDefaultButton(true); Pane spacer = new Pane(); HBox.setHgrow(spacer, Priority.ALWAYS); - buttonBox.getChildren().addAll(spacer, chatSession.extraButton()); + buttonBox.getChildren().addAll(spacer, extraButton); } messagesInputBox = new VBox(); @@ -481,7 +495,7 @@ private void updateMsgState(DisputeCommunicationMessage message) { } }); - addListenersOnSessionChange(chatSession.widthProperty()); + addListenersOnSessionChange(widthProperty); scrollToBottom(); } @@ -621,8 +635,8 @@ private DisputeCommunicationMessage sendDisputeDirectMessage(String text, ArrayL ); message.addAllAttachments(attachments); - NodeAddress peersNodeAddress = chatSession.getPeerNodeAddress(); - PubKeyRing receiverPubKeyRing = chatSession.getPeerPubKeyRing(); + NodeAddress peersNodeAddress = chatSession.getPeerNodeAddress(message); + PubKeyRing receiverPubKeyRing = chatSession.getPeerPubKeyRing(message); chatSession.addDisputeCommunicationMessage(message); diff --git a/desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java b/desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java deleted file mode 100644 index c3ee216e1ad..00000000000 --- a/desktop/src/main/java/bisq/desktop/main/Chat/ChatSession.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.desktop.main.Chat; - -import bisq.core.arbitration.messages.DisputeCommunicationMessage; - -import bisq.network.p2p.NodeAddress; - -import bisq.common.crypto.PubKeyRing; - -import javafx.scene.control.Button; - -import javafx.beans.property.ReadOnlyDoubleProperty; - -import javafx.collections.ObservableList; - -public interface ChatSession { - boolean isTrader(); - - String getTradeId(); - - PubKeyRing getPubKeyRing(); - - void addDisputeCommunicationMessage(DisputeCommunicationMessage message); - - void persist(); - - ObservableList getDisputeCommunicationMessages(); - - boolean chatIsOpen(); - - Button extraButton(); - - NodeAddress getPeerNodeAddress(); - - PubKeyRing getPeerPubKeyRing(); - - ReadOnlyDoubleProperty widthProperty(); -} diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java b/desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java deleted file mode 100644 index f6829d22412..00000000000 --- a/desktop/src/main/java/bisq/desktop/main/disputes/DisputeChatSession.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.desktop.main.disputes; - -import bisq.desktop.main.Chat.ChatSession; - -import bisq.core.arbitration.Dispute; -import bisq.core.arbitration.DisputeManager; -import bisq.core.arbitration.messages.DisputeCommunicationMessage; - -import bisq.network.p2p.NodeAddress; - -import bisq.common.crypto.PubKeyRing; - -import javafx.scene.control.Button; - -import javafx.beans.property.ReadOnlyDoubleProperty; - -import javafx.collections.ObservableList; - -import javax.annotation.Nullable; - -public class DisputeChatSession implements ChatSession { - private Dispute dispute; - private DisputeManager disputeManager; - private Button extraButton; - private ReadOnlyDoubleProperty widthProperty; - - public DisputeChatSession( - Dispute dispute, - DisputeManager disputeManager, - @Nullable Button extraButton, - ReadOnlyDoubleProperty widthProperty - ) { - this.dispute = dispute; - this.disputeManager = disputeManager; - this.extraButton = extraButton; - this.widthProperty = widthProperty; - } - - @Override - public boolean isTrader() { - return disputeManager.isTrader(dispute); - } - - @Override - public String getTradeId() { - return dispute.getTradeId(); - } - - @Override - public PubKeyRing getPubKeyRing() { - return dispute.getTraderPubKeyRing(); - } - - @Override - public void addDisputeCommunicationMessage(DisputeCommunicationMessage message) { - if (isTrader() || (!isTrader() && !message.isSystemMessage())) - dispute.addDisputeCommunicationMessage(message); - } - - @Override - public void persist() { - disputeManager.getDisputes().persist(); - } - - @Override - public ObservableList getDisputeCommunicationMessages() { - return dispute.getDisputeCommunicationMessages(); - } - - @Override - public boolean chatIsOpen() { - return !dispute.isClosed(); - } - - @Override - public Button extraButton() { - return extraButton; - } - - @Override - public NodeAddress getPeerNodeAddress() { - return disputeManager.getNodeAddressPubKeyRingTuple(dispute).first; - } - - @Override - public PubKeyRing getPeerPubKeyRing() { - return disputeManager.getNodeAddressPubKeyRingTuple(dispute).second; - } - - @Override - public ReadOnlyDoubleProperty widthProperty() { - return widthProperty; - } -} diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java b/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java index e4e92598e30..8a4022b4a5f 100644 --- a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java @@ -25,7 +25,9 @@ import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.components.InputTextField; import bisq.desktop.main.Chat.Chat; -import bisq.desktop.main.disputes.DisputeChatSession; + +import bisq.core.arbitration.DisputeChatSession; + import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.ContractWindow; import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; @@ -454,8 +456,11 @@ private void onSelectDispute(Dispute dispute) { closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); closeDisputeButton.setOnAction(e -> onCloseDispute(getSelectedDispute())); } - disputeChat.display(new DisputeChatSession(dispute, disputeManager, closeDisputeButton, - root.widthProperty())); + disputeManager.getChatManager().setChatSession(new DisputeChatSession(dispute, disputeManager, + disputeManager.getChatManager())); + disputeChat.display(disputeManager.getChatManager().getChatSession(), closeDisputeButton, + root.widthProperty() + ); } if (root.getChildren().size() > 2) From b93e7bac56e1e347a78a4f582ca0930579b7da09 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Sat, 25 May 2019 22:38:51 +0200 Subject: [PATCH 07/33] Add trader chat - Add communication messages to Trade protobuf message to be able to save chat messages per trade - Add Type enum and field to DisputeCommunicationMessage protobuf to be able to dispatch Dispute and Trade chat messages properly - Rename some function as isClient instead of isTrader to make it easier to understand who is who when two traders are communicating with each other --- .../java/bisq/common/storage/FileManager.java | 4 + common/src/main/proto/pb.proto | 6 + .../core/arbitration/DisputeChatSession.java | 34 ++-- .../bisq/core/arbitration/DisputeManager.java | 11 +- .../messages/DisputeCommunicationMessage.java | 41 ++++- .../main/java/bisq/core/chat/ChatManager.java | 2 +- .../main/java/bisq/core/chat/ChatSession.java | 17 +- .../java/bisq/core/trade/TradableList.java | 4 + core/src/main/java/bisq/core/trade/Trade.java | 26 ++- .../bisq/core/trade/TradeChatSession.java | 170 ++++++++++++++++++ .../java/bisq/core/trade/TradeManager.java | 15 ++ .../java/bisq/desktop/main/Chat/Chat.java | 42 ++--- .../disputes/trader/TraderDisputeView.java | 10 +- .../portfolio/pendingtrades/BuyerSubView.java | 1 - .../pendingtrades/PendingTradesView.java | 32 +++- 15 files changed, 347 insertions(+), 68 deletions(-) create mode 100644 core/src/main/java/bisq/core/trade/TradeChatSession.java diff --git a/common/src/main/java/bisq/common/storage/FileManager.java b/common/src/main/java/bisq/common/storage/FileManager.java index a0f907cc9cf..910c23780f6 100644 --- a/common/src/main/java/bisq/common/storage/FileManager.java +++ b/common/src/main/java/bisq/common/storage/FileManager.java @@ -74,6 +74,10 @@ public FileManager(File dir, File storageFile, long delay, PersistenceProtoResol try { Thread.currentThread().setName("Save-file-task-" + new Random().nextInt(10000)); // Runs in an auto save thread. + // TODO: this looks like it could cause corrupt data as the savePending is unset before the actual + // save. By moving to after the save there might be some persist operations that are not performed + // and data would be lost. Probably all persist operations should happen sequencially rather than + // skip one when there is already one scheduled if (!savePending.getAndSet(false)) { // Some other scheduled request already beat us to it. return null; diff --git a/common/src/main/proto/pb.proto b/common/src/main/proto/pb.proto index aba03e79d81..f0c125f4ea2 100644 --- a/common/src/main/proto/pb.proto +++ b/common/src/main/proto/pb.proto @@ -276,6 +276,10 @@ message PeerOpenedDisputeMessage { } message DisputeCommunicationMessage { + enum Type { + DISPUTE = 0; + TRADE = 1; + } int64 date = 1; string trade_id = 2; int32 trader_id = 3; @@ -290,6 +294,7 @@ message DisputeCommunicationMessage { string send_message_error = 12; bool acknowledged = 13; string ack_error = 14; + Type type = 15; } message DisputeResultMessage { @@ -1235,6 +1240,7 @@ message Trade { PubKeyRing arbitrator_pub_key_ring = 26; PubKeyRing mediator_pub_key_ring = 27; string counter_currency_tx_id = 28; + repeated DisputeCommunicationMessage communication_messages = 29; } message BuyerAsMakerTrade { diff --git a/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java b/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java index 5e1407b1fff..d853284e767 100644 --- a/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java +++ b/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java @@ -17,17 +17,13 @@ package bisq.core.arbitration; -import bisq.core.chat.ChatManager; -import bisq.core.chat.ChatSession; - -import bisq.core.arbitration.Dispute; -import bisq.core.arbitration.DisputeManager; import bisq.core.arbitration.messages.DisputeCommunicationMessage; import bisq.core.arbitration.messages.DisputeMessage; import bisq.core.arbitration.messages.DisputeResultMessage; import bisq.core.arbitration.messages.OpenNewDisputeMessage; import bisq.core.arbitration.messages.PeerOpenedDisputeMessage; import bisq.core.arbitration.messages.PeerPublishedDisputePayoutTxMessage; +import bisq.core.chat.ChatSession; import bisq.network.p2p.NodeAddress; @@ -49,16 +45,14 @@ public class DisputeChatSession extends ChatSession { private Dispute dispute; private DisputeManager disputeManager; - private ChatManager chatManager; public DisputeChatSession( @Nullable Dispute dispute, - DisputeManager disputeManager, - ChatManager chatManager + DisputeManager disputeManager ) { + super(DisputeCommunicationMessage.Type.DISPUTE); this.dispute = dispute; this.disputeManager = disputeManager; - this.chatManager = chatManager; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -66,7 +60,7 @@ public DisputeChatSession( /////////////////////////////////////////////////////////////////////////////////////////// @Override - public boolean isTrader() { + public boolean isClient() { return disputeManager.isTrader(dispute); } @@ -76,13 +70,14 @@ public String getTradeId() { } @Override - public PubKeyRing getPubKeyRing() { + public PubKeyRing getClientPubKeyRing() { + // Get pubkeyring of trader. Arbitrator is considered server for the chat session return dispute.getTraderPubKeyRing(); } @Override public void addDisputeCommunicationMessage(DisputeCommunicationMessage message) { - if (isTrader() || (!isTrader() && !message.isSystemMessage())) + if (isClient() || (!isClient() && !message.isSystemMessage())) dispute.addDisputeCommunicationMessage(message); } @@ -137,8 +132,13 @@ public void dispatchMessage(DisputeMessage message) { disputeManager.onOpenNewDisputeMessage((OpenNewDisputeMessage) message); else if (message instanceof PeerOpenedDisputeMessage) disputeManager.onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message); - else if (message instanceof DisputeCommunicationMessage) - chatManager.onDisputeDirectMessage((DisputeCommunicationMessage) message); + else if (message instanceof DisputeCommunicationMessage) { + if (((DisputeCommunicationMessage)message).getType() != DisputeCommunicationMessage.Type.DISPUTE){ + log.debug("Ignore non distpute type communication message"); + return; + } + disputeManager.getChatManager().onDisputeDirectMessage((DisputeCommunicationMessage) message); + } else if (message instanceof DisputeResultMessage) disputeManager.onDisputeResultMessage((DisputeResultMessage) message); else if (message instanceof PeerPublishedDisputePayoutTxMessage) @@ -154,16 +154,18 @@ public List getChatMessages() { .collect(Collectors.toList()); } + @Override public boolean channelOpen(DisputeCommunicationMessage message) { return disputeManager.findDispute(message.getTradeId(), message.getTraderId()).isPresent(); } + @Override public void storeDisputeCommunicationMessage(DisputeCommunicationMessage message) { Optional disputeOptional = disputeManager.findDispute(message.getTradeId(), message.getTraderId()); if (disputeOptional.isPresent()) { - if (!disputeOptional.get().getDisputeCommunicationMessages().stream() - .anyMatch(m -> m.getUid().equals(message.getUid()))) + if (disputeOptional.get().getDisputeCommunicationMessages().stream() + .noneMatch(m -> m.getUid().equals(message.getUid()))) disputeOptional.get().addDisputeCommunicationMessage(message); else log.warn("We got a disputeCommunicationMessage what we have already stored. UId = {} TradeId = {}", diff --git a/core/src/main/java/bisq/core/arbitration/DisputeManager.java b/core/src/main/java/bisq/core/arbitration/DisputeManager.java index 737458a6f9e..167c538f371 100644 --- a/core/src/main/java/bisq/core/arbitration/DisputeManager.java +++ b/core/src/main/java/bisq/core/arbitration/DisputeManager.java @@ -18,7 +18,6 @@ package bisq.core.arbitration; import bisq.core.arbitration.messages.DisputeCommunicationMessage; -import bisq.core.arbitration.messages.DisputeMessage; import bisq.core.arbitration.messages.DisputeResultMessage; import bisq.core.arbitration.messages.OpenNewDisputeMessage; import bisq.core.arbitration.messages.PeerOpenedDisputeMessage; @@ -40,10 +39,7 @@ import bisq.core.trade.TradeManager; import bisq.core.trade.closed.ClosedTradableManager; -import bisq.network.p2p.AckMessage; -import bisq.network.p2p.AckMessageSourceType; import bisq.network.p2p.BootstrapListener; -import bisq.network.p2p.DecryptedMessageWithPubKey; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; import bisq.network.p2p.SendMailboxMessageListener; @@ -55,7 +51,6 @@ import bisq.common.crypto.PubKeyRing; import bisq.common.handlers.FaultHandler; import bisq.common.handlers.ResultHandler; -import bisq.common.proto.network.NetworkEnvelope; import bisq.common.proto.persistable.PersistedDataHost; import bisq.common.proto.persistable.PersistenceProtoResolver; import bisq.common.storage.Storage; @@ -86,7 +81,6 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.CopyOnWriteArraySet; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -150,7 +144,7 @@ public DisputeManager(P2PService p2PService, this.keyRing = keyRing; chatManager = new ChatManager(p2PService, walletsSetup); - chatManager.setChatSession(new DisputeChatSession(null, this, chatManager)); + chatManager.setChatSession(new DisputeChatSession(null, this)); disputeStorage = new Storage<>(storageDir, persistenceProtoResolver); @@ -259,6 +253,7 @@ public void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen, ResultHan : Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage( + chatManager.getChatSession().getType(), dispute.getTradeId(), keyRing.getPubKeyRing().hashCode(), false, @@ -371,6 +366,7 @@ private String sendPeerOpenedDisputeMessage(Dispute disputeFromOpener, Contract Res.get("support.peerOpenedTicket", disputeInfo) : Res.get("support.peerOpenedDispute", disputeInfo); DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage( + chatManager.getChatSession().getType(), dispute.getTradeId(), keyRing.getPubKeyRing().hashCode(), false, @@ -452,6 +448,7 @@ public void onFault(String errorMessage) { // arbitrator send result to trader public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String text) { DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage( + chatManager.getChatSession().getType(), dispute.getTradeId(), dispute.getTraderPubKeyRing().hashCode(), false, diff --git a/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java b/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java index fa6ae9de04e..4cf78f1e835 100644 --- a/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java +++ b/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java @@ -22,6 +22,7 @@ import bisq.network.p2p.NodeAddress; import bisq.common.app.Version; +import bisq.common.proto.ProtoUtil; import bisq.common.util.Utilities; import io.bisq.generated.protobuffer.PB; @@ -49,17 +50,41 @@ import javax.annotation.Nullable; +/* Message for direct communication between two nodes. Originally built for trader to + * arbitrator communication as no other direct communication was allowed. Aribtrator is + * considered as the server and trader as the client in arbitration chats + * + * For trader to trader communication the maker is considered to be the server + * and the taker is considered as the client. + * */ @EqualsAndHashCode(callSuper = true) // listener is transient and therefore excluded anyway @Getter @Slf4j public final class DisputeCommunicationMessage extends DisputeMessage { + public enum Type { + DISPUTE, + TRADE; + + public static DisputeCommunicationMessage.Type fromProto( + PB.DisputeCommunicationMessage.Type type) { + return ProtoUtil.enumFromProto(DisputeCommunicationMessage.Type.class, type.name()); + } + + public static PB.DisputeCommunicationMessage.Type toProtoMessage(Type type) { + return PB.DisputeCommunicationMessage.Type.valueOf(type.name()); + } + } + public interface Listener { void onMessageStateChanged(); } + private final DisputeCommunicationMessage.Type type; private final String tradeId; private final int traderId; + // This is only used for the server client relationship + // If senderIsTrader == true then the sender is the client private final boolean senderIsTrader; private final String message; private final ArrayList attachments = new ArrayList<>(); @@ -76,12 +101,14 @@ public interface Listener { transient private WeakReference listener; - public DisputeCommunicationMessage(String tradeId, + public DisputeCommunicationMessage(DisputeCommunicationMessage.Type type, + String tradeId, int traderId, boolean senderIsTrader, String message, NodeAddress senderNodeAddress) { - this(tradeId, + this(type, + tradeId, traderId, senderIsTrader, message, @@ -102,7 +129,8 @@ public DisputeCommunicationMessage(String tradeId, // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - private DisputeCommunicationMessage(String tradeId, + private DisputeCommunicationMessage(Type type, + String tradeId, int traderId, boolean senderIsTrader, String message, @@ -117,6 +145,7 @@ private DisputeCommunicationMessage(String tradeId, @Nullable String sendMessageError, @Nullable String ackError) { super(messageVersion, uid); + this.type = type; this.tradeId = tradeId; this.traderId = traderId; this.senderIsTrader = senderIsTrader; @@ -135,6 +164,7 @@ private DisputeCommunicationMessage(String tradeId, @Override public PB.NetworkEnvelope toProtoNetworkEnvelope() { PB.DisputeCommunicationMessage.Builder builder = PB.DisputeCommunicationMessage.newBuilder() + .setType(DisputeCommunicationMessage.Type.toProtoMessage(type)) .setTradeId(tradeId) .setTraderId(traderId) .setSenderIsTrader(senderIsTrader) @@ -156,6 +186,7 @@ public PB.NetworkEnvelope toProtoNetworkEnvelope() { public static DisputeCommunicationMessage fromProto(PB.DisputeCommunicationMessage proto, int messageVersion) { final DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage( + DisputeCommunicationMessage.Type.fromProto(proto.getType()), proto.getTradeId(), proto.getTraderId(), proto.getSenderIsTrader(), @@ -186,7 +217,6 @@ public static DisputeCommunicationMessage fromPayloadProto(PB.DisputeCommunicati /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// - public void addAllAttachments(List attachments) { this.attachments.addAll(attachments); } @@ -258,7 +288,8 @@ private void notifyChangeListener() { @Override public String toString() { return "DisputeCommunicationMessage{" + - "\n tradeId='" + tradeId + '\'' + + "\n type='" + type + '\'' + + ",\n tradeId='" + tradeId + '\'' + ",\n traderId=" + traderId + ",\n senderIsTrader=" + senderIsTrader + ",\n message='" + message + '\'' + diff --git a/core/src/main/java/bisq/core/chat/ChatManager.java b/core/src/main/java/bisq/core/chat/ChatManager.java index 2446d73d25f..cba88500a9a 100644 --- a/core/src/main/java/bisq/core/chat/ChatManager.java +++ b/core/src/main/java/bisq/core/chat/ChatManager.java @@ -17,7 +17,6 @@ package bisq.core.chat; -import bisq.core.arbitration.Dispute; import bisq.core.arbitration.DisputeManager; import bisq.core.arbitration.messages.DisputeCommunicationMessage; import bisq.core.arbitration.messages.DisputeMessage; @@ -50,6 +49,7 @@ public class ChatManager { private static final Logger log = LoggerFactory.getLogger(DisputeManager.class); + @Getter private final P2PService p2PService; private final WalletsSetup walletsSetup; @Setter diff --git a/core/src/main/java/bisq/core/chat/ChatSession.java b/core/src/main/java/bisq/core/chat/ChatSession.java index 4972f9536a0..7727fc77e9f 100644 --- a/core/src/main/java/bisq/core/chat/ChatSession.java +++ b/core/src/main/java/bisq/core/chat/ChatSession.java @@ -20,28 +20,29 @@ import bisq.core.arbitration.messages.DisputeCommunicationMessage; import bisq.core.arbitration.messages.DisputeMessage; -import bisq.network.p2p.DecryptedMessageWithPubKey; import bisq.network.p2p.NodeAddress; import bisq.common.crypto.PubKeyRing; -import javafx.scene.control.Button; - -import javafx.beans.property.ReadOnlyDoubleProperty; - import javafx.collections.ObservableList; import java.util.List; -import java.util.concurrent.CopyOnWriteArraySet; import lombok.Getter; public abstract class ChatSession { - abstract public boolean isTrader(); + @Getter + DisputeCommunicationMessage.Type type; + + public ChatSession(DisputeCommunicationMessage.Type type) { + this.type = type; + } + + abstract public boolean isClient(); abstract public String getTradeId(); - abstract public PubKeyRing getPubKeyRing(); + abstract public PubKeyRing getClientPubKeyRing(); abstract public void addDisputeCommunicationMessage(DisputeCommunicationMessage message); diff --git a/core/src/main/java/bisq/core/trade/TradableList.java b/core/src/main/java/bisq/core/trade/TradableList.java index 37fd1f0046a..ecdaed5df0d 100644 --- a/core/src/main/java/bisq/core/trade/TradableList.java +++ b/core/src/main/java/bisq/core/trade/TradableList.java @@ -131,6 +131,10 @@ public boolean remove(T tradable) { return changed; } + public void persist() { + storage.queueUpForSave(); + } + public Stream stream() { return list.stream(); } diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index dd14c4f092a..5d3c2567ce0 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -20,6 +20,7 @@ import bisq.core.arbitration.Arbitrator; import bisq.core.arbitration.ArbitratorManager; import bisq.core.arbitration.Mediator; +import bisq.core.arbitration.messages.DisputeCommunicationMessage; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; @@ -71,10 +72,14 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + import java.util.Date; import java.util.HashSet; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import lombok.Getter; import lombok.Setter; @@ -333,6 +338,8 @@ public static PB.Trade.TradePeriodState toProtoMessage(Trade.TradePeriodState tr @Setter @Nullable private String counterCurrencyTxId; + @Getter + private final ObservableList communicationMessages = FXCollections.observableArrayList(); // Transient // Immutable @@ -431,7 +438,10 @@ public Message toProtoMessage() { .setTradePrice(tradePrice) .setState(PB.Trade.State.valueOf(state.name())) .setDisputeState(PB.Trade.DisputeState.valueOf(disputeState.name())) - .setTradePeriodState(PB.Trade.TradePeriodState.valueOf(tradePeriodState.name())); + .setTradePeriodState(PB.Trade.TradePeriodState.valueOf(tradePeriodState.name())) + .addAllCommunicationMessages(communicationMessages.stream() + .map(msg -> msg.toProtoNetworkEnvelope().getDisputeCommunicationMessage()) + .collect(Collectors.toList())); Optional.ofNullable(takerFeeTxId).ifPresent(builder::setTakerFeeTxId); Optional.ofNullable(depositTxId).ifPresent(builder::setDepositTxId); @@ -475,6 +485,11 @@ public static Trade fromProto(Trade trade, PB.Trade proto, CoreProtoResolver cor trade.setArbitratorPubKeyRing(proto.hasArbitratorPubKeyRing() ? PubKeyRing.fromProto(proto.getArbitratorPubKeyRing()) : null); trade.setMediatorPubKeyRing(proto.hasMediatorPubKeyRing() ? PubKeyRing.fromProto(proto.getMediatorPubKeyRing()) : null); trade.setCounterCurrencyTxId(proto.getCounterCurrencyTxId().isEmpty() ? null : proto.getCounterCurrencyTxId()); + + trade.communicationMessages.addAll(proto.getCommunicationMessagesList().stream() + .map(DisputeCommunicationMessage::fromPayloadProto) + .collect(Collectors.toList())); + return trade; } @@ -585,6 +600,14 @@ public void removeDecryptedMessageWithPubKey(DecryptedMessageWithPubKey decrypte decryptedMessageWithPubKeySet.remove(decryptedMessageWithPubKey); } + public void addCommunicationMessage(DisputeCommunicationMessage disputeCommunicationMessage) { + if (!communicationMessages.contains(disputeCommunicationMessage)) { + communicationMessages.add(disputeCommunicationMessage); + storage.queueUpForSave(); + } else { + log.error("Trade DisputeCommunicationMessage already exists"); + } + } /////////////////////////////////////////////////////////////////////////////////////////// // Model implementation @@ -991,6 +1014,7 @@ public String toString() { ",\n decryptedMessageWithPubKeySet=" + decryptedMessageWithPubKeySet + ",\n arbitratorPubKeyRing=" + arbitratorPubKeyRing + ",\n mediatorPubKeyRing=" + mediatorPubKeyRing + + ",\n communicationMessages=" + communicationMessages + "\n}"; } } diff --git a/core/src/main/java/bisq/core/trade/TradeChatSession.java b/core/src/main/java/bisq/core/trade/TradeChatSession.java new file mode 100644 index 00000000000..5d82551a03e --- /dev/null +++ b/core/src/main/java/bisq/core/trade/TradeChatSession.java @@ -0,0 +1,170 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.trade; + +import bisq.core.arbitration.DisputeManager; +import bisq.core.arbitration.messages.DisputeCommunicationMessage; +import bisq.core.arbitration.messages.DisputeMessage; +import bisq.core.chat.ChatManager; +import bisq.core.chat.ChatSession; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.PubKeyRing; + +import javafx.collections.ObservableList; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/* Makers are considered as servers and takers as clients for trader to trader chat + * sessions. This is only to make it easier to understand who's who, there is no real + * server/client relationship */ +public class TradeChatSession extends ChatSession { + private static final Logger log = LoggerFactory.getLogger(DisputeManager.class); + + private Trade trade; + private boolean isClient; + private boolean isBuyer; + private TradeManager tradeManager; + private ChatManager chatManager; + + public TradeChatSession( + Trade trade, + boolean isClient, + boolean isBuyer, + TradeManager tradeManager, + ChatManager chatManager + ) { + super(DisputeCommunicationMessage.Type.TRADE); + this.trade = trade; + this.isClient = isClient; + this.isBuyer = isBuyer; + this.tradeManager = tradeManager; + this.chatManager = chatManager; + } + + @Override + public boolean isClient() { + return isClient; + } + + @Override + public String getTradeId() { + return trade.getId(); + } + + @Override + public PubKeyRing getClientPubKeyRing() { + // Get pubkeyring of taker. Maker is considered server for chat sessions + if (trade.getContract() != null) + return trade.getContract().getTakerPubKeyRing(); + return null; + } + + @Override + public void addDisputeCommunicationMessage(DisputeCommunicationMessage message) { + trade.addCommunicationMessage(message); + } + + @Override + public void persist() { + tradeManager.persistTrades(); + } + + @Override + public ObservableList getDisputeCommunicationMessages() { + return trade.getCommunicationMessages(); + } + + @Override + public boolean chatIsOpen() { + return trade.getState() != Trade.State.WITHDRAW_COMPLETED; + } + + @Override + public NodeAddress getPeerNodeAddress(DisputeCommunicationMessage message) { + Optional tradeOptional = tradeManager.getTradeById(message.getTradeId()); + if (tradeOptional.isPresent()) { + Trade t = tradeOptional.get(); + if (t.getContract() != null) + return isBuyer ? + t.getContract().getSellerNodeAddress() : + t.getContract().getBuyerNodeAddress(); + } + return null; + } + + @Override + public PubKeyRing getPeerPubKeyRing(DisputeCommunicationMessage message) { + Optional tradeOptional = tradeManager.getTradeById(message.getTradeId()); + if (tradeOptional.isPresent()) { + Trade t = tradeOptional.get(); + if (t.getContract() != null) + return isClient ? + t.getContract().getMakerPubKeyRing() : + t.getContract().getTakerPubKeyRing(); + } + return null; + } + + @Override + public void dispatchMessage(DisputeMessage message) { + log.info("Received {} with tradeId {} and uid {}", + message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); + if (message instanceof DisputeCommunicationMessage) { + if (((DisputeCommunicationMessage)message).getType() != DisputeCommunicationMessage.Type.TRADE){ + log.debug("Ignore non trade type communication message"); + return; + } + chatManager.onDisputeDirectMessage((DisputeCommunicationMessage) message); + } + else + log.warn("Unsupported message at dispatchMessage.\nmessage=" + message); + } + + @Override + public List getChatMessages() { + return tradeManager.getTradableList().stream() + .flatMap(trade -> trade.getCommunicationMessages().stream()) + .collect(Collectors.toList()); + } + + @Override + public boolean channelOpen(DisputeCommunicationMessage message) { + return tradeManager.getTradeById(message.getTradeId()).isPresent(); + } + + @Override + public void storeDisputeCommunicationMessage(DisputeCommunicationMessage message) { + Optional tradeOptional = tradeManager.getTradeById(message.getTradeId()); + if (tradeOptional.isPresent()) { + if (tradeOptional.get().getCommunicationMessages().stream() + .noneMatch(m -> m.getUid().equals(message.getUid()))) + tradeOptional.get().addCommunicationMessage(message); + else + log.warn("Trade got a disputeCommunicationMessage what we have already stored. UId = {} TradeId = {}", + message.getUid(), message.getTradeId()); + } + } + +} diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 4e70e45ad6e..6dc3825c9ac 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -20,9 +20,11 @@ import bisq.core.arbitration.ArbitratorManager; import bisq.core.btc.exceptions.AddressEntryException; import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.chat.ChatManager; import bisq.core.filter.FilterManager; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; @@ -111,6 +113,7 @@ public class TradeManager implements PersistedDataHost { private final ClosedTradableManager closedTradableManager; private final FailedTradesManager failedTradesManager; private final P2PService p2PService; + private final WalletsSetup walletsSetup; private final PriceFeedService priceFeedService; private final FilterManager filterManager; private final TradeStatisticsManager tradeStatisticsManager; @@ -129,6 +132,9 @@ public class TradeManager implements PersistedDataHost { @Getter private final LongProperty numPendingTrades = new SimpleLongProperty(); + @Getter + private final ChatManager chatManager; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -144,6 +150,7 @@ public TradeManager(User user, ClosedTradableManager closedTradableManager, FailedTradesManager failedTradesManager, P2PService p2PService, + WalletsSetup walletsSetup, PriceFeedService priceFeedService, FilterManager filterManager, TradeStatisticsManager tradeStatisticsManager, @@ -162,6 +169,7 @@ public TradeManager(User user, this.closedTradableManager = closedTradableManager; this.failedTradesManager = failedTradesManager; this.p2PService = p2PService; + this.walletsSetup = walletsSetup; this.priceFeedService = priceFeedService; this.filterManager = filterManager; this.tradeStatisticsManager = tradeStatisticsManager; @@ -172,6 +180,9 @@ public TradeManager(User user, tradableListStorage = new Storage<>(storageDir, persistenceProtoResolver); + chatManager = new ChatManager(p2PService, walletsSetup); + chatManager.setChatSession(new TradeChatSession(null, true, true, this, chatManager)); + p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, peerNodeAddress) -> { NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); @@ -650,4 +661,8 @@ else if (now.after(halfTradePeriodDate)) } }); } + + public void persistTrades() { + tradableList.persist(); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java index c1cba432fdb..8dd99fac270 100644 --- a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java +++ b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java @@ -28,6 +28,7 @@ import bisq.core.arbitration.Attachment; import bisq.core.arbitration.messages.DisputeCommunicationMessage; +import bisq.core.chat.ChatManager; import bisq.core.chat.ChatSession; import bisq.core.locale.Res; import bisq.core.util.BSFormatter; @@ -115,7 +116,6 @@ public class Chat extends AnchorPane { private TableGroupHeadline tableGroupHeadline; private VBox messagesInputBox; - // TODO set these on new session or link to session action @Getter Button extraButton; @Getter @@ -123,7 +123,6 @@ public class Chat extends AnchorPane { // Communication stuff, to be renamed to something more generic private final P2PService p2PService; - private ChatSession chatSession; private DisputeCommunicationMessage disputeCommunicationMessage; private ObservableList disputeCommunicationMessages; private ListChangeListener disputeDirectMessageListListener; @@ -134,11 +133,12 @@ public class Chat extends AnchorPane { protected final BSFormatter formatter; private EventHandler keyEventEventHandler; + private ChatManager chatManager; - public Chat(P2PService p2PService, BSFormatter formatter - ) { - this.p2PService = p2PService; + public Chat(ChatManager chatManager, BSFormatter formatter) { + this.chatManager = chatManager; this.formatter = formatter; + this.p2PService = chatManager.getP2PService(); } public void initialize() { @@ -146,7 +146,7 @@ public void initialize() { keyEventEventHandler = event -> { if (Utilities.isAltOrCtrlPressed(KeyCode.ENTER, event)) { - if (chatSession.chatIsOpen() && inputTextArea.isFocused()) + if (chatManager.getChatSession().chatIsOpen() && inputTextArea.isFocused()) onTrySendMessage(); } }; @@ -164,8 +164,8 @@ public void deactivate() { public void display(ChatSession chatSession, @Nullable Button extraButton, ReadOnlyDoubleProperty widthProperty) { removeListenersOnSessionChange(); + chatManager.setChatSession(chatSession); this.getChildren().clear(); - this.chatSession = chatSession; this.extraButton = extraButton; this.widthProperty = widthProperty; @@ -193,7 +193,7 @@ public void display(ChatSession chatSession, @Nullable Button extraButton, inputTextArea = new BisqTextArea(); inputTextArea.setPrefHeight(70); inputTextArea.setWrapText(true); - if (chatSession.isTrader()) + if (chatSession.isClient()) inputTextArea.setPromptText(Res.get("support.input.prompt")); sendButton = new AutoTooltipButton(Res.get("support.send")); @@ -305,7 +305,7 @@ public void updateItem(final DisputeCommunicationMessage message, boolean empty) AnchorPane.setBottomAnchor(attachmentsBox, bottomBorder + 10); boolean senderIsTrader = message.isSenderIsTrader(); - boolean isMyMsg = chatSession.isTrader() == senderIsTrader; + boolean isMyMsg = chatSession.isClient() == senderIsTrader; arrow.setVisible(!message.isSystemMessage()); arrow.setManaged(!message.isSystemMessage()); @@ -328,7 +328,7 @@ public void updateItem(final DisputeCommunicationMessage message, boolean empty) bg.setId("message-bubble-blue"); messageLabel.getStyleClass().add("my-message"); copyIcon.getStyleClass().add("my-message"); - if (chatSession.isTrader()) + if (chatSession.isClient()) arrow.setId("bubble_arrow_blue_left"); else arrow.setId("bubble_arrow_blue_right"); @@ -349,7 +349,7 @@ public void updateItem(final DisputeCommunicationMessage message, boolean empty) bg.setId("message-bubble-grey"); messageLabel.getStyleClass().add("message"); copyIcon.getStyleClass().add("message"); - if (chatSession.isTrader()) + if (chatSession.isClient()) arrow.setId("bubble_arrow_grey_right"); else arrow.setId("bubble_arrow_grey_left"); @@ -624,21 +624,21 @@ private void onSendMessage(String inputText) { } } - // traders send msg to the arbitrator or arbitrator to 1 trader (trader to trader is not allowed) private DisputeCommunicationMessage sendDisputeDirectMessage(String text, ArrayList attachments) { DisputeCommunicationMessage message = new DisputeCommunicationMessage( - chatSession.getTradeId(), - chatSession.getPubKeyRing().hashCode(), - chatSession.isTrader(), + chatManager.getChatSession().getType(), + chatManager.getChatSession().getTradeId(), + chatManager.getChatSession().getClientPubKeyRing().hashCode(), + chatManager.getChatSession().isClient(), text, p2PService.getAddress() ); message.addAllAttachments(attachments); - NodeAddress peersNodeAddress = chatSession.getPeerNodeAddress(message); - PubKeyRing receiverPubKeyRing = chatSession.getPeerPubKeyRing(message); + NodeAddress peersNodeAddress = chatManager.getChatSession().getPeerNodeAddress(message); + PubKeyRing receiverPubKeyRing = chatManager.getChatSession().getPeerPubKeyRing(message); - chatSession.addDisputeCommunicationMessage(message); + chatManager.getChatSession().addDisputeCommunicationMessage(message); if (receiverPubKeyRing != null) { log.info("Send {} to peer {}. tradeId={}, uid={}", @@ -653,7 +653,7 @@ public void onArrived() { log.info("{} arrived at peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); message.setArrived(true); - chatSession.persist(); + chatManager.getChatSession().persist(); } @Override @@ -661,7 +661,7 @@ public void onStoredInMailbox() { log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); message.setStoredInMailbox(true); - chatSession.persist(); + chatManager.getChatSession().persist(); } @Override @@ -669,7 +669,7 @@ public void onFault(String errorMessage) { log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); message.setSendMessageError(errorMessage); - chatSession.persist(); + chatManager.getChatSession().persist(); } } ); diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java b/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java index 8a4022b4a5f..5e3538b766e 100644 --- a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java @@ -25,9 +25,6 @@ import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.components.InputTextField; import bisq.desktop.main.Chat.Chat; - -import bisq.core.arbitration.DisputeChatSession; - import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.ContractWindow; import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; @@ -38,6 +35,7 @@ import bisq.core.alert.PrivateNotificationManager; import bisq.core.app.AppOptionKeys; import bisq.core.arbitration.Dispute; +import bisq.core.arbitration.DisputeChatSession; import bisq.core.arbitration.DisputeManager; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; @@ -319,7 +317,7 @@ public void initialize() { } }; - disputeChat = new Chat(p2PService, formatter); + disputeChat = new Chat(disputeManager.getChatManager(), formatter); disputeChat.initialize(); } @@ -456,9 +454,7 @@ private void onSelectDispute(Dispute dispute) { closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); closeDisputeButton.setOnAction(e -> onCloseDispute(getSelectedDispute())); } - disputeManager.getChatManager().setChatSession(new DisputeChatSession(dispute, disputeManager, - disputeManager.getChatManager())); - disputeChat.display(disputeManager.getChatManager().getChatSession(), closeDisputeButton, + disputeChat.display(new DisputeChatSession(dispute, disputeManager), closeDisputeButton, root.widthProperty() ); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/BuyerSubView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/BuyerSubView.java index df5c4303a08..365e51627f9 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/BuyerSubView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/BuyerSubView.java @@ -39,7 +39,6 @@ public class BuyerSubView extends TradeSubView { public BuyerSubView(PendingTradesViewModel model) { super(model); - } @Override diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 9b3a874be6d..720c304bda6 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -22,6 +22,7 @@ import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.components.PeerInfoIcon; +import bisq.desktop.main.Chat.Chat; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; @@ -29,10 +30,12 @@ import bisq.core.app.AppOptionKeys; import bisq.core.locale.Res; import bisq.core.trade.Trade; +import bisq.core.trade.TradeChatSession; import bisq.core.user.Preferences; import bisq.core.util.BSFormatter; import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; import bisq.common.UserThread; import bisq.common.util.Utilities; @@ -75,6 +78,7 @@ public class PendingTradesView extends ActivatableViewAndModel tableView; @@ -88,6 +92,7 @@ public class PendingTradesView extends ActivatableViewAndModel 1) root.getChildren().set(1, selectedSubView); + boolean isTaker = !model.dataModel.isMaker(selectedItem.getTrade().getOffer()); + boolean isBuyer = model.dataModel.isBuyer(); + if (tradeChat != null) + tradeChat.display(new TradeChatSession(selectedItem.getTrade(), isTaker, isBuyer, + model.dataModel.tradeManager, + model.dataModel.tradeManager.getChatManager()), + null, + root.widthProperty()); + +// if (root.getChildren().size() == 2) +// root.getChildren().add(tradeChat); +// else if (root.getChildren().size() > 2) +// root.getChildren().set(2, tradeChat); } updateTableSelection(); @@ -244,6 +267,10 @@ else if (root.getChildren().size() == 2) }); updateTableSelection(); + if (tradeChat != null) { + tradeChat.activate(); + tradeChat.scrollToBottom(); + } } @Override @@ -256,6 +283,9 @@ protected void deactivate() { if (scene != null) scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + + if (tradeChat != null) + tradeChat.deactivate(); } private void removeSelectedSubView() { From 3b87045133f2796030caeebdec09b6385d9f641d Mon Sep 17 00:00:00 2001 From: sqrrm Date: Mon, 3 Jun 2019 13:12:12 +0200 Subject: [PATCH 08/33] Make attachments optional --- .../main/java/bisq/desktop/main/Chat/Chat.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java index 8dd99fac270..918205cbe8b 100644 --- a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java +++ b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java @@ -101,6 +101,7 @@ import org.slf4j.LoggerFactory; import lombok.Getter; +import lombok.Setter; import javax.annotation.Nullable; @@ -120,6 +121,8 @@ public class Chat extends AnchorPane { Button extraButton; @Getter private ReadOnlyDoubleProperty widthProperty; + @Setter + boolean allowAttachments; // Communication stuff, to be renamed to something more generic private final P2PService p2PService; @@ -139,6 +142,7 @@ public Chat(ChatManager chatManager, BSFormatter formatter) { this.chatManager = chatManager; this.formatter = formatter; this.p2PService = chatManager.getP2PService(); + allowAttachments = true; } public void initialize() { @@ -214,7 +218,10 @@ public void display(ChatSession chatSession, @Nullable Button extraButton, if (chatSession.chatIsOpen()) { HBox buttonBox = new HBox(); buttonBox.setSpacing(10); - buttonBox.getChildren().addAll(sendButton, uploadButton, sendMsgBusyAnimation, sendMsgInfoLabel); + if (allowAttachments) + buttonBox.getChildren().addAll(sendButton, uploadButton, sendMsgBusyAnimation, sendMsgInfoLabel); + else + buttonBox.getChildren().addAll(sendButton, sendMsgBusyAnimation, sendMsgInfoLabel); if (extraButton != null) { extraButton.setDefaultButton(true); @@ -396,7 +403,9 @@ public void updateItem(final DisputeCommunicationMessage message, boolean empty) headerLabel.setText(formatter.formatDateTime(new Date(message.getDate()))); messageLabel.setText(message.getMessage()); attachmentsBox.getChildren().clear(); - if (message.getAttachments() != null && message.getAttachments().size() > 0) { + if (allowAttachments && + message.getAttachments() != null && + message.getAttachments().size() > 0) { AnchorPane.setBottomAnchor(messageLabel, bottomBorder + attachmentsBoxHeight + 10); attachmentsBox.getChildren().add(new AutoTooltipLabel(Res.get("support.attachments") + " ") {{ setPadding(new Insets(0, 0, 3, 0)); @@ -519,6 +528,8 @@ private void onTrySendMessage() { } private void onRequestUpload() { + if (!allowAttachments) + return; int totalSize = tempAttachments.stream().mapToInt(a -> a.getBytes().length).sum(); if (tempAttachments.size() < 3) { FileChooser fileChooser = new FileChooser(); @@ -558,6 +569,8 @@ private void onRequestUpload() { } private void onOpenAttachment(Attachment attachment) { + if (!allowAttachments) + return; FileChooser fileChooser = new FileChooser(); fileChooser.setTitle(Res.get("support.save")); fileChooser.setInitialFileName(attachment.getFileName()); From fb71c3245fc3ec61e475279c0bdda2348cd97102 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Mon, 3 Jun 2019 13:12:49 +0200 Subject: [PATCH 09/33] Test trade chat UI --- .../main/portfolio/pendingtrades/PendingTradesView.java | 9 ++++----- .../main/portfolio/pendingtrades/TradeSubView.java | 8 ++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 720c304bda6..a512a154636 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -191,6 +191,7 @@ public void initialize() { }; tradeChat = new Chat(model.dataModel.tradeManager.getChatManager(), formatter); + tradeChat.setAllowAttachments(false); tradeChat.initialize(); } @@ -241,12 +242,10 @@ else if (root.getChildren().size() > 1) model.dataModel.tradeManager, model.dataModel.tradeManager.getChatManager()), null, - root.widthProperty()); + selectedSubView.getLeftVBox().widthProperty()); +// root.widthProperty()); -// if (root.getChildren().size() == 2) -// root.getChildren().add(tradeChat); -// else if (root.getChildren().size() > 2) -// root.getChildren().set(2, tradeChat); + selectedSubView.setChat(tradeChat); } updateTableSelection(); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java index 085d1d7c52a..8bad0fdcf83 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java @@ -19,6 +19,7 @@ import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.main.Chat.Chat; import bisq.desktop.main.portfolio.pendingtrades.steps.TradeStepView; import bisq.desktop.main.portfolio.pendingtrades.steps.TradeWizardItem; import bisq.desktop.util.Layout; @@ -42,6 +43,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import lombok.Getter; + import static bisq.desktop.util.FormBuilder.addButtonAfterGroup; import static bisq.desktop.util.FormBuilder.addMultilineLabel; import static bisq.desktop.util.FormBuilder.addTitledGroupBg; @@ -50,6 +53,7 @@ public abstract class TradeSubView extends HBox { protected final Logger log = LoggerFactory.getLogger(this.getClass()); protected final PendingTradesViewModel model; + @Getter protected VBox leftVBox; protected AnchorPane contentPane; protected TradeStepView tradeStepView; @@ -191,6 +195,10 @@ private void createAndAddTradeStepView(Class viewClass) } } + public void setChat(Chat chat) { + leftVBox.getChildren().setAll(chat); + } + private void addLeftBox() { leftVBox = new VBox(); leftVBox.setSpacing(Layout.SPACING_V_BOX); From 0217c3d63f7dc547b260adf451e7bb552757194f Mon Sep 17 00:00:00 2001 From: sqrrm Date: Wed, 5 Jun 2019 14:57:37 +0200 Subject: [PATCH 10/33] Add comments on BSQ trade fee calculation --- core/src/main/java/bisq/core/dao/governance/param/Param.java | 2 ++ core/src/main/java/bisq/core/util/CoinUtil.java | 1 + 2 files changed, 3 insertions(+) diff --git a/core/src/main/java/bisq/core/dao/governance/param/Param.java b/core/src/main/java/bisq/core/dao/governance/param/Param.java index 81fa09ede95..dc1d4292474 100644 --- a/core/src/main/java/bisq/core/dao/governance/param/Param.java +++ b/core/src/main/java/bisq/core/dao/governance/param/Param.java @@ -61,6 +61,8 @@ public enum Param { // Might need adjustment if BSQ/BTC rate changes. DEFAULT_MAKER_FEE_BSQ("0.50", ParamType.BSQ, 5, 5), // ~ 0.01% of trade amount DEFAULT_TAKER_FEE_BSQ("1.5", ParamType.BSQ, 5, 5), + // Min fee is the smallest fee allowed for a trade. If the default fee would be less than min fee the + // min fee is used instead. // 0.03 BSQ (3 satoshi) for a 1 BTC trade. 0.05 USD if 1 BSQ = 1 USD, 10 % of the BTC fee MIN_MAKER_FEE_BSQ("0.03", ParamType.BSQ, 5, 5), // 0.0003%. MIN_TAKER_FEE_BSQ("0.03", ParamType.BSQ, 5, 5), diff --git a/core/src/main/java/bisq/core/util/CoinUtil.java b/core/src/main/java/bisq/core/util/CoinUtil.java index cb79de0d29a..fa2c1a1922d 100644 --- a/core/src/main/java/bisq/core/util/CoinUtil.java +++ b/core/src/main/java/bisq/core/util/CoinUtil.java @@ -23,6 +23,7 @@ public class CoinUtil { + // Get the fee per amount public static Coin getFeePerBtc(Coin feePerBtc, Coin amount) { double feePerBtcAsDouble = feePerBtc != null ? (double) feePerBtc.value : 0; double amountAsDouble = amount != null ? (double) amount.value : 0; From 2375880d3c9793281afb76e133b71b5debfe92a7 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Wed, 5 Jun 2019 19:26:04 +0200 Subject: [PATCH 11/33] Change message box aid --- core/src/main/resources/i18n/displayStrings.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 231c4747806..2e27ada6494 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -841,7 +841,7 @@ support.attachment=Attachment support.tooManyAttachments=You cannot send more than 3 attachments in one message. support.save=Save file to disk support.messages=Messages -support.input.prompt=Please enter here your message to the arbitrator +support.input.prompt=Message... support.send=Send support.addAttachments=Add attachments support.closeTicket=Close ticket From 13f265122b764123cac29130d3727fa034b98f67 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Thu, 6 Jun 2019 00:33:32 +0200 Subject: [PATCH 12/33] Add basic chat UI Very basic switch between chat and overview per trade, improvements needed on the UI. --- .../java/bisq/desktop/main/Chat/Chat.java | 20 +++++---- .../pendingtrades/PendingTradesView.java | 6 +-- .../portfolio/pendingtrades/TradeSubView.java | 44 ++++++++++++++----- 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java index 918205cbe8b..373c829f22a 100644 --- a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java +++ b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java @@ -117,12 +117,15 @@ public class Chat extends AnchorPane { private TableGroupHeadline tableGroupHeadline; private VBox messagesInputBox; + // Options @Getter Button extraButton; @Getter private ReadOnlyDoubleProperty widthProperty; @Setter boolean allowAttachments; + @Setter + boolean displayHeader; // Communication stuff, to be renamed to something more generic private final P2PService p2PService; @@ -143,6 +146,7 @@ public Chat(ChatManager chatManager, BSFormatter formatter) { this.formatter = formatter; this.p2PService = chatManager.getP2PService(); allowAttachments = true; + displayHeader = true; } public void initialize() { @@ -215,6 +219,9 @@ public void display(ChatSession chatSession, @Nullable Button extraButton, sendMsgBusyAnimation = new BusyAnimation(false); + if (displayHeader) + this.getChildren().add(tableGroupHeadline); + if (chatSession.chatIsOpen()) { HBox buttonBox = new HBox(); buttonBox.setSpacing(10); @@ -241,17 +248,13 @@ public void display(ChatSession chatSession, @Nullable Button extraButton, AnchorPane.setBottomAnchor(messageListView, 120d); - this.getChildren().addAll(tableGroupHeadline, messageListView, messagesInputBox); - } else - - { + this.getChildren().addAll(messageListView, messagesInputBox); + } else { AnchorPane.setBottomAnchor(messageListView, 0d); - this.getChildren().addAll(tableGroupHeadline, messageListView); + this.getChildren().add(messageListView); } - messageListView.setCellFactory(new Callback<>() - - { + messageListView.setCellFactory(new Callback<>() { @Override public ListCell call(ListView list) { return new ListCell<>() { @@ -720,6 +723,7 @@ public void setInputBoxVisible(boolean visible) { public void removeInputBox() { this.getChildren().remove(messagesInputBox); } + /////////////////////////////////////////////////////////////////////////////////////////// // Bindings /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index a512a154636..12afcd67468 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -35,7 +35,6 @@ import bisq.core.util.BSFormatter; import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.P2PService; import bisq.common.UserThread; import bisq.common.util.Utilities; @@ -78,7 +77,6 @@ public class PendingTradesView extends ActivatableViewAndModel tableView; @@ -105,14 +103,12 @@ public PendingTradesView(PendingTradesViewModel model, BSFormatter formatter, PrivateNotificationManager privateNotificationManager, Preferences preferences, - P2PService p2PService, @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(model); this.tradeDetailsWindow = tradeDetailsWindow; this.formatter = formatter; this.privateNotificationManager = privateNotificationManager; this.preferences = preferences; - this.p2PService = p2PService; this.useDevPrivilegeKeys = useDevPrivilegeKeys; } @@ -192,6 +188,7 @@ public void initialize() { tradeChat = new Chat(model.dataModel.tradeManager.getChatManager(), formatter); tradeChat.setAllowAttachments(false); + tradeChat.setDisplayHeader(false); tradeChat.initialize(); } @@ -243,7 +240,6 @@ else if (root.getChildren().size() > 1) model.dataModel.tradeManager.getChatManager()), null, selectedSubView.getLeftVBox().widthProperty()); -// root.widthProperty()); selectedSubView.setChat(tradeChat); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java index 8bad0fdcf83..4ba85ae7c0b 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java @@ -29,6 +29,7 @@ import javafx.scene.control.Label; import javafx.scene.control.Separator; import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; @@ -55,14 +56,15 @@ public abstract class TradeSubView extends HBox { protected final PendingTradesViewModel model; @Getter protected VBox leftVBox; - protected AnchorPane contentPane; - protected TradeStepView tradeStepView; + private AnchorPane contentPane; + private TradeStepView tradeStepView; private AutoTooltipButton openDisputeButton; private NotificationGroup notificationGroup; - protected GridPane leftGridPane; - protected TitledGroupBg tradeProcessTitledGroupBg; - protected int leftGridPaneRowIndex = 0; - protected Subscription viewStateSubscription; + private GridPane leftGridPane; + private TitledGroupBg tradeProcessTitledGroupBg; + private int leftGridPaneRowIndex = 0; + Subscription viewStateSubscription; + private Chat chat; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation @@ -96,6 +98,15 @@ private void buildViews() { addLeftBox(); addContentPane(); + GridPane titleGridPane = new GridPane(); + leftVBox.getChildren().add(titleGridPane); + leftVBox.setPrefWidth(340); + ColumnConstraints cc1 = new ColumnConstraints(); + cc1.setPrefWidth(150); + ColumnConstraints cc2 = new ColumnConstraints(); + cc2.setPrefWidth(150); + titleGridPane.getColumnConstraints().addAll(cc1, cc2); + leftGridPane = new GridPane(); leftGridPane.setPrefWidth(340); leftGridPane.setHgap(Layout.GRID_GAP); @@ -103,10 +114,23 @@ private void buildViews() { VBox.setMargin(leftGridPane, new Insets(0, 10, 10, 10)); leftVBox.getChildren().add(leftGridPane); - leftGridPaneRowIndex = 0; - tradeProcessTitledGroupBg = addTitledGroupBg(leftGridPane, leftGridPaneRowIndex, 1, Res.get("portfolio.pending.tradeProcess")); - tradeProcessTitledGroupBg.getStyleClass().add("last"); + tradeProcessTitledGroupBg = addTitledGroupBg(titleGridPane, 0, 0, 1, Res.get("portfolio.pending.tradeProcess")); + tradeProcessTitledGroupBg.getStyleClass().addAll("last", "show-hand"); + tradeProcessTitledGroupBg.setOnMouseClicked(e -> { + leftVBox.setPrefWidth(340); + leftVBox.getChildren().set(1, leftGridPane); + }); + TitledGroupBg chatTitleGroupBg = addTitledGroupBg(titleGridPane, 0, 1, 1, "Chat"); + chatTitleGroupBg.getStyleClass().addAll("last", "show-hand"); + chatTitleGroupBg.setOnMouseClicked(e -> { + if (chat == null) + return; + VBox.setMargin(chat, new Insets(11, 10, 10, 10)); + leftVBox.setPrefWidth(640); + leftVBox.getChildren().set(1, chat); + }); + leftGridPaneRowIndex = 0; addWizards(); TitledGroupBg noticeTitledGroupBg = addTitledGroupBg(leftGridPane, leftGridPaneRowIndex, 1, "", @@ -196,7 +220,7 @@ private void createAndAddTradeStepView(Class viewClass) } public void setChat(Chat chat) { - leftVBox.getChildren().setAll(chat); + this.chat = chat; } private void addLeftBox() { From 5d338e693c4c5986a2261ee87889e2a427c6aca1 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 29 Aug 2019 17:07:39 +0200 Subject: [PATCH 13/33] Fix merge issues --- .../messages/DisputeCommunicationMessage.java | 49 +++++++++-------- core/src/main/java/bisq/core/trade/Trade.java | 54 +++++++++---------- 2 files changed, 51 insertions(+), 52 deletions(-) diff --git a/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java b/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java index 0158f5ea692..84d6854b73d 100644 --- a/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java +++ b/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java @@ -65,12 +65,12 @@ public enum Type { TRADE; public static DisputeCommunicationMessage.Type fromProto( - proto.DisputeCommunicationMessage.Type type) { + protobuf.DisputeCommunicationMessage.Type type) { return ProtoUtil.enumFromProto(DisputeCommunicationMessage.Type.class, type.name()); } - public static proto.DisputeCommunicationMessage.Type toProtoMessage(Type type) { - return proto.DisputeCommunicationMessage.Type.valueOf(type.name()); + public static protobuf.DisputeCommunicationMessage.Type toProtoMessage(Type type) { + return protobuf.DisputeCommunicationMessage.Type.valueOf(type.name()); } } @@ -160,10 +160,8 @@ private DisputeCommunicationMessage(Type type, } @Override - public proto.NetworkEnvelope toProtoNetworkEnvelope() { - proto.DisputeCommunicationMessage.Builder builder = proto.DisputeCommunicationMessage.newBuilder() - public proto.NetworkEnvelope toProtoNetworkEnvelope() { - proto.DisputeCommunicationMessage.Builder builder = proto.DisputeCommunicationMessage.newBuilder() + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + protobuf.DisputeCommunicationMessage.Builder builder = protobuf.DisputeCommunicationMessage.newBuilder() .setType(DisputeCommunicationMessage.Type.toProtoMessage(type)) .setTradeId(tradeId) .setTraderId(traderId) @@ -184,33 +182,34 @@ public proto.NetworkEnvelope toProtoNetworkEnvelope() { .build(); } - public static DisputeCommunicationMessage fromProto(protobuf.DisputeCommunicationMessage proto, int messageVersion) { + public static DisputeCommunicationMessage fromProto(protobuf.DisputeCommunicationMessage protobuf, + int messageVersion) { final DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage( - DisputeCommunicationMessage.Type.fromProto(proto.getType()), - proto.getTradeId(), - proto.getTraderId(), - proto.getSenderIsTrader(), - proto.getMessage(), - new ArrayList<>(proto.getAttachmentsList().stream().map(Attachment::fromProto).collect(Collectors.toList())), - NodeAddress.fromProto(proto.getSenderNodeAddress()), - proto.getDate(), - proto.getArrived(), - proto.getStoredInMailbox(), - proto.getUid(), + DisputeCommunicationMessage.Type.fromProto(protobuf.getType()), + protobuf.getTradeId(), + protobuf.getTraderId(), + protobuf.getSenderIsTrader(), + protobuf.getMessage(), + new ArrayList<>(protobuf.getAttachmentsList().stream().map(Attachment::fromProto).collect(Collectors.toList())), + NodeAddress.fromProto(protobuf.getSenderNodeAddress()), + protobuf.getDate(), + protobuf.getArrived(), + protobuf.getStoredInMailbox(), + protobuf.getUid(), messageVersion, - proto.getAcknowledged(), - proto.getSendMessageError().isEmpty() ? null : proto.getSendMessageError(), - proto.getAckError().isEmpty() ? null : proto.getAckError()); - disputeCommunicationMessage.setSystemMessage(proto.getIsSystemMessage()); + protobuf.getAcknowledged(), + protobuf.getSendMessageError().isEmpty() ? null : protobuf.getSendMessageError(), + protobuf.getAckError().isEmpty() ? null : protobuf.getAckError()); + disputeCommunicationMessage.setSystemMessage(protobuf.getIsSystemMessage()); return disputeCommunicationMessage; } - public static DisputeCommunicationMessage fromPayloadProto(protobuf.DisputeCommunicationMessage proto) { + public static DisputeCommunicationMessage fromPayloadProto(protobuf.DisputeCommunicationMessage protobuf) { // We have the case that an envelope got wrapped into a payload. // We don't check the message version here as it was checked in the carrier envelope already (in connection class) // Payloads don't have a message version and are also used for persistence // We set the value to -1 to indicate it is set but irrelevant - return fromProto(proto, -1); + return fromProto(protobuf, -1); } diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index a4f431a6c89..c7119cc537c 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -434,9 +434,9 @@ public Message toProtoMessage() { .setProcessModel(processModel.toProtoMessage()) .setTradeAmountAsLong(tradeAmountAsLong) .setTradePrice(tradePrice) - .setState(proto.Trade.State.valueOf(state.name())) - .setDisputeState(proto.Trade.DisputeState.valueOf(disputeState.name())) - .setTradePeriodState(proto.Trade.TradePeriodState.valueOf(tradePeriodState.name())) + .setState(protobuf.Trade.State.valueOf(state.name())) + .setDisputeState(protobuf.Trade.DisputeState.valueOf(disputeState.name())) + .setTradePeriodState(protobuf.Trade.TradePeriodState.valueOf(tradePeriodState.name())) .addAllCommunicationMessages(communicationMessages.stream() .map(msg -> msg.toProtoNetworkEnvelope().getDisputeCommunicationMessage()) .collect(Collectors.toList())); @@ -461,30 +461,30 @@ public Message toProtoMessage() { return builder.build(); } - public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolver coreProtoResolver) { - trade.setTakeOfferDate(proto.getTakeOfferDate()); - trade.setProcessModel(ProcessModel.fromProto(proto.getProcessModel(), coreProtoResolver)); - trade.setState(State.fromProto(proto.getState())); - trade.setDisputeState(DisputeState.fromProto(proto.getDisputeState())); - trade.setTradePeriodState(TradePeriodState.fromProto(proto.getTradePeriodState())); - trade.setTakerFeeTxId(ProtoUtil.stringOrNullFromProto(proto.getTakerFeeTxId())); - trade.setDepositTxId(ProtoUtil.stringOrNullFromProto(proto.getDepositTxId())); - trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId())); - trade.setContract(proto.hasContract() ? Contract.fromProto(proto.getContract(), coreProtoResolver) : null); - trade.setContractAsJson(ProtoUtil.stringOrNullFromProto(proto.getContractAsJson())); - trade.setContractHash(ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash())); - trade.setTakerContractSignature(ProtoUtil.stringOrNullFromProto(proto.getTakerContractSignature())); - trade.setMakerContractSignature(ProtoUtil.stringOrNullFromProto(proto.getMakerContractSignature())); - trade.setArbitratorNodeAddress(proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null); - trade.setMediatorNodeAddress(proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null); - trade.setArbitratorBtcPubKey(ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorBtcPubKey())); - trade.setTakerPaymentAccountId(ProtoUtil.stringOrNullFromProto(proto.getTakerPaymentAccountId())); - trade.setErrorMessage(ProtoUtil.stringOrNullFromProto(proto.getErrorMessage())); - trade.setArbitratorPubKeyRing(proto.hasArbitratorPubKeyRing() ? PubKeyRing.fromProto(proto.getArbitratorPubKeyRing()) : null); - trade.setMediatorPubKeyRing(proto.hasMediatorPubKeyRing() ? PubKeyRing.fromProto(proto.getMediatorPubKeyRing()) : null); - trade.setCounterCurrencyTxId(proto.getCounterCurrencyTxId().isEmpty() ? null : proto.getCounterCurrencyTxId()); - - trade.communicationMessages.addAll(proto.getCommunicationMessagesList().stream() + public static Trade fromProto(Trade trade, protobuf.Trade protobuf, CoreProtoResolver coreProtoResolver) { + trade.setTakeOfferDate(protobuf.getTakeOfferDate()); + trade.setProcessModel(ProcessModel.fromProto(protobuf.getProcessModel(), coreProtoResolver)); + trade.setState(State.fromProto(protobuf.getState())); + trade.setDisputeState(DisputeState.fromProto(protobuf.getDisputeState())); + trade.setTradePeriodState(TradePeriodState.fromProto(protobuf.getTradePeriodState())); + trade.setTakerFeeTxId(ProtoUtil.stringOrNullFromProto(protobuf.getTakerFeeTxId())); + trade.setDepositTxId(ProtoUtil.stringOrNullFromProto(protobuf.getDepositTxId())); + trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(protobuf.getPayoutTxId())); + trade.setContract(protobuf.hasContract() ? Contract.fromProto(protobuf.getContract(), coreProtoResolver) : null); + trade.setContractAsJson(ProtoUtil.stringOrNullFromProto(protobuf.getContractAsJson())); + trade.setContractHash(ProtoUtil.byteArrayOrNullFromProto(protobuf.getContractHash())); + trade.setTakerContractSignature(ProtoUtil.stringOrNullFromProto(protobuf.getTakerContractSignature())); + trade.setMakerContractSignature(ProtoUtil.stringOrNullFromProto(protobuf.getMakerContractSignature())); + trade.setArbitratorNodeAddress(protobuf.hasArbitratorNodeAddress() ? NodeAddress.fromProto(protobuf.getArbitratorNodeAddress()) : null); + trade.setMediatorNodeAddress(protobuf.hasMediatorNodeAddress() ? NodeAddress.fromProto(protobuf.getMediatorNodeAddress()) : null); + trade.setArbitratorBtcPubKey(ProtoUtil.byteArrayOrNullFromProto(protobuf.getArbitratorBtcPubKey())); + trade.setTakerPaymentAccountId(ProtoUtil.stringOrNullFromProto(protobuf.getTakerPaymentAccountId())); + trade.setErrorMessage(ProtoUtil.stringOrNullFromProto(protobuf.getErrorMessage())); + trade.setArbitratorPubKeyRing(protobuf.hasArbitratorPubKeyRing() ? PubKeyRing.fromProto(protobuf.getArbitratorPubKeyRing()) : null); + trade.setMediatorPubKeyRing(protobuf.hasMediatorPubKeyRing() ? PubKeyRing.fromProto(protobuf.getMediatorPubKeyRing()) : null); + trade.setCounterCurrencyTxId(protobuf.getCounterCurrencyTxId().isEmpty() ? null : protobuf.getCounterCurrencyTxId()); + + trade.communicationMessages.addAll(protobuf.getCommunicationMessagesList().stream() .map(DisputeCommunicationMessage::fromPayloadProto) .collect(Collectors.toList())); From cb673764df152c5830f2faf8b86b17ccbef75bd6 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 29 Aug 2019 17:13:57 +0200 Subject: [PATCH 14/33] Update string --- core/src/main/resources/i18n/displayStrings.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index f16e74b8d79..343dce38adb 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -846,7 +846,7 @@ support.attachment=Attachment support.tooManyAttachments=You cannot send more than 3 attachments in one message. support.save=Save file to disk support.messages=Messages -support.input.prompt=Message... +support.input.prompt=Enter message... support.send=Send support.addAttachments=Add attachments support.closeTicket=Close ticket From 8e3a3514a34d4b9e5a7833bd136cce55ed46fe10 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 29 Aug 2019 17:14:56 +0200 Subject: [PATCH 15/33] Add comments, cleanup, fix wrong param name --- .../core/arbitration/DisputeChatSession.java | 28 ++++++----- .../bisq/core/arbitration/DisputeManager.java | 5 +- .../messages/DisputeCommunicationMessage.java | 49 +++++++++++-------- core/src/main/java/bisq/core/trade/Trade.java | 48 +++++++++--------- .../bisq/core/trade/TradeChatSession.java | 7 ++- 5 files changed, 72 insertions(+), 65 deletions(-) diff --git a/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java b/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java index d853284e767..9573b6a33f1 100644 --- a/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java +++ b/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java @@ -96,10 +96,12 @@ public boolean chatIsOpen() { return !dispute.isClosed(); } + /////////////////////////////////////////////////////////////////////////////////////////// // Not dependent on selected dispute /////////////////////////////////////////////////////////////////////////////////////////// + @Nullable @Override public NodeAddress getPeerNodeAddress(DisputeCommunicationMessage message) { Optional disputeOptional = disputeManager.findDispute(message.getTradeId(), message.getTraderId()); @@ -111,6 +113,7 @@ public NodeAddress getPeerNodeAddress(DisputeCommunicationMessage message) { return disputeManager.getNodeAddressPubKeyRingTuple(disputeOptional.get()).first; } + @Nullable @Override public PubKeyRing getPeerPubKeyRing(DisputeCommunicationMessage message) { Optional disputeOptional = disputeManager.findDispute(message.getTradeId(), message.getTraderId()); @@ -128,23 +131,23 @@ public void dispatchMessage(DisputeMessage message) { log.info("Received {} with tradeId {} and uid {}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); - if (message instanceof OpenNewDisputeMessage) + if (message instanceof OpenNewDisputeMessage) { disputeManager.onOpenNewDisputeMessage((OpenNewDisputeMessage) message); - else if (message instanceof PeerOpenedDisputeMessage) + } else if (message instanceof PeerOpenedDisputeMessage) { disputeManager.onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message); - else if (message instanceof DisputeCommunicationMessage) { - if (((DisputeCommunicationMessage)message).getType() != DisputeCommunicationMessage.Type.DISPUTE){ - log.debug("Ignore non distpute type communication message"); + } else if (message instanceof DisputeCommunicationMessage) { + if (((DisputeCommunicationMessage) message).getType() != DisputeCommunicationMessage.Type.DISPUTE) { + log.debug("Ignore non dispute type communication message"); return; } disputeManager.getChatManager().onDisputeDirectMessage((DisputeCommunicationMessage) message); - } - else if (message instanceof DisputeResultMessage) + } else if (message instanceof DisputeResultMessage) { disputeManager.onDisputeResultMessage((DisputeResultMessage) message); - else if (message instanceof PeerPublishedDisputePayoutTxMessage) + } else if (message instanceof PeerPublishedDisputePayoutTxMessage) { disputeManager.onDisputedPayoutTxMessage((PeerPublishedDisputePayoutTxMessage) message); - else + } else { log.warn("Unsupported message at dispatchMessage.\nmessage=" + message); + } } @Override @@ -162,14 +165,13 @@ public boolean channelOpen(DisputeCommunicationMessage message) { @Override public void storeDisputeCommunicationMessage(DisputeCommunicationMessage message) { Optional disputeOptional = disputeManager.findDispute(message.getTradeId(), message.getTraderId()); - if (disputeOptional.isPresent()) { - if (disputeOptional.get().getDisputeCommunicationMessages().stream() - .noneMatch(m -> m.getUid().equals(message.getUid()))) + if (disputeOptional.get().getDisputeCommunicationMessages().stream().noneMatch(m -> m.getUid().equals(message.getUid()))) { disputeOptional.get().addDisputeCommunicationMessage(message); - else + } else { log.warn("We got a disputeCommunicationMessage what we have already stored. UId = {} TradeId = {}", message.getUid(), message.getTradeId()); + } } } } diff --git a/core/src/main/java/bisq/core/arbitration/DisputeManager.java b/core/src/main/java/bisq/core/arbitration/DisputeManager.java index 2a1f9fc438f..4f09586e478 100644 --- a/core/src/main/java/bisq/core/arbitration/DisputeManager.java +++ b/core/src/main/java/bisq/core/arbitration/DisputeManager.java @@ -70,8 +70,6 @@ import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; -import java.io.File; - import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -103,8 +101,6 @@ public class DisputeManager implements PersistedDataHost { @Getter private DisputeList disputes; private final String disputeInfo; - // private final CopyOnWriteArraySet decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>(); -// private final CopyOnWriteArraySet decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>(); private final Map openDisputes; private final Map closedDisputes; private final Map delayMsgMap = new HashMap<>(); @@ -632,6 +628,7 @@ public void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisput chatManager.sendAckMessage(peerOpenedDisputeMessage, dispute.getArbitratorPubKeyRing(), errorMessage == null, errorMessage); } + // We get that message at both peers. The dispute object is in context of the trader public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { String errorMessage = null; diff --git a/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java b/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java index 84d6854b73d..e4cddb64d36 100644 --- a/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java +++ b/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java @@ -78,6 +78,7 @@ public interface Listener { void onMessageStateChanged(); } + // Added with v1.1.6. Old clients will not have set that field and we fall back to entry 0 which is DISPUTE. private final DisputeCommunicationMessage.Type type; private final String tradeId; private final int traderId; @@ -182,40 +183,44 @@ public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { .build(); } - public static DisputeCommunicationMessage fromProto(protobuf.DisputeCommunicationMessage protobuf, + public static DisputeCommunicationMessage fromProto(protobuf.DisputeCommunicationMessage proto, int messageVersion) { + // If we get a msg from an old client type will be ordinal 0 which is the dispute entry and as we only added + // the trade case it is the desired behaviour. + DisputeCommunicationMessage.Type type = DisputeCommunicationMessage.Type.fromProto(proto.getType()); final DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage( - DisputeCommunicationMessage.Type.fromProto(protobuf.getType()), - protobuf.getTradeId(), - protobuf.getTraderId(), - protobuf.getSenderIsTrader(), - protobuf.getMessage(), - new ArrayList<>(protobuf.getAttachmentsList().stream().map(Attachment::fromProto).collect(Collectors.toList())), - NodeAddress.fromProto(protobuf.getSenderNodeAddress()), - protobuf.getDate(), - protobuf.getArrived(), - protobuf.getStoredInMailbox(), - protobuf.getUid(), + type, + proto.getTradeId(), + proto.getTraderId(), + proto.getSenderIsTrader(), + proto.getMessage(), + new ArrayList<>(proto.getAttachmentsList().stream().map(Attachment::fromProto).collect(Collectors.toList())), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getDate(), + proto.getArrived(), + proto.getStoredInMailbox(), + proto.getUid(), messageVersion, - protobuf.getAcknowledged(), - protobuf.getSendMessageError().isEmpty() ? null : protobuf.getSendMessageError(), - protobuf.getAckError().isEmpty() ? null : protobuf.getAckError()); - disputeCommunicationMessage.setSystemMessage(protobuf.getIsSystemMessage()); + proto.getAcknowledged(), + proto.getSendMessageError().isEmpty() ? null : proto.getSendMessageError(), + proto.getAckError().isEmpty() ? null : proto.getAckError()); + disputeCommunicationMessage.setSystemMessage(proto.getIsSystemMessage()); return disputeCommunicationMessage; } - public static DisputeCommunicationMessage fromPayloadProto(protobuf.DisputeCommunicationMessage protobuf) { + public static DisputeCommunicationMessage fromPayloadProto(protobuf.DisputeCommunicationMessage proto) { // We have the case that an envelope got wrapped into a payload. // We don't check the message version here as it was checked in the carrier envelope already (in connection class) // Payloads don't have a message version and are also used for persistence // We set the value to -1 to indicate it is set but irrelevant - return fromProto(protobuf, -1); + return fromProto(proto, -1); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// + public void addAllAttachments(List attachments) { this.attachments.addAll(attachments); } @@ -280,8 +285,12 @@ public void addWeakMessageStateListener(Listener listener) { } private void notifyChangeListener() { - if (listener != null && listener.get() != null) - listener.get().onMessageStateChanged(); + if (listener != null) { + Listener listener = this.listener.get(); + if (listener != null) { + listener.onMessageStateChanged(); + } + } } @Override diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index c7119cc537c..2a4ae411d50 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -461,30 +461,30 @@ public Message toProtoMessage() { return builder.build(); } - public static Trade fromProto(Trade trade, protobuf.Trade protobuf, CoreProtoResolver coreProtoResolver) { - trade.setTakeOfferDate(protobuf.getTakeOfferDate()); - trade.setProcessModel(ProcessModel.fromProto(protobuf.getProcessModel(), coreProtoResolver)); - trade.setState(State.fromProto(protobuf.getState())); - trade.setDisputeState(DisputeState.fromProto(protobuf.getDisputeState())); - trade.setTradePeriodState(TradePeriodState.fromProto(protobuf.getTradePeriodState())); - trade.setTakerFeeTxId(ProtoUtil.stringOrNullFromProto(protobuf.getTakerFeeTxId())); - trade.setDepositTxId(ProtoUtil.stringOrNullFromProto(protobuf.getDepositTxId())); - trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(protobuf.getPayoutTxId())); - trade.setContract(protobuf.hasContract() ? Contract.fromProto(protobuf.getContract(), coreProtoResolver) : null); - trade.setContractAsJson(ProtoUtil.stringOrNullFromProto(protobuf.getContractAsJson())); - trade.setContractHash(ProtoUtil.byteArrayOrNullFromProto(protobuf.getContractHash())); - trade.setTakerContractSignature(ProtoUtil.stringOrNullFromProto(protobuf.getTakerContractSignature())); - trade.setMakerContractSignature(ProtoUtil.stringOrNullFromProto(protobuf.getMakerContractSignature())); - trade.setArbitratorNodeAddress(protobuf.hasArbitratorNodeAddress() ? NodeAddress.fromProto(protobuf.getArbitratorNodeAddress()) : null); - trade.setMediatorNodeAddress(protobuf.hasMediatorNodeAddress() ? NodeAddress.fromProto(protobuf.getMediatorNodeAddress()) : null); - trade.setArbitratorBtcPubKey(ProtoUtil.byteArrayOrNullFromProto(protobuf.getArbitratorBtcPubKey())); - trade.setTakerPaymentAccountId(ProtoUtil.stringOrNullFromProto(protobuf.getTakerPaymentAccountId())); - trade.setErrorMessage(ProtoUtil.stringOrNullFromProto(protobuf.getErrorMessage())); - trade.setArbitratorPubKeyRing(protobuf.hasArbitratorPubKeyRing() ? PubKeyRing.fromProto(protobuf.getArbitratorPubKeyRing()) : null); - trade.setMediatorPubKeyRing(protobuf.hasMediatorPubKeyRing() ? PubKeyRing.fromProto(protobuf.getMediatorPubKeyRing()) : null); - trade.setCounterCurrencyTxId(protobuf.getCounterCurrencyTxId().isEmpty() ? null : protobuf.getCounterCurrencyTxId()); - - trade.communicationMessages.addAll(protobuf.getCommunicationMessagesList().stream() + public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolver coreProtoResolver) { + trade.setTakeOfferDate(proto.getTakeOfferDate()); + trade.setProcessModel(ProcessModel.fromProto(proto.getProcessModel(), coreProtoResolver)); + trade.setState(State.fromProto(proto.getState())); + trade.setDisputeState(DisputeState.fromProto(proto.getDisputeState())); + trade.setTradePeriodState(TradePeriodState.fromProto(proto.getTradePeriodState())); + trade.setTakerFeeTxId(ProtoUtil.stringOrNullFromProto(proto.getTakerFeeTxId())); + trade.setDepositTxId(ProtoUtil.stringOrNullFromProto(proto.getDepositTxId())); + trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId())); + trade.setContract(proto.hasContract() ? Contract.fromProto(proto.getContract(), coreProtoResolver) : null); + trade.setContractAsJson(ProtoUtil.stringOrNullFromProto(proto.getContractAsJson())); + trade.setContractHash(ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash())); + trade.setTakerContractSignature(ProtoUtil.stringOrNullFromProto(proto.getTakerContractSignature())); + trade.setMakerContractSignature(ProtoUtil.stringOrNullFromProto(proto.getMakerContractSignature())); + trade.setArbitratorNodeAddress(proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null); + trade.setMediatorNodeAddress(proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null); + trade.setArbitratorBtcPubKey(ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorBtcPubKey())); + trade.setTakerPaymentAccountId(ProtoUtil.stringOrNullFromProto(proto.getTakerPaymentAccountId())); + trade.setErrorMessage(ProtoUtil.stringOrNullFromProto(proto.getErrorMessage())); + trade.setArbitratorPubKeyRing(proto.hasArbitratorPubKeyRing() ? PubKeyRing.fromProto(proto.getArbitratorPubKeyRing()) : null); + trade.setMediatorPubKeyRing(proto.hasMediatorPubKeyRing() ? PubKeyRing.fromProto(proto.getMediatorPubKeyRing()) : null); + trade.setCounterCurrencyTxId(proto.getCounterCurrencyTxId().isEmpty() ? null : proto.getCounterCurrencyTxId()); + + trade.communicationMessages.addAll(proto.getCommunicationMessagesList().stream() .map(DisputeCommunicationMessage::fromPayloadProto) .collect(Collectors.toList())); diff --git a/core/src/main/java/bisq/core/trade/TradeChatSession.java b/core/src/main/java/bisq/core/trade/TradeChatSession.java index 5d82551a03e..9235909521a 100644 --- a/core/src/main/java/bisq/core/trade/TradeChatSession.java +++ b/core/src/main/java/bisq/core/trade/TradeChatSession.java @@ -132,14 +132,14 @@ public void dispatchMessage(DisputeMessage message) { log.info("Received {} with tradeId {} and uid {}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); if (message instanceof DisputeCommunicationMessage) { - if (((DisputeCommunicationMessage)message).getType() != DisputeCommunicationMessage.Type.TRADE){ + if (((DisputeCommunicationMessage) message).getType() != DisputeCommunicationMessage.Type.TRADE) { log.debug("Ignore non trade type communication message"); return; } chatManager.onDisputeDirectMessage((DisputeCommunicationMessage) message); - } - else + } else { log.warn("Unsupported message at dispatchMessage.\nmessage=" + message); + } } @Override @@ -166,5 +166,4 @@ public void storeDisputeCommunicationMessage(DisputeCommunicationMessage message message.getUid(), message.getTradeId()); } } - } From 2189cba41b28c0ba4b594114163a22feb3386acd Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 29 Aug 2019 17:50:14 +0200 Subject: [PATCH 16/33] Show chat in popup instead of tab (WIP) --- .../resources/i18n/displayStrings.properties | 3 + .../pendingtrades/PendingTradesView.fxml | 1 + .../pendingtrades/PendingTradesView.java | 141 ++++++++++++++---- .../portfolio/pendingtrades/TradeSubView.java | 40 +---- 4 files changed, 123 insertions(+), 62 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 343dce38adb..d84b02bc25f 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -608,6 +608,9 @@ portfolio.pending.step2_seller.waitPayment.msg=The deposit transaction has at le portfolio.pending.step2_seller.warn=The BTC buyer still has not done the {0} payment.\nYou need to wait until they have started the payment.\nIf the trade has not been completed on {1} the arbitrator will investigate. portfolio.pending.step2_seller.openForDispute=The BTC buyer has not started his payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the arbitrator for opening a dispute. +portfolio.pending.chatWindowTitle=Chat window for trade with ID '{0}' +portfolio.pending.openChat=Open chat window + # suppress inspection "UnusedProperty" message.state.UNDEFINED=Undefined # suppress inspection "UnusedProperty" diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml index ae2a0e29c83..16a304db66d 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.fxml @@ -38,6 +38,7 @@ + diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 12afcd67468..3ac79aa9639 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -23,6 +23,7 @@ import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.components.PeerInfoIcon; import bisq.desktop.main.Chat.Chat; +import bisq.desktop.main.MainView; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; @@ -43,17 +44,28 @@ import javax.inject.Inject; +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; + import javafx.fxml.FXML; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.stage.Window; + import javafx.scene.Node; import javafx.scene.Scene; +import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; +import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.geometry.Insets; @@ -81,7 +93,8 @@ public class PendingTradesView extends ActivatableViewAndModel tableView; @FXML - TableColumn priceColumn, volumeColumn, amountColumn, avatarColumn, marketColumn, roleColumn, paymentMethodColumn, tradeIdColumn, dateColumn; + TableColumn priceColumn, volumeColumn, amountColumn, avatarColumn, + marketColumn, roleColumn, paymentMethodColumn, tradeIdColumn, dateColumn, chatColumn; private SortedList sortedList; private TradeSubView selectedSubView; @@ -90,7 +103,6 @@ public class PendingTradesView extends ActivatableViewAndModel 1) + else if (root.getChildren().size() == 2) root.getChildren().set(1, selectedSubView); - - boolean isTaker = !model.dataModel.isMaker(selectedItem.getTrade().getOffer()); - boolean isBuyer = model.dataModel.isBuyer(); - if (tradeChat != null) - tradeChat.display(new TradeChatSession(selectedItem.getTrade(), isTaker, isBuyer, - model.dataModel.tradeManager, - model.dataModel.tradeManager.getChatManager()), - null, - selectedSubView.getLeftVBox().widthProperty()); - - selectedSubView.setChat(tradeChat); } updateTableSelection(); @@ -262,10 +260,6 @@ else if (root.getChildren().size() > 1) }); updateTableSelection(); - if (tradeChat != null) { - tradeChat.activate(); - tradeChat.scrollToBottom(); - } } @Override @@ -278,9 +272,6 @@ protected void deactivate() { if (scene != null) scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); - - if (tradeChat != null) - tradeChat.deactivate(); } private void removeSelectedSubView() { @@ -531,5 +522,103 @@ public void updateItem(final PendingTradesListItem newItem, boolean empty) { }); return avatarColumn; } + + @SuppressWarnings("UnusedReturnValue") + private TableColumn setChatColumnCellFactory() { + chatColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); + //TODO + chatColumn.getStyleClass().addAll("last-column", "avatar-column"); + chatColumn.setSortable(false); + chatColumn.setCellFactory( + new Callback<>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + + @Override + public void updateItem(final PendingTradesListItem newItem, boolean empty) { + super.updateItem(newItem, empty); + if (!empty && newItem != null) { + Trade trade = newItem.getTrade(); + + Label label = new Label(); + //todo + // label.setLayoutY(-5); + //label.setLayoutX(10); + label.getStyleClass().addAll("icon", "highlight"); + Tooltip.install(label, new Tooltip(Res.get("portfolio.pending.openChat"))); + AwesomeDude.setIcon(label, AwesomeIcon.COMMENTS_ALT); + label.setOnMouseClicked(e -> openChat(trade)); + setPadding(new Insets(-20, 0, 0, 20)); + setGraphic(label); + } else { + setGraphic(null); + } + } + }; + } + }); + return chatColumn; + } + + private void openChat(Trade trade) { + Chat tradeChat = new Chat(model.dataModel.tradeManager.getChatManager(), formatter); + tradeChat.setAllowAttachments(false); + tradeChat.setDisplayHeader(false); + tradeChat.initialize(); + + AnchorPane pane = new AnchorPane(tradeChat); + pane.setPrefSize(600, 400); + AnchorPane.setLeftAnchor(tradeChat, 10d); + AnchorPane.setRightAnchor(tradeChat, 10d); + AnchorPane.setTopAnchor(tradeChat, -20d); + AnchorPane.setBottomAnchor(tradeChat, 10d); + + boolean isTaker = !model.dataModel.isMaker(trade.getOffer()); + boolean isBuyer = model.dataModel.isBuyer(); + tradeChat.display(new TradeChatSession(trade, isTaker, isBuyer, + model.dataModel.tradeManager, + model.dataModel.tradeManager.getChatManager()), + null, + pane.widthProperty()); + + tradeChat.activate(); + tradeChat.scrollToBottom(); + + Stage stage = new Stage(); + stage.setTitle(Res.get("portfolio.pending.chatWindowTitle", trade.getShortId())); + StackPane owner = MainView.getRootContainer(); + Scene rootScene = owner.getScene(); + stage.initOwner(rootScene.getWindow()); + stage.initModality(Modality.NONE); + stage.initStyle(StageStyle.DECORATED); + + Scene scene = new Scene(pane); + scene.getStylesheets().setAll( + "/bisq/desktop/bisq.css", + "/bisq/desktop/images.css"); + scene.setOnKeyPressed(ev -> { + if (ev.getCode() == KeyCode.ESCAPE) { + ev.consume(); + stage.hide(); + } + }); + stage.setScene(scene); + + stage.setOpacity(0); + stage.show(); + + //todo exit listener + + Window window = rootScene.getWindow(); + double titleBarHeight = window.getHeight() - rootScene.getHeight(); + stage.setX(Math.round(window.getX() + (owner.getWidth() - stage.getWidth() / 4 * 3))); + stage.setY(Math.round(window.getY() + titleBarHeight + (owner.getHeight() - stage.getHeight() / 4 * 3))); + + // Delay display to next render frame to avoid that the popup is first quickly displayed in default position + // and after a short moment in the correct position + UserThread.execute(() -> stage.setOpacity(1)); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java index 4ba85ae7c0b..0bc31f87dc9 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java @@ -19,7 +19,6 @@ import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.TitledGroupBg; -import bisq.desktop.main.Chat.Chat; import bisq.desktop.main.portfolio.pendingtrades.steps.TradeStepView; import bisq.desktop.main.portfolio.pendingtrades.steps.TradeWizardItem; import bisq.desktop.util.Layout; @@ -29,7 +28,6 @@ import javafx.scene.control.Label; import javafx.scene.control.Separator; import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; @@ -44,8 +42,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import lombok.Getter; - import static bisq.desktop.util.FormBuilder.addButtonAfterGroup; import static bisq.desktop.util.FormBuilder.addMultilineLabel; import static bisq.desktop.util.FormBuilder.addTitledGroupBg; @@ -54,7 +50,6 @@ public abstract class TradeSubView extends HBox { protected final Logger log = LoggerFactory.getLogger(this.getClass()); protected final PendingTradesViewModel model; - @Getter protected VBox leftVBox; private AnchorPane contentPane; private TradeStepView tradeStepView; @@ -63,8 +58,7 @@ public abstract class TradeSubView extends HBox { private GridPane leftGridPane; private TitledGroupBg tradeProcessTitledGroupBg; private int leftGridPaneRowIndex = 0; - Subscription viewStateSubscription; - private Chat chat; + private Subscription viewStateSubscription; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation @@ -98,15 +92,6 @@ private void buildViews() { addLeftBox(); addContentPane(); - GridPane titleGridPane = new GridPane(); - leftVBox.getChildren().add(titleGridPane); - leftVBox.setPrefWidth(340); - ColumnConstraints cc1 = new ColumnConstraints(); - cc1.setPrefWidth(150); - ColumnConstraints cc2 = new ColumnConstraints(); - cc2.setPrefWidth(150); - titleGridPane.getColumnConstraints().addAll(cc1, cc2); - leftGridPane = new GridPane(); leftGridPane.setPrefWidth(340); leftGridPane.setHgap(Layout.GRID_GAP); @@ -114,23 +99,10 @@ private void buildViews() { VBox.setMargin(leftGridPane, new Insets(0, 10, 10, 10)); leftVBox.getChildren().add(leftGridPane); - tradeProcessTitledGroupBg = addTitledGroupBg(titleGridPane, 0, 0, 1, Res.get("portfolio.pending.tradeProcess")); - tradeProcessTitledGroupBg.getStyleClass().addAll("last", "show-hand"); - tradeProcessTitledGroupBg.setOnMouseClicked(e -> { - leftVBox.setPrefWidth(340); - leftVBox.getChildren().set(1, leftGridPane); - }); - TitledGroupBg chatTitleGroupBg = addTitledGroupBg(titleGridPane, 0, 1, 1, "Chat"); - chatTitleGroupBg.getStyleClass().addAll("last", "show-hand"); - chatTitleGroupBg.setOnMouseClicked(e -> { - if (chat == null) - return; - VBox.setMargin(chat, new Insets(11, 10, 10, 10)); - leftVBox.setPrefWidth(640); - leftVBox.getChildren().set(1, chat); - }); - leftGridPaneRowIndex = 0; + tradeProcessTitledGroupBg = addTitledGroupBg(leftGridPane, leftGridPaneRowIndex, 1, Res.get("portfolio.pending.tradeProcess")); + tradeProcessTitledGroupBg.getStyleClass().add("last"); + addWizards(); TitledGroupBg noticeTitledGroupBg = addTitledGroupBg(leftGridPane, leftGridPaneRowIndex, 1, "", @@ -219,10 +191,6 @@ private void createAndAddTradeStepView(Class viewClass) } } - public void setChat(Chat chat) { - this.chat = chat; - } - private void addLeftBox() { leftVBox = new VBox(); leftVBox.setSpacing(Layout.SPACING_V_BOX); From bfb111da67aa4cc1842053dc8ae1868f8a415d07 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 29 Aug 2019 18:56:11 +0200 Subject: [PATCH 17/33] Add close handler to popup - Close already open chat if new one opens --- .../resources/i18n/displayStrings.properties | 2 +- .../pendingtrades/PendingTradesView.java | 40 ++++++++++--------- .../portfolio/pendingtrades/TradeSubView.java | 2 +- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index d84b02bc25f..8c76ab67e31 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -608,7 +608,7 @@ portfolio.pending.step2_seller.waitPayment.msg=The deposit transaction has at le portfolio.pending.step2_seller.warn=The BTC buyer still has not done the {0} payment.\nYou need to wait until they have started the payment.\nIf the trade has not been completed on {1} the arbitrator will investigate. portfolio.pending.step2_seller.openForDispute=The BTC buyer has not started his payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the arbitrator for opening a dispute. -portfolio.pending.chatWindowTitle=Chat window for trade with ID '{0}' +portfolio.pending.chatWindowTitle=Chat window for trade with ID ''{0}'' portfolio.pending.openChat=Open chat window # suppress inspection "UnusedProperty" diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 3ac79aa9639..48f87247100 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -90,19 +90,19 @@ public class PendingTradesView extends ActivatableViewAndModel tableView; @FXML TableColumn priceColumn, volumeColumn, amountColumn, avatarColumn, marketColumn, roleColumn, paymentMethodColumn, tradeIdColumn, dateColumn, chatColumn; - private SortedList sortedList; private TradeSubView selectedSubView; private EventHandler keyEventEventHandler; private Scene scene; private Subscription selectedTableItemSubscription; private Subscription selectedItemSubscription; - private final Preferences preferences; + private Stage chatPopupStage; /////////////////////////////////////////////////////////////////////////////////////////// @@ -563,6 +563,9 @@ public void updateItem(final PendingTradesListItem newItem, boolean empty) { } private void openChat(Trade trade) { + if (chatPopupStage != null) + chatPopupStage.close(); + Chat tradeChat = new Chat(model.dataModel.tradeManager.getChatManager(), formatter); tradeChat.setAllowAttachments(false); tradeChat.setDisplayHeader(false); @@ -586,39 +589,38 @@ private void openChat(Trade trade) { tradeChat.activate(); tradeChat.scrollToBottom(); - Stage stage = new Stage(); - stage.setTitle(Res.get("portfolio.pending.chatWindowTitle", trade.getShortId())); + chatPopupStage = new Stage(); + chatPopupStage.setTitle(Res.get("portfolio.pending.chatWindowTitle", trade.getShortId())); StackPane owner = MainView.getRootContainer(); Scene rootScene = owner.getScene(); - stage.initOwner(rootScene.getWindow()); - stage.initModality(Modality.NONE); - stage.initStyle(StageStyle.DECORATED); + chatPopupStage.initOwner(rootScene.getWindow()); + chatPopupStage.initModality(Modality.NONE); + chatPopupStage.initStyle(StageStyle.DECORATED); + chatPopupStage.setOnHiding(event -> tradeChat.deactivate()); Scene scene = new Scene(pane); scene.getStylesheets().setAll( "/bisq/desktop/bisq.css", "/bisq/desktop/images.css"); - scene.setOnKeyPressed(ev -> { + scene.addEventHandler(KeyEvent.KEY_RELEASED, ev -> { if (ev.getCode() == KeyCode.ESCAPE) { ev.consume(); - stage.hide(); + chatPopupStage.hide(); } }); - stage.setScene(scene); - - stage.setOpacity(0); - stage.show(); + chatPopupStage.setScene(scene); - //todo exit listener + chatPopupStage.setOpacity(0); + chatPopupStage.show(); - Window window = rootScene.getWindow(); - double titleBarHeight = window.getHeight() - rootScene.getHeight(); - stage.setX(Math.round(window.getX() + (owner.getWidth() - stage.getWidth() / 4 * 3))); - stage.setY(Math.round(window.getY() + titleBarHeight + (owner.getHeight() - stage.getHeight() / 4 * 3))); + Window rootSceneWindow = rootScene.getWindow(); + double titleBarHeight = rootSceneWindow.getHeight() - rootScene.getHeight(); + chatPopupStage.setX(Math.round(rootSceneWindow.getX() + (owner.getWidth() - chatPopupStage.getWidth() / 4 * 3))); + chatPopupStage.setY(Math.round(rootSceneWindow.getY() + titleBarHeight + (owner.getHeight() - chatPopupStage.getHeight() / 4 * 3))); // Delay display to next render frame to avoid that the popup is first quickly displayed in default position // and after a short moment in the correct position - UserThread.execute(() -> stage.setOpacity(1)); + UserThread.execute(() -> chatPopupStage.setOpacity(1)); } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java index 0bc31f87dc9..33c9d548595 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java @@ -58,7 +58,7 @@ public abstract class TradeSubView extends HBox { private GridPane leftGridPane; private TitledGroupBg tradeProcessTitledGroupBg; private int leftGridPaneRowIndex = 0; - private Subscription viewStateSubscription; + protected Subscription viewStateSubscription; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation From 29e957a7ca10718aa0f7846fe4c65235b1290918 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 29 Aug 2019 19:11:09 +0200 Subject: [PATCH 18/33] Apply changes from master --- desktop/src/main/java/bisq/desktop/main/Chat/Chat.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java index 373c829f22a..27c2b49c585 100644 --- a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java +++ b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java @@ -64,7 +64,6 @@ import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; -import javafx.scene.paint.Paint; import javafx.scene.text.TextAlignment; import javafx.geometry.Insets; @@ -465,8 +464,8 @@ private void updateMsgState(DisputeCommunicationMessage message) { boolean visible; AwesomeIcon icon = null; String text = null; - statusIcon.setTextFill(Paint.valueOf("#0f87c3")); - statusInfoLabel.setTextFill(Paint.valueOf("#0f87c3")); + statusIcon.getStyleClass().add("status-icon"); + statusInfoLabel.getStyleClass().add("status-icon"); statusHBox.setOpacity(1); log.debug("updateMsgState msg-{}, ack={}, arrived={}", message.getMessage(), message.acknowledgedProperty().get(), message.arrivedProperty().get()); @@ -479,8 +478,8 @@ private void updateMsgState(DisputeCommunicationMessage message) { visible = true; icon = AwesomeIcon.EXCLAMATION_SIGN; text = Res.get("support.error", message.ackErrorProperty().get()); - statusIcon.setTextFill(Paint.valueOf("#dd0000")); - statusInfoLabel.setTextFill(Paint.valueOf("#dd0000")); + statusIcon.getStyleClass().add("error-text"); + statusInfoLabel.getStyleClass().add("error-text"); } else if (message.arrivedProperty().get()) { visible = true; icon = AwesomeIcon.OK; From 855cb4f6203653ba2a245c464d111b67f7db6e34 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 29 Aug 2019 21:40:32 +0200 Subject: [PATCH 19/33] Add missing allServicesInitialized check --- .../java/bisq/core/arbitration/DisputeManager.java | 4 +--- core/src/main/java/bisq/core/chat/ChatManager.java | 11 +++++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/bisq/core/arbitration/DisputeManager.java b/core/src/main/java/bisq/core/arbitration/DisputeManager.java index 4f09586e478..ddb7f1cb7c7 100644 --- a/core/src/main/java/bisq/core/arbitration/DisputeManager.java +++ b/core/src/main/java/bisq/core/arbitration/DisputeManager.java @@ -108,8 +108,6 @@ public class DisputeManager implements PersistedDataHost { private final Map disputeIsClosedSubscriptionsMap = new HashMap<>(); @Getter private final IntegerProperty numOpenDisputes = new SimpleIntegerProperty(); - private boolean servicesInitialized; - @Getter private final ChatManager chatManager; @@ -160,7 +158,7 @@ public void readPersisted() { } public void onAllServicesInitialized() { - servicesInitialized = true; + chatManager.onAllServicesInitialized(); p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onUpdatedDataReceived() { diff --git a/core/src/main/java/bisq/core/chat/ChatManager.java b/core/src/main/java/bisq/core/chat/ChatManager.java index cba88500a9a..f03f846ae6e 100644 --- a/core/src/main/java/bisq/core/chat/ChatManager.java +++ b/core/src/main/java/bisq/core/chat/ChatManager.java @@ -59,6 +59,7 @@ public class ChatManager { private final CopyOnWriteArraySet decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>(); + private boolean allServicesInitialized; public ChatManager(P2PService p2PService, WalletsSetup walletsSetup @@ -77,13 +78,18 @@ public ChatManager(P2PService p2PService, }); } + public void onAllServicesInitialized() { + allServicesInitialized = true; + } + public void tryApplyMessages() { if (isReadyForTxBroadcast()) applyMessages(); } private boolean isReadyForTxBroadcast() { - return p2PService.isBootstrapped() && + return allServicesInitialized && + p2PService.isBootstrapped() && walletsSetup.isDownloadComplete() && walletsSetup.hasSufficientPeersForBroadcast(); } @@ -139,7 +145,8 @@ public void onDisputeDirectMessage(DisputeCommunicationMessage disputeCommunicat sendAckMessage(disputeCommunicationMessage, receiverPubKeyRing, true, null); } - private void processAckMessage(AckMessage ackMessage, @Nullable DecryptedMessageWithPubKey decryptedMessageWithPubKey) { + private void processAckMessage(AckMessage ackMessage, + @Nullable DecryptedMessageWithPubKey decryptedMessageWithPubKey) { if (ackMessage.getSourceType() == AckMessageSourceType.DISPUTE_MESSAGE) { if (ackMessage.isSuccess()) { log.info("Received AckMessage for {} with tradeId {} and uid {}", From e0c1a85406ece6c21af9d461bccd70825d2fec96 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 29 Aug 2019 22:29:36 +0200 Subject: [PATCH 20/33] Add wasDisplayed field to chatMsg - Fix logger - Manage display state off trade chat msg (wip) --- common/src/main/proto/pb.proto | 1 + .../core/arbitration/DisputeChatSession.java | 2 +- .../messages/DisputeCommunicationMessage.java | 14 +- .../main/java/bisq/core/chat/ChatManager.java | 3 +- .../bisq/core/trade/TradeChatSession.java | 8 +- .../pendingtrades/PendingTradesView.java | 151 +++++++++++------- 6 files changed, 108 insertions(+), 71 deletions(-) diff --git a/common/src/main/proto/pb.proto b/common/src/main/proto/pb.proto index 94acf606f46..d5f3d0ee003 100644 --- a/common/src/main/proto/pb.proto +++ b/common/src/main/proto/pb.proto @@ -301,6 +301,7 @@ message DisputeCommunicationMessage { bool acknowledged = 13; string ack_error = 14; Type type = 15; + bool was_displayed = 16; } message DisputeResultMessage { diff --git a/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java b/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java index 9573b6a33f1..79847f84241 100644 --- a/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java +++ b/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java @@ -41,7 +41,7 @@ import javax.annotation.Nullable; public class DisputeChatSession extends ChatSession { - private static final Logger log = LoggerFactory.getLogger(DisputeManager.class); + private static final Logger log = LoggerFactory.getLogger(DisputeChatSession.class); private Dispute dispute; private DisputeManager disputeManager; diff --git a/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java b/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java index e4cddb64d36..c09c6132304 100644 --- a/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java +++ b/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java @@ -92,6 +92,10 @@ public interface Listener { @Setter private boolean isSystemMessage; + // Added in v1.1.6. + @Setter + private boolean wasDisplayed; + private final BooleanProperty arrivedProperty; private final BooleanProperty storedInMailboxProperty; private final BooleanProperty acknowledgedProperty; @@ -120,7 +124,8 @@ public DisputeCommunicationMessage(DisputeCommunicationMessage.Type type, Version.getP2PMessageVersion(), false, null, - null); + null, + false); } @@ -142,13 +147,15 @@ private DisputeCommunicationMessage(Type type, int messageVersion, boolean acknowledged, @Nullable String sendMessageError, - @Nullable String ackError) { + @Nullable String ackError, + boolean wasDisplayed) { super(messageVersion, uid); this.type = type; this.tradeId = tradeId; this.traderId = traderId; this.senderIsTrader = senderIsTrader; this.message = message; + this.wasDisplayed = wasDisplayed; Optional.ofNullable(attachments).ifPresent(e -> addAllAttachments(attachments)); this.senderNodeAddress = senderNodeAddress; this.date = date; @@ -203,7 +210,8 @@ public static DisputeCommunicationMessage fromProto(protobuf.DisputeCommunicatio messageVersion, proto.getAcknowledged(), proto.getSendMessageError().isEmpty() ? null : proto.getSendMessageError(), - proto.getAckError().isEmpty() ? null : proto.getAckError()); + proto.getAckError().isEmpty() ? null : proto.getAckError(), + proto.getWasDisplayed()); disputeCommunicationMessage.setSystemMessage(proto.getIsSystemMessage()); return disputeCommunicationMessage; } diff --git a/core/src/main/java/bisq/core/chat/ChatManager.java b/core/src/main/java/bisq/core/chat/ChatManager.java index f03f846ae6e..0c89c10ab92 100644 --- a/core/src/main/java/bisq/core/chat/ChatManager.java +++ b/core/src/main/java/bisq/core/chat/ChatManager.java @@ -17,7 +17,6 @@ package bisq.core.chat; -import bisq.core.arbitration.DisputeManager; import bisq.core.arbitration.messages.DisputeCommunicationMessage; import bisq.core.arbitration.messages.DisputeMessage; import bisq.core.btc.setup.WalletsSetup; @@ -47,7 +46,7 @@ import javax.annotation.Nullable; public class ChatManager { - private static final Logger log = LoggerFactory.getLogger(DisputeManager.class); + private static final Logger log = LoggerFactory.getLogger(ChatManager.class); @Getter private final P2PService p2PService; diff --git a/core/src/main/java/bisq/core/trade/TradeChatSession.java b/core/src/main/java/bisq/core/trade/TradeChatSession.java index 9235909521a..7f6f4fd61c9 100644 --- a/core/src/main/java/bisq/core/trade/TradeChatSession.java +++ b/core/src/main/java/bisq/core/trade/TradeChatSession.java @@ -17,7 +17,6 @@ package bisq.core.trade; -import bisq.core.arbitration.DisputeManager; import bisq.core.arbitration.messages.DisputeCommunicationMessage; import bisq.core.arbitration.messages.DisputeMessage; import bisq.core.chat.ChatManager; @@ -40,7 +39,7 @@ * sessions. This is only to make it easier to understand who's who, there is no real * server/client relationship */ public class TradeChatSession extends ChatSession { - private static final Logger log = LoggerFactory.getLogger(DisputeManager.class); + private static final Logger log = LoggerFactory.getLogger(TradeChatSession.class); private Trade trade; private boolean isClient; @@ -159,11 +158,12 @@ public void storeDisputeCommunicationMessage(DisputeCommunicationMessage message Optional tradeOptional = tradeManager.getTradeById(message.getTradeId()); if (tradeOptional.isPresent()) { if (tradeOptional.get().getCommunicationMessages().stream() - .noneMatch(m -> m.getUid().equals(message.getUid()))) + .noneMatch(m -> m.getUid().equals(message.getUid()))) { tradeOptional.get().addCommunicationMessage(message); - else + } else { log.warn("Trade got a disputeCommunicationMessage what we have already stored. UId = {} TradeId = {}", message.getUid(), message.getTradeId()); + } } } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 48f87247100..6be5129910c 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -77,11 +77,14 @@ import javafx.event.EventHandler; +import javafx.collections.ListChangeListener; import javafx.collections.transformation.SortedList; import javafx.util.Callback; import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; @FxmlView public class PendingTradesView extends ActivatableViewAndModel { @@ -103,6 +106,8 @@ public class PendingTradesView extends ActivatableViewAndModel tradesListChangeListener; + private Map newChatMessagesByTradeMap = new HashMap<>(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -199,6 +204,8 @@ public void initialize() { .show(); } }; + + tradesListChangeListener = c -> updateNewChatMessagesByTradeMap(); } @Override @@ -260,6 +267,9 @@ else if (root.getChildren().size() == 2) }); updateTableSelection(); + + model.dataModel.list.addListener(tradesListChangeListener); + updateNewChatMessagesByTradeMap(); } @Override @@ -270,6 +280,8 @@ protected void deactivate() { removeSelectedSubView(); + model.dataModel.list.removeListener(tradesListChangeListener); + if (scene != null) scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } @@ -282,6 +294,84 @@ private void removeSelectedSubView() { } } + /////////////////////////////////////////////////////////////////////////////////////////// + // Chat + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateNewChatMessagesByTradeMap() { + model.dataModel.list.forEach(t -> { + Trade trade = t.getTrade(); + newChatMessagesByTradeMap.put(trade.getId(), + trade.getCommunicationMessages().stream().filter(m -> !m.isWasDisplayed()).count()); + }); + + log.error(newChatMessagesByTradeMap.toString()); + } + + private void openChat(Trade trade) { + if (chatPopupStage != null) + chatPopupStage.close(); + + Chat tradeChat = new Chat(model.dataModel.tradeManager.getChatManager(), formatter); + tradeChat.setAllowAttachments(false); + tradeChat.setDisplayHeader(false); + tradeChat.initialize(); + + trade.getCommunicationMessages().forEach(m -> m.setWasDisplayed(true)); + trade.persist(); + + AnchorPane pane = new AnchorPane(tradeChat); + pane.setPrefSize(600, 400); + AnchorPane.setLeftAnchor(tradeChat, 10d); + AnchorPane.setRightAnchor(tradeChat, 10d); + AnchorPane.setTopAnchor(tradeChat, -20d); + AnchorPane.setBottomAnchor(tradeChat, 10d); + + boolean isTaker = !model.dataModel.isMaker(trade.getOffer()); + boolean isBuyer = model.dataModel.isBuyer(); + tradeChat.display(new TradeChatSession(trade, isTaker, isBuyer, + model.dataModel.tradeManager, + model.dataModel.tradeManager.getChatManager()), + null, + pane.widthProperty()); + + tradeChat.activate(); + tradeChat.scrollToBottom(); + + chatPopupStage = new Stage(); + chatPopupStage.setTitle(Res.get("portfolio.pending.chatWindowTitle", trade.getShortId())); + StackPane owner = MainView.getRootContainer(); + Scene rootScene = owner.getScene(); + chatPopupStage.initOwner(rootScene.getWindow()); + chatPopupStage.initModality(Modality.NONE); + chatPopupStage.initStyle(StageStyle.DECORATED); + chatPopupStage.setOnHiding(event -> tradeChat.deactivate()); + + Scene scene = new Scene(pane); + scene.getStylesheets().setAll("/bisq/desktop/theme-light.css", + "/bisq/desktop/bisq.css", + "/bisq/desktop/images.css"); + scene.addEventHandler(KeyEvent.KEY_RELEASED, ev -> { + if (ev.getCode() == KeyCode.ESCAPE) { + ev.consume(); + chatPopupStage.hide(); + } + }); + chatPopupStage.setScene(scene); + + chatPopupStage.setOpacity(0); + chatPopupStage.show(); + + Window rootSceneWindow = rootScene.getWindow(); + double titleBarHeight = rootSceneWindow.getHeight() - rootScene.getHeight(); + chatPopupStage.setX(Math.round(rootSceneWindow.getX() + (owner.getWidth() - chatPopupStage.getWidth() / 4 * 3))); + chatPopupStage.setY(Math.round(rootSceneWindow.getY() + titleBarHeight + (owner.getHeight() - chatPopupStage.getHeight() / 4 * 3))); + + // Delay display to next render frame to avoid that the popup is first quickly displayed in default position + // and after a short moment in the correct position + UserThread.execute(() -> chatPopupStage.setOpacity(1)); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private @@ -561,66 +651,5 @@ public void updateItem(final PendingTradesListItem newItem, boolean empty) { }); return chatColumn; } - - private void openChat(Trade trade) { - if (chatPopupStage != null) - chatPopupStage.close(); - - Chat tradeChat = new Chat(model.dataModel.tradeManager.getChatManager(), formatter); - tradeChat.setAllowAttachments(false); - tradeChat.setDisplayHeader(false); - tradeChat.initialize(); - - AnchorPane pane = new AnchorPane(tradeChat); - pane.setPrefSize(600, 400); - AnchorPane.setLeftAnchor(tradeChat, 10d); - AnchorPane.setRightAnchor(tradeChat, 10d); - AnchorPane.setTopAnchor(tradeChat, -20d); - AnchorPane.setBottomAnchor(tradeChat, 10d); - - boolean isTaker = !model.dataModel.isMaker(trade.getOffer()); - boolean isBuyer = model.dataModel.isBuyer(); - tradeChat.display(new TradeChatSession(trade, isTaker, isBuyer, - model.dataModel.tradeManager, - model.dataModel.tradeManager.getChatManager()), - null, - pane.widthProperty()); - - tradeChat.activate(); - tradeChat.scrollToBottom(); - - chatPopupStage = new Stage(); - chatPopupStage.setTitle(Res.get("portfolio.pending.chatWindowTitle", trade.getShortId())); - StackPane owner = MainView.getRootContainer(); - Scene rootScene = owner.getScene(); - chatPopupStage.initOwner(rootScene.getWindow()); - chatPopupStage.initModality(Modality.NONE); - chatPopupStage.initStyle(StageStyle.DECORATED); - chatPopupStage.setOnHiding(event -> tradeChat.deactivate()); - - Scene scene = new Scene(pane); - scene.getStylesheets().setAll( - "/bisq/desktop/bisq.css", - "/bisq/desktop/images.css"); - scene.addEventHandler(KeyEvent.KEY_RELEASED, ev -> { - if (ev.getCode() == KeyCode.ESCAPE) { - ev.consume(); - chatPopupStage.hide(); - } - }); - chatPopupStage.setScene(scene); - - chatPopupStage.setOpacity(0); - chatPopupStage.show(); - - Window rootSceneWindow = rootScene.getWindow(); - double titleBarHeight = rootSceneWindow.getHeight() - rootScene.getHeight(); - chatPopupStage.setX(Math.round(rootSceneWindow.getX() + (owner.getWidth() - chatPopupStage.getWidth() / 4 * 3))); - chatPopupStage.setY(Math.round(rootSceneWindow.getY() + titleBarHeight + (owner.getHeight() - chatPopupStage.getHeight() / 4 * 3))); - - // Delay display to next render frame to avoid that the popup is first quickly displayed in default position - // and after a short moment in the correct position - UserThread.execute(() -> chatPopupStage.setOpacity(1)); - } } From 81a93b56b105765ad8041057ca1100eda59308b3 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 29 Aug 2019 22:55:02 +0200 Subject: [PATCH 21/33] Add onAllServicesInitialized call, rename method --- core/src/main/java/bisq/core/chat/ChatManager.java | 4 ++-- core/src/main/java/bisq/core/trade/TradeManager.java | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/chat/ChatManager.java b/core/src/main/java/bisq/core/chat/ChatManager.java index 0c89c10ab92..2d28df105df 100644 --- a/core/src/main/java/bisq/core/chat/ChatManager.java +++ b/core/src/main/java/bisq/core/chat/ChatManager.java @@ -82,11 +82,11 @@ public void onAllServicesInitialized() { } public void tryApplyMessages() { - if (isReadyForTxBroadcast()) + if (isReady()) applyMessages(); } - private boolean isReadyForTxBroadcast() { + private boolean isReady() { return allServicesInitialized && p2PService.isBootstrapped() && walletsSetup.isDownloadComplete() && diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 2cdb4ee79c0..45a9c90fc04 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -250,6 +250,8 @@ public void onUpdatedDataReceived() { log.warn("Swapping pending OFFER_FUNDING entries at startup. offerId={}", addressEntry.getOfferId()); btcWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), AddressEntry.Context.OFFER_FUNDING); }); + + chatManager.onAllServicesInitialized(); } public void shutDown() { From a2cd80866d8b2bdc0bf337747956a1421386ac65 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 30 Aug 2019 00:07:32 +0200 Subject: [PATCH 22/33] Add missing setWasDisplayed --- .../core/arbitration/messages/DisputeCommunicationMessage.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java b/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java index c09c6132304..996dc128357 100644 --- a/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java +++ b/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java @@ -182,7 +182,8 @@ public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { .setStoredInMailbox(storedInMailboxProperty.get()) .setIsSystemMessage(isSystemMessage) .setUid(uid) - .setAcknowledged(acknowledgedProperty.get()); + .setAcknowledged(acknowledgedProperty.get()) + .setWasDisplayed(wasDisplayed); Optional.ofNullable(sendMessageErrorProperty.get()).ifPresent(builder::setSendMessageError); Optional.ofNullable(ackErrorProperty.get()).ifPresent(builder::setAckError); return getNetworkEnvelopeBuilder() From 6304c9ec8453665bb41423b95b04febceb3b5fe4 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 30 Aug 2019 00:07:51 +0200 Subject: [PATCH 23/33] Add badge for num new chat msg --- .../pendingtrades/PendingTradesView.java | 72 ++++++++++++++----- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 6be5129910c..c5fe66c5dda 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -29,6 +29,7 @@ import bisq.core.alert.PrivateNotificationManager; import bisq.core.app.AppOptionKeys; +import bisq.core.arbitration.messages.DisputeCommunicationMessage; import bisq.core.locale.Res; import bisq.core.trade.Trade; import bisq.core.trade.TradeChatSession; @@ -47,6 +48,8 @@ import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; +import com.jfoenix.controls.JFXBadge; + import javafx.fxml.FXML; import javafx.stage.Modality; @@ -64,11 +67,13 @@ import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.geometry.Insets; +import javafx.geometry.Pos; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; @@ -108,6 +113,7 @@ public class PendingTradesView extends ActivatableViewAndModel tradesListChangeListener; private Map newChatMessagesByTradeMap = new HashMap<>(); + private String tradeIdOfOpenChat; /////////////////////////////////////////////////////////////////////////////////////////// @@ -304,22 +310,21 @@ private void updateNewChatMessagesByTradeMap() { newChatMessagesByTradeMap.put(trade.getId(), trade.getCommunicationMessages().stream().filter(m -> !m.isWasDisplayed()).count()); }); - - log.error(newChatMessagesByTradeMap.toString()); } private void openChat(Trade trade) { if (chatPopupStage != null) chatPopupStage.close(); + trade.getCommunicationMessages().forEach(m -> m.setWasDisplayed(true)); + trade.persist(); + tradeIdOfOpenChat = trade.getId(); + Chat tradeChat = new Chat(model.dataModel.tradeManager.getChatManager(), formatter); tradeChat.setAllowAttachments(false); tradeChat.setDisplayHeader(false); tradeChat.initialize(); - trade.getCommunicationMessages().forEach(m -> m.setWasDisplayed(true)); - trade.persist(); - AnchorPane pane = new AnchorPane(tradeChat); pane.setPrefSize(600, 400); AnchorPane.setLeftAnchor(tradeChat, 10d); @@ -345,7 +350,13 @@ private void openChat(Trade trade) { chatPopupStage.initOwner(rootScene.getWindow()); chatPopupStage.initModality(Modality.NONE); chatPopupStage.initStyle(StageStyle.DECORATED); - chatPopupStage.setOnHiding(event -> tradeChat.deactivate()); + chatPopupStage.setOnHiding(event -> { + tradeChat.deactivate(); + // at close we set all as displayed. While open we ignore updates of the numNewMsg in the list icon. + trade.getCommunicationMessages().forEach(m -> m.setWasDisplayed(true)); + trade.persist(); + tradeIdOfOpenChat = null; + }); Scene scene = new Scene(pane); scene.getStylesheets().setAll("/bisq/desktop/theme-light.css", @@ -621,31 +632,56 @@ private TableColumn setChatColumnC chatColumn.setSortable(false); chatColumn.setCellFactory( new Callback<>() { - @Override public TableCell call(TableColumn column) { return new TableCell<>() { - @Override public void updateItem(final PendingTradesListItem newItem, boolean empty) { super.updateItem(newItem, empty); if (!empty && newItem != null) { Trade trade = newItem.getTrade(); - Label label = new Label(); - //todo - // label.setLayoutY(-5); - //label.setLayoutX(10); - label.getStyleClass().addAll("icon", "highlight"); - Tooltip.install(label, new Tooltip(Res.get("portfolio.pending.openChat"))); - AwesomeDude.setIcon(label, AwesomeIcon.COMMENTS_ALT); - label.setOnMouseClicked(e -> openChat(trade)); - setPadding(new Insets(-20, 0, 0, 20)); - setGraphic(label); + Label icon = AwesomeDude.createIconLabel(AwesomeIcon.COMMENTS_ALT, "25"); + Tooltip.install(icon, new Tooltip(Res.get("portfolio.pending.openChat"))); + + JFXBadge numNewMsg = new JFXBadge(icon); + // FIXME does not take position... + numNewMsg.setPosition(Pos.TOP_RIGHT); + icon.setOnMouseClicked(e -> { + openChat(trade); + update(trade, numNewMsg); + }); + + ListChangeListener listener = c -> update(trade, numNewMsg); + trade.getCommunicationMessages().addListener(listener); + + update(trade, numNewMsg); + + Pane pane = new Pane(); + pane.getChildren().addAll(icon, numNewMsg); + setGraphic(pane); } else { setGraphic(null); } } + + private void update(Trade trade, JFXBadge numNewMsg) { + if (!trade.getId().equals(tradeIdOfOpenChat)) { + updateNewChatMessagesByTradeMap(); + long num = newChatMessagesByTradeMap.get(trade.getId()); + if (num > 0) { + numNewMsg.setText(String.valueOf(num)); + numNewMsg.setEnabled(true); + } else { + numNewMsg.setText(""); + numNewMsg.setEnabled(false); + } + } else { + numNewMsg.setText(""); + numNewMsg.setEnabled(false); + } + numNewMsg.refreshBadge(); + } }; } }); From 5c1999f180e49ae4a34872bd0713e4cbbcc5698e Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 30 Aug 2019 00:57:21 +0200 Subject: [PATCH 24/33] Fix wrong pubkey --- .../bisq/core/trade/TradeChatSession.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/bisq/core/trade/TradeChatSession.java b/core/src/main/java/bisq/core/trade/TradeChatSession.java index 7f6f4fd61c9..265704a8497 100644 --- a/core/src/main/java/bisq/core/trade/TradeChatSession.java +++ b/core/src/main/java/bisq/core/trade/TradeChatSession.java @@ -35,25 +35,26 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nullable; + /* Makers are considered as servers and takers as clients for trader to trader chat * sessions. This is only to make it easier to understand who's who, there is no real * server/client relationship */ public class TradeChatSession extends ChatSession { private static final Logger log = LoggerFactory.getLogger(TradeChatSession.class); + @Nullable private Trade trade; private boolean isClient; private boolean isBuyer; private TradeManager tradeManager; private ChatManager chatManager; - public TradeChatSession( - Trade trade, - boolean isClient, - boolean isBuyer, - TradeManager tradeManager, - ChatManager chatManager - ) { + public TradeChatSession(@Nullable Trade trade, + boolean isClient, + boolean isBuyer, + TradeManager tradeManager, + ChatManager chatManager) { super(DisputeCommunicationMessage.Type.TRADE); this.trade = trade; this.isClient = isClient; @@ -118,10 +119,14 @@ public PubKeyRing getPeerPubKeyRing(DisputeCommunicationMessage message) { Optional tradeOptional = tradeManager.getTradeById(message.getTradeId()); if (tradeOptional.isPresent()) { Trade t = tradeOptional.get(); - if (t.getContract() != null) - return isClient ? - t.getContract().getMakerPubKeyRing() : - t.getContract().getTakerPubKeyRing(); + if (t.getContract() != null && t.getOffer() != null) { + if (t.getOffer().getOwnerPubKey().equals(tradeManager.getKeyRing().getPubKeyRing().getSignaturePubKey())) { + // I am maker + return t.getContract().getTakerPubKeyRing(); + } else { + return t.getContract().getMakerPubKeyRing(); + } + } } return null; } From 663d8591c4cb2d92d445811647c30ebe4f02ec86 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 30 Aug 2019 00:58:06 +0200 Subject: [PATCH 25/33] Add chat rules system msg; cleanups --- .../core/arbitration/DisputeChatSession.java | 7 +++---- .../java/bisq/core/trade/TradeManager.java | 3 +-- .../resources/i18n/displayStrings.properties | 13 +++++++++++-- .../java/bisq/desktop/main/Chat/Chat.java | 4 +++- .../pendingtrades/PendingTradesView.java | 19 ++++++++++++++++--- 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java b/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java index 79847f84241..5edb71df714 100644 --- a/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java +++ b/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java @@ -43,13 +43,12 @@ public class DisputeChatSession extends ChatSession { private static final Logger log = LoggerFactory.getLogger(DisputeChatSession.class); + @Nullable private Dispute dispute; private DisputeManager disputeManager; - public DisputeChatSession( - @Nullable Dispute dispute, - DisputeManager disputeManager - ) { + public DisputeChatSession(@Nullable Dispute dispute, + DisputeManager disputeManager) { super(DisputeCommunicationMessage.Type.DISPUTE); this.dispute = dispute; this.disputeManager = disputeManager; diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 45a9c90fc04..2d7a8f59683 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -101,6 +101,7 @@ public class TradeManager implements PersistedDataHost { private static final Logger log = LoggerFactory.getLogger(TradeManager.class); private final User user; + @Getter private final KeyRing keyRing; private final BtcWalletService btcWalletService; private final BsqWalletService bsqWalletService; @@ -109,7 +110,6 @@ public class TradeManager implements PersistedDataHost { private final ClosedTradableManager closedTradableManager; private final FailedTradesManager failedTradesManager; private final P2PService p2PService; - private final WalletsSetup walletsSetup; private final PriceFeedService priceFeedService; private final FilterManager filterManager; private final TradeStatisticsManager tradeStatisticsManager; @@ -164,7 +164,6 @@ public TradeManager(User user, this.closedTradableManager = closedTradableManager; this.failedTradesManager = failedTradesManager; this.p2PService = p2PService; - this.walletsSetup = walletsSetup; this.priceFeedService = priceFeedService; this.filterManager = filterManager; this.tradeStatisticsManager = tradeStatisticsManager; diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index b27c960b2bb..f9a3cde97c8 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -608,8 +608,17 @@ portfolio.pending.step2_seller.waitPayment.msg=The deposit transaction has at le portfolio.pending.step2_seller.warn=The BTC buyer still has not done the {0} payment.\nYou need to wait until they have started the payment.\nIf the trade has not been completed on {1} the arbitrator will investigate. portfolio.pending.step2_seller.openForDispute=The BTC buyer has not started his payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the arbitrator for opening a dispute. -portfolio.pending.chatWindowTitle=Chat window for trade with ID ''{0}'' -portfolio.pending.openChat=Open chat window +tradeChat.chatWindowTitle=Chat window for trade with ID ''{0}'' +tradeChat.openChat=Open chat window +tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade. It is not mandatory to reply in \ + the chat. If a trader violates the below rules, open a dispute with 'cmd + o' and report it to the arbitrator.\n\n\ + Chat rules:\n\ + \t● Do not send links to unknown URLs (risk of malware). Links to a block explorer are permitted.\n\ + \t● Do not try to motivate peer to trade outside of Bisq (no security).\n\ + \t● Keep conversation friendly and respectful.\n\ + \t● Do not engage in any form of social engineering scam attempts.\n\ + \t● Respect if the peer is not responding and prefers to not communicate via the chat.\n\ + \t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or trollbox. # suppress inspection "UnusedProperty" message.state.UNDEFINED=Undefined diff --git a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java index 27c2b49c585..d6a10383684 100644 --- a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java +++ b/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java @@ -31,6 +31,7 @@ import bisq.core.chat.ChatManager; import bisq.core.chat.ChatSession; import bisq.core.locale.Res; +import bisq.core.trade.TradeChatSession; import bisq.core.util.BSFormatter; import bisq.network.p2p.NodeAddress; @@ -200,8 +201,9 @@ public void display(ChatSession chatSession, @Nullable Button extraButton, inputTextArea = new BisqTextArea(); inputTextArea.setPrefHeight(70); inputTextArea.setWrapText(true); - if (chatSession.isClient()) + if (chatSession instanceof TradeChatSession || chatSession.isClient()) { inputTextArea.setPromptText(Res.get("support.input.prompt")); + } sendButton = new AutoTooltipButton(Res.get("support.send")); sendButton.setDefaultButton(true); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index c5fe66c5dda..988e8e6ac4e 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -316,6 +316,19 @@ private void openChat(Trade trade) { if (chatPopupStage != null) chatPopupStage.close(); + if (trade.getCommunicationMessages().isEmpty()) { + DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage( + DisputeCommunicationMessage.Type.TRADE, + trade.getId(), + 0, + false, + Res.get("tradeChat.rules"), + new NodeAddress("null:0000") + ); + disputeCommunicationMessage.setSystemMessage(true); + trade.getCommunicationMessages().add(disputeCommunicationMessage); + } + trade.getCommunicationMessages().forEach(m -> m.setWasDisplayed(true)); trade.persist(); tradeIdOfOpenChat = trade.getId(); @@ -326,7 +339,7 @@ private void openChat(Trade trade) { tradeChat.initialize(); AnchorPane pane = new AnchorPane(tradeChat); - pane.setPrefSize(600, 400); + pane.setPrefSize(700, 500); AnchorPane.setLeftAnchor(tradeChat, 10d); AnchorPane.setRightAnchor(tradeChat, 10d); AnchorPane.setTopAnchor(tradeChat, -20d); @@ -344,7 +357,7 @@ private void openChat(Trade trade) { tradeChat.scrollToBottom(); chatPopupStage = new Stage(); - chatPopupStage.setTitle(Res.get("portfolio.pending.chatWindowTitle", trade.getShortId())); + chatPopupStage.setTitle(Res.get("tradeChat.chatWindowTitle", trade.getShortId())); StackPane owner = MainView.getRootContainer(); Scene rootScene = owner.getScene(); chatPopupStage.initOwner(rootScene.getWindow()); @@ -642,7 +655,7 @@ public void updateItem(final PendingTradesListItem newItem, boolean empty) { Trade trade = newItem.getTrade(); Label icon = AwesomeDude.createIconLabel(AwesomeIcon.COMMENTS_ALT, "25"); - Tooltip.install(icon, new Tooltip(Res.get("portfolio.pending.openChat"))); + Tooltip.install(icon, new Tooltip(Res.get("tradeChat.openChat"))); JFXBadge numNewMsg = new JFXBadge(icon); // FIXME does not take position... From 5e267fa461414568536ca5c1c118cb691a75dd69 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 30 Aug 2019 01:01:29 +0200 Subject: [PATCH 26/33] Handle nullables --- .../bisq/core/arbitration/DisputeChatSession.java | 13 +++++++------ .../main/java/bisq/core/trade/TradeChatSession.java | 12 +++++++----- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java b/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java index 5edb71df714..2f943dfc758 100644 --- a/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java +++ b/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java @@ -29,6 +29,7 @@ import bisq.common.crypto.PubKeyRing; +import javafx.collections.FXCollections; import javafx.collections.ObservableList; import java.util.List; @@ -60,23 +61,23 @@ public DisputeChatSession(@Nullable Dispute dispute, @Override public boolean isClient() { - return disputeManager.isTrader(dispute); + return dispute != null && disputeManager.isTrader(dispute); } @Override public String getTradeId() { - return dispute.getTradeId(); + return dispute != null ? dispute.getTradeId() : ""; } @Override public PubKeyRing getClientPubKeyRing() { // Get pubkeyring of trader. Arbitrator is considered server for the chat session - return dispute.getTraderPubKeyRing(); + return dispute != null ? dispute.getTraderPubKeyRing() : null; } @Override public void addDisputeCommunicationMessage(DisputeCommunicationMessage message) { - if (isClient() || (!isClient() && !message.isSystemMessage())) + if (dispute != null && (isClient() || (!isClient() && !message.isSystemMessage()))) dispute.addDisputeCommunicationMessage(message); } @@ -87,12 +88,12 @@ public void persist() { @Override public ObservableList getDisputeCommunicationMessages() { - return dispute.getDisputeCommunicationMessages(); + return dispute != null ? dispute.getDisputeCommunicationMessages() : FXCollections.observableArrayList(); } @Override public boolean chatIsOpen() { - return !dispute.isClosed(); + return dispute != null && !dispute.isClosed(); } diff --git a/core/src/main/java/bisq/core/trade/TradeChatSession.java b/core/src/main/java/bisq/core/trade/TradeChatSession.java index 265704a8497..ab0c19eafdd 100644 --- a/core/src/main/java/bisq/core/trade/TradeChatSession.java +++ b/core/src/main/java/bisq/core/trade/TradeChatSession.java @@ -26,6 +26,7 @@ import bisq.common.crypto.PubKeyRing; +import javafx.collections.FXCollections; import javafx.collections.ObservableList; import java.util.List; @@ -70,20 +71,21 @@ public boolean isClient() { @Override public String getTradeId() { - return trade.getId(); + return trade != null ? trade.getId() : ""; } @Override public PubKeyRing getClientPubKeyRing() { // Get pubkeyring of taker. Maker is considered server for chat sessions - if (trade.getContract() != null) + if (trade != null && trade.getContract() != null) return trade.getContract().getTakerPubKeyRing(); return null; } @Override public void addDisputeCommunicationMessage(DisputeCommunicationMessage message) { - trade.addCommunicationMessage(message); + if (trade != null) + trade.addCommunicationMessage(message); } @Override @@ -93,12 +95,12 @@ public void persist() { @Override public ObservableList getDisputeCommunicationMessages() { - return trade.getCommunicationMessages(); + return trade != null ? trade.getCommunicationMessages() : FXCollections.observableArrayList(); } @Override public boolean chatIsOpen() { - return trade.getState() != Trade.State.WITHDRAW_COMPLETED; + return trade != null && trade.getState() != Trade.State.WITHDRAW_COMPLETED; } @Override From 964ca648b8479d6e0ef95f2fa25fdb050bdd47ab Mon Sep 17 00:00:00 2001 From: Christoph Atteneder Date: Fri, 30 Aug 2019 12:04:55 +0200 Subject: [PATCH 27/33] Fix positioning of badge --- .../main/portfolio/pendingtrades/PendingTradesView.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 988e8e6ac4e..6ad23d5101c 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -67,7 +67,6 @@ import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; @@ -658,7 +657,6 @@ public void updateItem(final PendingTradesListItem newItem, boolean empty) { Tooltip.install(icon, new Tooltip(Res.get("tradeChat.openChat"))); JFXBadge numNewMsg = new JFXBadge(icon); - // FIXME does not take position... numNewMsg.setPosition(Pos.TOP_RIGHT); icon.setOnMouseClicked(e -> { openChat(trade); @@ -670,9 +668,7 @@ public void updateItem(final PendingTradesListItem newItem, boolean empty) { update(trade, numNewMsg); - Pane pane = new Pane(); - pane.getChildren().addAll(icon, numNewMsg); - setGraphic(pane); + setGraphic(numNewMsg); } else { setGraphic(null); } From 8edd2611db4d5d24d328efa0b929b496abd0008b Mon Sep 17 00:00:00 2001 From: Christoph Atteneder Date: Fri, 30 Aug 2019 12:42:17 +0200 Subject: [PATCH 28/33] Use button instead of label to fix tooltip font size, use material design icon and add additional cleanup code --- .../pendingtrades/PendingTradesView.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 6ad23d5101c..f31831f368d 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -26,6 +26,7 @@ import bisq.desktop.main.MainView; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; +import bisq.desktop.util.FormBuilder; import bisq.core.alert.PrivateNotificationManager; import bisq.core.app.AppOptionKeys; @@ -45,8 +46,7 @@ import javax.inject.Inject; -import de.jensd.fx.fontawesome.AwesomeDude; -import de.jensd.fx.fontawesome.AwesomeIcon; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import com.jfoenix.controls.JFXBadge; @@ -59,7 +59,7 @@ import javafx.scene.Node; import javafx.scene.Scene; -import javafx.scene.control.Label; +import javafx.scene.control.Button; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; @@ -647,18 +647,23 @@ private TableColumn setChatColumnC @Override public TableCell call(TableColumn column) { return new TableCell<>() { + Button button; + @Override public void updateItem(final PendingTradesListItem newItem, boolean empty) { super.updateItem(newItem, empty); if (!empty && newItem != null) { Trade trade = newItem.getTrade(); - Label icon = AwesomeDude.createIconLabel(AwesomeIcon.COMMENTS_ALT, "25"); - Tooltip.install(icon, new Tooltip(Res.get("tradeChat.openChat"))); + if (button == null) { + button = FormBuilder.getIconButton(MaterialDesignIcon.COMMENT_MULTIPLE_OUTLINE); + button.setTooltip(new Tooltip(Res.get("tradeChat.openChat"))); + } - JFXBadge numNewMsg = new JFXBadge(icon); + JFXBadge numNewMsg = new JFXBadge(button); numNewMsg.setPosition(Pos.TOP_RIGHT); - icon.setOnMouseClicked(e -> { + + button.setOnAction(e -> { openChat(trade); update(trade, numNewMsg); }); @@ -671,6 +676,10 @@ public void updateItem(final PendingTradesListItem newItem, boolean empty) { setGraphic(numNewMsg); } else { setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } } } From fa307207aca19c95809c45fe6c9b1c1e4fc67dcb Mon Sep 17 00:00:00 2001 From: chimp1984 <54558767+chimp1984@users.noreply.github.com> Date: Fri, 30 Aug 2019 16:53:39 +0200 Subject: [PATCH 29/33] Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain --- core/src/main/resources/i18n/displayStrings.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index f9a3cde97c8..9ab05021d33 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -614,7 +614,7 @@ tradeChat.rules=You can communicate with your trade peer to resolve potential pr the chat. If a trader violates the below rules, open a dispute with 'cmd + o' and report it to the arbitrator.\n\n\ Chat rules:\n\ \t● Do not send links to unknown URLs (risk of malware). Links to a block explorer are permitted.\n\ - \t● Do not try to motivate peer to trade outside of Bisq (no security).\n\ + \t● Do not encourage trading outside of Bisq (no security).\n\ \t● Keep conversation friendly and respectful.\n\ \t● Do not engage in any form of social engineering scam attempts.\n\ \t● Respect if the peer is not responding and prefers to not communicate via the chat.\n\ From 16d3bc5d26660b57009948ae22183c762481f20c Mon Sep 17 00:00:00 2001 From: chimp1984 <54558767+chimp1984@users.noreply.github.com> Date: Fri, 30 Aug 2019 16:54:16 +0200 Subject: [PATCH 30/33] Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain --- core/src/main/resources/i18n/displayStrings.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 9ab05021d33..dcb086d4d07 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -617,7 +617,7 @@ tradeChat.rules=You can communicate with your trade peer to resolve potential pr \t● Do not encourage trading outside of Bisq (no security).\n\ \t● Keep conversation friendly and respectful.\n\ \t● Do not engage in any form of social engineering scam attempts.\n\ - \t● Respect if the peer is not responding and prefers to not communicate via the chat.\n\ + \t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\ \t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or trollbox. # suppress inspection "UnusedProperty" From c6157803331b920f3eb9ba56eb10a228f4c45cc3 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 30 Aug 2019 17:07:35 +0200 Subject: [PATCH 31/33] Update tradeChat.rules --- .../main/resources/i18n/displayStrings.properties | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index dcb086d4d07..12705f494a1 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -610,15 +610,17 @@ portfolio.pending.step2_seller.openForDispute=The BTC buyer has not started his tradeChat.chatWindowTitle=Chat window for trade with ID ''{0}'' tradeChat.openChat=Open chat window -tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade. It is not mandatory to reply in \ - the chat. If a trader violates the below rules, open a dispute with 'cmd + o' and report it to the arbitrator.\n\n\ +tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\n\ + It is not mandatory to reply in the chat.\n\ + If a trader violates the below rules, open a dispute with 'Cmd/Ctrl + o' and report it to the arbitrator.\n\n\ Chat rules:\n\ - \t● Do not send links to unknown URLs (risk of malware). Links to a block explorer are permitted.\n\ + \t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\ + \t● Do not send your seed words, private keys, passwords or other sensitive information!\n\ \t● Do not encourage trading outside of Bisq (no security).\n\ - \t● Keep conversation friendly and respectful.\n\ \t● Do not engage in any form of social engineering scam attempts.\n\ \t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\ - \t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or trollbox. + \t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or trollbox.\n\ + \t● Keep conversation friendly and respectful. # suppress inspection "UnusedProperty" message.state.UNDEFINED=Undefined From 57ec0dbc7efb0ae21d08d1b9e2187cdc04c33142 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 30 Aug 2019 18:08:12 +0200 Subject: [PATCH 32/33] Persist chat position. Improve listener handling. - Store last position of the chat window so if it gets closed and opened again it opens at the last position. - Fix issues with the listener for new messages. The handler was called multiple times before. Now its is called only once. Tested with multiple trades and scrolling. We use maps for each trade to avoid multiple listener registrations when switching views. With current implementation we avoid that but we do not remove listeners when a trade is removed (completed) but that has no consequences as we will not receive any message anyway from a closed trade. Supporting it more correctly would require more effort and managing listener deactivation at screen switches (currently we get the update called if we have selected another view. This part can be improved if any dev feels motivated but its not trivial... --- .../pendingtrades/PendingTradesView.java | 97 +++++++++++++------ 1 file changed, 70 insertions(+), 27 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index f31831f368d..42cf082e899 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -78,6 +78,7 @@ import org.fxmisc.easybind.Subscription; import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ChangeListener; import javafx.event.EventHandler; @@ -113,6 +114,14 @@ public class PendingTradesView extends ActivatableViewAndModel tradesListChangeListener; private Map newChatMessagesByTradeMap = new HashMap<>(); private String tradeIdOfOpenChat; + private double chatPopupStageXPosition = -1; + private double chatPopupStageYPosition = -1; + private ChangeListener xPositionListener; + private ChangeListener yPositionListener; + + private Map buttonByTrade = new HashMap<>(); + private Map badgeByTrade = new HashMap<>(); + private Map> listenerByTrade = new HashMap<>(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -338,7 +347,7 @@ private void openChat(Trade trade) { tradeChat.initialize(); AnchorPane pane = new AnchorPane(tradeChat); - pane.setPrefSize(700, 500); + pane.setPrefSize(760, 500); AnchorPane.setLeftAnchor(tradeChat, 10d); AnchorPane.setRightAnchor(tradeChat, 10d); AnchorPane.setTopAnchor(tradeChat, -20d); @@ -368,6 +377,13 @@ private void openChat(Trade trade) { trade.getCommunicationMessages().forEach(m -> m.setWasDisplayed(true)); trade.persist(); tradeIdOfOpenChat = null; + + if (xPositionListener != null) { + chatPopupStage.xProperty().removeListener(xPositionListener); + } + if (yPositionListener != null) { + chatPopupStage.xProperty().removeListener(yPositionListener); + } }); Scene scene = new Scene(pane); @@ -385,10 +401,20 @@ private void openChat(Trade trade) { chatPopupStage.setOpacity(0); chatPopupStage.show(); - Window rootSceneWindow = rootScene.getWindow(); - double titleBarHeight = rootSceneWindow.getHeight() - rootScene.getHeight(); - chatPopupStage.setX(Math.round(rootSceneWindow.getX() + (owner.getWidth() - chatPopupStage.getWidth() / 4 * 3))); - chatPopupStage.setY(Math.round(rootSceneWindow.getY() + titleBarHeight + (owner.getHeight() - chatPopupStage.getHeight() / 4 * 3))); + xPositionListener = (observable, oldValue, newValue) -> chatPopupStageXPosition = (double) newValue; + chatPopupStage.xProperty().addListener(xPositionListener); + yPositionListener = (observable, oldValue, newValue) -> chatPopupStageYPosition = (double) newValue; + chatPopupStage.yProperty().addListener(yPositionListener); + + if (chatPopupStageXPosition == -1) { + Window rootSceneWindow = rootScene.getWindow(); + double titleBarHeight = rootSceneWindow.getHeight() - rootScene.getHeight(); + chatPopupStage.setX(Math.round(rootSceneWindow.getX() + (owner.getWidth() - chatPopupStage.getWidth() / 4 * 3))); + chatPopupStage.setY(Math.round(rootSceneWindow.getY() + titleBarHeight + (owner.getHeight() - chatPopupStage.getHeight() / 4 * 3))); + } else { + chatPopupStage.setX(chatPopupStageXPosition); + chatPopupStage.setY(chatPopupStageYPosition); + } // Delay display to next render frame to avoid that the popup is first quickly displayed in default position // and after a short moment in the correct position @@ -647,58 +673,75 @@ private TableColumn setChatColumnC @Override public TableCell call(TableColumn column) { return new TableCell<>() { - Button button; @Override public void updateItem(final PendingTradesListItem newItem, boolean empty) { super.updateItem(newItem, empty); + if (!empty && newItem != null) { Trade trade = newItem.getTrade(); - - if (button == null) { + String id = trade.getId(); + + // We use maps for each trade to avoid multiple listener registrations when + // switching views. With current implementation we avoid that but we do not + // remove listeners when a trade is removed (completed) but that has no consequences + // as we will not receive any message anyway from a closed trade. Supporting it + // more correctly would require more effort and managing listener deactivation at + // screen switches (currently we get the update called if we have selected another + // view. + Button button; + if (!buttonByTrade.containsKey(id)) { button = FormBuilder.getIconButton(MaterialDesignIcon.COMMENT_MULTIPLE_OUTLINE); + buttonByTrade.put(id, button); button.setTooltip(new Tooltip(Res.get("tradeChat.openChat"))); + } else { + button = buttonByTrade.get(id); } - JFXBadge numNewMsg = new JFXBadge(button); - numNewMsg.setPosition(Pos.TOP_RIGHT); + JFXBadge badge; + if (!badgeByTrade.containsKey(id)) { + badge = new JFXBadge(button); + badgeByTrade.put(id, badge); + badge.setPosition(Pos.TOP_RIGHT); + } else { + badge = badgeByTrade.get(id); + } button.setOnAction(e -> { openChat(trade); - update(trade, numNewMsg); + update(trade, badge); }); - ListChangeListener listener = c -> update(trade, numNewMsg); - trade.getCommunicationMessages().addListener(listener); + if (!listenerByTrade.containsKey(id)) { + ListChangeListener listener = c -> update(trade, badge); + listenerByTrade.put(id, listener); + trade.getCommunicationMessages().addListener(listener); + } - update(trade, numNewMsg); + update(trade, badge); - setGraphic(numNewMsg); + setGraphic(badge); } else { setGraphic(null); - if (button != null) { - button.setOnAction(null); - button = null; - } } } - private void update(Trade trade, JFXBadge numNewMsg) { + private void update(Trade trade, JFXBadge badge) { if (!trade.getId().equals(tradeIdOfOpenChat)) { updateNewChatMessagesByTradeMap(); long num = newChatMessagesByTradeMap.get(trade.getId()); if (num > 0) { - numNewMsg.setText(String.valueOf(num)); - numNewMsg.setEnabled(true); + badge.setText(String.valueOf(num)); + badge.setEnabled(true); } else { - numNewMsg.setText(""); - numNewMsg.setEnabled(false); + badge.setText(""); + badge.setEnabled(false); } } else { - numNewMsg.setText(""); - numNewMsg.setEnabled(false); + badge.setText(""); + badge.setEnabled(false); } - numNewMsg.refreshBadge(); + badge.refreshBadge(); } }; } From 50baa9b02306e25f9181ef4792fe43b4921293d3 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 30 Aug 2019 18:44:43 +0200 Subject: [PATCH 33/33] Close chat window if dispute gets closed --- .../bisq/core/trade/TradeChatSession.java | 29 +++++++++++++++---- .../pendingtrades/PendingTradesView.java | 21 ++++++++++---- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/bisq/core/trade/TradeChatSession.java b/core/src/main/java/bisq/core/trade/TradeChatSession.java index ab0c19eafdd..862e817827b 100644 --- a/core/src/main/java/bisq/core/trade/TradeChatSession.java +++ b/core/src/main/java/bisq/core/trade/TradeChatSession.java @@ -19,6 +19,7 @@ import bisq.core.arbitration.messages.DisputeCommunicationMessage; import bisq.core.arbitration.messages.DisputeMessage; +import bisq.core.arbitration.messages.DisputeResultMessage; import bisq.core.chat.ChatManager; import bisq.core.chat.ChatSession; @@ -31,6 +32,7 @@ import java.util.List; import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -44,12 +46,18 @@ public class TradeChatSession extends ChatSession { private static final Logger log = LoggerFactory.getLogger(TradeChatSession.class); + public interface DisputeStateListener { + void onDisputeClosed(String tradeId); + } + @Nullable private Trade trade; private boolean isClient; private boolean isBuyer; private TradeManager tradeManager; private ChatManager chatManager; + // Needed to avoid ConcurrentModificationException as we remove a listener at the handler call + private List disputeStateListeners = new CopyOnWriteArrayList<>(); public TradeChatSession(@Nullable Trade trade, boolean isClient, @@ -64,6 +72,14 @@ public TradeChatSession(@Nullable Trade trade, this.chatManager = chatManager; } + public void addDisputeStateListener(DisputeStateListener disputeStateListener) { + disputeStateListeners.add(disputeStateListener); + } + + public void removeDisputeStateListener(DisputeStateListener disputeStateListener) { + disputeStateListeners.remove(disputeStateListener); + } + @Override public boolean isClient() { return isClient; @@ -138,14 +154,15 @@ public void dispatchMessage(DisputeMessage message) { log.info("Received {} with tradeId {} and uid {}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); if (message instanceof DisputeCommunicationMessage) { - if (((DisputeCommunicationMessage) message).getType() != DisputeCommunicationMessage.Type.TRADE) { - log.debug("Ignore non trade type communication message"); - return; + if (((DisputeCommunicationMessage) message).getType() == DisputeCommunicationMessage.Type.TRADE) { + chatManager.onDisputeDirectMessage((DisputeCommunicationMessage) message); } - chatManager.onDisputeDirectMessage((DisputeCommunicationMessage) message); - } else { - log.warn("Unsupported message at dispatchMessage.\nmessage=" + message); + // We ignore dispute messages + } else if (message instanceof DisputeResultMessage) { + // We notify about dispute closed state + disputeStateListeners.forEach(e -> e.onDisputeClosed(message.getTradeId())); } + // We ignore all other non DisputeCommunicationMessages } @Override diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 42cf082e899..4ea08ec2cef 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -122,6 +122,7 @@ public class PendingTradesView extends ActivatableViewAndModel buttonByTrade = new HashMap<>(); private Map badgeByTrade = new HashMap<>(); private Map> listenerByTrade = new HashMap<>(); + private TradeChatSession.DisputeStateListener disputeStateListener; /////////////////////////////////////////////////////////////////////////////////////////// @@ -355,11 +356,18 @@ private void openChat(Trade trade) { boolean isTaker = !model.dataModel.isMaker(trade.getOffer()); boolean isBuyer = model.dataModel.isBuyer(); - tradeChat.display(new TradeChatSession(trade, isTaker, isBuyer, - model.dataModel.tradeManager, - model.dataModel.tradeManager.getChatManager()), - null, - pane.widthProperty()); + TradeChatSession chatSession = new TradeChatSession(trade, isTaker, isBuyer, + model.dataModel.tradeManager, + model.dataModel.tradeManager.getChatManager()); + + disputeStateListener = tradeId -> { + if (trade.getId().equals(tradeId)) { + chatPopupStage.hide(); + } + }; + chatSession.addDisputeStateListener(disputeStateListener); + + tradeChat.display(chatSession, null, pane.widthProperty()); tradeChat.activate(); tradeChat.scrollToBottom(); @@ -384,6 +392,9 @@ private void openChat(Trade trade) { if (yPositionListener != null) { chatPopupStage.xProperty().removeListener(yPositionListener); } + if (disputeStateListener != null) { + chatSession.removeDisputeStateListener(disputeStateListener); + } }); Scene scene = new Scene(pane);