Skip to content

Commit bfc061a

Browse files
authored
Merge pull request #522 from code-payments/fix/network-state-balance-update
feat: improve network resiliency when starting in a no network state
2 parents acc07b8 + 73efc90 commit bfc061a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+749
-378
lines changed

api/src/main/java/com/getcode/db/ConversationDao.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.getcode.db
22

3+
import androidx.paging.PagingData
4+
import androidx.paging.PagingSource
35
import androidx.room.Dao
46
import androidx.room.Delete
57
import androidx.room.Insert
@@ -19,6 +21,10 @@ interface ConversationDao {
1921
@Insert(onConflict = OnConflictStrategy.REPLACE)
2022
suspend fun upsertConversations(vararg conversation: Conversation)
2123

24+
@RewriteQueriesToDropUnusedColumns
25+
@Query("SELECT * FROM conversations")
26+
fun observeConversations(): PagingSource<Int, Conversation>
27+
2228
@RewriteQueriesToDropUnusedColumns
2329
@Query("SELECT * FROM conversations LEFT JOIN conversation_pointers ON conversations.idBase58 = conversation_pointers.conversationIdBase58 WHERE conversations.idBase58 = :id")
2430
fun observeConversation(id: String): Flow<ConversationWithLastPointers?>

api/src/main/java/com/getcode/mapper/ConversationMapper.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import com.getcode.model.Conversation
44
import com.getcode.model.chat.Chat
55
import com.getcode.model.chat.ChatType
66
import com.getcode.model.chat.self
7-
import com.getcode.network.TipController
87
import com.getcode.network.localized
98
import com.getcode.network.repository.base58
109
import com.getcode.util.resources.ResourceHelper

api/src/main/java/com/getcode/model/Feature.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ data class TipCardOnHomeScreenFeature(
2222
override val available: Boolean = true, // always available
2323
): Feature
2424

25-
data class TipChatFeature(
26-
override val enabled: Boolean = BetaOptions.Defaults.tipsChatEnabled,
25+
data class ConversationsFeature(
26+
override val enabled: Boolean = BetaOptions.Defaults.conversationsEnabled,
2727
override val available: Boolean = true, // always available
2828
): Feature
2929

30-
data class TipChatCashFeature(
31-
override val enabled: Boolean = BetaOptions.Defaults.tipsChatCashEnabled,
30+
data class ConversationCashFeature(
31+
override val enabled: Boolean = BetaOptions.Defaults.conversationCashEnabled,
3232
override val available: Boolean = true, // always available
3333
): Feature
3434

api/src/main/java/com/getcode/model/PrefBool.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ sealed class PrefsBool(val value: String) {
4646
data object BUY_MODULE_ENABLED : PrefsBool("buy_kin_enabled"), BetaFlag
4747
data object CHAT_UNSUB_ENABLED: PrefsBool("chat_unsub_enabled"), BetaFlag
4848
data object TIPS_ENABLED : PrefsBool("tips_enabled"), BetaFlag
49-
data object TIPS_CHAT_ENABLED: PrefsBool("tips_chat_enabled"), BetaFlag
50-
data object TIPS_CHAT_CASH_ENABLED: PrefsBool("tips_chat_cash_enabled"), BetaFlag
49+
data object CONVERSATIONS_ENABLED: PrefsBool("conversations_enabled"), BetaFlag
50+
data object CONVERSATION_CASH_ENABLED: PrefsBool("convo_cash_enabled"), BetaFlag
5151
data object BALANCE_CURRENCY_SELECTION_ENABLED: PrefsBool("balance_currency_enabled"), BetaFlag
5252
data object KADO_WEBVIEW_ENABLED : PrefsBool("kado_inapp_enabled"), BetaFlag
5353
data object SHARE_TWEET_TO_TIP : PrefsBool("share_tweet_to_tip"), BetaFlag

api/src/main/java/com/getcode/model/chat/Chat.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.getcode.model.chat
22

33
import com.getcode.model.Cursor
44
import com.getcode.model.ID
5+
import kotlinx.serialization.Serializable
56
import java.util.UUID
67

78
/**
@@ -18,6 +19,7 @@ import java.util.UUID
1819
* @param cursor [Cursor] value for this chat for reference in subsequent GetChatsRequest
1920
* @param messages List of messages within this chat
2021
*/
22+
@Serializable
2123
data class Chat(
2224
val id: ID,
2325
val type: ChatType,
@@ -135,6 +137,9 @@ data class Chat(
135137
val Chat.isV2: Boolean
136138
get() = members.isNotEmpty()
137139

140+
val Chat.isNotification: Boolean
141+
get() = type == ChatType.Notification
142+
138143
val Chat.isConversation: Boolean
139144
get() = type == ChatType.TwoWay
140145

api/src/main/java/com/getcode/model/chat/ChatMessage.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import com.getcode.ed25519.Ed25519.KeyPair
44
import com.getcode.model.Cursor
55
import com.getcode.model.ID
66
import com.getcode.model.MessageStatus
7+
import com.getcode.utils.serializer.UUIDSerializer
8+
import kotlinx.serialization.Serializable
79
import java.util.UUID
810

911
/**
@@ -18,8 +20,10 @@ import java.util.UUID
1820
* @param contents Ordered message content. A message may have more than one piece of content.
1921
* @param status Derived [MessageStatus] from [Pointer]'s in [ChatMember].
2022
*/
23+
@Serializable
2124
data class ChatMessage(
2225
val id: ID, // time based UUID in v2
26+
@Serializable(with = UUIDSerializer::class)
2327
val senderId: UUID?,
2428
val isFromSelf: Boolean,
2529
val cursor: Cursor,

api/src/main/java/com/getcode/network/BalanceController.kt

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.getcode.solana.organizer.Organizer
1313
import com.getcode.solana.organizer.Tray
1414
import com.getcode.utils.FormatUtils
1515
import com.getcode.utils.network.NetworkConnectivityListener
16+
import com.getcode.utils.network.retryable
1617
import com.getcode.utils.trace
1718
import io.reactivex.rxjava3.core.Completable
1819
import kotlinx.coroutines.CoroutineScope
@@ -23,6 +24,7 @@ import kotlinx.coroutines.flow.SharingStarted
2324
import kotlinx.coroutines.flow.StateFlow
2425
import kotlinx.coroutines.flow.combine
2526
import kotlinx.coroutines.flow.distinctUntilChanged
27+
import kotlinx.coroutines.flow.flatMapLatest
2628
import kotlinx.coroutines.flow.flowOn
2729
import kotlinx.coroutines.flow.launchIn
2830
import kotlinx.coroutines.flow.map
@@ -38,8 +40,7 @@ data class BalanceDisplay(
3840
val marketValue: Double = 0.0,
3941
val formattedValue: String = "",
4042
val currency: Currency? = null,
41-
42-
)
43+
)
4344

4445
open class BalanceController @Inject constructor(
4546
exchange: Exchange,
@@ -65,25 +66,34 @@ open class BalanceController @Inject constructor(
6566
.stateIn(scope, SharingStarted.Eagerly, BalanceDisplay())
6667

6768
init {
68-
combine(
69-
exchange.observeLocalRate()
70-
.flowOn(Dispatchers.IO)
71-
.onEach {
72-
val display = _balanceDisplay.value ?: BalanceDisplay()
73-
_balanceDisplay.value = display.copy(currency = getCurrencyFromCode(it.currency))
69+
networkObserver.state
70+
.map { it.connected }
71+
.onEach { connected ->
72+
if (connected) {
73+
retryable({ fetchBalanceSuspend() })
74+
}
75+
}
76+
.flatMapLatest {
77+
combine(
78+
exchange.observeLocalRate()
79+
.flowOn(Dispatchers.IO)
80+
.onEach {
81+
val display = _balanceDisplay.value ?: BalanceDisplay()
82+
_balanceDisplay.value =
83+
display.copy(currency = getCurrencyFromCode(it.currency))
84+
}
85+
.onEach { exchange.fetchRatesIfNeeded() },
86+
balanceRepository.balanceFlow,
87+
) { rate, balance ->
88+
rate to balance.coerceAtLeast(0.0)
89+
}.map { (rate, balance) ->
90+
refreshBalance(balance, rate)
7491
}
75-
.onEach { exchange.fetchRatesIfNeeded() },
76-
balanceRepository.balanceFlow,
77-
networkObserver.state
78-
) { rate, balance, _ ->
79-
rate to balance.coerceAtLeast(0.0)
80-
}.map { (rate, balance) ->
81-
refreshBalance(balance, rate)
82-
}.distinctUntilChanged().onEach { (marketValue, amountText) ->
83-
val display = _balanceDisplay.value ?: BalanceDisplay()
84-
_balanceDisplay.value =
85-
display.copy(marketValue = marketValue, formattedValue = amountText)
86-
}.launchIn(scope)
92+
}.distinctUntilChanged().onEach { (marketValue, amountText) ->
93+
val display = _balanceDisplay.value ?: BalanceDisplay()
94+
_balanceDisplay.value =
95+
display.copy(marketValue = marketValue, formattedValue = amountText)
96+
}.launchIn(scope)
8797
}
8898

8999
fun setTray(organizer: Organizer, tray: Tray) {
@@ -155,6 +165,7 @@ open class BalanceController @Inject constructor(
155165

156166

157167
suspend fun fetchBalanceSuspend() {
168+
Timber.d("fetching balance")
158169
if (SessionManager.isAuthenticated() != true) {
159170
Timber.d("FetchBalance - Not authenticated")
160171
return

api/src/main/java/com/getcode/network/HistoryController.kt renamed to api/src/main/java/com/getcode/network/ChatHistoryController.kt

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@ import com.getcode.model.Cursor
1717
import com.getcode.model.ID
1818
import com.getcode.model.MessageStatus
1919
import com.getcode.model.chat.ChatMember
20-
import com.getcode.model.chat.ChatType
2120
import com.getcode.model.chat.Identity
2221
import com.getcode.model.chat.Platform
2322
import com.getcode.model.chat.Title
2423
import com.getcode.model.chat.isConversation
24+
import com.getcode.model.chat.isNotification
2525
import com.getcode.model.chat.selfId
26-
import com.getcode.model.description
2726
import com.getcode.network.client.Client
2827
import com.getcode.network.client.advancePointer
2928
import com.getcode.network.client.fetchChats
@@ -34,39 +33,42 @@ import com.getcode.network.repository.encodeBase64
3433
import com.getcode.network.source.ChatMessagePagingSource
3534
import com.getcode.util.resources.ResourceHelper
3635
import com.getcode.util.resources.ResourceType
37-
import com.getcode.utils.ErrorUtils
3836
import com.getcode.utils.TraceType
3937
import com.getcode.utils.trace
4038
import kotlinx.coroutines.CoroutineScope
4139
import kotlinx.coroutines.Dispatchers
4240
import kotlinx.coroutines.GlobalScope
4341
import kotlinx.coroutines.flow.Flow
4442
import kotlinx.coroutines.flow.MutableStateFlow
43+
import kotlinx.coroutines.flow.SharingStarted
4544
import kotlinx.coroutines.flow.StateFlow
46-
import kotlinx.coroutines.flow.asStateFlow
4745
import kotlinx.coroutines.flow.filterNotNull
4846
import kotlinx.coroutines.flow.map
47+
import kotlinx.coroutines.flow.stateIn
4948
import kotlinx.coroutines.flow.update
50-
import kotlinx.coroutines.launch
51-
import okhttp3.internal.toImmutableList
5249
import timber.log.Timber
5350
import java.util.Locale
5451
import javax.inject.Inject
5552
import javax.inject.Singleton
5653

57-
// TODO: See if we can merge this into [ChatController]
5854
@Singleton
59-
class HistoryController @Inject constructor(
55+
class ChatHistoryController @Inject constructor(
6056
private val client: Client,
61-
private val resources: ResourceHelper,
6257
private val tipController: TipController,
6358
private val conversationMapper: ConversationMapper,
6459
private val conversationMessageMapper: ConversationMessageMapper,
6560
) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
6661

67-
private val _chats = MutableStateFlow<List<Chat>?>(null)
62+
private val chatEntries = MutableStateFlow<List<Chat>?>(null)
63+
val notifications: StateFlow<List<Chat>?>
64+
get() = chatEntries
65+
.map { it?.filter { entry -> entry.isNotification } }
66+
.stateIn(this, SharingStarted.Eagerly, emptyList())
67+
6868
val chats: StateFlow<List<Chat>?>
69-
get() = _chats.asStateFlow()
69+
get() = chatEntries
70+
.map { it?.filter { entry -> entry.isConversation } }
71+
.stateIn(this, SharingStarted.Eagerly, emptyList())
7072

7173
var loadingMessages: Boolean = false
7274

@@ -86,9 +88,9 @@ class HistoryController @Inject constructor(
8688
pagerMap[chatId] ?: ChatMessagePagingSource(
8789
client = client,
8890
owner = owner()!!,
89-
chat = _chats.value?.find { it.id == chatId },
91+
chat = chatEntries.value?.find { it.id == chatId },
9092
onMessagesFetched = { messages ->
91-
val chat = _chats.value?.find { it.id == chatId } ?: return@ChatMessagePagingSource
93+
val chat = chatEntries.value?.find { it.id == chatId } ?: return@ChatMessagePagingSource
9294
updateChatWithMessages(chat, messages)
9395
}
9496
).also {
@@ -99,14 +101,14 @@ class HistoryController @Inject constructor(
99101
fun updateChatWithMessages(chat: Chat, messages: List<ChatMessage>) {
100102
val updatedMessages = (chat.messages + messages).distinctBy { it.id }
101103
val updatedChat = chat.copy(messages = updatedMessages)
102-
val chats = _chats.value?.map {
104+
val chats = chatEntries.value?.map {
103105
if (it.id == updatedChat.id) {
104106
updatedChat
105107
} else {
106108
it
107109
}
108110
}?.sortedByDescending { it.lastMessageMillis }
109-
_chats.update { chats }
111+
chatEntries.update { chats }
110112
}
111113

112114
fun chatFlow(chatId: ID) =
@@ -132,7 +134,7 @@ class HistoryController @Inject constructor(
132134
if (!update) {
133135
pagerMap.clear()
134136
chatFlows.clear()
135-
_chats.value = containers
137+
chatEntries.value = containers
136138

137139
loadingMessages = true
138140
}
@@ -151,13 +153,13 @@ class HistoryController @Inject constructor(
151153
}
152154

153155
loadingMessages = false
154-
_chats.value = updatedWithMessages.sortedByDescending { it.lastMessageMillis }
156+
chatEntries.value = updatedWithMessages.sortedByDescending { it.lastMessageMillis }
155157
}
156158

157159
suspend fun advanceReadPointer(chatId: ID) {
158160
val owner = owner() ?: return
159161

160-
_chats.update {
162+
chatEntries.update {
161163
it?.toMutableList()?.apply chats@{
162164
indexOfFirst { chat -> chat.id == chatId }
163165
.takeIf { index -> index >= 0 }
@@ -180,7 +182,7 @@ class HistoryController @Inject constructor(
180182
}
181183

182184
fun advanceReadPointerUpTo(chatId: ID, timestamp: Long) {
183-
_chats.update {
185+
chatEntries.update {
184186
it?.toMutableList()?.apply chats@{
185187
indexOfFirst { chat -> chat.id == chatId }
186188
.takeIf { index -> index >= 0 }
@@ -198,7 +200,7 @@ class HistoryController @Inject constructor(
198200
suspend fun setMuted(chat: Chat, muted: Boolean): Result<Boolean> {
199201
val owner = owner() ?: return Result.failure(Throwable("No owner detected"))
200202

201-
_chats.update {
203+
chatEntries.update {
202204
it?.toMutableList()?.apply chats@{
203205
indexOfFirst { item -> item.id == chat.id }
204206
.takeIf { index -> index >= 0 }
@@ -216,7 +218,7 @@ class HistoryController @Inject constructor(
216218
suspend fun setSubscribed(chat: Chat, subscribed: Boolean): Result<Boolean> {
217219
val owner = owner() ?: return Result.failure(Throwable("No owner detected"))
218220

219-
_chats.update {
221+
chatEntries.update {
220222
it?.toMutableList()?.apply chats@{
221223
indexOfFirst { item -> item.id == chat.id }
222224
.takeIf { index -> index >= 0 }
@@ -250,7 +252,6 @@ class HistoryController @Inject constructor(
250252
MessageStatus.Delivered
251253
)
252254
}
253-
254255
}
255256
.onFailure {
256257
Timber.e(t = it, "Failed to fetch messages for $encodedId.")

api/src/main/java/com/getcode/network/ConversationController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ interface ConversationController {
4747
}
4848

4949
class ConversationStreamController @Inject constructor(
50-
private val historyController: HistoryController,
50+
private val historyController: ChatHistoryController,
5151
private val exchange: Exchange,
5252
private val chatService: ChatServiceV2,
5353
private val conversationMapper: ConversationMapper,

api/src/main/java/com/getcode/network/api/CurrencyApi.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@ import io.grpc.ManagedChannel
77
import io.reactivex.rxjava3.core.Scheduler
88
import io.reactivex.rxjava3.core.Single
99
import io.reactivex.rxjava3.schedulers.Schedulers
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.coroutines.flow.Flow
12+
import kotlinx.coroutines.flow.flowOn
1013
import javax.inject.Inject
1114

1215
class CurrencyApi @Inject constructor(
1316
managedChannel: ManagedChannel,
14-
private val scheduler: Scheduler = Schedulers.io()
1517
) : GrpcApi(managedChannel) {
1618
private val api = CurrencyGrpc.newStub(managedChannel)
1719

18-
fun getRates(request: CurrencyService.GetAllRatesRequest = CurrencyService.GetAllRatesRequest.getDefaultInstance()): Single<CurrencyService.GetAllRatesResponse> =
20+
fun getRates(request: CurrencyService.GetAllRatesRequest = CurrencyService.GetAllRatesRequest.getDefaultInstance()): Flow<CurrencyService.GetAllRatesResponse> =
1921
api::getAllRates
20-
.callAsSingle(request)
21-
.subscribeOn(scheduler)
22+
.callAsCancellableFlow(request)
23+
.flowOn(Dispatchers.IO)
2224
}

0 commit comments

Comments
 (0)