Skip to content

Commit 65af1ed

Browse files
authored
Merge pull request #568 from code-payments/feat/chat-with-code-users
feat: implement pay to chat with code users (first pass)
2 parents 30e4633 + c3cc547 commit 65af1ed

50 files changed

Lines changed: 1041 additions & 500 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/src/androidTest/java/com/getcode/models/intents/IntentPrivateTransferTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ class IntentPrivateTransferTest {
5252
val rendezvous = PublicKey.generate()
5353

5454
val intent = IntentPrivateTransfer.newInstance(
55-
context = context,
5655
rendezvousKey = rendezvous,
5756
organizer = organizer,
5857
destination = destination,

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ sealed class IntentMetadata {
2121
metadata.receivePaymentsPublicly.exchangeData.currency,
2222
metadata.receivePaymentsPublicly.exchangeData.quarks,
2323
metadata.receivePaymentsPublicly.exchangeData.exchangeRate,
24+
metadata.sendPrivatePayment.isChat,
2425
)?.let { ReceivePaymentsPublicly(it) }
2526
}
2627
TransactionService.Metadata.TypeCase.UPGRADE_PRIVACY -> UpgradePrivacy
@@ -30,13 +31,15 @@ sealed class IntentMetadata {
3031
metadata.sendPrivatePayment.exchangeData.currency,
3132
metadata.sendPrivatePayment.exchangeData.quarks,
3233
metadata.sendPrivatePayment.exchangeData.exchangeRate,
34+
metadata.sendPrivatePayment.isChat,
3335
)?.let { SendPrivatePayment(it) }
3436
}
3537
TransactionService.Metadata.TypeCase.SEND_PUBLIC_PAYMENT -> {
3638
getPaymentMetadata(
3739
metadata.sendPublicPayment.exchangeData.currency,
3840
metadata.sendPrivatePayment.exchangeData.quarks,
3941
metadata.sendPublicPayment.exchangeData.exchangeRate,
42+
metadata.sendPrivatePayment.isChat,
4043
)?.let { SendPublicPayment(it) }
4144
}
4245
else -> null
@@ -47,6 +50,7 @@ sealed class IntentMetadata {
4750
currencyString: String,
4851
quarks: Long,
4952
exchangeRate: Double,
53+
isChat: Boolean,
5054
): PaymentMetadata? {
5155
val currency = CurrencyCode.tryValueOf(currencyString.uppercase())
5256
?: return null
@@ -58,12 +62,14 @@ sealed class IntentMetadata {
5862
fx = exchangeRate,
5963
currency = currency
6064
)
61-
)
65+
),
66+
isChat = isChat,
6267
)
6368
}
6469
}
6570
}
6671

6772
data class PaymentMetadata(
68-
val amount: KinAmount
73+
val amount: KinAmount,
74+
val isChat: Boolean,
6975
)

api/src/main/java/com/getcode/model/TipMetadata.kt renamed to api/src/main/java/com/getcode/model/SocialUser.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ import com.getcode.utils.serializer.PublicKeyAsStringSerializer
55
import kotlinx.serialization.Serializable
66

77
@Serializable
8-
sealed interface TipMetadata {
8+
sealed interface SocialUser {
99
val platform: String
1010
val username: String
1111
@Serializable(with = PublicKeyAsStringSerializer::class)
1212
val tipAddress: PublicKey
1313
val imageUrl: String?
1414

1515
val imageUrlSanitized: String?
16+
17+
val costOfFriendship: Fiat
18+
val isFriend: Boolean
19+
val chatId: ID
1620
}

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,11 @@ package com.getcode.model
22

33
import android.webkit.MimeTypeMap
44
import com.codeinc.gen.user.v1.IdentityService
5+
import com.codeinc.gen.user.v1.friendChatIdOrNull
6+
import com.codeinc.gen.user.v1.friendshipCostOrNull
57
import com.getcode.solana.keys.PublicKey
6-
import com.getcode.solana.keys.base58
78
import com.getcode.utils.serializer.PublicKeyAsStringSerializer
8-
import kotlinx.serialization.KSerializer
99
import kotlinx.serialization.Serializable
10-
import kotlinx.serialization.descriptors.PrimitiveKind
11-
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
12-
import kotlinx.serialization.descriptors.SerialDescriptor
13-
import kotlinx.serialization.encoding.Decoder
14-
import kotlinx.serialization.encoding.Encoder
1510

1611
@Serializable
1712
data class TwitterUser(
@@ -22,9 +17,10 @@ data class TwitterUser(
2217
val displayName: String,
2318
val followerCount: Int,
2419
val verificationStatus: VerificationStatus,
25-
val costOfFriendship: Fiat,
26-
val isFriend: Boolean,
27-
): TipMetadata {
20+
override val costOfFriendship: Fiat,
21+
override val isFriend: Boolean,
22+
override val chatId: ID,
23+
): SocialUser {
2824

2925
override val platform: String = "X"
3026

@@ -55,8 +51,12 @@ data class TwitterUser(
5551
followerCount = proto.followerCount,
5652
tipAddress = tipAddress,
5753
verificationStatus = VerificationStatus.entries.getOrNull(proto.verifiedTypeValue) ?: VerificationStatus.unknown,
58-
costOfFriendship = Fiat(currency = CurrencyCode.USD, amount = 1.00),
59-
isFriend = proto.isFriend
54+
costOfFriendship = proto.friendshipCostOrNull?.let {
55+
val currency = CurrencyCode.tryValueOf(it.currency) ?: return@let null
56+
Fiat(currency, it.nativeAmount)
57+
} ?: Fiat(currency = CurrencyCode.USD, amount = 1.00),
58+
isFriend = proto.isFriend,
59+
chatId = proto.friendChatId.value.toList()
6060
)
6161
}
6262
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ typealias ChatGrpcV1 = com.codeinc.gen.chat.v1.ChatGrpc
66
typealias ChatGrpcV2 = com.codeinc.gen.chat.v2.ChatGrpc
77

88
typealias ChatIdV1 = com.codeinc.gen.chat.v1.ChatService.ChatId
9-
typealias ChatIdV2 = com.codeinc.gen.chat.v2.ChatService.ChatId
9+
typealias ChatIdV2 = com.codeinc.gen.common.v1.Model.ChatId
1010

1111
typealias MessageContentV1 = com.codeinc.gen.chat.v1.ChatService.Content
1212
typealias MessageContentV2 = com.codeinc.gen.chat.v2.ChatService.Content

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ enum class Platform {
1212
}
1313

1414
fun named(name: String): Platform {
15-
return entries.firstOrNull { it.name.lowercase() == name.lowercase() } ?: Unknown
15+
val normalizedName = name.lowercase()
16+
return entries.firstOrNull {
17+
it.name.lowercase() == normalizedName ||
18+
(normalizedName == "x" && it.name.lowercase() == "twitter")
19+
} ?: Unknown
1620
}
1721
}
1822
}

api/src/main/java/com/getcode/model/intents/IntentPrivateTransfer.kt

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,32 @@
11
package com.getcode.model.intents
22

3-
import android.content.Context
3+
import com.codeinc.gen.chat.v2.ChatService
44
import com.codeinc.gen.transaction.v2.TransactionService
5-
import com.codeinc.gen.transaction.v2.TransactionService.TippedUser.Platform
6-
import com.getcode.model.TipMetadata
75
import com.getcode.model.Fee
6+
import com.getcode.model.ID
87
import com.getcode.model.Kin
98
import com.getcode.model.KinAmount
9+
import com.getcode.model.SocialUser
10+
import com.getcode.model.chat.ChatIdV2
11+
import com.getcode.model.chat.Platform
1012
import com.getcode.model.intents.actions.ActionFeePayment
1113
import com.getcode.model.intents.actions.ActionOpenAccount
1214
import com.getcode.model.intents.actions.ActionTransfer
1315
import com.getcode.model.intents.actions.ActionWithdraw
16+
import com.getcode.network.repository.toByteString
1417
import com.getcode.network.repository.toPublicKey
1518
import com.getcode.network.repository.toSolanaAccount
16-
import com.getcode.solana.keys.*
19+
import com.getcode.solana.keys.PublicKey
1720
import com.getcode.solana.organizer.AccountType
1821
import com.getcode.solana.organizer.Organizer
1922
import com.getcode.solana.organizer.Tray
2023
import timber.log.Timber
2124

25+
sealed interface PrivateTransferMetadata {
26+
data class Tip(val socialUser: SocialUser): PrivateTransferMetadata
27+
data class Chat(val socialUser: SocialUser): PrivateTransferMetadata
28+
}
29+
2230
class IntentPrivateTransfer(
2331
override val id: PublicKey,
2432
private val organizer: Organizer,
@@ -30,7 +38,7 @@ class IntentPrivateTransfer(
3038
private val fee: Kin,
3139
private val additionalFees: List<Fee>,
3240
private val isWithdrawal: Boolean,
33-
private val tipMetadata: TipMetadata?,
41+
private val metadata: PrivateTransferMetadata?,
3442
val resultTray: Tray,
3543

3644
override val actionGroup: ActionGroup,
@@ -49,12 +57,24 @@ class IntentPrivateTransfer(
4957
.setNativeAmount(grossAmount.fiat)
5058
)
5159

52-
if (tipMetadata != null) {
53-
setIsTip(true)
54-
setTippedUser(TransactionService.TippedUser.newBuilder()
55-
.setPlatformValue(Platform.TWITTER_VALUE)
56-
.setUsername(tipMetadata.username)
57-
)
60+
when (metadata) {
61+
is PrivateTransferMetadata.Chat -> {
62+
setIsChat(true)
63+
setChatId(ChatIdV2.newBuilder()
64+
.setValue(metadata.socialUser.chatId.toByteString())
65+
)
66+
}
67+
is PrivateTransferMetadata.Tip -> {
68+
setIsTip(true)
69+
setTippedUser(TransactionService.TippedUser.newBuilder()
70+
.setPlatformValue(when (Platform.named(metadata.socialUser.platform)) {
71+
Platform.Unknown -> ChatService.Platform.UNKNOWN_PLATFORM_VALUE
72+
Platform.Twitter -> ChatService.Platform.TWITTER_VALUE
73+
})
74+
.setUsername(metadata.socialUser.username)
75+
)
76+
}
77+
null -> Unit
5878
}
5979
}
6080
)
@@ -63,15 +83,14 @@ class IntentPrivateTransfer(
6383

6484
companion object {
6585
fun newInstance(
66-
context: Context,
6786
rendezvousKey: PublicKey,
6887
organizer: Organizer,
6988
destination: PublicKey,
7089
amount: KinAmount,
7190
fee: Kin,
7291
additionalFees: List<Fee>,
7392
isWithdrawal: Boolean,
74-
tipMetadata: TipMetadata?
93+
metadata: PrivateTransferMetadata?,
7594
): IntentPrivateTransfer {
7695
if (fee > amount.kin) {
7796
throw IntentPrivateTransferException.InvalidFeeException()
@@ -153,7 +172,7 @@ class IntentPrivateTransfer(
153172
kind = ActionWithdraw.Kind.NoPrivacyWithdraw(netAmount.kin),
154173
cluster = currentTray.outgoing.getCluster(),
155174
destination = destination,
156-
tipMetadata = tipMetadata
175+
metadata = metadata
157176
)
158177

159178
// 3. Redistribute the funds to optimize for a
@@ -217,7 +236,7 @@ class IntentPrivateTransfer(
217236
fee = fee,
218237
additionalFees = additionalFees,
219238
isWithdrawal = isWithdrawal,
220-
tipMetadata = tipMetadata,
239+
metadata = metadata,
221240
actionGroup = group,
222241
resultTray = currentTray,
223242
)

api/src/main/java/com/getcode/model/intents/actions/ActionWithdraw.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ package com.getcode.model.intents.actions
22

33
import com.codeinc.gen.transaction.v2.TransactionService
44
import com.getcode.ed25519.Ed25519
5-
import com.getcode.model.TipMetadata
5+
import com.getcode.model.SocialUser
66
import com.getcode.model.Kin
7+
import com.getcode.model.intents.PrivateTransferMetadata
78
import com.getcode.model.intents.ServerParameter
89
import com.getcode.network.repository.toPublicKey
910
import com.getcode.network.repository.toSolanaAccount
@@ -23,7 +24,7 @@ class ActionWithdraw(
2324
val cluster: AccountCluster,
2425
val destination: PublicKey,
2526
val legacy: Boolean,
26-
val tipMetadata: TipMetadata? = null,
27+
val metadata: PrivateTransferMetadata? = null,
2728
) : ActionType() {
2829

2930
override fun transactions(): List<SolanaTransaction> {
@@ -37,7 +38,7 @@ class ActionWithdraw(
3738
recentBlockhash = config.blockhash,
3839
kreIndex = kreIndex,
3940
legacy = legacy,
40-
tipMetadata = tipMetadata,
41+
metadata = metadata,
4142
)
4243
}.orEmpty()
4344
}
@@ -87,7 +88,7 @@ class ActionWithdraw(
8788
cluster: AccountCluster,
8889
destination: PublicKey,
8990
legacy: Boolean = false,
90-
tipMetadata: TipMetadata? = null,
91+
metadata: PrivateTransferMetadata? = null,
9192
): ActionWithdraw {
9293
return ActionWithdraw(
9394
id = 0,
@@ -97,7 +98,7 @@ class ActionWithdraw(
9798
cluster = cluster,
9899
destination = destination,
99100
legacy = legacy,
100-
tipMetadata = tipMetadata
101+
metadata = metadata
101102
)
102103
}
103104

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.getcode.ed25519.Ed25519.KeyPair
1111
import com.getcode.manager.SessionManager
1212
import com.getcode.mapper.ConversationMapper
1313
import com.getcode.mapper.ConversationMessageMapper
14+
import com.getcode.model.Conversation
1415
import com.getcode.model.chat.Chat
1516
import com.getcode.model.chat.ChatMessage
1617
import com.getcode.model.Cursor
@@ -156,6 +157,15 @@ class ChatHistoryController @Inject constructor(
156157
chatEntries.value = updatedWithMessages.sortedByDescending { it.lastMessageMillis }
157158
}
158159

160+
fun addChat(chat: Chat) {
161+
chatEntries.value = (chatEntries.value.orEmpty() + chat)
162+
.sortedByDescending { it.lastMessageMillis }
163+
}
164+
165+
fun findChat(predicate: (Chat) -> Boolean): Chat? {
166+
return chatEntries.value?.firstOrNull(predicate)
167+
}
168+
159169
suspend fun advanceReadPointer(chatId: ID) {
160170
val owner = owner() ?: return
161171

@@ -181,17 +191,14 @@ class ChatHistoryController @Inject constructor(
181191
}
182192
}
183193

184-
fun advanceReadPointerUpTo(chatId: ID, timestamp: Long) {
194+
fun resetUnreadCount(chatId: ID) {
185195
chatEntries.update {
186196
it?.toMutableList()?.apply chats@{
187197
indexOfFirst { chat -> chat.id == chatId }
188198
.takeIf { index -> index >= 0 }
189199
?.let { index ->
190200
val chat = this[index]
191-
val newestMessage = chat.newestMessage
192-
if (newestMessage != null) {
193-
this[index] = chat.resetUnreadCount()
194-
}
201+
this[index] = chat.resetUnreadCount()
195202
}
196203
}?.toList()
197204
}

0 commit comments

Comments
 (0)