Skip to content

Commit

Permalink
feat: Special behavior for target stop on combined stop/trip details (#…
Browse files Browse the repository at this point in the history
…581)

* feat: Add special behavior when vehicle at target

* test: Add tests for updated target stop behavior

* fix: Add some missed MainActor annotations
  • Loading branch information
EmmaSimon authored Dec 12, 2024
1 parent ce28845 commit 39e59d0
Show file tree
Hide file tree
Showing 9 changed files with 425 additions and 77 deletions.
4 changes: 4 additions & 0 deletions iosApp/iosApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@
9A7F12132CCB185D0042B0F1 /* TabLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7F12122CCB185D0042B0F1 /* TabLabel.swift */; };
9A7F12172CCFEFAA0042B0F1 /* MoreLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7F12162CCFEFAA0042B0F1 /* MoreLink.swift */; };
9A7F12192CCFF2D20042B0F1 /* MorePhone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7F12182CCFF2D20042B0F1 /* MorePhone.swift */; };
9A8375EE2D0A14DD00E3694F /* TripVehicleCardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8375ED2D0A14DD00E3694F /* TripVehicleCardTests.swift */; };
9A84DB102D03A6BF00A78C64 /* TripStopsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A84DB0F2D03A6BF00A78C64 /* TripStopsTests.swift */; };
9A887D572B683103006F5B80 /* SearchResultsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A887D562B683103006F5B80 /* SearchResultsContainer.swift */; };
9A887D592B698EF1006F5B80 /* SearchResultViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A887D582B698EF1006F5B80 /* SearchResultViewTests.swift */; };
Expand Down Expand Up @@ -473,6 +474,7 @@
9A7F12122CCB185D0042B0F1 /* TabLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabLabel.swift; sourceTree = "<group>"; };
9A7F12162CCFEFAA0042B0F1 /* MoreLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreLink.swift; sourceTree = "<group>"; };
9A7F12182CCFF2D20042B0F1 /* MorePhone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MorePhone.swift; sourceTree = "<group>"; };
9A8375ED2D0A14DD00E3694F /* TripVehicleCardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripVehicleCardTests.swift; sourceTree = "<group>"; };
9A84DB0F2D03A6BF00A78C64 /* TripStopsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripStopsTests.swift; sourceTree = "<group>"; };
9A887D562B683103006F5B80 /* SearchResultsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsContainer.swift; sourceTree = "<group>"; };
9A887D582B698EF1006F5B80 /* SearchResultViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultViewTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -961,6 +963,7 @@
9A9B9FFF2D03565800BCB2BD /* TripDetailsViewTests.swift */,
9A4092EA2D0258A20026EB01 /* TripStopRowTests.swift */,
9A84DB0F2D03A6BF00A78C64 /* TripStopsTests.swift */,
9A8375ED2D0A14DD00E3694F /* TripVehicleCardTests.swift */,
);
path = StopDetails;
sourceTree = "<group>";
Expand Down Expand Up @@ -1479,6 +1482,7 @@
ED5C93F62C4A1AD70086D017 /* TripDetailsHeaderTests.swift in Sources */,
9A9BA0002D03565800BCB2BD /* TripDetailsViewTests.swift in Sources */,
9A60E8E72B8501BD008A8D5C /* RoutePillTests.swift in Sources */,
9A8375EE2D0A14DD00E3694F /* TripVehicleCardTests.swift in Sources */,
6E35D4D32B72CD3900A2BF95 /* HomeMapViewTests.swift in Sources */,
9AF0937A2BD962FF001DF39F /* DirectionPickerTests.swift in Sources */,
9A6FA0282BC72F110067769C /* LegacyStopDetailsPageTests.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion iosApp/iosApp/ComponentViews/UpcomingTripView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ struct UpcomingTripView: View {
case let .some(prediction):
switch onEnum(of: prediction) {
case let .overridden(overridden):
Text(overridden.textWithLocale()).realtime()
Text(overridden.textWithLocale()).realtime(hideIndicator: hideRealtimeIndicators)
case .hidden, .skipped:
// should have been filtered out already
Text(verbatim: "")
Expand Down
41 changes: 41 additions & 0 deletions iosApp/iosApp/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -8721,6 +8721,47 @@
}
}
},
"Waiting to depart" : {
"comment" : "Label for a vehicle stopped at a terminal station waiting to start a trip. For example: Waiting to depart Alewife",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Esperando para partir"
}
},
"ht" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Ap tann pou yo pati"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Esperando para partir"
}
},
"vi" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Đang chờ khởi hành"
}
},
"zh-Hans-CN" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "等待出发"
}
},
"zh-Hant-TW" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "等待離開"
}
}
}
},
"We use your location to show you nearby transit options." : {
"localizations" : {
"es" : {
Expand Down
72 changes: 44 additions & 28 deletions iosApp/iosApp/Pages/StopDetails/TripDetailsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,35 @@ struct TripDetailsView: View {
self.analytics = analytics
}

func getParentFor(_ stopId: String?, stops: [String: Stop]) -> Stop? {
if let stopId { stops[stopId]?.resolveParent(stops: stops) } else { nil }
}

var body: some View {
content
.task { stopDetailsVM.handleTripFilterChange(tripFilter) }
.onDisappear {
if stopDetailsVM.tripData?.tripFilter == tripFilter {
stopDetailsVM.clearTripDetails()
}
clearMapVehicle()
}
.onChange(of: tripFilter) { nextTripFilter in stopDetailsVM.handleTripFilterChange(nextTripFilter) }
.onChange(of: stopDetailsVM.tripData?.vehicle) { vehicle in mapVM.selectedVehicle = vehicle }
.onReceive(inspection.notice) { inspection.visit(self, $0) }
.withScenePhaseHandlers(
onActive: {
stopDetailsVM.returnFromBackground()
if let tripFilter {
stopDetailsVM.joinTripChannels(tripFilter: tripFilter)
}
},
onInactive: stopDetailsVM.leaveTripChannels,
onBackground: stopDetailsVM.leaveTripChannels
)
}

@ViewBuilder private var content: some View {
VStack(spacing: 16) {
if nearbyVM.showDebugMessages {
DebugView {
Expand All @@ -71,50 +99,33 @@ struct TripDetailsView: View {
alertsData: nearbyVM.alerts,
globalData: global
) {
let vehicleStop: Stop? = if let stopId = vehicle.stopId, let allStops = stopDetailsVM.global?.stops {
allStops[stopId]?.resolveParent(stops: allStops)
} else {
nil
}
let vehicleStop: Stop? = getParentFor(vehicle.stopId, stops: global.stops)

let terminalStop: Stop? = getParentFor(tripData.trip.stopIds?.first, stops: global.stops)
let atTerminal = terminalStop != nil && terminalStop?.id == vehicleStop?.id
&& vehicle.currentStatus == .stoppedAt
let terminalEntry = atTerminal ? stops.terminalStop : nil

let routeAccents = stopDetailsVM.getTripRouteAccents()
tripDetails(tripFilter.tripId, stops, vehicle, vehicleStop, routeAccents)
tripDetails(tripFilter.tripId, stops, vehicle, vehicleStop, terminalEntry, routeAccents)
} else {
loadingBody()
}
}
.padding(.horizontal, 6)
.task { stopDetailsVM.handleTripFilterChange(tripFilter) }
.onDisappear {
if stopDetailsVM.tripData?.tripFilter == tripFilter {
stopDetailsVM.clearTripDetails()
}
clearMapVehicle()
}
.onChange(of: tripFilter) { nextTripFilter in stopDetailsVM.handleTripFilterChange(nextTripFilter) }
.onChange(of: stopDetailsVM.tripData?.vehicle) { vehicle in mapVM.selectedVehicle = vehicle }
.onReceive(inspection.notice) { inspection.visit(self, $0) }
.withScenePhaseHandlers(
onActive: {
stopDetailsVM.returnFromBackground()
if let tripFilter {
stopDetailsVM.joinTripChannels(tripFilter: tripFilter)
}
},
onInactive: stopDetailsVM.leaveTripChannels,
onBackground: stopDetailsVM.leaveTripChannels
)
}

@ViewBuilder private func tripDetails(
_ tripId: String,
_ stops: TripDetailsStopList,
_ vehicle: Vehicle?,
_ vehicleStop: Stop?,
_ terminalEntry: TripDetailsStopList.Entry?,
_ routeAccents: TripRouteAccents
) -> some View {
let vehicleShown = vehicle != nil && vehicleStop != nil
VStack(spacing: 0) {
vehicleCardView(vehicle, vehicleStop, tripId, routeAccents).zIndex(1)
vehicleCardView(vehicle, vehicleStop, tripId, terminalEntry, routeAccents).zIndex(1)
TripStops(
targetId: stopId,
stops: stops,
Expand All @@ -134,14 +145,18 @@ struct TripDetailsView: View {
_ vehicle: Vehicle?,
_ vehicleStop: Stop?,
_ tripId: String,
_ terminalEntry: TripDetailsStopList.Entry?,
_ routeAccents: TripRouteAccents
) -> some View {
if let vehicle, let vehicleStop {
TripVehicleCard(
vehicle: vehicle,
stop: vehicleStop,
tripId: tripId,
routeAccents: routeAccents
targetId: stopId,
terminalEntry: terminalEntry,
routeAccents: routeAccents,
now: now
)
}
}
Expand All @@ -153,6 +168,7 @@ struct TripDetailsView: View {
placeholderInfo.stops,
placeholderInfo.vehicle,
placeholderInfo.vehicleStop,
nil,
TripRouteAccents()
).loadingPlaceholder()
}
Expand Down
104 changes: 77 additions & 27 deletions iosApp/iosApp/Pages/StopDetails/TripVehicleCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ struct TripVehicleCard: View {
let vehicle: Vehicle
let stop: Stop
let tripId: String
let targetId: String
let terminalEntry: TripDetailsStopList.Entry?
let routeAccents: TripRouteAccents
let now: Date

var body: some View {
ZStack(alignment: .leading) {
Expand All @@ -31,7 +34,7 @@ struct TripVehicleCard: View {
liveIndicator
}
.frame(maxWidth: .infinity, minHeight: 56, alignment: .leading)
.padding(.vertical, 12)
.padding(.vertical, 8)
.padding(.leading, 30)
.padding(.trailing, 16)
}
Expand Down Expand Up @@ -92,7 +95,13 @@ struct TripVehicleCard: View {
"Next stop",
comment: "Label for a vehicle's next stop. For example: Next stop Alewife"
)
case .stoppedAt: NSLocalizedString(
case .stoppedAt: terminalEntry != nil ? NSLocalizedString(
"Waiting to depart",
comment: """
Label for a vehicle stopped at a terminal station waiting to start a trip.
For example: Waiting to depart Alewife
"""
) : NSLocalizedString(
"Now at",
comment: "Label for a where a vehicle is currently stopped. For example: Now at Alewife"
)
Expand All @@ -114,28 +123,59 @@ struct TripVehicleCard: View {
.rotationEffect(.degrees(225))
routeIcon(routeAccents.type)
.resizable()
.frame(width: 27, height: 27)
.frame(width: 27.5, height: 27.5)
.foregroundColor(routeAccents.textColor)
.overlay {
if targetId == stop.id, vehicle.currentStatus == .stoppedAt {
Image(.stopPinIndicator)
.resizable()
.scaledToFit()
.frame(width: 20, height: 26)
.padding(.bottom, 36)
}
}
}
.accessibilityHidden(true)
.padding([.bottom], 6)
}

var liveIndicator: some View {
HStack {
Image(.liveData)
.resizable()
.frame(width: 16, height: 16)
Text("Live", comment: "Indicates that data is being updated in real-time")
.font(Typography.footnote)
VStack {
HStack {
Image(.liveData)
.resizable()
.frame(width: 16, height: 16)
Text("Live", comment: "Indicates that data is being updated in real-time")
.font(Typography.footnote)
}
.opacity(0.6)
.accessibilityElement()
.accessibilityAddTraits(.isHeader)
.accessibilityLabel(Text(
"Real-time arrivals updating live",
comment: "VoiceOver label for real-time indicator icon"
))
if let upcomingTripViewState {
UpcomingTripView(
prediction: upcomingTripViewState,
routeType: routeAccents.type,
hideRealtimeIndicators: true
).foregroundStyle(Color.text).opacity(0.6)
}
}
}

var upcomingTripViewState: UpcomingTripView.State? {
guard let terminalEntry else { return nil }
if let alert = terminalEntry.alert {
return .noService(alert.effect)
} else {
let formatted = terminalEntry.format(now: now.toKotlinInstant(), routeType: routeAccents.type)
return switch onEnum(of: formatted) {
case .hidden, .skipped: nil
default: .some(formatted)
}
}
.opacity(0.6)
.accessibilityElement()
.accessibilityAddTraits(.isHeader)
.accessibilityLabel(Text(
"Real-time arrivals updating live",
comment: "VoiceOver label for real-time indicator icon"
))
}
}

Expand All @@ -153,23 +193,33 @@ struct TripVehicleCard_Previews: PreviewProvider {
trip.id = "1234"
trip.headsign = "Alewife"
}
let vehicle = Vehicle(id: "y1234", bearing: nil,
currentStatus: __Bridge__Vehicle_CurrentStatus.inTransitTo,
currentStopSequence: 30,
directionId: 1,
latitude: 0.0,
longitude: 0.0,
updatedAt: Date.now.addingTimeInterval(-10).toKotlinInstant(),
routeId: "66",
stopId: "place-davis",
tripId: trip.id)
let vehicle = Vehicle(
id: "y1234", bearing: nil,
currentStatus: __Bridge__Vehicle_CurrentStatus.inTransitTo,
currentStopSequence: 30,
directionId: 1,
latitude: 0.0,
longitude: 0.0,
updatedAt: Date.now.addingTimeInterval(-10).toKotlinInstant(),
routeId: "66",
stopId: "place-davis",
tripId: trip.id
)

let stop = objects.stop { stop in
stop.name = "Davis"
}

List {
TripVehicleCard(vehicle: vehicle, stop: stop, tripId: trip.id, routeAccents: TripRouteAccents(route: red))
TripVehicleCard(
vehicle: vehicle,
stop: stop,
tripId: trip.id,
targetId: "",
terminalEntry: nil,
routeAccents: TripRouteAccents(route: red),
now: Date.now
)
}
.previewDisplayName("VehicleCard")
}
Expand Down
3 changes: 2 additions & 1 deletion iosApp/iosApp/ViewModels/StopDetailsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ class StopDetailsViewModel: ObservableObject {
leaveVehicle()
guard let vehicleId = tripFilter.vehicleId else {
// If the filter has a null vehicle ID, we can't join anything, clear the vehicle and return
tripData?.vehicle = nil
Task { @MainActor in tripData?.vehicle = nil }
return
}
let errorKey = "TripDetailsPage.joinVehicle"
Expand Down Expand Up @@ -347,6 +347,7 @@ class StopDetailsViewModel: ObservableObject {
}
}

@MainActor
func returnFromBackground() {
if let predictionsByStop,
predictionsRepository
Expand Down
Loading

0 comments on commit 39e59d0

Please sign in to comment.