mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-10-01 01:35:48 -04:00
update chat views from upstream, support sending logs
Co-authored-by: jmacxx <47253594+jmacxx@users.noreply.github.com>
This commit is contained in:
parent
833cdb3b84
commit
1647a582f5
@ -26,6 +26,7 @@ import org.apache.commons.io.IOUtils;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@ -35,6 +36,7 @@ import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
@Slf4j
|
||||
public class FileUtil {
|
||||
@ -240,4 +242,14 @@ public class FileUtil {
|
||||
renameFile(storageFile, corruptedFile);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean doesFileContainKeyword(File file, String keyword) throws FileNotFoundException {
|
||||
Scanner s = new Scanner(file);
|
||||
while (s.hasNextLine()) {
|
||||
if (s.nextLine().contains(keyword)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ public class CoreNotificationService {
|
||||
sendNotification(NotificationMessage.newBuilder()
|
||||
.setType(NotificationType.CHAT_MESSAGE)
|
||||
.setTimestamp(System.currentTimeMillis())
|
||||
.setChatMessage(chatMessage.toProtoChatMessageBuilder())
|
||||
.setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage())
|
||||
.build());
|
||||
}
|
||||
|
||||
|
@ -55,6 +55,7 @@ import haveno.core.trade.messages.SignContractResponse;
|
||||
import haveno.network.p2p.AckMessage;
|
||||
import haveno.network.p2p.BundleOfEnvelopes;
|
||||
import haveno.network.p2p.CloseConnectionMessage;
|
||||
import haveno.network.p2p.FileTransferPart;
|
||||
import haveno.network.p2p.PrefixedSealedAndSignedMessage;
|
||||
import haveno.network.p2p.peers.getdata.messages.GetDataResponse;
|
||||
import haveno.network.p2p.peers.getdata.messages.GetUpdatedDataRequest;
|
||||
@ -178,6 +179,9 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo
|
||||
case GET_INVENTORY_RESPONSE:
|
||||
return GetInventoryResponse.fromProto(proto.getGetInventoryResponse(), messageVersion);
|
||||
|
||||
case FILE_TRANSFER_PART:
|
||||
return FileTransferPart.fromProto(proto.getFileTransferPart(), messageVersion);
|
||||
|
||||
default:
|
||||
throw new ProtobufferException("Unknown proto message case (PB.NetworkEnvelope). messageCase=" +
|
||||
proto.getMessageCase() + "; proto raw data=" + proto.toString());
|
||||
|
@ -30,8 +30,13 @@ import haveno.core.locale.Res;
|
||||
import haveno.core.payment.payload.PaymentAccountPayload;
|
||||
import haveno.core.proto.CoreProtoResolver;
|
||||
import haveno.core.support.SupportType;
|
||||
import haveno.core.support.dispute.mediation.FileTransferReceiver;
|
||||
import haveno.core.support.dispute.mediation.FileTransferSender;
|
||||
import haveno.core.support.dispute.mediation.FileTransferSession;
|
||||
import haveno.core.support.messages.ChatMessage;
|
||||
import haveno.core.trade.Contract;
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
import haveno.network.p2p.network.NetworkNode;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@ -49,6 +54,8 @@ import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
@ -151,6 +158,25 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
||||
private transient final BooleanProperty isClosedProperty = new SimpleBooleanProperty();
|
||||
private transient final IntegerProperty badgeCountProperty = new SimpleIntegerProperty();
|
||||
|
||||
private transient FileTransferReceiver fileTransferSession = null;
|
||||
|
||||
public FileTransferReceiver createOrGetFileTransferReceiver(NetworkNode networkNode,
|
||||
NodeAddress peerNodeAddress,
|
||||
FileTransferSession.FtpCallback callback) throws IOException {
|
||||
// the receiver stores its state temporarily here in the dispute
|
||||
// this method gets called to retrieve the session each time a part of the log files is received
|
||||
if (fileTransferSession == null) {
|
||||
fileTransferSession = new FileTransferReceiver(networkNode, peerNodeAddress, this.tradeId, this.traderId, this.getRoleStringForLogFile(), callback);
|
||||
}
|
||||
return fileTransferSession;
|
||||
}
|
||||
|
||||
public FileTransferSender createFileTransferSender(NetworkNode networkNode,
|
||||
NodeAddress peerNodeAddress,
|
||||
FileTransferSession.FtpCallback callback) {
|
||||
return new FileTransferSender(networkNode, peerNodeAddress, this.tradeId, this.traderId, this.getRoleStringForLogFile(), false, callback);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
@ -478,6 +504,11 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
||||
}
|
||||
}
|
||||
|
||||
public String getRoleStringForLogFile() {
|
||||
return (disputeOpenerIsBuyer ? "BUYER" : "SELLER") + "_"
|
||||
+ (disputeOpenerIsMaker ? "MAKER" : "TAKER");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PaymentAccountPayload getBuyerPaymentAccountPayload() {
|
||||
return contract.isBuyerMakerAndSellerTaker() ? makerPaymentAccountPayload : takerPaymentAccountPayload;
|
||||
|
@ -940,7 +940,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
return new Tuple2<>(peerNodeAddress, receiverPubKeyRing);
|
||||
}
|
||||
|
||||
private boolean isAgent(Dispute dispute) {
|
||||
public boolean isAgent(Dispute dispute) {
|
||||
return keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing());
|
||||
}
|
||||
|
||||
@ -1038,6 +1038,20 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
requestPersistence();
|
||||
}
|
||||
|
||||
protected void addMediationLogsReceivedMessage(Dispute dispute, String logsIdentifier) {
|
||||
String logsReceivedMessage = Res.get("support.mediatorReceivedLogs", logsIdentifier);
|
||||
ChatMessage chatMessage = new ChatMessage(
|
||||
getSupportType(),
|
||||
dispute.getTradeId(),
|
||||
keyRing.hashCode(),
|
||||
false,
|
||||
logsReceivedMessage,
|
||||
p2PService.getAddress());
|
||||
chatMessage.setSystemMessage(true);
|
||||
dispute.addAndPersistChatMessage(chatMessage);
|
||||
requestPersistence();
|
||||
}
|
||||
|
||||
// If price was going down between take offer time and open dispute time the buyer has an incentive to
|
||||
// not send the payment but to try to make a new trade with the better price. We risks to lose part of the
|
||||
// security deposit (in mediation we will always get back 0.003 BTC to keep some incentive to accept mediated
|
||||
|
@ -43,6 +43,7 @@ import haveno.common.UserThread;
|
||||
import haveno.common.app.Version;
|
||||
import haveno.common.config.Config;
|
||||
import haveno.common.crypto.KeyRing;
|
||||
import haveno.common.proto.network.NetworkEnvelope;
|
||||
import haveno.core.api.XmrConnectionService;
|
||||
import haveno.core.api.CoreNotificationService;
|
||||
import haveno.core.locale.Res;
|
||||
@ -55,6 +56,9 @@ import haveno.core.support.dispute.DisputeResult;
|
||||
import haveno.core.support.dispute.DisputeResult.Winner;
|
||||
import haveno.core.support.dispute.DisputeSummaryVerification;
|
||||
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
import haveno.core.support.dispute.mediation.FileTransferReceiver;
|
||||
import haveno.core.support.dispute.mediation.FileTransferSender;
|
||||
import haveno.core.support.dispute.mediation.FileTransferSession;
|
||||
import haveno.core.support.dispute.messages.DisputeClosedMessage;
|
||||
import haveno.core.support.dispute.messages.DisputeOpenedMessage;
|
||||
import haveno.core.support.messages.ChatMessage;
|
||||
@ -67,8 +71,11 @@ import haveno.core.trade.TradeManager;
|
||||
import haveno.core.xmr.wallet.TradeWalletService;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
import haveno.network.p2p.AckMessageSourceType;
|
||||
import haveno.network.p2p.FileTransferPart;
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
import haveno.network.p2p.P2PService;
|
||||
import haveno.network.p2p.network.Connection;
|
||||
import haveno.network.p2p.network.MessageListener;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.wallet.MoneroWallet;
|
||||
import monero.wallet.model.MoneroDestination;
|
||||
@ -76,6 +83,7 @@ import monero.wallet.model.MoneroMultisigSignResult;
|
||||
import monero.wallet.model.MoneroTxSet;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@ -88,7 +96,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeList> {
|
||||
public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeList> implements MessageListener, FileTransferSession.FtpCallback {
|
||||
|
||||
private final ArbitratorManager arbitratorManager;
|
||||
|
||||
@ -116,6 +124,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
openOfferManager, keyRing, arbitrationDisputeListService, config, priceFeedService);
|
||||
this.arbitratorManager = arbitratorManager;
|
||||
HavenoUtils.arbitrationManager = this; // TODO: storing static reference, better way?
|
||||
p2PService.getNetworkNode().addMessageListener(this); // listening for FileTransferPart message
|
||||
}
|
||||
|
||||
|
||||
@ -497,4 +506,60 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FileTransferSender initLogUpload(FileTransferSession.FtpCallback callback,
|
||||
String tradeId,
|
||||
int traderId) throws IOException {
|
||||
Dispute dispute = findDispute(tradeId, traderId)
|
||||
.orElseThrow(() -> new IOException("could not locate Dispute for tradeId/traderId"));
|
||||
return dispute.createFileTransferSender(p2PService.getNetworkNode(),
|
||||
dispute.getContract().getArbitratorNodeAddress(), callback);
|
||||
}
|
||||
|
||||
private void processFilePartReceived(FileTransferPart ftp) {
|
||||
if (!ftp.isInitialRequest()) {
|
||||
return; // existing sessions are processed by FileTransferSession object directly
|
||||
}
|
||||
// we create a new session which is related to an open dispute from our list
|
||||
Optional<Dispute> dispute = findDispute(ftp.getTradeId(), ftp.getTraderId());
|
||||
if (dispute.isEmpty()) {
|
||||
log.error("Received log upload request for unknown TradeId/TraderId {}/{}", ftp.getTradeId(), ftp.getTraderId());
|
||||
return;
|
||||
}
|
||||
if (dispute.get().isClosed()) {
|
||||
log.error("Received a file transfer request for closed dispute {}", ftp.getTradeId());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
FileTransferReceiver session = dispute.get().createOrGetFileTransferReceiver(
|
||||
p2PService.getNetworkNode(), ftp.getSenderNodeAddress(), this);
|
||||
session.processFilePartReceived(ftp);
|
||||
} catch (IOException e) {
|
||||
log.error("Unable to process a received file message" + e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
|
||||
if (networkEnvelope instanceof FileTransferPart) { // mediator receiving log file data
|
||||
FileTransferPart ftp = (FileTransferPart) networkEnvelope;
|
||||
processFilePartReceived(ftp);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFtpProgress(double progressPct) {
|
||||
log.trace("ftp progress: {}", progressPct);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFtpComplete(FileTransferSession session) {
|
||||
Optional<Dispute> dispute = findDispute(session.getFullTradeId(), session.getTraderId());
|
||||
dispute.ifPresent(d -> addMediationLogsReceivedMessage(d, session.getZipId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFtpTimeout(String statusMsg, FileTransferSession session) {
|
||||
session.resetSession();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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.core.support.dispute.mediation;
|
||||
|
||||
import haveno.network.p2p.AckMessage;
|
||||
import haveno.network.p2p.AckMessageSourceType;
|
||||
import haveno.network.p2p.FileTransferPart;
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
import haveno.network.p2p.network.NetworkNode;
|
||||
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.config.Config;
|
||||
import haveno.common.util.Utilities;
|
||||
|
||||
import java.nio.file.FileSystems;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@Slf4j
|
||||
public class FileTransferReceiver extends FileTransferSession {
|
||||
protected final String zipFilePath;
|
||||
|
||||
public FileTransferReceiver(NetworkNode networkNode,
|
||||
NodeAddress peerNodeAddress,
|
||||
String tradeId,
|
||||
int traderId,
|
||||
String traderRole,
|
||||
@Nullable FileTransferSession.FtpCallback callback) throws IOException {
|
||||
super(networkNode, peerNodeAddress, tradeId, traderId, traderRole, callback);
|
||||
zipFilePath = ensureReceivingDirectoryExists().getAbsolutePath() + FileSystems.getDefault().getSeparator() + zipId + ".zip";
|
||||
}
|
||||
|
||||
public void processFilePartReceived(FileTransferPart ftp) {
|
||||
checkpointLastActivity();
|
||||
// check that the supplied sequence number is in line with what we are expecting
|
||||
if (currentBlockSeqNum < 0) {
|
||||
// we have not yet started receiving a file, validate this ftp packet as the initiation request
|
||||
initReceiveSession(ftp.uid, ftp.seqNumOrFileLength);
|
||||
} else if (currentBlockSeqNum == ftp.seqNumOrFileLength) {
|
||||
// we are in the middle of receiving a file; add the block of data to the file
|
||||
processReceivedBlock(ftp, networkNode, peerNodeAddress);
|
||||
} else {
|
||||
log.error("ftp sequence num mismatch, expected {} received {}", currentBlockSeqNum, ftp.seqNumOrFileLength);
|
||||
resetSession(); // aborts the file transfer
|
||||
}
|
||||
}
|
||||
|
||||
public void initReceiveSession(String uid, long expectedFileBytes) {
|
||||
networkNode.addMessageListener(this);
|
||||
this.expectedFileLength = expectedFileBytes;
|
||||
fileOffsetBytes = 0;
|
||||
currentBlockSeqNum = 0;
|
||||
initSessionTimer();
|
||||
log.info("Received a start file transfer request, tradeId={}, traderId={}, size={}", fullTradeId, traderId, expectedFileBytes);
|
||||
log.info("New file will be written to {}", zipFilePath);
|
||||
UserThread.execute(() -> ackReceivedPart(uid, networkNode, peerNodeAddress));
|
||||
}
|
||||
|
||||
private void processReceivedBlock(FileTransferPart ftp, NetworkNode networkNode, NodeAddress peerNodeAddress) {
|
||||
try {
|
||||
RandomAccessFile file = new RandomAccessFile(zipFilePath, "rwd");
|
||||
file.seek(fileOffsetBytes);
|
||||
file.write(ftp.messageData.toByteArray(), 0, ftp.messageData.size());
|
||||
fileOffsetBytes = fileOffsetBytes + ftp.messageData.size();
|
||||
log.info("Sequence number {} for {}, received data {} / {}",
|
||||
ftp.seqNumOrFileLength, Utilities.getShortId(ftp.tradeId), fileOffsetBytes, expectedFileLength);
|
||||
currentBlockSeqNum++;
|
||||
UserThread.runAfter(() -> {
|
||||
ackReceivedPart(ftp.uid, networkNode, peerNodeAddress);
|
||||
if (fileOffsetBytes >= expectedFileLength) {
|
||||
log.info("Success! We have reached the EOF, received {} expected {}", fileOffsetBytes, expectedFileLength);
|
||||
ftpCallback.ifPresent(c -> c.onFtpComplete(this));
|
||||
resetSession();
|
||||
}
|
||||
}, 100, TimeUnit.MILLISECONDS);
|
||||
} catch (IOException e) {
|
||||
log.error(e.toString());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void ackReceivedPart(String uid, NetworkNode networkNode, NodeAddress peerNodeAddress) {
|
||||
AckMessage ackMessage = new AckMessage(peerNodeAddress,
|
||||
AckMessageSourceType.LOG_TRANSFER,
|
||||
FileTransferPart.class.getSimpleName(),
|
||||
uid,
|
||||
Utilities.getShortId(fullTradeId),
|
||||
true, // result
|
||||
null); // errorMessage
|
||||
log.info("Send AckMessage for {} to peer {}. id={}, uid={}",
|
||||
ackMessage.getSourceMsgClassName(), peerNodeAddress, ackMessage.getSourceId(), ackMessage.getSourceUid());
|
||||
sendMessage(ackMessage, networkNode, peerNodeAddress);
|
||||
}
|
||||
|
||||
private static File ensureReceivingDirectoryExists() throws IOException {
|
||||
File directory = new File(Config.appDataDir() + "/clientLogs");
|
||||
if (!directory.exists() && !directory.mkdirs()) {
|
||||
log.error("Could not create directory {}", directory.getAbsolutePath());
|
||||
throw new IOException("Could not create directory: " + directory.getAbsolutePath());
|
||||
}
|
||||
return directory;
|
||||
}
|
||||
}
|
@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 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.core.support.dispute.mediation;
|
||||
|
||||
import haveno.network.p2p.FileTransferPart;
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
import haveno.network.p2p.network.NetworkNode;
|
||||
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.config.Config;
|
||||
import haveno.common.util.Utilities;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static haveno.common.file.FileUtil.doesFileContainKeyword;
|
||||
|
||||
@Slf4j
|
||||
public class FileTransferSender extends FileTransferSession {
|
||||
protected final String zipFilePath;
|
||||
private final boolean isTest;
|
||||
|
||||
public FileTransferSender(NetworkNode networkNode,
|
||||
NodeAddress peerNodeAddress,
|
||||
String tradeId,
|
||||
int traderId,
|
||||
String traderRole,
|
||||
boolean isTest,
|
||||
@Nullable FileTransferSession.FtpCallback callback) {
|
||||
super(networkNode, peerNodeAddress, tradeId, traderId, traderRole, callback);
|
||||
zipFilePath = Utilities.getUserDataDir() + FileSystems.getDefault().getSeparator() + zipId + ".zip";
|
||||
this.isTest = isTest;
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
public void createZipFileToSend() {
|
||||
createZipFileOfLogs(zipFilePath, zipId, fullTradeId);
|
||||
}
|
||||
|
||||
public static void createZipFileOfLogs(String zipFilePath, String zipId, String fullTradeId) {
|
||||
try {
|
||||
Map<String, String> env = new HashMap<>();
|
||||
env.put("create", "true");
|
||||
URI uri = URI.create("jar:file:///" + zipFilePath
|
||||
.replace('\\', '/')
|
||||
.replaceAll(" ", "%20"));
|
||||
FileSystem zipfs = FileSystems.newFileSystem(uri, env);
|
||||
Files.createDirectory(zipfs.getPath(zipId)); // store logfiles in a usefully-named subdir
|
||||
Stream<Path> paths = Files.walk(Paths.get(Config.appDataDir().toString()), 1);
|
||||
paths.filter(Files::isRegularFile).forEach(externalTxtFile -> {
|
||||
try {
|
||||
// always include haveno.log; and other .log files if they contain the TradeId
|
||||
if (externalTxtFile.getFileName().toString().equals("haveno.log") ||
|
||||
(fullTradeId == null && externalTxtFile.getFileName().toString().matches(".*.log")) ||
|
||||
(externalTxtFile.getFileName().toString().matches(".*.log") &&
|
||||
doesFileContainKeyword(externalTxtFile.toFile(), fullTradeId))) {
|
||||
Path pathInZipfile = zipfs.getPath(zipId + "/" + externalTxtFile.getFileName().toString());
|
||||
log.info("adding {} to zip file {}", pathInZipfile, zipfs);
|
||||
Files.copy(externalTxtFile, pathInZipfile, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error(e.toString());
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
zipfs.close();
|
||||
} catch (IOException | IllegalArgumentException ex) {
|
||||
log.error(ex.toString());
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void initSend() throws IOException {
|
||||
initSessionTimer();
|
||||
networkNode.addMessageListener(this);
|
||||
RandomAccessFile file = new RandomAccessFile(zipFilePath, "r");
|
||||
expectedFileLength = file.length();
|
||||
file.close();
|
||||
// an empty block is sent as request to initiate file transfer, peer must ACK for transfer to continue
|
||||
dataAwaitingAck = Optional.of(new FileTransferPart(networkNode.getNodeAddress(), fullTradeId, traderId, UUID.randomUUID().toString(), expectedFileLength, ByteString.EMPTY));
|
||||
uploadData();
|
||||
}
|
||||
|
||||
public void sendNextBlock() throws IOException, IllegalStateException {
|
||||
if (dataAwaitingAck.isPresent()) {
|
||||
log.warn("prepNextBlockToSend invoked, but we are still waiting for a previous ACK");
|
||||
throw new IllegalStateException("prepNextBlockToSend invoked, but we are still waiting for a previous ACK");
|
||||
}
|
||||
RandomAccessFile file = new RandomAccessFile(zipFilePath, "r");
|
||||
file.seek(fileOffsetBytes);
|
||||
byte[] buff = new byte[FILE_BLOCK_SIZE];
|
||||
int nBytesRead = file.read(buff, 0, FILE_BLOCK_SIZE);
|
||||
file.close();
|
||||
if (nBytesRead < 0) {
|
||||
log.info("Success! We have reached the EOF, {} bytes sent. Removing zip file {}", fileOffsetBytes, zipFilePath);
|
||||
Files.delete(Paths.get(zipFilePath));
|
||||
ftpCallback.ifPresent(c -> c.onFtpComplete(this));
|
||||
UserThread.runAfter(this::resetSession, 1);
|
||||
return;
|
||||
}
|
||||
dataAwaitingAck = Optional.of(new FileTransferPart(networkNode.getNodeAddress(), fullTradeId, traderId, UUID.randomUUID().toString(), currentBlockSeqNum, ByteString.copyFrom(buff, 0, nBytesRead)));
|
||||
uploadData();
|
||||
}
|
||||
|
||||
public void retrySend() {
|
||||
if (transferIsInProgress()) {
|
||||
log.info("Retry send of current block");
|
||||
initSessionTimer();
|
||||
uploadData();
|
||||
} else {
|
||||
UserThread.runAfter(() -> ftpCallback.ifPresent((f) -> f.onFtpTimeout("Could not re-send", this)), 1);
|
||||
}
|
||||
}
|
||||
|
||||
protected void uploadData() {
|
||||
if (dataAwaitingAck.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
FileTransferPart ftp = dataAwaitingAck.get();
|
||||
log.info("Send FileTransferPart seq {} length {} to peer {}, UID={}",
|
||||
ftp.seqNumOrFileLength, ftp.messageData.size(), peerNodeAddress, ftp.uid);
|
||||
sendMessage(ftp, networkNode, peerNodeAddress);
|
||||
}
|
||||
|
||||
public boolean processAckForFilePart(String ackUid) {
|
||||
if (dataAwaitingAck.isEmpty()) {
|
||||
log.warn("We received an ACK we were not expecting. {}", ackUid);
|
||||
return false;
|
||||
}
|
||||
if (!dataAwaitingAck.get().uid.equals(ackUid)) {
|
||||
log.warn("We received an ACK that has a different UID to what we were expecting. We ignore and wait for the correct ACK");
|
||||
log.info("Received {} expecting {}", ackUid, dataAwaitingAck.get().uid);
|
||||
return false;
|
||||
}
|
||||
// fileOffsetBytes gets incremented by the size of the block that was ack'd
|
||||
fileOffsetBytes += dataAwaitingAck.get().messageData.size();
|
||||
currentBlockSeqNum++;
|
||||
dataAwaitingAck = Optional.empty();
|
||||
checkpointLastActivity();
|
||||
updateProgress();
|
||||
if (isTest) {
|
||||
return true;
|
||||
}
|
||||
UserThread.runAfter(() -> { // to trigger continuing the file transfer
|
||||
try {
|
||||
sendNextBlock();
|
||||
} catch (IOException e) {
|
||||
log.error(e.toString());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, 100, TimeUnit.MILLISECONDS);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void updateProgress() {
|
||||
double progressPct = expectedFileLength > 0 ?
|
||||
((double) fileOffsetBytes / expectedFileLength) : 0.0;
|
||||
ftpCallback.ifPresent(c -> c.onFtpProgress(progressPct));
|
||||
log.info("ftp progress: {}", String.format("%.0f%%", progressPct * 100));
|
||||
}
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
/*
|
||||
* 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.core.support.dispute.mediation;
|
||||
|
||||
import haveno.network.p2p.AckMessage;
|
||||
import haveno.network.p2p.AckMessageSourceType;
|
||||
import haveno.network.p2p.FileTransferPart;
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
import haveno.network.p2p.network.Connection;
|
||||
import haveno.network.p2p.network.MessageListener;
|
||||
import haveno.network.p2p.network.NetworkNode;
|
||||
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.proto.network.NetworkEnvelope;
|
||||
import haveno.common.util.Utilities;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static haveno.network.p2p.network.Connection.getPermittedMessageSize;
|
||||
|
||||
@Slf4j
|
||||
public abstract class FileTransferSession implements MessageListener {
|
||||
protected static final int FTP_SESSION_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(60);
|
||||
protected static final int FILE_BLOCK_SIZE = getPermittedMessageSize() - 1024; // allowing space for protobuf
|
||||
|
||||
public interface FtpCallback {
|
||||
void onFtpProgress(double progressPct);
|
||||
|
||||
void onFtpComplete(FileTransferSession session);
|
||||
|
||||
void onFtpTimeout(String statusMsg, FileTransferSession session);
|
||||
}
|
||||
|
||||
@Getter
|
||||
protected final String fullTradeId;
|
||||
@Getter
|
||||
protected final int traderId;
|
||||
@Getter
|
||||
protected final String zipId;
|
||||
protected final Optional<FtpCallback> ftpCallback;
|
||||
protected final NetworkNode networkNode; // for sending network messages
|
||||
protected final NodeAddress peerNodeAddress;
|
||||
protected Optional<FileTransferPart> dataAwaitingAck;
|
||||
protected long fileOffsetBytes;
|
||||
protected long currentBlockSeqNum;
|
||||
protected long expectedFileLength;
|
||||
protected long lastActivityTime;
|
||||
|
||||
public FileTransferSession(NetworkNode networkNode,
|
||||
NodeAddress peerNodeAddress,
|
||||
String tradeId,
|
||||
int traderId,
|
||||
String traderRole,
|
||||
@Nullable FileTransferSession.FtpCallback callback) {
|
||||
this.networkNode = networkNode;
|
||||
this.peerNodeAddress = peerNodeAddress;
|
||||
this.fullTradeId = tradeId;
|
||||
this.traderId = traderId;
|
||||
this.ftpCallback = Optional.ofNullable(callback);
|
||||
this.zipId = Utilities.getShortId(fullTradeId) + "_" + traderRole.toUpperCase() + "_"
|
||||
+ new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
|
||||
resetSession();
|
||||
}
|
||||
|
||||
public void resetSession() {
|
||||
lastActivityTime = 0;
|
||||
currentBlockSeqNum = -1;
|
||||
fileOffsetBytes = 0;
|
||||
expectedFileLength = 0;
|
||||
dataAwaitingAck = Optional.empty();
|
||||
networkNode.removeMessageListener(this);
|
||||
log.info("Ftp session parameters have been reset.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
|
||||
if (networkEnvelope instanceof FileTransferPart) {
|
||||
// mediator receiving log file data
|
||||
FileTransferPart ftp = (FileTransferPart) networkEnvelope;
|
||||
if (this instanceof FileTransferReceiver) {
|
||||
((FileTransferReceiver) this).processFilePartReceived(ftp);
|
||||
}
|
||||
} else if (networkEnvelope instanceof AckMessage) {
|
||||
AckMessage ackMessage = (AckMessage) networkEnvelope;
|
||||
if (ackMessage.getSourceType() == AckMessageSourceType.LOG_TRANSFER) {
|
||||
if (ackMessage.isSuccess()) {
|
||||
log.info("Received AckMessage for {} with id {} and uid {}",
|
||||
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
|
||||
if (this instanceof FileTransferSender) {
|
||||
((FileTransferSender) this).processAckForFilePart(ackMessage.getSourceUid());
|
||||
}
|
||||
} else {
|
||||
log.warn("Received AckMessage with error state for {} with id {} and errorMessage={}",
|
||||
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void checkpointLastActivity() {
|
||||
lastActivityTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
protected void initSessionTimer() {
|
||||
UserThread.runAfter(() -> {
|
||||
if (!transferIsInProgress()) // transfer may have finished before this timer executes
|
||||
return;
|
||||
if (System.currentTimeMillis() - lastActivityTime < FTP_SESSION_TIMEOUT_MILLIS) {
|
||||
log.info("Last activity was {}, we have not yet timed out.", new Date(lastActivityTime));
|
||||
initSessionTimer();
|
||||
} else {
|
||||
log.warn("File transfer session timed out. expected: {} received: {}", expectedFileLength, fileOffsetBytes);
|
||||
ftpCallback.ifPresent((e) -> e.onFtpTimeout("Timed out during send", this));
|
||||
}
|
||||
}, FTP_SESSION_TIMEOUT_MILLIS / 4, TimeUnit.MILLISECONDS); // check more frequently than the timeout
|
||||
}
|
||||
|
||||
protected boolean transferIsInProgress() {
|
||||
return fileOffsetBytes != expectedFileLength;
|
||||
}
|
||||
|
||||
protected void sendMessage(NetworkEnvelope message, NetworkNode networkNode, NodeAddress nodeAddress) {
|
||||
SettableFuture<Connection> future = networkNode.sendMessage(nodeAddress, message);
|
||||
if (future != null) { // is null when testing with Mockito
|
||||
Futures.addCallback(future, new FutureCallback<>() {
|
||||
@Override
|
||||
public void onSuccess(Connection connection) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
String errorSend = "Sending " + message.getClass().getSimpleName() +
|
||||
" to " + nodeAddress.getFullAddress() +
|
||||
" failed. That is expected if the peer is offline.\n\t" +
|
||||
".\n\tException=" + throwable.getMessage();
|
||||
log.warn(errorSend);
|
||||
ftpCallback.ifPresent((f) -> f.onFtpTimeout("Peer offline", FileTransferSession.this));
|
||||
resetSession();
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
}
|
||||
}
|
||||
}
|
@ -17,25 +17,25 @@
|
||||
|
||||
package haveno.core.support.messages;
|
||||
|
||||
import haveno.common.app.Version;
|
||||
import haveno.common.util.Utilities;
|
||||
import haveno.core.locale.Res;
|
||||
import haveno.core.support.SupportType;
|
||||
import haveno.core.support.dispute.Attachment;
|
||||
import haveno.core.support.dispute.Dispute;
|
||||
import haveno.core.support.dispute.DisputeResult;
|
||||
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.app.Version;
|
||||
import haveno.common.util.Utilities;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@ -44,13 +44,22 @@ import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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. Arbitrator 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
|
||||
@ -84,14 +93,14 @@ public final class ChatMessage extends SupportMessage {
|
||||
private final StringProperty sendMessageErrorProperty;
|
||||
private final StringProperty ackErrorProperty;
|
||||
|
||||
transient private Listener listener;
|
||||
transient private WeakReference<Listener> listener;
|
||||
|
||||
public ChatMessage(SupportType supportType,
|
||||
String tradeId,
|
||||
int traderId,
|
||||
boolean senderIsTrader,
|
||||
String message,
|
||||
NodeAddress senderNodeAddress) {
|
||||
String tradeId,
|
||||
int traderId,
|
||||
boolean senderIsTrader,
|
||||
String message,
|
||||
NodeAddress senderNodeAddress) {
|
||||
this(supportType,
|
||||
tradeId,
|
||||
traderId,
|
||||
@ -111,12 +120,12 @@ public final class ChatMessage extends SupportMessage {
|
||||
}
|
||||
|
||||
public ChatMessage(SupportType supportType,
|
||||
String tradeId,
|
||||
int traderId,
|
||||
boolean senderIsTrader,
|
||||
String message,
|
||||
NodeAddress senderNodeAddress,
|
||||
ArrayList<Attachment> attachments) {
|
||||
String tradeId,
|
||||
int traderId,
|
||||
boolean senderIsTrader,
|
||||
String message,
|
||||
NodeAddress senderNodeAddress,
|
||||
ArrayList<Attachment> attachments) {
|
||||
this(supportType,
|
||||
tradeId,
|
||||
traderId,
|
||||
@ -136,12 +145,12 @@ public final class ChatMessage extends SupportMessage {
|
||||
}
|
||||
|
||||
public ChatMessage(SupportType supportType,
|
||||
String tradeId,
|
||||
int traderId,
|
||||
boolean senderIsTrader,
|
||||
String message,
|
||||
NodeAddress senderNodeAddress,
|
||||
long date) {
|
||||
String tradeId,
|
||||
int traderId,
|
||||
boolean senderIsTrader,
|
||||
String message,
|
||||
NodeAddress senderNodeAddress,
|
||||
long date) {
|
||||
this(supportType,
|
||||
tradeId,
|
||||
traderId,
|
||||
@ -198,7 +207,9 @@ public final class ChatMessage extends SupportMessage {
|
||||
notifyChangeListener();
|
||||
}
|
||||
|
||||
public protobuf.ChatMessage.Builder toProtoChatMessageBuilder() {
|
||||
// We cannot rename protobuf definition because it would break backward compatibility
|
||||
@Override
|
||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||
protobuf.ChatMessage.Builder builder = protobuf.ChatMessage.newBuilder()
|
||||
.setType(SupportType.toProtoMessage(supportType))
|
||||
.setTradeId(tradeId)
|
||||
@ -216,14 +227,6 @@ public final class ChatMessage extends SupportMessage {
|
||||
.setWasDisplayed(wasDisplayed);
|
||||
Optional.ofNullable(sendMessageErrorProperty.get()).ifPresent(builder::setSendMessageError);
|
||||
Optional.ofNullable(ackErrorProperty.get()).ifPresent(builder::setAckError);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
// We cannot rename protobuf definition because it would break backward compatibility
|
||||
@Override
|
||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||
protobuf.ChatMessage.Builder builder = toProtoChatMessageBuilder();
|
||||
return getNetworkEnvelopeBuilder()
|
||||
.setChatMessage(builder)
|
||||
.build();
|
||||
@ -296,6 +299,16 @@ public final class ChatMessage extends SupportMessage {
|
||||
notifyChangeListener();
|
||||
}
|
||||
|
||||
// each chat message notifies the user if an ACK is not received in time
|
||||
public void startAckTimer() {
|
||||
UserThread.runAfter(() -> {
|
||||
if (!this.getAcknowledgedProperty().get() && !this.getStoredInMailboxProperty().get()) {
|
||||
this.setArrived(false);
|
||||
this.setAckError(Res.get("support.errorTimeout"));
|
||||
}
|
||||
}, 60, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty acknowledgedProperty() {
|
||||
return acknowledgedProperty;
|
||||
}
|
||||
@ -327,12 +340,8 @@ public final class ChatMessage extends SupportMessage {
|
||||
return Utilities.getShortId(tradeId);
|
||||
}
|
||||
|
||||
public void addChangeListener(Listener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void removeChangeListener() {
|
||||
this.listener = null;
|
||||
public void addWeakMessageStateListener(Listener listener) {
|
||||
this.listener = new WeakReference<>(listener);
|
||||
}
|
||||
|
||||
public boolean isResultMessage(Dispute dispute) {
|
||||
@ -352,7 +361,10 @@ public final class ChatMessage extends SupportMessage {
|
||||
|
||||
private void notifyChangeListener() {
|
||||
if (listener != null) {
|
||||
listener.onMessageStateChanged();
|
||||
Listener listener = this.listener.get();
|
||||
if (listener != null) {
|
||||
listener.onMessageStateChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1174,11 +1174,28 @@ support.chat=Chat
|
||||
support.requested=Requested
|
||||
support.closed=Closed
|
||||
support.open=Open
|
||||
support.moreButton=MORE...
|
||||
support.sendLogFiles=Send Log Files
|
||||
support.uploadTraderChat=Upload Trader Chat
|
||||
support.process=Process
|
||||
support.buyerMaker=XMR Buyer/Maker
|
||||
support.sellerMaker=XMR Seller/Maker
|
||||
support.buyerTaker=XMR Buyer/Taker
|
||||
support.sellerTaker=XMR Seller/Taker
|
||||
support.sendLogs.title=Send Log Files
|
||||
support.sendLogs.backgroundInfo=When you experience a bug, arbitrators and support staff will often request copies of the your log files to diagnose the issue.\n\n\
|
||||
Upon pressing 'Send', your log files will be compressed and transmitted directly to the arbitrator.
|
||||
support.sendLogs.step1=Create Zip Archive of Log Files
|
||||
support.sendLogs.step2=Connection Request to Arbitrator
|
||||
support.sendLogs.step3=Upload Archived Log Data
|
||||
support.sendLogs.send=Send
|
||||
support.sendLogs.cancel=Cancel
|
||||
support.sendLogs.init=Initializing
|
||||
support.sendLogs.retry=Retrying send
|
||||
support.sendLogs.stopped=Transfer stopped
|
||||
support.sendLogs.progress=Transfer progress: %.0f%%
|
||||
support.sendLogs.finished=Transfer complete!
|
||||
support.sendLogs.command=Press 'Send' to retry, or 'Stop' to abort
|
||||
support.txKeyImages=Key Images
|
||||
support.txHash=Transaction Hash
|
||||
support.txHex=Transaction Hex
|
||||
@ -2312,6 +2329,7 @@ peerInfoIcon.tooltip.trade.traded={0} onion address: {1}\nYou have already trade
|
||||
peerInfoIcon.tooltip.trade.notTraded={0} onion address: {1}\nYou have not traded with that peer so far.\n{2}
|
||||
peerInfoIcon.tooltip.age=Payment account created {0} ago.
|
||||
peerInfoIcon.tooltip.unknownAge=Payment account age not known.
|
||||
peerInfoIcon.tooltip.dispute={0}\nNumber of disputes: {1}.\n{2}
|
||||
|
||||
tooltip.openPopupForDetails=Open popup for details
|
||||
tooltip.invalidTradeState.warning=This trade is in an invalid state. Open the details window for more information
|
||||
|
@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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.core.support.dispute.mediation;
|
||||
|
||||
import haveno.network.p2p.FileTransferPart;
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
import haveno.network.p2p.network.NetworkNode;
|
||||
|
||||
import haveno.common.config.Config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class FileTransferSessionTest implements FileTransferSession.FtpCallback {
|
||||
|
||||
double notedProgressPct = -1.0;
|
||||
int progressInvocations = 0;
|
||||
boolean ftpCompleteStatus = false;
|
||||
String testTradeId = "foo";
|
||||
int testTraderId = 123;
|
||||
String testClientId = "bar";
|
||||
NetworkNode networkNode;
|
||||
NodeAddress counterpartyNodeAddress;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() throws Exception {
|
||||
new Config(); // static methods like Config.appDataDir() require config to be created once
|
||||
networkNode = mock(NetworkNode.class);
|
||||
when(networkNode.getNodeAddress()).thenReturn(new NodeAddress("null:0000"));
|
||||
counterpartyNodeAddress = new NodeAddress("null:0000");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendCreate() {
|
||||
new FileTransferSender(networkNode, counterpartyNodeAddress, testTradeId, testTraderId, testClientId, true, this);
|
||||
assertEquals(0.0, notedProgressPct, 0.0);
|
||||
assertEquals(1, progressInvocations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateZip() {
|
||||
FileTransferSender sender = new FileTransferSender(networkNode, counterpartyNodeAddress, testTradeId, testTraderId, testClientId, true, this);
|
||||
assertEquals(0.0, notedProgressPct, 0.0);
|
||||
assertEquals(1, progressInvocations);
|
||||
sender.createZipFileToSend();
|
||||
File file = new File(sender.zipFilePath);
|
||||
assertTrue(file.getAbsoluteFile().exists());
|
||||
assertTrue(file.getAbsoluteFile().length() > 0);
|
||||
file.deleteOnExit();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendInitialize() {
|
||||
// checks that the initial send request packet contains correct information
|
||||
try {
|
||||
int testVerifyDataSize = 13;
|
||||
FileTransferSender session = initializeSession(testVerifyDataSize);
|
||||
session.initSend();
|
||||
FileTransferPart ftp = session.dataAwaitingAck.get();
|
||||
assertEquals(ftp.tradeId, testTradeId);
|
||||
assertTrue(ftp.uid.length() > 0);
|
||||
assertEquals(0, ftp.messageData.size());
|
||||
assertEquals(ftp.seqNumOrFileLength, testVerifyDataSize);
|
||||
assertEquals(-1, session.currentBlockSeqNum);
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
fail();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendSmallFile() {
|
||||
try {
|
||||
int testVerifyDataSize = 13;
|
||||
FileTransferSender session = initializeSession(testVerifyDataSize);
|
||||
// the first block contains zero data, as it is a "request to send"
|
||||
session.initSend();
|
||||
simulateAckFromPeerAndVerify(session, 0, 0, 2);
|
||||
// the second block contains all the test file data (because it is a small file)
|
||||
session.sendNextBlock();
|
||||
simulateAckFromPeerAndVerify(session, testVerifyDataSize, 1, 3);
|
||||
// the final invocation sends no data, and wraps up the session
|
||||
session.sendNextBlock();
|
||||
assertEquals(1, session.currentBlockSeqNum);
|
||||
assertEquals(3, progressInvocations);
|
||||
assertEquals(1.0, notedProgressPct, 0.0);
|
||||
assertTrue(ftpCompleteStatus);
|
||||
} catch (IOException ioe) {
|
||||
ioe.printStackTrace();
|
||||
fail();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendOneFullBlock() {
|
||||
try {
|
||||
int testVerifyDataSize = FileTransferSession.FILE_BLOCK_SIZE;
|
||||
FileTransferSender session = initializeSession(testVerifyDataSize);
|
||||
// the first block contains zero data, as it is a "request to send"
|
||||
session.initSend();
|
||||
simulateAckFromPeerAndVerify(session, 0, 0, 2);
|
||||
// the second block contains all the test file data (because it is a small file)
|
||||
session.sendNextBlock();
|
||||
simulateAckFromPeerAndVerify(session, testVerifyDataSize, 1, 3);
|
||||
// the final invocation sends no data, and wraps up the session
|
||||
session.sendNextBlock();
|
||||
assertEquals(1, session.currentBlockSeqNum);
|
||||
assertEquals(3, progressInvocations);
|
||||
assertEquals(1.0, notedProgressPct, 0.0);
|
||||
assertTrue(ftpCompleteStatus);
|
||||
} catch (IOException ioe) {
|
||||
ioe.printStackTrace();
|
||||
fail();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendTwoFullBlocks() {
|
||||
try {
|
||||
int testVerifyDataSize = FileTransferSession.FILE_BLOCK_SIZE * 2;
|
||||
FileTransferSender session = initializeSession(testVerifyDataSize);
|
||||
// the first block contains zero data, as it is a "request to send"
|
||||
session.initSend();
|
||||
simulateAckFromPeerAndVerify(session, 0, 0, 2);
|
||||
// the second block contains half of the test file data
|
||||
session.sendNextBlock();
|
||||
simulateAckFromPeerAndVerify(session, testVerifyDataSize / 2, 1, 3);
|
||||
// the third block contains half of the test file data
|
||||
session.sendNextBlock();
|
||||
simulateAckFromPeerAndVerify(session, testVerifyDataSize / 2, 2, 4);
|
||||
// the final invocation sends no data, and wraps up the session
|
||||
session.sendNextBlock();
|
||||
assertEquals(2, session.currentBlockSeqNum);
|
||||
assertEquals(4, progressInvocations);
|
||||
assertEquals(1.0, notedProgressPct, 0.0);
|
||||
assertTrue(ftpCompleteStatus);
|
||||
} catch (IOException ioe) {
|
||||
ioe.printStackTrace();
|
||||
fail();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendTwoFullBlocksPlusOneByte() {
|
||||
try {
|
||||
int testVerifyDataSize = 1 + FileTransferSession.FILE_BLOCK_SIZE * 2;
|
||||
FileTransferSender session = initializeSession(testVerifyDataSize);
|
||||
// the first block contains zero data, as it is a "request to send"
|
||||
session.initSend();
|
||||
simulateAckFromPeerAndVerify(session, 0, 0, 2);
|
||||
session.sendNextBlock();
|
||||
simulateAckFromPeerAndVerify(session, FileTransferSession.FILE_BLOCK_SIZE, 1, 3);
|
||||
session.sendNextBlock();
|
||||
simulateAckFromPeerAndVerify(session, FileTransferSession.FILE_BLOCK_SIZE, 2, 4);
|
||||
// the fourth block contains one byte
|
||||
session.sendNextBlock();
|
||||
simulateAckFromPeerAndVerify(session, 1, 3, 5);
|
||||
// the final invocation sends no data, and wraps up the session
|
||||
session.sendNextBlock();
|
||||
assertEquals(3, session.currentBlockSeqNum);
|
||||
assertEquals(5, progressInvocations);
|
||||
assertEquals(1.0, notedProgressPct, 0.0);
|
||||
assertTrue(ftpCompleteStatus);
|
||||
} catch (IOException ioe) {
|
||||
ioe.printStackTrace();
|
||||
fail();
|
||||
}
|
||||
}
|
||||
|
||||
private FileTransferSender initializeSession(int testSize) {
|
||||
try {
|
||||
FileTransferSender session = new FileTransferSender(networkNode, counterpartyNodeAddress, testTradeId, testTraderId, testClientId, true, this);
|
||||
// simulate a file for sending
|
||||
FileWriter fileWriter = new FileWriter(session.zipFilePath);
|
||||
char[] buf = new char[testSize];
|
||||
for (int x = 0; x < testSize; x++)
|
||||
buf[x] = 'A';
|
||||
fileWriter.write(buf);
|
||||
fileWriter.close();
|
||||
assertFalse(ftpCompleteStatus);
|
||||
assertEquals(1, progressInvocations);
|
||||
assertEquals(0.0, notedProgressPct, 0.0);
|
||||
assertFalse(session.processAckForFilePart("not_expected_uid"));
|
||||
return session;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
fail();
|
||||
return null;
|
||||
}
|
||||
|
||||
private void simulateAckFromPeerAndVerify(FileTransferSender session, int expectedDataSize, long expectedSeqNum, int expectedProgressInvocations) {
|
||||
FileTransferPart ftp = session.dataAwaitingAck.get();
|
||||
assertEquals(expectedDataSize, ftp.messageData.size());
|
||||
assertTrue(session.processAckForFilePart(ftp.uid));
|
||||
assertEquals(expectedSeqNum, session.currentBlockSeqNum);
|
||||
assertEquals(expectedProgressInvocations, progressInvocations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFtpProgress(double progressPct) {
|
||||
notedProgressPct = progressPct;
|
||||
progressInvocations++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFtpComplete(FileTransferSession session) {
|
||||
ftpCompleteStatus = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFtpTimeout(String status, FileTransferSession session) {
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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,12 +495,8 @@ 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);
|
||||
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -24,5 +24,6 @@ public enum AckMessageSourceType {
|
||||
ARBITRATION_MESSAGE,
|
||||
MEDIATION_MESSAGE,
|
||||
TRADE_CHAT_MESSAGE,
|
||||
REFUND_MESSAGE
|
||||
REFUND_MESSAGE,
|
||||
LOG_TRANSFER
|
||||
}
|
||||
|
106
p2p/src/main/java/haveno/network/p2p/FileTransferPart.java
Normal file
106
p2p/src/main/java/haveno/network/p2p/FileTransferPart.java
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.network.p2p;
|
||||
|
||||
import haveno.common.app.Version;
|
||||
import haveno.common.proto.network.NetworkEnvelope;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Value
|
||||
public class FileTransferPart extends NetworkEnvelope implements ExtendedDataSizePermission, SendersNodeAddressMessage {
|
||||
NodeAddress senderNodeAddress;
|
||||
public String uid;
|
||||
public String tradeId;
|
||||
public int traderId;
|
||||
public long seqNumOrFileLength;
|
||||
public ByteString messageData; // if message_data is empty it is the first message, requesting file upload permission
|
||||
|
||||
public FileTransferPart(NodeAddress senderNodeAddress,
|
||||
String tradeId,
|
||||
int traderId,
|
||||
String uid,
|
||||
long seqNumOrFileLength,
|
||||
ByteString messageData) {
|
||||
this(senderNodeAddress, tradeId, traderId, uid, seqNumOrFileLength, messageData, Version.getP2PMessageVersion());
|
||||
}
|
||||
|
||||
public boolean isInitialRequest() {
|
||||
return messageData.size() == 0;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PROTO BUFFER
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private FileTransferPart(NodeAddress senderNodeAddress,
|
||||
String tradeId,
|
||||
int traderId,
|
||||
String uid,
|
||||
long seqNumOrFileLength,
|
||||
ByteString messageData,
|
||||
String messageVersion) {
|
||||
super(messageVersion);
|
||||
this.senderNodeAddress = senderNodeAddress;
|
||||
this.tradeId = tradeId;
|
||||
this.traderId = traderId;
|
||||
this.uid = uid;
|
||||
this.seqNumOrFileLength = seqNumOrFileLength;
|
||||
this.messageData = messageData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||
return getNetworkEnvelopeBuilder()
|
||||
.setFileTransferPart(protobuf.FileTransferPart.newBuilder()
|
||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
||||
.setTradeId(tradeId)
|
||||
.setTraderId(traderId)
|
||||
.setUid(uid)
|
||||
.setSeqNumOrFileLength(seqNumOrFileLength)
|
||||
.setMessageData(messageData)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static FileTransferPart fromProto(protobuf.FileTransferPart proto, String messageVersion) {
|
||||
return new FileTransferPart(
|
||||
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
||||
proto.getTradeId(),
|
||||
proto.getTraderId(),
|
||||
proto.getUid(),
|
||||
proto.getSeqNumOrFileLength(),
|
||||
proto.getMessageData(),
|
||||
messageVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FileTransferPart{" +
|
||||
"\n senderNodeAddress='" + senderNodeAddress.getHostNameForDisplay() + '\'' +
|
||||
",\n uid='" + uid + '\'' +
|
||||
",\n tradeId='" + tradeId + '\'' +
|
||||
",\n traderId='" + traderId + '\'' +
|
||||
",\n seqNumOrFileLength=" + seqNumOrFileLength +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
@ -64,6 +64,8 @@ message NetworkEnvelope {
|
||||
|
||||
MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 37;
|
||||
MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 38;
|
||||
|
||||
FileTransferPart file_transfer_part = 39;
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,6 +102,15 @@ message GetUpdatedDataRequest {
|
||||
string version = 4;
|
||||
}
|
||||
|
||||
message FileTransferPart {
|
||||
NodeAddress sender_node_address = 1;
|
||||
string uid = 2;
|
||||
string trade_id = 3;
|
||||
int32 trader_id = 4;
|
||||
int64 seq_num_or_file_length = 5;
|
||||
bytes message_data = 6;
|
||||
}
|
||||
|
||||
message GetPeersRequest {
|
||||
NodeAddress sender_node_address = 1;
|
||||
int32 nonce = 2;
|
||||
|
Loading…
Reference in New Issue
Block a user