Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Android): Elevator alert UI #705

Merged
merged 7 commits into from
Feb 3, 2025
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 @@ -54,7 +54,7 @@ class AlertDetailsTest {
alert,
null,
listOf(route),
stopId = null,
stop = null,
affectedStops = listOf(stop1, stop2, stop3),
now = now
)
Expand Down Expand Up @@ -94,7 +94,7 @@ class AlertDetailsTest {
alert,
line = null,
routes = null,
stopId = null,
stop = null,
affectedStops = emptyList(),
now = now
)
Expand Down Expand Up @@ -124,7 +124,7 @@ class AlertDetailsTest {
alert,
line = null,
routes = null,
stopId = null,
stop = null,
affectedStops = emptyList(),
now = now
)
Expand Down Expand Up @@ -154,7 +154,7 @@ class AlertDetailsTest {
alert,
line = null,
routes = null,
stopId = null,
stop = null,
affectedStops = emptyList(),
now = now
)
Expand Down Expand Up @@ -192,7 +192,7 @@ class AlertDetailsTest {
alert,
line = null,
routes = null,
stopId = null,
stop = null,
affectedStops = affectedStops.value,
now = now
)
Expand All @@ -215,4 +215,74 @@ class AlertDetailsTest {
.assertDoesNotExist()
composeTestRule.onNodeWithText("More details").assertIsDisplayed()
}

@Test
fun testStopHeader() {
val objects = ObjectCollectionBuilder()

val now = Clock.System.now()

val stop = objects.stop { name = "Stop" }

val alert =
objects.alert {
activePeriod(now - 5.minutes, now + 5.minutes)
cause = Alert.Cause.UnrulyPassenger
effect = Alert.Effect.StopClosure
effectName = "Closure"
updatedAt = now - 100.minutes
header = "Alert header"
description = "Alert description"
}

composeTestRule.setContent {
AlertDetails(
alert,
line = null,
routes = null,
stop = stop,
affectedStops = listOf(stop),
now = now
)
}

composeTestRule.onNodeWithText("${stop.name} Stop Closure").assertExists()
}

@Test
fun testElevatorAlert() {
val objects = ObjectCollectionBuilder()

val now = Clock.System.now()

val stop = objects.stop { name = "Stop" }

val alert =
objects.alert {
activePeriod(now - 5.minutes, now + 5.minutes)
cause = Alert.Cause.Maintenance
effect = Alert.Effect.ElevatorClosure
effectName = "Elevator Closure"
updatedAt = now - 100.minutes
header = "Alert header"
description = "Alert description"
}

composeTestRule.setContent {
AlertDetails(
alert,
line = null,
routes = null,
stop = stop,
affectedStops = listOf(stop),
now = now
)
}

composeTestRule.onNodeWithText("${stop.name} Elevator Closure").assertIsDisplayed()
composeTestRule.onNodeWithText(alert.header!!).assertIsDisplayed()
composeTestRule.onNodeWithText("Alternative path").assertIsDisplayed()
composeTestRule.onNodeWithText(alert.description!!).assertIsDisplayed()
composeTestRule.onNodeWithText("1 affected stop").assertDoesNotExist()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,14 @@ fun AlertDetails(
alert: Alert,
line: Line?,
routes: List<Route>?,
stopId: String?,
stop: Stop?,
affectedStops: List<Stop>,
now: Instant,
analytics: Analytics = koinInject()
) {
val routeId = line?.id ?: routes?.firstOrNull()?.id ?: ""
val routeLabel = line?.longName ?: routes?.firstOrNull()?.label ?: ""
val routeLabel = line?.longName ?: routes?.firstOrNull()?.label
val stopLabel = stop?.name
val effectLabel = FormattedAlert.format(alert)?.effect?.let { stringResource(it) }
val causeLabel =
when (alert.cause) {
Expand Down Expand Up @@ -129,48 +130,85 @@ fun AlertDetails(
else -> null
}
val currentPeriod = alert.activePeriod.firstOrNull { it.activeAt(now) }
val isElevatorAlert = alert.effect == Alert.Effect.ElevatorClosure
val elevatorSubtitle = if (isElevatorAlert) alert.header else null

Column(
Modifier.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp)
.padding(top = 24.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
AlertTitle(routeLabel, effectLabel, causeLabel)
AlertPeriod(currentPeriod)
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
AffectedStopCollapsible(
affectedStops,
onTappedAffectedStops = {
analytics.tappedAffectedStops(routeId, stopId ?: "", alert.id)
}
)
TripPlannerLink(
onTappedTripPlanner = {
analytics.tappedTripPlanner(routeId, stopId ?: "", alert.id)
}
)
AlertTitle(
routeLabel,
stopLabel,
effectLabel,
causeLabel,
elevatorSubtitle,
isElevatorAlert
)
if (!isElevatorAlert) {
AlertPeriod(currentPeriod)

Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
AffectedStopCollapsible(
affectedStops,
onTappedAffectedStops = {
analytics.tappedAffectedStops(routeId, stop?.id ?: "", alert.id)
}
)
TripPlannerLink(
onTappedTripPlanner = {
analytics.tappedTripPlanner(routeId, stop?.id ?: "", alert.id)
}
)
}
}
AlertDescription(alert, affectedStopsKnown = affectedStops.isNotEmpty())
if (isElevatorAlert) {
AlertPeriod(currentPeriod)
}
AlertFooter(alert.updatedAt)
}
}

@Composable
private fun AlertTitle(routeLabel: String, effectLabel: String?, causeLabel: String?) {
private fun AlertTitle(
routeLabel: String?,
stopLabel: String?,
effectLabel: String?,
causeLabel: String?,
elevatorSubtitle: String?,
isElevatorAlert: Boolean = false,
) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
if (effectLabel != null) {
Text(
stringResource(R.string.route_effect, routeLabel, effectLabel),
style = MaterialTheme.typography.titleLarge
)
if (routeLabel != null) {
Text(
stringResource(R.string.route_effect, routeLabel, effectLabel),
style = MaterialTheme.typography.titleLarge
)
} else if (stopLabel != null) {
Text(
stringResource(R.string.route_effect, stopLabel, effectLabel),
style = MaterialTheme.typography.titleLarge
)
} else {
Text(effectLabel, style = MaterialTheme.typography.titleLarge)
}
}
if (causeLabel != null) {
if (!isElevatorAlert && causeLabel != null) {
Text(
causeLabel,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.bodySmall
)
} else if (isElevatorAlert && elevatorSubtitle != null) {
Text(
elevatorSubtitle,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.bodySmall
)
}
}
}
Expand Down Expand Up @@ -297,9 +335,10 @@ private fun splitDetails(details: String, separator: String? = null): List<Strin

@Composable
private fun AlertDescription(alert: Alert, affectedStopsKnown: Boolean) {
val isElevatorAlert = alert.effect == Alert.Effect.ElevatorClosure
val alertDescriptionParagraphs = buildList {
val header = alert.header
if (header != null) {
if (!isElevatorAlert && header != null) {
addAll(splitDetails(header))
}
val description = alert.description
Expand All @@ -314,7 +353,8 @@ private fun AlertDescription(alert: Alert, affectedStopsKnown: Boolean) {
if (alertDescriptionParagraphs.isNotEmpty()) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(
stringResource(R.string.full_description),
if (!isElevatorAlert) stringResource(R.string.full_description)
else stringResource(R.string.alternative_path),
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.bodySmall
)
Expand Down Expand Up @@ -366,7 +406,7 @@ private fun AlertDetailsPreview() {
updatedAt = now - 10.minutes
}
Column(Modifier.background(colorResource(R.color.fill2)).padding(16.dp)) {
AlertDetails(alert, null, listOf(route), stop.id, listOf(stop), now, MockAnalytics())
AlertDetails(alert, null, listOf(route), stop, listOf(stop), now, MockAnalytics())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
Expand All @@ -22,6 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -52,6 +54,7 @@ fun AlertDetailsPage(

val line = globalResponse?.getLine(lineId)
val routes = routeIds?.mapNotNull { globalResponse?.routes?.get(it) }
val stop = globalResponse?.stops?.get(stopId)

val firstRoute = routes?.firstOrNull()

Expand Down Expand Up @@ -93,7 +96,14 @@ fun AlertDetailsPage(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (firstRoute != null) {
if (alert?.effect == Alert.Effect.ElevatorClosure) {
Image(
painterResource(R.drawable.elevator_alert_monochrome),
null,
Modifier.height(24.dp),
colorFilter = ColorFilter.tint(headerTextColor)
)
} else if (firstRoute != null) {
val (icon, description) = routeIcon(firstRoute)
Image(
icon,
Expand All @@ -112,7 +122,7 @@ fun AlertDetailsPage(
ActionButton(ActionButtonKind.Close) { goBack() }
}
if (alert != null) {
AlertDetails(alert, line, routes, stopId, affectedStops, now)
AlertDetails(alert, line, routes, stop, affectedStops, now)
} else {
CircularProgressIndicator()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import com.mbta.tid.mbta_app.model.StopAlertState
fun AlertIcon(alertState: StopAlertState, color: Color?, modifier: Modifier = Modifier) {
val iconId =
when (alertState) {
StopAlertState.Elevator -> R.drawable.accessibility_icon_inaccessible
StopAlertState.Elevator -> R.drawable.elevator_alert
StopAlertState.Issue -> R.drawable.alert_borderless_issue
StopAlertState.Shuttle -> R.drawable.alert_borderless_shuttle
StopAlertState.Suspension -> R.drawable.alert_borderless_suspension
Expand All @@ -25,6 +25,8 @@ fun AlertIcon(alertState: StopAlertState, color: Color?, modifier: Modifier = Mo
painterResource(iconId),
contentDescription = stringResource(R.string.alert),
modifier,
tint = color ?: colorResource(R.color.text)
tint =
if (alertState == StopAlertState.Elevator) Color.Unspecified
else color ?: colorResource(R.color.text)
)
}
Loading