diff --git a/src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java b/src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java index 88efa5ed..e1680100 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java @@ -17,31 +17,52 @@ import org.slf4j.LoggerFactory; import java.io.InputStream; import java.net.Proxy; import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; public class PayNymAvatar extends StackPane { private static final Logger log = LoggerFactory.getLogger(PayNymAvatar.class); private final ObjectProperty paymentCodeProperty = new SimpleObjectProperty<>(null); + private static final Map paymentCodeCache = Collections.synchronizedMap(new HashMap<>()); + private static final Map paymentCodeLoading = Collections.synchronizedMap(new HashMap<>()); + public PayNymAvatar() { super(); - paymentCodeProperty.addListener((observable, oldValue, newValue) -> { - if(Config.get().isUsePayNym()) { - PayNymAvatarService payNymAvatarService = new PayNymAvatarService(newValue); - payNymAvatarService.setOnRunning(runningEvent -> { - getChildren().clear(); - }); - payNymAvatarService.setOnSucceeded(successEvent -> { - Circle circle = new Circle(getWidth() / 2,getHeight() / 2,getWidth() / 2); - circle.setFill(new ImagePattern(payNymAvatarService.getValue())); - getChildren().add(circle); - }); - payNymAvatarService.start(); + paymentCodeProperty.addListener((observable, oldValue, paymentCode) -> { + if(paymentCode == null) { + getChildren().clear(); + } else if(Config.get().isUsePayNym() && (oldValue == null || !oldValue.toString().equals(paymentCode.toString()))) { + String cacheId = getCacheId(paymentCode, getPrefWidth()); + if(paymentCodeCache.containsKey(cacheId)) { + setImage(paymentCodeCache.get(cacheId)); + } else { + PayNymAvatarService payNymAvatarService = new PayNymAvatarService(paymentCode, getPrefWidth()); + payNymAvatarService.setOnRunning(runningEvent -> { + getChildren().clear(); + }); + payNymAvatarService.setOnSucceeded(successEvent -> { + setImage(payNymAvatarService.getValue()); + }); + payNymAvatarService.setOnFailed(failedEvent -> { + log.error("Error", failedEvent.getSource().getException()); + }); + payNymAvatarService.start(); + } } }); } + private void setImage(Image image) { + getChildren().clear(); + Circle circle = new Circle(getPrefWidth() / 2,getPrefHeight() / 2,getPrefWidth() / 2); + circle.setFill(new ImagePattern(image)); + getChildren().add(circle); + } + public PaymentCode getPaymentCode() { return paymentCodeProperty.get(); } @@ -54,11 +75,17 @@ public class PayNymAvatar extends StackPane { this.paymentCodeProperty.set(paymentCode); } - private class PayNymAvatarService extends Service { - private final PaymentCode paymentCode; + private static String getCacheId(PaymentCode paymentCode, double width) { + return paymentCode.toString(); + } - public PayNymAvatarService(PaymentCode paymentCode) { + private static class PayNymAvatarService extends Service { + private final PaymentCode paymentCode; + private final double width; + + public PayNymAvatarService(PaymentCode paymentCode, double width) { this.paymentCode = paymentCode; + this.width = width; } @Override @@ -67,14 +94,34 @@ public class PayNymAvatar extends StackPane { @Override protected Image call() throws Exception { String paymentCodeStr = paymentCode.toString(); - String url = "https://paynym.is/" + paymentCodeStr + "/avatar"; - Proxy proxy = AppServices.getProxy(); + String cacheId = getCacheId(paymentCode, width); - try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) { - return new Image(is, getWidth(), getHeight(), true, false); - } catch(Exception e) { - log.debug("Error loading PayNym avatar", e); - throw e; + Object lock = paymentCodeLoading.get(cacheId); + if(lock != null) { + synchronized(lock) { + if(paymentCodeCache.containsKey(cacheId)) { + return paymentCodeCache.get(cacheId); + } + } + } else { + lock = new Object(); + paymentCodeLoading.put(cacheId, lock); + } + + synchronized(lock) { + String url = "https://paynym.is/" + paymentCodeStr + "/avatar"; + Proxy proxy = AppServices.getProxy(); + + try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) { + Image image = new Image(is, 150, 150, true, false); + paymentCodeCache.put(cacheId, image); + return image; + } catch(Exception e) { + log.debug("Error loading PayNym avatar", e); + throw e; + } finally { + lock.notifyAll(); + } } } }; diff --git a/src/main/java/com/sparrowwallet/sparrow/control/PayNymCell.java b/src/main/java/com/sparrowwallet/sparrow/control/PayNymCell.java new file mode 100644 index 00000000..cf52cee3 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/PayNymCell.java @@ -0,0 +1,65 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.FollowPayNymEvent; +import com.sparrowwallet.sparrow.soroban.PayNym; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; + +public class PayNymCell extends ListCell { + private final String walletId; + + public PayNymCell(String walletId) { + super(); + setAlignment(Pos.CENTER_LEFT); + setContentDisplay(ContentDisplay.LEFT); + getStyleClass().add("paynym-cell"); + setPrefHeight(50); + this.walletId = walletId; + } + + @Override + protected void updateItem(PayNym payNym, boolean empty) { + super.updateItem(payNym, empty); + + if(empty || payNym == null) { + setText(null); + setGraphic(null); + } else { + BorderPane pane = new BorderPane(); + pane.setPadding(new Insets(5, 5,5, 5)); + + PayNymAvatar payNymAvatar = new PayNymAvatar(); + payNymAvatar.setPrefWidth(30); + payNymAvatar.setPrefHeight(30); + payNymAvatar.setPaymentCode(payNym.paymentCode()); + + HBox labelBox = new HBox(); + labelBox.setAlignment(Pos.CENTER); + Label label = new Label(payNym.nymName(), payNymAvatar); + label.setGraphicTextGap(10); + labelBox.getChildren().add(label); + pane.setLeft(labelBox); + + if(getListView().getUserData() == Boolean.TRUE) { + HBox hBox = new HBox(); + hBox.setAlignment(Pos.CENTER); + Button button = new Button("Follow"); + hBox.getChildren().add(button); + pane.setRight(hBox); + button.setOnAction(event -> { + EventManager.get().post(new FollowPayNymEvent(walletId, payNym.paymentCode())); + }); + } + + setText(null); + setGraphic(pane); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index fe777bd3..2072731e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java @@ -564,7 +564,7 @@ public class TransactionDiagram extends GridPane { private Pane getTransactionPane() { VBox txPane = new VBox(); - txPane.setPadding(new Insets(0, 8, 0, 8)); + txPane.setPadding(new Insets(0, 5, 0, 5)); txPane.setAlignment(Pos.CENTER); txPane.getChildren().add(createSpacer()); diff --git a/src/main/java/com/sparrowwallet/sparrow/event/FollowPayNymEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/FollowPayNymEvent.java new file mode 100644 index 00000000..0589b803 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/FollowPayNymEvent.java @@ -0,0 +1,21 @@ +package com.sparrowwallet.sparrow.event; + +import com.samourai.wallet.bip47.rpc.PaymentCode; + +public class FollowPayNymEvent { + private final String walletId; + private final PaymentCode paymentCode; + + public FollowPayNymEvent(String walletId, PaymentCode paymentCode) { + this.walletId = walletId; + this.paymentCode = paymentCode; + } + + public String getWalletId() { + return walletId; + } + + public PaymentCode getPaymentCode() { + return paymentCode; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index 1a4acbf4..cb0f0238 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -56,6 +56,7 @@ public class FontAwesome5 extends GlyphFont { QUESTION_CIRCLE('\uf059'), RANDOM('\uf074'), REPLY_ALL('\uf122'), + ROBOT('\uf544'), SATELLITE_DISH('\uf7c0'), SD_CARD('\uf7c2'), SEARCH('\uf002'), diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java index b1e481b5..529838a4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java @@ -55,6 +55,9 @@ public class CounterpartyController extends SorobanController { @FXML private CopyableTextField payNym; + @FXML + private Button showPayNym; + @FXML private PayNymAvatar payNymAvatar; @@ -157,6 +160,8 @@ public class CounterpartyController extends SorobanController { } payNym.managedProperty().bind(payNym.visibleProperty()); + showPayNym.managedProperty().bind(showPayNym.visibleProperty()); + showPayNym.visibleProperty().bind(payNym.visibleProperty()); payNymAvatar.managedProperty().bind(payNymAvatar.visibleProperty()); payNymAvatar.visibleProperty().bind(payNym.visibleProperty()); payNymButton.managedProperty().bind(payNymButton.visibleProperty()); @@ -169,6 +174,7 @@ public class CounterpartyController extends SorobanController { paymentCode.setPaymentCode(soroban.getPaymentCode()); paymentCodeQR.prefHeightProperty().bind(paymentCode.heightProperty()); + paymentCodeQR.prefWidthProperty().bind(showPayNym.widthProperty()); mixingPartner.managedProperty().bind(mixingPartner.visibleProperty()); meetingFail.managedProperty().bind(meetingFail.visibleProperty()); @@ -388,6 +394,11 @@ public class CounterpartyController extends SorobanController { }); } + public void showPayNym(ActionEvent event) { + PayNymDialog payNymDialog = new PayNymDialog(walletId, false); + payNymDialog.showAndWait(); + } + public void showPayNymQR(ActionEvent event) { Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(soroban.getPaymentCode().toString()); diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java index c1280b1a..cb6ddd98 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java @@ -30,7 +30,10 @@ import io.reactivex.schedulers.Schedulers; import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; +import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.*; import javafx.scene.layout.VBox; @@ -44,7 +47,6 @@ import org.slf4j.LoggerFactory; import java.util.*; import java.util.function.UnaryOperator; -import java.util.regex.Pattern; import static com.sparrowwallet.sparrow.AppServices.showErrorDialog; import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS; @@ -52,8 +54,7 @@ import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS; public class InitiatorController extends SorobanController { private static final Logger log = LoggerFactory.getLogger(InitiatorController.class); - private static final Pattern PAYNYM_REGEX = Pattern.compile("\\+[a-z]+[0-9][0-9a-fA-F][0-9a-fA-F]"); - private static final PayNym FIND_FOLLOWERS = new PayNym(null, null, "Retrieve PayNyms...", false); + private static final PayNym FIND_FOLLOWERS = new PayNym(null, null, "Retrieve PayNyms...", false, Collections.emptyList(), Collections.emptyList()); private String walletId; private Wallet wallet; @@ -74,6 +75,12 @@ public class InitiatorController extends SorobanController { @FXML private TextField counterparty; + @FXML + private ProgressIndicator payNymLoading; + + @FXML + private Button findPayNym; + @FXML private PayNymAvatar payNymAvatar; @@ -101,6 +108,8 @@ public class InitiatorController extends SorobanController { @FXML private TransactionDiagram transactionDiagram; + private final StringProperty counterpartyPayNymName = new SimpleStringProperty(); + private final ObjectProperty counterpartyPaymentCode = new SimpleObjectProperty<>(null); private final ObjectProperty stepProperty = new SimpleObjectProperty<>(Step.SETUP); @@ -147,6 +156,13 @@ public class InitiatorController extends SorobanController { } }); + payNymLoading.managedProperty().bind(payNymLoading.visibleProperty()); + payNymLoading.maxHeightProperty().bind(counterparty.heightProperty()); + payNymLoading.setVisible(false); + + findPayNym.managedProperty().bind(findPayNym.visibleProperty()); + findPayNym.setVisible(Config.get().isUsePayNym()); + payNymAvatar.managedProperty().bind(payNymAvatar.visibleProperty()); payNymFollowers.prefWidthProperty().bind(counterparty.widthProperty()); payNymFollowers.valueProperty().addListener((observable, oldValue, payNym) -> { @@ -154,8 +170,11 @@ public class InitiatorController extends SorobanController { Config.get().setUsePayNym(true); setPayNymFollowers(); } else if(payNym != null) { - counterparty.setText(payNym.nymName()); + counterpartyPayNymName.set(payNym.nymName()); + counterpartyPaymentCode.set(payNym.paymentCode()); payNymAvatar.setPaymentCode(payNym.paymentCode()); + counterparty.setText(payNym.nymName()); + step1.requestFocus(); } }); payNymFollowers.setConverter(new StringConverter<>() { @@ -177,7 +196,9 @@ public class InitiatorController extends SorobanController { PaymentCode paymentCode = new PaymentCode(input); if(paymentCode.isValid()) { counterpartyPaymentCode.set(paymentCode); - payNymAvatar.setPaymentCode(paymentCode); + if(payNymAvatar.getPaymentCode() == null || !input.equals(payNymAvatar.getPaymentCode().toString())) { + payNymAvatar.setPaymentCode(paymentCode); + } TextInputControl control = (TextInputControl)change.getControl(); change.setText(input.substring(0, 12) + "..." + input.substring(input.length() - 5)); @@ -199,16 +220,23 @@ public class InitiatorController extends SorobanController { if(newValue.startsWith("P") && newValue.contains("...") && newValue.length() == 20 && counterpartyPaymentCode.get() != null) { //Assumed valid payment code } else if(Config.get().isUsePayNym() && PAYNYM_REGEX.matcher(newValue).matches()) { - Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); - soroban.getPayNym(newValue).subscribe(payNym -> { - counterpartyPaymentCode.set(payNym.paymentCode()); - payNymAvatar.setPaymentCode(payNym.paymentCode()); - }, error -> { - //ignore, probably doesn't exist but will try again on meeting request - }); + if(!newValue.equals(counterpartyPayNymName.get())) { + Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); + payNymLoading.setVisible(true); + soroban.getPayNym(newValue).subscribe(payNym -> { + payNymLoading.setVisible(false); + counterpartyPayNymName.set(payNym.nymName()); + counterpartyPaymentCode.set(payNym.paymentCode()); + payNymAvatar.setPaymentCode(payNym.paymentCode()); + }, error -> { + payNymLoading.setVisible(false); + //ignore, probably doesn't exist but will try again on meeting request + }); + } } else { + counterpartyPayNymName.set(null); counterpartyPaymentCode.set(null); - payNymAvatar.getChildren().clear(); + payNymAvatar.setPaymentCode(null); } } }); @@ -231,7 +259,8 @@ public class InitiatorController extends SorobanController { private void setPayNymFollowers() { Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); if(soroban.getPaymentCode() != null) { - soroban.getFollowers().subscribe(followerPayNyms -> { + soroban.getFollowing().subscribe(followerPayNyms -> { + findPayNym.setVisible(true); payNymFollowers.setItems(FXCollections.observableList(followerPayNyms)); }, error -> { if(error.getMessage().endsWith("404")) { @@ -352,7 +381,7 @@ public class InitiatorController extends SorobanController { Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); Payment payment = walletTransaction.getPayments().get(0); - Map firstSetUtxos = walletTransaction.getSelectedUtxoSets().get(0); + Map firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getWalletUtxos(); for(Map.Entry entry : firstSetUtxos.entrySet()) { initiatorCahootsWallet.addUtxo(wallet, entry.getValue(), wallet.getTransactions().get(entry.getKey().getHash()), (int)entry.getKey().getIndex()); } @@ -461,6 +490,18 @@ public class InitiatorController extends SorobanController { transactionAccepted.set(Boolean.FALSE); } + public void findPayNym(ActionEvent event) { + PayNymDialog payNymDialog = new PayNymDialog(walletId, true); + Optional optPayNym = payNymDialog.showAndWait(); + optPayNym.ifPresent(payNym -> { + counterpartyPayNymName.set(payNym.nymName()); + counterpartyPaymentCode.set(payNym.paymentCode()); + payNymAvatar.setPaymentCode(payNym.paymentCode()); + counterparty.setText(payNym.nymName()); + step1.requestFocus(); + }); + } + public ObjectProperty counterpartyPaymentCodeProperty() { return counterpartyPaymentCode; } diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNym.java b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNym.java index 451ce5bc..5ec34d2d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNym.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNym.java @@ -2,4 +2,6 @@ package com.sparrowwallet.sparrow.soroban; import com.samourai.wallet.bip47.rpc.PaymentCode; -public record PayNym(PaymentCode paymentCode, String nymId, String nymName, boolean segwit) {} +import java.util.List; + +public record PayNym(PaymentCode paymentCode, String nymId, String nymName, boolean segwit, List following, List followers) {} diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymController.java new file mode 100644 index 00000000..3c4b5ddb --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymController.java @@ -0,0 +1,273 @@ +package com.sparrowwallet.sparrow.soroban; + +import com.google.common.eventbus.Subscribe; +import com.samourai.wallet.bip47.rpc.PaymentCode; +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.control.*; +import com.sparrowwallet.sparrow.event.FollowPayNymEvent; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.function.UnaryOperator; + +public class PayNymController extends SorobanController { + private static final Logger log = LoggerFactory.getLogger(PayNymController.class); + + private String walletId; + private PayNym walletPayNym; + + @FXML + private CopyableTextField payNymName; + + @FXML + private PaymentCodeTextField paymentCode; + + @FXML + private CopyableTextField searchPayNyms; + + @FXML + private ProgressIndicator findPayNym; + + @FXML + private PayNymAvatar payNymAvatar; + + @FXML + private ListView followingList; + + @FXML + private ListView followersList; + + private final ObjectProperty payNymProperty = new SimpleObjectProperty<>(null); + + private final StringProperty findNymProperty = new SimpleStringProperty(); + + public void initializeView(String walletId) { + this.walletId = walletId; + + findNymProperty.addListener((observable, oldValue, nymIdentifier) -> { + if(nymIdentifier != null) { + searchFollowing(nymIdentifier); + } + }); + + UnaryOperator paymentCodeFilter = change -> { + String input = change.getControlNewText(); + if(input.startsWith("P") && !input.contains("...")) { + try { + PaymentCode paymentCode = new PaymentCode(input); + if(paymentCode.isValid()) { + findNymProperty.set(input); + + TextInputControl control = (TextInputControl)change.getControl(); + change.setText(input.substring(0, 12) + "..." + input.substring(input.length() - 5)); + change.setRange(0, control.getLength()); + change.setAnchor(change.getText().length()); + change.setCaretPosition(change.getText().length()); + } + } catch(Exception e) { + //ignore + } + } else if(PAYNYM_REGEX.matcher(input).matches()) { + findNymProperty.set(input); + } else { + findNymProperty.set(null); + resetFollowing(); + } + + return change; + }; + searchPayNyms.setTextFormatter(new TextFormatter<>(paymentCodeFilter)); + findPayNym.managedProperty().bind(findPayNym.visibleProperty()); + findPayNym.maxHeightProperty().bind(searchPayNyms.heightProperty()); + findPayNym.setVisible(false); + + followingList.setCellFactory(param -> { + return new PayNymCell(walletId); + }); + + followingList.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, payNym) -> { + payNymProperty.set(payNym); + }); + + followersList.setCellFactory(param -> { + return new PayNymCell(walletId); + }); + + followersList.setSelectionModel(new NoSelectionModel<>()); + followersList.setFocusTraversable(false); + + refresh(); + } + + private void refresh() { + Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); + if(soroban.getPaymentCode() == null) { + throw new IllegalStateException("Payment code has not been set"); + } + + soroban.getPayNym(soroban.getPaymentCode().toString()).subscribe(payNym -> { + walletPayNym = payNym; + payNymName.setText(payNym.nymName()); + paymentCode.setPaymentCode(payNym.paymentCode()); + payNymAvatar.setPaymentCode(payNym.paymentCode()); + followingList.setUserData(null); + followingList.setItems(FXCollections.observableList(payNym.following())); + followersList.setItems(FXCollections.observableList(payNym.followers())); + }); + } + + private void resetFollowing() { + if(followingList.getUserData() != null) { + followingList.setUserData(null); + followingList.setItems(FXCollections.observableList(walletPayNym.following())); + } + } + + private void searchFollowing(String nymIdentifier) { + Optional optExisting = walletPayNym.following().stream().filter(payNym -> payNym.nymName().equals(nymIdentifier) || payNym.paymentCode().toString().equals(nymIdentifier)).findFirst(); + if(optExisting.isPresent()) { + followingList.setUserData(Boolean.FALSE); + List existingPayNym = new ArrayList<>(); + existingPayNym.add(optExisting.get()); + followingList.setItems(FXCollections.observableList(existingPayNym)); + } else { + followingList.setUserData(Boolean.TRUE); + followingList.setItems(FXCollections.observableList(new ArrayList<>())); + findPayNym.setVisible(true); + + Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); + soroban.getPayNym(nymIdentifier).subscribe(searchedPayNym -> { + findPayNym.setVisible(false); + List searchList = new ArrayList<>(); + searchList.add(searchedPayNym); + followingList.setUserData(Boolean.TRUE); + followingList.setItems(FXCollections.observableList(searchList)); + }, error -> { + findPayNym.setVisible(false); + }); + } + } + + public void showQR(ActionEvent event) { + Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); + QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(soroban.getPaymentCode().toString()); + qrDisplayDialog.showAndWait(); + } + + public void scanQR(ActionEvent event) { + QRScanDialog qrScanDialog = new QRScanDialog(); + Optional optResult = qrScanDialog.showAndWait(); + if(optResult.isPresent()) { + QRScanDialog.Result result = optResult.get(); + if(result.payload != null) { + searchPayNyms.setText(result.payload); + } else { + AppServices.showErrorDialog("Invalid QR Code", "Cannot parse QR code into a payment code"); + } + } + } + + public PayNym getPayNym() { + return payNymProperty.get(); + } + + public ObjectProperty payNymProperty() { + return payNymProperty; + } + + @Subscribe + public void followPayNym(FollowPayNymEvent event) { + if(event.getWalletId().equals(walletId)) { + Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); + soroban.getAuthToken(new HashMap<>()).subscribe(authToken -> { + String signature = soroban.getSignature(authToken); + soroban.followPaymentCode(event.getPaymentCode(), authToken, signature).subscribe(followMap -> { + refresh(); + }, error -> { + log.error("Could not follow payment code", error); + AppServices.showErrorDialog("Could not follow payment code", error.getMessage()); + }); + }); + } + } + + public static class NoSelectionModel extends MultipleSelectionModel { + + @Override + public ObservableList getSelectedIndices() { + return FXCollections.emptyObservableList(); + } + + @Override + public ObservableList getSelectedItems() { + return FXCollections.emptyObservableList(); + } + + @Override + public void selectIndices(int index, int... indices) { + } + + @Override + public void selectAll() { + } + + @Override + public void selectFirst() { + } + + @Override + public void selectLast() { + } + + @Override + public void clearAndSelect(int index) { + } + + @Override + public void select(int index) { + } + + @Override + public void select(T obj) { + } + + @Override + public void clearSelection(int index) { + } + + @Override + public void clearSelection() { + } + + @Override + public boolean isSelected(int index) { + return false; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void selectPrevious() { + } + + @Override + public void selectNext() { + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymDialog.java b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymDialog.java new file mode 100644 index 00000000..062712c5 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymDialog.java @@ -0,0 +1,55 @@ +package com.sparrowwallet.sparrow.soroban; + +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.EventManager; +import javafx.fxml.FXMLLoader; +import javafx.scene.control.*; + +import java.io.IOException; + +public class PayNymDialog extends Dialog { + public PayNymDialog(String walletId, boolean selectPayNym) { + final DialogPane dialogPane = getDialogPane(); + AppServices.setStageIcon(dialogPane.getScene().getWindow()); + AppServices.onEscapePressed(dialogPane.getScene(), this::close); + + try { + FXMLLoader payNymLoader = new FXMLLoader(AppServices.class.getResource("soroban/paynym.fxml")); + dialogPane.setContent(payNymLoader.load()); + PayNymController payNymController = payNymLoader.getController(); + payNymController.initializeView(walletId); + EventManager.get().register(payNymController); + + dialogPane.setPrefWidth(730); + dialogPane.setPrefHeight(600); + AppServices.moveToActiveWindowScreen(this); + + dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm()); + dialogPane.getStylesheets().add(AppServices.class.getResource("soroban/paynym.css").toExternalForm()); + + final ButtonType selectButtonType = new javafx.scene.control.ButtonType("Select PayNym", ButtonBar.ButtonData.APPLY); + final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); + final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE); + + if(selectPayNym) { + dialogPane.getButtonTypes().addAll(selectButtonType, cancelButtonType); + Button selectButton = (Button)dialogPane.lookupButton(selectButtonType); + selectButton.setDisable(true); + selectButton.setDefaultButton(true); + payNymController.payNymProperty().addListener((observable, oldValue, payNym) -> { + selectButton.setDisable(payNym == null); + }); + } else { + dialogPane.getButtonTypes().add(doneButtonType); + } + + setOnCloseRequest(event -> { + EventManager.get().unregister(payNymController); + }); + + setResultConverter(dialogButton -> dialogButton == selectButtonType ? payNymController.getPayNym() : null); + } catch(IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymService.java b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymService.java index 99302d6f..21943ad6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymService.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/PayNymService.java @@ -9,6 +9,7 @@ import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; import io.reactivex.schedulers.Schedulers; import java8.util.Optional; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -124,16 +125,18 @@ public class PayNymService { return fetchPayNym(nymIdentifier).map(nymMap -> { List> codes = (List>)nymMap.get("codes"); PaymentCode code = new PaymentCode((String)codes.stream().filter(codeMap -> codeMap.get("segwit") == Boolean.FALSE).map(codeMap -> codeMap.get("code")).findFirst().orElse(codes.get(0).get("code"))); - return new PayNym(code, (String)nymMap.get("nymID"), (String)nymMap.get("nymName"), (Boolean)nymMap.get("segwit")); - }); - } - public Observable> getFollowers(String nymIdentifier) { - return fetchPayNym(nymIdentifier).flatMap(nymMap -> { - List> followers = (List>)nymMap.get("following"); - return Observable.fromArray(followers.stream().map(followerMap -> { - return new PayNym(new PaymentCode((String)followerMap.get("code")), (String)followerMap.get("nymId"), (String)followerMap.get("nymName"), (Boolean)followerMap.get("segwit")); - }).collect(Collectors.toList())); + List> followingMaps = (List>)nymMap.get("following"); + List following = followingMaps.stream().map(followingMap -> { + return new PayNym(new PaymentCode((String)followingMap.get("code")), (String)followingMap.get("nymId"), (String)followingMap.get("nymName"), (Boolean)followingMap.get("segwit"), Collections.emptyList(), Collections.emptyList()); + }).collect(Collectors.toList()); + + List> followersMaps = (List>)nymMap.get("followers"); + List followers = followersMaps.stream().map(followerMap -> { + return new PayNym(new PaymentCode((String)followerMap.get("code")), (String)followerMap.get("nymId"), (String)followerMap.get("nymName"), (Boolean)followerMap.get("segwit"), Collections.emptyList(), Collections.emptyList()); + }).collect(Collectors.toList()); + + return new PayNym(code, (String)nymMap.get("nymID"), (String)nymMap.get("nymName"), (Boolean)nymMap.get("segwit"), following, followers); }); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java b/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java index ccc531d7..5b99990d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java @@ -171,8 +171,8 @@ public class Soroban { return payNymService.getPayNym(nymIdentifier); } - public Observable> getFollowers() { - return payNymService.getFollowers(paymentCode.toString()); + public Observable> getFollowing() { + return payNymService.getPayNym(paymentCode.toString()).map(PayNym::following); } public Observable getAuthToken(Map map) { diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java index fb39aa1f..fd3b1caf 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java @@ -16,10 +16,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; +import java.util.regex.Pattern; import java.util.stream.Collectors; public class SorobanController { private static final Logger log = LoggerFactory.getLogger(SorobanController.class); + protected static final Pattern PAYNYM_REGEX = Pattern.compile("\\+[a-z]+[0-9][0-9a-fA-F][0-9a-fA-F]"); protected Transaction getTransaction(Cahoots cahoots) throws PSBTParseException { if(cahoots.getPSBT() != null) { diff --git a/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.fxml b/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.fxml index 2ed7e8f9..a7b0289d 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/soroban/counterparty.fxml @@ -34,39 +34,58 @@