From f76ac12c44bad39403983afd98fe000f743c6054 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 12 Sep 2024 07:11:55 -0400 Subject: [PATCH 1/2] chore: fetch updated protos Signed-off-by: Brandon McAnsh --- .../proto/account/v1/account_service.proto | 48 ++++--------------- .../main/proto/badge/v1/badge_service.proto | 1 + .../src/main/proto/chat/v1/chat_service.proto | 1 + .../src/main/proto/chat/v2/chat_service.proto | 47 +++++++++++++----- .../src/main/proto/common/v1/model.proto | 1 + .../contact/v1/contact_list_service.proto | 1 + .../proto/currency/v1/currency_service.proto | 1 + .../main/proto/device/v1/device_service.proto | 1 + .../main/proto/invite/v2/invite_service.proto | 1 + .../messaging/v1/messaging_service.proto | 1 + .../v1/micro_payment_service.proto | 1 + .../phone/v1/phone_verification_service.proto | 1 + .../src/main/proto/push/v1/push_service.proto | 1 + .../transaction/v2/transaction_service.proto | 25 +++++++--- .../main/proto/user/v1/identity_service.proto | 19 +++++++- 15 files changed, 91 insertions(+), 59 deletions(-) diff --git a/service/protos/src/main/proto/account/v1/account_service.proto b/service/protos/src/main/proto/account/v1/account_service.proto index 73c02bab6..d8cdef961 100644 --- a/service/protos/src/main/proto/account/v1/account_service.proto +++ b/service/protos/src/main/proto/account/v1/account_service.proto @@ -28,22 +28,18 @@ service Account { rpc LinkAdditionalAccounts(LinkAdditionalAccountsRequest) returns (LinkAdditionalAccountsResponse); } - - message IsCodeAccountRequest { // The owner account to check against. - common.v1.SolanaAccountId owner = 1; + common.v1.SolanaAccountId owner = 1; // The signature is of serialize(IsCodeAccountRequest) without this field set // using the private key of the owner account. This provides an authentication // mechanism to the RPC. - common.v1.Signature signature = 2; + common.v1.Signature signature = 2; } - - message IsCodeAccountResponse { Result result = 1; enum Result { @@ -54,27 +50,21 @@ message IsCodeAccountResponse { // The account exists, but at least one timelock account is unlocked. UNLOCKED_TIMELOCK_ACCOUNT = 2; } - - } - - message GetTokenAccountInfosRequest { // The owner account, which can also be thought of as a parent account for this // RPC that links to one or more token accounts. - common.v1.SolanaAccountId owner = 1; + common.v1.SolanaAccountId owner = 1; // The signature is of serialize(GetTokenAccountInfosRequest) without this field set // using the private key of the owner account. This provides an authentication // mechanism to the RPC. - common.v1.Signature signature = 2; + common.v1.Signature signature = 2; } - - message GetTokenAccountInfosResponse { Result result = 1; enum Result { @@ -82,34 +72,28 @@ message GetTokenAccountInfosResponse { NOT_FOUND = 1; } - - map token_account_infos = 2; } - - message LinkAdditionalAccountsRequest { // The owner account to link to - common.v1.SolanaAccountId owner = 1; + common.v1.SolanaAccountId owner = 1; // The authority account derived off the user's 12 words, which contains // the USDC ATA (and potentially others in the future) that will be used // in swaps. - common.v1.SolanaAccountId swap_authority = 2; + common.v1.SolanaAccountId swap_authority = 2; // Signature values for each account provided in this request. Each signature // must be generated without this array set. The expected ordering of signatures: // 1. owner // 2. swap_authority - repeated common.v1.Signature signatures = 3; + repeated common.v1.Signature signatures = 3 ; } - - message LinkAdditionalAccountsResponse { Result result = 1; enum Result { @@ -121,15 +105,11 @@ message LinkAdditionalAccountsResponse { // An account being linked is not valid INVALID_ACCOUNT = 2; } - - } - - message TokenAccountInfo { // The token account's address - common.v1.SolanaAccountId address = 1; + common.v1.SolanaAccountId address = 1; // The owner of the token account, which can also be thought of as a parent @@ -143,7 +123,7 @@ message TokenAccountInfo { common.v1.SolanaAccountId authority = 3; // The type of token account, which infers its intended use. - common.v1.AccountType account_type = 4; + common.v1.AccountType account_type = 4; // The account's derivation index for applicable account types. When this field @@ -164,8 +144,6 @@ message TokenAccountInfo { BALANCE_SOURCE_CACHE = 2; } - - // The balance in quarks, as observed by Code. This may not reflect the value // on the blockchain and could be non-zero even if the account hasn't been created. // Use balance_source to determine how this value was calculated. @@ -197,8 +175,6 @@ message TokenAccountInfo { MANAGEMENT_STATE_CLOSED = 7; } - - // The state of the account on the blockchain. BlockchainState blockchain_state = 9; enum BlockchainState { @@ -211,8 +187,6 @@ message TokenAccountInfo { BLOCKCHAIN_STATE_EXISTS = 2; } - - // For temporary incoming accounts only. Flag indicates whether client must // actively try rotating it by issuing a ReceivePaymentsPrivately intent. In // general, clients should wait as long as possible until this flag is true @@ -235,8 +209,6 @@ message TokenAccountInfo { CLAIM_STATE_EXPIRED = 3; } - - // For account types used as an intermediary for sending money between two // users (eg. REMOTE_SEND_GIFT_CARD), this represents the original exchange // data used to fund the account. Over time, this value will become stale: @@ -265,5 +237,3 @@ message TokenAccountInfo { // the tiem created on the blockchain. google.protobuf.Timestamp created_at = 17; } - - diff --git a/service/protos/src/main/proto/badge/v1/badge_service.proto b/service/protos/src/main/proto/badge/v1/badge_service.proto index 6b67b6c64..5885e2fe2 100644 --- a/service/protos/src/main/proto/badge/v1/badge_service.proto +++ b/service/protos/src/main/proto/badge/v1/badge_service.proto @@ -4,6 +4,7 @@ option go_package = "github.com/code-payments/code-protobuf-api/generated/go/bad option java_package = "com.codeinc.gen.badge.v1"; option objc_class_prefix = "CPBBadgeV1"; import "common/v1/model.proto"; + service Badge { // ResetBadgeCount resets an owner account's app icon badge count back to zero rpc ResetBadgeCount(ResetBadgeCountRequest) returns (ResetBadgeCountResponse); diff --git a/service/protos/src/main/proto/chat/v1/chat_service.proto b/service/protos/src/main/proto/chat/v1/chat_service.proto index 0d95aa619..ebfc6d1a3 100644 --- a/service/protos/src/main/proto/chat/v1/chat_service.proto +++ b/service/protos/src/main/proto/chat/v1/chat_service.proto @@ -6,6 +6,7 @@ option objc_class_prefix = "CPBChatV1"; import "common/v1/model.proto"; import "transaction/v2/transaction_service.proto"; import "google/protobuf/timestamp.proto"; + // Deprecated: Use the v2 service service Chat { // GetChats gets the set of chats for an owner account diff --git a/service/protos/src/main/proto/chat/v2/chat_service.proto b/service/protos/src/main/proto/chat/v2/chat_service.proto index 022bff99f..1479fba5c 100644 --- a/service/protos/src/main/proto/chat/v2/chat_service.proto +++ b/service/protos/src/main/proto/chat/v2/chat_service.proto @@ -6,6 +6,7 @@ option objc_class_prefix = "CPBChatV2"; import "common/v1/model.proto"; import "transaction/v2/transaction_service.proto"; import "google/protobuf/timestamp.proto"; + service Chat { // GetChats gets the set of chats for an owner account using a paged API. // This RPC is aware of all identities tied to the owner account. @@ -143,28 +144,50 @@ message StreamChatEventsResponse { message StartChatRequest { common.v1.SolanaAccountId owner = 1; common.v1.Signature signature = 2; + ChatMemberIdentity self = 3; oneof parameters { - StartTipChatParameters tip_chat = 3; + StartTwoWayChatParameters two_way_chat = 4; // GroupChatParameters group_chat = 4; } } -// Starts a two-way chat between a tipper and tippee. Chat members are -// inferred from the 12 word public keys involved in the intent. Only -// the tippee can start the chat, and the tipper is anonymous if this -// is the first between the involved Code users. -message StartTipChatParameters { - // The tip's intent ID, which can be extracted from the reference in - // an ExchangeDataContent message content where the verb is RECEIVED_TIP. - common.v1.IntentId intent_id = 1; +// StartTwoWayChatParameters contains the parameters required to start +// or recover a two way chat between the caller and the specified 'other_user'. +// +// The 'other_user' is currently the 'tip_address', normally retrieved from +// user.Identity.GetTwitterUser(username). +message StartTwoWayChatParameters { + // The account id of the user the caller wishes to chat with. + // + // This will be the `tip` (or equivalent) address. + common.v1.SolanaAccountId other_user = 1; + // The intent_id of the payment that initiated the chat/friendship. + // + // This field is optional. It is used as an optimization when the server has not + // yet observed the establishment of a friendship. In this case, the server will + // use the provided intent_id to verify the friendship. + // + // This is most likely to occur when initiating a chat with a user for the first + // time. + common.v1.IntentId intent_id = 2; + // The identity of the other user. + // + // Note: This can/should be removed with proper intent plumbing. + ChatMemberIdentity identity = 3; } message StartChatResponse { Result result = 1; enum Result { - OK = 0; - DENIED = 1; + OK = 0; + // DENIED indicates the caller is not allowed to start/join the chat. + DENIED = 1; + // INVALID_PRAMETER indicates one of the parameters is invalid. INVALID_PARAMETER = 2; + // PENDING indicates that the payment (for chat) intent is pending confirmation + // before the service will permit the creation of the chat. This can happen in + // cases where the block chain is particularly slow (beyond our RPC timeouts). + PENDING = 3; } - // The chat to use if the RPC was successful + // The chat to use if the RPC was successful. ChatMetadata chat = 2; } message SendMessageRequest { diff --git a/service/protos/src/main/proto/common/v1/model.proto b/service/protos/src/main/proto/common/v1/model.proto index 70aab885c..6d681311e 100644 --- a/service/protos/src/main/proto/common/v1/model.proto +++ b/service/protos/src/main/proto/common/v1/model.proto @@ -5,6 +5,7 @@ option java_package = "com.codeinc.gen.common.v1"; option objc_class_prefix = "CPBCommonV1"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; + // AccountType associates a type to an account, which infers how an account is used // within the Code ecosystem. enum AccountType { diff --git a/service/protos/src/main/proto/contact/v1/contact_list_service.proto b/service/protos/src/main/proto/contact/v1/contact_list_service.proto index 4fd312c2e..0eca54b93 100644 --- a/service/protos/src/main/proto/contact/v1/contact_list_service.proto +++ b/service/protos/src/main/proto/contact/v1/contact_list_service.proto @@ -4,6 +4,7 @@ option go_package = "github.com/code-payments/code-protobuf-api/generated/go/con option java_package = "com.codeinc.gen.contact.v1"; option objc_class_prefix = "CPBContactV1"; import "common/v1/model.proto"; + service ContactList { // AddContacts adds a batch of contacts to a user's contact list rpc AddContacts(AddContactsRequest) returns (AddContactsResponse); diff --git a/service/protos/src/main/proto/currency/v1/currency_service.proto b/service/protos/src/main/proto/currency/v1/currency_service.proto index 0f8f86277..5458a224b 100644 --- a/service/protos/src/main/proto/currency/v1/currency_service.proto +++ b/service/protos/src/main/proto/currency/v1/currency_service.proto @@ -3,6 +3,7 @@ package code.currency.v1; option go_package = "github.com/code-payments/code-protobuf-api/generated/go/currency/v1;currency"; option java_package = "com.codeinc.gen.currency.v1"; option objc_class_prefix = "CPBCurrencyV1"; + import "google/protobuf/timestamp.proto"; service Currency { // GetAllRates returns the exchange rates for Kin against all available currencies diff --git a/service/protos/src/main/proto/device/v1/device_service.proto b/service/protos/src/main/proto/device/v1/device_service.proto index 04d1b8783..1b8e420a7 100644 --- a/service/protos/src/main/proto/device/v1/device_service.proto +++ b/service/protos/src/main/proto/device/v1/device_service.proto @@ -4,6 +4,7 @@ option go_package = "github.com/code-payments/code-protobuf-api/generated/go/dev option java_package = "com.codeinc.gen.device.v1"; option objc_class_prefix = "CPBDevicetV1"; import "common/v1/model.proto"; + service Device { // RegisterLoggedInAccounts registers a set of owner accounts logged for // an app install. Currently, a single login is enforced per app install. diff --git a/service/protos/src/main/proto/invite/v2/invite_service.proto b/service/protos/src/main/proto/invite/v2/invite_service.proto index 99f73857e..c3676cc62 100644 --- a/service/protos/src/main/proto/invite/v2/invite_service.proto +++ b/service/protos/src/main/proto/invite/v2/invite_service.proto @@ -4,6 +4,7 @@ option go_package = "github.com/code-payments/code-protobuf-api/generated/go/inv option java_package = "com.codeinc.gen.invite.v2"; option objc_class_prefix = "CPBInviteV2"; import "common/v1/model.proto"; + service Invite { // GetInviteCount gets the number of invites that a user can send out. rpc GetInviteCount(GetInviteCountRequest) returns (GetInviteCountResponse); diff --git a/service/protos/src/main/proto/messaging/v1/messaging_service.proto b/service/protos/src/main/proto/messaging/v1/messaging_service.proto index 1eb32863b..5e4a222d1 100644 --- a/service/protos/src/main/proto/messaging/v1/messaging_service.proto +++ b/service/protos/src/main/proto/messaging/v1/messaging_service.proto @@ -5,6 +5,7 @@ option java_package = "com.codeinc.gen.messaging.v1"; option objc_class_prefix = "CPBMessagingV1"; import "common/v1/model.proto"; import "transaction/v2/transaction_service.proto"; + import "google/protobuf/timestamp.proto"; service Messaging { // OpenMessageStream opens a stream of messages. Messages are routed using the diff --git a/service/protos/src/main/proto/micropayment/v1/micro_payment_service.proto b/service/protos/src/main/proto/micropayment/v1/micro_payment_service.proto index a4cf15021..fc502ae5b 100644 --- a/service/protos/src/main/proto/micropayment/v1/micro_payment_service.proto +++ b/service/protos/src/main/proto/micropayment/v1/micro_payment_service.proto @@ -4,6 +4,7 @@ option go_package = "github.com/code-payments/code-protobuf-api/generated/go/mic option java_package = "com.codeinc.gen.micropayment.v1"; option objc_class_prefix = "APBMicroPaymentV1"; import "common/v1/model.proto"; + // todo: Migrate this to a generic "request" service service MicroPayment { // GetStatus gets basic request status diff --git a/service/protos/src/main/proto/phone/v1/phone_verification_service.proto b/service/protos/src/main/proto/phone/v1/phone_verification_service.proto index 63f37b75c..a7390c097 100644 --- a/service/protos/src/main/proto/phone/v1/phone_verification_service.proto +++ b/service/protos/src/main/proto/phone/v1/phone_verification_service.proto @@ -4,6 +4,7 @@ option go_package = "github.com/code-payments/code-protobuf-api/generated/go/pho option java_package = "com.codeinc.gen.phone.v1"; option objc_class_prefix = "CPBPhoneV1"; import "common/v1/model.proto"; + service PhoneVerification { // SendVerificationCode sends a verification code to the provided phone number // over SMS. If an active verification is already taking place, the existing code diff --git a/service/protos/src/main/proto/push/v1/push_service.proto b/service/protos/src/main/proto/push/v1/push_service.proto index de3cac502..9f070916e 100644 --- a/service/protos/src/main/proto/push/v1/push_service.proto +++ b/service/protos/src/main/proto/push/v1/push_service.proto @@ -4,6 +4,7 @@ option go_package = "github.com/code-payments/code-protobuf-api/generated/go/pus option java_package = "com.codeinc.gen.push.v1"; option objc_class_prefix = "APBPushV1"; import "common/v1/model.proto"; + service Push { // AddToken stores a push token in a data container. The call is idempotent // and adding an existing valid token will not fail. Token types will be diff --git a/service/protos/src/main/proto/transaction/v2/transaction_service.proto b/service/protos/src/main/proto/transaction/v2/transaction_service.proto index 143716e8b..af20a6772 100644 --- a/service/protos/src/main/proto/transaction/v2/transaction_service.proto +++ b/service/protos/src/main/proto/transaction/v2/transaction_service.proto @@ -5,6 +5,7 @@ option java_package = "com.codeinc.gen.transaction.v2"; option objc_class_prefix = "APBTransactionV2"; import "common/v1/model.proto"; import "google/protobuf/timestamp.proto"; + service Transaction { // SubmitIntent is the mechanism for client and server to agree upon a set of // client actions to execute on the blockchain using the Code sequencer for @@ -96,7 +97,6 @@ message SubmitIntentRequest { // The globally unique client generated intent ID. Use the original intent // ID when operating on actions that mutate the intent. common.v1.IntentId id = 1; - // The verified owner account public key common.v1.SolanaAccountId owner = 2; // Additional metadata that describes the high-level intention @@ -125,7 +125,7 @@ message SubmitIntentResponse { message ServerParameters { // The set of all server paremeters required to fill missing transaction // details. Server guarantees to provide a message for each client action - // in an order consistent with the received action list. + // in an order consistent with the received action list. repeated ServerParameter server_parameters = 1 ; } message Success { @@ -341,7 +341,7 @@ message SwapRequest { uint64 limit = 3; // Whether the client wants the RPC to wait for blockchain status. If false, // then the RPC will return Success when the swap is submitted to the blockchain. - // Otherwise, the RPC will observe and report back the status of the transaction. + // Otherwise, the RPC will observe and report back the status of the transaction. bool wait_for_blockchain_status = 4; // The signature is of serialize(Initiate) without this field set using the // private key of the owner account. This provides an authentication mechanism @@ -556,6 +556,10 @@ message SendPrivatePaymentMetadata { bool is_tip = 5; // If is_tip is true, the user being tipped TippedUser tipped_user = 6; + // Is the payment for a friendship? + bool is_friendship = 7; + // If is_friendship is true, the user being friended + FriendedUser friended_user = 8; } // Send a payment to a destination account publicly. // @@ -801,7 +805,6 @@ message NoPrivacyWithdrawAction { common.v1.SolanaAccountId destination = 3; // The intended Kin quark amount to withdraw uint64 amount = 4; - // Whether the account is closed afterwards. This is always true, since there // are no current se cases to leave it open. bool should_close = 5; @@ -985,8 +988,8 @@ message ReasonStringErrorDetails { string reason = 1 ; } message InvalidSignatureErrorDetails { - // The action whose signature mismatched - uint32 action_id = 1; + // The action whose signature mismatched + uint32 action_id = 1; // The transaction the server expected to have signed. common.v1.Transaction expected_transaction = 2; // The signature that was provided by the client. @@ -1072,7 +1075,7 @@ message PaymentHistoryItem { bool is_airdrop = 9; // If is_airdrop is true, the type of airdrop received. AirdropType airdrop_type = 10; - // Is this a micro payment? + // Is this a micro payment? bool is_micro_payment = 11; // The intent ID associated with this history item common.v1.IntentId intent_id = 12; @@ -1139,6 +1142,14 @@ message TippedUser { } string username = 2 ; } +message FriendedUser { + Platform platform = 1 ; + enum Platform { + UNKNOWN = 0; + TWITTER = 1; + } + string username = 2 ; +} message Cursor { bytes value = 1 ; } diff --git a/service/protos/src/main/proto/user/v1/identity_service.proto b/service/protos/src/main/proto/user/v1/identity_service.proto index db3574b6f..c3eaa5061 100644 --- a/service/protos/src/main/proto/user/v1/identity_service.proto +++ b/service/protos/src/main/proto/user/v1/identity_service.proto @@ -6,6 +6,7 @@ option objc_class_prefix = "CPBUserV1"; import "common/v1/model.proto"; import "phone/v1/phone_verification_service.proto"; import "transaction/v2/transaction_service.proto"; + service Identity { // LinkAccount links an owner account to the user identified and authenticated // by a one-time use token. @@ -236,6 +237,10 @@ message GetTwitterUserRequest { // The tip address to query against common.v1.SolanaAccountId tip_address = 2; } + // An optional set of authentication information that allows for more + // information to be returned in the request. + common.v1.SolanaAccountId requestor = 10; + common.v1.Signature signature = 11; } message GetTwitterUserResponse { Result result = 1; @@ -273,6 +278,8 @@ message PhoneMetadata { } message TwitterUser { // Public key for a token account where tips are routed + // + // TODO(tip_rename): Candidate for renaming to something more generic. common.v1.SolanaAccountId tip_address = 1; // The user's username on Twitter string username = 2 ; @@ -289,5 +296,15 @@ message TwitterUser { GOVERNMENT = 3; } // The number of followers the user has on Twitter - uint32 follower_count = 6; + uint32 follower_count = 6; + // The cost of establishing the friendship (regardless if caller is a friend). + // + // This should not be cached for an extended period, as exchange rate / value + // may change at any time. + transaction.v2.ExchangeDataWithoutRate friendship_cost = 7; + // =========================================================== + // The rest of the fields require authentication to be present. + // =========================================================== + // Indicates the user is a friend of the caller. + bool is_friend = 10; } From 02a9abc59f178292b4cec78ad88a1138b529cc02 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 12 Sep 2024 11:05:39 -0400 Subject: [PATCH 2/2] feat: update RPC for fetching TwitterUser; drive chat flow Signed-off-by: Brandon McAnsh --- .../java/com/getcode/db/ConversationDao.kt | 2 - api/src/main/java/com/getcode/model/Fiat.kt | 3 ++ .../main/java/com/getcode/model/KinAmount.kt | 2 + .../java/com/getcode/model/TwitterUser.kt | 8 ++- .../network/ConversationListController.kt | 53 +++++++++++++++++++ .../java/com/getcode/network/TipController.kt | 9 ++-- .../getcode/network/TwitterUserController.kt | 38 +++++++++++++ .../java/com/getcode/network/api/ChatApiV2.kt | 10 ++-- .../getcode/network/client/Client_Identity.kt | 14 +++-- .../network/repository/IdentityRepository.kt | 6 ++- .../getcode/network/service/ChatServiceV2.kt | 2 +- app/src/main/java/com/getcode/Session.kt | 17 ++++-- .../getcode/navigation/screens/ChatScreens.kt | 23 ++++++-- .../main/java/com/getcode/util/Currency.kt | 4 +- .../java/com/getcode/util/KinAmountExt.kt | 17 +++--- .../conversation/ChatConversationScreen.kt | 40 +++++++++++--- .../conversation/ConversationViewModel.kt | 51 ++++++++++++++++++ .../create/byusername/ChatByUsernameScreen.kt | 4 +- .../byusername/ChatByUsernameViewModel.kt | 10 ++-- .../view/main/chat/list/ChatListScreen.kt | 14 +++-- .../view/main/chat/list/ChatListViewModel.kt | 20 ++----- app/src/main/res/values/strings.xml | 2 + 22 files changed, 279 insertions(+), 70 deletions(-) create mode 100644 api/src/main/java/com/getcode/network/ConversationListController.kt create mode 100644 api/src/main/java/com/getcode/network/TwitterUserController.kt diff --git a/api/src/main/java/com/getcode/db/ConversationDao.kt b/api/src/main/java/com/getcode/db/ConversationDao.kt index ff98c92ad..ab0f87182 100644 --- a/api/src/main/java/com/getcode/db/ConversationDao.kt +++ b/api/src/main/java/com/getcode/db/ConversationDao.kt @@ -1,6 +1,5 @@ package com.getcode.db -import androidx.paging.PagingData import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Delete @@ -8,7 +7,6 @@ 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 diff --git a/api/src/main/java/com/getcode/model/Fiat.kt b/api/src/main/java/com/getcode/model/Fiat.kt index 7482c67d2..fc6926abb 100644 --- a/api/src/main/java/com/getcode/model/Fiat.kt +++ b/api/src/main/java/com/getcode/model/Fiat.kt @@ -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, diff --git a/api/src/main/java/com/getcode/model/KinAmount.kt b/api/src/main/java/com/getcode/model/KinAmount.kt index b6d726dde..a777f7ee3 100644 --- a/api/src/main/java/com/getcode/model/KinAmount.kt +++ b/api/src/main/java/com/getcode/model/KinAmount.kt @@ -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) } diff --git a/api/src/main/java/com/getcode/model/TwitterUser.kt b/api/src/main/java/com/getcode/model/TwitterUser.kt index ee558709a..bc9f059b1 100644 --- a/api/src/main/java/com/getcode/model/TwitterUser.kt +++ b/api/src/main/java/com/getcode/model/TwitterUser.kt @@ -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" @@ -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 ) } } diff --git a/api/src/main/java/com/getcode/network/ConversationListController.kt b/api/src/main/java/com/getcode/network/ConversationListController.kt new file mode 100644 index 000000000..453136051 --- /dev/null +++ b/api/src/main/java/com/getcode/network/ConversationListController.kt @@ -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 +) : PagingSource() { + + override fun getRefreshKey(state: PagingState): 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): LoadResult { + 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) + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/getcode/network/TipController.kt b/api/src/main/java/com/getcode/network/TipController.kt index 2bb5567da..177abb850 100644 --- a/api/src/main/java/com/getcode/network/TipController.kt +++ b/api/src/main/java/com/getcode/network/TipController.kt @@ -39,6 +39,7 @@ import kotlin.concurrent.fixedRateTimer typealias TipUser = Pair + @Singleton class TipController @Inject constructor( private val client: Client, @@ -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)) @@ -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() } } diff --git a/api/src/main/java/com/getcode/network/TwitterUserController.kt b/api/src/main/java/com/getcode/network/TwitterUserController.kt new file mode 100644 index 000000000..14e9dba86 --- /dev/null +++ b/api/src/main/java/com/getcode/network/TwitterUserController.kt @@ -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() + + + 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() + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/getcode/network/api/ChatApiV2.kt b/api/src/main/java/com/getcode/network/api/ChatApiV2.kt index febd65fe1..efbcc76db 100644 --- a/api/src/main/java/com/getcode/network/api/ChatApiV2.kt +++ b/api/src/main/java/com/getcode/network/api/ChatApiV2.kt @@ -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 @@ -54,11 +50,11 @@ class ChatApiV2 @Inject constructor( ) : GrpcApi(managedChannel) { private val api = ChatGrpc.newStub(managedChannel) - fun createTipChat(owner: KeyPair, intentId: ID): Flow { + fun startChat(owner: KeyPair, intentId: ID): Flow { val request = StartChatRequest.newBuilder() .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .setTipChat( - ChatService.StartTipChatParameters.newBuilder() + .setTwoWayChat( + ChatService.StartTwoWayChatParameters.newBuilder() .setIntentId(IntentId.newBuilder() .setValue(intentId.toByteString())) .build() diff --git a/api/src/main/java/com/getcode/network/client/Client_Identity.kt b/api/src/main/java/com/getcode/network/client/Client_Identity.kt index e5e93f55c..452450e2a 100644 --- a/api/src/main/java/com/getcode/network/client/Client_Identity.kt +++ b/api/src/main/java/com/getcode/network/client/Client_Identity.kt @@ -17,10 +17,16 @@ suspend fun Client.updatePreferences(organizer: Organizer): Result { ) } -suspend fun Client.fetchTwitterUser(username: String): Result { - return identityRepository.fetchTwitterUserByUsername(username) +suspend fun Client.fetchTwitterUser( + organizer: Organizer, + username: String +): Result { + return identityRepository.fetchTwitterUserByUsername(organizer.ownerKeyPair, username) } -suspend fun Client.fetchTwitterUser(address: PublicKey): Result { - return identityRepository.fetchTwitterUserByAddress(address) +suspend fun Client.fetchTwitterUser( + organizer: Organizer, + address: PublicKey +): Result { + return identityRepository.fetchTwitterUserByAddress(organizer.ownerKeyPair, address) } \ No newline at end of file diff --git a/api/src/main/java/com/getcode/network/repository/IdentityRepository.kt b/api/src/main/java/com/getcode/network/repository/IdentityRepository.kt index 644b6d912..d0466c2ad 100644 --- a/api/src/main/java/com/getcode/network/repository/IdentityRepository.kt +++ b/api/src/main/java/com/getcode/network/repository/IdentityRepository.kt @@ -303,9 +303,10 @@ class IdentityRepository @Inject constructor( } } - suspend fun fetchTwitterUserByUsername(username: String): Result { + suspend fun fetchTwitterUserByUsername(owner: KeyPair, username: String): Result { val request = GetTwitterUserRequest.newBuilder() .setUsername(username) + .setRequestor(owner.publicKeyBytes.toSolanaAccount()) .build() return try { @@ -349,9 +350,10 @@ class IdentityRepository @Inject constructor( } } - suspend fun fetchTwitterUserByAddress(address: PublicKey): Result { + suspend fun fetchTwitterUserByAddress(owner: KeyPair, address: PublicKey): Result { val request = GetTwitterUserRequest.newBuilder() .setTipAddress(address.byteArray.toSolanaAccount()) + .setRequestor(owner.publicKeyBytes.toSolanaAccount()) .build() return try { diff --git a/api/src/main/java/com/getcode/network/service/ChatServiceV2.kt b/api/src/main/java/com/getcode/network/service/ChatServiceV2.kt index 7726e7336..741d71b25 100644 --- a/api/src/main/java/com/getcode/network/service/ChatServiceV2.kt +++ b/api/src/main/java/com/getcode/network/service/ChatServiceV2.kt @@ -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 -> { diff --git a/app/src/main/java/com/getcode/Session.kt b/app/src/main/java/com/getcode/Session.kt index 973a89edd..358db60be 100644 --- a/app/src/main/java/com/getcode/Session.kt +++ b/app/src/main/java/com/getcode/Session.kt @@ -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( @@ -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, ), diff --git a/app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt b/app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt index 2ddbdcf44..6e1964f86 100644 --- a/app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt +++ b/app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt @@ -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 @@ -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 { @@ -59,8 +61,8 @@ data object ChatListModal: ChatGraph, ModalRoot { closeButtonEnabled = { it is ChatListModal }, ) { val viewModel = getViewModel() -// val conversations = viewModel.conversations.collectAsLazyPagingItems() - ChatListScreen(viewModel) + val conversations = viewModel.conversations.collectAsLazyPagingItems() + ChatListScreen(viewModel, conversations) } } } @@ -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 { @@ -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) @@ -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( diff --git a/app/src/main/java/com/getcode/util/Currency.kt b/app/src/main/java/com/getcode/util/Currency.kt index 51dbbd40d..ecef5fb40 100644 --- a/app/src/main/java/com/getcode/util/Currency.kt +++ b/app/src/main/java/com/getcode/util/Currency.kt @@ -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 "" } } } diff --git a/app/src/main/java/com/getcode/util/KinAmountExt.kt b/app/src/main/java/com/getcode/util/KinAmountExt.kt index 16e3886fc..742f2eb09 100644 --- a/app/src/main/java/com/getcode/util/KinAmountExt.kt +++ b/app/src/main/java/com/getcode/util/KinAmountExt.kt @@ -2,6 +2,7 @@ package com.getcode.util import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import com.getcode.LocalCurrencyUtils import com.getcode.R import com.getcode.model.Currency @@ -12,17 +13,21 @@ import com.getcode.utils.FormatUtils fun KinAmount.formattedRaw() = FormatUtils.formatWholeRoundDown(kin.toKin().toDouble()) @Composable -fun KinAmount.formatted(currency: Currency) = formatAmountString( - AndroidResources(LocalContext.current), - currency, - fiat +fun KinAmount.formatted( + currency: Currency, + suffix: String = stringResource(R.string.core_ofKin) +) = formatAmountString( + resources = AndroidResources(context = LocalContext.current), + currency = currency, + amount = fiat, + suffix = suffix ) @Composable -fun KinAmount.formatted(): String { +fun KinAmount.formatted(suffix: String = stringResource(R.string.core_ofKin)): String { val currency = LocalCurrencyUtils.current?.getCurrency(rate.currency.name) ?: Currency.Kin - return formatted(currency = currency) + return formatted(currency = currency, suffix = suffix) } fun KinAmount.formatted( diff --git a/app/src/main/java/com/getcode/view/main/chat/conversation/ChatConversationScreen.kt b/app/src/main/java/com/getcode/view/main/chat/conversation/ChatConversationScreen.kt index bfb349bb3..4c54925ab 100644 --- a/app/src/main/java/com/getcode/view/main/chat/conversation/ChatConversationScreen.kt +++ b/app/src/main/java/com/getcode/view/main/chat/conversation/ChatConversationScreen.kt @@ -22,18 +22,25 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.paging.compose.LazyPagingItems +import cafe.adriel.voyager.navigator.currentOrThrow +import com.getcode.LocalSession +import com.getcode.R import com.getcode.navigation.core.LocalCodeNavigator import com.getcode.navigation.screens.ConnectAccount import com.getcode.theme.CodeTheme +import com.getcode.ui.components.ButtonState +import com.getcode.ui.components.CodeButton import com.getcode.ui.components.CodeScaffold import com.getcode.ui.components.chat.utils.ChatItem import com.getcode.ui.components.chat.ChatInput import com.getcode.ui.components.chat.MessageList import com.getcode.ui.components.chat.MessageListEvent import com.getcode.ui.components.chat.utils.HandleMessageChanges +import com.getcode.util.formatted import com.getcode.view.main.tip.IdentityConnectionReason import kotlinx.coroutines.delay @@ -44,6 +51,8 @@ fun ChatConversationScreen( dispatchEvent: (ConversationViewModel.Event) -> Unit, ) { val navigator = LocalCodeNavigator.current + val session = LocalSession.currentOrThrow + CodeScaffold( topBar = { IdentityRevealHeader(state = state) { @@ -59,12 +68,31 @@ fun ChatConversationScreen( modifier = Modifier .imePadding() ) { - ChatInput( - state = state.textFieldState, - sendCashEnabled = state.tipChatCash.enabled, - onSendMessage = { dispatchEvent(ConversationViewModel.Event.SendMessage) }, - onSendCash = { dispatchEvent(ConversationViewModel.Event.SendCash) } - ) + val canChat = remember(state.twitterUser) { + state.twitterUser == null || state.twitterUser.isFriend + } + if (canChat) { + ChatInput( + state = state.textFieldState, + sendCashEnabled = state.tipChatCash.enabled, + onSendMessage = { dispatchEvent(ConversationViewModel.Event.SendMessage) }, + onSendCash = { dispatchEvent(ConversationViewModel.Event.SendCash) } + ) + } else { + CodeButton( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = CodeTheme.dimens.grid.x2) + .padding(horizontal = CodeTheme.dimens.inset), + buttonState = ButtonState.Filled, + text = stringResource( + R.string.action_payToChat, + state.costToChat.formatted(suffix = "") + ) + ) { +// session.presentTipConfirmation(state.costToChat) + } + } } } ) { padding -> diff --git a/app/src/main/java/com/getcode/view/main/chat/conversation/ConversationViewModel.kt b/app/src/main/java/com/getcode/view/main/chat/conversation/ConversationViewModel.kt index 5b02d448a..535e14778 100644 --- a/app/src/main/java/com/getcode/view/main/chat/conversation/ConversationViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/chat/conversation/ConversationViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.flatMap import androidx.paging.map +import com.codeinc.gen.user.v1.user import com.getcode.BuildConfig import com.getcode.R import com.getcode.manager.BottomBarManager @@ -17,13 +18,18 @@ import com.getcode.model.Feature import com.getcode.model.ID import com.getcode.model.MessageStatus import com.getcode.model.ConversationCashFeature +import com.getcode.model.CurrencyCode +import com.getcode.model.Fiat +import com.getcode.model.KinAmount import com.getcode.model.TwitterUser +import com.getcode.model.chat.ChatMember import com.getcode.model.chat.ChatType import com.getcode.model.chat.Platform import com.getcode.model.chat.Reference import com.getcode.model.uuid import com.getcode.network.ConversationController import com.getcode.network.TipController +import com.getcode.network.exchange.Exchange import com.getcode.network.repository.FeatureRepository import com.getcode.ui.components.chat.utils.ChatItem import com.getcode.ui.components.chat.utils.ConversationMessageIndice @@ -50,12 +56,14 @@ import kotlinx.datetime.Instant import timber.log.Timber import java.util.UUID import javax.inject.Inject +import kotlin.math.cos @HiltViewModel class ConversationViewModel @Inject constructor( private val conversationController: ConversationController, features: FeatureRepository, tipController: TipController, + exchange: Exchange, resources: ResourceHelper, ) : BaseViewModel2( initialState = State.Default, @@ -64,6 +72,8 @@ class ConversationViewModel @Inject constructor( data class State( val conversationId: ID?, + val twitterUser: TwitterUser?, + val costToChat: KinAmount, val reference: Reference.IntentId?, val textFieldState: TextFieldState, val tipChatCash: Feature, @@ -84,6 +94,8 @@ class ConversationViewModel @Inject constructor( companion object { val Default = State( + twitterUser = null, + costToChat = KinAmount.Zero, conversationId = null, reference = null, tipChatCash = ConversationCashFeature(), @@ -98,6 +110,9 @@ class ConversationViewModel @Inject constructor( } sealed interface Event { + data class OnTwitterUserChanged(val user: TwitterUser?) : Event + data class OnCostToChatChanged(val cost: KinAmount): Event + data class OnMembersChanged(val members: List): Event data class OnChatIdChanged(val chatId: ID?) : Event data class OnReferenceChanged(val reference: Reference.IntentId?) : Event data class OnConversationChanged(val conversationWithPointers: ConversationWithLastPointers) : @@ -138,6 +153,31 @@ class ConversationViewModel @Inject constructor( dispatchEvent(Event.OnConversationChanged(it)) }.launchIn(viewModelScope) + eventFlow + .filterIsInstance() + .map { it.user } + .filterNotNull() + .mapNotNull { user -> + val currencySymbol = user.costOfFriendship.currency + val rate = exchange.rateFor(currencySymbol) ?: exchange.rateForUsd()!! + + user to KinAmount.fromFiatAmount(fiat = user.costOfFriendship, rate = rate) + }.map { (user, cost) -> + dispatchEvent(Event.OnCostToChatChanged(cost)) + user + }.onEach { user -> + val member = user.let { + State.User( + memberId = UUID.randomUUID(), + username = user.username, + imageUrl = user.imageUrl + ) + } + + dispatchEvent(Event.OnMembersChanged(listOf(member))) + } + .launchIn(viewModelScope) + // reference ID is used to create a chat that is non-existent if needed eventFlow .filterIsInstance() @@ -350,6 +390,14 @@ class ConversationViewModel @Inject constructor( ) } + is Event.OnCostToChatChanged -> { state -> + state.copy(costToChat = event.cost) + } + + is Event.OnMembersChanged -> { state -> + state.copy(users = event.members) + } + is Event.OnPointersUpdated -> { state -> state.copy(pointers = event.pointers) } @@ -358,6 +406,9 @@ class ConversationViewModel @Inject constructor( state.copy(identityAvailable = event.available) } + is Event.OnTwitterUserChanged -> { state -> + state.copy(twitterUser = event.user) + } is Event.OnChatIdChanged, is Event.Error, Event.RevealIdentity, diff --git a/app/src/main/java/com/getcode/view/main/chat/create/byusername/ChatByUsernameScreen.kt b/app/src/main/java/com/getcode/view/main/chat/create/byusername/ChatByUsernameScreen.kt index cf503c141..1b8518a59 100644 --- a/app/src/main/java/com/getcode/view/main/chat/create/byusername/ChatByUsernameScreen.kt +++ b/app/src/main/java/com/getcode/view/main/chat/create/byusername/ChatByUsernameScreen.kt @@ -70,9 +70,9 @@ fun ChatByUsernameScreen( LaunchedEffect(viewModel) { viewModel.eventFlow .filterIsInstance() - .map { it.username } + .map { it.user } .onEach { - navigator.push(ChatMessageConversationScreen(username = it)) + navigator.push(ChatMessageConversationScreen(user = it)) }.launchIn(this) } diff --git a/app/src/main/java/com/getcode/view/main/chat/create/byusername/ChatByUsernameViewModel.kt b/app/src/main/java/com/getcode/view/main/chat/create/byusername/ChatByUsernameViewModel.kt index 4141a04e2..1dc263c38 100644 --- a/app/src/main/java/com/getcode/view/main/chat/create/byusername/ChatByUsernameViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/chat/create/byusername/ChatByUsernameViewModel.kt @@ -8,7 +8,9 @@ import androidx.compose.foundation.text2.input.clearText import androidx.lifecycle.viewModelScope import com.getcode.R import com.getcode.manager.TopBarManager +import com.getcode.model.TwitterUser import com.getcode.network.TipController +import com.getcode.network.TwitterUserController import com.getcode.util.resources.ResourceHelper import com.getcode.view.BaseViewModel2 import com.getcode.view.main.chat.conversation.ConversationViewModel.Event @@ -24,7 +26,7 @@ import javax.inject.Inject @HiltViewModel class ChatByUsernameViewModel @Inject constructor( resources: ResourceHelper, - tipController: TipController, + twitterUserController: TwitterUserController, ): BaseViewModel2( initialState = State(), updateStateForEvent = updateStateForEvent @@ -40,7 +42,7 @@ class ChatByUsernameViewModel @Inject constructor( sealed interface Event { data object CheckUsername : Event - data class OnSuccess(val username: String) : Event + data class OnSuccess(val user: TwitterUser) : Event data object OnError : Event } @@ -52,14 +54,14 @@ class ChatByUsernameViewModel @Inject constructor( val textFieldState = it.textFieldState val text = textFieldState.text.toString() - runCatching { tipController.fetch(text) } + runCatching { twitterUserController.fetchUser(text) } } .map { it.getOrNull() } .onEach { twitterUser -> if (twitterUser == null) { dispatchEvent(Event.OnError) } else { - dispatchEvent(Event.OnSuccess(twitterUser.username)) + dispatchEvent(Event.OnSuccess(twitterUser)) } }.launchIn(viewModelScope) diff --git a/app/src/main/java/com/getcode/view/main/chat/list/ChatListScreen.kt b/app/src/main/java/com/getcode/view/main/chat/list/ChatListScreen.kt index 4fecf4bb5..2e6401c65 100644 --- a/app/src/main/java/com/getcode/view/main/chat/list/ChatListScreen.kt +++ b/app/src/main/java/com/getcode/view/main/chat/list/ChatListScreen.kt @@ -6,16 +6,20 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.paging.compose.LazyPagingItems +import com.getcode.model.chat.Chat import com.getcode.navigation.core.LocalCodeNavigator import com.getcode.navigation.screens.ChatByUsernameScreen import com.getcode.theme.CodeTheme import com.getcode.ui.components.ButtonState import com.getcode.ui.components.CodeButton import com.getcode.ui.components.CodeScaffold +import com.getcode.ui.components.chat.ChatNode @Composable fun ChatListScreen( viewModel: ChatListViewModel, + conversations: LazyPagingItems ) { val navigator = LocalCodeNavigator.current CodeScaffold( @@ -32,11 +36,11 @@ fun ChatListScreen( } ) { padding -> LazyColumn(modifier = Modifier.padding(padding)) { -// items(conversations.itemCount) { index -> -// conversations[index]?.let { chat -> -// ChatNode(chat = chat) { } -// } -// } + items(conversations.itemCount) { index -> + conversations[index]?.let { chat -> + ChatNode(chat = chat) { } + } + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/getcode/view/main/chat/list/ChatListViewModel.kt b/app/src/main/java/com/getcode/view/main/chat/list/ChatListViewModel.kt index 7be3733e6..04a662c28 100644 --- a/app/src/main/java/com/getcode/view/main/chat/list/ChatListViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/chat/list/ChatListViewModel.kt @@ -1,19 +1,13 @@ package com.getcode.view.main.chat.list -import androidx.paging.PagingData -import androidx.paging.map -import com.getcode.model.Conversation -import com.getcode.network.ConversationController -import com.getcode.network.TipController +import com.getcode.network.ConversationListController import com.getcode.view.BaseViewModel2 import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import javax.inject.Inject @HiltViewModel class ChatListViewModel @Inject constructor( - conversationController: ConversationController, + conversationsController: ConversationListController, ): BaseViewModel2( initialState = State(), updateStateForEvent = updateStateForEvent @@ -26,6 +20,7 @@ class ChatListViewModel @Inject constructor( data object Noop: Event } + val conversations = conversationsController.observeConversations() companion object { val updateStateForEvent: (Event) -> ((State) -> State) = { event -> @@ -34,11 +29,4 @@ class ChatListViewModel @Inject constructor( } } } -} - -data class ConversationWithMetadata( - val conversation: Conversation, - val image: String?, - val latestMessage: String?, - val latestMessageMillis: Long? -) \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0fcca1d98..b2a858681 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,4 +56,6 @@ Don\'t have cash?\nTip me with Code! Scan to download the app, and then scan the code on the other side to tip + + Send %1$s to Start Chatting