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
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,8 @@ class EventStreamDelegate @Inject constructor(
reactionState(chatId, reactionUpdate.messageId).applyUpdate(
emoji = reactionUpdate.emoji.value,
actorIsSelf = reactionUpdate.actor == userManager.accountId,
added = reactionUpdate.action == ReactionUpdate.Action.ADDED,
// BOOSTED means the reaction still stands (boosts are permanent), so only REMOVED clears it.
added = reactionUpdate.action != ReactionUpdate.Action.REMOVED,
count = reactionUpdate.count,
version = reactionUpdate.version,
reactedAt = reactionUpdate.reactedAt,
Expand Down
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ protovalidate-kt = "0.1.3"
# cadence and there is nothing to keep aligned.
# 0.3.0 is the first release of either package to ship R8 keep rules for its generated
# messages, which is what lets proguard-rules.pro drop its own.
ocp-client-protocol = "0.7.0"
flipcash2-client-protocol = "0.19.0"
ocp-client-protocol = "0.8.0"
flipcash2-client-protocol = "0.20.0"

# The Android port is the ONLY libphonenumber this app depends on, deliberately. Google's
# `com.googlecode` artifact used to sit alongside it; the two ship separate copies of the metadata,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.flipcash.services.controllers

import com.flipcash.services.models.Community
import com.flipcash.services.repository.CommunityRepository
import com.flipcash.services.user.UserManager
import com.getcode.solana.keys.Mint
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class CommunityController @Inject constructor(
private val repository: CommunityRepository,
private val userManager: UserManager,
) {
/** Fetches the community built around the currency [mint]. */
suspend fun getCommunity(mint: Mint): Result<Community> {
val owner = userManager.accountCluster?.authority?.keyPair
?: return Result.failure(Throwable("No account cluster in UserManager"))
return repository.getCommunity(owner, mint)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import com.flipcash.services.internal.network.services.PhoneVerificationService
import com.flipcash.services.internal.network.services.ProfileService
import com.flipcash.services.internal.network.services.PurchaseService
import com.flipcash.services.internal.network.services.PushService
import com.flipcash.services.internal.network.services.CommunityService
import com.flipcash.services.internal.network.services.ResolverService
import com.flipcash.services.internal.network.services.ReportingService
import com.flipcash.services.internal.network.services.SettingsService
Expand All @@ -51,6 +52,7 @@ import com.flipcash.services.internal.repositories.InternalModerationRepository
import com.flipcash.services.internal.repositories.InternalProfileRepository
import com.flipcash.services.internal.repositories.InternalPurchaseRepository
import com.flipcash.services.internal.repositories.InternalPushRepository
import com.flipcash.services.internal.repositories.InternalCommunityRepository
import com.flipcash.services.internal.repositories.InternalResolverRepository
import com.flipcash.services.internal.repositories.InternalReportingRepository
import com.flipcash.services.internal.repositories.InternalSettingsRepository
Expand All @@ -68,6 +70,7 @@ import com.flipcash.services.repository.ProfileRepository
import com.flipcash.services.repository.PurchaseRepository
import com.flipcash.services.repository.PushRepository
import com.flipcash.services.repository.BlobStorageRepository
import com.flipcash.services.repository.CommunityRepository
import com.flipcash.services.repository.ResolverRepository
import com.flipcash.services.repository.ReportingRepository
import com.flipcash.services.repository.SettingsRepository
Expand Down Expand Up @@ -185,6 +188,13 @@ internal object FlipcashModule {
contactMapper: ContactMapper,
): ContactListRepository = InternalContactListRepository(service, contactMapper)

@Provides
internal fun providesCommunityRepository(
service: CommunityService,
chatMapper: ChatMetadataMapper,
userProfileMapper: UserProfileMapper,
): CommunityRepository = InternalCommunityRepository(service, chatMapper, userProfileMapper)

@Provides
internal fun providesResolverRepository(
service: ResolverService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import com.flipcash.services.internal.network.extensions.toId
import com.flipcash.services.internal.network.extensions.toMint
import com.flipcash.services.internal.network.extensions.toPublicKey
import com.flipcash.services.models.ActivityFeedNotification
import com.flipcash.services.models.BoostedReaction
import com.flipcash.services.models.NotificationMetadata
import com.flipcash.services.models.chat.Emoji
import com.flipcash.services.internal.network.extensions.toChatId
import com.flipcash.services.models.NotificationState
import com.flipcash.services.models.SwapState
import com.flipcash.services.models.SwappedCryptoMetadata
Expand Down Expand Up @@ -51,6 +54,7 @@ internal class ActivityFeedMessageMapper @Inject constructor(
userId = if (meta.destinationIdentifierCase ==
Model.DirectlySentCryptoNotificationMetadata.DestinationIdentifierCase.USER_ID
) meta.userId.value.toByteArray().toList() else null,
boostedReaction = if (meta.hasBoostedReaction()) meta.boostedReaction.toDomain() else null,
)
}
Model.Notification.AdditionalMetadataCase.RECEIVED_CRYPTO -> {
Expand All @@ -62,6 +66,7 @@ internal class ActivityFeedMessageMapper @Inject constructor(
userId = if (meta.sourceIdentifierCase ==
Model.ReceivedCryptoNotificationMetadata.SourceIdentifierCase.USER_ID
) meta.userId.value.toByteArray().toList() else null,
boostedReaction = if (meta.hasBoostedReaction()) meta.boostedReaction.toDomain() else null,
)
}
Model.Notification.AdditionalMetadataCase.WITHDREW_CRYPTO -> NotificationMetadata.WithdrewCrypto(
Expand Down Expand Up @@ -161,4 +166,9 @@ private fun Model.SwapState.toDomain(): SwapState = when (this) {
Model.SwapState.SWAP_STATE_UNKNOWN,
Model.SwapState.UNRECOGNIZED,
null -> SwapState.UNKNOWN
}
}
private fun Model.BoostedReaction.toDomain(): BoostedReaction = BoostedReaction(
chatId = chatId.toChatId(),
messageId = messageId.value,
emoji = Emoji(emoji.value),
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.flipcash.services.internal.network.extensions.toChatId
import com.flipcash.services.internal.network.extensions.toChatMessage
import com.flipcash.services.internal.network.extensions.toChatRules
import com.flipcash.services.internal.network.extensions.toChatType
import com.flipcash.services.internal.network.extensions.toHoldingsSummary
import com.flipcash.services.internal.network.extensions.toId
import com.flipcash.services.internal.network.extensions.toMediaItem
import com.flipcash.services.internal.network.extensions.toPointer
Expand Down Expand Up @@ -57,6 +58,7 @@ class ChatMetadataMapper @Inject constructor(
useE2ee = from.useE2Ee,
isPrivate = from.isPrivate,
inLobby = from.inLobby,
holdingsSummary = if (from.hasHoldingsSummary()) from.holdingsSummary.toHoldingsSummary() else null,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.flipcash.services.internal.network.api

import com.codeinc.flipcash.gen.common.v1.Common
import com.codeinc.flipcash.gen.community.v1.CommunityGrpcKt
import com.codeinc.flipcash.gen.community.v1.validate
import com.flipcash.services.internal.annotations.FlipcashManagedChannel
import com.flipcash.services.internal.network.extensions.authenticate
import com.getcode.ed25519.Ed25519.KeyPair
import com.getcode.opencode.internal.network.core.GrpcApi
import com.getcode.solana.keys.Mint
import com.getcode.utils.toByteString
import dev.bmcreations.protovalidate.orThrow
import io.grpc.ManagedChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
import com.codeinc.flipcash.gen.community.v1.CommunityService as RpcCommunityService

@Singleton
internal class CommunityApi @Inject constructor(
@FlipcashManagedChannel
managedChannel: ManagedChannel,
) : GrpcApi(managedChannel) {

private val api = CommunityGrpcKt.CommunityCoroutineStub(managedChannel)
.withWaitForReady()

suspend fun getCommunity(
owner: KeyPair,
mint: Mint,
): RpcCommunityService.GetCommunityResponse {
val request = RpcCommunityService.GetCommunityRequest.newBuilder()
.setMint(Common.PublicKey.newBuilder().setValue(mint.bytes.toByteString()))
.apply { setAuth(authenticate(owner)) }
.build()

request.validate().orThrow()

return withContext(Dispatchers.IO) {
api.getCommunity(request)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.flipcash.services.models.chat.ChatMetadata
import com.flipcash.services.models.chat.ChatMutation
import com.flipcash.services.models.chat.ChatType
import com.flipcash.services.models.chat.ChatUpdate
import com.flipcash.services.models.chat.HoldingsSummary
import com.flipcash.services.models.chat.Emoji
import com.flipcash.services.models.chat.KeyEnvelope
import com.flipcash.services.models.chat.LobbyMember
Expand Down Expand Up @@ -312,6 +313,7 @@ internal fun MessagingModel.EmojiReaction.toEmojiReaction(): EmojiReaction {
selfReactor = if (hasSelfReactor()) selfReactor.toReactor() else null,
sampleReactors = sampleReactorsList.map { it.toReactor() },
version = version,
boostUsd = boostUsd,
)
}

Expand All @@ -320,6 +322,7 @@ internal fun MessagingModel.Reactor.toReactor(): Reactor {
userId = userId.toId(),
reactedAt = Instant.fromEpochSeconds(reactedTs.seconds, reactedTs.nanos),
version = version,
boostUsd = boostUsd,
)
}

Expand All @@ -331,11 +334,14 @@ internal fun MessagingModel.ReactionUpdate.toReactionUpdate(): ReactionUpdate {
action = when (action) {
MessagingModel.ReactionUpdate.Action.ADDED -> ReactionUpdate.Action.ADDED
MessagingModel.ReactionUpdate.Action.REMOVED -> ReactionUpdate.Action.REMOVED
MessagingModel.ReactionUpdate.Action.BOOSTED -> ReactionUpdate.Action.BOOSTED
else -> ReactionUpdate.Action.UNKNOWN
},
count = count,
version = version,
reactedAt = Instant.fromEpochSeconds(reactedTs.seconds, reactedTs.nanos),
boostUsd = boostUsd,
actorBoostUsd = actorBoostUsd,
)
}

Expand Down Expand Up @@ -455,6 +461,16 @@ internal fun ChatModel.Metadata.toChatMetadata(): ChatMetadata {
useE2ee = useE2Ee,
isPrivate = isPrivate,
inLobby = inLobby,
holdingsSummary = if (hasHoldingsSummary()) holdingsSummary.toHoldingsSummary() else null,
)
}

internal fun ChatModel.HoldingsSummary.toHoldingsSummary(): HoldingsSummary {
return HoldingsSummary(
mint = mint.toMint(),
holdings = Fiat(quarks = quarks),
holderCount = holderCount,
asOf = Instant.fromEpochSeconds(asOf.seconds, asOf.nanos),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ internal class ChatMessagingService @Inject constructor(
RpcMessagingService.RemoveReactionResponse.Result.OK -> Result.success(response.reaction.toEmojiReaction())
RpcMessagingService.RemoveReactionResponse.Result.DENIED -> Result.failure(RemoveReactionError.Denied())
RpcMessagingService.RemoveReactionResponse.Result.MESSAGE_NOT_FOUND -> Result.failure(RemoveReactionError.MessageNotFound())
RpcMessagingService.RemoveReactionResponse.Result.CANNOT_REMOVE_BOOSTED -> Result.failure(RemoveReactionError.CannotRemoveBoosted())
RpcMessagingService.RemoveReactionResponse.Result.UNRECOGNIZED -> Result.failure(RemoveReactionError.Unrecognized())
else -> Result.failure(RemoveReactionError.Other())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.flipcash.services.internal.network.services

import com.flipcash.services.internal.network.api.CommunityApi
import com.flipcash.services.models.GetCommunityError
import com.getcode.ed25519.Ed25519.KeyPair
import com.getcode.opencode.internal.network.extensions.foldWithSuppression
import com.getcode.opencode.utils.toValidationOrElse
import com.getcode.solana.keys.Mint
import javax.inject.Inject
import com.codeinc.flipcash.gen.community.v1.Model as CommunityModel
import com.codeinc.flipcash.gen.community.v1.CommunityService as RpcCommunityService

internal class CommunityService @Inject constructor(
private val api: CommunityApi,
) {
suspend fun getCommunity(
owner: KeyPair,
mint: Mint,
): Result<CommunityModel.CommunityMetadata> {
return runCatching {
api.getCommunity(owner, mint)
}.foldWithSuppression(
onSuccess = { response ->
when (response.result) {
RpcCommunityService.GetCommunityResponse.Result.OK -> Result.success(response.community)
RpcCommunityService.GetCommunityResponse.Result.NOT_FOUND -> Result.failure(GetCommunityError.NotFound())
RpcCommunityService.GetCommunityResponse.Result.UNRECOGNIZED -> Result.failure(GetCommunityError.Unrecognized())
else -> Result.failure(GetCommunityError.Other())
}
},
onFailure = { cause ->
Result.failure(cause.toValidationOrElse { GetCommunityError.Other(cause = it) })
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.flipcash.services.internal.repositories

import com.flipcash.services.internal.domain.ChatMetadataMapper
import com.flipcash.services.internal.domain.UserProfileMapper
import com.flipcash.services.internal.network.extensions.toMint
import com.flipcash.services.internal.network.services.CommunityService
import com.flipcash.services.models.Community
import com.flipcash.services.models.CommunityLeader
import com.flipcash.services.repository.CommunityRepository
import com.getcode.ed25519.Ed25519.KeyPair
import com.getcode.solana.keys.Mint
import com.getcode.utils.ErrorUtils

internal class InternalCommunityRepository(
private val service: CommunityService,
private val chatMapper: ChatMetadataMapper,
private val userProfileMapper: UserProfileMapper,
) : CommunityRepository {
override suspend fun getCommunity(owner: KeyPair, mint: Mint): Result<Community> {
return service.getCommunity(owner, mint)
.map { metadata ->
Community(
mint = metadata.mint.toMint(),
name = metadata.name,
leader = if (metadata.hasLeader()) {
CommunityLeader(profile = userProfileMapper.map(metadata.leader.profile))
} else {
null
},
topHolderChats = metadata.topHolderChatsList.map { chatMapper.map(it) },
)
}
.onFailure { ErrorUtils.handleError(it) }
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.flipcash.services.models

import com.flipcash.services.models.chat.ChatId
import com.flipcash.services.models.chat.Emoji
import com.getcode.opencode.model.core.ID
import com.getcode.opencode.model.financial.Fiat
import com.getcode.opencode.model.financial.LocalFiat
Expand Down Expand Up @@ -58,6 +60,7 @@ sealed interface NotificationMetadata {
data class DirectlySentCrypto(
val phoneNumber: String? = null,
val userId: ID? = null,
val boostedReaction: BoostedReaction? = null,
) : NotificationMetadata

/**
Expand All @@ -77,6 +80,7 @@ sealed interface NotificationMetadata {
data class ReceivedCrypto(
val phoneNumber: String? = null,
val userId: ID? = null,
val boostedReaction: BoostedReaction? = null,
) : NotificationMetadata

/**
Expand Down Expand Up @@ -108,6 +112,19 @@ sealed interface NotificationMetadata {
/**
* The state of a swap as a whole.
*/
/**
* Set on a sent/received payment notification when the payment boosted a reaction. A boost that
* paid one person names them as the notification's counterparty; one that paid both the
* message's sender and the group's creator names neither, and the notification amount is the
* whole boost.
*/
@Serializable
data class BoostedReaction(
val chatId: ChatId,
val messageId: Long,
val emoji: Emoji,
)

enum class SwapState {
UNKNOWN,
PENDING,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.flipcash.services.models

import com.flipcash.services.models.chat.ChatMetadata
import com.getcode.solana.keys.Mint

/**
* A community built around a Flipcash-launched currency.
*
* @param mint The currency the community is built around
* @param name The community's display name
* @param leader The community's leader, if one has been established
* @param topHolderChats Chats among the currency's top holders, as returned by the server
*/
data class Community(
val mint: Mint,
val name: String,
val leader: CommunityLeader?,
val topHolderChats: List<ChatMetadata>,
)

data class CommunityLeader(
val profile: UserProfile,
)
Loading
Loading