Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add offer list in Bisq Easy offerbook #2128

Merged
merged 12 commits into from
Apr 28, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
public class DropdownMenu extends HBox {
public static final Double INITIAL_WIDTH = 24.0;
@Getter
private final Label label = new Label();
private Label label = new Label();
private final ImageView defaultIcon, activeIcon;
private final ContextMenu contextMenu = new ContextMenu();
private ImageView buttonIcon;
Expand Down Expand Up @@ -77,6 +77,11 @@ public void setLabel(String text) {
label.setText(text);
}

public void setLabel(Label label) {
this.label = label;
getChildren().set(0, label);
}

private void toggleContextMenu() {
if (!contextMenu.isShowing()) {
contextMenu.setAnchorLocation(PopupWindow.AnchorLocation.WINDOW_TOP_RIGHT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import bisq.chat.ChatMessage;
import bisq.chat.bisqeasy.offerbook.BisqEasyOfferbookChannel;
import bisq.chat.bisqeasy.offerbook.BisqEasyOfferbookChannelService;
import bisq.chat.bisqeasy.offerbook.BisqEasyOfferbookMessage;
import bisq.chat.bisqeasy.open_trades.BisqEasyOpenTradeChannel;
import bisq.common.currency.Market;
import bisq.common.observable.Pin;
Expand All @@ -37,10 +38,13 @@
import bisq.desktop.main.content.bisq_easy.trade_wizard.TradeWizardController;
import bisq.desktop.main.content.chat.ChatController;
import bisq.desktop.main.content.components.MarketImageComposition;
import bisq.i18n.Res;
import bisq.offer.bisq_easy.BisqEasyOffer;
import bisq.presentation.formatters.PriceFormatter;
import bisq.settings.CookieKey;
import bisq.settings.SettingsService;
import bisq.user.profile.UserProfile;
import bisq.user.reputation.ReputationService;
import javafx.collections.ListChangeListener;
import javafx.collections.SetChangeListener;
import javafx.scene.layout.StackPane;
Expand All @@ -50,6 +54,7 @@

import java.lang.ref.WeakReference;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static com.google.common.base.Preconditions.checkArgument;
Expand All @@ -63,10 +68,11 @@ public final class BisqEasyOfferbookController extends ChatController<BisqEasyOf
private final BisqEasyOfferbookChannelService bisqEasyOfferbookChannelService;
private final BisqEasyOfferbookModel bisqEasyOfferbookModel;
private final SetChangeListener<Market> favouriteMarketsListener;
private final ReputationService reputationService = serviceProvider.getUserService().getReputationService();
private Pin offerOnlySettingsPin, bisqEasyPrivateTradeChatChannelsPin, selectedChannelPin,
marketPriceByCurrencyMapPin, favouriteMarketsPin;
marketPriceByCurrencyMapPin, favouriteMarketsPin, offerMessagesPin;
private Subscription marketSelectorSearchPin, selectedMarketFilterPin, selectedOfferDirectionOrOwnerFilterPin,
selectedPeerReputationFilterPin, selectedMarketSortTypePin;
selectedPeerReputationFilterPin, selectedMarketSortTypePin, showBuyFromOfferMessageItemsPin;

public BisqEasyOfferbookController(ServiceProvider serviceProvider) {
super(serviceProvider, ChatChannelDomain.BISQ_EASY_OFFERBOOK, NavigationTarget.BISQ_EASY_OFFERBOOK);
Expand Down Expand Up @@ -184,6 +190,12 @@ public void onActivate() {
}
});

showBuyFromOfferMessageItemsPin = EasyBind.subscribe(model.getShowBuyFromOfferMessageItems(), showBuyFromOfferMessageItems -> {
model.getFilteredOfferMessageItems().setPredicate(item ->
showBuyFromOfferMessageItems ? item.isSellOffer() : !item.isSellOffer()
);
});

MarketSortType persistedMarketSortType = settingsService.getCookie().asString(CookieKey.MARKET_SORT_TYPE).map(name ->
ProtobufUtils.enumFromProto(MarketSortType.class, name, MarketSortType.NUM_OFFERS))
.orElse(MarketSortType.NUM_OFFERS);
Expand Down Expand Up @@ -227,6 +239,10 @@ public void clear() {
public void onDeactivate() {
super.onDeactivate();

if (offerMessagesPin != null) {
offerMessagesPin.unbind();
}

offerOnlySettingsPin.unbind();
bisqEasyPrivateTradeChatChannelsPin.unbind();
selectedChannelPin.unbind();
Expand All @@ -237,6 +253,7 @@ public void onDeactivate() {
marketPriceByCurrencyMapPin.unbind();
selectedMarketSortTypePin.unsubscribe();
favouriteMarketsPin.unbind();
showBuyFromOfferMessageItemsPin.unsubscribe();
model.getFavouriteMarkets().removeListener(favouriteMarketsListener);

resetSelectedChildTarget();
Expand All @@ -255,7 +272,6 @@ protected void selectedChannelChanged(ChatChannel<? extends ChatMessage> chatCha
if (chatChannel instanceof BisqEasyOfferbookChannel) {
BisqEasyOfferbookChannel channel = (BisqEasyOfferbookChannel) chatChannel;

// FIXME (low prio): marketChannelItems needs to be a hashmap
model.getMarketChannelItems().stream()
.filter(item -> item.getChannel().equals(channel))
.findAny()
Expand All @@ -277,18 +293,14 @@ protected void selectedChannelChanged(ChatChannel<? extends ChatMessage> chatCha
market.getQuoteCurrencyCode().toLowerCase());
model.getChannelIconNode().set(marketsImage);

model.getFiatAmountTitle().set(Res.get("bisqEasy.offerbook.offerList.table.columns.fiatAmount", channel.getMarket().getQuoteCurrencyCode()).toUpperCase());

updateMarketPrice();
bindOfferMessages(channel);
}
});
}

private void createMarketChannels() {
List<MarketChannelItem> marketChannelItems = bisqEasyOfferbookChannelService.getChannels().stream()
.map(MarketChannelItem::new)
.collect(Collectors.toList());
model.getMarketChannelItems().setAll(marketChannelItems);
}

void onCreateOffer() {
ChatChannel<? extends ChatMessage> chatChannel = model.getSelectedChannel();
checkArgument(chatChannel instanceof BisqEasyOfferbookChannel,
Expand All @@ -301,6 +313,29 @@ void onSortMarkets(MarketSortType marketSortType) {
model.getSortedMarketChannelItems().setComparator(marketSortType.getComparator());
}

void onSelectMarketChannelItem(MarketChannelItem item) {
if (item == null) {
selectionService.selectChannel(null);
} else if (!item.getChannel().equals(selectionService.getSelectedChannel().get())) {
selectionService.selectChannel(item.getChannel());
}
}

void onSelectOfferMessageItem(OfferMessageItem item) {
chatMessageContainerController.highlightOfferChatMessage(item == null ? null : item.getMessage());
}

double getMarketSelectionListCellHeight() {
return MARKET_SELECTION_LIST_CELL_HEIGHT;
}

private void createMarketChannels() {
List<MarketChannelItem> marketChannelItems = bisqEasyOfferbookChannelService.getChannels().stream()
.map(MarketChannelItem::new)
.collect(Collectors.toList());
model.getMarketChannelItems().setAll(marketChannelItems);
}

private void updateMarketPrice() {
Market selectedMarket = getModel().getSelectedMarketChannelItem().get().getMarket();
if (selectedMarket != null) {
Expand Down Expand Up @@ -330,14 +365,6 @@ private boolean isMaker(BisqEasyOffer bisqEasyOffer) {
return bisqEasyOffer.isMyOffer(userIdentityService.getMyUserProfileIds());
}

void onSelectMarketChannelItem(MarketChannelItem item) {
if (item == null) {
selectionService.selectChannel(null);
} else if (!item.getChannel().equals(selectionService.getSelectedChannel().get())) {
selectionService.selectChannel(item.getChannel());
}
}

private void maybeSelectFirst() {
if (selectionService.getSelectedChannel().get() == null &&
!bisqEasyOfferbookChannelService.getChannels().isEmpty() &&
Expand All @@ -346,7 +373,47 @@ private void maybeSelectFirst() {
}
}

double getMarketSelectionListCellHeight() {
return MARKET_SELECTION_LIST_CELL_HEIGHT;
private void bindOfferMessages(BisqEasyOfferbookChannel channel) {
model.getOfferMessageItems().clear();
offerMessagesPin = channel.getChatMessages().addObserver(new CollectionObserver<>() {
@Override
public void add(BisqEasyOfferbookMessage element) {
Optional<UserProfile> userProfile = userProfileService.findUserProfile(element.getAuthorUserProfileId());
boolean shouldAddOfferMessage = element.hasBisqEasyOffer()
&& element.getBisqEasyOffer().isPresent()
&& userProfile.isPresent();
if (shouldAddOfferMessage) {
UIThread.run(() -> {
OfferMessageItem item = new OfferMessageItem(element, element.getBisqEasyOffer().get(),
userProfile.get(), reputationService, marketPriceService);
model.getOfferMessageItems().add(item);
});
}
}

@Override
public void remove(Object element) {
if (element instanceof BisqEasyOfferbookMessage && ((BisqEasyOfferbookMessage) element).hasBisqEasyOffer()) {
UIThread.run(() -> {
BisqEasyOfferbookMessage offerMessage = (BisqEasyOfferbookMessage) element;
Optional<OfferMessageItem> toRemove = model.getOfferMessageItems().stream()
.filter(item -> item.getMessage().getId().equals(offerMessage.getId()))
.findAny();
toRemove.ifPresent(item -> {
item.dispose();
model.getOfferMessageItems().remove(item);
});
});
}
}

@Override
public void clear() {
UIThread.run(() -> {
model.getOfferMessageItems().forEach(OfferMessageItem::dispose);
model.getOfferMessageItems().clear();
});
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,29 @@

import bisq.chat.ChatChannelDomain;
import bisq.common.currency.Market;
import bisq.desktop.components.table.StandardTable;
import bisq.desktop.main.content.chat.ChatModel;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
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.collections.ObservableSet;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.scene.control.ToggleGroup;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

@Slf4j
Expand All @@ -51,6 +62,13 @@ public final class BisqEasyOfferbookModel extends ChatModel {
private final StringProperty marketPrice = new SimpleStringProperty();
private final ObservableSet<Market> favouriteMarkets = FXCollections.observableSet();
private final FilteredList<MarketChannelItem> favouriteMarketChannelItems = new FilteredList<>(marketChannelItems);
private final ObservableList<OfferMessageItem> offerMessageItems = FXCollections.observableArrayList();
private final FilteredList<OfferMessageItem> filteredOfferMessageItems = new FilteredList<>(offerMessageItems);
private final SortedList<OfferMessageItem> sortedOfferMessageItems = new SortedList<>(filteredOfferMessageItems);
private final List<StandardTable.FilterMenuItem<OfferMessageItem>> filterOfferMessageItems = new ArrayList<>();
private final ToggleGroup filterOfferMessageMenuItemToggleGroup = new ToggleGroup();
private final StringProperty fiatAmountTitle = new SimpleStringProperty();
private final BooleanProperty showBuyFromOfferMessageItems = new SimpleBooleanProperty(true); // TODO: save user pref in settings

@Setter
private Predicate<MarketChannelItem> marketPricePredicate = marketChannelItem -> true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
import bisq.desktop.common.utils.ImageUtil;
import bisq.desktop.components.containers.Spacer;
import bisq.desktop.components.controls.BisqTooltip;
import bisq.desktop.main.content.components.ReputationScoreDisplay;
import bisq.desktop.main.content.components.UserProfileIcon;
import bisq.i18n.Res;
import bisq.presentation.formatters.PercentageFormatter;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringExpression;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.*;
Expand Down Expand Up @@ -51,6 +55,11 @@ static Comparator<MarketChannelItem> sortByMarketActivity() {
.compare(lhs, rhs);
}


///////////////////////////////////////////////////////////////////////////////////////////////////
// MARKETS' LIST
///////////////////////////////////////////////////////////////////////////////////////////////////

static Callback<TableColumn<MarketChannelItem, MarketChannelItem>,
TableCell<MarketChannelItem, MarketChannelItem>> getMarketLabelCellFactory(boolean isFavouritesTableView) {
return column -> new TableCell<>() {
Expand Down Expand Up @@ -163,4 +172,80 @@ private static String getFormattedTooltip(int numOffers, String quoteCurrencyNam
? Res.get("bisqEasy.offerbook.marketListCell.numOffers.tooltip.many", numOffers, quoteCurrencyName)
: Res.get("bisqEasy.offerbook.marketListCell.numOffers.tooltip.one", numOffers, quoteCurrencyName);
}


///////////////////////////////////////////////////////////////////////////////////////////////////
// OFFERS' LIST
///////////////////////////////////////////////////////////////////////////////////////////////////

static Callback<TableColumn<OfferMessageItem, OfferMessageItem>,
TableCell<OfferMessageItem, OfferMessageItem>> getOfferMessageUserProfileCellFactory() {
return column -> new TableCell<>() {
private final Label userNameLabel = new Label();
private final ReputationScoreDisplay reputationScoreDisplay = new ReputationScoreDisplay();
private final VBox nameAndReputationBox = new VBox(userNameLabel, reputationScoreDisplay);
private final UserProfileIcon userProfileIcon = new UserProfileIcon(30);
private final HBox userProfileBox = new HBox(10, userProfileIcon, nameAndReputationBox);

{
userNameLabel.setId("chat-user-name");
HBox.setMargin(userProfileIcon, new Insets(0, 0, 0, -1));
nameAndReputationBox.setAlignment(Pos.CENTER_LEFT);
userProfileBox.setAlignment(Pos.CENTER_LEFT);
}

@Override
protected void updateItem(OfferMessageItem item, boolean empty) {
super.updateItem(item, empty);

if (item != null && !empty) {
userNameLabel.setText(item.getUserNickname());
reputationScoreDisplay.setReputationScore(item.getReputationScore());
userProfileIcon.setUserProfile(item.getUserProfile());
setGraphic(userProfileBox);
} else {
setGraphic(null);
}
}
};
}

static Callback<TableColumn<OfferMessageItem, OfferMessageItem>,
TableCell<OfferMessageItem, OfferMessageItem>> getOfferMessagePriceCellFactory() {
return column -> new TableCell<>() {
private final Label priceSpecAsPercentLabel = new Label();

@Override
protected void updateItem(OfferMessageItem item, boolean empty) {
super.updateItem(item, empty);

if (item != null && !empty) {
// TODO: react to priceSpec if it changes
priceSpecAsPercentLabel.setText(PercentageFormatter.formatToPercentWithSymbol(item.getPriceSpecAsPercent()));
setGraphic(priceSpecAsPercentLabel);
} else {
setGraphic(null);
}
}
};
}

static Callback<TableColumn<OfferMessageItem, OfferMessageItem>,
TableCell<OfferMessageItem, OfferMessageItem>> getOfferMessageFiatAmountCellFactory() {
return column -> new TableCell<>() {
private final Label fiatAmountLabel = new Label();

@Override
protected void updateItem(OfferMessageItem item, boolean empty) {
super.updateItem(item, empty);

if (item != null && !empty) {
fiatAmountLabel.setText(item.getMinMaxAmountAsString());
setGraphic(fiatAmountLabel);
} else {
setGraphic(null);
}
}
};
}
}
Loading
Loading