diff --git a/bisqapps/androidNode/build.gradle.kts b/bisqapps/androidNode/build.gradle.kts index a8e84bfb..53db9735 100644 --- a/bisqapps/androidNode/build.gradle.kts +++ b/bisqapps/androidNode/build.gradle.kts @@ -159,6 +159,7 @@ dependencies { implementation(libs.bisq.core.application) implementation(libs.bisq.core.chat) implementation(libs.bisq.core.presentation) + implementation(libs.bisq.core.bisq.easy) // protobuf implementation(libs.protobuf.gradle.plugin) diff --git a/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/AndroidApplicationService.kt b/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/AndroidApplicationService.kt index dc454292..d182ba6e 100644 --- a/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/AndroidApplicationService.kt +++ b/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/AndroidApplicationService.kt @@ -20,6 +20,7 @@ import androidx.core.util.Supplier import bisq.account.AccountService import bisq.application.ApplicationService import bisq.application.State +import bisq.bisq_easy.BisqEasyService import bisq.bonded_roles.BondedRolesService import bisq.bonded_roles.security_manager.alert.AlertNotificationsService import bisq.chat.ChatService @@ -38,7 +39,6 @@ import bisq.settings.SettingsService import bisq.support.SupportService import bisq.trade.TradeService import bisq.user.UserService -import com.google.common.base.Preconditions import lombok.Getter import lombok.Setter import lombok.extern.slf4j.Slf4j @@ -87,6 +87,8 @@ class AndroidApplicationService(androidMemoryReportService: AndroidMemoryReportS Supplier { applicationService.chatService } var settingsServiceSupplier: androidx.core.util.Supplier = Supplier { applicationService.settingsService } + var bisqEasyServiceSupplier: androidx.core.util.Supplier = + Supplier { applicationService.bisqEasyService } var supportServiceSupplier: androidx.core.util.Supplier = Supplier { applicationService.supportService } var systemNotificationServiceSupplier: androidx.core.util.Supplier = @@ -107,7 +109,6 @@ class AndroidApplicationService(androidMemoryReportService: AndroidMemoryReportS val log: Logger = LoggerFactory.getLogger(ApplicationService::class.java) } - val state = Observable(State.INITIALIZE_APP) private val shutDownErrorMessage = Observable() private val startupErrorMessage = Observable() @@ -150,11 +151,11 @@ class AndroidApplicationService(androidMemoryReportService: AndroidMemoryReportS val supportService: SupportService val systemNotificationService = SystemNotificationService(Optional.empty()) val tradeService: TradeService + val bisqEasyService:BisqEasyService val alertNotificationsService: AlertNotificationsService val favouriteMarketsService: FavouriteMarketsService val dontShowAgainService: DontShowAgainService - init { chatService = ChatService( persistenceService, @@ -186,6 +187,20 @@ class AndroidApplicationService(androidMemoryReportService: AndroidMemoryReportS settingsService ) + bisqEasyService = BisqEasyService( persistenceService, + securityService, + networkService, + identityService, + bondedRolesService, + accountService, + offerService, + contractService, + userService, + chatService, + settingsService, + supportService, + systemNotificationService, + tradeService) alertNotificationsService = AlertNotificationsService(settingsService, bondedRolesService.alertService) @@ -342,15 +357,6 @@ class AndroidApplicationService(androidMemoryReportService: AndroidMemoryReportS } } - private fun setState(newState: State) { - Preconditions.checkArgument( - state.get().ordinal < newState.ordinal, - "New state %s must have a higher ordinal as the current state %s", newState, state.get() - ) - state.set(newState) - log.info("New state {}", newState) - } - private fun logError(throwable: Throwable): Boolean { log.error("Exception at shutdown", throwable) return false diff --git a/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/di/AndroidNodeModule.kt b/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/di/AndroidNodeModule.kt index 3612cf73..c1a547fa 100644 --- a/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/di/AndroidNodeModule.kt +++ b/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/di/AndroidNodeModule.kt @@ -2,11 +2,12 @@ package network.bisq.mobile.android.node.di import network.bisq.mobile.android.node.AndroidApplicationService import network.bisq.mobile.android.node.domain.bootstrap.NodeApplicationBootstrapFacade -import network.bisq.mobile.android.node.domain.data.repository.NodeGreetingRepository +import network.bisq.mobile.android.node.domain.offerbook.NodeOfferbookServiceFacade import network.bisq.mobile.android.node.domain.user_profile.NodeUserProfileServiceFacade import network.bisq.mobile.android.node.presentation.NodeMainPresenter import network.bisq.mobile.android.node.service.AndroidMemoryReportService import network.bisq.mobile.domain.data.repository.main.bootstrap.ApplicationBootstrapFacade +import network.bisq.mobile.domain.offerbook.OfferbookServiceFacade import network.bisq.mobile.domain.user_profile.UserProfileServiceFacade import network.bisq.mobile.presentation.MainPresenter import network.bisq.mobile.presentation.ui.AppPresenter @@ -15,9 +16,6 @@ import org.koin.dsl.bind import org.koin.dsl.module val androidNodeModule = module { - // this one is for example properties, will be eliminated soon - single { NodeGreetingRepository() } - single { AndroidMemoryReportService(androidContext()) } @@ -28,8 +26,8 @@ val androidNodeModule = module { single { NodeUserProfileServiceFacade(get()) } - + single { NodeOfferbookServiceFacade(get()) } // this line showcases both, the possibility to change behaviour of the app by changing one definition // and binding the same obj to 2 different abstractions - single { NodeMainPresenter(get(), get(), get()) } bind AppPresenter::class + single { NodeMainPresenter(get(), get(), get(), get()) } bind AppPresenter::class } \ No newline at end of file diff --git a/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/data/repository/Repositories.kt b/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/data/repository/Repositories.kt deleted file mode 100644 index 841187e5..00000000 --- a/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/data/repository/Repositories.kt +++ /dev/null @@ -1,8 +0,0 @@ -package network.bisq.mobile.android.node.domain.data.repository - -import network.bisq.mobile.android.node.AndroidNodeGreeting -import network.bisq.mobile.domain.data.repository.GreetingRepository - -// this way of definingsupports both platforms -// add your repositories here and then in your DI module call this classes for instanciation -class NodeGreetingRepository: GreetingRepository() \ No newline at end of file diff --git a/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/NodeOfferbookServiceFacade.kt b/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/NodeOfferbookServiceFacade.kt new file mode 100644 index 00000000..1659bc77 --- /dev/null +++ b/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/NodeOfferbookServiceFacade.kt @@ -0,0 +1,63 @@ +package network.bisq.mobile.android.node.domain.offerbook + +import bisq.common.currency.Market +import co.touchlab.kermit.Logger +import kotlinx.coroutines.flow.StateFlow +import network.bisq.mobile.android.node.AndroidApplicationService +import network.bisq.mobile.android.node.domain.offerbook.market.MarketChannelSelectionService +import network.bisq.mobile.android.node.domain.offerbook.market.MarketListItemService +import network.bisq.mobile.android.node.domain.offerbook.offers.OfferbookListItemService +import network.bisq.mobile.client.replicated_model.common.currency.MarketListItem +import network.bisq.mobile.domain.offerbook.OfferbookListItem +import network.bisq.mobile.domain.offerbook.OfferbookMarket +import network.bisq.mobile.domain.offerbook.OfferbookServiceFacade + +class NodeOfferbookServiceFacade(private val applicationServiceSupplier: AndroidApplicationService.Supplier) : + OfferbookServiceFacade { + + // Dependencies + + + // Properties + override val marketListItemList: List get() = marketListItemService.marketListItems + override val offerbookListItemList: StateFlow> get() = offerbookListItemService.offerbookListItems + override val selectedOfferbookMarket: StateFlow get() = marketChannelSelectionService.selectedOfferbookMarket + + // Misc + private val log = Logger.withTag(this::class.simpleName ?: "NodeOfferbookServiceFacade") + private var offerbookListItemService: OfferbookListItemService = + OfferbookListItemService(applicationServiceSupplier) + private var marketListItemService: MarketListItemService = + MarketListItemService(applicationServiceSupplier) + private var marketChannelSelectionService: MarketChannelSelectionService = + MarketChannelSelectionService(applicationServiceSupplier) + + // Life cycle + override fun initialize() { + marketListItemService.initialize() + marketChannelSelectionService.initialize() + offerbookListItemService.initialize() + } + + override fun resume() { + marketListItemService.resume() + marketChannelSelectionService.resume() + offerbookListItemService.resume() + } + + override fun dispose() { + marketListItemService.dispose() + marketChannelSelectionService.dispose() + offerbookListItemService.dispose() + } + + // API + override fun selectMarket(marketListItem: MarketListItem) { + val market = Market( + marketListItem.baseCurrencyCode, + marketListItem.quoteCurrencyCode, + marketListItem.baseCurrencyName, marketListItem.quoteCurrencyName + ) + marketChannelSelectionService.selectMarket(market) + } +} \ No newline at end of file diff --git a/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/market/MarketChannelSelectionService.kt b/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/market/MarketChannelSelectionService.kt new file mode 100644 index 00000000..6561232d --- /dev/null +++ b/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/market/MarketChannelSelectionService.kt @@ -0,0 +1,90 @@ +package network.bisq.mobile.android.node.domain.offerbook.market + +import bisq.bonded_roles.market_price.MarketPriceService +import bisq.chat.bisqeasy.offerbook.BisqEasyOfferbookChannel +import bisq.chat.bisqeasy.offerbook.BisqEasyOfferbookChannelService +import bisq.chat.bisqeasy.offerbook.BisqEasyOfferbookSelectionService +import bisq.common.currency.Market +import bisq.common.observable.Pin +import bisq.presentation.formatters.PriceFormatter +import co.touchlab.kermit.Logger +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import network.bisq.mobile.android.node.AndroidApplicationService +import network.bisq.mobile.domain.LifeCycleAware +import network.bisq.mobile.domain.offerbook.OfferbookMarket + + +class MarketChannelSelectionService(private val applicationServiceSupplier: AndroidApplicationService.Supplier) : + LifeCycleAware { + + // Dependencies + private lateinit var bisqEasyOfferbookChannelService: BisqEasyOfferbookChannelService + private lateinit var bisqEasyOfferbookChannelSelectionService: BisqEasyOfferbookSelectionService + private lateinit var marketPriceService: MarketPriceService + + // Properties + private val _selectedOfferbookMarket = MutableStateFlow(OfferbookMarket("", "", "", "")) + val selectedOfferbookMarket: StateFlow get() = _selectedOfferbookMarket + + // Misc + private val log = Logger.withTag(this::class.simpleName ?: "SelectedMarket") + + private var selectedChannelPin: Pin? = null + + // Life cycle + override fun initialize() { + bisqEasyOfferbookChannelService = + applicationServiceSupplier.chatServiceSupplier.get().bisqEasyOfferbookChannelService + bisqEasyOfferbookChannelSelectionService = + applicationServiceSupplier.chatServiceSupplier.get().bisqEasyOfferbookChannelSelectionService + marketPriceService = + applicationServiceSupplier.bondedRolesServiceSupplier.get().marketPriceService + + observeSelectedChannel() + } + + override fun resume() { + observeSelectedChannel() + } + + override fun dispose() { + selectedChannelPin?.unbind() + selectedChannelPin = null + } + + // API + fun selectMarket(market: Market) { + log.i { "selectMarket " + market } + bisqEasyOfferbookChannelService.findChannel(market).ifPresent { + bisqEasyOfferbookChannelSelectionService.selectChannel(it) + } + } + + // Private + private fun observeSelectedChannel() { + selectedChannelPin = + bisqEasyOfferbookChannelSelectionService.selectedChannel.addObserver { marketChannel -> + marketChannel as BisqEasyOfferbookChannel + val market = marketChannel.market + + marketPriceService.setSelectedMarket(market) + + val title = marketChannel.shortDescription + val iconId = "channels-" + marketChannel.id.replace(".", "-") + val marketCodes = market.marketCodes + val formattedPrice = marketPriceService.findMarketPrice(market) + .map { PriceFormatter.format(it.priceQuote, true) } + .orElse("") + + _selectedOfferbookMarket.value = + OfferbookMarket(title, iconId, marketCodes, formattedPrice) + + log.i { "selectedChannel " + marketChannel } + log.i { "title " + title } + log.i { "iconId " + iconId } + log.i { "_marketCodes " + marketCodes } + log.i { "_formattedPrice " + formattedPrice } + } + } +} \ No newline at end of file diff --git a/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/market/MarketListItemService.kt b/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/market/MarketListItemService.kt new file mode 100644 index 00000000..9697b532 --- /dev/null +++ b/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/market/MarketListItemService.kt @@ -0,0 +1,81 @@ +package network.bisq.mobile.android.node.domain.offerbook.market + +import bisq.chat.bisqeasy.offerbook.BisqEasyOfferbookChannel +import bisq.chat.bisqeasy.offerbook.BisqEasyOfferbookMessage +import bisq.common.observable.Pin +import co.touchlab.kermit.Logger +import network.bisq.mobile.android.node.AndroidApplicationService +import network.bisq.mobile.client.replicated_model.common.currency.MarketListItem +import network.bisq.mobile.domain.LifeCycleAware + + +class MarketListItemService(private val applicationServiceSupplier: AndroidApplicationService.Supplier) : + LifeCycleAware { + // Properties + private val _marketListItems: List by lazy { fillMarketListItems() } + val marketListItems: List get() = _marketListItems + + // Misc + private val log = Logger.withTag(this::class.simpleName ?: "Markets") + private var numOffersObservers: MutableList = mutableListOf() + + // Life cycle + override fun initialize() { + } + + override fun resume() { + numOffersObservers.forEach { it.resume() } + } + + override fun dispose() { + numOffersObservers.forEach { it.dispose() } + } + + private fun fillMarketListItems(): MutableList { + val marketListItems: MutableList = mutableListOf() + applicationServiceSupplier.chatServiceSupplier.get().bisqEasyOfferbookChannelService.channels + .forEach { channel -> + // We convert channel.market to our replicated Market model + val marketListItem = MarketListItem( + channel.market.baseCurrencyCode, + channel.market.quoteCurrencyCode, + channel.market.baseCurrencyName, + channel.market.quoteCurrencyName, + ) + marketListItems.add(marketListItem) + + val numOffersObserver = NumOffersObserver(channel, marketListItem::setNumOffers) + numOffersObservers.add(numOffersObserver) + } + return marketListItems + } + + // Inner class + inner class NumOffersObserver( + private val channel: BisqEasyOfferbookChannel, + val setNumOffers: (Int) -> Unit + ) { + private var channelPin: Pin? = null + + init { + channelPin = channel.chatMessages.addObserver { this.updateNumOffers() } + } + + fun resume() { + dispose() + channelPin = channel.chatMessages.addObserver { this.updateNumOffers() } + } + + fun dispose() { + channelPin?.unbind() + channelPin = null + } + + private fun updateNumOffers() { + val numOffers = channel.chatMessages.stream() + .filter { obj: BisqEasyOfferbookMessage -> obj.hasBisqEasyOffer() } + .count().toInt() + setNumOffers(numOffers) + } + } +} \ No newline at end of file diff --git a/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/offers/OfferbookListItemService.kt b/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/offers/OfferbookListItemService.kt new file mode 100644 index 00000000..8f606dfc --- /dev/null +++ b/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/offers/OfferbookListItemService.kt @@ -0,0 +1,208 @@ +package network.bisq.mobile.android.node.domain.offerbook.offers + +import bisq.bisq_easy.BisqEasyServiceUtil +import bisq.bonded_roles.market_price.MarketPriceService +import bisq.chat.bisqeasy.offerbook.BisqEasyOfferbookChannel +import bisq.chat.bisqeasy.offerbook.BisqEasyOfferbookChannelService +import bisq.chat.bisqeasy.offerbook.BisqEasyOfferbookMessage +import bisq.chat.bisqeasy.offerbook.BisqEasyOfferbookSelectionService +import bisq.common.currency.Market +import bisq.common.observable.Pin +import bisq.common.observable.collection.CollectionObserver +import bisq.common.observable.collection.ObservableSet +import bisq.common.util.StringUtils +import bisq.i18n.Res +import bisq.offer.Direction +import bisq.offer.amount.OfferAmountFormatter +import bisq.offer.amount.spec.AmountSpec +import bisq.offer.amount.spec.RangeAmountSpec +import bisq.offer.bisq_easy.BisqEasyOffer +import bisq.offer.payment_method.PaymentMethodSpecUtil +import bisq.offer.price.spec.PriceSpec +import bisq.presentation.formatters.DateFormatter +import bisq.user.identity.UserIdentityService +import bisq.user.profile.UserProfile +import bisq.user.profile.UserProfileService +import bisq.user.reputation.ReputationService +import co.touchlab.kermit.Logger +import com.google.common.base.Joiner +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import network.bisq.mobile.android.node.AndroidApplicationService +import network.bisq.mobile.client.replicated_model.user.reputation.ReputationScore +import network.bisq.mobile.domain.LifeCycleAware +import network.bisq.mobile.domain.offerbook.OfferbookListItem +import java.text.DateFormat +import java.util.Date +import java.util.Optional + + +class OfferbookListItemService(private val applicationServiceSupplier: AndroidApplicationService.Supplier) : + LifeCycleAware { + // Dependencies + private lateinit var userProfileService: UserProfileService + private lateinit var userIdentityService: UserIdentityService + private lateinit var reputationService: ReputationService + private lateinit var bisqEasyOfferbookChannelService: BisqEasyOfferbookChannelService + private lateinit var bisqEasyOfferbookChannelSelectionService: BisqEasyOfferbookSelectionService + private lateinit var marketPriceService: MarketPriceService + + // Properties + private val _offerbookListItems = MutableStateFlow>(ArrayList()) + val offerbookListItems: StateFlow> get() = _offerbookListItems + + // Misc + private val log = Logger.withTag(this::class.simpleName ?: "Offers") + private var chatMessagesPin: Pin? = null + private var selectedChannelPin: Pin? = null + + + // Life cycle + override fun initialize() { + userProfileService = + applicationServiceSupplier.userServiceSupplier.get().userProfileService + userIdentityService = + applicationServiceSupplier.userServiceSupplier.get().userIdentityService + reputationService = applicationServiceSupplier.userServiceSupplier.get().reputationService + bisqEasyOfferbookChannelService = + applicationServiceSupplier.chatServiceSupplier.get().bisqEasyOfferbookChannelService + bisqEasyOfferbookChannelSelectionService = + applicationServiceSupplier.chatServiceSupplier.get().bisqEasyOfferbookChannelSelectionService + marketPriceService = + applicationServiceSupplier.bondedRolesServiceSupplier.get().marketPriceService + + addSelectedChannelObservers() + } + + override fun resume() { + addSelectedChannelObservers() + } + + override fun dispose() { + chatMessagesPin?.unbind() + chatMessagesPin = null + + selectedChannelPin?.unbind() + selectedChannelPin = null + } + + // Private + private fun addSelectedChannelObservers() { + selectedChannelPin = + bisqEasyOfferbookChannelSelectionService.selectedChannel.addObserver { channel -> + if (channel is BisqEasyOfferbookChannel) { + addChatMessagesObservers(channel) + } + } + } + + private fun addChatMessagesObservers(marketChannel: BisqEasyOfferbookChannel) { + val chatMessages: ObservableSet = marketChannel.chatMessages + chatMessagesPin = + chatMessages.addObserver(object : CollectionObserver { + override fun add(message: BisqEasyOfferbookMessage) { + if (message.hasBisqEasyOffer()) { + val offerbookListItem: OfferbookListItem = createOfferItem(message) + log.e { "add offer $offerbookListItem" } + _offerbookListItems.value += offerbookListItem + } + } + + override fun remove(message: Any) { + if (message is BisqEasyOfferbookMessage && message.hasBisqEasyOffer()) { + val toRemove = + _offerbookListItems.value.first { it.messageId == message.id } + log.e { "remove offer $toRemove" } + _offerbookListItems.value += toRemove + } + } + + override fun clear() { + _offerbookListItems.value.clear() + } + }) + } + + private fun createOfferItem(message: BisqEasyOfferbookMessage): OfferbookListItem { + val bisqEasyOffer: BisqEasyOffer = message.bisqEasyOffer.get() + val date = DateFormatter.formatDateTime( + Date(message.date), DateFormat.MEDIUM, DateFormat.SHORT, + true, " " + Res.get("temporal.at") + " " + ) + val authorUserProfileId = message.authorUserProfileId + val senderUserProfile: Optional = + userProfileService.findUserProfile(authorUserProfileId) + val nym: String = senderUserProfile.map { it.nym }.orElse("") + val userName: String = senderUserProfile.map { it.userName }.orElse("") + val reputationScore = + senderUserProfile.flatMap(reputationService::findReputationScore) + .map { + ReputationScore( + it.totalScore, + it.fiveSystemScore, + it.ranking + ) + } + .orElse(ReputationScore.NONE) + val amountSpec: AmountSpec = bisqEasyOffer.amountSpec + val priceSpec: PriceSpec = bisqEasyOffer.priceSpec + val hasAmountRange = amountSpec is RangeAmountSpec + val market: Market = bisqEasyOffer.market + val formattedQuoteAmount: String = + OfferAmountFormatter.formatQuoteAmount( + marketPriceService, + amountSpec, + priceSpec, + market, + hasAmountRange, + true + ) + val formattedPrice: String = + BisqEasyServiceUtil.getFormattedPriceSpec(priceSpec) + val quoteSidePaymentMethods: List = + PaymentMethodSpecUtil.getPaymentMethods(bisqEasyOffer.quoteSidePaymentMethodSpecs) + .map { it.name } + .toList() + val baseSidePaymentMethods: List = + PaymentMethodSpecUtil.getPaymentMethods(bisqEasyOffer.baseSidePaymentMethodSpecs) + .map { it.name } + .toList() + val supportedLanguageCodes: String = + Joiner.on(",").join(bisqEasyOffer.supportedLanguageCodes) + val isMyMessage = message.isMyMessage(userIdentityService) + val offerTitle = getOfferTitle(message, isMyMessage) + val messageId = message.id + val offerId = bisqEasyOffer.id + val offerbookListItem = OfferbookListItem( + messageId, + offerId, + isMyMessage, + offerTitle, + date, + nym, + userName, + reputationScore, + formattedQuoteAmount, + formattedPrice, + quoteSidePaymentMethods, + baseSidePaymentMethods, + supportedLanguageCodes + ) + return offerbookListItem + } + + private fun getOfferTitle(message: BisqEasyOfferbookMessage, isMyMessage: Boolean): String { + if (isMyMessage) { + val direction: Direction = message.bisqEasyOffer.get().direction + val directionString: String = + StringUtils.capitalize(Res.get("offer." + direction.name.lowercase())) + return Res.get( + "bisqEasy.tradeWizard.review.chatMessage.myMessageTitle", + directionString + ) + } else { + return message.text + } + + } +} \ No newline at end of file diff --git a/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/presentation/NodeMainPresenter.kt b/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/presentation/NodeMainPresenter.kt index cb65e446..0a477650 100644 --- a/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/presentation/NodeMainPresenter.kt +++ b/bisqapps/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/presentation/NodeMainPresenter.kt @@ -4,18 +4,19 @@ import android.app.Activity import network.bisq.mobile.android.node.AndroidApplicationService import network.bisq.mobile.android.node.service.AndroidMemoryReportService import network.bisq.mobile.domain.data.repository.main.bootstrap.ApplicationBootstrapFacade +import network.bisq.mobile.domain.offerbook.OfferbookServiceFacade import network.bisq.mobile.presentation.MainPresenter class NodeMainPresenter( private val supplier: AndroidApplicationService.Supplier, private val androidMemoryReportService: AndroidMemoryReportService, - private val applicationBootstrapFacade: ApplicationBootstrapFacade -) : MainPresenter(applicationBootstrapFacade) { + private val applicationBootstrapFacade: ApplicationBootstrapFacade, + private val offerbookServiceFacade: OfferbookServiceFacade +) : MainPresenter() { - var applicationServiceInited = false + private var applicationServiceInited = false override fun onViewAttached() { -// full override -// super.onViewAttached() + super.onViewAttached() if (!applicationServiceInited) { applicationServiceInited = true @@ -25,6 +26,7 @@ class NodeMainPresenter( AndroidApplicationService(androidMemoryReportService, filesDirsPath) applicationBootstrapFacade.initialize() supplier.applicationService.initialize() + offerbookServiceFacade.initialize() } } diff --git a/bisqapps/gradle/libs.versions.toml b/bisqapps/gradle/libs.versions.toml index 0533898c..aa465c18 100644 --- a/bisqapps/gradle/libs.versions.toml +++ b/bisqapps/gradle/libs.versions.toml @@ -129,6 +129,7 @@ bisq-core-chat = { module = "bisq:chat", version.ref = "bisq-core" } bisq-core-contract = { module = "bisq:contract", version.ref = "bisq-core" } bisq-core-i18n = { module = "bisq:i18n", version.ref = "bisq-core" } bisq-core-identity = { module = "bisq:identity", version.ref = "bisq-core" } +bisq-core-bisq-easy = { module = "bisq:bisq-easy", version.ref = "bisq-core" } # bisq core transitive dependencies chimp-jsocks = { module = 'com.github.chimp1984:jsocks', version.ref = 'chimp-jsocks-lib' } diff --git a/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/bootstrap/ClientApplicationBootstrapFacade.kt b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/bootstrap/ClientApplicationBootstrapFacade.kt index 76ef94ac..3e3391ba 100644 --- a/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/bootstrap/ClientApplicationBootstrapFacade.kt +++ b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/bootstrap/ClientApplicationBootstrapFacade.kt @@ -8,14 +8,12 @@ import network.bisq.mobile.domain.data.repository.main.bootstrap.ApplicationBoot class ClientApplicationBootstrapFacade() : ApplicationBootstrapFacade() { - private val coroutineScope = CoroutineScope(BackgroundDispatcher) - override fun initialize() { setState("Dummy state 1") setProgress(0f) // just dummy loading simulation, might be that there is no loading delay at the end... - coroutineScope.launch { + CoroutineScope(BackgroundDispatcher).launch { delay(500L) setState("Dummy state 2") setProgress(0.25f) diff --git a/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/di/ClientModule.kt b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/di/ClientModule.kt index 70f67302..87fe24df 100644 --- a/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/di/ClientModule.kt +++ b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/di/ClientModule.kt @@ -8,10 +8,13 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.contextual import network.bisq.mobile.android.node.main.bootstrap.ClientApplicationBootstrapFacade +import network.bisq.mobile.client.offerbook.ClientOfferbookServiceFacade import network.bisq.mobile.client.service.ApiRequestService import network.bisq.mobile.domain.client.main.user_profile.ClientUserProfileServiceFacade +import network.bisq.mobile.domain.client.main.user_profile.OfferbookApiGateway import network.bisq.mobile.domain.client.main.user_profile.UserProfileApiGateway import network.bisq.mobile.domain.data.repository.main.bootstrap.ApplicationBootstrapFacade +import network.bisq.mobile.domain.offerbook.OfferbookServiceFacade import network.bisq.mobile.domain.user_profile.UserProfileServiceFacade import network.bisq.mobile.utils.ByteArrayAsBase64Serializer import org.koin.core.qualifier.named @@ -39,8 +42,12 @@ val clientModule = module { single(named("ApiBaseUrl")) { provideApiBaseUrl() } single { ApiRequestService(get(), get(named("ApiBaseUrl"))) } + single { UserProfileApiGateway(get()) } single { ClientUserProfileServiceFacade(get()) } + + single { OfferbookApiGateway(get()) } + single { ClientOfferbookServiceFacade(get()) } } fun provideApiBaseUrl(): String { diff --git a/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/offerbook/ClientOfferbookServiceFacade.kt b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/offerbook/ClientOfferbookServiceFacade.kt new file mode 100644 index 00000000..adc1b859 --- /dev/null +++ b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/offerbook/ClientOfferbookServiceFacade.kt @@ -0,0 +1,83 @@ +package network.bisq.mobile.client.offerbook + +import co.touchlab.kermit.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import network.bisq.mobile.client.replicated_model.common.currency.MarketListItem +import network.bisq.mobile.client.service.Polling +import network.bisq.mobile.domain.client.main.user_profile.OfferbookApiGateway +import network.bisq.mobile.domain.data.BackgroundDispatcher +import network.bisq.mobile.domain.offerbook.OfferbookListItem +import network.bisq.mobile.domain.offerbook.OfferbookMarket +import network.bisq.mobile.domain.offerbook.OfferbookServiceFacade + +class ClientOfferbookServiceFacade(private val apiGateway: OfferbookApiGateway) : + OfferbookServiceFacade { + + // Properties + private val _marketListItems: MutableList = mutableListOf() + override val marketListItemList: List get() = _marketListItems + + private val _offerbookListItems: MutableStateFlow> = + MutableStateFlow(mutableListOf()) + override val offerbookListItemList: StateFlow> get() = _offerbookListItems + + private val _selectedOfferbookMarket: MutableStateFlow = + MutableStateFlow(OfferbookMarket("", "", "", "")) + override val selectedOfferbookMarket: StateFlow get() = _selectedOfferbookMarket + + // Misc + private val log = Logger.withTag(this::class.simpleName ?: "ClientOfferbookServiceFacade") + + // TODO for dev testing we keep it short, later it should be maybe 5 sec. or we use websockets + private var polling = Polling(1000) { getNumOffersByMarketCode() } + + // Life cycle + override fun initialize() { + CoroutineScope(BackgroundDispatcher).launch { + val numOffersByMarketCode = apiGateway.getNumOffersByMarketCode() + + val list = apiGateway.getMarkets() + .map { marketDto -> + val marketListItemWithNumOffers = MarketListItem( + marketDto.baseCurrencyCode, + marketDto.quoteCurrencyCode, + marketDto.baseCurrencyName, + marketDto.quoteCurrencyName, + ) + val numOffers = numOffersByMarketCode[marketDto.quoteCurrencyCode] ?: 0 + marketListItemWithNumOffers.setNumOffers(numOffers) + marketListItemWithNumOffers + } + _marketListItems.addAll(list) + } + + polling.start() + } + + override fun resume() { + polling.start() + } + + override fun dispose() { + polling.stop() + } + + override fun selectMarket(marketListItem: MarketListItem) { + //todo + log.i { "market " + marketListItem } + } + + private fun getNumOffersByMarketCode() { + CoroutineScope(BackgroundDispatcher).launch { + val numOffersByMarketCode = apiGateway.getNumOffersByMarketCode() + marketListItemList.map { marketListItem -> + val numOffers = numOffersByMarketCode[marketListItem.quoteCurrencyCode] ?: 0 + marketListItem.setNumOffers(numOffers) + marketListItem + } + } + } +} \ No newline at end of file diff --git a/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/offerbook/OfferbookApiGateway.kt b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/offerbook/OfferbookApiGateway.kt new file mode 100644 index 00000000..5a0c4faf --- /dev/null +++ b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/offerbook/OfferbookApiGateway.kt @@ -0,0 +1,28 @@ +package network.bisq.mobile.domain.client.main.user_profile + +import co.touchlab.kermit.Logger +import kotlinx.serialization.Serializable +import network.bisq.mobile.client.service.ApiRequestService + +class OfferbookApiGateway( + private val apiRequestService: ApiRequestService +) { + private val log = Logger.withTag(this::class.simpleName ?: "UserProfileApiGateway") + private val basePath = "offerbook" + + suspend fun getMarkets(): List { + return apiRequestService.get("$basePath/markets") + } + + suspend fun getNumOffersByMarketCode(): Map { + return apiRequestService.get("$basePath/markets/offers/count") + } +} + +@Serializable +class MarketDto( + val baseCurrencyCode: String, + val quoteCurrencyCode: String, + val baseCurrencyName: String, + val quoteCurrencyName: String +) \ No newline at end of file diff --git a/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/replicated_model/common/currency/MarketListItem.kt b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/replicated_model/common/currency/MarketListItem.kt new file mode 100644 index 00000000..09e56691 --- /dev/null +++ b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/replicated_model/common/currency/MarketListItem.kt @@ -0,0 +1,45 @@ +/* + * 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 network.bisq.mobile.client.replicated_model.common.currency + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class MarketListItem( + val baseCurrencyCode: String, + val quoteCurrencyCode: String, + val baseCurrencyName: String, + val quoteCurrencyName: String, +) { + companion object { + private const val QUOTE_SEPARATOR = "/" + } + + private val _numOffers = MutableStateFlow(0) + val numOffers: StateFlow get() = _numOffers + fun setNumOffers(value: Int) { + _numOffers.value = value + } + + val marketCodes: String + get() = baseCurrencyCode + QUOTE_SEPARATOR + quoteCurrencyCode + + override fun toString(): String { + return "Market(baseCurrencyCode='$baseCurrencyCode', quoteCurrencyCode='$quoteCurrencyCode', baseCurrencyName='$baseCurrencyName', quoteCurrencyName='$quoteCurrencyName', _numOffers=$_numOffers)" + } + +} \ No newline at end of file diff --git a/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/replicated_model/user/reputation/ReputationScore.kt b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/replicated_model/user/reputation/ReputationScore.kt new file mode 100644 index 00000000..fcb2515d --- /dev/null +++ b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/replicated_model/user/reputation/ReputationScore.kt @@ -0,0 +1,38 @@ +/* + * 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 network.bisq.mobile.client.replicated_model.user.reputation + +class ReputationScore( + val totalScore: Long, + val fiveSystemScore: Double, + val ranking: Int +) { + val tooltipString: String + get() = "Score: $totalScore\nRanking: $rankingAsString" + + val rankingAsString: String + get() = if (ranking == Int.MAX_VALUE) "-" else ranking.toString() + + companion object { + val NONE: ReputationScore = ReputationScore(0, 0.0, Int.MAX_VALUE) + } + + override fun toString(): String { + return "ReputationScore(totalScore=$totalScore, fiveSystemScore=$fiveSystemScore, ranking=$ranking)" + } +} + diff --git a/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/service/Polling.kt b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/service/Polling.kt new file mode 100644 index 00000000..70012912 --- /dev/null +++ b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/service/Polling.kt @@ -0,0 +1,34 @@ +package network.bisq.mobile.client.service + +import co.touchlab.kermit.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import network.bisq.mobile.domain.data.BackgroundDispatcher + +class Polling(private val intervalMillis: Long, private val task: () -> Unit) { + private val log = Logger.withTag(this::class.simpleName ?: "Polling") + + private var job: Job? = null + private var isPolling = false + + fun start() { + if (!isPolling) { + isPolling = true + job = CoroutineScope(BackgroundDispatcher).launch { + while (isPolling) { + //log.i { "poll" } + task() + delay(intervalMillis) + } + } + } + } + + fun stop() { + isPolling = false + job?.cancel() + job = null + } +} \ No newline at end of file diff --git a/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/LifeCycleAware.kt b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/LifeCycleAware.kt new file mode 100644 index 00000000..6df79572 --- /dev/null +++ b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/LifeCycleAware.kt @@ -0,0 +1,9 @@ +package network.bisq.mobile.domain + +interface LifeCycleAware { + fun initialize() + + fun resume() + + fun dispose() +} \ No newline at end of file diff --git a/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/offerbook/OfferbookListItem.kt b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/offerbook/OfferbookListItem.kt new file mode 100644 index 00000000..b142f84a --- /dev/null +++ b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/offerbook/OfferbookListItem.kt @@ -0,0 +1,37 @@ +package network.bisq.mobile.domain.offerbook + +import network.bisq.mobile.client.replicated_model.user.reputation.ReputationScore + +class OfferbookListItem( + val messageId: String, + val offerId: String, + val isMyMessage: Boolean, + val offerTitle: String, + val date: String, + val nym: String, + val userName: String, + val reputationScore: ReputationScore, + val formattedQuoteAmount: String, + val formattedPrice: String, + val quoteSidePaymentMethods: List, + val baseSidePaymentMethods: List, + val supportedLanguageCodes: String +) { + override fun toString(): String { + return "OfferItem(\n" + + "MessageId ID='${messageId}'\n" + + "Offer ID='${offerId}'\n" + + "offerTitle='${offerTitle}'\n" + + "isMyMessage='${isMyMessage}'\n" + + "date='$date'\n" + + "nym='$nym'\n" + + "userName='$userName'\n" + + "reputationScore=$reputationScore\n" + + "formattedQuoteAmount='$formattedQuoteAmount'\n" + + "formattedPrice='$formattedPrice'\n" + + "quoteSidePaymentMethods=$quoteSidePaymentMethods\n" + + "baseSidePaymentMethods=$baseSidePaymentMethods\n" + + "supportedLanguageCodes='$supportedLanguageCodes'\n" + + ")" + } +} \ No newline at end of file diff --git a/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/offerbook/OfferbookMarket.kt b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/offerbook/OfferbookMarket.kt new file mode 100644 index 00000000..6608d2da --- /dev/null +++ b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/offerbook/OfferbookMarket.kt @@ -0,0 +1,9 @@ +package network.bisq.mobile.domain.offerbook + +class OfferbookMarket( + val title: String, + val iconId: String, + val marketCodes: String, + val formattedPrice: String +) { +} \ No newline at end of file diff --git a/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/offerbook/OfferbookServiceFacade.kt b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/offerbook/OfferbookServiceFacade.kt new file mode 100644 index 00000000..69feaaca --- /dev/null +++ b/bisqapps/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/offerbook/OfferbookServiceFacade.kt @@ -0,0 +1,18 @@ +package network.bisq.mobile.domain.offerbook + +import kotlinx.coroutines.flow.StateFlow +import network.bisq.mobile.client.replicated_model.common.currency.MarketListItem +import network.bisq.mobile.domain.LifeCycleAware + +interface OfferbookServiceFacade: LifeCycleAware { + val marketListItemList: List + val offerbookListItemList: StateFlow> + val selectedOfferbookMarket: StateFlow + + fun selectMarket(marketListItem: MarketListItem) + + companion object { + val mainCurrencies: List = + listOf("usd", "eur", "gbp", "cad", "aud", "rub", "cny", "inr", "ngn") + } +} \ No newline at end of file diff --git a/bisqapps/shared/presentation/src/commonMain/composeResources/drawable/currency_euro.png b/bisqapps/shared/presentation/src/commonMain/composeResources/drawable/currency_eur.png similarity index 100% rename from bisqapps/shared/presentation/src/commonMain/composeResources/drawable/currency_euro.png rename to bisqapps/shared/presentation/src/commonMain/composeResources/drawable/currency_eur.png diff --git a/bisqapps/shared/presentation/src/commonMain/composeResources/drawable/currency_gpb.png b/bisqapps/shared/presentation/src/commonMain/composeResources/drawable/currency_gbp.png similarity index 100% rename from bisqapps/shared/presentation/src/commonMain/composeResources/drawable/currency_gpb.png rename to bisqapps/shared/presentation/src/commonMain/composeResources/drawable/currency_gbp.png diff --git a/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/client/ClientMainPresenter.kt b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/client/ClientMainPresenter.kt new file mode 100644 index 00000000..0e516932 --- /dev/null +++ b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/client/ClientMainPresenter.kt @@ -0,0 +1,26 @@ +package network.bisq.mobile.client + +import network.bisq.mobile.domain.data.repository.main.bootstrap.ApplicationBootstrapFacade +import network.bisq.mobile.domain.offerbook.OfferbookServiceFacade +import network.bisq.mobile.presentation.MainPresenter + +class ClientMainPresenter( + private val applicationBootstrapFacade: ApplicationBootstrapFacade, + private val offerbookServiceFacade: OfferbookServiceFacade +) : MainPresenter() { + + private var applicationServiceInited = false + override fun onViewAttached() { + super.onViewAttached() + + if (!applicationServiceInited) { + applicationServiceInited = true + applicationBootstrapFacade.initialize() + offerbookServiceFacade.initialize() + } + } + + override fun onDestroying() { + super.onDestroying() + } +} \ No newline at end of file diff --git a/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/MainPresenter.kt b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/MainPresenter.kt index 041360a3..67ce8087 100644 --- a/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/MainPresenter.kt +++ b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/MainPresenter.kt @@ -2,16 +2,16 @@ package network.bisq.mobile.presentation import androidx.navigation.NavHostController import co.touchlab.kermit.Logger -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import network.bisq.mobile.android.node.BuildNodeConfig import network.bisq.mobile.client.shared.BuildConfig -import network.bisq.mobile.domain.data.repository.main.bootstrap.ApplicationBootstrapFacade import network.bisq.mobile.presentation.ui.AppPresenter /** * Main Presenter as an example of implementation for now. */ -open class MainPresenter(private val applicationBootstrapFacade: ApplicationBootstrapFacade) : BasePresenter(null), AppPresenter { +open class MainPresenter : BasePresenter(null), AppPresenter { lateinit var navController: NavHostController private set @@ -24,16 +24,6 @@ open class MainPresenter(private val applicationBootstrapFacade: ApplicationBoot private val _isContentVisible = MutableStateFlow(false) override val isContentVisible: StateFlow = _isContentVisible - private var applicationServiceInited = false - override fun onViewAttached() { - super.onViewAttached() - - if (!applicationServiceInited) { - applicationServiceInited = true - applicationBootstrapFacade.initialize() - } - } - // passthrough example // private val _greetingText: StateFlow = stateFlowFromRepository( // repositoryFlow = greetingRepository.data, @@ -56,6 +46,4 @@ open class MainPresenter(private val applicationBootstrapFacade: ApplicationBoot override fun toggleContentVisibility() { _isContentVisible.value = !_isContentVisible.value } - - } \ No newline at end of file diff --git a/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/di/PresentationModule.kt b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/di/PresentationModule.kt index 1482abb4..3c017e89 100644 --- a/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/di/PresentationModule.kt +++ b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/di/PresentationModule.kt @@ -1,10 +1,13 @@ package network.bisq.mobile.presentation.di +import androidx.navigation.NavController import androidx.navigation.NavHostController +import network.bisq.mobile.client.ClientMainPresenter import network.bisq.mobile.presentation.MainPresenter import network.bisq.mobile.presentation.ui.AppPresenter import network.bisq.mobile.presentation.ui.uicases.GettingStartedPresenter import network.bisq.mobile.presentation.ui.uicases.IGettingStarted +import network.bisq.mobile.presentation.ui.uicases.exchange.ExchangePresenter import network.bisq.mobile.presentation.ui.uicases.startup.CreateProfilePresenter import network.bisq.mobile.presentation.ui.uicases.startup.IOnboardingPresenter import network.bisq.mobile.presentation.ui.uicases.startup.ITrustedNodeSetupPresenter @@ -19,10 +22,10 @@ val presentationModule = module { single(named("RootNavController")) { getKoin().getProperty("RootNavController") } single(named("TabNavController")) { getKoin().getProperty("TabNavController") } - single { MainPresenter(get()) } bind AppPresenter::class - + single { ClientMainPresenter(get(), get()) } bind AppPresenter::class single { SplashPresenter( + get(), get(), get() ) @@ -48,6 +51,13 @@ val presentationModule = module { get() ) } + single { (navController: NavController) -> + ExchangePresenter( + get(), + navController = navController, + get() + ) + } single { TrustedNodeSetupPresenter( diff --git a/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/components/CurrencyProfileCard.kt b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/components/CurrencyProfileCard.kt index 3bfdcca4..269c0a21 100644 --- a/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/components/CurrencyProfileCard.kt +++ b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/components/CurrencyProfileCard.kt @@ -1,6 +1,7 @@ package network.bisq.mobile.presentation.ui.components import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -10,12 +11,12 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.StateFlow import network.bisq.mobile.presentation.ui.components.atoms.BisqText import network.bisq.mobile.presentation.ui.theme.BisqTheme import org.jetbrains.compose.resources.DrawableResource @@ -24,32 +25,41 @@ import org.jetbrains.compose.resources.painterResource @OptIn(ExperimentalResourceApi::class) @Composable -fun CurrencyProfileCard(currencyName: String, currencyShort: String, image: DrawableResource) { +fun CurrencyProfileCard( + name: String, + code: String, + numOffers: StateFlow, + icon: DrawableResource, + onClick: () -> Unit +) { Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 16.dp) + .clickable { onClick() }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween - ) { + ) { Row( verticalAlignment = Alignment.CenterVertically, ) { - Image(painterResource(image), null, modifier = Modifier.size(36.dp)) + Image(painterResource(icon), null, modifier = Modifier.size(36.dp)) Spacer(modifier = Modifier.width(8.dp)) Column { BisqText.baseRegular( - text = currencyName, + text = name, color = BisqTheme.colors.light1, ) Spacer(modifier = Modifier.height(8.dp)) BisqText.baseRegular( - text = currencyShort, + text = code, color = BisqTheme.colors.grey2, ) } } BisqText.smallRegular( - text = "43 offers", + text = numOffers.collectAsState().value.toString() + " offers", color = BisqTheme.colors.primary, ) } -} \ No newline at end of file +} diff --git a/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/exchange/ExchangePresenter.kt b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/exchange/ExchangePresenter.kt new file mode 100644 index 00000000..7d68a3c4 --- /dev/null +++ b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/exchange/ExchangePresenter.kt @@ -0,0 +1,59 @@ +package network.bisq.mobile.presentation.ui.uicases.exchange + +import androidx.navigation.NavController +import bisqapps.shared.presentation.generated.resources.Res +import bisqapps.shared.presentation.generated.resources.currency_usd +import co.touchlab.kermit.Logger +import network.bisq.mobile.client.replicated_model.common.currency.MarketListItem +import network.bisq.mobile.domain.offerbook.OfferbookServiceFacade +import network.bisq.mobile.presentation.BasePresenter +import network.bisq.mobile.presentation.MainPresenter +import network.bisq.mobile.presentation.ui.uicases.exchange.IconMap.Companion.ICON_BY_CODE + +class ExchangePresenter( + mainPresenter: MainPresenter, + navController: NavController, + val service: OfferbookServiceFacade, +) : BasePresenter(mainPresenter) { + + private val log = Logger.withTag(this::class.simpleName ?: "ExchangePresenter") + private var mainCurrencies = OfferbookServiceFacade.mainCurrencies + + var marketListItemWithNumOffers: List = service.marketListItemList + .sortedWith( + compareByDescending { it.numOffers.value } + .thenByDescending { mainCurrencies.contains(it.quoteCurrencyCode.lowercase()) } // [1] + .thenBy { item-> + if (!mainCurrencies.contains(item.quoteCurrencyCode.lowercase())) item.quoteCurrencyName + else null // Null values will naturally be sorted together + } + ) + // [1] thenBy doesn’t work as expected for boolean expressions because true and false are + // sorted alphabetically (false before true), thus we use thenByDescending + + fun drawableResource(code: String) = + ICON_BY_CODE[code.lowercase()] ?: Res.drawable.currency_usd + + override fun onViewAttached() { + } + + override fun onResume() { + service.resume() + } + + override fun onPause() { + service.dispose() + } + + override fun onViewUnattaching() { + service.dispose() + } + + override fun onDestroying() { + service.dispose() + } + + fun onSelectMarket(marketListItem: MarketListItem) { + service.selectMarket(marketListItem) + } +} diff --git a/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/exchange/ExchangeScreen.kt b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/exchange/ExchangeScreen.kt index e3751023..633c6111 100644 --- a/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/exchange/ExchangeScreen.kt +++ b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/exchange/ExchangeScreen.kt @@ -12,27 +12,32 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import bisqapps.shared.presentation.generated.resources.Res -import bisqapps.shared.presentation.generated.resources.currency_euro -import bisqapps.shared.presentation.generated.resources.currency_gpb -import bisqapps.shared.presentation.generated.resources.currency_usd -import network.bisq.mobile.presentation.ui.components.CurrencyProfileCard import network.bisq.mobile.components.MaterialTextField -import network.bisq.mobile.presentation.ui.components.molecules.TopBar +import network.bisq.mobile.presentation.ui.components.CurrencyProfileCard import network.bisq.mobile.presentation.ui.components.atoms.icons.SortIcon -import org.jetbrains.compose.resources.ExperimentalResourceApi +import network.bisq.mobile.presentation.ui.components.molecules.TopBar import org.koin.compose.koinInject +import org.koin.core.parameter.parametersOf import org.koin.core.qualifier.named @Composable fun ExchangeScreen() { val navController: NavHostController = koinInject(named("RootNavController")) + val presenter: ExchangePresenter = koinInject { parametersOf(navController) } + val originDirection = LocalLayoutDirection.current + + LaunchedEffect(Unit) { + presenter.onViewAttached() + } + + Column( modifier = Modifier.fillMaxSize() ) { @@ -50,9 +55,16 @@ fun ExchangeScreen() { } Spacer(modifier = Modifier.height(12.dp)) Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - CurrencyProfileCard("US Dollars", "USD", Res.drawable.currency_usd) - CurrencyProfileCard("Euro", "EUR", Res.drawable.currency_euro) - CurrencyProfileCard("British Pounds", "GPB", Res.drawable.currency_gpb) + presenter.marketListItemWithNumOffers + .forEach { item -> + val card = CurrencyProfileCard(item.quoteCurrencyName, + item.quoteCurrencyCode, + item.numOffers, + presenter.drawableResource(item.quoteCurrencyCode), + onClick = { + presenter.onSelectMarket(item) + }) + } } } } diff --git a/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/exchange/IconMap.kt b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/exchange/IconMap.kt new file mode 100644 index 00000000..14e8b5dd --- /dev/null +++ b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/exchange/IconMap.kt @@ -0,0 +1,166 @@ +package network.bisq.mobile.presentation.ui.uicases.exchange + +import bisqapps.shared.presentation.generated.resources.Res +import bisqapps.shared.presentation.generated.resources.currency_eur +import bisqapps.shared.presentation.generated.resources.currency_gbp +import bisqapps.shared.presentation.generated.resources.currency_usd + +class IconMap { + // TODO add remaining icons + companion object { + val ICON_BY_CODE = mapOf( + "usd" to Res.drawable.currency_usd, + "eur" to Res.drawable.currency_eur, + "gbp" to Res.drawable.currency_gbp, + /* "cad" to Res.drawable.currency_cad, + "aud" to Res.drawable.currency_aud, + "rub" to Res.drawable.currency_rub, + "cny" to Res.drawable.currency_cny, + "inr" to Res.drawable.currency_inr, + "ngn" to Res.drawable.currency_ngn, + "afn" to Res.drawable.currency_afn, + "all" to Res.drawable.currency_all, + "dzd" to Res.drawable.currency_dzd, + "aoa" to Res.drawable.currency_aoa, + "ars" to Res.drawable.currency_ars, + "amd" to Res.drawable.currency_amd, + "awg" to Res.drawable.currency_awg, + "azn" to Res.drawable.currency_azn, + "bsd" to Res.drawable.currency_bsd, + "bhd" to Res.drawable.currency_bhd, + "bdt" to Res.drawable.currency_bdt, + "bbd" to Res.drawable.currency_bbd, + "byn" to Res.drawable.currency_byn, + "bzd" to Res.drawable.currency_bzd, + "bmd" to Res.drawable.currency_bmd, + "btn" to Res.drawable.currency_btn, + "bob" to Res.drawable.currency_bob, + "bam" to Res.drawable.currency_bam, + "bwp" to Res.drawable.currency_bwp, + "brl" to Res.drawable.currency_brl, + "bnd" to Res.drawable.currency_bnd, + "bgn" to Res.drawable.currency_bgn, + "bif" to Res.drawable.currency_bif, + "xpf" to Res.drawable.currency_xpf, + "khr" to Res.drawable.currency_khr, + "cve" to Res.drawable.currency_cve, + "kyd" to Res.drawable.currency_kyd, + "xaf" to Res.drawable.currency_xaf, + "clp" to Res.drawable.currency_clp, + "cop" to Res.drawable.currency_cop, + "kmf" to Res.drawable.currency_kmf, + "cdf" to Res.drawable.currency_cdf, + "crc" to Res.drawable.currency_crc, + "cup" to Res.drawable.currency_cup, + "czk" to Res.drawable.currency_czk, + "dkk" to Res.drawable.currency_dkk, + "djf" to Res.drawable.currency_djf, + "dop" to Res.drawable.currency_dop, + "xcd" to Res.drawable.currency_xcd, + "egp" to Res.drawable.currency_egp, + "ern" to Res.drawable.currency_ern, + "etb" to Res.drawable.currency_etb, + "fkp" to Res.drawable.currency_fkp, + "fjd" to Res.drawable.currency_fjd, + "gmd" to Res.drawable.currency_gmd, + "gel" to Res.drawable.currency_gel, + "ghs" to Res.drawable.currency_ghs, + "gip" to Res.drawable.currency_gip, + "gtq" to Res.drawable.currency_gtq, + "gnf" to Res.drawable.currency_gnf, + "gyd" to Res.drawable.currency_gyd, + "htg" to Res.drawable.currency_htg, + "hnl" to Res.drawable.currency_hnl, + "hkd" to Res.drawable.currency_hkd, + "huf" to Res.drawable.currency_huf, + "isk" to Res.drawable.currency_isk, + "idr" to Res.drawable.currency_idr, + "irr" to Res.drawable.currency_irr, + "iqd" to Res.drawable.currency_iqd, + "ils" to Res.drawable.currency_ils, + "jmd" to Res.drawable.currency_jmd, + "jpy" to Res.drawable.currency_jpy, + "jod" to Res.drawable.currency_jod, + "kzt" to Res.drawable.currency_kzt, + "kes" to Res.drawable.currency_kes, + "kwd" to Res.drawable.currency_kwd, + "kgs" to Res.drawable.currency_kgs, + "lak" to Res.drawable.currency_lak, + "lbp" to Res.drawable.currency_lbp, + "lrd" to Res.drawable.currency_lrd, + "lyd" to Res.drawable.currency_lyd, + "mop" to Res.drawable.currency_mop, + "mkd" to Res.drawable.currency_mkd, + "mga" to Res.drawable.currency_mga, + "mwk" to Res.drawable.currency_mwk, + "myr" to Res.drawable.currency_myr, + "mvr" to Res.drawable.currency_mvr, + "mru" to Res.drawable.currency_mru, + "mur" to Res.drawable.currency_mur, + "mxn" to Res.drawable.currency_mxn, + "mdl" to Res.drawable.currency_mdl, + "mnt" to Res.drawable.currency_mnt, + "mad" to Res.drawable.currency_mad, + "mzn" to Res.drawable.currency_mzn, + "mmk" to Res.drawable.currency_mmk, + "nad" to Res.drawable.currency_nad, + "npr" to Res.drawable.currency_npr, + "ang" to Res.drawable.currency_ang, + "twd" to Res.drawable.currency_twd, + "nzd" to Res.drawable.currency_nzd, + "nio" to Res.drawable.currency_nio, + "kpw" to Res.drawable.currency_kpw, + "nok" to Res.drawable.currency_nok, + "omr" to Res.drawable.currency_omr, + "pkr" to Res.drawable.currency_pkr, + "pab" to Res.drawable.currency_pab, + "pgk" to Res.drawable.currency_pgk, + "pyg" to Res.drawable.currency_pyg, + "pen" to Res.drawable.currency_pen, + "php" to Res.drawable.currency_php, + "pln" to Res.drawable.currency_pln, + "qar" to Res.drawable.currency_qar, + "ron" to Res.drawable.currency_ron, + "rwf" to Res.drawable.currency_rwf, + "wst" to Res.drawable.currency_wst, + "sar" to Res.drawable.currency_sar, + "rsd" to Res.drawable.currency_rsd, + "scr" to Res.drawable.currency_scr, + "sle" to Res.drawable.currency_sle, + "sgd" to Res.drawable.currency_sgd, + "sbd" to Res.drawable.currency_sbd, + "sos" to Res.drawable.currency_sos, + "zar" to Res.drawable.currency_zar, + "krw" to Res.drawable.currency_krw, + "ssp" to Res.drawable.currency_ssp, + "lkr" to Res.drawable.currency_lkr, + "shp" to Res.drawable.currency_shp, + "sdg" to Res.drawable.currency_sdg, + "srd" to Res.drawable.currency_srd, + "szl" to Res.drawable.currency_szl, + "sek" to Res.drawable.currency_sek, + "chf" to Res.drawable.currency_chf, + "syp" to Res.drawable.currency_syp, + "stn" to Res.drawable.currency_stn, + "tjs" to Res.drawable.currency_tjs, + "tzs" to Res.drawable.currency_tzs, + "thb" to Res.drawable.currency_thb, + "top" to Res.drawable.currency_top, + "ttd" to Res.drawable.currency_ttd, + "tnd" to Res.drawable.currency_tnd, + "try" to Res.drawable.currency_try, + "tmt" to Res.drawable.currency_tmt, + "ugx" to Res.drawable.currency_ugx, + "uah" to Res.drawable.currency_uah, + "aed" to Res.drawable.currency_aed, + "uyu" to Res.drawable.currency_uyu, + "uzs" to Res.drawable.currency_uzs, + "vuv" to Res.drawable.currency_vuv, + "ves" to Res.drawable.currency_ves, + "vnd" to Res.drawable.currency_vnd, + "xof" to Res.drawable.currency_xof, + "yer" to Res.drawable.currency_yer, + "zmw" to Res.drawable.currency_zmw*/ + ) + } +} \ No newline at end of file diff --git a/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/startup/CreateProfilePresenter.kt b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/startup/CreateProfilePresenter.kt index 1c23eb19..08f67ccf 100644 --- a/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/startup/CreateProfilePresenter.kt +++ b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/startup/CreateProfilePresenter.kt @@ -90,7 +90,11 @@ open class CreateProfilePresenter( CoroutineScope(Dispatchers.Main).launch { // todo stop busy animation in UI - rootNavigator.navigate(Routes.TrustedNodeSetup.name) { + // Skip for now the TrustedNodeSetup until its fully implemented with persisting the api URL. + /* rootNavigator.navigate(Routes.TrustedNodeSetup.name) { + popUpTo(Routes.CreateProfile.name) { inclusive = true } + } */ + rootNavigator.navigate(Routes.TabContainer.name) { popUpTo(Routes.CreateProfile.name) { inclusive = true } } } diff --git a/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/startup/SplashPresenter.kt b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/startup/SplashPresenter.kt index f79dd735..034bce6d 100644 --- a/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/startup/SplashPresenter.kt +++ b/bisqapps/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/startup/SplashPresenter.kt @@ -1,18 +1,19 @@ package network.bisq.mobile.presentation.ui.uicases.startup -import androidx.navigation.NavController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import network.bisq.mobile.domain.data.repository.main.bootstrap.ApplicationBootstrapFacade +import network.bisq.mobile.domain.user_profile.UserProfileServiceFacade import network.bisq.mobile.presentation.BasePresenter import network.bisq.mobile.presentation.MainPresenter import network.bisq.mobile.presentation.ui.navigation.Routes open class SplashPresenter( mainPresenter: MainPresenter, - applicationBootstrapFacade: ApplicationBootstrapFacade + applicationBootstrapFacade: ApplicationBootstrapFacade, + private val userProfileService: UserProfileServiceFacade ) : BasePresenter(mainPresenter) { private val coroutineScope = CoroutineScope(Dispatchers.Main) @@ -29,21 +30,15 @@ open class SplashPresenter( } } - private fun navigateToNextScreen() { - // TODO: Conditional nav - // If firstTimeApp launch, goto Onboarding[clientMode] (androidNode / xClient) - // If not, goto TabContainerScreen - /* rootNavigator.navigate(Routes.Onboarding.name) { - popUpTo(Routes.Splash.name) { inclusive = true } - }*/ - - //TODO - /* rootNavigator.navigate(Routes.TabContainer.name) { - popUpTo(Routes.TrustedNodeSetup.name) { inclusive = true } - }*/ - rootNavigator.navigate(Routes.CreateProfile.name) { - popUpTo(Routes.Splash.name) { inclusive = true } + private suspend fun navigateToNextScreen() { + if(userProfileService.hasUserProfile()){ + rootNavigator.navigate(Routes.TabContainer.name) { + popUpTo(Routes.Splash.name) { inclusive = true } + } + }else{ + rootNavigator.navigate(Routes.CreateProfile.name) { + popUpTo(Routes.Splash.name) { inclusive = true } + } } } - }