diff --git a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/models/ChatListItem.kt b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/models/ChatListItem.kt index 7978ace09c..fcb7886f04 100644 --- a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/models/ChatListItem.kt +++ b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/models/ChatListItem.kt @@ -219,6 +219,7 @@ sealed interface ChatListItem { is MessageContent.Media -> "media" is MessageContent.Reply -> "reply-message" is MessageContent.System -> "system-message" + is MessageContent.Widget -> "widget-message" is MessageContent.Encrypted -> "encrypted-message" } } diff --git a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/ChatSummaryMapping.kt b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/ChatSummaryMapping.kt index 01278c75cd..5282068175 100644 --- a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/ChatSummaryMapping.kt +++ b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/ChatSummaryMapping.kt @@ -182,6 +182,9 @@ private fun MessageContent.previewText( // TODO: is MessageContent.Media -> null is MessageContent.System -> null + + // TODO: preview text for widgets once the design settles. + is MessageContent.Widget -> null } private const val MAX_REPLY_UNWRAP_DEPTH = 4 diff --git a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/LinkDetection.kt b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/LinkDetection.kt index 255217fd0b..a37961ced3 100644 --- a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/LinkDetection.kt +++ b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/LinkDetection.kt @@ -136,6 +136,7 @@ fun MessageContent.linkableText(): String? = when (this) { is MessageContent.Deleted, is MessageContent.Media, is MessageContent.System, + is MessageContent.Widget, is MessageContent.Encrypted, -> null } diff --git a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/MessageBubble.kt b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/MessageBubble.kt index 9d9c9bdea1..9d7114a4cb 100644 --- a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/MessageBubble.kt +++ b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/MessageBubble.kt @@ -270,6 +270,8 @@ fun ContentBubble( // TODO is MessageContent.Media -> Unit is MessageContent.System -> Unit + // TODO: render ShareProfile; an unsupported widget variant draws nothing until then. + is MessageContent.Widget -> Unit } } } diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/GroupAccess.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/GroupAccess.kt index e1ce4aded7..7b8f861ba4 100644 --- a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/GroupAccess.kt +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/GroupAccess.kt @@ -93,6 +93,9 @@ sealed interface GroupAccess { // one they left. Everyone else is blocked with nothing to buy, which is the arm // that gets a disabled button rather than a purchase they cannot make. ChatRuleRequirement.Staff -> !isStaff + // A speaker-only rule; the server does not send it as a listener rule. If one + // ever appears here nobody satisfies it. + ChatRuleRequirement.Never -> true } } diff --git a/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/converters/ChatTypeConverters.kt b/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/converters/ChatTypeConverters.kt index 4a6c9e13ae..245cf244cd 100644 --- a/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/converters/ChatTypeConverters.kt +++ b/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/converters/ChatTypeConverters.kt @@ -225,6 +225,11 @@ sealed interface MessageContentSerialized { val caption: Text?, ) : MessageContentSerialized + // Mirrors MessageContent.Widget. [username] is null for an unsupported widget variant. + @Serializable + @SerialName("widget_share_profile") + data class WidgetShareProfile(val username: String?) : MessageContentSerialized + @Serializable @SerialName("system") data class System(val fallbackText: String) : MessageContentSerialized @@ -343,4 +348,8 @@ sealed interface ChatRuleRequirementSerialized { @Serializable @SerialName("staff") data object Staff : ChatRuleRequirementSerialized + + @Serializable + @SerialName("never") + data object Never : ChatRuleRequirementSerialized } diff --git a/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapper.kt b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapper.kt index 2fe4560a1f..72fe465d4b 100644 --- a/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapper.kt +++ b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapper.kt @@ -33,6 +33,7 @@ import com.flipcash.services.models.chat.ChatType import com.flipcash.services.models.chat.Emoji import com.flipcash.services.models.chat.EmojiReaction import com.flipcash.services.models.chat.MessageContent +import com.flipcash.services.models.chat.WidgetContent import com.flipcash.services.models.chat.MessagePointer import com.flipcash.services.models.chat.MuteState import com.flipcash.services.models.chat.PointerType @@ -394,6 +395,9 @@ private fun MessageContent.toSerialized(): MessageContentSerialized = when (this items = items, caption = caption?.let { MessageContentSerialized.Text(it.text) }, ) + is MessageContent.Widget -> MessageContentSerialized.WidgetShareProfile( + username = (widget as? WidgetContent.ShareProfile)?.username, + ) is MessageContent.System -> MessageContentSerialized.System(fallbackText = fallbackText) is MessageContent.Deleted -> MessageContentSerialized.Deleted( deletedAt = deletedTs.epochSeconds, @@ -407,6 +411,9 @@ private fun MessageContent.toSerialized(): MessageContentSerialized = when (this } private fun MessageContentSerialized.toDomain(): MessageContent = when (this) { + is MessageContentSerialized.WidgetShareProfile -> MessageContent.Widget( + username?.let { WidgetContent.ShareProfile(it) } ?: WidgetContent.Unsupported, + ) is MessageContentSerialized.Text -> MessageContent.Text(text) is MessageContentSerialized.Cash -> MessageContent.Cash( intentId = intentId.decodeBase58().toList(), @@ -466,6 +473,7 @@ private fun ChatRuleRequirement.toSerialized(): ChatRuleRequirementSerialized = mints = mints.map { it.base58() }, ) ChatRuleRequirement.Staff -> ChatRuleRequirementSerialized.Staff + ChatRuleRequirement.Never -> ChatRuleRequirementSerialized.Never } private fun ChatRulesSerialized.toDomain(): ChatRules = ChatRules( @@ -482,6 +490,7 @@ private fun ChatRuleRequirementSerialized.toDomain(): ChatRuleRequirement = when mints = mints.map { PublicKey(it.decodeBase58().toList()) }, ) ChatRuleRequirementSerialized.Staff -> ChatRuleRequirement.Staff + ChatRuleRequirementSerialized.Never -> ChatRuleRequirement.Never } private fun SocialAccount.toSerialized(): SocialAccountSerialized = when (this) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 299b147351..4e7395f7d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -69,7 +69,7 @@ protovalidate-kt = "0.1.3" # 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.6.0" -flipcash2-client-protocol = "0.12.0" +flipcash2-client-protocol = "0.13.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, diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt index 0a2b789aa5..f0991c3695 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt @@ -89,14 +89,20 @@ class ProfileController @Inject constructor( suspend fun setDisplayName( displayName: String, - ): Result { + ): Result { val owner = userManager.accountCluster?.authority?.keyPair ?: return Result.failure(Throwable("No account cluster in UserManager")) return repository.setDisplayName(displayName, owner) // Reflect the change locally so anything observing the profile (e.g. a setup flow // deciding which steps remain) sees it without waiting for a refresh. - .onSuccess { mergeLocalProfile { it.copy(displayName = displayName) } } + .onSuccess { assigned -> + mergeLocalProfile { + // Use the server-assigned username when it sent one, rather than assuming the + // existing username is unchanged. + it.copy(displayName = displayName, username = assigned ?: it.username) + } + } } /** diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/LocalToProtobuf.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/LocalToProtobuf.kt index 6f44bf74e0..d484fd9d31 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/LocalToProtobuf.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/LocalToProtobuf.kt @@ -14,6 +14,7 @@ import com.flipcash.services.models.chat.ChatType import com.flipcash.services.models.chat.ClientMessageId import com.flipcash.services.models.chat.IdempotencyKey import com.flipcash.services.models.chat.MessageContent +import com.flipcash.services.models.chat.WidgetContent import com.flipcash.services.models.chat.MuteState import com.flipcash.services.models.chat.PointerType import com.flipcash.services.models.chat.TypingState @@ -160,6 +161,18 @@ internal fun MessageContent.asContent(): MessagingModel.Content { .apply { if (caption != null) setCaption(MessagingModel.TextContent.newBuilder().setText(caption.text)) } ) .build() + is MessageContent.Widget -> when (val w = widget) { + is WidgetContent.ShareProfile -> MessagingModel.Content.newBuilder() + .setWidget( + MessagingModel.WidgetContent.newBuilder() + .setShareProfile( + MessagingModel.ShareProfileWidget.newBuilder().setUsername(w.username.asUsername()) + ) + ) + .build() + // Received-only: there is nothing to send for a variant this client can't read. + WidgetContent.Unsupported -> MessagingModel.Content.getDefaultInstance() + } is MessageContent.System -> MessagingModel.Content.newBuilder() .setSystem(MessagingModel.SystemContent.newBuilder().setFallbackText(fallbackText)) .build() @@ -216,7 +229,7 @@ internal fun ChatType.asProtoChatType(): ChatModel.ChatType { return when (this) { ChatType.UNKNOWN -> ChatModel.ChatType.UNKNOWN ChatType.CONTACT_DM -> ChatModel.ChatType.CONTACT_DM - ChatType.TIP_DM -> ChatModel.ChatType.TIP_DM + ChatType.TIP_DM -> ChatModel.ChatType.DM ChatType.GROUP -> ChatModel.ChatType.GROUP } } @@ -257,6 +270,7 @@ internal fun ChatRules.asProtoRules(): ChatModel.Rules { internal fun ChatRuleRequirement.asProtoListenerRules(): ChatModel.ListenerRules { val builder = ChatModel.ListenerRules.newBuilder() return when (this) { + ChatRuleRequirement.Never -> error("ListenerRules has no `never` arm; Never is speaker-only") is ChatRuleRequirement.MinimumBalance -> builder.setMinimumBalance(asProtoMinimumBalanceRequirement()).build() ChatRuleRequirement.Staff -> builder.setStaff(ChatModel.StaffRequirement.getDefaultInstance()).build() } @@ -267,6 +281,7 @@ internal fun ChatRuleRequirement.asProtoSpeakerRules(): ChatModel.SpeakerRules { return when (this) { is ChatRuleRequirement.MinimumBalance -> builder.setMinimumBalance(asProtoMinimumBalanceRequirement()).build() ChatRuleRequirement.Staff -> builder.setStaff(ChatModel.StaffRequirement.getDefaultInstance()).build() + ChatRuleRequirement.Never -> builder.setNever(ChatModel.Never.getDefaultInstance()).build() } } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt index ac0f46d624..47e5fc22a2 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt @@ -42,6 +42,7 @@ import com.flipcash.services.models.chat.ImageMetadata import com.flipcash.services.models.chat.MediaItem import com.flipcash.services.models.chat.MediaItemRendition import com.flipcash.services.models.chat.MessageContent +import com.flipcash.services.models.chat.WidgetContent import com.flipcash.services.models.chat.MessagePointer import com.flipcash.services.models.chat.MetadataUpdate import com.flipcash.services.models.chat.MuteState @@ -199,6 +200,14 @@ internal fun MessagingModel.Content.toMessageContent(): MessageContent { nonce = encrypted.nonce.toByteArray(), ciphertext = encrypted.ciphertext.toByteArray(), ) + // A widget variant this client doesn't know renders as unsupported rather than dropping. + MessagingModel.Content.TypeCase.WIDGET -> MessageContent.Widget( + when (widget.typeCase) { + MessagingModel.WidgetContent.TypeCase.SHARE_PROFILE -> + WidgetContent.ShareProfile(widget.shareProfile.username.value) + else -> WidgetContent.Unsupported + } + ) else -> MessageContent.Text("") } } @@ -387,7 +396,7 @@ internal fun ChatModel.MetadataUpdate.toMetadataUpdate( internal fun ChatModel.ChatType.toChatType(): ChatType { return when (this) { ChatModel.ChatType.CONTACT_DM -> ChatType.CONTACT_DM - ChatModel.ChatType.TIP_DM -> ChatType.TIP_DM + ChatModel.ChatType.DM -> ChatType.TIP_DM ChatModel.ChatType.GROUP -> ChatType.GROUP else -> ChatType.UNKNOWN } @@ -525,6 +534,7 @@ internal fun ChatModel.SpeakerRules.toRuleRequirementOrNull(): ChatRuleRequireme return when (kindCase) { ChatModel.SpeakerRules.KindCase.MINIMUM_BALANCE -> minimumBalance.toRuleRequirement() ChatModel.SpeakerRules.KindCase.STAFF -> ChatRuleRequirement.Staff + ChatModel.SpeakerRules.KindCase.NEVER -> ChatRuleRequirement.Never else -> null } } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt index 8bf3c5f756..7dd5d158f1 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt @@ -48,13 +48,15 @@ internal class ProfileService @Inject constructor( suspend fun setDisplayName( displayName: String, owner: Ed25519.KeyPair, - ): Result { + ): Result { return runCatching { api.setDisplayName(displayName, owner) }.foldWithSuppression( onSuccess = { response -> when (response.result) { - ProfileService.SetDisplayNameResponse.Result.OK -> Result.success(Unit) + ProfileService.SetDisplayNameResponse.Result.OK -> + // The server may auto-assign a username derived from the display name. + Result.success(if (response.hasUsername()) response.username.value else null) ProfileService.SetDisplayNameResponse.Result.INVALID_DISPLAY_NAME -> Result.failure(SetDisplayNameError.InvalidDisplayName()) ProfileService.SetDisplayNameResponse.Result.DENIED -> Result.failure(SetDisplayNameError.Denied()) ProfileService.SetDisplayNameResponse.Result.FAILED_MODERATED -> diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalProfileRepository.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalProfileRepository.kt index 0330218e69..e1886e66ad 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalProfileRepository.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalProfileRepository.kt @@ -39,7 +39,7 @@ internal class InternalProfileRepository( override suspend fun setDisplayName( displayName: String, owner: Ed25519.KeyPair - ): Result { + ): Result { return service.setDisplayName(displayName, owner) .onFailure { // The rejections below are the server answering a user's choice of diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/DmPaymentMetadata.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/DmPaymentMetadata.kt index 7bed493244..aac38cad15 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/DmPaymentMetadata.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/DmPaymentMetadata.kt @@ -65,22 +65,22 @@ fun buildTipDmPaymentMetadata( .setChat( FlipcashIntentModel.ChatMetadata.newBuilder() .setChatId(Common.ChatId.newBuilder().setValue(chatId.bytes.toByteString())) - .setTipDmPayment( - FlipcashIntentModel.ChatMetadata.TipDmPayment.newBuilder() + .setDmPayment( + FlipcashIntentModel.ChatMetadata.DmPayment.newBuilder() .setLocation( when (origin) { TipOrigin.TIPCARD -> - FlipcashIntentModel.ChatMetadata.TipDmPayment.Location.TIPCARD + FlipcashIntentModel.ChatMetadata.DmPayment.Location.FLIPCARD TipOrigin.CHAT -> - FlipcashIntentModel.ChatMetadata.TipDmPayment.Location.CHAT + FlipcashIntentModel.ChatMetadata.DmPayment.Location.CHAT } ) .setAction( when (action) { TipAction.SEND -> - FlipcashIntentModel.ChatMetadata.TipDmPayment.Action.SEND + FlipcashIntentModel.ChatMetadata.DmPayment.Action.SEND TipAction.TIP -> - FlipcashIntentModel.ChatMetadata.TipDmPayment.Action.TIP + FlipcashIntentModel.ChatMetadata.DmPayment.Action.TIP } ) ) diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ChatRules.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ChatRules.kt index 93491858c0..ede78b27e8 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ChatRules.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ChatRules.kt @@ -35,4 +35,7 @@ sealed interface ChatRuleRequirement { /** Requires Flipcash staff membership, as indicated by `UserFlags.is_staff`. */ data object Staff : ChatRuleRequirement + + /** Nobody may take the action (`chat.v1.Never`). Only the server sends it, and only as a speaker rule. */ + data object Never : ChatRuleRequirement } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MessageContent.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MessageContent.kt index d08f12a3fb..00dceaf24b 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MessageContent.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/MessageContent.kt @@ -28,6 +28,11 @@ sealed interface MessageContent { val items: List, val caption: Text?, ) : MessageContent + /** + * A structured widget (`messaging.v1.Content.widget`). A widget variant this client does not + * recognise arrives as [WidgetContent.Unsupported] and is rendered as an unsupported message. + */ + data class Widget(val widget: WidgetContent) : MessageContent data class System(val fallbackText: String) : MessageContent data class Deleted( val deletedTs: Instant, diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/WidgetContent.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/WidgetContent.kt new file mode 100644 index 0000000000..bafd31e9e2 --- /dev/null +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/WidgetContent.kt @@ -0,0 +1,10 @@ +package com.flipcash.services.models.chat + +/** The variant carried by a [MessageContent.Widget]. */ +sealed interface WidgetContent { + /** Invites the viewer to open [username]'s profile. */ + data class ShareProfile(val username: String) : WidgetContent + + /** A `WidgetContent.type` this client does not recognise. */ + data object Unsupported : WidgetContent +} diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/repository/ProfileRepository.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/repository/ProfileRepository.kt index b212fd9b22..e9ab4e99e8 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/repository/ProfileRepository.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/repository/ProfileRepository.kt @@ -12,7 +12,7 @@ import com.getcode.opencode.model.financial.Fiat interface ProfileRepository { suspend fun getProfile(identifier: ProfileIdentifier, owner: Ed25519.KeyPair): Result - suspend fun setDisplayName(displayName: String, owner: Ed25519.KeyPair): Result + suspend fun setDisplayName(displayName: String, owner: Ed25519.KeyPair): Result suspend fun setUsername(username: String, owner: Ed25519.KeyPair): Result suspend fun setProfilePicture(blobId: BlobId, owner: Ed25519.KeyPair): Result suspend fun updateTipCard(owner: Ed25519.KeyPair, hexColor: String): Result diff --git a/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ProfileControllerTest.kt b/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ProfileControllerTest.kt index 013c52dc55..c0c547c3ab 100644 --- a/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ProfileControllerTest.kt +++ b/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ProfileControllerTest.kt @@ -209,13 +209,35 @@ class ProfileControllerTest { @Test fun `setDisplayName delegates to repository`() = runTest { stubOwner() - repository.setDisplayNameResult = Result.success(Unit) + repository.setDisplayNameResult = Result.success(null) val result = controller.setDisplayName("Test") assertTrue(result.isSuccess) } + @Test + fun `setDisplayName caches the username the server assigned`() = runTest { + stubOwner() + every { userManager.profile } returns stubProfile().copy(username = "old") + repository.setDisplayNameResult = Result.success("assigned") + + controller.setDisplayName("Test") + + verify { userManager.set(match { it.displayName == "Test" && it.username == "assigned" }) } + } + + @Test + fun `setDisplayName keeps the existing username when the server assigns none`() = runTest { + stubOwner() + every { userManager.profile } returns stubProfile().copy(username = "old") + repository.setDisplayNameResult = Result.success(null) + + controller.setDisplayName("Test") + + verify { userManager.set(match { it.displayName == "Test" && it.username == "old" }) } + } + // endregion // region linkTwitterXAccount @@ -302,7 +324,7 @@ class ProfileControllerTest { private class FakeProfileRepository : ProfileRepository { var getProfileResult: Result = Result.failure(RuntimeException("not configured")) - var setDisplayNameResult: Result = Result.success(Unit) + var setDisplayNameResult: Result = Result.success(null) var setUsernameResult: Result = Result.success(Unit) var setProfilePictureResult: Result = Result.failure(RuntimeException("not configured")) var updateTipCardResult: Result = Result.success(Unit) diff --git a/services/flipcash/src/test/kotlin/com/flipcash/services/internal/network/extensions/WidgetAndNeverMappingTest.kt b/services/flipcash/src/test/kotlin/com/flipcash/services/internal/network/extensions/WidgetAndNeverMappingTest.kt new file mode 100644 index 0000000000..7b98ade634 --- /dev/null +++ b/services/flipcash/src/test/kotlin/com/flipcash/services/internal/network/extensions/WidgetAndNeverMappingTest.kt @@ -0,0 +1,54 @@ +package com.flipcash.services.internal.network.extensions + +import com.codeinc.flipcash.gen.chat.v1.Model as ChatModel +import com.codeinc.flipcash.gen.common.v1.Common +import com.codeinc.flipcash.gen.messaging.v1.Model as MessagingModel +import com.flipcash.services.models.chat.ChatRuleRequirement +import com.flipcash.services.models.chat.MessageContent +import com.flipcash.services.models.chat.WidgetContent +import org.junit.Test +import kotlin.test.assertEquals + +class WidgetAndNeverMappingTest { + + @Test + fun `share profile widget maps to the domain widget`() { + val content = MessagingModel.Content.newBuilder() + .setWidget( + MessagingModel.WidgetContent.newBuilder().setShareProfile( + MessagingModel.ShareProfileWidget.newBuilder() + .setUsername(Common.Username.newBuilder().setValue("alice")) + ) + ) + .build() + + assertEquals( + MessageContent.Widget(WidgetContent.ShareProfile("alice")), + content.toMessageContent(), + ) + } + + @Test + fun `widget with no recognised variant maps to unsupported`() { + val content = MessagingModel.Content.newBuilder() + .setWidget(MessagingModel.WidgetContent.getDefaultInstance()) + .build() + + assertEquals(MessageContent.Widget(WidgetContent.Unsupported), content.toMessageContent()) + } + + @Test + fun `share profile widget round trips through the wire type`() { + val domain = MessageContent.Widget(WidgetContent.ShareProfile("alice")) + + assertEquals(domain, domain.asContent().toMessageContent()) + } + + @Test + fun `speaker never maps to Never and back`() { + val proto = ChatRuleRequirement.Never.asProtoSpeakerRules() + + assertEquals(ChatModel.SpeakerRules.KindCase.NEVER, proto.kindCase) + assertEquals(ChatRuleRequirement.Never, proto.toRuleRequirementOrNull()) + } +} diff --git a/services/flipcash/src/test/kotlin/com/flipcash/services/internal/proto/ProtoWireFormatTest.kt b/services/flipcash/src/test/kotlin/com/flipcash/services/internal/proto/ProtoWireFormatTest.kt index c59952b90b..dc9865fd3b 100644 --- a/services/flipcash/src/test/kotlin/com/flipcash/services/internal/proto/ProtoWireFormatTest.kt +++ b/services/flipcash/src/test/kotlin/com/flipcash/services/internal/proto/ProtoWireFormatTest.kt @@ -96,7 +96,7 @@ class ProtoWireFormatTest { @Test fun `preserves unknown fields when re-encoding`() { val original = Model.Metadata.newBuilder() - .setType(Model.ChatType.TIP_DM) + .setType(Model.ChatType.DM) .setLatestEventSequence(42L) .setTitle("forward compatible") .build() diff --git a/services/flipcash/src/test/kotlin/com/flipcash/services/models/DmPaymentMetadataTest.kt b/services/flipcash/src/test/kotlin/com/flipcash/services/models/DmPaymentMetadataTest.kt index 0cd0bf2b19..ffb85ca43a 100644 --- a/services/flipcash/src/test/kotlin/com/flipcash/services/models/DmPaymentMetadataTest.kt +++ b/services/flipcash/src/test/kotlin/com/flipcash/services/models/DmPaymentMetadataTest.kt @@ -10,7 +10,7 @@ import kotlin.test.assertNull /** * [buildTipDmPaymentMetadata] and [buildDmPaymentMetadata] are the only places that build a - * `ChatMetadata.TipDmPayment` / `ChatMetadata.ContactDmPayment`. Nothing round-tripped either + * `ChatMetadata.DmPayment` / `ChatMetadata.ContactDmPayment`. Nothing round-tripped either * through the wire before this, so a mistake in which field gets which value — or in which enum * constant a [TipOrigin]/[TipAction] maps to — would ship silently. These parse the built bytes * back into `AppMetadata` and assert on the decoded fields, not just on the builder calls. @@ -30,16 +30,16 @@ class DmPaymentMetadataTest { val decoded = FlipcashIntentModel.AppMetadata.parseFrom(bytes!!) assertEquals( - FlipcashIntentModel.ChatMetadata.TipDmPayment.Location.TIPCARD, - decoded.chat.tipDmPayment.location, + FlipcashIntentModel.ChatMetadata.DmPayment.Location.FLIPCARD, + decoded.chat.dmPayment.location, ) assertEquals( - FlipcashIntentModel.ChatMetadata.TipDmPayment.Action.TIP, - decoded.chat.tipDmPayment.action, + FlipcashIntentModel.ChatMetadata.DmPayment.Action.TIP, + decoded.chat.dmPayment.action, ) assertNotEquals( - FlipcashIntentModel.ChatMetadata.TipDmPayment.Action.DEFAULT, - decoded.chat.tipDmPayment.action, + FlipcashIntentModel.ChatMetadata.DmPayment.Action.DEFAULT, + decoded.chat.dmPayment.action, ) assertEquals(chatId.bytes.toByteString(), decoded.chat.chatId.value) } @@ -55,16 +55,16 @@ class DmPaymentMetadataTest { val decoded = FlipcashIntentModel.AppMetadata.parseFrom(bytes!!) assertEquals( - FlipcashIntentModel.ChatMetadata.TipDmPayment.Location.CHAT, - decoded.chat.tipDmPayment.location, + FlipcashIntentModel.ChatMetadata.DmPayment.Location.CHAT, + decoded.chat.dmPayment.location, ) assertEquals( - FlipcashIntentModel.ChatMetadata.TipDmPayment.Action.SEND, - decoded.chat.tipDmPayment.action, + FlipcashIntentModel.ChatMetadata.DmPayment.Action.SEND, + decoded.chat.dmPayment.action, ) assertNotEquals( - FlipcashIntentModel.ChatMetadata.TipDmPayment.Action.DEFAULT, - decoded.chat.tipDmPayment.action, + FlipcashIntentModel.ChatMetadata.DmPayment.Action.DEFAULT, + decoded.chat.dmPayment.action, ) }