Updated on 2026-08-14
This commit is contained in:
parent
b0ac045d0d
commit
626d0914a0
31 changed files with 1201 additions and 95 deletions
|
|
@ -37,6 +37,14 @@ object MarketsDomainModule {
|
|||
return GetMarketsTokenListFlowUseCase(marketsTokenRepository = marketsTokenRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetTopFiveMarketTokenUseCase(
|
||||
marketsTokenRepository: MarketsTokenRepository,
|
||||
): GetTopFiveMarketTokenUseCase {
|
||||
return GetTopFiveMarketTokenUseCase(marketsTokenRepository = marketsTokenRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetTokenPriceChartUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenPriceChartUseCase {
|
||||
|
|
|
|||
1
common/ui-markets/.gitignore
vendored
Normal file
1
common/ui-markets/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
29
common/ui-markets/build.gradle.kts
Normal file
29
common/ui-markets/build.gradle.kts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.common.ui.markets"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Project - Core */
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Project - Common */
|
||||
implementation(projects.common.uiCharts)
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.models)
|
||||
|
||||
implementation(deps.lifecycle.compose)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.ui.utils)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.common.ui.markets
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun MarketChartLoadingError(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.padding(vertical = 32.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_unable_to_load),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = TextReference.Res(R.string.try_to_load_data_again_button_title),
|
||||
onClick = onRetryClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.feed.ui.market.components
|
||||
package com.tangem.common.ui.markets
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -19,6 +19,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import com.tangem.common.ui.charts.MarketChartMini
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.markets.preview.MarketChartListItemPreviewDataProvider
|
||||
import com.tangem.common.ui.tokens.TokenPriceText
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
|
|
@ -32,13 +34,11 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.MarketsTestTags
|
||||
import com.tangem.core.ui.windowsize.WindowSizeType
|
||||
import com.tangem.features.feed.ui.market.preview.MarketChartListItemPreviewDataProvider
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
internal fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
|
||||
fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
|
||||
MarketsListItemContent(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.feed.ui.market.components
|
||||
package com.tangem.common.ui.markets
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.feed.ui.market.state
|
||||
package com.tangem.common.ui.markets.models
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
package com.tangem.features.feed.ui.market.preview
|
||||
package com.tangem.common.ui.markets.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemUM>(
|
||||
class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemUM>(
|
||||
collection = listOf(
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
|
|
@ -22,6 +22,7 @@ import com.tangem.core.ui.components.block.TangemBlockCardColors
|
|||
import com.tangem.core.ui.components.label.Label
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
|
@ -87,7 +88,7 @@ private fun TrendingArticle(articleConfigUM: ArticleConfigUM) {
|
|||
|
||||
ArticleInfo(
|
||||
score = articleConfigUM.score,
|
||||
createdAt = articleConfigUM.createdAt,
|
||||
createdAt = articleConfigUM.createdAt.resolveReference(),
|
||||
)
|
||||
|
||||
SpacerH(32.dp)
|
||||
|
|
@ -104,7 +105,7 @@ private fun DefaultArticle(articleConfigUM: ArticleConfigUM) {
|
|||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
ArticleInfo(
|
||||
score = articleConfigUM.score,
|
||||
createdAt = articleConfigUM.createdAt,
|
||||
createdAt = articleConfigUM.createdAt.resolveReference(),
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
|
|
@ -182,7 +183,7 @@ private fun ArticleCardsPreview() {
|
|||
id = 1,
|
||||
title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)",
|
||||
score = 9.5f,
|
||||
createdAt = "1h ago",
|
||||
createdAt = TextReference.Str("1h ago"),
|
||||
isTrending = true,
|
||||
tags = tags,
|
||||
isViewed = false,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
package com.tangem.common.ui.news
|
||||
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
data class ArticleConfigUM(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val score: Float,
|
||||
val createdAt: String,
|
||||
val createdAt: TextReference,
|
||||
val isTrending: Boolean,
|
||||
val tags: ImmutableSet<LabelUM>,
|
||||
val isViewed: Boolean,
|
||||
|
|
|
|||
|
|
@ -849,6 +849,9 @@
|
|||
<string name="no_trustline_xlm_asset">Das Zielkonto verfügt nicht über eine Vertrauensstellung für das gesendete Asset.</string>
|
||||
<string name="notification_black_friday_text">Sicher Dir 10 $ in BTC mit jeder Wallet greif schnell zu!</string>
|
||||
<string name="notification_black_friday_title">Black Friday: bis zu 30% Nachlass</string>
|
||||
<string name="notification_one_plus_one_button">Los geht\'s</string>
|
||||
<string name="notification_one_plus_one_text">Nur für kurze Zeit!</string>
|
||||
<string name="notification_one_plus_one_title">1+1: Beim Kauf einer Wallet erhältst Du 50% Rabatt</string>
|
||||
<string name="notification_referral_promo_button">Jetzt beitreten</string>
|
||||
<string name="notification_referral_promo_text">Teile Deinen Code – verdiene 5 USDT pro Verkauf. Deine Freunde erhalten 10 % Rabatt.</string>
|
||||
<string name="notification_referral_promo_title">Erhalte BELOHNUNGEN für jeden Freund!</string>
|
||||
|
|
|
|||
|
|
@ -848,8 +848,8 @@
|
|||
<string name="notification_black_friday_text">ウォレットごとに$10相当のBTCをプレゼント\nお早めに!</string>
|
||||
<string name="notification_black_friday_title">ブラックフライデー:最大30% オフ</string>
|
||||
<string name="notification_one_plus_one_button">今すぐチェック</string>
|
||||
<string name="notification_one_plus_one_text">理想のTangemセットを揃えよう。期間限定。</string>
|
||||
<string name="notification_one_plus_one_title">1+1:1つ購入で、2つ目が50%オフ</string>
|
||||
<string name="notification_one_plus_one_text">期間限定!</string>
|
||||
<string name="notification_one_plus_one_title">1+1:ウォレット1つ購入で、2つ目が50%オフ</string>
|
||||
<string name="notification_referral_promo_button">今すぐ参加</string>
|
||||
<string name="notification_referral_promo_text">コードを共有すると、販売ごとに5 USDTを獲得できます。お友達は10%割引になります。</string>
|
||||
<string name="notification_referral_promo_title">友達への紹介で報酬を獲得しよう!</string>
|
||||
|
|
@ -1391,7 +1391,7 @@
|
|||
<string name="tangem_pay_explore_transaction">取引を表示</string>
|
||||
<string name="tangem_pay_fee_subtitle">サービス手数料</string>
|
||||
<string name="tangem_pay_fee_title">手数料</string>
|
||||
<string name="tangem_pay_freeze_card_alert_body">カードを紛失したり盗まれたりしても、資金を安全に保てます。一時停止はいつでも解除できます。</string>
|
||||
<string name="tangem_pay_freeze_card_alert_body">資金を安全に保管します。凍結はいつでも解除できます。</string>
|
||||
<string name="tangem_pay_freeze_card_alert_title">カードを一時停止しますか?</string>
|
||||
<string name="tangem_pay_freeze_card_failed">カードを凍結できませんでした。しばらくしてからもう一度お試しください。</string>
|
||||
<string name="tangem_pay_freeze_card_freeze">一時停止</string>
|
||||
|
|
|
|||
|
|
@ -16,8 +16,10 @@
|
|||
<string name="access_code_navtitle">Код доступа</string>
|
||||
<string name="account_add_limit_dialog_description">Превышен лимит %1$s аккаунтов. Архивируйте, чтобы создать новый.</string>
|
||||
<string name="account_add_limit_dialog_title">Невозможно добавить новый аккаунт.</string>
|
||||
<string name="account_archive_success_message">Аккаунт заархивирован</string>
|
||||
<string name="account_archived_accounts">Архивированные аккаунты</string>
|
||||
<string name="account_archived_recover">Восстановить</string>
|
||||
<string name="account_archived_recover_dialog_description">Вы собираетесь восстановить \"%1$s\".</string>
|
||||
<string name="account_archived_recover_dialog_title">Восстановить аккаунт</string>
|
||||
<string name="account_archived_recover_error_message">Лимит в 20 активных аккаунтов достигнут. Пожалуйста, зархивируйте один аккаунт, чтобы продолжить.</string>
|
||||
<string name="account_archived_recover_error_title">Невозможно восстановить аккаунт</string>
|
||||
|
|
@ -26,12 +28,14 @@
|
|||
<string name="account_could_not_archive_referral_program_message">Этот аккаунт участвует в реферальной программе</string>
|
||||
<string name="account_could_not_archive_referral_program_title">Этот аккаунт не может быть архивирован.</string>
|
||||
<string name="account_could_not_create">Мы не смогли создать аккаунт. Пожалуйста, попробуйте позже.</string>
|
||||
<string name="account_create_success_message">Аккаунт создан</string>
|
||||
<string name="account_details_archive">Архивный аккаунт</string>
|
||||
<string name="account_details_archive_action">Архив</string>
|
||||
<string name="account_details_archive_description">Вы архивируете свой аккаунт, но в любое время можете вернуть его обратно</string>
|
||||
<string name="account_details_archiving">Архивация...</string>
|
||||
<string name="account_details_title">Аккаунт</string>
|
||||
<string name="account_edit_failure_dialog_title">Невозможно отредактировать аккаунт</string>
|
||||
<string name="account_edit_success_message">Аккаунт сохранен</string>
|
||||
<string name="account_for_rewards">Аккаунт для наград</string>
|
||||
<string name="account_form_create_button">Добавить аккаунт</string>
|
||||
<string name="account_form_edit_button">Сохранить</string>
|
||||
|
|
@ -46,6 +50,8 @@
|
|||
<string name="account_main_account_title">Основной аккаунт</string>
|
||||
<string name="account_recover_limit_dialog_description">Лимит %1$s активных аккаунтов превышен. Архивируйте один, чтобы продолжить.</string>
|
||||
<string name="account_recover_limit_dialog_title">Невозможно восстановить аккаунт</string>
|
||||
<string name="account_recover_success_message">Аккаунт восстановлен</string>
|
||||
<string name="account_reorder_description">Нажмите и удерживайте аккаунт, чтобы изменить порядок аккаунтов.</string>
|
||||
<string name="account_unsaved_dialog_action_first">Продолжить</string>
|
||||
<string name="account_unsaved_dialog_action_second">Отменить</string>
|
||||
<string name="account_unsaved_dialog_message_create">Вы уверены, что хотите создание нового аккаунта?</string>
|
||||
|
|
@ -87,14 +93,17 @@
|
|||
<string name="alert_manage_tokens_unsupported_message">Токены в сети %1$s не поддерживаются этой картой или кольцом из-за ограничений прошивки.</string>
|
||||
<string name="alert_troubleshooting_scan_card_title">У вас возникли трудности со сканированием карты или кольца?</string>
|
||||
<string name="alert_unsupported_card">Эта карта не предназначена для работы с этим приложением</string>
|
||||
<string name="app_settings_access_code_warning">Сначала установите код доступа, чтобы включить биометрию.</string>
|
||||
<string name="app_settings_biometrics_footer">Используйте %1$s, чтобы быстро и безопасно разблокировать кошелёк и выполнять чувствительные действия, например, подписывать транзакции. Для аппаратных кошельков всё ещё требуется карта или кольцо для подписи.</string>
|
||||
<string name="app_settings_default_fee">Комиссия по-умолчанию</string>
|
||||
<string name="app_settings_default_fee_footer">Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться.</string>
|
||||
<string name="app_settings_enable_biometrics_description">Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem</string>
|
||||
<string name="app_settings_enable_biometrics_title">Включите биометрическую аутентификацию</string>
|
||||
<string name="app_settings_off_biometrics_alert_message">Отключение %1$s потребует ввода вашего кода доступа для разблокировки приложения и работы с кошельком.</string>
|
||||
<string name="app_settings_off_require_access_code_alert_message">Позже будет необходимо ввести код доступа к вашему кошельку, чтобы мы могли безопасно сохранить его для будущего использования.</string>
|
||||
<string name="app_settings_off_saved_access_code_alert_message">Все сохраненные коды доступа будут удалены. Вам потребуется вводить код доступа при работе с кошельком.</string>
|
||||
<string name="app_settings_off_saved_wallet_alert_message">При отключении функции сохранения кошелька все ранее сохраненные кошельки будут удалены из приложения.</string>
|
||||
<string name="app_settings_on_require_access_code_alert_message">Это удалит все сохранённые коды доступа к кошелькам. Любое дальнейшее взаимодействие с кошельком будет требовать ввода кода доступа.</string>
|
||||
<string name="app_settings_require_access_code">Требуется код доступа</string>
|
||||
<string name="app_settings_require_access_code_footer">Эта опция отключает использование биометрии для выполнения чувствительных действий. Каждый раз, например при подписании транзакции, вам потребуется вводить код доступа.</string>
|
||||
<string name="app_settings_saved_access_codes">Сохранение кода доступа</string>
|
||||
|
|
@ -144,6 +153,7 @@
|
|||
<string name="biometric_lockout_warning_description_2">Вход по биометрии временно заблокирован. Попробуйте снова через 30 секунд или разблокируйте кошелёк с помощью прикладывания устройства или кода доступа.</string>
|
||||
<string name="biometric_lockout_warning_title">Слишком много попыток</string>
|
||||
<string name="biometric_unavailable_warning">Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона.</string>
|
||||
<string name="biometric_updated_warning_description">Биометрические данные на вашем устройстве были обновлены. Пожалуйста, выберите свой кошелёк и введите его код доступа, чтобы повторно включить вход по биометрии.</string>
|
||||
<string name="biometric_updated_warning_title">Внимание</string>
|
||||
<string name="bitcoin_promo_activation_error">При обработке промокода произошла ошибка. Пожалуйста, попробуйте позже.</string>
|
||||
<string name="bitcoin_promo_activation_error_title">Ошибка активации</string>
|
||||
|
|
@ -299,6 +309,7 @@
|
|||
<string name="common_later">Позже</string>
|
||||
<string name="common_learn_more">Узнать больше</string>
|
||||
<string name="common_left">Осталось %1$s</string>
|
||||
<string name="common_legacy_bitcoin_address">Легаси Bitcoin</string>
|
||||
<string name="common_locked">Заблокирован</string>
|
||||
<string name="common_locked_wallets">Заблокированные кошельки</string>
|
||||
<string name="common_main_network">Основная сеть</string>
|
||||
|
|
@ -311,6 +322,8 @@
|
|||
<item quantity="many">%d сетей</item>
|
||||
<item quantity="other">%d сети</item>
|
||||
</plurals>
|
||||
<string name="common_new_address">Новый адрес</string>
|
||||
<string name="common_news">Новости</string>
|
||||
<string name="common_next">Далее</string>
|
||||
<string name="common_nft">NFT</string>
|
||||
<string name="common_no">Нет</string>
|
||||
|
|
@ -363,6 +376,7 @@
|
|||
<string name="common_support">Поддержка</string>
|
||||
<string name="common_supported_networks">Поддерживаемые сети</string>
|
||||
<string name="common_swap">Обменять</string>
|
||||
<string name="common_tangem">Tangem</string>
|
||||
<string name="common_terms_and_conditions">условия участия</string>
|
||||
<string name="common_terms_of_use">Условиями использования</string>
|
||||
<string name="common_to">На</string>
|
||||
|
|
@ -391,6 +405,7 @@
|
|||
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
|
||||
<string name="currency_subtitle_expanded">Доступные сети</string>
|
||||
<string name="custom_token_another_account_dialog_description">Деривация вашего токена совпадает с деривацией %1$s. Токен будет добавлен к этой учётной записи.</string>
|
||||
<string name="custom_token_another_account_dialog_title">Деривация принадлежит другому аккаунту</string>
|
||||
<string name="custom_token_another_account_snackbar_text">Токен добавлен в аккаунт %1$s</string>
|
||||
<string name="custom_token_contract_address_input_title">Адрес контракта</string>
|
||||
<string name="custom_token_creation_error_invalid_contract_address">Адрес контракта некорректен</string>
|
||||
|
|
@ -527,6 +542,8 @@
|
|||
<string name="express_transaction_id">ID: %s</string>
|
||||
<string name="express_transaction_id_copied">ID транзакции скопирован</string>
|
||||
<string name="exсhange_token_description">Обменяйте любой актив в вашем портфеле на этот токен</string>
|
||||
<string name="feed_market_and_news">Рынок и Новости</string>
|
||||
<string name="feed_trending_now">В тренде</string>
|
||||
<string name="feedback_data_collection_message">Информация ниже не является обязательной. Вы можете стереть её, если хотите.</string>
|
||||
<string name="feedback_preface_rate_negative">Расскажите, каких функций вам не хватает, и мы постараемся вам помочь.</string>
|
||||
<string name="feedback_preface_scan_failed">Скажите, пожалуйста, какая у вас карта или кольцо?</string>
|
||||
|
|
@ -687,6 +704,7 @@
|
|||
<string name="markets_apy_placeholder">APY %s</string>
|
||||
<string name="markets_common_my_portfolio">Мой портфель</string>
|
||||
<string name="markets_common_title">Рынок</string>
|
||||
<string name="markets_earn_common_title">Зарабатывай с Tangem</string>
|
||||
<string name="markets_generate_addresses_notification">Чтобы создать адреса для выбранных сетей, отсканируйте вашу карту или кольцо Tangem кошелька</string>
|
||||
<string name="markets_hint">Потяните вверх или коснитесь поисковой строки, чтобы добавить токен</string>
|
||||
<string name="markets_insights_info_description_message">Данные раздела получены из следующих сетей: %s</string>
|
||||
|
|
@ -791,6 +809,8 @@
|
|||
<string name="markets_yield_supply_banner_title">Активировать режим доходности</string>
|
||||
<string name="mobile_wallet_requires_min_os_warning_body">Обновитесь до версии %1$s, чтобы создать мобильный кошелёк</string>
|
||||
<string name="mobile_wallet_requires_min_os_warning_title">Мобильный кошелек требует %1$s или новее.</string>
|
||||
<string name="news_all_news">Все новости</string>
|
||||
<string name="news_stay_in_the_loop">Будьте в курсе</string>
|
||||
<string name="nfc_error_unavailable">Функция NFC недоступна на вашем устройстве</string>
|
||||
<string name="nft_about_title">О NFT</string>
|
||||
<string name="nft_asset">NFT</string>
|
||||
|
|
@ -1198,6 +1218,7 @@
|
|||
<string name="send_tron_account_activation_error">%1$s — это монета в сети Tron. Чтобы рассчитать комиссию и совершить транзакцию, вам необходимо внести немного Tron (TRX) на свой адрес.</string>
|
||||
<string name="send_validation_amount_exceeds_balance">Сумма превышает остаток</string>
|
||||
<string name="send_validation_destination_tag_required_description">Для завершения транзакции на указанный адрес необходим тег назначения (мемо).</string>
|
||||
<string name="send_validation_destination_tag_required_title">Требуется Destination Tag</string>
|
||||
<string name="send_validation_invalid_amount">Недопустимая сумма</string>
|
||||
<string name="send_validation_invalid_fee">Комиссия превышает остаток</string>
|
||||
<string name="send_validation_invalid_total">Отправляемая сумма превышает остаток</string>
|
||||
|
|
@ -1225,6 +1246,8 @@
|
|||
<string name="staking_account_initialization_footer">Сетевая комиссия — это небольшая плата за обработку и подтверждение вашей транзакции в блокчейне.</string>
|
||||
<string name="staking_account_initialization_message">Чтобы начать стейкинг, ваш TON-аккаунт должен быть активирован транзакцией самому себе на 1 TON. Средства остаются в вашем кошельке — этот шаг лишь активирует ваш аккаунт для участия в стейкинге.</string>
|
||||
<string name="staking_account_initialization_title">Активация аккаунта</string>
|
||||
<string name="staking_alert_network_fee_updated_message">Сетевая комиссия изменилась. Пожалуйста, проверьте новую сумму перед продолжением.</string>
|
||||
<string name="staking_alert_network_fee_updated_title">Комиссия сети обновлена</string>
|
||||
<string name="staking_amount_requirement_error">Сумма для стейкинга должна быть не менее %s</string>
|
||||
<string name="staking_amount_tron_integer_error">Согласно правилам сети, сумма стейкинга будет округлена до %1$sTRX.</string>
|
||||
<string name="staking_amount_tron_integer_error_unstaking">Сумма для вывода из стейкинга будет округлена до %1$s TRX ввиду особенностей сети.</string>
|
||||
|
|
@ -1263,6 +1286,7 @@
|
|||
<string name="staking_give_permission_fee_footer">Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для стейкинга.</string>
|
||||
<string name="staking_legal">Пользуясь стейкинг сервисом, вы соглашаетесь с %1$s и %2$s</string>
|
||||
<string name="staking_locked">Заблокировано</string>
|
||||
<string name="staking_max_amount_requirement_error">Максимальная сумму: %s</string>
|
||||
<string name="staking_migrate">Переместить</string>
|
||||
<string name="staking_native">Нативный стейкинг</string>
|
||||
<string name="staking_no_validators_error_message">В данный момент нет доступных активных валидаторов для стейкинга. Пожалуйста, попробуйте позже.</string>
|
||||
|
|
@ -1426,7 +1450,7 @@
|
|||
<string name="tangem_pay_unfreeze_card_alert_title">Разморозить карту?</string>
|
||||
<string name="tangem_pay_unfreeze_card_failed">Не удалось разморозить карту, попробуйте еще раз</string>
|
||||
<string name="tangem_pay_unfreeze_card_success">Карта разморожена</string>
|
||||
<string name="tangem_pay_withdrawal">Вывод</string>
|
||||
<string name="tangem_pay_withdrawal">Вывести</string>
|
||||
<string name="tangempay_card_details_add_funds">Пополнить</string>
|
||||
<string name="tangempay_card_details_add_funds_subtitle">Способы пополнения</string>
|
||||
<string name="tangempay_card_details_card_number">Номер</string>
|
||||
|
|
@ -1462,7 +1486,7 @@
|
|||
<string name="tangempay_card_details_swap_description">Пополните карту любым активом через обмен </string>
|
||||
<string name="tangempay_card_details_title">Реквизиты</string>
|
||||
<string name="tangempay_card_details_unfreeze_card">Разморозить карту</string>
|
||||
<string name="tangempay_card_details_withdraw">Вывод</string>
|
||||
<string name="tangempay_card_details_withdraw">Вывести</string>
|
||||
<string name="tangempay_card_details_withdraw_error_title">Вывод сейчас недоступен</string>
|
||||
<string name="tangempay_card_details_withdraw_in_progress_description">Вы не можете начать обмен или новый вывод, пока не завершится текущий.</string>
|
||||
<string name="tangempay_card_details_withdraw_in_progress_title">Вывод выполняется</string>
|
||||
|
|
@ -1598,6 +1622,7 @@
|
|||
<string name="visa_onboarding_access_code_navigation_title">Код доступа</string>
|
||||
<string name="visa_onboarding_account_activation_navigation_title">Активация аккаунта</string>
|
||||
<string name="visa_onboarding_pin_not_accepted">ПИН не принят. Попробуйте ещё раз или введите другой код.</string>
|
||||
<string name="visa_unlock_notification_button">Разблокировать</string>
|
||||
<string name="wallet_add_common_title">Выберите способ добавления кошелька</string>
|
||||
<string name="wallet_add_hardware_description">Отсканируйте вашу карту или кольцо Tangem, чтобы восстановить её или импортировать из другого кошелька.</string>
|
||||
<string name="wallet_add_hardware_info_create">Создать аппаратный кошелёк</string>
|
||||
|
|
@ -1934,6 +1959,7 @@
|
|||
<string name="yield_module_fee_policy_sheet_title">Политика комиссий за пополнение</string>
|
||||
<string name="yield_module_fee_policy_tangem_service_fee_title">Tangem взимает комиссию за обслуживание в размере 15% от полученного дохода.</string>
|
||||
<string name="yield_module_high_fee_error">Ваши средства будут автоматически переведены в Aave, как только комиссия сети снизится или баланс достигнет минимально необходимой суммы.</string>
|
||||
<string name="yield_module_high_network_fees_notification_title">Высокая сетевая комиссия</string>
|
||||
<string name="yield_module_historical_returns">Историческая доходность</string>
|
||||
<string name="yield_module_main_screen_promo_banner_message">Получай до %1$s APY на свой баланс</string>
|
||||
<string name="yield_module_main_view_approve_notification_description">Разрешение для вашего токена в режиме доходности было отозвано. Откройте токен, чтобы выдать разрешение снова.</string>
|
||||
|
|
|
|||
|
|
@ -584,30 +584,30 @@
|
|||
<string name="hw_activation_need_title">Finalize wallet setup</string>
|
||||
<string name="hw_activation_need_warning_description">Complete setup by securing the app with an access code.</string>
|
||||
<string name="hw_backup_alert_description">If you do, you\'ll need to start over.</string>
|
||||
<string name="hw_backup_alert_title">Are you sure you want to quit the activation process?</string>
|
||||
<string name="hw_backup_banner_description">Keeps your crypto safe and offline. Slim as a credit card, safer than a bank vault.</string>
|
||||
<string name="hw_backup_alert_title">Are you sure you want to quit the setup process?</string>
|
||||
<string name="hw_backup_banner_description">Keeps your crypto safe and offline. Slim as a credit card, safer than a bank vault.</string>
|
||||
<string name="hw_backup_close_description">If you do, you\'ll need to start over.</string>
|
||||
<string name="hw_backup_google_drive_description">Recover existing wallet via Google Drive backup</string>
|
||||
<string name="hw_backup_google_drive_title">Google Drive backup</string>
|
||||
<string name="hw_backup_hardware_create_description">Create a new secure wallet and transfer your funds for extra protection.</string>
|
||||
<string name="hw_backup_hardware_create_description">Create a secure wallet and transfer your funds for extra protection.</string>
|
||||
<string name="hw_backup_hardware_create_title">Create new wallet</string>
|
||||
<string name="hw_backup_hardware_description">Level up your security with the superior Tangem hardware wallet.</string>
|
||||
<string name="hw_backup_hardware_title">Hardware Wallet</string>
|
||||
<string name="hw_backup_hardware_upgrade_description">Move your current wallet into Tangem Wallet.</string>
|
||||
<string name="hw_backup_hardware_upgrade_description">Move your current wallet into Tangem.</string>
|
||||
<string name="hw_backup_hardware_upgrade_title">Upgrade current wallet</string>
|
||||
<string name="hw_backup_need_action">Go to backup</string>
|
||||
<string name="hw_backup_need_description">Please back up your wallet before creating an access code.</string>
|
||||
<string name="hw_backup_need_finish_first">Finish Backup First</string>
|
||||
<string name="hw_backup_need_finish_first">Finish backup first</string>
|
||||
<string name="hw_backup_need_title">Finalize backup first</string>
|
||||
<string name="hw_backup_no_backup">Incomplete</string>
|
||||
<string name="hw_backup_section_other_title">Other methods</string>
|
||||
<string name="hw_backup_seed_description">Manually save your recovery phrase in a secure place and keep it private to protect your funds.</string>
|
||||
<string name="hw_backup_seed_description">Save your recovery phrase in a secure place and keep it private to protect your funds.</string>
|
||||
<string name="hw_backup_seed_title">Recovery phrase</string>
|
||||
<string name="hw_backup_to_secure_description">To secure your wallet with an Access Code, complete the backup first.</string>
|
||||
<string name="hw_backup_to_upgrade_description">To upgrade your wallet to hardware, back it up first.</string>
|
||||
<string name="hw_backup_to_upgrade_description">To upgrade to a hardware wallet, complete the backup process.</string>
|
||||
<string name="hw_create_keys_description">Your private keys are securely encrypted and stored on your phone</string>
|
||||
<string name="hw_create_keys_title">Keys are stored in the app</string>
|
||||
<string name="hw_create_seed_description">Create or restore your wallet using a recovery phrase — your built-in backup.</string>
|
||||
<string name="hw_create_keys_title">Private keys stay on your device</string>
|
||||
<string name="hw_create_seed_description">Create or restore your wallet with a recovery phrase.</string>
|
||||
<string name="hw_create_seed_title">Seed phrase backup</string>
|
||||
<string name="hw_create_title">Create Mobile Wallet</string>
|
||||
<string name="hw_import_existing_wallet">Import existing wallet</string>
|
||||
|
|
@ -620,13 +620,13 @@
|
|||
<string name="hw_remove_wallet_notification_action_backup_go">Go to backup</string>
|
||||
<string name="hw_remove_wallet_notification_action_backup_view">View backup</string>
|
||||
<string name="hw_remove_wallet_notification_action_forget">Forget wallet</string>
|
||||
<string name="hw_remove_wallet_notification_action_forget_anyway">Forget Anyway</string>
|
||||
<string name="hw_remove_wallet_notification_description_has_backup">A backup for this wallet exists. Review it before forgetting to make sure you can recover later.</string>
|
||||
<string name="hw_remove_wallet_notification_description_without_backup">If you forget this wallet without a backup, you’ll permanently lose access to your funds.</string>
|
||||
<string name="hw_remove_wallet_notification_title">Forget this wallet?</string>
|
||||
<string name="hw_remove_wallet_notification_action_forget_anyway">Forget anyway</string>
|
||||
<string name="hw_remove_wallet_notification_description_has_backup">This wallet has a backup. Ensure you can recover it before forgetting the wallet.</string>
|
||||
<string name="hw_remove_wallet_notification_description_without_backup">If you forget this wallet without a backup, you\'ll permanently lose access to your funds.</string>
|
||||
<string name="hw_remove_wallet_notification_title">Are you sure you want to forget this wallet?</string>
|
||||
<string name="hw_remove_wallet_warning_access">I understand that if I haven\'t backed up my wallet before removing it, I may lose access to it.</string>
|
||||
<string name="hw_remove_wallet_warning_device">I understand that removing my wallet does not delete it—but simply removes it from my device.</string>
|
||||
<string name="hw_upgrade_backup_description">No seed phrase is needed anymore. Your Tangem card or ring becomes your secure backup.</string>
|
||||
<string name="hw_remove_wallet_warning_device">I understand that removing my wallet does not delete it, only removes it from my device.</string>
|
||||
<string name="hw_upgrade_backup_description">Seed phrase not required. Your Tangem card or ring becomes your secure backup.</string>
|
||||
<string name="hw_upgrade_backup_title">Backup with Tangem</string>
|
||||
<string name="hw_upgrade_error_card_already_has_wallet">Can’t upgrade. A wallet already exists on this device.</string>
|
||||
<string name="hw_upgrade_error_card_key_import">Pick another device. This one can’t be used for the upgrade.</string>
|
||||
|
|
@ -639,7 +639,7 @@
|
|||
<string name="hw_upgrade_key_migration_title">Key Migration</string>
|
||||
<string name="hw_upgrade_scan_device">Scan device</string>
|
||||
<string name="hw_upgrade_start_action">Start upgrade</string>
|
||||
<string name="hw_upgrade_start_description">You’re about to upgrade to our hardware wallet. This will keep your assets safe in cold storage.</string>
|
||||
<string name="hw_upgrade_start_description">You\'re about to upgrade to our hardware wallet. It will keep your assets safe in cold storage.</string>
|
||||
<string name="hw_upgrade_start_title">Tangem Wallet</string>
|
||||
<string name="hw_upgrade_title">Upgrade to Hardware Wallet</string>
|
||||
<string name="hw_upgrade_to_cold_banner_description">Keep your crypto safe with Tangem\'s top-tier hardware wallet.</string>
|
||||
|
|
@ -726,6 +726,7 @@
|
|||
<string name="markets_sort_by_top_gainers_title">Top Gainers</string>
|
||||
<string name="markets_sort_by_top_losers_title">Top Losers</string>
|
||||
<string name="markets_sort_by_trending_title">Trending</string>
|
||||
<string name="markets_sort_by_yield_mode_title">Yield Mode</string>
|
||||
<string name="markets_staking_banner_description_placeholder">Staking is the easiest way to receive rewards on your crypto. %s</string>
|
||||
<string name="markets_staking_banner_title">Earn up to %s APY</string>
|
||||
<string name="markets_token_added">Token Added</string>
|
||||
|
|
@ -801,7 +802,7 @@
|
|||
<string name="mobile_wallet_requires_min_os_warning_title">Mobile Wallet requires %1$s or later</string>
|
||||
<string name="news_all_news">All news</string>
|
||||
<plurals name="news_published_hours_ago">
|
||||
<item quantity="one"></item>
|
||||
<item quantity="one">%dh ago</item>
|
||||
<item quantity="other">%dh ago</item>
|
||||
</plurals>
|
||||
<plurals name="news_published_minutes_ago">
|
||||
|
|
@ -983,6 +984,7 @@
|
|||
<string name="onramp_currency_other">Other currencies</string>
|
||||
<string name="onramp_currency_popular">Popular Fiats</string>
|
||||
<string name="onramp_currency_search">Search by currency</string>
|
||||
<string name="onramp_error_transaction_already_processed">This transaction has already been processed. No further action is required.</string>
|
||||
<string name="onramp_fetching_best_rates">Fetching best rates...</string>
|
||||
<string name="onramp_instant_status">Instant</string>
|
||||
<string name="onramp_legal">By using onramp functionality, you agree with provider’s %1$s and %2$s</string>
|
||||
|
|
|
|||
|
|
@ -66,4 +66,6 @@ dependencies {
|
|||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.core.ui.utils.DateTimeFormatters.dateMMMdd
|
|||
import com.tangem.core.ui.utils.DateTimeFormatters.dateTimeFormatter
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters.dateYYYY
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import org.joda.time.format.DateTimeFormat
|
||||
import org.joda.time.format.DateTimeFormatter
|
||||
import org.joda.time.format.DateTimeFormatterBuilder
|
||||
|
|
@ -98,6 +99,23 @@ object DateTimeFormatters {
|
|||
getBestFormatterBySkeleton("dd.MM.yyyy HH:mm")
|
||||
}
|
||||
|
||||
/**
|
||||
* Local full date formatter (e.g., "dd MMMM, HH:mm")
|
||||
*/
|
||||
val localFullDate: DateTimeFormatter by lazy {
|
||||
DateTimeFormatterBuilder()
|
||||
.appendDayOfMonth(2)
|
||||
.appendLiteral(' ')
|
||||
.appendMonthOfYearText()
|
||||
.appendLiteral(", ")
|
||||
.appendHourOfDay(2)
|
||||
.appendLiteral(':')
|
||||
.appendMinuteOfHour(2)
|
||||
.toFormatter()
|
||||
.withLocale(Locale.getDefault())
|
||||
.withZone(DateTimeZone.getDefault())
|
||||
}
|
||||
|
||||
fun formatDate(date: DateTime, formatter: DateTimeFormatter = dateFormatter): String {
|
||||
return formatter.print(date)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,3 +38,50 @@ fun Long.toTimeFormat(formatter: DateTimeFormatter = DateTimeFormatters.timeForm
|
|||
fun Long.formatAsDateTime(formatter: DateTimeFormatter): String {
|
||||
return DateTimeFormatters.formatDate(date = DateTime(this, DateTimeZone.getDefault()), formatter = formatter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an ISO 8601 date string and compares it to the current UTC time.
|
||||
*
|
||||
|
||||
* @param now The current date to compare against.
|
||||
* @return A [FormattedDate] subclass.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun getFormattedDate(createdAt: String, now: DateTime): FormattedDate {
|
||||
val pastDateUtc = try {
|
||||
DateTime.parse(createdAt)
|
||||
} catch (_: Exception) {
|
||||
return FormattedDate.FullDate(createdAt)
|
||||
}
|
||||
|
||||
val pastDateLocal = pastDateUtc.withZone(DateTimeZone.getDefault())
|
||||
val isToday = pastDateLocal.isToday()
|
||||
|
||||
val diffInMillis = now.millis - pastDateUtc.millis
|
||||
val diffInMinutes = diffInMillis / (1000 * 60)
|
||||
val diffInHours = diffInMillis / (1000 * 60 * 60)
|
||||
|
||||
return when {
|
||||
diffInMinutes < 1 -> FormattedDate.MinutesAgo(1)
|
||||
diffInMinutes < 60 -> FormattedDate.MinutesAgo(diffInMinutes.toInt())
|
||||
diffInHours < 12 && isToday -> FormattedDate.HoursAgo(diffInHours.toInt())
|
||||
isToday -> {
|
||||
val timeString = DateTimeFormatters.timeFormatter.print(pastDateLocal)
|
||||
FormattedDate.Today(timeString)
|
||||
}
|
||||
else -> {
|
||||
val dateString = DateTimeFormatters.localFullDate.print(pastDateLocal)
|
||||
FormattedDate.FullDate(dateString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Representing different formatted date representations.
|
||||
*/
|
||||
sealed class FormattedDate {
|
||||
data class MinutesAgo(val minutes: Int) : FormattedDate()
|
||||
data class HoursAgo(val hours: Int) : FormattedDate()
|
||||
data class Today(val time: String) : FormattedDate()
|
||||
data class FullDate(val date: String) : FormattedDate()
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DateUtilsTest {
|
||||
|
||||
private lateinit var defaultTimeZone: DateTimeZone
|
||||
|
||||
private val now = createDateTime(day = 14, hour = 12)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
defaultTimeZone = DateTimeZone.getDefault()
|
||||
DateTimeZone.setDefault(DateTimeZone.forID("Europe/Moscow"))
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
DateTimeZone.setDefault(defaultTimeZone)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return MinutesAgo when difference is less than 1 minute`() {
|
||||
val pastDate = now.minusSeconds(30)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return MinutesAgo when difference is 1 minute`() {
|
||||
val pastDate = now.minusMinutes(1)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return MinutesAgo when difference is 30 minutes`() {
|
||||
val pastDate = now.minusMinutes(30)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(30)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return MinutesAgo when difference is 59 minutes`() {
|
||||
val pastDate = now.minusMinutes(59).minusSeconds(59)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(59)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return HoursAgo when difference is exactly 1 hour`() {
|
||||
val pastDate = now.minusHours(1)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.HoursAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.HoursAgo).hours).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return HoursAgo when difference is 11 hours`() {
|
||||
val pastDate = now.minusHours(11)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.HoursAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.HoursAgo).hours).isEqualTo(11)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return Today when difference is exactly 12 hours`() {
|
||||
val pastDate = createDateTime(day = 14, hour = 0)
|
||||
val createdAt = pastDate.toString()
|
||||
val nowInTest = createDateTime(day = 14, hour = 12)
|
||||
val result = getFormattedDate(createdAt, nowInTest)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.Today::class.java)
|
||||
Truth.assertThat((result as FormattedDate.Today).time).isEqualTo("03:00")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return FullDate when difference is 18 hours and past date is another day`() {
|
||||
val pastDate = createDateTime(day = 13, hour = 18)
|
||||
val createdAt = pastDate.toString()
|
||||
val nowInTest = createDateTime(day = 14, hour = 12)
|
||||
val result = getFormattedDate(createdAt, nowInTest)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return FullDate when difference is exactly 24 hours`() {
|
||||
val pastDate = now.minusDays(1)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return FullDate when difference is 2 days`() {
|
||||
val pastDate = now.minusDays(2)
|
||||
val createdAt = pastDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return FullDate with original string when format has wrong date separator`() {
|
||||
val wrongFormat = "2025/10/14T12:00:00.000Z"
|
||||
val result = getFormattedDate(wrongFormat, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java)
|
||||
Truth.assertThat((result as FormattedDate.FullDate).date).isEqualTo(wrongFormat)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle future dates correctly`() {
|
||||
val futureDate = now.plusHours(1)
|
||||
val createdAt = futureDate.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle edge case of exactly 0 milliseconds difference`() {
|
||||
val createdAt = now.toString()
|
||||
val result = getFormattedDate(createdAt, now)
|
||||
Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java)
|
||||
Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1)
|
||||
}
|
||||
|
||||
private fun createDateTime(day: Int, hour: Int): DateTime {
|
||||
return DateTime(
|
||||
/* year = */ 2025,
|
||||
/* monthOfYear = */ 10,
|
||||
/* dayOfMonth = */ day,
|
||||
/* hourOfDay = */ hour,
|
||||
/* minuteOfHour = */ 0,
|
||||
/* secondOfMinute = */ 0,
|
||||
/* millisOfSecond = */ 0,
|
||||
/* zone = */ DateTimeZone.UTC,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.domain.markets
|
||||
|
||||
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||
|
||||
class GetTopFiveMarketTokenUseCase(
|
||||
private val marketsTokenRepository: MarketsTokenRepository,
|
||||
) {
|
||||
operator fun invoke(
|
||||
batchingContext: TokenListBatchingContext,
|
||||
order: TokenMarketListConfig.Order,
|
||||
): TokenListBatchFlow {
|
||||
return marketsTokenRepository.getTokenListFlow(
|
||||
batchingContext = batchingContext,
|
||||
firstBatchSize = DEFAULT_BATCH_SIZE,
|
||||
nextBatchSize = NEXT_BATCH_SIZE,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_BATCH_SIZE = 5
|
||||
private const val NEXT_BATCH_SIZE = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -93,6 +93,7 @@ dependencies {
|
|||
implementation(projects.common.ui)
|
||||
implementation(projects.common.uiCharts)
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.common.uiMarkets)
|
||||
|
||||
/* Libs */
|
||||
implementation(projects.libs.crypto)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ComponentModule {
|
||||
internal interface FeedComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
package com.tangem.features.feed.model.converter
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.*
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class MarketsTokenItemConverter(
|
||||
private val currentTrendInterval: MarketsListUM.TrendInterval,
|
||||
private val appCurrency: AppCurrency,
|
||||
) : Converter<TokenMarket, MarketsListItemUM> {
|
||||
|
||||
private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(shouldFormatAxis = false)
|
||||
|
||||
override fun convert(value: TokenMarket): MarketsListItemUM {
|
||||
return MarketsListItemUM(
|
||||
id = value.id,
|
||||
name = value.name,
|
||||
currencySymbol = value.symbol,
|
||||
ratingPosition = value.marketRating?.toString(),
|
||||
marketCap = value.getMarketCap(),
|
||||
iconUrl = value.imageUrlLarge,
|
||||
price = value.getCurrentPrice(),
|
||||
trendPercentText = value.getTrendPercent(),
|
||||
trendType = value.getTrendType(),
|
||||
chartData = value.getChartData(),
|
||||
isUnder100kMarketCap = value.isUnderMarketCapLimit,
|
||||
stakingRate = value.stakingRate?.format { percent() }?.let {
|
||||
resourceReference(R.string.markets_apy_placeholder, wrappedList(it))
|
||||
},
|
||||
updateTimestamp = value.updateTimestamp,
|
||||
)
|
||||
}
|
||||
|
||||
fun update(prev: TokenMarket, prevUI: MarketsListItemUM, new: TokenMarket): MarketsListItemUM {
|
||||
require(prev.id == new.id) {
|
||||
"Ids is not the same during update TokenMarket item: previousItem[${prev.id}] != newItem[${new.id}]"
|
||||
}
|
||||
|
||||
return prevUI.copy(
|
||||
name = new.name,
|
||||
currencySymbol = new.symbol,
|
||||
ratingPosition = new.marketRating?.toString(),
|
||||
marketCap = ifChanged(prev.marketCap, new.marketCap, prevUI.marketCap) { new.getMarketCap() },
|
||||
iconUrl = new.imageUrlLarge,
|
||||
price = ifChanged(prev = prev.tokenQuotesShort, new = new.tokenQuotesShort, prevR = prevUI.price) {
|
||||
new.getCurrentPrice(
|
||||
prev = prev,
|
||||
)
|
||||
},
|
||||
trendPercentText = ifChanged(
|
||||
prev.tokenQuotesShort,
|
||||
new.tokenQuotesShort,
|
||||
prevUI.trendPercentText,
|
||||
) { new.getTrendPercent() },
|
||||
trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() },
|
||||
chartData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chartData) { new.getChartData() },
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun <T, R> ifChanged(prev: T, new: T, prevR: R, force: Boolean = false, change: (T) -> R): R {
|
||||
return if (force || prev != new) change(new) else prevR
|
||||
}
|
||||
|
||||
private fun TokenMarket.getMarketCap(): String? {
|
||||
val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null
|
||||
|
||||
return value.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
).compact(
|
||||
threeDigitsMethod = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price {
|
||||
val prevPrice = prev?.tokenQuotesShort?.currentPrice
|
||||
|
||||
val priceText = tokenQuotesShort.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
).price()
|
||||
}
|
||||
|
||||
val changeType = if (prevPrice != null) {
|
||||
if (tokenQuotesShort.currentPrice > prevPrice) {
|
||||
PriceChangeType.UP
|
||||
} else {
|
||||
PriceChangeType.DOWN
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return MarketsListItemUM.Price(
|
||||
text = priceText,
|
||||
changeType = changeType,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarket.getChartData(): MarketChartRawData? {
|
||||
val chart = when (currentTrendInterval) {
|
||||
MarketsListUM.TrendInterval.H24 -> tokenCharts.h24
|
||||
MarketsListUM.TrendInterval.D7 -> tokenCharts.week
|
||||
MarketsListUM.TrendInterval.M1 -> tokenCharts.month
|
||||
}
|
||||
|
||||
return chart?.let { ct ->
|
||||
priceAndTimePointValuesConverter.convert(
|
||||
MarketChartData.Data(
|
||||
y = ct.priceY.toImmutableList(),
|
||||
x = ct.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
).sorted(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun TokenMarket.getTrendType(): PriceChangeType {
|
||||
val percent = when (currentTrendInterval) {
|
||||
MarketsListUM.TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent
|
||||
MarketsListUM.TrendInterval.D7 -> tokenQuotesShort.weekChangePercent
|
||||
MarketsListUM.TrendInterval.M1 -> tokenQuotesShort.monthChangePercent
|
||||
}
|
||||
val scaled = percent?.setScale(4, RoundingMode.HALF_UP)
|
||||
return when {
|
||||
scaled == null -> PriceChangeType.NEUTRAL
|
||||
scaled > BigDecimal.ZERO -> PriceChangeType.UP
|
||||
scaled < BigDecimal.ZERO -> PriceChangeType.DOWN
|
||||
else -> PriceChangeType.NEUTRAL
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarket.getTrendPercent(): String {
|
||||
val percent = when (currentTrendInterval) {
|
||||
MarketsListUM.TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent
|
||||
MarketsListUM.TrendInterval.D7 -> tokenQuotesShort.weekChangePercent
|
||||
MarketsListUM.TrendInterval.M1 -> tokenQuotesShort.monthChangePercent
|
||||
}
|
||||
|
||||
return percent.format { percent() }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
package com.tangem.features.feed.model.feed
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.GetTopFiveMarketTokenUseCase
|
||||
import com.tangem.domain.markets.TokenMarketListConfig
|
||||
import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase
|
||||
import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
|
||||
import com.tangem.features.feed.impl.R
|
||||
|
|
@ -13,10 +18,12 @@ import com.tangem.features.feed.ui.feed.state.*
|
|||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentHashMap
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
|
|
@ -28,11 +35,30 @@ internal class FeedComponentModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase,
|
||||
private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase,
|
||||
getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val _state = MutableStateFlow(initialState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private var quotesUpdateJob: Job? = null
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
|
||||
private val marketsBatchFlowManager = FeedMarketsBatchFlowManager(
|
||||
getTopFiveMarketTokenUseCase = getTopFiveMarketTokenUseCase,
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
private val searchBarStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SearchBarStateFactory(
|
||||
currentStateProvider = Provider { _state.value },
|
||||
|
|
@ -55,8 +81,45 @@ internal class FeedComponentModel @Inject constructor(
|
|||
_state.update { feedListUM ->
|
||||
feedListUM.copy(
|
||||
searchBar = _state.value.searchBar.copy(onQueryChange = searchBarStateFactory::onSearchQueryChange),
|
||||
feedListCallbacks = feedListUM.feedListCallbacks.copy(
|
||||
onSortTypeClick = ::onSortTypeClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
modelScope.launch(dispatchers.default) {
|
||||
combine(
|
||||
marketsBatchFlowManager.itemsByOrder,
|
||||
marketsBatchFlowManager.loadingStatesByOrder,
|
||||
marketsBatchFlowManager.errorStatesByOrder,
|
||||
) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder ->
|
||||
updateMarketCharts(itemsByOrder, loadingStatesByOrder, errorStatesByOrder)
|
||||
val currentSortType = _state.value.marketChartConfig.currentSortByType
|
||||
val items = itemsByOrder[currentSortType]
|
||||
val isLoading = loadingStatesByOrder[currentSortType] == true
|
||||
if (items != null && items.isNotEmpty() && !isLoading) {
|
||||
val order = currentSortType.toOrder()
|
||||
marketsBatchFlowManager.loadCharts(order)
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
|
||||
modelScope.launch(dispatchers.default) {
|
||||
currentAppCurrency.drop(1).collect {
|
||||
marketsBatchFlowManager.reloadAll()
|
||||
}
|
||||
}
|
||||
|
||||
modelScope.launch(dispatchers.default) {
|
||||
TokenMarketListConfig.Order.entries.forEach { order ->
|
||||
marketsBatchFlowManager.getOnLastBatchLoadedSuccessFlow(order)?.collect { batchKey ->
|
||||
marketsBatchFlowManager.loadCharts(order)
|
||||
if (batchKey == 0) {
|
||||
startQuotesUpdateTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun subscribeOnTrendingNews() {
|
||||
|
|
@ -88,10 +151,10 @@ internal class FeedComponentModel @Inject constructor(
|
|||
marketChartConfig = MarketChartConfig(
|
||||
marketCharts = buildMap {
|
||||
SortByTypeUM.entries.forEach {
|
||||
put(it, MarketChartUM.Loading)
|
||||
} // TODO in [REDACTED_TASK_KEY] add correct sorting
|
||||
put(it, MarketChartUM.LoadingError(onRetryClicked = marketsBatchFlowManager::reloadAll))
|
||||
}
|
||||
}.toPersistentHashMap(),
|
||||
currentSortByType = SortByTypeUM.TopGainers,
|
||||
currentSortByType = SortByTypeUM.Trending,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -100,4 +163,116 @@ internal class FeedComponentModel @Inject constructor(
|
|||
val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault())
|
||||
return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate)
|
||||
}
|
||||
|
||||
private fun updateMarketCharts(
|
||||
itemsByOrder: Map<SortByTypeUM, ImmutableList<com.tangem.common.ui.markets.models.MarketsListItemUM>>,
|
||||
loadingStatesByOrder: Map<SortByTypeUM, Boolean>,
|
||||
errorStatesByOrder: Map<SortByTypeUM, Boolean>,
|
||||
) {
|
||||
_state.update { currentState ->
|
||||
val newMarketCharts = buildMap {
|
||||
SortByTypeUM.entries.forEach { sortByType ->
|
||||
val items = itemsByOrder[sortByType] ?: persistentListOf()
|
||||
val isLoading = loadingStatesByOrder[sortByType] == true
|
||||
val hasError = errorStatesByOrder[sortByType] == true
|
||||
|
||||
when {
|
||||
hasError -> {
|
||||
put(
|
||||
sortByType,
|
||||
MarketChartUM.LoadingError(
|
||||
onRetryClicked = {
|
||||
marketsBatchFlowManager.reloadAll()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
isLoading -> {
|
||||
put(sortByType, MarketChartUM.Loading)
|
||||
}
|
||||
items.isEmpty() -> {
|
||||
put(
|
||||
sortByType,
|
||||
MarketChartUM.LoadingError(
|
||||
onRetryClicked = {
|
||||
marketsBatchFlowManager.reloadAll()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
put(
|
||||
sortByType,
|
||||
MarketChartUM.Content(
|
||||
items = items,
|
||||
sortChartConfig = SortChartConfigUM(
|
||||
sortByType = sortByType,
|
||||
isSelected = sortByType == currentState.marketChartConfig.currentSortByType,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toPersistentHashMap()
|
||||
|
||||
currentState.copy(
|
||||
marketChartConfig = currentState.marketChartConfig.copy(
|
||||
marketCharts = newMarketCharts,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSortTypeClick(sortByType: SortByTypeUM) {
|
||||
_state.update { currentState ->
|
||||
val updatedCharts = currentState.marketChartConfig.marketCharts.mapValues { (chartSortType, chart) ->
|
||||
when (chart) {
|
||||
is MarketChartUM.Content -> {
|
||||
chart.copy(
|
||||
sortChartConfig = chart.sortChartConfig.copy(
|
||||
isSelected = chartSortType == sortByType,
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> chart
|
||||
}
|
||||
}
|
||||
|
||||
currentState.copy(
|
||||
marketChartConfig = currentState.marketChartConfig.copy(
|
||||
currentSortByType = sortByType,
|
||||
marketCharts = updatedCharts.toPersistentHashMap(),
|
||||
),
|
||||
)
|
||||
}
|
||||
modelScope.launch(dispatchers.default) {
|
||||
marketsBatchFlowManager.loadCharts(sortByType.toOrder())
|
||||
}
|
||||
}
|
||||
|
||||
private fun startQuotesUpdateTimer() {
|
||||
quotesUpdateJob?.cancel()
|
||||
quotesUpdateJob = modelScope.launch {
|
||||
while (true) {
|
||||
delay(DELAY_TO_FETCH_QUOTES)
|
||||
marketsBatchFlowManager.updateQuotes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SortByTypeUM.toOrder(): TokenMarketListConfig.Order {
|
||||
return when (this) {
|
||||
SortByTypeUM.Rating -> TokenMarketListConfig.Order.ByRating
|
||||
SortByTypeUM.Trending -> TokenMarketListConfig.Order.Trending
|
||||
SortByTypeUM.ExperiencedBuyers -> TokenMarketListConfig.Order.Buyers
|
||||
SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers
|
||||
SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers
|
||||
SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DELAY_TO_FETCH_QUOTES = 60_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -29,6 +31,10 @@ import androidx.compose.ui.text.withStyle
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.common.ui.markets.MarketChartLoadingError
|
||||
import com.tangem.common.ui.markets.MarketsListItem
|
||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.R
|
||||
|
|
@ -49,9 +55,6 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.ui.market.components.MarketsListItem
|
||||
import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
|
||||
@Composable
|
||||
|
|
@ -101,10 +104,12 @@ internal fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) {
|
|||
|
||||
SpacerH(32.dp)
|
||||
|
||||
MarketBlock(
|
||||
marketChartConfig = state.marketChartConfig,
|
||||
feedListCallbacks = state.feedListCallbacks,
|
||||
)
|
||||
state.marketChartConfig.marketCharts[SortByTypeUM.Rating]?.let { marketChartUM ->
|
||||
MarketBlock(
|
||||
marketChart = marketChartUM,
|
||||
feedListCallbacks = state.feedListCallbacks,
|
||||
)
|
||||
}
|
||||
|
||||
NewsBlock(
|
||||
news = state.news,
|
||||
|
|
@ -120,44 +125,43 @@ internal fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
|
||||
if (marketChartConfig.marketCharts.isNotEmpty()) {
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) },
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
marketChartConfig.marketCharts[SortByTypeUM.Rating]?.let { chart ->
|
||||
Charts(
|
||||
onItemClick = feedListCallbacks.onMarketItemClick,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
marketChart = chart,
|
||||
private fun MarketBlock(marketChart: MarketChartUM, feedListCallbacks: FeedListCallbacks) {
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) },
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
Charts(
|
||||
onItemClick = feedListCallbacks.onMarketItemClick,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
marketChart = marketChart,
|
||||
)
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
|
||||
val onSeeAllClick by rememberUpdatedState {
|
||||
feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType)
|
||||
}
|
||||
if (marketChartConfig.marketCharts.isNotEmpty()) {
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
text = stringResourceSafe(R.string.markets_pulse_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) },
|
||||
onSeeAllClick = { onSeeAllClick() },
|
||||
)
|
||||
|
||||
LazyRow(
|
||||
|
|
@ -330,11 +334,7 @@ private fun Charts(
|
|||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
when (marketChart) {
|
||||
MarketChartUM.Loading -> {
|
||||
repeat(DEFAULT_CHART_SIZE_IN_MARKET) {
|
||||
|
|
@ -342,7 +342,10 @@ private fun Charts(
|
|||
}
|
||||
}
|
||||
is MarketChartUM.LoadingError -> {
|
||||
// TODO will be created in [REDACTED_TASK_KEY]
|
||||
MarketChartLoadingError(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onRetryClick = marketChart.onRetryClicked,
|
||||
)
|
||||
}
|
||||
is MarketChartUM.Content -> {
|
||||
marketChart.items.fastForEach { chart ->
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||
import com.tangem.common.ui.news.DefaultLoadingArticle
|
||||
import com.tangem.common.ui.news.TrendingLoadingArticle
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder
|
||||
|
||||
@Composable
|
||||
internal fun MarketLoadingBlock() {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
package com.tangem.features.feed.ui.feed.preview
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import kotlinx.collections.immutable.*
|
||||
|
||||
|
|
@ -94,7 +93,6 @@ internal object FeedListPreviewDataProvider {
|
|||
): MarketChartUM.Content {
|
||||
return MarketChartUM.Content(
|
||||
items = items,
|
||||
triggerScrollReset = consumedEvent(),
|
||||
sortChartConfig = SortChartConfigUM(
|
||||
sortByType = sortByType,
|
||||
isSelected = isSelected,
|
||||
|
|
@ -107,7 +105,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 1,
|
||||
title = "Bitcoin ETF reaches new highs, institutions pile in",
|
||||
score = 0.82f,
|
||||
createdAt = "2h ago",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = true,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
|
|
@ -116,7 +114,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 2,
|
||||
title = "Layer 2 networks battle for dominance amid fee wars",
|
||||
score = 0.71f,
|
||||
createdAt = "4h ago",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = true,
|
||||
|
|
@ -125,7 +123,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 3,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
|
|
@ -134,7 +132,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 4,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
|
|
@ -143,7 +141,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 5,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
|
|
@ -152,7 +150,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 6,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
|
@ -46,7 +45,6 @@ internal sealed interface MarketChartUM {
|
|||
|
||||
data class Content(
|
||||
val items: ImmutableList<MarketsListItemUM>,
|
||||
val triggerScrollReset: StateEvent<Unit>,
|
||||
val sortChartConfig: SortChartConfigUM,
|
||||
) : MarketChartUM
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,378 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.*
|
||||
import com.tangem.features.feed.model.converter.MarketsTokenItemConverter
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListUM
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class FeedMarketsBatchFlowManager(
|
||||
private val getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
|
||||
private val currentAppCurrency: Provider<AppCurrency>,
|
||||
private val modelScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
private val managersByOrder = TokenMarketListConfig.Order.entries.associateWith { order ->
|
||||
createManagerForOrder(order)
|
||||
}
|
||||
|
||||
val itemsByOrder: StateFlow<Map<SortByTypeUM, ImmutableList<MarketsListItemUM>>> =
|
||||
combine(
|
||||
TokenMarketListConfig.Order.entries.mapNotNull { order ->
|
||||
managersByOrder[order]?.uiItems?.map { items -> order to items }
|
||||
},
|
||||
) { itemsList ->
|
||||
itemsList.associate { (order, items) ->
|
||||
val sortByType = order.toSortByTypeUM()
|
||||
sortByType to items
|
||||
}
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
val loadingStatesByOrder: StateFlow<Map<SortByTypeUM, Boolean>> =
|
||||
combine(
|
||||
TokenMarketListConfig.Order.entries.mapNotNull { order ->
|
||||
managersByOrder[order]?.isLoading?.map { isLoading -> order to isLoading }
|
||||
},
|
||||
) { loadingStatesList ->
|
||||
loadingStatesList.associate { (order, isLoading) ->
|
||||
val sortByType = order.toSortByTypeUM()
|
||||
sortByType to isLoading
|
||||
}
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
val errorStatesByOrder: StateFlow<Map<SortByTypeUM, Boolean>> =
|
||||
combine(
|
||||
TokenMarketListConfig.Order.entries.mapNotNull { order ->
|
||||
managersByOrder[order]?.hasError?.map { hasError -> order to hasError }
|
||||
},
|
||||
) { errorStatesList ->
|
||||
errorStatesList.associate { (order, hasError) ->
|
||||
val sortByType = order.toSortByTypeUM()
|
||||
sortByType to hasError
|
||||
}
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
init {
|
||||
managersByOrder.values.forEach { manager ->
|
||||
manager.reload(currentAppCurrency().code)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createManagerForOrder(order: TokenMarketListConfig.Order): SingleOrderManager {
|
||||
val actionsFlow = MutableSharedFlow<BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>>()
|
||||
|
||||
val batchFlow = getTopFiveMarketTokenUseCase(
|
||||
batchingContext = TokenListBatchingContext(
|
||||
actionsFlow = actionsFlow,
|
||||
coroutineScope = modelScope,
|
||||
),
|
||||
order = order,
|
||||
)
|
||||
|
||||
return SingleOrderManager(
|
||||
order = order,
|
||||
actionsFlow = actionsFlow,
|
||||
batchFlow = batchFlow,
|
||||
currentAppCurrency = currentAppCurrency,
|
||||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
fun reloadAll() {
|
||||
managersByOrder.values.forEach { manager ->
|
||||
manager.reload(currentAppCurrency().code)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateQuotes() {
|
||||
managersByOrder.values.forEach { manager ->
|
||||
manager.updateQuotes(currentAppCurrency().code)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCharts(order: TokenMarketListConfig.Order) {
|
||||
managersByOrder[order]?.loadCharts()
|
||||
}
|
||||
|
||||
fun getOnLastBatchLoadedSuccessFlow(order: TokenMarketListConfig.Order): Flow<Int>? {
|
||||
return managersByOrder[order]?.onLastBatchLoadedSuccess
|
||||
}
|
||||
|
||||
private class SingleOrderManager(
|
||||
val order: TokenMarketListConfig.Order,
|
||||
private val actionsFlow: MutableSharedFlow<BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>>,
|
||||
private val batchFlow: TokenListBatchFlow,
|
||||
private val currentAppCurrency: Provider<AppCurrency>,
|
||||
private val modelScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
private val updateStateJob = JobHolder()
|
||||
private val resultBatches = MutableStateFlow(ResultBatches())
|
||||
private val uiBatches = resultBatches.map { it.uiBatches }
|
||||
|
||||
val uiItems: StateFlow<ImmutableList<MarketsListItemUM>> =
|
||||
uiBatches
|
||||
.map { batches ->
|
||||
batches.asSequence()
|
||||
.map { it.data }
|
||||
.flatten()
|
||||
.toImmutableList()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = persistentListOf(),
|
||||
)
|
||||
|
||||
val isLoading = batchFlow.state
|
||||
.map { state ->
|
||||
when (state.status) {
|
||||
is PaginationStatus.InitialLoading -> true
|
||||
is PaginationStatus.NextBatchLoading -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
val hasError = batchFlow.state
|
||||
.map { state ->
|
||||
when (val status = state.status) {
|
||||
is PaginationStatus.InitialLoadingError -> true
|
||||
is PaginationStatus.Paginating -> status.lastResult is BatchFetchResult.Error
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
val onLastBatchLoadedSuccess = batchFlow.state
|
||||
.distinctUntilChanged { old, new -> old.status == new.status && old.data.size == new.data.size }
|
||||
.mapNotNull { batchListState ->
|
||||
when (val status = batchListState.status) {
|
||||
is PaginationStatus.Paginating -> {
|
||||
if (status.lastResult is BatchFetchResult.Success) {
|
||||
batchListState.data.lastOrNull()?.key
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
is PaginationStatus.EndOfPagination -> {
|
||||
batchListState.data.lastOrNull()?.key
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
batchFlow.state
|
||||
.map { it.data }
|
||||
.distinctUntilChanged { a, b ->
|
||||
a.size == b.size &&
|
||||
a.map { it.key } == b.map { it.key } &&
|
||||
a.map { it.data }.flatten() == b.map { it.data }.flatten()
|
||||
}
|
||||
.onEach {
|
||||
coroutineScope {
|
||||
launch {
|
||||
updateState(it)
|
||||
}.saveIn(updateStateJob)
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private suspend fun updateState(newList: List<Batch<Int, List<TokenMarket>>>, forceUpdate: Boolean = false) =
|
||||
withContext(dispatchers.default) {
|
||||
resultBatches.update { resultBatches ->
|
||||
val items = resultBatches.uiBatches
|
||||
val previousList = resultBatches.processedItems
|
||||
|
||||
val converter = MarketsTokenItemConverter(
|
||||
currentTrendInterval = MarketsListUM.TrendInterval.H24,
|
||||
appCurrency = currentAppCurrency(),
|
||||
)
|
||||
|
||||
if (newList.isEmpty()) {
|
||||
return@update ResultBatches(processedItems = emptyList())
|
||||
}
|
||||
|
||||
val isInitialLoading =
|
||||
forceUpdate || previousList.isNullOrEmpty() || newList.first().key != previousList.first().key
|
||||
|
||||
val outItems = if (isInitialLoading) {
|
||||
newList.map { batch ->
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = converter.convertList(batch.data),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// As nextBatchSize = 0, we only have one batch, but keep the logic for safety
|
||||
if (previousList.size != newList.size) {
|
||||
val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet())
|
||||
val newBatches = newList.filter { keysToAdd.contains(it.key) }
|
||||
|
||||
items + newBatches.map { batch ->
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = converter.convertList(batch.data),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items.mapIndexed { batchIndex, batch ->
|
||||
val prevBatch = previousList[batchIndex]
|
||||
val newBatch = newList[batchIndex]
|
||||
if (prevBatch == newBatch) return@mapIndexed batch
|
||||
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = batch.data.mapIndexed { index, marketsListItemUM ->
|
||||
val prevItem = prevBatch.data.getOrNull(index)
|
||||
val newItem = newBatch.data.getOrNull(index)
|
||||
if (prevItem != null && newItem != null) {
|
||||
converter.update(prevItem, marketsListItemUM, newItem)
|
||||
} else {
|
||||
newItem?.let { converter.convert(it) } ?: marketsListItemUM
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentCoroutineContext().ensureActive()
|
||||
|
||||
ResultBatches(
|
||||
uiBatches = outItems,
|
||||
processedItems = newList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun reload(fiatPriceCurrency: String) {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
resultBatches.value = ResultBatches()
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = TokenMarketListConfig(
|
||||
fiatPriceCurrency = fiatPriceCurrency,
|
||||
searchText = null,
|
||||
priceChangeInterval = TokenMarketListConfig.Interval.H24,
|
||||
order = order,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateQuotes(fiatPriceCurrency: String) {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
actionsFlow.emit(
|
||||
BatchAction.CancelUpdates {
|
||||
it.updateRequest is TokenMarketUpdateRequest.UpdateQuotes
|
||||
},
|
||||
)
|
||||
|
||||
actionsFlow.emit(
|
||||
BatchAction.UpdateBatches(
|
||||
keys = batchFlow
|
||||
.state
|
||||
.value
|
||||
.data
|
||||
.map { it.key }
|
||||
.toSet(),
|
||||
updateRequest = TokenMarketUpdateRequest.UpdateQuotes(
|
||||
currencyId = fiatPriceCurrency,
|
||||
),
|
||||
async = true,
|
||||
operationId = "update quotes",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCharts() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
val currentData = batchFlow.state.value.data
|
||||
val alreadyLoadedChartsBatchKeys = currentData
|
||||
.filter { batch ->
|
||||
val first = batch.data.firstOrNull() ?: return@filter false
|
||||
first.tokenCharts.h24 != null
|
||||
}
|
||||
.map { it.key }
|
||||
.toSet()
|
||||
|
||||
val batchesKeysToLoad = currentData.map { it.key }.toSet().minus(alreadyLoadedChartsBatchKeys)
|
||||
|
||||
if (batchesKeysToLoad.isNotEmpty()) {
|
||||
actionsFlow.emit(
|
||||
BatchAction.UpdateBatches(
|
||||
keys = batchesKeysToLoad,
|
||||
updateRequest = TokenMarketUpdateRequest.UpdateChart(
|
||||
interval = TokenMarketListConfig.Interval.H24,
|
||||
currency = currentAppCurrency().code,
|
||||
),
|
||||
async = true,
|
||||
operationId = batchesKeysToLoad.toString() + "h24",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class ResultBatches(
|
||||
val uiBatches: List<Batch<Int, List<MarketsListItemUM>>> = emptyList(),
|
||||
val processedItems: List<Batch<Int, List<TokenMarket>>>? = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketListConfig.Order.toSortByTypeUM(): SortByTypeUM {
|
||||
return when (this) {
|
||||
TokenMarketListConfig.Order.ByRating -> SortByTypeUM.Rating
|
||||
TokenMarketListConfig.Order.Trending -> SortByTypeUM.Trending
|
||||
TokenMarketListConfig.Order.Buyers -> SortByTypeUM.ExperiencedBuyers
|
||||
TokenMarketListConfig.Order.TopGainers -> SortByTypeUM.TopGainers
|
||||
TokenMarketListConfig.Order.TopLosers -> SortByTypeUM.TopLosers
|
||||
TokenMarketListConfig.Order.Staking -> SortByTypeUM.Staking
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,19 @@ import com.tangem.common.ui.news.ArticleConfigUM
|
|||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.WrappedList
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.FormattedDate
|
||||
import com.tangem.core.ui.utils.getFormattedDate
|
||||
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import org.joda.time.DateTime
|
||||
|
||||
internal class TrendingNewsStateFactory(
|
||||
private val currentStateProvider: Provider<FeedListUM>,
|
||||
|
|
@ -47,7 +54,7 @@ internal class TrendingNewsStateFactory(
|
|||
)
|
||||
},
|
||||
).toPersistentSet(),
|
||||
createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY]
|
||||
createdAt = mapFormattedDate(article.createdAt),
|
||||
isViewed = article.viewed,
|
||||
)
|
||||
},
|
||||
|
|
@ -72,7 +79,7 @@ internal class TrendingNewsStateFactory(
|
|||
)
|
||||
},
|
||||
).toPersistentSet(),
|
||||
createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY]
|
||||
createdAt = mapFormattedDate(article.createdAt),
|
||||
isViewed = article.viewed,
|
||||
)
|
||||
}.toPersistentList(),
|
||||
|
|
@ -80,4 +87,34 @@ internal class TrendingNewsStateFactory(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapFormattedDate(createdAt: String): TextReference {
|
||||
val formattedDate = getFormattedDate(
|
||||
createdAt = createdAt,
|
||||
now = DateTime.now(),
|
||||
)
|
||||
return when (formattedDate) {
|
||||
is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date)
|
||||
is FormattedDate.HoursAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_hours_ago,
|
||||
count = formattedDate.hours,
|
||||
formatArgs = wrappedList(formattedDate.hours),
|
||||
)
|
||||
is FormattedDate.MinutesAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_minutes_ago,
|
||||
count = formattedDate.minutes,
|
||||
formatArgs = wrappedList(formattedDate.minutes),
|
||||
)
|
||||
is FormattedDate.Today -> TextReference.Combined(
|
||||
refs = WrappedList(
|
||||
data = listOf(
|
||||
TextReference.Res(R.string.common_today),
|
||||
TextReference.Str(StringsSigns.COMA_SIGN),
|
||||
TextReference.Str(StringsSigns.WHITE_SPACE),
|
||||
TextReference.Str(formattedDate.time),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.feed.ui.market.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ include(":common:routing")
|
|||
include(":common:test")
|
||||
include(":common:ui")
|
||||
include(":common:ui-charts")
|
||||
include(":common:ui-markets")
|
||||
// endregion
|
||||
|
||||
// region Libs modules
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue