diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookController.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookController.java index 5150a8d413..f5fe699c02 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookController.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookController.java @@ -27,6 +27,7 @@ import bisq.chat.bisqeasy.open_trades.BisqEasyOpenTradeChannel; import bisq.common.currency.Market; import bisq.common.observable.Pin; +import bisq.common.observable.collection.CollectionObserver; import bisq.common.observable.collection.ObservableArray; import bisq.common.util.ProtobufUtils; import bisq.desktop.ServiceProvider; @@ -41,6 +42,7 @@ import bisq.settings.CookieKey; import bisq.settings.SettingsService; import javafx.collections.ListChangeListener; +import javafx.collections.SetChangeListener; import javafx.scene.layout.StackPane; import lombok.extern.slf4j.Slf4j; import org.fxmisc.easybind.EasyBind; @@ -54,11 +56,15 @@ @Slf4j public final class BisqEasyOfferbookController extends ChatController { + private static final double MARKET_SELECTION_LIST_CELL_HEIGHT = 53; + private final SettingsService settingsService; private final MarketPriceService marketPriceService; private final BisqEasyOfferbookChannelService bisqEasyOfferbookChannelService; private final BisqEasyOfferbookModel bisqEasyOfferbookModel; - private Pin offerOnlySettingsPin, bisqEasyPrivateTradeChatChannelsPin, selectedChannelPin, marketPriceByCurrencyMapPin; + private final SetChangeListener favouriteMarketsListener; + private Pin offerOnlySettingsPin, bisqEasyPrivateTradeChatChannelsPin, selectedChannelPin, + marketPriceByCurrencyMapPin, favouriteMarketsPin; private Subscription marketSelectorSearchPin, selectedMarketFilterPin, selectedOfferDirectionOrOwnerFilterPin, selectedPeerReputationFilterPin, selectedMarketSortTypePin; @@ -69,6 +75,24 @@ public BisqEasyOfferbookController(ServiceProvider serviceProvider) { settingsService = serviceProvider.getSettingsService(); marketPriceService = serviceProvider.getBondedRolesService().getMarketPriceService(); bisqEasyOfferbookModel = getModel(); + favouriteMarketsListener = change -> { + if (change.wasAdded()) { + Market market = change.getElementAdded(); + model.getMarketChannelItems().forEach(item -> item.getIsFavourite().set(market.equals(item.getMarket()))); + } + + if (change.wasRemoved()) { + Market market = change.getElementRemoved(); + model.getMarketChannelItems().forEach(item -> { + if (market.equals(item.getMarket()) && item.getIsFavourite().get()) { + item.getIsFavourite().set(false); + } + }); + } + + updateFilteredMarketChannelItems(); + updateFavouriteMarketChannelItems(); + }; createMarketChannels(); } @@ -169,8 +193,32 @@ public void onActivate() { } }); + CollectionObserver favouriteMarketsObserver = new CollectionObserver<>() { + @Override + public void add(Market market) { + model.getFavouriteMarkets().add(market); + } + + @Override + public void remove(Object element) { + if (element instanceof Market) { + model.getFavouriteMarkets().remove((Market) element); + } + } + + @Override + public void clear() { + model.getFavouriteMarkets().clear(); + } + }; + favouriteMarketsPin = settingsService.getFavouriteMarkets().addObserver(favouriteMarketsObserver); + + model.getFavouriteMarkets().addListener(favouriteMarketsListener); + model.getSortedMarketChannelItems().setComparator(model.getSelectedMarketSortType().get().getComparator()); + updateFilteredMarketChannelItems(); + updateFavouriteMarketChannelItems(); maybeSelectFirst(); } @@ -187,10 +235,10 @@ public void onDeactivate() { selectedPeerReputationFilterPin.unsubscribe(); marketPriceByCurrencyMapPin.unbind(); selectedMarketSortTypePin.unsubscribe(); + favouriteMarketsPin.unbind(); + model.getFavouriteMarkets().removeListener(favouriteMarketsListener); resetSelectedChildTarget(); - - model.getMarketChannelItems().forEach(MarketChannelItem::cleanUp); } @Override @@ -210,10 +258,7 @@ protected void selectedChannelChanged(ChatChannel chatCha model.getMarketChannelItems().stream() .filter(item -> item.getChannel().equals(channel)) .findAny() - .ifPresent(item -> { - model.getSelectedMarketChannelItem().set(item); - updateSelectedMarketChannelItem(item); - }); + .ifPresent(item -> model.getSelectedMarketChannelItem().set(item)); model.getSearchText().set(""); resetSelectedChildTarget(); @@ -269,7 +314,15 @@ private void updateFilteredMarketChannelItems() { model.getFilteredMarketChannelItems().setPredicate(item -> model.getMarketFilterPredicate().test(item) && model.getMarketSearchTextPredicate().test(item) && - model.getMarketPricePredicate().test(item)); + model.getMarketPricePredicate().test(item) && + !model.getFavouriteMarkets().contains(item.getMarket())); + } + + private void updateFavouriteMarketChannelItems() { + model.getFavouriteMarketChannelItems().setPredicate(item -> model.getFavouriteMarkets().contains(item.getMarket())); + double padding = 15; + double tableViewHeight = (model.getFavouriteMarketChannelItems().size() * MARKET_SELECTION_LIST_CELL_HEIGHT) + padding; + model.getFavouritesTableViewHeight().set(tableViewHeight); } private boolean isMaker(BisqEasyOffer bisqEasyOffer) { @@ -292,7 +345,7 @@ private void maybeSelectFirst() { } } - private void updateSelectedMarketChannelItem(MarketChannelItem selectedItem) { - model.getMarketChannelItems().forEach(item -> item.getSelected().set(item == selectedItem)); + double getMarketSelectionListCellHeight() { + return MARKET_SELECTION_LIST_CELL_HEIGHT; } } diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookModel.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookModel.java index 200d3753ef..a190ecb4e7 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookModel.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookModel.java @@ -18,11 +18,13 @@ package bisq.desktop.main.content.bisq_easy.offerbook; import bisq.chat.ChatChannelDomain; +import bisq.common.currency.Market; import bisq.desktop.main.content.chat.ChatModel; import javafx.beans.Observable; import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.collections.ObservableSet; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import lombok.Getter; @@ -47,6 +49,8 @@ public final class BisqEasyOfferbookModel extends ChatModel { private final ObjectProperty selectedPeerReputationFilter = new SimpleObjectProperty<>(); private final ObjectProperty selectedMarketSortType = new SimpleObjectProperty<>(MarketSortType.NUM_OFFERS); private final StringProperty marketPrice = new SimpleStringProperty(); + private final ObservableSet favouriteMarkets = FXCollections.observableSet(); + private final FilteredList favouriteMarketChannelItems = new FilteredList<>(marketChannelItems); @Setter private Predicate marketPricePredicate = marketChannelItem -> true; @@ -54,6 +58,8 @@ public final class BisqEasyOfferbookModel extends ChatModel { private Predicate marketSearchTextPredicate = marketChannelItem -> true; @Setter private Predicate marketFilterPredicate = marketChannelItem -> true; + @Setter + private DoubleProperty favouritesTableViewHeight = new SimpleDoubleProperty(0); public BisqEasyOfferbookModel(ChatChannelDomain chatChannelDomain) { super(chatChannelDomain); diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookUtil.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookUtil.java index 1e764a493e..a390c76d0c 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookUtil.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookUtil.java @@ -2,31 +2,33 @@ import bisq.common.currency.Market; import bisq.common.currency.MarketRepository; +import bisq.desktop.common.utils.ImageUtil; +import bisq.desktop.components.containers.Spacer; import bisq.desktop.components.controls.BisqTooltip; import bisq.i18n.Res; import javafx.beans.binding.Bindings; import javafx.beans.binding.StringExpression; import javafx.geometry.Pos; import javafx.scene.Cursor; -import javafx.scene.control.Label; -import javafx.scene.control.TableCell; -import javafx.scene.control.TableColumn; -import javafx.scene.control.Tooltip; +import javafx.scene.control.*; +import javafx.scene.image.ImageView; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.util.Callback; +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; import java.util.Comparator; import java.util.List; public class BisqEasyOfferbookUtil { - private static final List majorMarkets = MarketRepository.getMajorMarkets(); + static final List majorMarkets = MarketRepository.getMajorMarkets(); - public static Comparator sortByNumOffers() { + static Comparator sortByNumOffers() { return (lhs, rhs) -> Integer.compare(rhs.getNumOffers().get(), lhs.getNumOffers().get()); } - public static Comparator sortByMajorMarkets() { + static Comparator sortByMajorMarkets() { return (lhs, rhs) -> { int index1 = majorMarkets.indexOf(lhs.getMarket()); int index2 = majorMarkets.indexOf(rhs.getMarket()); @@ -34,43 +36,67 @@ public static Comparator sortByMajorMarkets() { }; } - public static Comparator sortByMarketNameAsc() { - return Comparator.comparing(MarketChannelItem::getMarketString); + static Comparator sortByMarketNameAsc() { + return Comparator.comparing(MarketChannelItem::toString); } - public static Comparator sortByMarketNameDesc() { - return Comparator.comparing(MarketChannelItem::getMarketString).reversed(); + static Comparator sortByMarketNameDesc() { + return Comparator.comparing(MarketChannelItem::toString).reversed(); } - public static Comparator sortByMarketActivity() { + static Comparator sortByMarketActivity() { return (lhs, rhs) -> BisqEasyOfferbookUtil.sortByNumOffers() .thenComparing(BisqEasyOfferbookUtil.sortByMajorMarkets()) .thenComparing(BisqEasyOfferbookUtil.sortByMarketNameAsc()) .compare(lhs, rhs); } - public static Callback, - TableCell> getMarketLabelCellFactory() { + static Callback, + TableCell> getMarketLabelCellFactory(boolean isFavouritesTableView) { return column -> new TableCell<>() { private final Label marketName = new Label(); private final Label marketCode = new Label(); private final Label numOffers = new Label(); + private final Label favouriteLabel = new Label(); + private final ImageView star; private final HBox hBox = new HBox(10, marketCode, numOffers); private final VBox vBox = new VBox(0, marketName, hBox); - private final Tooltip tooltip = new BisqTooltip(); + private final HBox container = new HBox(0, vBox, Spacer.fillHBox(), favouriteLabel); + private final Tooltip marketDetailsTooltip = new BisqTooltip(); + private final Tooltip favouriteTooltip = new BisqTooltip(); + private Subscription selectedPin; { setCursor(Cursor.HAND); marketName.getStyleClass().add("market-name"); hBox.setAlignment(Pos.CENTER_LEFT); vBox.setAlignment(Pos.CENTER_LEFT); - Tooltip.install(vBox, tooltip); + Tooltip.install(vBox, marketDetailsTooltip); + marketDetailsTooltip.setStyle("-fx-text-fill: -fx-dark-text-color;"); + + favouriteTooltip.textProperty().set(isFavouritesTableView + ? Res.get("bisqEasy.offerbook.marketListCell.favourites.tooltip.removeFromFavourites") + : Res.get("bisqEasy.offerbook.marketListCell.favourites.tooltip.addToFavourites")); + favouriteTooltip.setStyle("-fx-text-fill: -fx-dark-text-color;"); + star = ImageUtil.getImageViewById(isFavouritesTableView + ? "favourites-star-yellow" + : "favourites-star-grey-hollow"); + favouriteLabel.setGraphic(star); + favouriteLabel.getStyleClass().add("favourite-label"); + Tooltip.install(favouriteLabel, favouriteTooltip); + + container.setAlignment(Pos.CENTER_LEFT); } @Override protected void updateItem(MarketChannelItem item, boolean empty) { super.updateItem(item, empty); + // Clean up previous row + if (getTableRow() != null && selectedPin != null) { + selectedPin.unsubscribe(); + } + if (item != null && !empty) { marketName.setText(item.getMarket().getQuoteCurrencyName()); marketCode.setText(item.getMarket().getQuoteCurrencyCode()); @@ -79,13 +105,22 @@ protected void updateItem(MarketChannelItem item, boolean empty) { numOffers.textProperty().bind(formattedNumOffers); StringExpression formattedTooltip = Bindings.createStringBinding(() -> BisqEasyOfferbookUtil.getFormattedTooltip(item.getNumOffers().get(), item.getMarket().getQuoteCurrencyName()), item.getNumOffers()); - tooltip.textProperty().bind(formattedTooltip); - tooltip.setStyle("-fx-text-fill: -fx-dark-text-color;"); + marketDetailsTooltip.textProperty().bind(formattedTooltip); + + // Set up new row + TableRow newRow = getTableRow(); + if (newRow != null) { + selectedPin = EasyBind.subscribe(newRow.selectedProperty(), item::updateMarketLogoEffect); + } - setGraphic(vBox); + favouriteLabel.setOnMouseClicked(e -> item.toggleFavourite()); + + setGraphic(container); } else { numOffers.textProperty().unbind(); - tooltip.textProperty().unbind(); + marketDetailsTooltip.textProperty().unbind(); + + favouriteLabel.setOnMouseClicked(null); setGraphic(null); } @@ -93,7 +128,7 @@ protected void updateItem(MarketChannelItem item, boolean empty) { }; } - public static Callback, + static Callback, TableCell> getMarketLogoCellFactory() { return column -> new TableCell<>() { { diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookView.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookView.java index 2d9aed301b..aa6e727a86 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookView.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/BisqEasyOfferbookView.java @@ -31,6 +31,8 @@ import bisq.desktop.main.content.chat.ChatView; import bisq.desktop.main.content.components.chatMessages.ChatMessageListItem; import bisq.i18n.Res; +import javafx.beans.binding.Bindings; +import javafx.collections.ListChangeListener; import javafx.css.PseudoClass; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -41,10 +43,7 @@ import javafx.scene.control.Label; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.image.ImageView; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.VBox; +import javafx.scene.layout.*; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.fxmisc.easybind.EasyBind; @@ -52,11 +51,13 @@ @Slf4j public final class BisqEasyOfferbookView extends ChatView { + private final ListChangeListener listChangeListener; private SearchBox marketSelectorSearchBox; - private BisqTableView tableView; + private BisqTableView marketsTableView, favouritesTableView; private VBox marketSelectionList; - private Subscription tableViewSelectionPin, selectedModelItemPin, channelHeaderIconPin, selectedMarketFilterPin, - selectedOfferDirectionOrOwnerFilterPin, selectedPeerReputationFilterPin, selectedMarketSortTypePin; + private Subscription marketsTableViewSelectionPin, selectedModelItemPin, channelHeaderIconPin, selectedMarketFilterPin, + selectedOfferDirectionOrOwnerFilterPin, selectedPeerReputationFilterPin, selectedMarketSortTypePin, + marketSelectorSearchPin, favouritesTableViewHeightPin, favouritesTableViewSelectionPin; private Button createOfferButton; private DropdownMenu sortAndFilterMarketsMenu, filterOffersByDirectionOrOwnerMenu, filterOffersByPeerReputationMenu; private DropdownSortByMenuItem sortByMostOffers, sortByNameAZ, sortByNameZA; @@ -75,6 +76,8 @@ public BisqEasyOfferbookView(BisqEasyOfferbookModel model, VBox chatMessagesComponent, Pane channelSidebar) { super(model, controller, chatMessagesComponent, channelSidebar); + + listChangeListener = change -> updateTableViewSelection(getModel().getSelectedMarketChannelItem().get()); } @Override @@ -120,15 +123,26 @@ protected void onViewAttached() { marketPrice.textProperty().bind(getModel().getMarketPrice()); withOffersDisplayHint.visibleProperty().bind(getModel().getSelectedMarketsFilter().isEqualTo(Filters.Markets.WITH_OFFERS)); withOffersDisplayHint.managedProperty().bind(getModel().getSelectedMarketsFilter().isEqualTo(Filters.Markets.WITH_OFFERS)); + favouritesTableView.visibleProperty().bind(Bindings.isNotEmpty(getModel().getFavouriteMarketChannelItems())); + favouritesTableView.managedProperty().bind(Bindings.isNotEmpty(getModel().getFavouriteMarketChannelItems())); + + selectedModelItemPin = EasyBind.subscribe(getModel().getSelectedMarketChannelItem(), this::updateTableViewSelection); - selectedModelItemPin = EasyBind.subscribe(getModel().getSelectedMarketChannelItem(), selected -> { - tableView.getSelectionModel().select(selected); + marketsTableViewSelectionPin = EasyBind.subscribe(marketsTableView.getSelectionModel().selectedItemProperty(), item -> { + if (item != null) { + getController().onSelectMarketChannelItem(item); + } + }); + marketSelectorSearchPin = EasyBind.subscribe(getModel().getMarketSelectorSearchText(), searchText -> { + marketsTableView.getSelectionModel().select(getModel().getSelectedMarketChannelItem().get()); }); - tableViewSelectionPin = EasyBind.subscribe(tableView.getSelectionModel().selectedItemProperty(), item -> { + favouritesTableViewSelectionPin = EasyBind.subscribe(favouritesTableView.getSelectionModel().selectedItemProperty(), item -> { if (item != null) { getController().onSelectMarketChannelItem(item); } }); + getModel().getFavouriteMarketChannelItems().addListener(listChangeListener); + channelHeaderIconPin = EasyBind.subscribe(model.getChannelIconNode(), this::updateChannelHeaderIcon); selectedMarketFilterPin = EasyBind.subscribe(getModel().getSelectedMarketsFilter(), this::updateSelectedMarketFilter); selectedOfferDirectionOrOwnerFilterPin = EasyBind.subscribe(getModel().getSelectedOfferDirectionOrOwnerFilter(), filter -> @@ -137,6 +151,9 @@ protected void onViewAttached() { updateSelectedFilterInDropdownMenu(filter, filterOffersByPeerReputationMenu)); selectedMarketSortTypePin = EasyBind.subscribe(getModel().getSelectedMarketSortType(), this::updateMarketSortType); + favouritesTableViewHeightPin = EasyBind.subscribe(getModel().getFavouritesTableViewHeight(), + height -> updateFavouritesTableViewHeight(height.doubleValue())); + sortByMostOffers.setOnAction(e -> getController().onSortMarkets(MarketSortType.NUM_OFFERS)); sortByNameAZ.setOnAction(e -> getController().onSortMarkets(MarketSortType.ASC)); sortByNameZA.setOnAction(e -> getController().onSortMarkets(MarketSortType.DESC)); @@ -163,6 +180,17 @@ protected void onViewAttached() { withOffersDisplayHint.setOnMouseExited(e -> removeWithOffersFilter.setGraphic(defaultCloseIcon)); } + private void updateTableViewSelection(MarketChannelItem selectedItem) { + marketsTableView.getSelectionModel().clearSelection(); + marketsTableView.getSelectionModel().select(selectedItem); + favouritesTableView.getSelectionModel().clearSelection(); + favouritesTableView.getSelectionModel().select(selectedItem); + } + + private void updateFavouritesTableViewHeight(double height) { + favouritesTableView.setPrefHeight(height); + } + private void setOfferDirectionOrOwnerFilter(DropdownFilterMenuItem filterMenuItem) { getModel().getSelectedOfferDirectionOrOwnerFilter().set((Filters.OfferDirectionOrOwner) filterMenuItem.getFilter()); } @@ -180,14 +208,19 @@ protected void onViewDetached() { marketPrice.textProperty().unbind(); withOffersDisplayHint.visibleProperty().unbind(); withOffersDisplayHint.managedProperty().unbind(); + favouritesTableView.visibleProperty().unbind(); + favouritesTableView.managedProperty().unbind(); selectedModelItemPin.unsubscribe(); - tableViewSelectionPin.unsubscribe(); + marketsTableViewSelectionPin.unsubscribe(); + marketSelectorSearchPin.unsubscribe(); + favouritesTableViewSelectionPin.unsubscribe(); channelHeaderIconPin.unsubscribe(); selectedMarketFilterPin.unsubscribe(); selectedOfferDirectionOrOwnerFilterPin.unsubscribe(); selectedPeerReputationFilterPin.unsubscribe(); selectedMarketSortTypePin.unsubscribe(); + favouritesTableViewHeightPin.unsubscribe(); sortByMostOffers.setOnAction(null); sortByNameAZ.setOnAction(null); @@ -209,6 +242,8 @@ protected void onViewDetached() { removeWithOffersFilter.setOnMouseClicked(null); withOffersDisplayHint.setOnMouseEntered(null); withOffersDisplayHint.setOnMouseExited(null); + + getModel().getFavouriteMarketChannelItems().removeListener(listChangeListener); } private BisqEasyOfferbookModel getModel() { @@ -241,15 +276,23 @@ private void addMarketSelectionList() { appliedFiltersSection.getStyleClass().add("market-selection-applied-filters"); HBox.setHgrow(appliedFiltersSection, Priority.ALWAYS); - tableView = new BisqTableView<>(getModel().getSortedMarketChannelItems()); - tableView.getStyleClass().add("market-selection-list"); - tableView.allowVerticalScrollbar(); - tableView.hideHorizontalScrollbar(); - tableView.setFixedCellSize(53); - configTableView(); - VBox.setVgrow(tableView, Priority.ALWAYS); - - marketSelectionList = new VBox(header, Layout.hLine(), subheader, appliedFiltersSection, tableView); + favouritesTableView = new BisqTableView<>(getModel().getFavouriteMarketChannelItems()); + favouritesTableView.getStyleClass().addAll("market-selection-list", "favourites-list"); + favouritesTableView.hideVerticalScrollbar(); + favouritesTableView.hideHorizontalScrollbar(); + favouritesTableView.setFixedCellSize(getController().getMarketSelectionListCellHeight()); + configTableView(favouritesTableView); + + marketsTableView = new BisqTableView<>(getModel().getSortedMarketChannelItems()); + marketsTableView.getStyleClass().addAll("market-selection-list", "markets-list"); + marketsTableView.allowVerticalScrollbar(); + marketsTableView.hideHorizontalScrollbar(); + marketsTableView.setFixedCellSize(getController().getMarketSelectionListCellHeight()); + configTableView(marketsTableView); + VBox.setVgrow(marketsTableView, Priority.ALWAYS); + + marketSelectionList = new VBox(header, Layout.hLine(), subheader, appliedFiltersSection, favouritesTableView, + marketsTableView); marketSelectionList.setPrefWidth(210); marketSelectionList.setMinWidth(210); marketSelectionList.setFillWidth(true); @@ -313,7 +356,7 @@ private Button createAndGetCreateOfferButton() { return createOfferButton; } - private void configTableView() { + private void configTableView(BisqTableView tableView) { BisqTableColumn marketLogoTableColumn = new BisqTableColumn.Builder() .fixWidth(55) .setCellFactory(BisqEasyOfferbookUtil.getMarketLogoCellFactory()) @@ -323,7 +366,7 @@ private void configTableView() { BisqTableColumn marketLabelTableColumn = new BisqTableColumn.Builder() .minWidth(100) .left() - .setCellFactory(BisqEasyOfferbookUtil.getMarketLabelCellFactory()) + .setCellFactory(BisqEasyOfferbookUtil.getMarketLabelCellFactory(tableView.equals(favouritesTableView))) .build(); tableView.getColumns().add(tableView.getSelectionMarkerColumn()); diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/Filters.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/Filters.java index 6ffc176d45..b64138e82b 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/Filters.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/Filters.java @@ -36,6 +36,7 @@ interface FilterPredicate { @Getter enum Markets implements FilterPredicate { ALL(item -> true), + FAVOURITES(item -> item.getIsFavourite().get()), WITH_OFFERS(item -> item.getNumOffers().get() > 0); private final Predicate predicate; diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/MarketChannelItem.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/MarketChannelItem.java index 0632a63100..42685c40f7 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/MarketChannelItem.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/MarketChannelItem.java @@ -22,11 +22,11 @@ import bisq.common.currency.Market; import bisq.desktop.common.threading.UIThread; import bisq.desktop.main.content.components.MarketImageComposition; +import bisq.settings.FavouriteMarketsService; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; -import javafx.beans.value.ChangeListener; import javafx.scene.CacheHint; import javafx.scene.Node; import javafx.scene.effect.ColorAdjust; @@ -37,17 +37,17 @@ @EqualsAndHashCode @Getter -public class MarketChannelItem { +class MarketChannelItem { + private static final ColorAdjust DEFAULT_COLOR_ADJUST = new ColorAdjust(); + private static final ColorAdjust SELECTED_COLOR_ADJUST = new ColorAdjust(); + private final BisqEasyOfferbookChannel channel; private final Market market; private final Node marketLogo; private final IntegerProperty numOffers = new SimpleIntegerProperty(0); - private final BooleanProperty selected = new SimpleBooleanProperty(false); - private final ChangeListener selectedChangeListener; - private final ColorAdjust defaultColorAdjust = new ColorAdjust(); - private final ColorAdjust selectedColorAdjust = new ColorAdjust(); + private final BooleanProperty isFavourite = new SimpleBooleanProperty(false); - public MarketChannelItem(BisqEasyOfferbookChannel channel) { + MarketChannelItem(BisqEasyOfferbookChannel channel) { this.channel = channel; market = channel.getMarket(); marketLogo = MarketImageComposition.createMarketLogo(market.getQuoteCurrencyCode()); @@ -55,21 +55,18 @@ public MarketChannelItem(BisqEasyOfferbookChannel channel) { marketLogo.setCacheHint(CacheHint.SPEED); setUpColorAdjustments(); - selectedChangeListener = (observable, oldValue, newValue) -> - marketLogo.setEffect(newValue ? selectedColorAdjust : defaultColorAdjust); - selected.addListener(selectedChangeListener); - marketLogo.setEffect(defaultColorAdjust); + marketLogo.setEffect(DEFAULT_COLOR_ADJUST); channel.getChatMessages().addObserver(new WeakReference(this::updateNumOffers).get()); updateNumOffers(); } private void setUpColorAdjustments() { - defaultColorAdjust.setBrightness(-0.4); - defaultColorAdjust.setSaturation(-0.2); - defaultColorAdjust.setContrast(-0.1); + DEFAULT_COLOR_ADJUST.setBrightness(-0.4); + DEFAULT_COLOR_ADJUST.setSaturation(-0.2); + DEFAULT_COLOR_ADJUST.setContrast(-0.1); - selectedColorAdjust.setBrightness(-0.1); + SELECTED_COLOR_ADJUST.setBrightness(-0.1); } private void updateNumOffers() { @@ -77,12 +74,12 @@ private void updateNumOffers() { int numOffers = (int) channel.getChatMessages().stream() .filter(BisqEasyOfferbookMessage::hasBisqEasyOffer) .count(); - this.getNumOffers().set(numOffers); + getNumOffers().set(numOffers); }); } - public String getMarketString() { - return market.toString(); + void updateMarketLogoEffect(boolean isSelectedMarket) { + getMarketLogo().setEffect(isSelectedMarket ? SELECTED_COLOR_ADJUST : DEFAULT_COLOR_ADJUST); } @Override @@ -90,7 +87,23 @@ public String toString() { return market.toString(); } - public void cleanUp() { - selected.removeListener(selectedChangeListener); + void toggleFavourite() { + if (isFavourite()) { + removeFromFavourites(); + } else { + addAsFavourite(); + } + } + + private boolean isFavourite() { + return FavouriteMarketsService.isFavourite(getMarket()); + } + + private void addAsFavourite() { + FavouriteMarketsService.addFavourite(getMarket()); + } + + private void removeFromFavourites() { + FavouriteMarketsService.removeFavourite(getMarket()); } } diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/components/ReputationScoreDisplay.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/components/ReputationScoreDisplay.java index 8eed7975da..14c72d8985 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/components/ReputationScoreDisplay.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/components/ReputationScoreDisplay.java @@ -52,6 +52,7 @@ public ReputationScoreDisplay(ReputationScore reputationScore) { public ReputationScoreDisplay() { super(SPACING); + setAlignment(Pos.CENTER_LEFT); tooltip.setStyle("-fx-text-fill: black; -fx-background-color: -bisq-light-grey-10;"); diff --git a/apps/desktop/desktop/src/main/resources/css/bisq_easy.css b/apps/desktop/desktop/src/main/resources/css/bisq_easy.css index 6c3d8b88fa..9f43302c2b 100644 --- a/apps/desktop/desktop/src/main/resources/css/bisq_easy.css +++ b/apps/desktop/desktop/src/main/resources/css/bisq_easy.css @@ -261,8 +261,6 @@ } .market-selection-list .table-cell { - /*-fx-border-width: 1;*/ - /*-fx-border-color: blue;*/ -fx-padding: 0; } @@ -276,6 +274,28 @@ -fx-text-fill: -fx-light-text-color !important; } +.market-selection-list .favourite-label { + /*-fx-background-color: blue;*/ + -fx-alignment: center; + -fx-min-width: 30; + /*-fx-min-height: 50;*/ + /*-fx-padding: 0 3 0 0;*/ +} + +.favourites-list { + -fx-padding: 0 0 10 0; + -fx-border-width: 0 0 1 0; + -fx-border-color: -bisq-dark-grey-50; +} + +.markets-list .favourite-label { + -fx-opacity: 0; +} + +.markets-list .table-row-cell:hover .favourite-label { + -fx-opacity: 1; +} + .create-offer-button { -fx-background-color: -fx-default-button; -fx-text-fill: -fx-light-text-color; diff --git a/apps/desktop/desktop/src/main/resources/css/images.css b/apps/desktop/desktop/src/main/resources/css/images.css index 983217bb79..ff181a7bc0 100644 --- a/apps/desktop/desktop/src/main/resources/css/images.css +++ b/apps/desktop/desktop/src/main/resources/css/images.css @@ -660,6 +660,38 @@ -fx-image: url("/images/icons/star-grey.png"); } +#favourites-star-green { + -fx-image: url("/images/icons/favourites/star-green.png"); +} + +#favourites-star-green-hollow { + -fx-image: url("/images/icons/favourites/star-green-hollow.png"); +} + +#favourites-star-white { + -fx-image: url("/images/icons/favourites/star-white.png"); +} + +#favourites-star-white-hollow { + -fx-image: url("/images/icons/favourites/star-white-hollow.png"); +} + +#favourites-star-grey { + -fx-image: url("/images/icons/favourites/star-grey.png"); +} + +#favourites-star-grey-hollow { + -fx-image: url("/images/icons/favourites/star-grey-hollow.png"); +} + +#favourites-star-yellow { + -fx-image: url("/images/icons/favourites/star-yellow.png"); +} + +#favourites-star-yellow-hollow { + -fx-image: url("/images/icons/favourites/star-yellow-hollow.png"); +} + #scroll-down-white { -fx-image: url("/images/icons/scroll-down-white.png"); } diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-green-hollow.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-green-hollow.png new file mode 100644 index 0000000000..a88b6f1659 Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-green-hollow.png differ diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-green-hollow@2x.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-green-hollow@2x.png new file mode 100644 index 0000000000..06bc33bc55 Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-green-hollow@2x.png differ diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-green.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-green.png new file mode 100644 index 0000000000..aaa630a437 Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-green.png differ diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-green@2x.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-green@2x.png new file mode 100644 index 0000000000..b3fbf60bee Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-green@2x.png differ diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-grey-hollow.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-grey-hollow.png new file mode 100644 index 0000000000..16d1db7662 Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-grey-hollow.png differ diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-grey-hollow@2x.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-grey-hollow@2x.png new file mode 100644 index 0000000000..c471eac022 Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-grey-hollow@2x.png differ diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-grey.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-grey.png new file mode 100644 index 0000000000..06d2169e66 Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-grey.png differ diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-grey@2x.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-grey@2x.png new file mode 100644 index 0000000000..1afd327262 Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-grey@2x.png differ diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-white-hollow.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-white-hollow.png new file mode 100644 index 0000000000..b667421b94 Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-white-hollow.png differ diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-white-hollow@2x.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-white-hollow@2x.png new file mode 100644 index 0000000000..537ebc217b Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-white-hollow@2x.png differ diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-white.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-white.png new file mode 100644 index 0000000000..e9612559ca Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-white.png differ diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-white@2x.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-white@2x.png new file mode 100644 index 0000000000..eefaf4ecd8 Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-white@2x.png differ diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-yellow-hollow.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-yellow-hollow.png new file mode 100644 index 0000000000..c1f2051415 Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-yellow-hollow.png differ diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-yellow-hollow@2x.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-yellow-hollow@2x.png new file mode 100644 index 0000000000..c51b7802b7 Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-yellow-hollow@2x.png differ diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-yellow.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-yellow.png new file mode 100644 index 0000000000..19dc4ec77d Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-yellow.png differ diff --git a/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-yellow@2x.png b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-yellow@2x.png new file mode 100644 index 0000000000..0b30c15f23 Binary files /dev/null and b/apps/desktop/desktop/src/main/resources/images/icons/favourites/star-yellow@2x.png differ diff --git a/i18n/src/main/resources/bisq_easy.properties b/i18n/src/main/resources/bisq_easy.properties index 3a1e94a10b..21c8a2ffc3 100644 --- a/i18n/src/main/resources/bisq_easy.properties +++ b/i18n/src/main/resources/bisq_easy.properties @@ -381,6 +381,8 @@ bisqEasy.offerbook.marketListCell.numOffers.many={0} offers bisqEasy.offerbook.marketListCell.numOffers.tooltip.none=No offers yet available in the {0} market bisqEasy.offerbook.marketListCell.numOffers.tooltip.one={0} offer is available in the {1} market bisqEasy.offerbook.marketListCell.numOffers.tooltip.many={0} offers are available in the {1} market +bisqEasy.offerbook.marketListCell.favourites.tooltip.addToFavourites=Add to favourites +bisqEasy.offerbook.marketListCell.favourites.tooltip.removeFromFavourites=Remove from favourites bisqEasy.offerbook.dropdownMenu.sortAndFilterMarkets.tooltip=Sort and filter markets bisqEasy.offerbook.dropdownMenu.sortAndFilterMarkets.sortTitle=Sort by: diff --git a/settings/src/main/java/bisq/settings/FavouriteMarketsService.java b/settings/src/main/java/bisq/settings/FavouriteMarketsService.java new file mode 100644 index 0000000000..e21c8144d8 --- /dev/null +++ b/settings/src/main/java/bisq/settings/FavouriteMarketsService.java @@ -0,0 +1,65 @@ +/* + * 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 . + */ + +package bisq.settings; + +import bisq.common.currency.Market; +import bisq.common.observable.collection.ObservableSet; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class FavouriteMarketsService { + private static final int MAX_ALLOWED_FAVOURITES = 5; + + public static boolean isFavourite(Market market) { + return SettingsService.getInstance().getFavouriteMarkets().contains(market); + } + + public static void addFavourite(Market market) { + ObservableSet favouriteMarkets = SettingsService.getInstance().getFavouriteMarkets(); + log.info("Current number of favourite markets: {}", favouriteMarkets.size()); + + if (favouriteMarkets.size() == MAX_ALLOWED_FAVOURITES) { + log.info("Cannot add more favourites. Max number of favourites ({}) reached.", MAX_ALLOWED_FAVOURITES); + return; + } + + if (!favouriteMarkets.contains(market)) { + favouriteMarkets.add(market); + persist(); + log.info("Market added to favourites. Total favourites now: {}", favouriteMarkets.size()); + } else { + log.info("Market is already in favourites."); + } + } + + public static void removeFavourite(Market market) { + ObservableSet favouriteMarkets = SettingsService.getInstance().getFavouriteMarkets(); + + if (favouriteMarkets.contains(market)) { + favouriteMarkets.remove(market); + persist(); + log.info("Market removed from favourites. Total favourites now: {}", favouriteMarkets.size()); + } else { + log.info("Attempted to remove a market that is not in favourites."); + } + } + + private static void persist() { + SettingsService.getInstance().persist(); + } +} diff --git a/settings/src/main/java/bisq/settings/SettingsService.java b/settings/src/main/java/bisq/settings/SettingsService.java index 0599525424..84292cfb6e 100644 --- a/settings/src/main/java/bisq/settings/SettingsService.java +++ b/settings/src/main/java/bisq/settings/SettingsService.java @@ -77,6 +77,7 @@ public CompletableFuture initialize() { getLanguageCode().addObserver(value -> persist()); getDifficultyAdjustmentFactor().addObserver(value -> persist()); getIgnoreDiffAdjustmentFromSecManager().addObserver(value -> persist()); + getFavouriteMarkets().addObserver(this::persist); isInitialized = true; if (DevMode.isDevMode() && @@ -175,6 +176,10 @@ public Observable getLanguageCode() { return persistableStore.languageCode; } + public ObservableSet getFavouriteMarkets() { + return persistableStore.favouriteMarkets; + } + /////////////////////////////////////////////////////////////////////////////////////////////////// // DontShowAgainMap @@ -240,4 +245,4 @@ public void setCookie(CookieKey key, String subKey, String value) { private void updateCookieChangedFlag() { cookieChanged.set(!cookieChanged.get()); } -} \ No newline at end of file +} diff --git a/settings/src/main/java/bisq/settings/SettingsStore.java b/settings/src/main/java/bisq/settings/SettingsStore.java index 4053748420..eb1d351320 100644 --- a/settings/src/main/java/bisq/settings/SettingsStore.java +++ b/settings/src/main/java/bisq/settings/SettingsStore.java @@ -51,6 +51,7 @@ public final class SettingsStore implements PersistableStore { final ObservableSet supportedLanguageCodes = new ObservableSet<>(); final Observable difficultyAdjustmentFactor = new Observable<>(); final Observable ignoreDiffAdjustmentFromSecManager = new Observable<>(); + final ObservableSet favouriteMarkets = new ObservableSet<>(); public SettingsStore() { this(new Cookie(), @@ -68,7 +69,8 @@ public SettingsStore() { true, Set.of(LanguageRepository.getDefaultLanguage()), NetworkLoad.DEFAULT_DIFFICULTY_ADJUSTMENT, - false); + false, + new HashSet<>()); } public SettingsStore(Cookie cookie, @@ -86,7 +88,8 @@ public SettingsStore(Cookie cookie, boolean preventStandbyMode, Set supportedLanguageCodes, double difficultyAdjustmentFactor, - boolean ignoreDiffAdjustmentFromSecManager) { + boolean ignoreDiffAdjustmentFromSecManager, + Set favouriteMarkets) { this.cookie = cookie; this.dontShowAgainMap.putAll(dontShowAgainMap); this.useAnimations.set(useAnimations); @@ -103,6 +106,7 @@ public SettingsStore(Cookie cookie, this.supportedLanguageCodes.setAll(supportedLanguageCodes); this.difficultyAdjustmentFactor.set(difficultyAdjustmentFactor); this.ignoreDiffAdjustmentFromSecManager.set(ignoreDiffAdjustmentFromSecManager); + this.favouriteMarkets.setAll(favouriteMarkets); } @Override @@ -124,6 +128,7 @@ public bisq.settings.protobuf.SettingsStore toProto() { .addAllSupportedLanguageCodes(new ArrayList<>(supportedLanguageCodes)) .setDifficultyAdjustmentFactor(difficultyAdjustmentFactor.get()) .setIgnoreDiffAdjustmentFromSecManager(ignoreDiffAdjustmentFromSecManager.get()) + .addAllFavouriteMarkets(favouriteMarkets.stream().map(Market::toProto).collect(Collectors.toList())) .build(); } @@ -144,7 +149,9 @@ public static SettingsStore fromProto(bisq.settings.protobuf.SettingsStore proto proto.getPreventStandbyMode(), new HashSet<>(proto.getSupportedLanguageCodesList()), proto.getDifficultyAdjustmentFactor(), - proto.getIgnoreDiffAdjustmentFromSecManager()); + proto.getIgnoreDiffAdjustmentFromSecManager(), + new HashSet<>(proto.getFavouriteMarketsList().stream() + .map(Market::fromProto).collect(Collectors.toSet()))); } @Override @@ -175,7 +182,8 @@ public SettingsStore getClone() { preventStandbyMode.get(), new HashSet<>(supportedLanguageCodes), difficultyAdjustmentFactor.get(), - ignoreDiffAdjustmentFromSecManager.get()); + ignoreDiffAdjustmentFromSecManager.get(), + favouriteMarkets); } @Override @@ -197,8 +205,9 @@ public void applyPersisted(SettingsStore persisted) { supportedLanguageCodes.setAll(persisted.supportedLanguageCodes); difficultyAdjustmentFactor.set(persisted.difficultyAdjustmentFactor.get()); ignoreDiffAdjustmentFromSecManager.set(persisted.ignoreDiffAdjustmentFromSecManager.get()); + favouriteMarkets.setAll(persisted.favouriteMarkets); } catch (Exception e) { log.error("Exception at applyPersisted", e); } } -} \ No newline at end of file +} diff --git a/settings/src/main/proto/settings.proto b/settings/src/main/proto/settings.proto index c0885d18e7..9de7a18422 100644 --- a/settings/src/main/proto/settings.proto +++ b/settings/src/main/proto/settings.proto @@ -54,4 +54,5 @@ message SettingsStore { repeated string supportedLanguageCodes = 14; double difficultyAdjustmentFactor = 15; bool ignoreDiffAdjustmentFromSecManager = 16; + repeated common.Market favouriteMarkets = 17; }