Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
415 changes: 415 additions & 0 deletions api/schemas/com.getcode.db.AppDatabase/15.json

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion api/src/main/java/com/getcode/db/AppDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ import java.io.File
AutoMigration(from = 11, to = 12, spec = AppDatabase.Migration11To12::class),
AutoMigration(from = 12, to = 13, spec = AppDatabase.Migration12To13::class),
AutoMigration(from = 13, to = 14, spec = AppDatabase.Migration13To14::class),
AutoMigration(from = 14, to = 15, spec = AppDatabase.Migration14To15::class),
],
version = 14
version = 15
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
Expand Down Expand Up @@ -123,6 +124,18 @@ abstract class AppDatabase : RoomDatabase() {
)
)
class Migration13To14: AutoMigrationSpec

@DeleteColumn.Entries(
DeleteColumn(
tableName = "conversations",
columnName = "user"
),
DeleteColumn(
tableName = "conversations",
columnName = "userImage"
)
)
class Migration14To15: AutoMigrationSpec
}

object Database {
Expand Down
26 changes: 26 additions & 0 deletions api/src/main/java/com/getcode/db/Converters.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import androidx.room.TypeConverter
import com.getcode.model.CurrencyCode
import com.getcode.model.KinAmount
import com.getcode.model.Rate
import com.getcode.model.chat.ChatMember
import com.getcode.model.chat.MessageContent
import com.getcode.model.chat.Pointer
import com.getcode.network.repository.decodeBase64
import com.getcode.network.repository.encodeBase64
import kotlinx.serialization.decodeFromString
Expand Down Expand Up @@ -44,4 +46,28 @@ class Converters {

@TypeConverter
fun stringToKinAmount(value: String) = Json.decodeFromString(KinAmount.serializer(), value)

@TypeConverter
fun chatMemberToString(member: ChatMember) = Json.encodeToString(ChatMember.serializer(), member)

@TypeConverter
fun stringToChatMember(value: String) = Json.decodeFromString(ChatMember.serializer(), value)

@TypeConverter
fun chatMembersToString(members: List<ChatMember>) = Json.encodeToString(members)

@TypeConverter
fun stringToChatMembers(value: String) = Json.decodeFromString<List<ChatMember>>(value)

@TypeConverter
fun pointerToString(pointer: Pointer) = Json.encodeToString(pointer)

@TypeConverter
fun stringToPointer(value: String) = Json.decodeFromString<Pointer>(value)

@TypeConverter
fun pointersToString(pointer: List<Pointer>) = Json.encodeToString(pointer)

@TypeConverter
fun stringToPointers(value: String) = Json.decodeFromString<List<Pointer>>(value)
}
11 changes: 7 additions & 4 deletions api/src/main/java/com/getcode/mapper/ConversationMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.getcode.mapper

import com.getcode.model.Conversation
import com.getcode.model.chat.Chat
import com.getcode.model.chat.ChatType
import com.getcode.model.chat.self
import com.getcode.network.TipController
import com.getcode.network.localized
Expand All @@ -15,15 +16,17 @@ class ConversationMapper @Inject constructor(
override fun map(from: Chat): Conversation {

val self = from.self?.identity
val identity = from.members.filterNot { it.isSelf }.firstNotNullOfOrNull { it.identity }

return Conversation(
idBase58 = from.id.base58,
title = from.title.localized(resources),
title = when (from.type) {
ChatType.Unknown,
ChatType.Notification -> from.title.localized(resources)
ChatType.TwoWay -> null
},
hasRevealedIdentity = self != null,
members = from.members.map { it },
lastActivity = null, // TODO: ?
user = identity?.username,
userImage = null,
)
}
}
19 changes: 14 additions & 5 deletions api/src/main/java/com/getcode/model/Conversation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import androidx.room.Relation
import com.getcode.model.chat.ChatMember
import com.getcode.model.chat.MessageContent
import com.getcode.utils.serializer.MessageContentSerializer
import com.getcode.vendor.Base58
Expand All @@ -18,23 +19,31 @@ import java.util.UUID
data class Conversation(
@PrimaryKey
val idBase58: String,
@ColumnInfo(defaultValue = "Tip Chat")
val title: String,
val title: String?,
val hasRevealedIdentity: Boolean,
val user: String?,
val userImage: String?,
@ColumnInfo(defaultValue = "")
val members: List<ChatMember>,
val lastActivity: Long?,
) {
@Ignore
val id: ID = Base58.decode(idBase58).toList()

val name: String?
get() = nonSelfMembers
.mapNotNull { it.identity?.username }
.joinToString()
.takeIf { it.isNotEmpty() }

val nonSelfMembers: List<ChatMember>
get() = members.filterNot { it.isSelf }

override fun toString(): String {
return """
{
id:${idBase58},
title:$title,
hasRevealedIdentity:$hasRevealedIdentity,
user:$user,
members:${members.joinToString()}
}
""".trimIndent()
}
Expand Down
19 changes: 19 additions & 0 deletions api/src/main/java/com/getcode/model/chat/Chat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,25 @@ data class Chat(
val cursor: Cursor,
val messages: List<ChatMessage>
) {
val imageData: Any
get() {
return when (type) {
ChatType.Unknown -> id
ChatType.Notification -> id
ChatType.TwoWay -> {
members
.filterNot { it.isSelf }
.firstNotNullOf {
if (it.identity != null) {
it.identity.imageUrl.orEmpty()
} else {
it.id
}
}
}
}
}

val unreadCount: Int
get() {
if (!isV2) return _unreadCount
Expand Down
8 changes: 7 additions & 1 deletion api/src/main/java/com/getcode/model/chat/ChatMember.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.getcode.model.chat

import com.codeinc.gen.chat.v2.ChatService
import com.getcode.model.ID
import com.getcode.utils.serializer.UUIDSerializer
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.util.UUID

Expand All @@ -22,7 +24,9 @@ import java.util.UUID
* @param isSubscribed Is the chat member subscribed to this chat?
* Only valid when `isSelf = true`
*/
@Serializable
data class ChatMember(
@Serializable(with = UUIDSerializer::class)
val id: UUID,
val isSelf: Boolean,
val identity: Identity?,
Expand All @@ -42,13 +46,15 @@ data class ChatMember(
data class Identity(
val platform: Platform,
val username: String,
val imageUrl: String?
) {
companion object {
operator fun invoke(proto: ChatService.ChatMemberIdentity): Identity? {
val platform = Platform(proto.platform).takeIf { it != Platform.Unknown } ?: return null
return Identity(
platform = platform,
username = proto.username
username = proto.username,
imageUrl = null,
)
}
}
Expand Down
29 changes: 26 additions & 3 deletions api/src/main/java/com/getcode/model/chat/Pointer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,40 @@ package com.getcode.model.chat
import com.codeinc.gen.chat.v2.ChatService
import com.getcode.model.ID
import com.getcode.model.uuid
import com.getcode.utils.serializer.UUIDSerializer
import kotlinx.serialization.Serializable
import java.util.UUID

@Serializable
sealed interface Pointer {
val messageId: UUID?
val memberId: ID?

@Serializable
data class Unknown(override val memberId: ID) : Pointer {
@Serializable(with = UUIDSerializer::class)
override val messageId: UUID? = null
}
data class Sent(override val memberId: ID, override val messageId: UUID?): Pointer
data class Delivered(override val memberId: ID, override val messageId: UUID?): Pointer
data class Read(override val memberId: ID, override val messageId: UUID?) : Pointer
@Serializable
data class Sent(
override val memberId: ID,
@Serializable(with = UUIDSerializer::class)
override val messageId: UUID?
): Pointer

@Serializable
data class Delivered(
override val memberId: ID,
@Serializable(with = UUIDSerializer::class)
override val messageId: UUID?
): Pointer

@Serializable
data class Read(
override val memberId: ID,
@Serializable(with = UUIDSerializer::class)
override val messageId: UUID?
) : Pointer

companion object {
operator fun invoke(proto: ChatService.Pointer): Pointer {
Expand Down
12 changes: 10 additions & 2 deletions api/src/main/java/com/getcode/network/ConversationController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.getcode.network.exchange.Exchange
import com.getcode.network.repository.base58
import com.getcode.network.service.ChatServiceV2
import com.getcode.utils.ErrorUtils
import com.getcode.utils.bytes
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -143,11 +144,18 @@ class ConversationStreamController @Inject constructor(
.firstOrNull()
.takeIf { chat.isConversation }

if (identityRevealed != null && conversation.user == null) {
if (identityRevealed != null && conversation.members.isNotEmpty()) {
val members = conversation.members.map {
if (identityRevealed.memberId == it.id.bytes) {
it.copy(identity = identityRevealed.identity)
} else {
it
}
}
scope.launch(Dispatchers.IO) {
db.conversationDao()
.upsertConversations(
conversation.copy(user = identityRevealed.identity.username)
conversation.copy(members = members)
)
}
}
Expand Down
51 changes: 40 additions & 11 deletions api/src/main/java/com/getcode/network/HistoryController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import com.getcode.model.chat.ChatMessage
import com.getcode.model.Cursor
import com.getcode.model.ID
import com.getcode.model.MessageStatus
import com.getcode.model.chat.ChatMember
import com.getcode.model.chat.ChatType
import com.getcode.model.chat.Identity
import com.getcode.model.chat.Platform
import com.getcode.model.chat.Title
import com.getcode.model.chat.isConversation
import com.getcode.model.chat.selfId
Expand All @@ -43,6 +46,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import okhttp3.internal.toImmutableList
import timber.log.Timber
import java.util.Locale
Expand All @@ -54,6 +58,7 @@ import javax.inject.Singleton
class HistoryController @Inject constructor(
private val client: Client,
private val resources: ResourceHelper,
private val tipController: TipController,
private val conversationMapper: ConversationMapper,
private val conversationMessageMapper: ConversationMessageMapper,
) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
Expand Down Expand Up @@ -132,13 +137,15 @@ class HistoryController @Inject constructor(
}

containers.onEach { chat ->
val result = fetchLatestMessageForChat(chat)
val members = fetchMemberImages(chat)
val updatedChat = chat.copy(members = members)
val result = fetchLatestMessageForChat(updatedChat)
result.onSuccess { message ->
if (message != null) {
updatedWithMessages.add(chat.copy(messages = listOf(message)))
updatedWithMessages.add(updatedChat.copy(messages = listOf(message)))
}
}.onFailure {
updatedWithMessages.add(chat)
updatedWithMessages.add(updatedChat)
}
}

Expand All @@ -163,8 +170,8 @@ class HistoryController @Inject constructor(
to = newestMessage.id,
status = MessageStatus.Read
).onSuccess {
this[index] = chat.resetUnreadCount()
}
this[index] = chat.resetUnreadCount()
}
}
}
}?.toList()
Expand Down Expand Up @@ -234,7 +241,13 @@ class HistoryController @Inject constructor(
result.map { message -> conversationMessageMapper.map(chat.id to message) }
val memberId = chat.selfId ?: return@onSuccess
val latestRef = messages.maxBy { it.dateMillis }
client.advancePointer(owner, chat, latestRef.id, memberId, MessageStatus.Delivered)
client.advancePointer(
owner,
chat,
latestRef.id,
memberId,
MessageStatus.Delivered
)
}

}
Expand All @@ -251,11 +264,7 @@ class HistoryController @Inject constructor(
// map revealed identity as title if known
if (chat.isConversation) {
val conversation = conversationMapper.map(chat)
if (conversation.user != null) {
chat.copy(title = Title.Localized(conversation.user))
} else {
chat
}
conversation.name?.let { chat.copy(title = Title.Localized(it)) } ?: chat
} else {
chat
}
Expand All @@ -278,6 +287,26 @@ class HistoryController @Inject constructor(
}
return result.getOrNull().orEmpty()
}

private suspend fun fetchMemberImages(chat: Chat): List<ChatMember> {
return chat.members
.map { member ->
if (member.isSelf) return@map member
if (member.identity == null) return@map member
if (member.identity.imageUrl != null) return@map member
val metadata = runCatching {
tipController.fetch(member.identity.username)
}.getOrNull() ?: return@map member

member.copy(
identity = Identity(
platform = Platform.named(metadata.platform),
username = metadata.username,
imageUrl = metadata.imageUrl
)
)
}
}
}

fun Title?.localized(resources: ResourceHelper): String {
Expand Down
Loading