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
2 changes: 0 additions & 2 deletions api/src/main/java/com/getcode/db/ConversationDao.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package com.getcode.db

import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RewriteQueriesToDropUnusedColumns
import androidx.room.Transaction
import com.getcode.model.Conversation
import com.getcode.model.ConversationWithLastPointers
import com.getcode.model.ID
Expand Down
3 changes: 3 additions & 0 deletions api/src/main/java/com/getcode/model/Fiat.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.getcode.model

import kotlinx.serialization.Serializable

sealed interface Value

@Serializable
data class Fiat(
val currency: CurrencyCode,
val amount: Double,
Expand Down
2 changes: 2 additions & 0 deletions api/src/main/java/com/getcode/model/KinAmount.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ data class KinAmount(
}

companion object {
val Zero = newInstance(0, Rate.oneToOne)

fun newInstance(kin: Int, rate: Rate): KinAmount {
return newInstance(fromKin(kin), rate)
}
Expand Down
8 changes: 6 additions & 2 deletions api/src/main/java/com/getcode/model/TwitterUser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ data class TwitterUser(
override val imageUrl: String?,
val displayName: String,
val followerCount: Int,
val verificationStatus: VerificationStatus
val verificationStatus: VerificationStatus,
val costOfFriendship: Fiat,
val isFriend: Boolean,
): TipMetadata {

override val platform: String = "X"
Expand Down Expand Up @@ -52,7 +54,9 @@ data class TwitterUser(
imageUrl = avatarUrl,
followerCount = proto.followerCount,
tipAddress = tipAddress,
verificationStatus = VerificationStatus.entries.getOrNull(proto.verifiedTypeValue) ?: VerificationStatus.unknown
verificationStatus = VerificationStatus.entries.getOrNull(proto.verifiedTypeValue) ?: VerificationStatus.unknown,
costOfFriendship = Fiat(currency = CurrencyCode.USD, amount = 1.00),
isFriend = proto.isFriend
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.getcode.network

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.getcode.model.chat.Chat
import javax.inject.Inject

class ConversationListController @Inject constructor(
private val historyController: ChatHistoryController,
) {
private val pagingConfig = PagingConfig(pageSize = 20)

fun observeConversations() =
Pager(
config = pagingConfig,
initialKey = null,
) { ChatPagingSource(historyController.chats.value.orEmpty()) }.flow
}

class ChatPagingSource(
private val chats: List<Chat>
) : PagingSource<Int, Chat>() {

override fun getRefreshKey(state: PagingState<Int, Chat>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Chat> {
val currentList = chats
val position = params.key ?: 0
val pageSize = params.loadSize

return try {
val items = currentList.subList(
position.coerceAtMost(currentList.size),
(position + pageSize).coerceAtMost(currentList.size)
)

LoadResult.Page(
data = items,
prevKey = if (position > 0) position - pageSize else null,
nextKey = if (position + pageSize < currentList.size) position + pageSize else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
9 changes: 6 additions & 3 deletions api/src/main/java/com/getcode/network/TipController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import kotlin.concurrent.fixedRateTimer

typealias TipUser = Pair<String, CodePayload>


@Singleton
class TipController @Inject constructor(
private val client: Client,
Expand Down Expand Up @@ -108,10 +109,11 @@ class TipController @Inject constructor(

private suspend fun callForConnectedUser() {
Timber.d("twitter poll call")
val tipAddress = SessionManager.getOrganizer()?.primaryVault ?: return
val organizer = SessionManager.getOrganizer() ?: return
val tipAddress = organizer.primaryVault
// only set lastPoll if we actively attempt to reach RPC
lastPoll = System.currentTimeMillis()
client.fetchTwitterUser(tipAddress)
client.fetchTwitterUser(organizer, tipAddress)
.onSuccess {
Timber.d("current user twitter connected @ ${it.username}")
prefRepository.set(PrefsString.KEY_TIP_ACCOUNT, Json.encodeToString(it))
Expand Down Expand Up @@ -152,10 +154,11 @@ class TipController @Inject constructor(
}

suspend fun fetch(username: String): TwitterUser? {
val organizer = SessionManager.getOrganizer() ?: return null
val key = username.lowercase()
return cachedUsers.getOrPutIfNonNull(key) {
Timber.d("fetching user $username")
client.fetchTwitterUser(username).getOrThrow()
client.fetchTwitterUser(organizer, username).getOrThrow()
}
}

Expand Down
38 changes: 38 additions & 0 deletions api/src/main/java/com/getcode/network/TwitterUserController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.getcode.network

import com.getcode.manager.SessionManager
import com.getcode.model.TwitterUser
import com.getcode.network.client.Client
import com.getcode.network.client.fetchTwitterUser
import com.getcode.network.repository.BetaFlagsRepository
import com.getcode.network.repository.PrefRepository
import com.getcode.utils.getOrPutIfNonNull
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class TwitterUserController @Inject constructor(
private val client: Client,
betaFlags: BetaFlagsRepository,
private val prefRepository: PrefRepository,
) {
private var cachedUsers = mutableMapOf<String, TwitterUser>()


suspend fun fetchUser(username: String, ignoreCache: Boolean = false): TwitterUser? {
val organizer = SessionManager.getOrganizer() ?: return null
val key = username.lowercase()

if (ignoreCache) {
val user = client.fetchTwitterUser(organizer, username).getOrThrow()
cachedUsers[key] = user
return user
}

return cachedUsers.getOrPutIfNonNull(key) {
Timber.d("fetching user $username")
client.fetchTwitterUser(organizer, username).getOrThrow()
}
}
}
10 changes: 3 additions & 7 deletions api/src/main/java/com/getcode/network/api/ChatApiV2.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,15 @@ import com.codeinc.gen.common.v1.Model
import com.getcode.ed25519.Ed25519.KeyPair
import com.getcode.model.Cursor
import com.getcode.model.ID
import com.getcode.model.chat.Chat
import com.getcode.model.chat.OutgoingMessageContent
import com.getcode.model.chat.Platform
import com.getcode.model.chat.StartChatRequest
import com.getcode.model.chat.StartChatResponse
import com.getcode.model.description
import com.getcode.network.core.GrpcApi
import com.getcode.network.repository.toByteString
import com.getcode.network.repository.toSolanaAccount
import com.getcode.utils.TraceType
import com.getcode.utils.bytes
import com.getcode.utils.sign
import com.getcode.utils.trace
import io.grpc.ManagedChannel
import io.grpc.stub.StreamObserver
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -54,11 +50,11 @@ class ChatApiV2 @Inject constructor(
) : GrpcApi(managedChannel) {
private val api = ChatGrpc.newStub(managedChannel)

fun createTipChat(owner: KeyPair, intentId: ID): Flow<StartChatResponse> {
fun startChat(owner: KeyPair, intentId: ID): Flow<StartChatResponse> {
val request = StartChatRequest.newBuilder()
.setOwner(owner.publicKeyBytes.toSolanaAccount())
.setTipChat(
ChatService.StartTipChatParameters.newBuilder()
.setTwoWayChat(
ChatService.StartTwoWayChatParameters.newBuilder()
.setIntentId(IntentId.newBuilder()
.setValue(intentId.toByteString()))
.build()
Expand Down
14 changes: 10 additions & 4 deletions api/src/main/java/com/getcode/network/client/Client_Identity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@ suspend fun Client.updatePreferences(organizer: Organizer): Result<Boolean> {
)
}

suspend fun Client.fetchTwitterUser(username: String): Result<TwitterUser> {
return identityRepository.fetchTwitterUserByUsername(username)
suspend fun Client.fetchTwitterUser(
organizer: Organizer,
username: String
): Result<TwitterUser> {
return identityRepository.fetchTwitterUserByUsername(organizer.ownerKeyPair, username)
}

suspend fun Client.fetchTwitterUser(address: PublicKey): Result<TwitterUser> {
return identityRepository.fetchTwitterUserByAddress(address)
suspend fun Client.fetchTwitterUser(
organizer: Organizer,
address: PublicKey
): Result<TwitterUser> {
return identityRepository.fetchTwitterUserByAddress(organizer.ownerKeyPair, address)
}
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,10 @@ class IdentityRepository @Inject constructor(
}
}

suspend fun fetchTwitterUserByUsername(username: String): Result<TwitterUser> {
suspend fun fetchTwitterUserByUsername(owner: KeyPair, username: String): Result<TwitterUser> {
val request = GetTwitterUserRequest.newBuilder()
.setUsername(username)
.setRequestor(owner.publicKeyBytes.toSolanaAccount())
.build()

return try {
Expand Down Expand Up @@ -349,9 +350,10 @@ class IdentityRepository @Inject constructor(
}
}

suspend fun fetchTwitterUserByAddress(address: PublicKey): Result<TwitterUser> {
suspend fun fetchTwitterUserByAddress(owner: KeyPair, address: PublicKey): Result<TwitterUser> {
val request = GetTwitterUserRequest.newBuilder()
.setTipAddress(address.byteArray.toSolanaAccount())
.setRequestor(owner.publicKeyBytes.toSolanaAccount())
.build()

return try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ class ChatServiceV2 @Inject constructor(
ChatType.Notification -> throw IllegalArgumentException("Unable to create notification chats from client")
ChatType.TwoWay -> {
try {
networkOracle.managedRequest(api.createTipChat(owner, intentId))
networkOracle.managedRequest(api.startChat(owner, intentId))
.map { response ->
when (response.result) {
ChatService.StartChatResponse.Result.OK -> {
Expand Down
17 changes: 12 additions & 5 deletions app/src/main/java/com/getcode/Session.kt
Original file line number Diff line number Diff line change
Expand Up @@ -937,11 +937,18 @@ class Session @Inject constructor(
}
}

fun presentTipConfirmation(amount: KinAmount) {
val data = tipController.scannedUserData ?: return
val (_, payload) = data
fun presentTipConfirmation(amount: KinAmount, user: TwitterUser? = null) {
val scannedUserData = tipController.scannedUserData?.second
val payload = if (user != null) {
CodePayload(
kind = Kind.Tip,
value = Username(user.username)
)
} else {
scannedUserData
} ?: return

val metadata = tipController.userMetadata ?: return
val metadata = user ?: tipController.userMetadata ?: return
uiFlow.update {
val billState = it.billState.copy(
tipConfirmation = TipConfirmation(
Expand Down Expand Up @@ -1252,7 +1259,7 @@ class Session @Inject constructor(
presentationStyle = PresentationStyle.Pop,
billState = it.billState.copy(
bill = Bill.Login(
amount = KinAmount.newInstance(Kin.fromKin(0), Rate.oneToOne),
amount = KinAmount.Zero,
payload = payload,
request = request,
),
Expand Down
23 changes: 20 additions & 3 deletions app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.hilt.getViewModel
import com.getcode.R
import com.getcode.model.ID
import com.getcode.model.TwitterUser
import com.getcode.model.chat.Reference
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.theme.CodeTheme
Expand All @@ -43,6 +44,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue

@Parcelize
data object ChatListModal: ChatGraph, ModalRoot {
Expand All @@ -59,8 +61,8 @@ data object ChatListModal: ChatGraph, ModalRoot {
closeButtonEnabled = { it is ChatListModal },
) {
val viewModel = getViewModel<ChatListViewModel>()
// val conversations = viewModel.conversations.collectAsLazyPagingItems()
ChatListScreen(viewModel)
val conversations = viewModel.conversations.collectAsLazyPagingItems()
ChatListScreen(viewModel, conversations)
}
}
}
Expand All @@ -85,7 +87,7 @@ data object ChatByUsernameScreen: ChatGraph, ModalContent {

@Parcelize
data class ChatMessageConversationScreen(
val username: String? = null,
val user: @RawValue TwitterUser? = null,
val chatId: ID? = null,
val intentId: ID? = null
) : AppScreen(), ChatGraph, ModalContent {
Expand Down Expand Up @@ -153,6 +155,13 @@ data class ChatMessageConversationScreen(

},
backButtonEnabled = { it is ChatMessageConversationScreen },
onBackClicked = {
if (state.twitterUser != null) {
navigator.popUntil { it is ChatListModal }
} else {
navigator.pop()
}
}
) {
val messages = vm.messages.collectAsLazyPagingItems()
ChatConversationScreen(state, messages, vm::dispatchEvent)
Expand All @@ -178,6 +187,14 @@ data class ChatMessageConversationScreen(
}.launchIn(this)
}

LaunchedEffect(user) {
if (user != null) {
vm.dispatchEvent(
ConversationViewModel.Event.OnTwitterUserChanged(user)
)
}
}

LaunchedEffect(chatId) {
if (chatId != null) {
vm.dispatchEvent(
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/getcode/util/Currency.kt
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,10 @@ fun formatAmountString(
} else {
when {
currency.code == currency.symbol -> {
"${FormatUtils.format(amount)} $suffix"
FormatUtils.format(amount) + if (suffix.isNotEmpty()) " $suffix" else ""
}
else -> {
"${currency.symbol}${FormatUtils.format(amount)} $suffix"
"${currency.symbol}${FormatUtils.format(amount)}" + if (suffix.isNotEmpty()) " $suffix" else ""
}
}
}
Expand Down
Loading