mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
add and integrate paynym dialog
This commit is contained in:
parent
4edd84f6e2
commit
44194a074c
17 changed files with 834 additions and 117 deletions
|
@ -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<PaymentCode> paymentCodeProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private static final Map<String, Image> paymentCodeCache = Collections.synchronizedMap(new HashMap<>());
|
||||
private static final Map<String, Object> paymentCodeLoading = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
public PayNymAvatar() {
|
||||
super();
|
||||
|
||||
paymentCodeProperty.addListener((observable, oldValue, newValue) -> {
|
||||
if(Config.get().isUsePayNym()) {
|
||||
PayNymAvatarService payNymAvatarService = new PayNymAvatarService(newValue);
|
||||
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 -> {
|
||||
Circle circle = new Circle(getWidth() / 2,getHeight() / 2,getWidth() / 2);
|
||||
circle.setFill(new ImagePattern(payNymAvatarService.getValue()));
|
||||
getChildren().add(circle);
|
||||
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<Image> {
|
||||
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<Image> {
|
||||
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 cacheId = getCacheId(paymentCode, width);
|
||||
|
||||
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())) {
|
||||
return new Image(is, getWidth(), getHeight(), true, false);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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<PayNym> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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'),
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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<PaymentCode> counterpartyPaymentCode = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<Step> 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);
|
||||
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()) {
|
||||
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<BlockTransactionHashIndex, WalletNode> firstSetUtxos = walletTransaction.getSelectedUtxoSets().get(0);
|
||||
Map<BlockTransactionHashIndex, WalletNode> firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getWalletUtxos();
|
||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> 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<PayNym> 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<PaymentCode> counterpartyPaymentCodeProperty() {
|
||||
return counterpartyPaymentCode;
|
||||
}
|
||||
|
|
|
@ -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<PayNym> following, List<PayNym> followers) {}
|
||||
|
|
|
@ -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<PayNym> followingList;
|
||||
|
||||
@FXML
|
||||
private ListView<PayNym> followersList;
|
||||
|
||||
private final ObjectProperty<PayNym> 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<TextFormatter.Change> 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<PayNym> optExisting = walletPayNym.following().stream().filter(payNym -> payNym.nymName().equals(nymIdentifier) || payNym.paymentCode().toString().equals(nymIdentifier)).findFirst();
|
||||
if(optExisting.isPresent()) {
|
||||
followingList.setUserData(Boolean.FALSE);
|
||||
List<PayNym> 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<PayNym> 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<QRScanDialog.Result> 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<PayNym> 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<T> extends MultipleSelectionModel<T> {
|
||||
|
||||
@Override
|
||||
public ObservableList<Integer> getSelectedIndices() {
|
||||
return FXCollections.emptyObservableList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<T> 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() {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<PayNym> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Map<String, Object>> codes = (List<Map<String, Object>>)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<List<PayNym>> getFollowers(String nymIdentifier) {
|
||||
return fetchPayNym(nymIdentifier).flatMap(nymMap -> {
|
||||
List<Map<String, Object>> followers = (List<Map<String, Object>>)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<Map<String, Object>> followingMaps = (List<Map<String, Object>>)nymMap.get("following");
|
||||
List<PayNym> 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<Map<String, Object>> followersMaps = (List<Map<String, Object>>)nymMap.get("followers");
|
||||
List<PayNym> 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,8 +171,8 @@ public class Soroban {
|
|||
return payNymService.getPayNym(nymIdentifier);
|
||||
}
|
||||
|
||||
public Observable<List<PayNym>> getFollowers() {
|
||||
return payNymService.getFollowers(paymentCode.toString());
|
||||
public Observable<List<PayNym>> getFollowing() {
|
||||
return payNymService.getPayNym(paymentCode.toString()).map(PayNym::following);
|
||||
}
|
||||
|
||||
public Observable<String> getAuthToken(Map<String, Object> map) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -34,15 +34,33 @@
|
|||
</graphic>
|
||||
</Label>
|
||||
<Label text="Perform a two person coinjoin transaction using the Samourai Soroban service. Your mix partner will start the mix, and will need either your PayNym or the Payment code shown below. Click Next once they have indicated they are ready." wrapText="true" styleClass="content-text" />
|
||||
<HBox>
|
||||
<BorderPane>
|
||||
<padding>
|
||||
<Insets top="20" />
|
||||
<Insets top="20" right="70" />
|
||||
</padding>
|
||||
<center>
|
||||
<VBox spacing="15">
|
||||
<HBox styleClass="field-box">
|
||||
<Label text="PayNym:" styleClass="field-label" />
|
||||
<HBox spacing="10">
|
||||
<CopyableTextField fx:id="payNym" promptText="Retrieving..." styleClass="field-control" editable="false"/>
|
||||
<Button fx:id="payNymButton" text="Retrieve PayNym" onAction="#retrievePayNym" />
|
||||
<Button fx:id="showPayNym" onAction="#showPayNym">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="ROBOT" />
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="Show PayNym" />
|
||||
</tooltip>
|
||||
</Button>
|
||||
</HBox>
|
||||
<Button fx:id="payNymButton" text="Retrieve PayNym" graphicTextGap="8" onAction="#retrievePayNym">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="ROBOT" />
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="Retrieves and claims the PayNym for this wallet" />
|
||||
</tooltip>
|
||||
</Button>
|
||||
</HBox>
|
||||
<HBox styleClass="field-box">
|
||||
<Label text="Payment code:" styleClass="field-label" />
|
||||
|
@ -63,10 +81,11 @@
|
|||
<ComboBox fx:id="mixWallet" />
|
||||
</HBox>
|
||||
</VBox>
|
||||
<AnchorPane>
|
||||
<PayNymAvatar AnchorPane.leftAnchor="100" fx:id="payNymAvatar" prefWidth="150" prefHeight="150" />
|
||||
</AnchorPane>
|
||||
</HBox>
|
||||
</center>
|
||||
<right>
|
||||
<PayNymAvatar fx:id="payNymAvatar" prefWidth="150" prefHeight="150" />
|
||||
</right>
|
||||
</BorderPane>
|
||||
</VBox>
|
||||
<VBox fx:id="step2" spacing="15">
|
||||
<HBox>
|
||||
|
@ -79,14 +98,15 @@
|
|||
<ProgressTimer fx:id="step2Timer" seconds="60" />
|
||||
</HBox>
|
||||
<Label fx:id="step2Desc" text="Your mix partner will now initiate the Soroban communication. Once communication is established, check the details of the mix and click Next if you'd like to proceed." wrapText="true" styleClass="content-text" />
|
||||
<HBox>
|
||||
<BorderPane>
|
||||
<padding>
|
||||
<Insets top="20" />
|
||||
<Insets top="20" right="70" />
|
||||
</padding>
|
||||
<center>
|
||||
<VBox spacing="10">
|
||||
<HBox styleClass="field-box">
|
||||
<Label text="Mix partner:" styleClass="field-label" />
|
||||
<Label fx:id="mixingPartner" text="Waiting for mix partner..." graphicTextGap="10" contentDisplay="RIGHT" />
|
||||
<Label fx:id="mixingPartner" text="Waiting for mix partner..." styleClass="field-control" />
|
||||
<Label fx:id="meetingFail" text="Failed to find mix partner." styleClass="failure" graphicTextGap="5">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_CIRCLE" styleClass="failure" />
|
||||
|
@ -104,10 +124,11 @@
|
|||
</HBox>
|
||||
</VBox>
|
||||
</VBox>
|
||||
<AnchorPane>
|
||||
<PayNymAvatar AnchorPane.leftAnchor="100" fx:id="mixPartnerAvatar" prefWidth="150" prefHeight="150" />
|
||||
</AnchorPane>
|
||||
</HBox>
|
||||
</center>
|
||||
<right>
|
||||
<PayNymAvatar fx:id="mixPartnerAvatar" prefWidth="150" prefHeight="150" />
|
||||
</right>
|
||||
</BorderPane>
|
||||
</VBox>
|
||||
<VBox fx:id="step3" spacing="15">
|
||||
<HBox>
|
||||
|
@ -140,7 +161,7 @@
|
|||
<Label text="The broadcasted transaction is shown below." wrapText="true" styleClass="content-text" />
|
||||
<HBox>
|
||||
<padding>
|
||||
<Insets top="20" />
|
||||
<Insets top="20" left="10" />
|
||||
</padding>
|
||||
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
|
||||
</HBox>
|
||||
|
|
|
@ -34,25 +34,36 @@
|
|||
</graphic>
|
||||
</Label>
|
||||
<Label text="Add a mix partner to your two person coinjoin transaction using the Samourai Soroban service. Ask your partner for their PayNym, or use their payment code found in their Sparrow Tools menu → Find Mix Partner." wrapText="true" styleClass="content-text" />
|
||||
<HBox>
|
||||
<BorderPane>
|
||||
<padding>
|
||||
<Insets top="20" />
|
||||
<Insets top="20" right="70" />
|
||||
</padding>
|
||||
<center>
|
||||
<VBox spacing="15">
|
||||
<HBox styleClass="field-box">
|
||||
<Label text="Payment code or PayNym:" styleClass="field-label" />
|
||||
<HBox spacing="10">
|
||||
<HBox>
|
||||
<StackPane>
|
||||
<ComboBox fx:id="payNymFollowers" />
|
||||
<ComboBoxTextField fx:id="counterparty" styleClass="field-control" comboProperty="$payNymFollowers" />
|
||||
</StackPane>
|
||||
<ProgressIndicator fx:id="payNymLoading" />
|
||||
</HBox>
|
||||
</HBox>
|
||||
<HBox styleClass="field-box">
|
||||
<Label styleClass="field-label" />
|
||||
<Button fx:id="findPayNym" text="Find PayNym" graphicTextGap="10" onAction="#findPayNym">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="ROBOT" />
|
||||
</graphic>
|
||||
</Button>
|
||||
</HBox>
|
||||
</VBox>
|
||||
<AnchorPane>
|
||||
<PayNymAvatar fx:id="payNymAvatar" AnchorPane.leftAnchor="80" prefWidth="150" prefHeight="150"/>
|
||||
</AnchorPane>
|
||||
</HBox>
|
||||
</center>
|
||||
<right>
|
||||
<PayNymAvatar fx:id="payNymAvatar" prefWidth="150" prefHeight="150"/>
|
||||
</right>
|
||||
</BorderPane>
|
||||
</VBox>
|
||||
<VBox fx:id="step2" spacing="15">
|
||||
<HBox>
|
||||
|
@ -89,7 +100,7 @@
|
|||
<Label fx:id="step3Desc" text="Review the transaction and broadcast when ready." wrapText="true" styleClass="content-text" />
|
||||
<HBox>
|
||||
<padding>
|
||||
<Insets top="20" />
|
||||
<Insets top="20" left="10" />
|
||||
</padding>
|
||||
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
|
||||
</HBox>
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
.paynym-pane {
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
.title-area {
|
||||
-fx-background-color: -fx-control-inner-background;
|
||||
-fx-padding: 10 25 10 25;
|
||||
-fx-border-width: 0px 0px 1px 0px;
|
||||
-fx-border-color: #e5e5e6;
|
||||
}
|
||||
|
||||
.button-bar {
|
||||
-fx-padding: 10 25 25 25;
|
||||
}
|
||||
|
||||
.button-bar .container {
|
||||
-fx-padding: 0 0 15px 0;
|
||||
}
|
||||
|
||||
.title-label {
|
||||
-fx-font-size: 24px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
-fx-font-size: 20px;
|
||||
-fx-padding: 0 0 15px 0;
|
||||
-fx-graphic-text-gap: 10px;
|
||||
}
|
||||
|
||||
.content-text {
|
||||
-fx-font-size: 16px;
|
||||
-fx-text-fill: derive(-fx-text-base-color, 15%);
|
||||
}
|
||||
|
||||
.field-box {
|
||||
-fx-pref-height: 30px;
|
||||
-fx-alignment: CENTER_LEFT;
|
||||
}
|
||||
|
||||
.wide-field-label {
|
||||
-fx-pref-width: 180px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
-fx-pref-width: 110px;
|
||||
}
|
||||
|
||||
.field-control {
|
||||
-fx-pref-width: 184px;
|
||||
}
|
||||
|
||||
.listview-label {
|
||||
-fx-font-weight: bold;
|
||||
-fx-font-size: 1.2em;
|
||||
-fx-padding: 10 0 10 0;
|
||||
}
|
108
src/main/resources/com/sparrowwallet/sparrow/soroban/paynym.fxml
Normal file
108
src/main/resources/com/sparrowwallet/sparrow/soroban/paynym.fxml
Normal file
|
@ -0,0 +1,108 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.*?>
|
||||
<?import java.util.*?>
|
||||
<?import javafx.scene.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import com.sparrowwallet.sparrow.control.PayNymAvatar?>
|
||||
<?import com.sparrowwallet.sparrow.control.CopyableTextField?>
|
||||
<?import com.sparrowwallet.sparrow.control.PaymentCodeTextField?>
|
||||
<?import org.controlsfx.glyphfont.Glyph?>
|
||||
|
||||
<StackPane prefHeight="460.0" prefWidth="600.0" stylesheets="@paynym.css, @../general.css" styleClass="paynym-pane" fx:controller="com.sparrowwallet.sparrow.soroban.PayNymController" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
|
||||
<VBox spacing="10">
|
||||
<HBox styleClass="title-area">
|
||||
<HBox alignment="CENTER_LEFT">
|
||||
<Label text="PayNym" styleClass="title-label" />
|
||||
</HBox>
|
||||
<Region HBox.hgrow="ALWAYS"/>
|
||||
<ImageView AnchorPane.rightAnchor="0">
|
||||
<Image url="/image/paynym.png" requestedWidth="50" requestedHeight="50" smooth="false" />
|
||||
</ImageView>
|
||||
</HBox>
|
||||
<BorderPane>
|
||||
<padding>
|
||||
<Insets top="20" left="25" right="100" />
|
||||
</padding>
|
||||
<center>
|
||||
<VBox spacing="15">
|
||||
<HBox styleClass="field-box">
|
||||
<Label text="PayNym:" styleClass="field-label" />
|
||||
<CopyableTextField fx:id="payNymName" promptText="Retrieving..." styleClass="field-control" editable="false"/>
|
||||
</HBox>
|
||||
<HBox styleClass="field-box">
|
||||
<Label text="Payment code:" styleClass="field-label" />
|
||||
<HBox spacing="10">
|
||||
<PaymentCodeTextField fx:id="paymentCode" styleClass="field-control" editable="false"/>
|
||||
<Button onAction="#showQR">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="QRCODE" />
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="Show as QR code" />
|
||||
</tooltip>
|
||||
</Button>
|
||||
</HBox>
|
||||
</HBox>
|
||||
<HBox styleClass="field-box">
|
||||
<padding>
|
||||
<Insets top="35" />
|
||||
</padding>
|
||||
<Label text="Find:" styleClass="field-label" />
|
||||
<HBox spacing="10">
|
||||
<CopyableTextField fx:id="searchPayNyms" promptText="PayNym or Payment code" styleClass="field-control"/>
|
||||
<Button onAction="#scanQR">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="CAMERA" />
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="Scan payment code" />
|
||||
</tooltip>
|
||||
</Button>
|
||||
<ProgressIndicator fx:id="findPayNym" />
|
||||
</HBox>
|
||||
</HBox>
|
||||
</VBox>
|
||||
</center>
|
||||
<right>
|
||||
<PayNymAvatar fx:id="payNymAvatar" prefHeight="150" prefWidth="150" />
|
||||
</right>
|
||||
</BorderPane>
|
||||
<GridPane hgap="15">
|
||||
<padding>
|
||||
<Insets right="25" left="25" />
|
||||
</padding>
|
||||
<columnConstraints>
|
||||
<ColumnConstraints percentWidth="50.0" />
|
||||
<ColumnConstraints percentWidth="50.0" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints percentHeight="100" />
|
||||
</rowConstraints>
|
||||
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||
<top>
|
||||
<HBox alignment="CENTER_LEFT">
|
||||
<Label styleClass="listview-label" text="Following"/>
|
||||
</HBox>
|
||||
</top>
|
||||
<center>
|
||||
<ListView fx:id="followingList" prefHeight="220" />
|
||||
</center>
|
||||
</BorderPane>
|
||||
<BorderPane GridPane.columnIndex="1" GridPane.rowIndex="0">
|
||||
<top>
|
||||
<HBox alignment="CENTER_LEFT">
|
||||
<Label styleClass="listview-label" text="Followers"/>
|
||||
</HBox>
|
||||
</top>
|
||||
<center>
|
||||
<ListView fx:id="followersList" prefHeight="220" />
|
||||
</center>
|
||||
</BorderPane>
|
||||
</GridPane>
|
||||
</VBox>
|
||||
</StackPane>
|
Loading…
Reference in a new issue