Skip to content

Commit d6006ac

Browse files
authored
Merge pull request #574 from code-payments/chore/split-history-controllers
chore: split history controllers to let v1 and v2 chats
2 parents 3b84792 + f81ee41 commit d6006ac

18 files changed

Lines changed: 377 additions & 236 deletions

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,15 @@ typealias SetSubscriptionStateRequestV1 = com.codeinc.gen.chat.v1.ChatService.Se
5252
typealias SetSubscriptionStateRequestV2 = com.codeinc.gen.chat.v2.ChatService.SetSubscriptionStateRequest
5353
typealias SetSubscriptionStateResponseV1 = com.codeinc.gen.chat.v1.ChatService.SetSubscriptionStateResponse
5454
typealias SetSubscriptionStateResponseV2 = com.codeinc.gen.chat.v2.ChatService.SetSubscriptionStateResponse
55+
56+
/**
57+
* Code reference to a V1 [Chat] that serves as a collection of messages associated
58+
* with a notification type (Tips, Cash Payments, Web Payments, etc.)
59+
*/
60+
typealias NotificationCollectionEntity = Chat
61+
62+
/**
63+
* Code reference to a V2 [Chat] that is a full end-to-end chat that suports
64+
* peer-to-peer messaging between users.
65+
*/
66+
typealias ConversationEntity = Chat
Lines changed: 17 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,33 @@
11
package com.getcode.network
22

3-
import androidx.paging.Pager
4-
import androidx.paging.PagingConfig
53
import androidx.paging.PagingData
64
import androidx.paging.PagingSource
7-
import androidx.paging.cachedIn
85
import com.getcode.db.AppDatabase
96
import com.getcode.db.Database
107
import com.getcode.ed25519.Ed25519.KeyPair
118
import com.getcode.manager.SessionManager
129
import com.getcode.mapper.ConversationMapper
1310
import com.getcode.mapper.ConversationMessageMapper
14-
import com.getcode.model.Conversation
15-
import com.getcode.model.chat.Chat
16-
import com.getcode.model.chat.ChatMessage
1711
import com.getcode.model.Cursor
1812
import com.getcode.model.ID
1913
import com.getcode.model.MessageStatus
2014
import com.getcode.model.chat.ChatMember
15+
import com.getcode.model.chat.ChatMessage
16+
import com.getcode.model.chat.ConversationEntity
2117
import com.getcode.model.chat.Identity
2218
import com.getcode.model.chat.Platform
2319
import com.getcode.model.chat.Title
2420
import com.getcode.model.chat.isConversation
25-
import com.getcode.model.chat.isNotification
2621
import com.getcode.model.chat.selfId
2722
import com.getcode.network.client.Client
2823
import com.getcode.network.client.advancePointer
29-
import com.getcode.network.client.fetchChats
3024
import com.getcode.network.client.fetchMessagesFor
31-
import com.getcode.network.client.setMuted
32-
import com.getcode.network.client.setSubscriptionState
25+
import com.getcode.network.client.fetchV2Chats
3326
import com.getcode.network.repository.encodeBase64
34-
import com.getcode.network.source.ChatMessagePagingSource
35-
import com.getcode.util.resources.ResourceHelper
36-
import com.getcode.util.resources.ResourceType
3727
import com.getcode.utils.TraceType
3828
import com.getcode.utils.trace
3929
import kotlinx.coroutines.CoroutineScope
4030
import kotlinx.coroutines.Dispatchers
41-
import kotlinx.coroutines.GlobalScope
4231
import kotlinx.coroutines.flow.Flow
4332
import kotlinx.coroutines.flow.MutableStateFlow
4433
import kotlinx.coroutines.flow.SharingStarted
@@ -48,25 +37,20 @@ import kotlinx.coroutines.flow.map
4837
import kotlinx.coroutines.flow.stateIn
4938
import kotlinx.coroutines.flow.update
5039
import timber.log.Timber
51-
import java.util.Locale
5240
import javax.inject.Inject
5341
import javax.inject.Singleton
5442

5543
@Singleton
5644
class ChatHistoryController @Inject constructor(
5745
private val client: Client,
58-
private val tipController: TipController,
46+
private val twitterUserController: TwitterUserController,
5947
private val conversationMapper: ConversationMapper,
6048
private val conversationMessageMapper: ConversationMessageMapper,
6149
) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
6250

63-
private val chatEntries = MutableStateFlow<List<Chat>?>(null)
64-
val notifications: StateFlow<List<Chat>?>
65-
get() = chatEntries
66-
.map { it?.filter { entry -> entry.isNotification } }
67-
.stateIn(this, SharingStarted.Eagerly, emptyList())
51+
private val chatEntries = MutableStateFlow<List<ConversationEntity>?>(null)
6852

69-
val chats: StateFlow<List<Chat>?>
53+
val chats: StateFlow<List<ConversationEntity>?>
7054
get() = chatEntries
7155
.map { it?.filter { entry -> entry.isConversation } }
7256
.stateIn(this, SharingStarted.Eagerly, emptyList())
@@ -78,28 +62,12 @@ class ChatHistoryController @Inject constructor(
7862
private val pagerMap = mutableMapOf<ID, PagingSource<Cursor, ChatMessage>>()
7963
private val chatFlows = mutableMapOf<ID, Flow<PagingData<ChatMessage>>>()
8064

81-
private val pagingConfig = PagingConfig(pageSize = 20)
82-
8365
fun reset() {
8466
pagerMap.clear()
8567
chatFlows.clear()
8668
}
8769

88-
private fun chatMessagePager(chatId: ID) = Pager(pagingConfig) {
89-
pagerMap[chatId] ?: ChatMessagePagingSource(
90-
client = client,
91-
owner = owner()!!,
92-
chat = chatEntries.value?.find { it.id == chatId },
93-
onMessagesFetched = { messages ->
94-
val chat = chatEntries.value?.find { it.id == chatId } ?: return@ChatMessagePagingSource
95-
updateChatWithMessages(chat, messages)
96-
}
97-
).also {
98-
pagerMap[chatId] = it
99-
}
100-
}
101-
102-
fun updateChatWithMessages(chat: Chat, messages: List<ChatMessage>) {
70+
fun updateChatWithMessages(chat: ConversationEntity, messages: List<ChatMessage>) {
10371
val updatedMessages = (chat.messages + messages).distinctBy { it.id }
10472
val updatedChat = chat.copy(messages = updatedMessages)
10573
val chats = chatEntries.value?.map {
@@ -112,29 +80,18 @@ class ChatHistoryController @Inject constructor(
11280
chatEntries.update { chats }
11381
}
11482

115-
fun chatFlow(chatId: ID) =
116-
chatFlows[chatId] ?: chatMessagePager(chatId).flow.cachedIn(GlobalScope).also {
117-
chatFlows[chatId] = it
118-
}
119-
120-
val notificationsUnreadCount = notifications
121-
.filterNotNull()
122-
// Ignore muted chats and unsubscribed chats
123-
.map { it.filter { c -> !c.isMuted && c.isSubscribed } }
124-
.map { it.sumOf { c -> c.unreadCount } }
125-
126-
val chatUnreadCount = chats
83+
val unreadCount = chats
12784
.filterNotNull()
12885
// Ignore muted chats and unsubscribed chats
12986
.map { it.filter { c -> !c.isMuted && c.isSubscribed } }
13087
.map { it.sumOf { c -> c.unreadCount } }
13188

13289
private fun owner(): KeyPair? = SessionManager.getKeyPair()
13390

134-
suspend fun fetchChats(update: Boolean = false) {
91+
suspend fun fetch(update: Boolean = false) {
13592
if (loadingMessages) return
13693

137-
val updatedWithMessages = mutableListOf<Chat>()
94+
val updatedWithMessages = mutableListOf<ConversationEntity>()
13895
val containers = fetchChatsWithoutMessages()
13996
trace(message = "Fetched ${containers.count()} chats", type = TraceType.Silent)
14097

@@ -163,40 +120,15 @@ class ChatHistoryController @Inject constructor(
163120
chatEntries.value = updatedWithMessages.sortedByDescending { it.lastMessageMillis }
164121
}
165122

166-
fun addChat(chat: Chat) {
123+
fun addChat(chat: ConversationEntity) {
167124
chatEntries.value = (chatEntries.value.orEmpty() + chat)
168125
.sortedByDescending { it.lastMessageMillis }
169126
}
170127

171-
fun findChat(predicate: (Chat) -> Boolean): Chat? {
128+
fun findChat(predicate: (ConversationEntity) -> Boolean): ConversationEntity? {
172129
return chatEntries.value?.firstOrNull(predicate)
173130
}
174131

175-
suspend fun advanceReadPointer(chatId: ID) {
176-
val owner = owner() ?: return
177-
178-
chatEntries.update {
179-
it?.toMutableList()?.apply chats@{
180-
indexOfFirst { chat -> chat.id == chatId }
181-
.takeIf { index -> index >= 0 }
182-
?.let { index ->
183-
val chat = this[index]
184-
val newestMessage = chat.newestMessage
185-
if (newestMessage != null) {
186-
client.advancePointer(
187-
owner = owner,
188-
chat = chat,
189-
to = newestMessage.id,
190-
status = MessageStatus.Read
191-
).onSuccess {
192-
this[index] = chat.resetUnreadCount()
193-
}
194-
}
195-
}
196-
}?.toList()
197-
}
198-
}
199-
200132
fun resetUnreadCount(chatId: ID) {
201133
chatEntries.update {
202134
it?.toMutableList()?.apply chats@{
@@ -210,43 +142,7 @@ class ChatHistoryController @Inject constructor(
210142
}
211143
}
212144

213-
suspend fun setMuted(chat: Chat, muted: Boolean): Result<Boolean> {
214-
val owner = owner() ?: return Result.failure(Throwable("No owner detected"))
215-
216-
chatEntries.update {
217-
it?.toMutableList()?.apply chats@{
218-
indexOfFirst { item -> item.id == chat.id }
219-
.takeIf { index -> index >= 0 }
220-
?.let { index ->
221-
val c = this[index]
222-
Timber.d("changing mute state for chat locally")
223-
this[index] = c.setMuteState(muted)
224-
}
225-
}?.toList()
226-
}
227-
228-
return client.setMuted(owner, chat, muted)
229-
}
230-
231-
suspend fun setSubscribed(chat: Chat, subscribed: Boolean): Result<Boolean> {
232-
val owner = owner() ?: return Result.failure(Throwable("No owner detected"))
233-
234-
chatEntries.update {
235-
it?.toMutableList()?.apply chats@{
236-
indexOfFirst { item -> item.id == chat.id }
237-
.takeIf { index -> index >= 0 }
238-
?.let { index ->
239-
val c = this[index]
240-
Timber.d("changing subscribed state for chat locally")
241-
this[index] = c.setSubscriptionState(subscribed)
242-
}
243-
}?.toList()
244-
}
245-
246-
return client.setSubscriptionState(owner, chat, subscribed)
247-
}
248-
249-
private suspend fun fetchLatestMessageForChat(chat: Chat): Result<ChatMessage?> {
145+
private suspend fun fetchLatestMessageForChat(chat: ConversationEntity): Result<ChatMessage?> {
250146
val encodedId = chat.id.toByteArray().encodeBase64()
251147
Timber.d("fetching last message for $encodedId")
252148
val owner = owner() ?: return Result.success(null)
@@ -271,9 +167,9 @@ class ChatHistoryController @Inject constructor(
271167
}.map { it.getOrNull(0) }
272168
}
273169

274-
private suspend fun fetchChatsWithoutMessages(): List<Chat> {
170+
private suspend fun fetchChatsWithoutMessages(): List<ConversationEntity> {
275171
val owner = owner() ?: return emptyList()
276-
val result = client.fetchChats(owner)
172+
val result = client.fetchV2Chats(owner)
277173
.map { chats ->
278174
chats.map { chat ->
279175
// map revealed identity as title if known
@@ -303,14 +199,14 @@ class ChatHistoryController @Inject constructor(
303199
return result.getOrNull().orEmpty()
304200
}
305201

306-
private suspend fun fetchMemberImages(chat: Chat): List<ChatMember> {
202+
private suspend fun fetchMemberImages(chat: ConversationEntity): List<ChatMember> {
307203
return chat.members
308204
.map { member ->
309205
if (member.isSelf) return@map member
310206
if (member.identity == null) return@map member
311207
if (member.identity.imageUrl != null) return@map member
312208
val metadata = runCatching {
313-
tipController.fetch(member.identity.username)
209+
twitterUserController.fetchUser(member.identity.username)
314210
}.getOrNull() ?: return@map member
315211

316212
member.copy(
@@ -322,23 +218,4 @@ class ChatHistoryController @Inject constructor(
322218
)
323219
}
324220
}
325-
}
326-
327-
fun Title?.localized(resources: ResourceHelper): String {
328-
return when (val t = this) {
329-
is Title.Domain -> {
330-
t.value.capitalize(Locale.getDefault())
331-
}
332-
333-
is Title.Localized -> {
334-
val resId = resources.getIdentifier(
335-
t.value,
336-
ResourceType.String,
337-
).let { if (it == 0) null else it }
338-
339-
resId?.let { resources.getString(it) } ?: t.value
340-
}
341-
342-
else -> "Anonymous"
343-
}
344221
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import kotlinx.coroutines.CoroutineScope
3434
import kotlinx.coroutines.Dispatchers
3535
import kotlinx.coroutines.flow.Flow
3636
import kotlinx.coroutines.flow.MutableStateFlow
37-
import kotlinx.coroutines.flow.first
3837
import kotlinx.coroutines.flow.map
3938
import kotlinx.coroutines.launch
4039
import javax.inject.Inject

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import javax.inject.Inject
88
class ConversationListController @Inject constructor(
99
private val historyController: ChatHistoryController,
1010
) {
11-
val isLoadingChats: Boolean
11+
val isLoading: Boolean
1212
get() = historyController.loadingMessages
1313

1414
fun observeConversations() = historyController.chats
1515

16-
suspend fun fetchChats() = historyController.fetchChats(true)
16+
suspend fun fetchChats() = historyController.fetch(true)
1717
}
1818

1919
class ChatPagingSource(

0 commit comments

Comments
 (0)