Craig Raw
3 years ago
17 changed files with 834 additions and 117 deletions
@ -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); |
|||
} |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
|||
} |
@ -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; |
|||
} |
@ -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 new issue