update chat views from upstream, support sending logs

Co-authored-by: jmacxx <47253594+jmacxx@users.noreply.github.com>
This commit is contained in:
woodser 2024-03-20 18:20:24 -04:00
parent 833cdb3b84
commit 1647a582f5
23 changed files with 1691 additions and 206 deletions

View file

@ -17,40 +17,46 @@
package haveno.desktop.components;
import com.google.common.base.Charsets;
import haveno.desktop.main.overlays.editor.PeerInfoWithTagEditor;
import haveno.desktop.util.DisplayUtils;
import haveno.core.alert.PrivateNotificationManager;
import haveno.core.locale.Res;
import haveno.core.offer.Offer;
import haveno.core.trade.Trade;
import haveno.core.user.Preferences;
import haveno.desktop.main.overlays.editor.PeerInfoWithTagEditor;
import haveno.desktop.util.DisplayUtils;
import haveno.network.p2p.NodeAddress;
import javafx.geometry.Point2D;
import com.google.common.base.Charsets;
import javafx.scene.Group;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import lombok.Setter;
import javafx.geometry.Point2D;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
@Slf4j
public class PeerInfoIcon extends Group {
public interface notify {
void avatarTagUpdated();
}
@Setter
private notify callback;
protected Preferences preferences;
protected final String fullAddress;
protected String tooltipText;
@ -59,10 +65,12 @@ public class PeerInfoIcon extends Group {
protected Pane tagPane;
protected Pane numTradesPane;
protected int numTrades = 0;
private final StringProperty tag;
public PeerInfoIcon(NodeAddress nodeAddress, Preferences preferences) {
this.preferences = preferences;
this.fullAddress = nodeAddress != null ? nodeAddress.getFullAddress() : "";
this.tag = new SimpleStringProperty("");
}
protected void createAvatar(Color ringColor) {
@ -162,23 +170,24 @@ public class PeerInfoIcon extends Group {
Res.get("peerInfo.unknownAge") :
null;
setOnMouseClicked(e -> new PeerInfoWithTagEditor(privateNotificationManager, trade, offer, preferences, useDevPrivilegeKeys)
.fullAddress(fullAddress)
.numTrades(numTrades)
.accountAge(accountAgeFormatted)
.signAge(signAgeFormatted)
.accountAgeInfo(peersAccountAgeInfo)
.signAgeInfo(peersSignAgeInfo)
.accountSigningState(accountSigningState)
.position(localToScene(new Point2D(0, 0)))
.onSave(newTag -> {
preferences.setTagForPeer(fullAddress, newTag);
updatePeerInfoIcon();
if (callback != null) {
callback.avatarTagUpdated();
}
})
.show());
setOnMouseClicked(e -> {
if (e.getButton().equals(MouseButton.PRIMARY)) {
new PeerInfoWithTagEditor(privateNotificationManager, trade, offer, preferences, useDevPrivilegeKeys)
.fullAddress(fullAddress)
.numTrades(numTrades)
.accountAge(accountAgeFormatted)
.signAge(signAgeFormatted)
.accountAgeInfo(peersAccountAgeInfo)
.signAgeInfo(peersSignAgeInfo)
.accountSigningState(accountSigningState)
.position(localToScene(new Point2D(0, 0)))
.onSave(newTag -> {
preferences.setTagForPeer(fullAddress, newTag);
tag.set(newTag);
})
.show();
}
});
}
protected double getScaleFactor() {
@ -192,20 +201,6 @@ public class PeerInfoIcon extends Group {
}
protected void updatePeerInfoIcon() {
String tag;
Map<String, String> peerTagMap = preferences.getPeerTagMap();
if (peerTagMap.containsKey(fullAddress)) {
tag = peerTagMap.get(fullAddress);
final String text = !tag.isEmpty() ? Res.get("peerInfoIcon.tooltip", tooltipText, tag) : tooltipText;
Tooltip.install(this, new Tooltip(text));
} else {
tag = "";
Tooltip.install(this, new Tooltip(tooltipText));
}
if (!tag.isEmpty())
tagLabel.setText(tag.substring(0, 1));
if (numTrades > 0) {
numTradesLabel.setText(numTrades > 99 ? "*" : String.valueOf(numTrades));
@ -216,9 +211,27 @@ public class PeerInfoIcon extends Group {
numTradesLabel.relocate(scaleFactor * 5, scaleFactor * 1);
}
}
numTradesPane.setVisible(numTrades > 0);
tagPane.setVisible(!tag.isEmpty());
refreshTag();
}
protected void refreshTag() {
Map<String, String> peerTagMap = preferences.getPeerTagMap();
if (peerTagMap.containsKey(fullAddress)) {
tag.set(peerTagMap.get(fullAddress));
}
Tooltip.install(this, new Tooltip(!tag.get().isEmpty() ?
Res.get("peerInfoIcon.tooltip", tooltipText, tag.get()) : tooltipText));
if (!tag.get().isEmpty()) {
tagLabel.setText(tag.get().substring(0, 1));
}
tagPane.setVisible(!tag.get().isEmpty());
}
protected StringProperty tagProperty() {
return tag;
}
}

View file

@ -40,8 +40,4 @@ public class PeerInfoIconDispute extends PeerInfoIcon {
addMouseListener(numTrades, null, null, null, preferences, false,
false, accountAge, 0L, null, null, null);
}
public void refreshTag() {
updatePeerInfoIcon();
}
}

View file

@ -0,0 +1,48 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import java.util.HashMap;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class PeerInfoIconMap extends HashMap<String, PeerInfoIcon> implements ChangeListener<String> {
@Override
public PeerInfoIcon put(String key, PeerInfoIcon icon) {
icon.tagProperty().addListener(this);
return super.put(key, icon);
}
@Override
public void changed(ObservableValue<? extends String> o, String oldVal, String newVal) {
log.info("Updating avatar tags, the avatar map size is {}", size());
forEach((key, icon) -> {
// We update all avatars, as some could be sharing the same tag.
// We also temporarily remove listeners to prevent firing of
// events while each icon's tagProperty is being reset.
icon.tagProperty().removeListener(this);
icon.refreshTag();
icon.tagProperty().addListener(this);
});
}
}

View file

@ -0,0 +1,257 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.main.overlays.windows;
import haveno.desktop.components.AutoTooltipButton;
import haveno.desktop.main.overlays.Overlay;
import haveno.desktop.main.portfolio.pendingtrades.steps.TradeWizardItem;
import haveno.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep1View;
import haveno.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep2View;
import haveno.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep3View;
import haveno.desktop.util.Layout;
import haveno.core.locale.Res;
import haveno.core.support.dispute.arbitration.ArbitrationManager;
import haveno.core.support.dispute.mediation.FileTransferSender;
import haveno.core.support.dispute.mediation.FileTransferSession;
import haveno.common.UserThread;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.Separator;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import static haveno.desktop.util.FormBuilder.addMultilineLabel;
@Slf4j
public class SendLogFilesWindow extends Overlay<SendLogFilesWindow> implements FileTransferSession.FtpCallback {
private final String tradeId;
private final int traderId;
private final ArbitrationManager arbitrationManager;
private Label statusLabel;
private Button sendButton, stopButton;
private final DoubleProperty ftpProgress = new SimpleDoubleProperty(-1);
TradeWizardItem step1, step2, step3;
private FileTransferSender fileTransferSender;
public SendLogFilesWindow(String tradeId, int traderId,
ArbitrationManager arbitrationManager) {
this.tradeId = tradeId;
this.traderId = traderId;
this.arbitrationManager = arbitrationManager;
type = Type.Attention;
}
public void show() {
headLine = Res.get("support.sendLogs.title");
width = 668;
createGridPane();
addHeadLine();
addContent();
addButtons();
applyStyles();
display();
}
@Override
protected void createGridPane() {
gridPane = new GridPane();
gridPane.setHgap(5);
gridPane.setVgap(5);
gridPane.setPadding(new Insets(64, 64, 64, 64));
gridPane.setPrefWidth(width);
}
void addWizardsToGridPane(TradeWizardItem tradeWizardItem) {
GridPane.setRowIndex(tradeWizardItem, rowIndex++);
GridPane.setColumnIndex(tradeWizardItem, 0);
GridPane.setHalignment(tradeWizardItem, HPos.LEFT);
gridPane.getChildren().add(tradeWizardItem);
}
void addLineSeparatorToGridPane() {
final Separator separator = new Separator(Orientation.VERTICAL);
separator.setMinHeight(22);
GridPane.setMargin(separator, new Insets(0, 0, 0, 13));
GridPane.setHalignment(separator, HPos.LEFT);
GridPane.setRowIndex(separator, rowIndex++);
gridPane.getChildren().add(separator);
}
void addRegionToGridPane() {
final Region region = new Region();
region.setMinHeight(22);
GridPane.setMargin(region, new Insets(0, 0, 0, 13));
GridPane.setRowIndex(region, rowIndex++);
gridPane.getChildren().add(region);
}
private void addContent() {
this.hideCloseButton = true;
addMultilineLabel(gridPane, ++rowIndex, Res.get("support.sendLogs.backgroundInfo"), 0);
addRegionToGridPane();
step1 = new TradeWizardItem(BuyerStep1View.class, Res.get("support.sendLogs.step1"), "1");
step2 = new TradeWizardItem(BuyerStep2View.class, Res.get("support.sendLogs.step2"), "2");
step3 = new TradeWizardItem(BuyerStep3View.class, Res.get("support.sendLogs.step3"), "3");
addRegionToGridPane();
addRegionToGridPane();
addWizardsToGridPane(step1);
addLineSeparatorToGridPane();
addWizardsToGridPane(step2);
addLineSeparatorToGridPane();
addWizardsToGridPane(step3);
addRegionToGridPane();
ProgressBar progressBar = new ProgressBar();
progressBar.setMinHeight(19);
progressBar.setMaxHeight(19);
progressBar.setPrefWidth(9305);
progressBar.setVisible(false);
progressBar.progressProperty().bind(ftpProgress);
gridPane.add(progressBar, 0, ++rowIndex);
statusLabel = addMultilineLabel(gridPane, ++rowIndex, "", -Layout.FLOATING_LABEL_DISTANCE);
statusLabel.getStyleClass().add("sub-info");
addRegionToGridPane();
sendButton = new AutoTooltipButton(Res.get("support.sendLogs.send"));
stopButton = new AutoTooltipButton(Res.get("support.sendLogs.cancel"));
stopButton.setDisable(true);
closeButton = new AutoTooltipButton(Res.get("shared.close"));
sendButton.setOnAction(e -> {
try {
progressBar.setVisible(true);
if (fileTransferSender == null) {
setActiveStep(1);
statusLabel.setText(Res.get("support.sendLogs.init"));
fileTransferSender = arbitrationManager.initLogUpload(this, tradeId, traderId);
UserThread.runAfter(() -> {
fileTransferSender.createZipFileToSend();
setActiveStep(2);
UserThread.runAfter(() -> {
setActiveStep(3);
try {
fileTransferSender.initSend();
} catch (IOException ioe) {
log.error(ioe.toString());
statusLabel.setText(ioe.toString());
ioe.printStackTrace();
}
}, 1);
}, 1);
sendButton.setDisable(true);
stopButton.setDisable(false);
} else {
// resend the latest block in the event of a timeout
statusLabel.setText(Res.get("support.sendLogs.retry"));
fileTransferSender.retrySend();
sendButton.setDisable(true);
}
} catch (IOException ex) {
log.error(ex.toString());
statusLabel.setText(ex.toString());
ex.printStackTrace();
}
});
stopButton.setOnAction(e -> {
if (fileTransferSender != null) {
fileTransferSender.resetSession();
statusLabel.setText(Res.get("support.sendLogs.stopped"));
stopButton.setDisable(true);
}
});
closeButton.setOnAction(e -> {
hide();
closeHandlerOptional.ifPresent(Runnable::run);
});
HBox hBox = new HBox();
hBox.setSpacing(10);
hBox.setAlignment(Pos.CENTER_RIGHT);
GridPane.setRowIndex(hBox, ++rowIndex);
GridPane.setColumnSpan(hBox, 2);
GridPane.setColumnIndex(hBox, 0);
hBox.getChildren().addAll(sendButton, stopButton, closeButton);
gridPane.getChildren().add(hBox);
GridPane.setMargin(hBox, new Insets(10, 0, 0, 0));
}
void setActiveStep(int step) {
if (step < 1) {
step1.setDisabled();
step2.setDisabled();
step3.setDisabled();
} else if (step == 1) {
step1.setActive();
} else if (step == 2) {
step1.setCompleted();
step2.setActive();
} else if (step == 3) {
step2.setCompleted();
step3.setActive();
} else {
step3.setCompleted();
}
}
@Override
public void onFtpProgress(double progressPct) {
UserThread.execute(() -> {
if (progressPct > 0.0) {
statusLabel.setText(String.format(Res.get("support.sendLogs.progress"), progressPct * 100));
sendButton.setDisable(true);
}
ftpProgress.set(progressPct);
});
}
@Override
public void onFtpComplete(FileTransferSession session) {
UserThread.execute(() -> {
setActiveStep(4); // all finished
statusLabel.setText(Res.get("support.sendLogs.finished"));
stopButton.setDisable(true);
});
}
@Override
public void onFtpTimeout(String statusMsg, FileTransferSession session) {
UserThread.execute(() -> {
statusLabel.setText(statusMsg + "\r\n" + Res.get("support.sendLogs.command"));
sendButton.setDisable(false);
});
}
}

View file

@ -443,7 +443,7 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
model.dataModel.getTradeManager().requestPersistence();
tradeIdOfOpenChat = trade.getId();
ChatView chatView = new ChatView(traderChatManager, formatter, Res.get("offerbook.trader"));
ChatView chatView = new ChatView(traderChatManager, Res.get("offerbook.trader"));
chatView.setAllowAttachments(false);
chatView.setDisplayHeader(false);
chatView.initialize();

View file

@ -17,35 +17,36 @@
package haveno.desktop.main.shared;
import com.google.common.io.ByteStreams;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.util.Utilities;
import haveno.desktop.components.AutoTooltipButton;
import haveno.desktop.components.AutoTooltipLabel;
import haveno.desktop.components.HavenoTextArea;
import haveno.desktop.components.BusyAnimation;
import haveno.desktop.components.TableGroupHeadline;
import haveno.desktop.main.overlays.notifications.Notification;
import haveno.desktop.main.overlays.popups.Popup;
import haveno.desktop.util.DisplayUtils;
import haveno.desktop.util.GUIUtil;
import haveno.core.locale.Res;
import haveno.core.support.SupportManager;
import haveno.core.support.SupportSession;
import haveno.core.support.dispute.Attachment;
import haveno.core.support.messages.ChatMessage;
import haveno.core.util.coin.CoinFormatter;
import haveno.desktop.components.AutoTooltipButton;
import haveno.desktop.components.AutoTooltipLabel;
import haveno.desktop.components.BusyAnimation;
import haveno.desktop.components.HavenoTextArea;
import haveno.desktop.components.TableGroupHeadline;
import haveno.desktop.components.TextFieldWithIcon;
import haveno.desktop.main.overlays.popups.Popup;
import haveno.desktop.util.DisplayUtils;
import haveno.desktop.util.GUIUtil;
import haveno.network.p2p.network.Connection;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.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.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
@ -61,22 +62,31 @@ import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;
import javafx.stage.FileChooser;
import javafx.util.Callback;
import lombok.Getter;
import lombok.Setter;
import javafx.geometry.Insets;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
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.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
@ -84,8 +94,14 @@ import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
public class ChatView extends AnchorPane {
public static final Logger log = LoggerFactory.getLogger(TextFieldWithIcon.class);
// UI
private TextArea inputTextArea;
@ -98,7 +114,7 @@ public class ChatView extends AnchorPane {
// Options
@Getter
Button extraButton;
Node extraButton;
@Getter
private ReadOnlyDoubleProperty widthProperty;
@Setter
@ -112,18 +128,16 @@ public class ChatView extends AnchorPane {
private ListChangeListener<ChatMessage> disputeDirectMessageListListener;
private Subscription inputTextAreaTextSubscription;
private final List<Attachment> tempAttachments = new ArrayList<>();
private ChangeListener<Boolean> storedInMailboxPropertyListener, arrivedPropertyListener;
private ChangeListener<Boolean> storedInMailboxPropertyListener, acknowledgedPropertyListener;
private ChangeListener<String> sendMessageErrorPropertyListener;
protected final CoinFormatter formatter;
private EventHandler<KeyEvent> keyEventEventHandler;
private SupportManager supportManager;
private Optional<SupportSession> optionalSupportSession = Optional.empty();
private String counterpartyName;
public ChatView(SupportManager supportManager, CoinFormatter formatter, String counterpartyName) {
public ChatView(SupportManager supportManager, String counterpartyName) {
this.supportManager = supportManager;
this.formatter = formatter;
this.counterpartyName = counterpartyName;
allowAttachments = true;
displayHeader = true;
@ -157,7 +171,7 @@ public class ChatView extends AnchorPane {
}
public void display(SupportSession supportSession,
@Nullable Button extraButton,
@Nullable Node extraButton,
ReadOnlyDoubleProperty widthProperty) {
optionalSupportSession = Optional.of(supportSession);
removeListenersOnSessionChange();
@ -201,6 +215,10 @@ public class ChatView extends AnchorPane {
Button uploadButton = new AutoTooltipButton(Res.get("support.addAttachments"));
uploadButton.setOnAction(e -> onRequestUpload());
Button clipboardButton = new AutoTooltipButton(Res.get("shared.copyToClipboard"));
clipboardButton.setOnAction(e -> copyChatMessagesToClipboard(clipboardButton));
uploadButton.setStyle("-fx-pref-width: 125; -fx-padding: 3 3 3 3;");
clipboardButton.setStyle("-fx-pref-width: 125; -fx-padding: 3 3 3 3;");
sendMsgInfoLabel = new AutoTooltipLabel();
sendMsgInfoLabel.setVisible(false);
@ -216,12 +234,11 @@ public class ChatView extends AnchorPane {
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
if (allowAttachments)
buttonBox.getChildren().addAll(sendButton, uploadButton, sendMsgBusyAnimation, sendMsgInfoLabel);
buttonBox.getChildren().addAll(sendButton, uploadButton, clipboardButton, sendMsgBusyAnimation, sendMsgInfoLabel);
else
buttonBox.getChildren().addAll(sendButton, sendMsgBusyAnimation, sendMsgInfoLabel);
if (extraButton != null) {
extraButton.setDefaultButton(true);
Pane spacer = new Pane();
HBox.setHgrow(spacer, Priority.ALWAYS);
buttonBox.getChildren().addAll(spacer, extraButton);
@ -329,7 +346,7 @@ public class ChatView extends AnchorPane {
bg.setId("message-bubble-green");
messageLabel.getStyleClass().add("my-message");
copyIcon.getStyleClass().add("my-message");
message.addChangeListener(() -> updateMsgState(message));
message.addWeakMessageStateListener(() -> updateMsgState(message));
updateMsgState(message);
} else if (isMyMsg) {
headerLabel.getStyleClass().add("my-message-header");
@ -350,7 +367,7 @@ public class ChatView extends AnchorPane {
};
sendMsgBusyAnimation.isRunningProperty().addListener(sendMsgBusyAnimationListener);
message.addChangeListener(() -> updateMsgState(message));
message.addWeakMessageStateListener(() -> updateMsgState(message));
updateMsgState(message);
} else {
headerLabel.getStyleClass().add("message-header");
@ -401,13 +418,13 @@ public class ChatView extends AnchorPane {
String metaData = DisplayUtils.formatDateTime(new Date(message.getDate()));
if (!message.isSystemMessage())
metaData = (isMyMsg ? "Sent " : "Received ") + metaData
+ (isMyMsg ? "" : " from " + counterpartyName);
+ (isMyMsg ? "" : " from " + counterpartyName);
headerLabel.setText(metaData);
messageLabel.setText(message.getMessage());
attachmentsBox.getChildren().clear();
if (allowAttachments &&
message.getAttachments() != null &&
!message.getAttachments().isEmpty()) {
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));
@ -466,6 +483,10 @@ public class ChatView extends AnchorPane {
visible = true;
icon = AwesomeIcon.OK_SIGN;
text = Res.get("support.acknowledged");
} else if (message.storedInMailboxProperty().get()) {
visible = true;
icon = AwesomeIcon.ENVELOPE;
text = Res.get("support.savedInMailbox");
} else if (message.ackErrorProperty().get() != null) {
visible = true;
icon = AwesomeIcon.EXCLAMATION_SIGN;
@ -474,17 +495,13 @@ public class ChatView extends AnchorPane {
statusInfoLabel.getStyleClass().add("error-text");
} else if (message.arrivedProperty().get()) {
visible = true;
icon = AwesomeIcon.OK;
text = Res.get("support.arrived");
} else if (message.storedInMailboxProperty().get()) {
visible = true;
icon = AwesomeIcon.ENVELOPE;
text = Res.get("support.savedInMailbox");
icon = AwesomeIcon.MAIL_REPLY;
text = Res.get("support.transient");
} else {
visible = false;
log.debug("updateMsgState called but no msg state available. message={}", message);
}
statusHBox.setVisible(visible);
if (visible) {
AwesomeDude.setIcon(statusIcon, icon, "14");
@ -529,7 +546,7 @@ public class ChatView extends AnchorPane {
int maxMsgSize = Connection.getPermittedMessageSize();
int maxSizeInKB = maxMsgSize / 1024;
fileChooser.setTitle(Res.get("support.openFile", maxSizeInKB));
/* if (Utilities.isUnix())
/* if (Utilities.isUnix())
fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));*/
File result = fileChooser.showOpenDialog(getScene().getWindow());
if (result != null) {
@ -561,13 +578,51 @@ public class ChatView extends AnchorPane {
}
}
public void onAttachText(String textAttachment, String name) {
if (!allowAttachments)
return;
try {
byte[] filesAsBytes = textAttachment.getBytes("UTF8");
int size = filesAsBytes.length;
int maxMsgSize = Connection.getPermittedMessageSize();
int maxSizeInKB = maxMsgSize / 1024;
if (size > maxMsgSize) {
new Popup().warning(Res.get("support.attachmentTooLarge", (size / 1024), maxSizeInKB)).show();
} else {
tempAttachments.add(new Attachment(name, filesAsBytes));
inputTextArea.setText(inputTextArea.getText() + "\n[" + Res.get("support.attachment") + " " + name + "]");
}
} catch (Exception e) {
log.error(e.toString());
e.printStackTrace();
}
}
private void copyChatMessagesToClipboard(Button sourceBtn) {
optionalSupportSession.ifPresent(session -> {
StringBuilder stringBuilder = new StringBuilder();
chatMessages.forEach(i -> {
String metaData = DisplayUtils.formatDateTime(new Date(i.getDate()));
metaData = metaData + (i.isSystemMessage() ? " (System message)" :
(i.isSenderIsTrader() ? " (from Trader)" : " (from Agent)"));
stringBuilder.append(metaData).append("\n").append(i.getMessage()).append("\n\n");
});
Utilities.copyToClipboard(stringBuilder.toString());
new Notification()
.notification(Res.get("shared.copiedToClipboard"))
.hideCloseButton()
.autoClose()
.show();
});
}
private void onOpenAttachment(Attachment attachment) {
if (!allowAttachments)
return;
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(Res.get("support.save"));
fileChooser.setInitialFileName(attachment.getFileName());
/* if (Utilities.isUnix())
/* if (Utilities.isUnix())
fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));*/
File file = fileChooser.showSaveDialog(getScene().getWindow());
if (file != null) {
@ -582,7 +637,7 @@ public class ChatView extends AnchorPane {
private void onSendMessage(String inputText) {
if (chatMessage != null) {
chatMessage.arrivedProperty().removeListener(arrivedPropertyListener);
chatMessage.acknowledgedProperty().removeListener(acknowledgedPropertyListener);
chatMessage.storedInMailboxProperty().removeListener(storedInMailboxPropertyListener);
chatMessage.sendMessageErrorProperty().removeListener(sendMessageErrorPropertyListener);
}
@ -594,6 +649,8 @@ public class ChatView extends AnchorPane {
inputTextArea.setDisable(true);
inputTextArea.clear();
chatMessage.startAckTimer();
Timer timer = UserThread.runAfter(() -> {
sendMsgInfoLabel.setVisible(true);
sendMsgInfoLabel.setManaged(true);
@ -602,8 +659,9 @@ public class ChatView extends AnchorPane {
sendMsgBusyAnimation.play();
}, 500, TimeUnit.MILLISECONDS);
arrivedPropertyListener = (observable, oldValue, newValue) -> {
acknowledgedPropertyListener = (observable, oldValue, newValue) -> {
if (newValue) {
sendMsgInfoLabel.setVisible(false);
hideSendMsgInfo(timer);
}
};
@ -624,7 +682,7 @@ public class ChatView extends AnchorPane {
}
};
if (chatMessage != null) {
chatMessage.arrivedProperty().addListener(arrivedPropertyListener);
chatMessage.acknowledgedProperty().addListener(acknowledgedPropertyListener);
chatMessage.storedInMailboxProperty().addListener(storedInMailboxPropertyListener);
chatMessage.sendMessageErrorProperty().addListener(sendMessageErrorPropertyListener);
}
@ -697,15 +755,12 @@ public class ChatView extends AnchorPane {
}
private void removeListenersOnSessionChange() {
if (chatMessages != null) {
if (disputeDirectMessageListListener != null) chatMessages.removeListener(disputeDirectMessageListListener);
chatMessages.forEach(ChatMessage::removeChangeListener);
}
if (chatMessages != null && disputeDirectMessageListListener != null)
chatMessages.removeListener(disputeDirectMessageListListener);
if (chatMessage != null) {
chatMessage.removeChangeListener();
if (arrivedPropertyListener != null)
chatMessage.arrivedProperty().removeListener(arrivedPropertyListener);
if (acknowledgedPropertyListener != null)
chatMessage.arrivedProperty().removeListener(acknowledgedPropertyListener);
if (storedInMailboxPropertyListener != null)
chatMessage.storedInMailboxProperty().removeListener(storedInMailboxPropertyListener);
}
@ -722,4 +777,4 @@ public class ChatView extends AnchorPane {
inputTextAreaTextSubscription.unsubscribe();
}
}
}

View file

@ -1,5 +1,5 @@
/*
* This file is part of Bisq.
* This file is part of haveno.
*
* 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
@ -12,56 +12,68 @@
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
* along with haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.main.support.dispute;
import haveno.common.UserThread;
import haveno.desktop.components.AutoTooltipButton;
import haveno.desktop.main.MainView;
import haveno.desktop.main.shared.ChatView;
import haveno.desktop.util.CssTheme;
import haveno.desktop.util.DisplayUtils;
import haveno.core.locale.Res;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeList;
import haveno.core.support.dispute.DisputeManager;
import haveno.core.support.dispute.DisputeSession;
import haveno.core.support.messages.ChatMessage;
import haveno.core.user.Preferences;
import haveno.core.util.coin.CoinFormatter;
import haveno.desktop.components.AutoTooltipButton;
import haveno.desktop.main.MainView;
import haveno.desktop.main.shared.ChatView;
import haveno.desktop.util.CssTheme;
import javafx.beans.value.ChangeListener;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import haveno.common.UserThread;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import javafx.beans.value.ChangeListener;
import java.util.Date;
import java.util.List;
import lombok.Getter;
public class DisputeChatPopup {
public interface ChatCallback {
void onCloseDisputeFromChatWindow(Dispute dispute);
void onSendLogsFromChatWindow(Dispute dispute);
}
private Stage chatPopupStage;
protected final DisputeManager<? extends DisputeList<Dispute>> disputeManager;
protected final CoinFormatter formatter;
protected final Preferences preferences;
private ChatCallback chatCallback;
private final ChatCallback chatCallback;
private double chatPopupStageXPosition = -1;
private double chatPopupStageYPosition = -1;
private ChangeListener<Number> xPositionListener;
private ChangeListener<Number> yPositionListener;
@Getter private Dispute selectedDispute;
DisputeChatPopup(DisputeManager<? extends DisputeList<Dispute>> disputeManager,
CoinFormatter formatter,
Preferences preferences,
ChatCallback chatCallback) {
CoinFormatter formatter,
Preferences preferences,
ChatCallback chatCallback) {
this.disputeManager = disputeManager;
this.formatter = formatter;
this.preferences = preferences;
@ -84,7 +96,7 @@ public class DisputeChatPopup {
selectedDispute.getChatMessages().forEach(m -> m.setWasDisplayed(true));
disputeManager.requestPersistence();
ChatView chatView = new ChatView(disputeManager, formatter, counterpartyName);
ChatView chatView = new ChatView(disputeManager, counterpartyName);
chatView.setAllowAttachments(true);
chatView.setDisplayHeader(false);
chatView.initialize();
@ -96,12 +108,27 @@ public class DisputeChatPopup {
AnchorPane.setTopAnchor(chatView, -20d);
AnchorPane.setBottomAnchor(chatView, 10d);
pane.getStyleClass().add("dispute-chat-border");
Button closeDisputeButton = null;
if (!selectedDispute.isClosed() && !disputeManager.isTrader(selectedDispute)) {
closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket"));
closeDisputeButton.setOnAction(e -> chatCallback.onCloseDisputeFromChatWindow(selectedDispute));
if (selectedDispute.isClosed()) {
chatView.display(concreteDisputeSession, null, pane.widthProperty());
} else {
if (disputeManager.isAgent(selectedDispute)) {
Button closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket"));
closeDisputeButton.setDefaultButton(true);
closeDisputeButton.setOnAction(e -> chatCallback.onCloseDisputeFromChatWindow(selectedDispute));
chatView.display(concreteDisputeSession, closeDisputeButton, pane.widthProperty());
} else {
MenuButton menuButton = new MenuButton(Res.get("support.moreButton"));
MenuItem menuItem1 = new MenuItem(Res.get("support.uploadTraderChat"));
MenuItem menuItem2 = new MenuItem(Res.get("support.sendLogFiles"));
menuItem1.setOnAction(e -> doTextAttachment(chatView));
setChatUploadEnabledState(menuItem1);
menuItem2.setOnAction(e -> chatCallback.onSendLogsFromChatWindow(selectedDispute));
menuButton.getItems().addAll(menuItem1, menuItem2);
menuButton.getStyleClass().add("jfx-button");
menuButton.setStyle("-fx-padding: 0 10 0 10;");
chatView.display(concreteDisputeSession, menuButton, pane.widthProperty());
}
}
chatView.display(concreteDisputeSession, closeDisputeButton, pane.widthProperty());
chatView.activate();
chatView.scrollToBottom();
chatPopupStage = new Stage();
@ -132,9 +159,9 @@ public class DisputeChatPopup {
chatPopupStage.setOpacity(0);
chatPopupStage.show();
xPositionListener = (observable, oldValue, newValue) -> chatPopupStageXPosition = (double) newValue;
ChangeListener<Number> xPositionListener = (observable, oldValue, newValue) -> chatPopupStageXPosition = (double) newValue;
chatPopupStage.xProperty().addListener(xPositionListener);
yPositionListener = (observable, oldValue, newValue) -> chatPopupStageYPosition = (double) newValue;
ChangeListener<Number> yPositionListener = (observable, oldValue, newValue) -> chatPopupStageYPosition = (double) newValue;
chatPopupStage.yProperty().addListener(yPositionListener);
if (chatPopupStageXPosition == -1) {
@ -149,8 +176,33 @@ public class DisputeChatPopup {
// 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(() -> {
if (chatPopupStage != null) chatPopupStage.setOpacity(1);
UserThread.execute(() -> chatPopupStage.setOpacity(1));
}
private void doTextAttachment(ChatView chatView) {
disputeManager.findTrade(selectedDispute).ifPresent(t -> {
List<ChatMessage> chatMessages = t.getChatMessages();
if (chatMessages.size() > 0) {
StringBuilder stringBuilder = new StringBuilder();
chatMessages.forEach(i -> {
boolean isMyMsg = i.isSenderIsTrader();
String metaData = DisplayUtils.formatDateTime(new Date(i.getDate()));
if (!i.isSystemMessage())
metaData = (isMyMsg ? "Sent " : "Received ") + metaData
+ (isMyMsg ? "" : " from Trader");
stringBuilder.append(metaData).append("\n").append(i.getMessage()).append("\n\n");
});
String fileName = selectedDispute.getShortTradeId() + "_" + selectedDispute.getRoleStringForLogFile() + "_TraderChat.txt";
chatView.onAttachText(stringBuilder.toString(), fileName);
}
});
}
private void setChatUploadEnabledState(MenuItem menuItem) {
disputeManager.findTrade(selectedDispute).ifPresentOrElse(t -> {
menuItem.setDisable(t.getChatMessages().size() == 0);
}, () -> {
menuItem.setDisable(true);
});
}
}

View file

@ -51,6 +51,7 @@ import haveno.core.support.dispute.DisputeManager;
import haveno.core.support.dispute.DisputeResult;
import haveno.core.support.dispute.DisputeSession;
import haveno.core.support.dispute.agent.DisputeAgentLookupMap;
import haveno.core.support.dispute.arbitration.ArbitrationManager;
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import haveno.core.support.dispute.mediation.MediationManager;
import haveno.core.support.messages.ChatMessage;
@ -67,9 +68,12 @@ import haveno.desktop.components.AutoTooltipLabel;
import haveno.desktop.components.AutoTooltipTableColumn;
import haveno.desktop.components.HyperlinkWithIcon;
import haveno.desktop.components.InputTextField;
import haveno.desktop.components.PeerInfoIconDispute;
import haveno.desktop.components.PeerInfoIconMap;
import haveno.desktop.main.overlays.popups.Popup;
import haveno.desktop.main.overlays.windows.ContractWindow;
import haveno.desktop.main.overlays.windows.DisputeSummaryWindow;
import haveno.desktop.main.overlays.windows.SendLogFilesWindow;
import haveno.desktop.main.overlays.windows.SendPrivateNotificationWindow;
import haveno.desktop.main.overlays.windows.TradeDetailsWindow;
import haveno.desktop.main.overlays.windows.VerifyDisputeResultSignatureWindow;
@ -119,7 +123,7 @@ import java.util.concurrent.atomic.AtomicReference;
import static haveno.desktop.util.FormBuilder.getIconForLabel;
import static haveno.desktop.util.FormBuilder.getRegularIconButton;
public abstract class DisputeView extends ActivatableView<VBox, Void> {
public abstract class DisputeView extends ActivatableView<VBox, Void> implements DisputeChatPopup.ChatCallback {
public enum FilterResult {
NO_MATCH("No Match"),
NO_FILTER("No filter text"),
@ -181,6 +185,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
private Map<String, Button> chatButtonByDispute = new HashMap<>();
private Map<String, JFXBadge> chatBadgeByDispute = new HashMap<>();
private Map<String, JFXBadge> newBadgeByDispute = new HashMap<>();
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
private final PeerInfoIconMap avatarMap = new PeerInfoIconMap();
protected DisputeChatPopup chatPopup;
@ -212,8 +218,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
this.accountAgeWitnessService = accountAgeWitnessService;
this.arbitratorManager = arbitratorManager;
this.useDevPrivilegeKeys = useDevPrivilegeKeys;
DisputeChatPopup.ChatCallback chatCallback = this::handleOnProcessDispute;
chatPopup = new DisputeChatPopup(disputeManager, formatter, preferences, chatCallback);
chatPopup = new DisputeChatPopup(disputeManager, formatter, preferences, this);
}
@Override
@ -223,6 +228,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
HBox.setHgrow(label, Priority.NEVER);
filterTextField = new InputTextField();
filterTextField.setPromptText(Res.get("support.filter.prompt"));
Tooltip tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(100));
tooltip.setShowDuration(Duration.seconds(10));
@ -382,7 +388,9 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
ObservableList<ChatMessage> chatMessages = dispute.getChatMessages();
// If last message is not a result message we re-open as we might have received a new message from the
// trader/mediator/arbitrator who has reopened the case
if (!chatMessages.isEmpty() && !chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute)) {
if (!chatMessages.isEmpty() &&
!chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute) &&
dispute.unreadMessageCount(senderFlag()) > 0) {
onSelectDispute(dispute);
reOpenDispute();
}
@ -428,7 +436,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
// For open filter we do not want to continue further as json data would cause a match
if (filter.equalsIgnoreCase("open")) {
return !dispute.isClosed() ? FilterResult.OPEN_DISPUTES : FilterResult.NO_MATCH;
return !dispute.isClosed() || dispute.unreadMessageCount(senderFlag()) > 0 ?
FilterResult.OPEN_DISPUTES : FilterResult.NO_MATCH;
}
if (dispute.getTradeId().toLowerCase().contains(filter)) {
@ -1083,7 +1092,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
private TableColumn<Dispute, Dispute> getDateColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("shared.date")) {
{
setMinWidth(180);
setMinWidth(100);
setPrefWidth(150);
}
};
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
@ -1109,7 +1119,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
private TableColumn<Dispute, Dispute> getTradeIdColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("shared.tradeId")) {
{
setMinWidth(110);
setMinWidth(50);
setPrefWidth(100);
}
};
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
@ -1167,10 +1178,14 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
@Override
public void updateItem(final Dispute item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty)
if (item != null && !empty) {
setText(getBuyerOnionAddressColumnLabel(item));
else
PeerInfoIconDispute peerInfoIconDispute = createAvatar(tableRowProperty().get().getIndex(), item, true);
setGraphic(peerInfoIconDispute);
} else {
setText("");
setText(null);
}
}
};
}
@ -1193,10 +1208,14 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
@Override
public void updateItem(final Dispute item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty)
if (item != null && !empty) {
setText(getSellerOnionAddressColumnLabel(item));
else
PeerInfoIconDispute peerInfoIconDispute = createAvatar(tableRowProperty().get().getIndex(), item, false);
setGraphic(peerInfoIconDispute);
} else {
setText("");
setGraphic(null);
}
}
};
}
@ -1314,8 +1333,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
return;
}
String keyBaseUserName = DisputeAgentLookupMap.getMatrixUserName(agentNodeAddress.getFullAddress());
setText(keyBaseUserName);
String MatrixUserName = DisputeAgentLookupMap.getMatrixUserName(agentNodeAddress.getFullAddress());
setText(MatrixUserName);
} else {
setText("");
}
@ -1448,4 +1467,36 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
return (disputeManager instanceof MediationManager) ? Res.get("shared.mediator") : Res.get("shared.refundAgent");
}
}
private PeerInfoIconDispute createAvatar(Integer tableRowId, Dispute dispute, boolean isBuyer) {
NodeAddress nodeAddress = isBuyer ? dispute.getContract().getBuyerNodeAddress() : dispute.getContract().getSellerNodeAddress();
String key = tableRowId + nodeAddress.getHostNameWithoutPostFix() + (isBuyer ? "BUYER" : "SELLER");
Long accountAge = isBuyer ?
accountAgeWitnessService.getAccountAge(dispute.getBuyerPaymentAccountPayload(), dispute.getContract().getBuyerPubKeyRing()) :
accountAgeWitnessService.getAccountAge(dispute.getSellerPaymentAccountPayload(), dispute.getContract().getSellerPubKeyRing());
PeerInfoIconDispute peerInfoIcon = new PeerInfoIconDispute(
nodeAddress,
disputeManager.getNrOfDisputes(isBuyer, dispute.getContract()),
accountAge,
preferences);
avatarMap.put(key, peerInfoIcon); // TODO
return peerInfoIcon;
}
@Override
public void onCloseDisputeFromChatWindow(Dispute dispute) {
if (dispute.getDisputeState() == Dispute.State.NEW || dispute.getDisputeState() == Dispute.State.OPEN) {
handleOnProcessDispute(dispute);
} else {
closeDisputeFromButton();
}
}
@Override
public void onSendLogsFromChatWindow(Dispute dispute) {
if (!(disputeManager instanceof ArbitrationManager))
return;
ArbitrationManager arbitrationManager = (ArbitrationManager) disputeManager;
new SendLogFilesWindow(dispute.getTradeId(), dispute.getTraderId(), arbitrationManager).show();
}
}