Merge pull request #5423 from jmacxx/dupOffer_2

Functionality to duplicate an offer
This commit is contained in:
Christoph Atteneder 2021-04-22 09:16:33 +02:00 committed by GitHub
commit c17b46fc2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 471 additions and 69 deletions

View file

@ -583,6 +583,9 @@ portfolio.tab.pendingTrades=Open trades
portfolio.tab.history=History
portfolio.tab.failed=Failed
portfolio.tab.editOpenOffer=Edit offer
portfolio.tab.duplicateOffer=Duplicate offer
portfolio.context.offerLikeThis=Create new offer like this...
portfolio.context.notYourOffer=You can only duplicate offers where you were the maker.
portfolio.closedTrades.deviation.help=Percentage price deviation from market
@ -2945,9 +2948,9 @@ popup.shutDownInProgress.headline=Shut down in progress
popup.shutDownInProgress.msg=Shutting down application can take a few seconds.\nPlease don't interrupt this process.
popup.attention.forTradeWithId=Attention required for trade with ID {0}
popup.attention.reasonForPaymentRuleChange=Version 1.5.5 introduces a critical trade rule change regarding \
the \"reason for payment\" field in bank transfers. Please leave this field empty -- \
DO NOT use the trade ID as \"reason for payment\" anymore.
popup.attention.newFeatureDuplicateOffer=Version 1.6.3 introduces a new feature allowing easy re-entry of offers \
by right-clicking on an existing offer or trade and choosing `Create new offer like this`. This is useful for \
traders who frequently make the same offer.
popup.info.multiplePaymentAccounts.headline=Multiple payment accounts available
popup.info.multiplePaymentAccounts.msg=You have multiple payment accounts available for this offer. Please make sure you've picked the right one.

View file

@ -48,10 +48,7 @@ public final class Navigation implements PersistedDataHost {
private static final ViewPath DEFAULT_VIEW_PATH = ViewPath.to(MainView.class, MarketView.class);
public interface Listener {
void onNavigationRequested(ViewPath path);
default void onNavigationRequested(ViewPath path, @Nullable Object data) {
}
void onNavigationRequested(ViewPath path, @Nullable Object data);
}
// New listeners can be added during iteration so we use CopyOnWriteArrayList to
@ -137,7 +134,6 @@ public final class Navigation implements PersistedDataHost {
currentPath = newPath;
previousPath = currentPath;
listeners.forEach((e) -> e.onNavigationRequested(currentPath));
listeners.forEach((e) -> e.onNavigationRequested(currentPath, data));
requestPersistence();
}

View file

@ -43,4 +43,8 @@ public class CachingViewLoader implements ViewLoader {
cache.put(viewClass, view);
return view;
}
public void removeFromCache(Class<? extends View> viewClass) {
cache.remove(viewClass);
}
}

View file

@ -367,7 +367,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel>
setupBadge(supportButtonWithBadge, model.getNumOpenSupportTickets(), model.getShowOpenSupportTicketsNotification());
setupBadge(settingsButtonWithBadge, new SimpleStringProperty(Res.get("shared.new")), model.getShowSettingsUpdatesNotification());
navigation.addListener(viewPath -> {
navigation.addListener((viewPath, data) -> {
if (viewPath.size() != 2 || viewPath.indexOf(MainView.class) != 0)
return;

View file

@ -278,22 +278,14 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
// We only show the popup if the user has already set up any fiat account. For new users it is not a rule
// change and for altcoins its not relevant.
String key = "reasonForPaymentChange";
boolean hasFiatAccount = user.getPaymentAccounts() != null &&
user.getPaymentAccounts().stream()
.filter(e -> !(e.getPaymentAccountPayload() instanceof AssetsAccountPayload))
.findAny()
.isPresent();
if (hasFiatAccount && DontShowAgainLookup.showAgain(key)) {
String key = "newFeatureDuplicateOffer";
if (DontShowAgainLookup.showAgain(key)) {
UserThread.runAfter(() -> {
new Popup().attention(Res.get("popup.attention.reasonForPaymentRuleChange")).
new Popup().attention(Res.get("popup.attention.newFeatureDuplicateOffer")).
dontShowAgainId(key)
.closeButtonText(Res.get("shared.iUnderstand"))
.show();
}, 1);
} else {
// If user add a fiat account later we don't show the popup as we assume it is a new user.
DontShowAgainLookup.dontShowAgain(key, true);
}
}

View file

@ -106,7 +106,7 @@ public class AccountView extends ActivatableView<TabPane, Void> {
walletInfoTab.setText(Res.get("account.menu.walletInfo").toUpperCase());
backupTab.setText(Res.get("account.menu.backup").toUpperCase());
navigationListener = viewPath -> {
navigationListener = (viewPath, data) -> {
if (viewPath.size() == 3 && viewPath.indexOf(AccountView.class) == 1) {
if (arbitratorRegistrationTab == null && viewPath.get(2).equals(ArbitratorRegistrationView.class)) {
navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class);

View file

@ -111,7 +111,7 @@ public class DaoView extends ActivatableView<TabPane, Void> {
root.getTabs().addAll(factsAndFiguresTab, bsqWalletTab, proposalsTab, bondingTab, burnBsqTab, monitorTab);
}
navigationListener = viewPath -> {
navigationListener = (viewPath, data) -> {
if (viewPath.size() == 3 && viewPath.indexOf(DaoView.class) == 1) {
if (proposalsTab == null && viewPath.get(2).equals(EconomyView.class))
navigation.navigateTo(MainView.class, DaoView.class, EconomyView.class);

View file

@ -73,19 +73,12 @@ public class BondingView extends ActivatableView<AnchorPane, Void> {
@Override
public void initialize() {
listener = new Navigation.Listener() {
@Override
public void onNavigationRequested(ViewPath path) {
}
listener = (viewPath, data) -> {
if (viewPath.size() != 4 || viewPath.indexOf(bisq.desktop.main.dao.bonding.BondingView.class) != 2)
return;
@Override
public void onNavigationRequested(ViewPath viewPath, @Nullable Object data) {
if (viewPath.size() != 4 || viewPath.indexOf(bisq.desktop.main.dao.bonding.BondingView.class) != 2)
return;
selectedViewClass = viewPath.tip();
loadView(selectedViewClass, data);
}
selectedViewClass = viewPath.tip();
loadView(selectedViewClass, data);
};
toggleGroup = new ToggleGroup();

View file

@ -68,7 +68,7 @@ public class BurnBsqView extends ActivatableView<AnchorPane, Void> {
@Override
public void initialize() {
listener = viewPath -> {
listener = (viewPath, data) -> {
if (viewPath.size() != 4 || viewPath.indexOf(BurnBsqView.class) != 2)
return;

View file

@ -71,7 +71,7 @@ public class EconomyView extends ActivatableView<AnchorPane, Void> {
@Override
public void initialize() {
listener = viewPath -> {
listener = (viewPath, data) -> {
if (viewPath.size() != 4 || viewPath.indexOf(EconomyView.class) != 2)
return;

View file

@ -81,7 +81,7 @@ public class GovernanceView extends ActivatableView<AnchorPane, Void> implements
@Override
public void initialize() {
navigationListener = viewPath -> {
navigationListener = (viewPath, data) -> {
if (viewPath.size() != 4 || viewPath.indexOf(GovernanceView.class) != 2)
return;

View file

@ -67,7 +67,7 @@ public class MonitorView extends ActivatableView<AnchorPane, Void> {
@Override
public void initialize() {
navigationListener = viewPath -> {
navigationListener = (viewPath, data) -> {
if (viewPath.size() != 4 || viewPath.indexOf(MonitorView.class) != 2)
return;

View file

@ -74,24 +74,12 @@ public class BsqWalletView extends ActivatableView<AnchorPane, Void> {
@Override
public void initialize() {
listener = new Navigation.Listener() {
@Override
public void onNavigationRequested(ViewPath viewPath) {
if (viewPath.size() != 4 || viewPath.indexOf(BsqWalletView.class) != 2)
return;
listener = (viewPath, data) -> {
if (viewPath.size() != 4 || viewPath.indexOf(BsqWalletView.class) != 2)
return;
selectedViewClass = viewPath.tip();
loadView(selectedViewClass);
}
@Override
public void onNavigationRequested(ViewPath viewPath, @Nullable Object data) {
if (viewPath.size() != 4 || viewPath.indexOf(BsqWalletView.class) != 2)
return;
selectedViewClass = viewPath.tip();
loadView(selectedViewClass, data);
}
selectedViewClass = viewPath.tip();
loadView(selectedViewClass, data);
};
toggleGroup = new ToggleGroup();

View file

@ -68,7 +68,7 @@ public class FundsView extends ActivatableView<TabPane, Void> {
lockedTab.setText(Res.get("funds.tab.locked").toUpperCase());
transactionsTab.setText(Res.get("funds.tab.transactions").toUpperCase());
navigationListener = viewPath -> {
navigationListener = (viewPath, data) -> {
if (viewPath.size() == 3 && viewPath.indexOf(FundsView.class) == 1)
loadView(viewPath.tip());
};

View file

@ -101,7 +101,7 @@ public class MarketView extends ActivatableView<TabPane, Void> {
spreadTabPaymentMethod.setText(Res.get("market.tabs.spreadPayment").toUpperCase());
tradesTab.setText(Res.get("market.tabs.trades").toUpperCase());
navigationListener = viewPath -> {
navigationListener = (viewPath, data) -> {
if (viewPath.size() == 3 && viewPath.indexOf(MarketView.class) == 1)
loadView(viewPath.tip());
};

View file

@ -100,7 +100,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
private final BtcValidator btcValidator;
private final BsqValidator bsqValidator;
protected final SecurityDepositValidator securityDepositValidator;
private final PriceFeedService priceFeedService;
protected final PriceFeedService priceFeedService;
private final AccountAgeWitnessService accountAgeWitnessService;
private final Navigation navigation;
private final Preferences preferences;
@ -179,7 +179,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
private ChangeListener<Boolean> isWalletFundedListener;
private ChangeListener<String> errorMessageListener;
private Offer offer;
protected Offer offer;
private Timer timeoutTimer;
private boolean inputIsMarketBasedPrice;
private ChangeListener<Boolean> useMarketBasedPriceListener;

View file

@ -94,7 +94,7 @@ public abstract class OfferView extends ActivatableView<TabPane, Void> {
@Override
protected void initialize() {
navigationListener = viewPath -> {
navigationListener = (viewPath, data) -> {
if (viewPath.size() == 3 && viewPath.indexOf(this.getClass()) == 1)
loadView(viewPath.tip());
};

View file

@ -22,15 +22,16 @@ import bisq.desktop.common.view.ActivatableView;
import bisq.desktop.common.view.CachingViewLoader;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.common.view.View;
import bisq.desktop.common.view.ViewLoader;
import bisq.desktop.main.MainView;
import bisq.desktop.main.portfolio.closedtrades.ClosedTradesView;
import bisq.desktop.main.portfolio.duplicateoffer.DuplicateOfferView;
import bisq.desktop.main.portfolio.editoffer.EditOfferView;
import bisq.desktop.main.portfolio.failedtrades.FailedTradesView;
import bisq.desktop.main.portfolio.openoffer.OpenOffersView;
import bisq.desktop.main.portfolio.pendingtrades.PendingTradesView;
import bisq.core.locale.Res;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OpenOffer;
import bisq.core.trade.Trade;
import bisq.core.trade.failed.FailedTradesManager;
@ -48,22 +49,25 @@ import javafx.collections.ListChangeListener;
import java.util.List;
import javax.annotation.Nullable;
@FxmlView
public class PortfolioView extends ActivatableView<TabPane, Void> {
@FXML
Tab openOffersTab, pendingTradesTab, closedTradesTab;
private Tab editOpenOfferTab;
private Tab editOpenOfferTab, duplicateOfferTab;
private final Tab failedTradesTab = new Tab(Res.get("portfolio.tab.failed").toUpperCase());
private Tab currentTab;
private Navigation.Listener navigationListener;
private ChangeListener<Tab> tabChangeListener;
private ListChangeListener<Tab> tabListChangeListener;
private final ViewLoader viewLoader;
private final CachingViewLoader viewLoader;
private final Navigation navigation;
private final FailedTradesManager failedTradesManager;
private EditOfferView editOfferView;
private DuplicateOfferView duplicateOfferView;
private boolean editOpenOfferViewOpen;
private OpenOffer openOffer;
private OpenOffersView openOffersView;
@ -84,9 +88,9 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
pendingTradesTab.setText(Res.get("portfolio.tab.pendingTrades").toUpperCase());
closedTradesTab.setText(Res.get("portfolio.tab.history").toUpperCase());
navigationListener = viewPath -> {
navigationListener = (viewPath, data) -> {
if (viewPath.size() == 3 && viewPath.indexOf(PortfolioView.class) == 1)
loadView(viewPath.tip());
loadView(viewPath.tip(), data);
};
tabChangeListener = (ov, oldValue, newValue) -> {
@ -98,12 +102,16 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
navigation.navigateTo(MainView.class, PortfolioView.class, ClosedTradesView.class);
else if (newValue == failedTradesTab)
navigation.navigateTo(MainView.class, PortfolioView.class, FailedTradesView.class);
else if (newValue == editOpenOfferTab) {
else if (newValue == editOpenOfferTab)
navigation.navigateTo(MainView.class, PortfolioView.class, EditOfferView.class);
else if (newValue == duplicateOfferTab) {
navigation.navigateTo(MainView.class, PortfolioView.class, DuplicateOfferView.class);
}
if (oldValue != null && oldValue == editOpenOfferTab)
editOfferView.onTabSelected(false);
if (oldValue != null && oldValue == duplicateOfferTab)
duplicateOfferView.onTabSelected(false);
};
@ -112,6 +120,8 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
List<? extends Tab> removedTabs = change.getRemoved();
if (removedTabs.size() == 1 && removedTabs.get(0).equals(editOpenOfferTab))
onEditOpenOfferRemoved();
if (removedTabs.size() == 1 && removedTabs.get(0).equals(duplicateOfferTab))
onDuplicateOfferRemoved();
};
}
@ -125,6 +135,15 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
navigation.navigateTo(MainView.class, this.getClass(), OpenOffersView.class);
}
private void onDuplicateOfferRemoved() {
if (duplicateOfferView != null) {
duplicateOfferView.onClose();
duplicateOfferView = null;
}
navigation.navigateTo(MainView.class, this.getClass(), OpenOffersView.class);
}
@Override
protected void activate() {
failedTradesManager.getObservableList().addListener((ListChangeListener<Trade>) c -> {
@ -149,6 +168,9 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
else if (root.getSelectionModel().getSelectedItem() == editOpenOfferTab) {
navigation.navigateTo(MainView.class, PortfolioView.class, EditOfferView.class);
if (editOfferView != null) editOfferView.onTabSelected(true);
} else if (root.getSelectionModel().getSelectedItem() == duplicateOfferTab) {
navigation.navigateTo(MainView.class, PortfolioView.class, DuplicateOfferView.class);
if (duplicateOfferView != null) duplicateOfferView.onTabSelected(true);
}
}
@ -160,7 +182,7 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
currentTab = null;
}
private void loadView(Class<? extends View> viewClass) {
private void loadView(Class<? extends View> viewClass, @Nullable Object data) {
// we want to get activate/deactivate called, so we remove the old view on tab change
// TODO Don't understand the check for currentTab != editOpenOfferTab
if (currentTab != null && currentTab != editOpenOfferTab)
@ -195,6 +217,26 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
view = viewLoader.load(OpenOffersView.class);
selectOpenOffersView((OpenOffersView) view);
}
} else if (view instanceof DuplicateOfferView) {
if (duplicateOfferView == null && data instanceof OfferPayload && data != null) {
viewLoader.removeFromCache(viewClass); // remove cached dialog
view = viewLoader.load(viewClass); // and load a fresh one
duplicateOfferView = (DuplicateOfferView) view;
duplicateOfferView.initWithData((OfferPayload) data);
duplicateOfferTab = new Tab(Res.get("portfolio.tab.duplicateOffer").toUpperCase());
duplicateOfferView.setCloseHandler(() -> {
root.getTabs().remove(duplicateOfferTab);
});
root.getTabs().add(duplicateOfferTab);
}
if (duplicateOfferView != null) {
if (currentTab != duplicateOfferTab)
duplicateOfferView.onTabSelected(true);
currentTab = duplicateOfferTab;
} else {
view = viewLoader.load(OpenOffersView.class);
selectOpenOffersView((OpenOffersView) view);
}
}
currentTab.setContent(view.getRoot());

View file

@ -17,6 +17,7 @@
package bisq.desktop.main.portfolio.closedtrades;
import bisq.desktop.Navigation;
import bisq.desktop.common.view.ActivatableViewAndModel;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.AutoTooltipButton;
@ -25,14 +26,19 @@ import bisq.desktop.components.AutoTooltipTableColumn;
import bisq.desktop.components.HyperlinkWithIcon;
import bisq.desktop.components.InputTextField;
import bisq.desktop.components.PeerInfoIcon;
import bisq.desktop.main.MainView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.ClosedTradesSummaryWindow;
import bisq.desktop.main.overlays.windows.OfferDetailsWindow;
import bisq.desktop.main.overlays.windows.TradeDetailsWindow;
import bisq.desktop.main.portfolio.PortfolioView;
import bisq.desktop.main.portfolio.duplicateoffer.DuplicateOfferView;
import bisq.desktop.util.GUIUtil;
import bisq.core.alert.PrivateNotificationManager;
import bisq.core.locale.Res;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OpenOffer;
import bisq.core.trade.Contract;
import bisq.core.trade.Tradable;
@ -42,6 +48,7 @@ import bisq.core.user.Preferences;
import bisq.network.p2p.NodeAddress;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import com.googlecode.jcsv.writer.CSVEntryConverter;
@ -53,9 +60,12 @@ import javafx.fxml.FXML;
import javafx.stage.Stage;
import javafx.scene.Node;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
@ -66,6 +76,7 @@ import javafx.scene.layout.VBox;
import javafx.geometry.Insets;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ChangeListener;
@ -132,6 +143,8 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
Region footerSpacer;
private final OfferDetailsWindow offerDetailsWindow;
private final Navigation navigation;
private final KeyRing keyRing;
private final Preferences preferences;
private final TradeDetailsWindow tradeDetailsWindow;
private final PrivateNotificationManager privateNotificationManager;
@ -143,12 +156,16 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
@Inject
public ClosedTradesView(ClosedTradesViewModel model,
OfferDetailsWindow offerDetailsWindow,
Navigation navigation,
KeyRing keyRing,
Preferences preferences,
TradeDetailsWindow tradeDetailsWindow,
PrivateNotificationManager privateNotificationManager,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
super(model);
this.offerDetailsWindow = offerDetailsWindow;
this.navigation = navigation;
this.keyRing = keyRing;
this.preferences = preferences;
this.tradeDetailsWindow = tradeDetailsWindow;
this.privateNotificationManager = privateNotificationManager;
@ -232,6 +249,31 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
dateColumn.setSortType(TableColumn.SortType.DESCENDING);
tableView.getSortOrder().add(dateColumn);
tableView.setRowFactory(
tableView -> {
final TableRow<ClosedTradableListItem> row = new TableRow<>();
final ContextMenu rowMenu = new ContextMenu();
MenuItem editItem = new MenuItem(Res.get("portfolio.context.offerLikeThis"));
editItem.setOnAction((event) -> {
try {
OfferPayload offerPayload = row.getItem().getTradable().getOffer().getOfferPayload();
if (offerPayload.getPubKeyRing().equals(keyRing.getPubKeyRing())) {
navigation.navigateToWithData(offerPayload, MainView.class, PortfolioView.class, DuplicateOfferView.class);
} else {
new Popup().warning(Res.get("portfolio.context.notYourOffer")).show();
}
} catch (NullPointerException e) {
log.warn("Unable to get offerPayload - {}", e.toString());
}
});
rowMenu.getItems().add(editItem);
row.contextMenuProperty().bind(
Bindings.when(Bindings.isNotNull(row.itemProperty()))
.then(rowMenu)
.otherwise((ContextMenu) null));
return row;
});
filterLabel.setText(Res.get("shared.filter"));
HBox.setMargin(filterLabel, new Insets(5, 0, 0, 10));
filterTextFieldListener = (observable, oldValue, newValue) -> applyFilteredListPredicate(filterTextField.getText());

View file

@ -0,0 +1,92 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.portfolio.duplicateoffer;
import bisq.desktop.Navigation;
import bisq.desktop.main.offer.MutableOfferDataModel;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.offer.CreateOfferService;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferUtil;
import bisq.core.offer.OpenOfferManager;
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter;
import bisq.network.p2p.P2PService;
import com.google.inject.Inject;
import javax.inject.Named;
class DuplicateOfferDataModel extends MutableOfferDataModel {
@Inject
DuplicateOfferDataModel(CreateOfferService createOfferService,
OpenOfferManager openOfferManager,
OfferUtil offerUtil,
BtcWalletService btcWalletService,
BsqWalletService bsqWalletService,
Preferences preferences,
User user,
P2PService p2PService,
PriceFeedService priceFeedService,
AccountAgeWitnessService accountAgeWitnessService,
FeeService feeService,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
TradeStatisticsManager tradeStatisticsManager,
Navigation navigation) {
super(createOfferService,
openOfferManager,
offerUtil,
btcWalletService,
bsqWalletService,
preferences,
user,
p2PService,
priceFeedService,
accountAgeWitnessService,
feeService,
btcFormatter,
tradeStatisticsManager,
navigation);
}
public void populateData(Offer offer) {
if (offer == null)
return;
paymentAccount = user.getPaymentAccount(offer.getMakerPaymentAccountId());
setMinAmount(offer.getMinAmount());
setAmount(offer.getAmount());
setPrice(offer.getPrice());
setVolume(offer.getVolume());
setUseMarketBasedPrice(offer.isUseMarketBasedPrice());
if (offer.isUseMarketBasedPrice()) {
setMarketPriceMargin(offer.getMarketPriceMargin());
}
}
}

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of Bisq.
~
~ Bisq is free software: you can redistribute it and/or modify it
~ under the terms of the GNU Affero General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or (at
~ your option) any later version.
~
~ Bisq is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
~ License for more details.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with Bisq. If not, see <http://www.gnu.org/licenses/>.
-->
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane fx:id="root" fx:controller="bisq.desktop.main.portfolio.duplicateoffer.DuplicateOfferView"
xmlns:fx="http://javafx.com/fxml">
</AnchorPane>

View file

@ -0,0 +1,68 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.portfolio.duplicateoffer;
import bisq.desktop.Navigation;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.main.offer.MutableOfferView;
import bisq.desktop.main.overlays.windows.OfferDetailsWindow;
import bisq.core.locale.CurrencyUtil;
import bisq.core.offer.OfferPayload;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.BsqFormatter;
import bisq.core.util.coin.CoinFormatter;
import com.google.inject.Inject;
import javax.inject.Named;
@FxmlView
public class DuplicateOfferView extends MutableOfferView<DuplicateOfferViewModel> {
@Inject
private DuplicateOfferView(DuplicateOfferViewModel model,
Navigation navigation,
Preferences preferences,
OfferDetailsWindow offerDetailsWindow,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
BsqFormatter bsqFormatter) {
super(model, navigation, preferences, offerDetailsWindow, btcFormatter, bsqFormatter);
}
@Override
protected void initialize() {
super.initialize();
}
@Override
protected void doActivate() {
super.doActivate();
updatePriceToggle();
// To force re-validation of payment account validation
onPaymentAccountsComboBoxSelected();
}
public void initWithData(OfferPayload offerPayload) {
initWithData(offerPayload.getDirection(), CurrencyUtil.getTradeCurrency(offerPayload.getCurrencyCode()).get());
model.initWithData(offerPayload);
}
}

View file

@ -0,0 +1,92 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.portfolio.duplicateoffer;
import bisq.desktop.Navigation;
import bisq.desktop.main.offer.MutableOfferViewModel;
import bisq.desktop.util.validation.AltcoinValidator;
import bisq.desktop.util.validation.BsqValidator;
import bisq.desktop.util.validation.BtcValidator;
import bisq.desktop.util.validation.FiatPriceValidator;
import bisq.desktop.util.validation.FiatVolumeValidator;
import bisq.desktop.util.validation.SecurityDepositValidator;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferUtil;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.BsqFormatter;
import bisq.core.util.coin.CoinFormatter;
import com.google.inject.Inject;
import javax.inject.Named;
import lombok.extern.slf4j.Slf4j;
@Slf4j
class DuplicateOfferViewModel extends MutableOfferViewModel<DuplicateOfferDataModel> {
@Inject
public DuplicateOfferViewModel(DuplicateOfferDataModel dataModel,
FiatVolumeValidator fiatVolumeValidator,
FiatPriceValidator fiatPriceValidator,
AltcoinValidator altcoinValidator,
BtcValidator btcValidator,
BsqValidator bsqValidator,
SecurityDepositValidator securityDepositValidator,
PriceFeedService priceFeedService,
AccountAgeWitnessService accountAgeWitnessService,
Navigation navigation,
Preferences preferences,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
BsqFormatter bsqFormatter,
OfferUtil offerUtil) {
super(dataModel,
fiatVolumeValidator,
fiatPriceValidator,
altcoinValidator,
btcValidator,
bsqValidator,
securityDepositValidator,
priceFeedService,
accountAgeWitnessService,
navigation,
preferences,
btcFormatter,
bsqFormatter,
offerUtil);
syncMinAmountWithAmount = false;
}
public void initWithData(OfferPayload offerPayload) {
this.offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
}
@Override
public void activate() {
super.activate();
dataModel.populateData(offer);
triggerFocusOutOnAmountFields();
onFocusOutPriceAsPercentageTextField(true, false);
}
}

View file

@ -32,10 +32,12 @@ import bisq.desktop.main.funds.withdrawal.WithdrawalView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.OfferDetailsWindow;
import bisq.desktop.main.portfolio.PortfolioView;
import bisq.desktop.main.portfolio.duplicateoffer.DuplicateOfferView;
import bisq.desktop.util.GUIUtil;
import bisq.core.locale.Res;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OpenOffer;
import bisq.core.user.DontShowAgainLookup;
@ -52,9 +54,12 @@ import javafx.stage.Stage;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
@ -66,6 +71,7 @@ import javafx.scene.layout.VBox;
import javafx.geometry.Insets;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ChangeListener;
@ -173,6 +179,27 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
dateColumn.setSortType(TableColumn.SortType.DESCENDING);
tableView.getSortOrder().add(dateColumn);
tableView.setRowFactory(
tableView -> {
final TableRow<OpenOfferListItem> row = new TableRow<>();
final ContextMenu rowMenu = new ContextMenu();
MenuItem editItem = new MenuItem(Res.get("portfolio.context.offerLikeThis"));
editItem.setOnAction((event) -> {
try {
OfferPayload offerPayload = row.getItem().getOffer().getOfferPayload();
navigation.navigateToWithData(offerPayload, MainView.class, PortfolioView.class, DuplicateOfferView.class);
} catch (NullPointerException e) {
log.warn("Unable to get offerPayload - {}", e.toString());
}
});
rowMenu.getItems().add(editItem);
row.contextMenuProperty().bind(
Bindings.when(Bindings.isNotNull(row.itemProperty()))
.then(rowMenu)
.otherwise((ContextMenu) null));
return row;
});
filterLabel.setText(Res.get("shared.filter"));
HBox.setMargin(filterLabel, new Insets(5, 0, 0, 10));
filterTextFieldListener = (observable, oldValue, newValue) -> applyFilteredListPredicate(filterTextField.getText());

View file

@ -17,6 +17,7 @@
package bisq.desktop.main.portfolio.pendingtrades;
import bisq.desktop.Navigation;
import bisq.desktop.common.view.ActivatableViewAndModel;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.AutoTooltipLabel;
@ -25,6 +26,8 @@ import bisq.desktop.components.PeerInfoIcon;
import bisq.desktop.main.MainView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.TradeDetailsWindow;
import bisq.desktop.main.portfolio.PortfolioView;
import bisq.desktop.main.portfolio.duplicateoffer.DuplicateOfferView;
import bisq.desktop.main.shared.ChatView;
import bisq.desktop.util.CssTheme;
import bisq.desktop.util.DisplayUtils;
@ -32,6 +35,7 @@ import bisq.desktop.util.FormBuilder;
import bisq.core.alert.PrivateNotificationManager;
import bisq.core.locale.Res;
import bisq.core.offer.OfferPayload;
import bisq.core.support.dispute.mediation.MediationResultState;
import bisq.core.support.messages.ChatMessage;
import bisq.core.support.traderchat.TradeChatSession;
@ -46,6 +50,7 @@ import bisq.network.p2p.NodeAddress;
import bisq.common.UserThread;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import bisq.common.crypto.PubKeyRing;
import bisq.common.util.Utilities;
@ -67,8 +72,11 @@ import javafx.stage.Window;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCode;
@ -86,6 +94,7 @@ import javafx.geometry.Pos;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ChangeListener;
@ -108,6 +117,8 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
}
private final TradeDetailsWindow tradeDetailsWindow;
private final Navigation navigation;
private final KeyRing keyRing;
private final CoinFormatter formatter;
private final PrivateNotificationManager privateNotificationManager;
private final boolean useDevPrivilegeKeys;
@ -149,6 +160,8 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
@Inject
public PendingTradesView(PendingTradesViewModel model,
TradeDetailsWindow tradeDetailsWindow,
Navigation navigation,
KeyRing keyRing,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
PrivateNotificationManager privateNotificationManager,
Preferences preferences,
@ -156,6 +169,8 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
@Named(Config.USE_DEV_MODE_HEADER) boolean useDevModeHeader) {
super(model);
this.tradeDetailsWindow = tradeDetailsWindow;
this.navigation = navigation;
this.keyRing = keyRing;
this.formatter = formatter;
this.privateNotificationManager = privateNotificationManager;
this.preferences = preferences;
@ -213,6 +228,30 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
dateColumn.setSortType(TableColumn.SortType.DESCENDING);
tableView.getSortOrder().add(dateColumn);
tableView.setRowFactory(
tableView -> {
final TableRow<PendingTradesListItem> row = new TableRow<>();
final ContextMenu rowMenu = new ContextMenu();
MenuItem editItem = new MenuItem(Res.get("portfolio.context.offerLikeThis"));
editItem.setOnAction((event) -> {
try {
OfferPayload offerPayload = row.getItem().getTrade().getOffer().getOfferPayload();
if (offerPayload.getPubKeyRing().equals(keyRing.getPubKeyRing())) {
navigation.navigateToWithData(offerPayload, MainView.class, PortfolioView.class, DuplicateOfferView.class);
} else {
new Popup().warning(Res.get("portfolio.context.notYourOffer")).show();
}
} catch (NullPointerException e) {
log.warn("Unable to get offerPayload - {}", e.toString());
}
});
rowMenu.getItems().add(editItem);
row.contextMenuProperty().bind(
Bindings.when(Bindings.isNotNull(row.itemProperty()))
.then(rowMenu)
.otherwise((ContextMenu) null));
return row;
});
// we use a hidden emergency shortcut to open support ticket
keyEventEventHandler = keyEvent -> {

View file

@ -66,7 +66,7 @@ public class SettingsView extends ActivatableView<TabPane, Void> {
networkTab.setText(Res.get("settings.tab.network").toUpperCase());
aboutTab.setText(Res.get("settings.tab.about").toUpperCase());
navigationListener = viewPath -> {
navigationListener = (viewPath, data) -> {
if (viewPath.size() == 3 && viewPath.indexOf(SettingsView.class) == 1)
loadView(viewPath.tip());
};

View file

@ -133,7 +133,7 @@ public class SupportView extends ActivatableView<TabPane, Void> {
if (tradersArbitrationDisputesTab != null) {
tradersArbitrationDisputesTab.setText(Res.get("support.tab.legacyArbitration.support").toUpperCase());
}
navigationListener = viewPath -> {
navigationListener = (viewPath, data) -> {
if (viewPath.size() == 3 && viewPath.indexOf(SupportView.class) == 1)
loadView(viewPath.tip());
};