Skip to content

Commit 0c482dd

Browse files
authored
Merge pull request #489 from code-payments/feat/reveal-identity
feat: add user avatars and load in chat and balance
2 parents 99c4355 + 34bf804 commit 0c482dd

21 files changed

Lines changed: 1237 additions & 110 deletions

File tree

api/schemas/com.getcode.db.AppDatabase/15.json

Lines changed: 415 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ import java.io.File
4747
AutoMigration(from = 11, to = 12, spec = AppDatabase.Migration11To12::class),
4848
AutoMigration(from = 12, to = 13, spec = AppDatabase.Migration12To13::class),
4949
AutoMigration(from = 13, to = 14, spec = AppDatabase.Migration13To14::class),
50+
AutoMigration(from = 14, to = 15, spec = AppDatabase.Migration14To15::class),
5051
],
51-
version = 14
52+
version = 15
5253
)
5354
@TypeConverters(Converters::class)
5455
abstract class AppDatabase : RoomDatabase() {
@@ -123,6 +124,18 @@ abstract class AppDatabase : RoomDatabase() {
123124
)
124125
)
125126
class Migration13To14: AutoMigrationSpec
127+
128+
@DeleteColumn.Entries(
129+
DeleteColumn(
130+
tableName = "conversations",
131+
columnName = "user"
132+
),
133+
DeleteColumn(
134+
tableName = "conversations",
135+
columnName = "userImage"
136+
)
137+
)
138+
class Migration14To15: AutoMigrationSpec
126139
}
127140

128141
object Database {

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import androidx.room.TypeConverter
44
import com.getcode.model.CurrencyCode
55
import com.getcode.model.KinAmount
66
import com.getcode.model.Rate
7+
import com.getcode.model.chat.ChatMember
78
import com.getcode.model.chat.MessageContent
9+
import com.getcode.model.chat.Pointer
810
import com.getcode.network.repository.decodeBase64
911
import com.getcode.network.repository.encodeBase64
1012
import kotlinx.serialization.decodeFromString
@@ -44,4 +46,28 @@ class Converters {
4446

4547
@TypeConverter
4648
fun stringToKinAmount(value: String) = Json.decodeFromString(KinAmount.serializer(), value)
49+
50+
@TypeConverter
51+
fun chatMemberToString(member: ChatMember) = Json.encodeToString(ChatMember.serializer(), member)
52+
53+
@TypeConverter
54+
fun stringToChatMember(value: String) = Json.decodeFromString(ChatMember.serializer(), value)
55+
56+
@TypeConverter
57+
fun chatMembersToString(members: List<ChatMember>) = Json.encodeToString(members)
58+
59+
@TypeConverter
60+
fun stringToChatMembers(value: String) = Json.decodeFromString<List<ChatMember>>(value)
61+
62+
@TypeConverter
63+
fun pointerToString(pointer: Pointer) = Json.encodeToString(pointer)
64+
65+
@TypeConverter
66+
fun stringToPointer(value: String) = Json.decodeFromString<Pointer>(value)
67+
68+
@TypeConverter
69+
fun pointersToString(pointer: List<Pointer>) = Json.encodeToString(pointer)
70+
71+
@TypeConverter
72+
fun stringToPointers(value: String) = Json.decodeFromString<List<Pointer>>(value)
4773
}

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.getcode.mapper
22

33
import com.getcode.model.Conversation
44
import com.getcode.model.chat.Chat
5+
import com.getcode.model.chat.ChatType
56
import com.getcode.model.chat.self
67
import com.getcode.network.TipController
78
import com.getcode.network.localized
@@ -15,15 +16,17 @@ class ConversationMapper @Inject constructor(
1516
override fun map(from: Chat): Conversation {
1617

1718
val self = from.self?.identity
18-
val identity = from.members.filterNot { it.isSelf }.firstNotNullOfOrNull { it.identity }
1919

2020
return Conversation(
2121
idBase58 = from.id.base58,
22-
title = from.title.localized(resources),
22+
title = when (from.type) {
23+
ChatType.Unknown,
24+
ChatType.Notification -> from.title.localized(resources)
25+
ChatType.TwoWay -> null
26+
},
2327
hasRevealedIdentity = self != null,
28+
members = from.members.map { it },
2429
lastActivity = null, // TODO: ?
25-
user = identity?.username,
26-
userImage = null,
2730
)
2831
}
2932
}

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.room.Entity
66
import androidx.room.Ignore
77
import androidx.room.PrimaryKey
88
import androidx.room.Relation
9+
import com.getcode.model.chat.ChatMember
910
import com.getcode.model.chat.MessageContent
1011
import com.getcode.utils.serializer.MessageContentSerializer
1112
import com.getcode.vendor.Base58
@@ -18,23 +19,31 @@ import java.util.UUID
1819
data class Conversation(
1920
@PrimaryKey
2021
val idBase58: String,
21-
@ColumnInfo(defaultValue = "Tip Chat")
22-
val title: String,
22+
val title: String?,
2323
val hasRevealedIdentity: Boolean,
24-
val user: String?,
25-
val userImage: String?,
24+
@ColumnInfo(defaultValue = "")
25+
val members: List<ChatMember>,
2626
val lastActivity: Long?,
2727
) {
2828
@Ignore
2929
val id: ID = Base58.decode(idBase58).toList()
3030

31+
val name: String?
32+
get() = nonSelfMembers
33+
.mapNotNull { it.identity?.username }
34+
.joinToString()
35+
.takeIf { it.isNotEmpty() }
36+
37+
val nonSelfMembers: List<ChatMember>
38+
get() = members.filterNot { it.isSelf }
39+
3140
override fun toString(): String {
3241
return """
3342
{
3443
id:${idBase58},
3544
title:$title,
3645
hasRevealedIdentity:$hasRevealedIdentity,
37-
user:$user,
46+
members:${members.joinToString()}
3847
}
3948
""".trimIndent()
4049
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,25 @@ data class Chat(
3131
val cursor: Cursor,
3232
val messages: List<ChatMessage>
3333
) {
34+
val imageData: Any
35+
get() {
36+
return when (type) {
37+
ChatType.Unknown -> id
38+
ChatType.Notification -> id
39+
ChatType.TwoWay -> {
40+
members
41+
.filterNot { it.isSelf }
42+
.firstNotNullOf {
43+
if (it.identity != null) {
44+
it.identity.imageUrl.orEmpty()
45+
} else {
46+
it.id
47+
}
48+
}
49+
}
50+
}
51+
}
52+
3453
val unreadCount: Int
3554
get() {
3655
if (!isV2) return _unreadCount

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

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

33
import com.codeinc.gen.chat.v2.ChatService
44
import com.getcode.model.ID
5+
import com.getcode.utils.serializer.UUIDSerializer
6+
import kotlinx.serialization.Contextual
57
import kotlinx.serialization.Serializable
68
import java.util.UUID
79

@@ -22,7 +24,9 @@ import java.util.UUID
2224
* @param isSubscribed Is the chat member subscribed to this chat?
2325
* Only valid when `isSelf = true`
2426
*/
27+
@Serializable
2528
data class ChatMember(
29+
@Serializable(with = UUIDSerializer::class)
2630
val id: UUID,
2731
val isSelf: Boolean,
2832
val identity: Identity?,
@@ -42,13 +46,15 @@ data class ChatMember(
4246
data class Identity(
4347
val platform: Platform,
4448
val username: String,
49+
val imageUrl: String?
4550
) {
4651
companion object {
4752
operator fun invoke(proto: ChatService.ChatMemberIdentity): Identity? {
4853
val platform = Platform(proto.platform).takeIf { it != Platform.Unknown } ?: return null
4954
return Identity(
5055
platform = platform,
51-
username = proto.username
56+
username = proto.username,
57+
imageUrl = null,
5258
)
5359
}
5460
}

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,40 @@ package com.getcode.model.chat
33
import com.codeinc.gen.chat.v2.ChatService
44
import com.getcode.model.ID
55
import com.getcode.model.uuid
6+
import com.getcode.utils.serializer.UUIDSerializer
7+
import kotlinx.serialization.Serializable
68
import java.util.UUID
79

10+
@Serializable
811
sealed interface Pointer {
912
val messageId: UUID?
1013
val memberId: ID?
14+
15+
@Serializable
1116
data class Unknown(override val memberId: ID) : Pointer {
17+
@Serializable(with = UUIDSerializer::class)
1218
override val messageId: UUID? = null
1319
}
14-
data class Sent(override val memberId: ID, override val messageId: UUID?): Pointer
15-
data class Delivered(override val memberId: ID, override val messageId: UUID?): Pointer
16-
data class Read(override val memberId: ID, override val messageId: UUID?) : Pointer
20+
@Serializable
21+
data class Sent(
22+
override val memberId: ID,
23+
@Serializable(with = UUIDSerializer::class)
24+
override val messageId: UUID?
25+
): Pointer
26+
27+
@Serializable
28+
data class Delivered(
29+
override val memberId: ID,
30+
@Serializable(with = UUIDSerializer::class)
31+
override val messageId: UUID?
32+
): Pointer
33+
34+
@Serializable
35+
data class Read(
36+
override val memberId: ID,
37+
@Serializable(with = UUIDSerializer::class)
38+
override val messageId: UUID?
39+
) : Pointer
1740

1841
companion object {
1942
operator fun invoke(proto: ChatService.Pointer): Pointer {

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.getcode.network.exchange.Exchange
2525
import com.getcode.network.repository.base58
2626
import com.getcode.network.service.ChatServiceV2
2727
import com.getcode.utils.ErrorUtils
28+
import com.getcode.utils.bytes
2829
import kotlinx.coroutines.CoroutineScope
2930
import kotlinx.coroutines.Dispatchers
3031
import kotlinx.coroutines.flow.Flow
@@ -143,11 +144,18 @@ class ConversationStreamController @Inject constructor(
143144
.firstOrNull()
144145
.takeIf { chat.isConversation }
145146

146-
if (identityRevealed != null && conversation.user == null) {
147+
if (identityRevealed != null && conversation.members.isNotEmpty()) {
148+
val members = conversation.members.map {
149+
if (identityRevealed.memberId == it.id.bytes) {
150+
it.copy(identity = identityRevealed.identity)
151+
} else {
152+
it
153+
}
154+
}
147155
scope.launch(Dispatchers.IO) {
148156
db.conversationDao()
149157
.upsertConversations(
150-
conversation.copy(user = identityRevealed.identity.username)
158+
conversation.copy(members = members)
151159
)
152160
}
153161
}

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

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import com.getcode.model.chat.ChatMessage
1616
import com.getcode.model.Cursor
1717
import com.getcode.model.ID
1818
import com.getcode.model.MessageStatus
19+
import com.getcode.model.chat.ChatMember
1920
import com.getcode.model.chat.ChatType
21+
import com.getcode.model.chat.Identity
22+
import com.getcode.model.chat.Platform
2023
import com.getcode.model.chat.Title
2124
import com.getcode.model.chat.isConversation
2225
import com.getcode.model.chat.selfId
@@ -43,6 +46,7 @@ import kotlinx.coroutines.flow.asStateFlow
4346
import kotlinx.coroutines.flow.filterNotNull
4447
import kotlinx.coroutines.flow.map
4548
import kotlinx.coroutines.flow.update
49+
import kotlinx.coroutines.launch
4650
import okhttp3.internal.toImmutableList
4751
import timber.log.Timber
4852
import java.util.Locale
@@ -54,6 +58,7 @@ import javax.inject.Singleton
5458
class HistoryController @Inject constructor(
5559
private val client: Client,
5660
private val resources: ResourceHelper,
61+
private val tipController: TipController,
5762
private val conversationMapper: ConversationMapper,
5863
private val conversationMessageMapper: ConversationMessageMapper,
5964
) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
@@ -132,13 +137,15 @@ class HistoryController @Inject constructor(
132137
}
133138

134139
containers.onEach { chat ->
135-
val result = fetchLatestMessageForChat(chat)
140+
val members = fetchMemberImages(chat)
141+
val updatedChat = chat.copy(members = members)
142+
val result = fetchLatestMessageForChat(updatedChat)
136143
result.onSuccess { message ->
137144
if (message != null) {
138-
updatedWithMessages.add(chat.copy(messages = listOf(message)))
145+
updatedWithMessages.add(updatedChat.copy(messages = listOf(message)))
139146
}
140147
}.onFailure {
141-
updatedWithMessages.add(chat)
148+
updatedWithMessages.add(updatedChat)
142149
}
143150
}
144151

@@ -163,8 +170,8 @@ class HistoryController @Inject constructor(
163170
to = newestMessage.id,
164171
status = MessageStatus.Read
165172
).onSuccess {
166-
this[index] = chat.resetUnreadCount()
167-
}
173+
this[index] = chat.resetUnreadCount()
174+
}
168175
}
169176
}
170177
}?.toList()
@@ -234,7 +241,13 @@ class HistoryController @Inject constructor(
234241
result.map { message -> conversationMessageMapper.map(chat.id to message) }
235242
val memberId = chat.selfId ?: return@onSuccess
236243
val latestRef = messages.maxBy { it.dateMillis }
237-
client.advancePointer(owner, chat, latestRef.id, memberId, MessageStatus.Delivered)
244+
client.advancePointer(
245+
owner,
246+
chat,
247+
latestRef.id,
248+
memberId,
249+
MessageStatus.Delivered
250+
)
238251
}
239252

240253
}
@@ -251,11 +264,7 @@ class HistoryController @Inject constructor(
251264
// map revealed identity as title if known
252265
if (chat.isConversation) {
253266
val conversation = conversationMapper.map(chat)
254-
if (conversation.user != null) {
255-
chat.copy(title = Title.Localized(conversation.user))
256-
} else {
257-
chat
258-
}
267+
conversation.name?.let { chat.copy(title = Title.Localized(it)) } ?: chat
259268
} else {
260269
chat
261270
}
@@ -278,6 +287,26 @@ class HistoryController @Inject constructor(
278287
}
279288
return result.getOrNull().orEmpty()
280289
}
290+
291+
private suspend fun fetchMemberImages(chat: Chat): List<ChatMember> {
292+
return chat.members
293+
.map { member ->
294+
if (member.isSelf) return@map member
295+
if (member.identity == null) return@map member
296+
if (member.identity.imageUrl != null) return@map member
297+
val metadata = runCatching {
298+
tipController.fetch(member.identity.username)
299+
}.getOrNull() ?: return@map member
300+
301+
member.copy(
302+
identity = Identity(
303+
platform = Platform.named(metadata.platform),
304+
username = metadata.username,
305+
imageUrl = metadata.imageUrl
306+
)
307+
)
308+
}
309+
}
281310
}
282311

283312
fun Title?.localized(resources: ResourceHelper): String {

0 commit comments

Comments
 (0)