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 @@ -36,6 +36,7 @@ import com.flipcash.app.currency.RegionSelectionScreen
import com.flipcash.app.deposit.DepositFlowScreen
import com.flipcash.app.invite.InviteContactScreen
import com.flipcash.app.messenger.ChatFlowScreen
import com.flipcash.app.messenger.ReportFlowScreen
import com.flipcash.app.discovery.TokenDiscoveryScreen
import com.flipcash.app.internal.ui.navigation.decorators.rememberNavMessagingEntryDecorator
import com.flipcash.app.lab.LabsScreen
Expand Down Expand Up @@ -119,6 +120,9 @@ fun appEntryProvider(
annotatedEntry<AppRoute.Messaging.NewGroup> { key ->
NewGroupFlowScreen(route = key, resultStateRegistry = resultStateRegistry)
}
annotatedEntry<AppRoute.Messaging.Report> { key ->
ReportFlowScreen(route = key, resultStateRegistry = resultStateRegistry)
}

// Tokens
annotatedEntry<AppRoute.Token.Info>(testTag = "token_info_screen") { key ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ fun NavMessagingEntryDecorator(
val analytics = rememberAnalytics()
Box {
entry.Content()
val isTopEntry = entry.contentKey == backStack.lastOrNull()?.toString()
if (isTopEntry && entry.metadata[NavMetadataKeys.IsSheet.key] != true) {
val hosts = hostsBars(
isTopEntry = entry.contentKey == backStack.lastOrNull()?.toString(),
isSheet = entry.metadata[NavMetadataKeys.IsSheet.key] == true,
coversScreen = entry.metadata[NavMetadataKeys.IsFullscreenSheet.key] == true,
)
if (hosts) {
BottomBarContainer(barManager.barMessages) { message ->
trace(
message = "bottom bar message shown [${message.type.name}]",
Expand Down Expand Up @@ -71,3 +75,16 @@ private fun NavKey.screenName(): String? {
val name = this::class.java.name
return name.removePrefix("$prefix$").replace('$', '.').takeIf { it != name }
}

/**
* Whether an entry is the one that puts the app's bars on screen.
*
* A sheet that covers the screen carries its own bars, exactly as a route does: the bar has the
* whole window to sit in, and its scrim has nothing left to leave showing. A partial sheet has
* neither — a bar inside one is clipped to the sheet, and one behind it is covered — so a partial
* sheet leaves bars to the route beneath it. That is why a bar raised from inside a partial sheet
* is only seen once the sheet has closed, and why a flow that wants to be acknowledged before it
* closes has to cover the screen.
*/
internal fun hostsBars(isTopEntry: Boolean, isSheet: Boolean, coversScreen: Boolean): Boolean =
isTopEntry && (!isSheet || coversScreen)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.flipcash.app.internal.ui.navigation.decorators

import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

/**
* Which entry puts the app's bars on screen.
*
* The rule is easy to get wrong in the direction that costs a whole feature: get it wrong for a
* sheet and a bar raised from inside that sheet is never seen, because it renders behind the sheet
* or is clipped to it. Report asks for a confirmation before it closes, so it needs the answer to
* be yes while it is still open.
*/
class BarHostingTest {

@Test
fun `a route hosts its own bars`() {
assertTrue(hostsBars(isTopEntry = true, isSheet = false, coversScreen = false))
}

@Test
fun `a sheet that covers the screen hosts its own bars`() {
assertTrue(hostsBars(isTopEntry = true, isSheet = true, coversScreen = true))
}

@Test
fun `a partial sheet leaves bars to the route beneath it`() {
assertFalse(hostsBars(isTopEntry = true, isSheet = true, coversScreen = false))
}

@Test
fun `an entry that is not on top never hosts bars`() {
assertFalse(hostsBars(isTopEntry = false, isSheet = false, coversScreen = false))
assertFalse(hostsBars(isTopEntry = false, isSheet = true, coversScreen = true))
}
}
1 change: 1 addition & 0 deletions apps/flipcash/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies {

api(project(":services:flipcash-compose"))

api(project(":libs:reporting"))
implementation(project(":libs:messaging"))
api(project(":libs:permissions:public"))
implementation(project(":libs:vibrator:public"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import android.os.Parcelable
import androidx.navigation3.runtime.NavKey
import com.flipcash.app.core.chat.ChatIdentifier
import com.flipcash.app.core.chat.ChatStep
import com.flipcash.app.core.chat.ReportSubject
import com.flipcash.app.core.reporting.ReportStep
import com.flipcash.app.core.chat.NewGroupStep
import com.flipcash.app.core.deposit.DepositResult
import com.flipcash.app.core.deposit.DepositStep
Expand Down Expand Up @@ -360,6 +362,33 @@ sealed interface AppRoute : NavKey, Parcelable {
override val initialStack: List<NavKey>
get() = listOf(NewGroupStep.Form)
}

/**
* Reporting a person, a group, or a single message.
*
* A top-level route rather than a step of [Chat], although every way in is a chat surface.
* Reporting a person is about the person, not the conversation it was opened from, and the
* flow outlives the surface: it is reachable from a profile, a group profile, and the
* selection bar, and sharing one route means those three cannot drift apart. It also keeps
* a half-written report out of the chat's view model.
*
* A [com.getcode.navigation.FullscreenSheet] without `WrapContentSheet`: it rests at a
* fixed height and stays there while the steps change. The wrap-content version of this
* deadlocked — such a sheet derives its drag anchors from the height its content reports
* and reads them again while placing that content, so content that resizes itself never
* settles. Fullscreen rather than the usual expanded detent because both steps are worked
* through rather than glanced at, and the second is a text field with the keyboard up.
*/
@Serializable
@Parcelize
data class Report(val subject: ReportSubject) :
Messaging,
FlowRoute,
com.getcode.navigation.Sheet,
com.getcode.navigation.FullscreenSheet {
override val initialStack: List<NavKey>
get() = listOf(ReportStep.ReasonSelection)
}
}

@Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.flipcash.app.core.chat

import android.os.Parcelable
import com.flipcash.services.models.chat.ChatId
import com.getcode.opencode.model.core.ID
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable

/**
* What a report is about, already resolved to the ids the request will be keyed by.
*
* Mirrors [com.flipcash.services.models.ReportTarget] rather than reusing it: that one belongs to
* the service layer and is not a navigation type, and this has to survive process death on the
* back stack.
*
* It carries ids rather than naming a surface ("the chat this was opened on") because reporting is
* its own flow now — a top-level route — so there is no shared chat view model behind it to ask.
* Each surface resolves the ids it already holds when it opens the flow.
*
* The user case names the person, not the DM: a tip DM's id is derived on the client, so naming
* the chat would name something the server has never been told about, and a report made from a
* group member's profile is about the person either way.
*/
@Serializable
sealed interface ReportSubject : Parcelable {
@Parcelize
@Serializable
data class User(val userId: ID) : ReportSubject

@Parcelize
@Serializable
data class Chat(val chatId: ChatId) : ReportSubject

@Parcelize
@Serializable
data class Message(val chatId: ChatId, val messageId: Long) : ReportSubject
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.flipcash.app.core.reporting

import android.os.Parcelable
import com.flipcash.reporting.ReportReason
import com.getcode.navigation.NonDraggableRoute
import com.getcode.navigation.flow.FlowStep
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable

/**
* Picking a reason, and — for [ReportReason.Other] alone — saying what happened.
*
* [NonDraggableRoute] at the interface level, as `CurrencyCreatorStep` does: the enclosing route is
* a sheet, and a sheet that can be dragged away is the wrong affordance over a half-written report.
* Leaving is offered by the app bar, which [com.getcode.navigation.flow.FlowHost] turns from a
* close into a back arrow once there is a step to go back to.
*
* Steps rather than one screen that swaps its own contents. The earlier version did the latter
* inside a wrap-content sheet and deadlocked the main thread: such a sheet takes its drag anchors
* from the height its content reports and reads those anchors again while placing that content, so
* content that resizes itself keeps changing the number it was measured against. A step boundary
* makes the height change once, between two fixed layouts.
*/
@Serializable
sealed interface ReportStep : FlowStep, Parcelable, NonDraggableRoute {
@Parcelize
@Serializable
data object ReasonSelection : ReportStep

/** Reached only from [ReportReason.Other]; every other reason submits from the list. */
@Parcelize
@Serializable
data class Details(val reason: ReportReason) : ReportStep
}
25 changes: 25 additions & 0 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,31 @@
<string name="error_title_failedToUnblock">Something Went Wrong</string>
<string name="error_description_failedToUnblock">We were unable to unblock the user. Please try again</string>

<string name="title_report">Report</string>
<string name="subtitle_report">Tell us what’s wrong. Your report is private — we won’t tell them who reported</string>
<string name="action_reportReason_spam">Spam</string>
<string name="action_reportReason_scamOrFraud">Scam or Fraud</string>
<string name="action_reportReason_harassment">Harassment or Bullying</string>
<string name="action_reportReason_sexualContent">Sexual Content</string>
<string name="action_reportReason_violence">Violence or Threats</string>
<string name="action_reportReason_other">Something Else</string>
<string name="description_reportReason_spam">Repeated unwanted messages, promotions, or links</string>
<string name="description_reportReason_scamOrFraud">Attempts to take money or account details by deception</string>
<string name="description_reportReason_harassment">Abuse, insults, or repeated unwanted contact</string>
<string name="description_reportReason_sexualContent">Explicit content, or unwanted sexual messages</string>
<string name="description_reportReason_violence">Threats of harm, or content showing violence</string>
<string name="description_reportReason_other">None of these fit — tell us in your own words</string>
<string name="title_reportDetails">Tell us what’s wrong</string>
<string name="subtitle_reportDetails">Your report is private — we won’t tell them who reported</string>
<string name="hint_reportDetails">Tell us more</string>
<string name="action_submitReport">Submit Report</string>
<string name="action_expandReportDetails">Expand</string>
<string name="action_collapseReportDetails">Collapse</string>
<string name="prompt_title_reportSubmitted">Report Sent</string>
<string name="prompt_description_reportSubmitted">Thanks. We’ll take a look</string>
<string name="error_title_failedToReport">Something Went Wrong</string>
<string name="error_description_failedToReport">We were unable to send your report. Please try again</string>

<string name="title_tipOnboarding">Send Your First Tip</string>

<!-- You tab (tip card) -->
Expand Down
1 change: 1 addition & 0 deletions apps/flipcash/features/messenger/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies {
implementation(project(":apps:flipcash:shared:userflags"))
implementation(project(":libs:vibrator:bindings"))
implementation(project(":libs:messaging"))
implementation(project(":libs:reporting"))
implementation(project(":services:flipcash"))
implementation(project(":services:opencode"))
implementation(project(":libs:datetime"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ fun ChatFlowScreen(
// Popping with the IME still up drags the screen behind it out from under the keyboard.
onExit = { _, _ -> keyboard.hideIfVisible { navigator.pop() } },
entryProvider = chatEntryProvider(route.identifier, route.openKeyboard),
// ChatStep.AmountEntry, ChatStep.InitPayment, ChatStep.InviteToGroup and ChatStep.MuteChat
// are Sheets, so the
// ChatStep.AmountEntry, ChatStep.InitPayment, ChatStep.InviteToGroup and
// ChatStep.MuteChat are Sheets, so the
// flow needs the sheet strategy to draw them as such; without it the step would fall
// through to SinglePane and cover the thread. Amount entry
// returns its result inside the flow (resultBackNavigator), so the strategy's own
Expand Down Expand Up @@ -98,6 +98,7 @@ private fun chatEntryProvider(
annotatedEntry<ChatStep.MuteChat> {
FlowMuteChatSheet()
}

annotatedEntry<ChatStep.Profile> { step ->
FlowChatProfileScreen(step.contact)
}
Expand Down Expand Up @@ -274,6 +275,7 @@ private fun FlowMuteChatSheet() {
)
}


@Composable
private fun FlowChatProfileScreen(participant: ChatParticipant) {
val viewModel = flowSharedViewModel<ChatProfileViewModel>()
Expand Down
Loading
Loading