Skip to content

Commit 0b0b8ab

Browse files
committed
feat: add support for creating a chat from a tip message
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 29005a2 commit 0b0b8ab

28 files changed

Lines changed: 390 additions & 207 deletions

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

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,11 @@ interface ConversationDao {
5656
@Query("SELECT * FROM conversations")
5757
suspend fun queryConversations(): List<Conversation>
5858

59-
@Query("SELECT EXISTS (SELECT * FROM messages WHERE conversationIdBase58 = :messageId AND content LIKE '%1|%')")
60-
suspend fun hasTipMessage(messageId: String): Boolean
59+
@Query("SELECT EXISTS (SELECT 1 FROM messages WHERE conversationIdBase58 = :messageId)")
60+
suspend fun hasInteracted(messageId: String): Boolean
6161

62-
suspend fun hasTipMessage(messageId: ID): Boolean {
63-
return hasTipMessage(messageId.base58)
64-
}
65-
66-
@Query("SELECT EXISTS (SELECT * FROM messages WHERE conversationIdBase58 = :messageId AND content LIKE '%2|%')")
67-
suspend fun hasThanked(messageId: String): Boolean
68-
69-
suspend fun hasThanked(messageId: ID): Boolean {
70-
return hasThanked(messageId.base58)
62+
suspend fun hasInteracted(messageId: ID): Boolean {
63+
return hasInteracted(messageId.base58)
7164
}
7265

7366
@Query("SELECT EXISTS (SELECT * FROM messages WHERE conversationIdBase58 = :messageId AND content LIKE '%4|%')")

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import com.codeinc.gen.chat.v2.ChatService
44
import com.getcode.model.chat.ChatMember
55
import com.getcode.model.chat.Identity
66
import com.getcode.model.chat.Pointer
7+
import com.getcode.model.uuid
8+
import java.util.UUID
79
import javax.inject.Inject
810

9-
class ChatMemberMapper @Inject constructor(): Mapper<ChatService.ChatMember, ChatMember> {
10-
override fun map(from: ChatService.ChatMember): ChatMember {
11+
class ChatMemberMapper @Inject constructor(): Mapper<ChatService.ChatMember, ChatMember?> {
12+
override fun map(from: ChatService.ChatMember): ChatMember? {
1113
return ChatMember(
12-
id = from.memberId.toByteArray().toList(),
14+
id = from.memberId.toByteArray().toList().uuid ?: return null,
1315
identity = runCatching { Identity(from.identity) }.getOrNull(),
1416
isMuted = from.isMuted,
1517
isSelf = from.isSelf,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ class ChatMessageV1Mapper @Inject constructor(
1515
override fun map(from: Pair<Chat, ApiChatMessage>): ChatMessage {
1616
val (chat, message) = from
1717

18-
val messageId = message.messageId.toByteArray().toList()
19-
val contents = message.contentList.mapNotNull { MessageContent.fromV1(it) }
18+
val messageId = message.messageId.value.toList()
19+
val contents = message.contentList.mapNotNull { MessageContent.fromV1(messageId, it) }
2020
val isFromSelf = contents.firstOrNull { it.isFromSelf } != null
2121

2222
return ChatMessage(

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.getcode.model.chat.Chat
66
import com.getcode.model.chat.ChatMessage
77
import com.getcode.model.chat.MessageContent
88
import com.getcode.model.chat.Pointer
9+
import com.getcode.model.uuid
910
import javax.inject.Inject
1011
import com.codeinc.gen.chat.v2.ChatService.ChatMessage as ApiChatMessage
1112
import com.getcode.model.chat.ChatMessage as DomainChatMessage
@@ -19,8 +20,8 @@ class ChatMessageV2Mapper @Inject constructor(
1920
val messageId = message.messageId.toByteArray().toList()
2021
val messageSenderId = message.senderId.toByteArray().toList()
2122
val selfMember = chat.members.firstOrNull { it.isSelf }
22-
val isFromSelf = selfMember?.id == messageSenderId
23-
val pointers = chat.members.firstOrNull { it.id == messageSenderId }?.pointers
23+
val isFromSelf = selfMember?.id == messageSenderId.uuid
24+
val pointers = chat.members.firstOrNull { it.id == messageSenderId.uuid }?.pointers
2425
val messagePointer = pointers?.find { it.id == messageId }
2526

2627
return ChatMessage(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class ChatMetadataV2Mapper @Inject constructor(
1616
cursor = from.cursor.value.toByteArray().toList(),
1717
canMute = from.canMute,
1818
canUnsubscribe = from.canUnsubscribe,
19-
members = from.membersList.map { chatMemberMapper.map(it) },
19+
members = from.membersList.mapNotNull { chatMemberMapper.map(it) },
2020
type = ChatType(from.type),
2121
messages = emptyList(),
2222
)

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

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
package com.getcode.mapper
22

3-
import com.getcode.model.chat.ChatMessage
43
import com.getcode.model.Conversation
54
import com.getcode.model.KinAmount
6-
import com.getcode.model.chat.MessageContent
75
import com.getcode.model.Rate
6+
import com.getcode.model.chat.Chat
7+
import com.getcode.model.chat.ChatMessage
8+
import com.getcode.model.chat.MessageContent
89
import com.getcode.model.orOneToOne
910
import com.getcode.network.exchange.Exchange
1011
import com.getcode.network.repository.base58
1112
import javax.inject.Inject
1213

1314
class ConversationMapper @Inject constructor(
1415
private val exchange: Exchange,
15-
) : Mapper<ChatMessage, Conversation> {
16-
override fun map(from: ChatMessage): Conversation {
17-
val exchangeMessage = from.contents.firstOrNull {
16+
) : Mapper<Pair<Chat, ChatMessage>, Conversation> {
17+
override fun map(from: Pair<Chat, ChatMessage>): Conversation {
18+
val (chat, message) = from
19+
val exchangeMessage = message.contents.firstOrNull {
1820
it is MessageContent.Exchange
1921
} as? MessageContent.Exchange
2022

@@ -25,14 +27,13 @@ class ConversationMapper @Inject constructor(
2527
KinAmount.newInstance(0, Rate.oneToOne)
2628
}
2729

28-
val identity = from.contents.filterIsInstance<MessageContent.IdentityRevealed>()
29-
.map { it.identity }.firstOrNull()
30+
val identity = chat.members.filterNot { it.isSelf }.firstNotNullOfOrNull { it.identity }
3031

3132
return Conversation(
32-
messageIdBase58 = from.id.base58,
33-
cursorBase58 = from.cursor.base58,
33+
messageIdBase58 = chat.id.base58,
34+
cursorBase58 = chat.cursor.base58,
3435
tipAmount = tipAmount,
35-
createdByUser = from.isFromSelf,
36+
createdByUser = true, // only tippee can create a conversation
3637
hasRevealedIdentity = identity != null,
3738
lastActivity = null, // TODO: ?
3839
user = identity?.username,

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

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

33
import com.codeinc.gen.chat.v2.ChatService
44
import com.getcode.model.ID
5+
import java.util.UUID
56

67
/**
78
* A user in a chat
@@ -21,7 +22,7 @@ import com.getcode.model.ID
2122
* Only valid when `isSelf = true`
2223
*/
2324
data class ChatMember(
24-
val id: ID,
25+
val id: UUID,
2526
val isSelf: Boolean,
2627
val identity: Identity?,
2728
val pointers: List<Pointer>,

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

Lines changed: 0 additions & 5 deletions
This file was deleted.

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ sealed interface MessageContent {
2929
val amount: GenericAmount,
3030
val verb: Verb,
3131
val reference: Reference,
32-
val didThank: Boolean,
32+
val hasInteracted: Boolean,
3333
override val isFromSelf: Boolean = false,
3434
) : MessageContent
3535

@@ -86,7 +86,7 @@ sealed interface MessageContent {
8686
amount = GenericAmount.Exact(kinAmount),
8787
verb = verb,
8888
reference = reference,
89-
didThank = false,
89+
hasInteracted = false,
9090
)
9191
}
9292

@@ -106,7 +106,7 @@ sealed interface MessageContent {
106106
amount = GenericAmount.Partial(fiat),
107107
verb = verb,
108108
reference = reference,
109-
didThank = false
109+
hasInteracted = false
110110
)
111111
}
112112

@@ -154,6 +154,7 @@ sealed interface MessageContent {
154154
}
155155

156156
fun fromV1(
157+
messageId: ID,
157158
proto: MessageContentV1,
158159
): MessageContent? {
159160
return when (proto.typeCase) {
@@ -181,8 +182,8 @@ sealed interface MessageContent {
181182
isFromSelf = isFromSelf,
182183
amount = GenericAmount.Exact(kinAmount),
183184
verb = verb,
184-
reference = Reference.NoneSet,
185-
didThank = false,
185+
reference = Reference.IntentId(messageId),
186+
hasInteracted = false,
186187
)
187188
}
188189

@@ -199,8 +200,8 @@ sealed interface MessageContent {
199200
isFromSelf = isFromSelf,
200201
amount = GenericAmount.Partial(fiat),
201202
verb = verb,
202-
reference = Reference.NoneSet,
203-
didThank = false
203+
reference = Reference.IntentId(messageId),
204+
hasInteracted = false
204205
)
205206
}
206207

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

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,18 @@ import androidx.paging.RemoteMediator
1010
import com.getcode.db.AppDatabase
1111
import com.getcode.db.Database
1212
import com.getcode.manager.SessionManager
13+
import com.getcode.mapper.ConversationMapper
1314
import com.getcode.model.chat.ChatType
1415
import com.getcode.model.Conversation
1516
import com.getcode.model.ConversationMessage
1617
import com.getcode.model.ID
18+
import com.getcode.model.chat.ChatMessage
19+
import com.getcode.model.chat.MessageContent
20+
import com.getcode.model.chat.Reference
1721
import com.getcode.network.client.ChatMessageStreamReference
1822
import com.getcode.network.client.Client
1923
import com.getcode.network.client.openChatStream
24+
import com.getcode.network.client.startChat
2025
import com.getcode.network.exchange.Exchange
2126
import com.getcode.network.repository.base58
2227
import com.getcode.utils.bytes
@@ -29,12 +34,12 @@ import kotlin.jvm.Throws
2934

3035
interface ConversationController {
3136
fun observeConversationForMessage(messageId: ID): Flow<Conversation?>
32-
suspend fun createConversation(identifier: ID, type: ChatType)
37+
suspend fun createConversation(identifier: ID, type: ChatType): Conversation
3338
suspend fun getConversation(identifier: ID): Conversation?
34-
fun openChatStream(scope: CoroutineScope, messageId: ID)
39+
suspend fun getOrCreateConversation(identifier: ID, type: ChatType): Conversation?
40+
fun openChatStream(scope: CoroutineScope, identifier: ID)
3541
fun closeChatStream()
36-
suspend fun hasThanked(messageId: ID): Boolean
37-
suspend fun thankTipper(messageId: ID)
42+
suspend fun hasInteracted(messageId: ID): Boolean
3843
suspend fun revealIdentity(messageId: ID)
3944
fun sendMessage(conversationId: ID, message: String)
4045
fun conversationPagingData(conversationId: ID): Flow<PagingData<ConversationMessage>>
@@ -43,7 +48,8 @@ interface ConversationController {
4348
class ConversationStreamController @Inject constructor(
4449
private val historyController: HistoryController,
4550
private val exchange: Exchange,
46-
private val client: Client
51+
private val client: Client,
52+
private val conversationMapper: ConversationMapper,
4753
): ConversationController {
4854
private val pagingConfig = PagingConfig(pageSize = 20)
4955

@@ -60,24 +66,50 @@ class ConversationStreamController @Inject constructor(
6066
return db.conversationDao().observeConversationForMessage(messageId)
6167
}
6268

63-
override suspend fun createConversation(identifier: ID, type: ChatType) {
69+
override suspend fun createConversation(identifier: ID, type: ChatType): Conversation {
70+
val owner = SessionManager.getOrganizer()?.ownerKeyPair ?: throw IllegalStateException()
71+
val lookup = { msg: ChatMessage ->
72+
msg.id == identifier ||
73+
msg.contents.filterIsInstance<MessageContent.Exchange>()
74+
.map { it.reference }
75+
.filterIsInstance<Reference.IntentId>()
76+
.any { it.id == identifier }
77+
}
6478

79+
val message = historyController.chats.value?.firstOrNull {
80+
it.messages.find { msg -> lookup(msg) } != null
81+
}?.messages?.firstOrNull { msg -> lookup(msg) } ?: throw IllegalArgumentException()
82+
83+
return client.startChat(owner, identifier, type)
84+
.map { conversationMapper.map(it to message) }
85+
.onSuccess {
86+
db.conversationDao().upsertConversations(it)
87+
// update chats
88+
historyController.fetchChats()
89+
}
90+
.getOrThrow()
6591
}
6692

6793
override suspend fun getConversation(identifier: ID): Conversation? {
68-
return null
94+
return db.conversationDao().findConversation(identifier)
95+
}
96+
97+
override suspend fun getOrCreateConversation(identifier: ID, type: ChatType): Conversation? {
98+
return getConversation(identifier) ?: createConversation(identifier, type)
6999
}
70100

71101
@Throws(IllegalStateException::class)
72-
override fun openChatStream(scope: CoroutineScope, messageId: ID) {
102+
override fun openChatStream(scope: CoroutineScope, identifier: ID) {
73103
val owner = SessionManager.getOrganizer()?.ownerKeyPair ?: throw IllegalStateException()
74104
val chat = historyController.chats.value?.firstOrNull {
75-
it.messages.find { msg -> msg.id == messageId } != null
105+
it.messages.find { msg -> msg.id == identifier } != null
76106
} ?: throw IllegalArgumentException()
77107

78-
stream = client.openChatStream(scope, chat, memberId.bytes, owner) { result ->
108+
stream = client.openChatStream(scope, chat, identifier, memberId.bytes, owner) { result ->
79109
if (result.isSuccess) {
80110
println("chat messages: ${result.getOrNull()}")
111+
} else {
112+
println("Error: ${result.exceptionOrNull()?.message}")
81113
}
82114
}
83115
}
@@ -86,12 +118,9 @@ class ConversationStreamController @Inject constructor(
86118
stream?.destroy()
87119
}
88120

89-
override suspend fun hasThanked(messageId: ID): Boolean {
121+
override suspend fun hasInteracted(messageId: ID): Boolean {
90122
val conversation = db.conversationDao().findConversationForMessage(messageId) ?: return false
91-
return db.conversationDao().hasThanked(conversation.messageId)
92-
}
93-
94-
override suspend fun thankTipper(messageId: ID) {
123+
return db.conversationDao().hasInteracted(conversation.messageId)
95124
}
96125

97126
override suspend fun revealIdentity(messageId: ID) {

0 commit comments

Comments
 (0)