diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/MainApplication.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/MainApplication.kt index 5014909d82..8e7ed44802 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/MainApplication.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/MainApplication.kt @@ -26,9 +26,14 @@ import org.phoenixframework.Socket // expect val appVariant: AppVariant class MainApplication : Application() { - private val socket = Socket(appVariant.socketUrl, decode = ::decodeMessage) - override fun onCreate() { + val locale = super.applicationContext.resources.getString(R.string.current_locale) + val socket = + Socket( + appVariant.socketUrl, + params = mapOf("locale" to locale), + decode = ::decodeMessage, + ) super.onCreate() initKoin( appVariant, diff --git a/iosApp/iosApp/ProductionAppView.swift b/iosApp/iosApp/ProductionAppView.swift index 92f8d64463..01f52f322d 100644 --- a/iosApp/iosApp/ProductionAppView.swift +++ b/iosApp/iosApp/ProductionAppView.swift @@ -96,7 +96,8 @@ struct ProductionAppView: View { } private static func initSocket() -> PhoenixSocket { - let socket = Socket(appVariant.socketUrl) + let locale = NSLocalizedString("key/current_locale", comment: "") + let socket = Socket(appVariant.socketUrl, paramsClosure: { ["locale": locale] }) // decreasing default from 5s socket.reconnectAfter = { tries in diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/dependencyInjection/RepositoryDI.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/dependencyInjection/RepositoryDI.kt index 06104e5991..faaa323fcd 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/dependencyInjection/RepositoryDI.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/dependencyInjection/RepositoryDI.kt @@ -5,7 +5,7 @@ import com.mbta.tid.mbta_app.cache.MockKeyedCache import com.mbta.tid.mbta_app.cache.ScheduleCache import com.mbta.tid.mbta_app.model.AppVersion import com.mbta.tid.mbta_app.model.ObjectCollectionBuilder -import com.mbta.tid.mbta_app.model.response.AlertsStreamDataResponse +import com.mbta.tid.mbta_app.model.response.AlertsStreamUpdateResponse import com.mbta.tid.mbta_app.model.response.ApiResult import com.mbta.tid.mbta_app.model.response.GlobalResponse import com.mbta.tid.mbta_app.model.response.NearbyResponse @@ -232,7 +232,7 @@ public class MockRepositories : IRepositories { selectedTripId: String? = null, selectedVehicleId: String? = null, ) { - alerts = MockAlertsRepository(AlertsStreamDataResponse(objects)) + alerts = MockAlertsRepository(AlertsStreamUpdateResponse(objects)) global = MockGlobalRepository(GlobalResponse(objects)) nearby = MockNearbyRepository(NearbyResponse(objects)) predictions = diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/Alert.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/Alert.kt index 290852fe41..62425cad3c 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/Alert.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/Alert.kt @@ -27,6 +27,7 @@ internal constructor( @SerialName("informed_entity") val informedEntity: List, val lifecycle: Lifecycle, val severity: Int, + val summaries: List? = null, @SerialName("updated_at") val updatedAt: EasternTimeInstant, // This field is not parsed from the Alert object from the backend, it is injected from // global data in the AlertsUsecase if any informed entities apply to a facility. diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/AlertSummaryEntity.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/AlertSummaryEntity.kt new file mode 100644 index 0000000000..7dea885f73 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/AlertSummaryEntity.kt @@ -0,0 +1,14 @@ +package com.mbta.tid.mbta_app.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class AlertSummaryEntity( + @SerialName("alert_id") val alertId: String, + @SerialName("route_id") val routeId: String?, + @SerialName("stop_id") val stopId: String?, + @SerialName("trip_id") val tripId: String?, + @SerialName("direction_id") val directionId: Int?, + val summary: String?, +) diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/ObjectCollectionBuilder.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/ObjectCollectionBuilder.kt index a49af033bd..bc8219ee42 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/ObjectCollectionBuilder.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/ObjectCollectionBuilder.kt @@ -104,6 +104,8 @@ private constructor( public var header: String? = null public var lifecycle: Alert.Lifecycle = Alert.Lifecycle.New public var severity: Int = 0 + + public var summaries: List? = null public var updatedAt: EasternTimeInstant = EasternTimeInstant(Instant.fromEpochMilliseconds(0)) public var facilities: Map? = null @@ -153,6 +155,7 @@ private constructor( informedEntity, lifecycle, severity, + summaries, updatedAt, facilities, ) diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/response/AlertsStreamDataResponse.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/response/AlertsStreamDataResponse.kt index a65581a3f4..fcdb86d6be 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/response/AlertsStreamDataResponse.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/response/AlertsStreamDataResponse.kt @@ -4,6 +4,21 @@ import com.mbta.tid.mbta_app.model.Alert import com.mbta.tid.mbta_app.model.ObjectCollectionBuilder import kotlinx.serialization.Serializable +@Serializable +public data class AlertsStreamUpdateResponse( + internal val remove: List, + internal val update: Map, +) { + public constructor(objects: ObjectCollectionBuilder) : this(emptyList(), objects.alerts.toMap()) + + public fun mergeInto(alertsData: AlertsStreamDataResponse?): AlertsStreamDataResponse = + alertsData?.let { + AlertsStreamDataResponse( + it.alerts.filter { (key, _) -> !remove.contains(key) } + update + ) + } ?: AlertsStreamDataResponse(update) +} + @Serializable public data class AlertsStreamDataResponse(internal val alerts: Map) { public constructor(objects: ObjectCollectionBuilder) : this(objects.alerts.toMap()) diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/phoenix/AlertsChannel.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/phoenix/AlertsChannel.kt index 88f061ff35..ebd832254b 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/phoenix/AlertsChannel.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/phoenix/AlertsChannel.kt @@ -1,17 +1,17 @@ package com.mbta.tid.mbta_app.phoenix import com.mbta.tid.mbta_app.json -import com.mbta.tid.mbta_app.model.response.AlertsStreamDataResponse +import com.mbta.tid.mbta_app.model.response.AlertsStreamUpdateResponse internal object AlertsChannel : ChannelSpec { - override val topic = "alerts:v2" + override val topic = "alerts:v3" override val updateEvent = "stream_data" override val params = emptyMap() @Throws(IllegalArgumentException::class) - fun parseMessage(payload: String): AlertsStreamDataResponse { + fun parseMessage(payload: String): AlertsStreamUpdateResponse { return json.decodeFromString(payload) } } diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/AlertsRepository.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/AlertsRepository.kt index 474b1b7bd0..2d64cb50a5 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/AlertsRepository.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/AlertsRepository.kt @@ -1,7 +1,7 @@ package com.mbta.tid.mbta_app.repositories import co.touchlab.skie.configuration.annotations.DefaultArgumentInterop -import com.mbta.tid.mbta_app.model.response.AlertsStreamDataResponse +import com.mbta.tid.mbta_app.model.response.AlertsStreamUpdateResponse import com.mbta.tid.mbta_app.model.response.ApiResult import com.mbta.tid.mbta_app.network.PhoenixChannel import com.mbta.tid.mbta_app.network.PhoenixSocket @@ -11,7 +11,7 @@ import kotlinx.coroutines.CoroutineDispatcher import org.koin.core.component.KoinComponent public interface IAlertsRepository { - public fun connect(onReceive: (ApiResult) -> Unit) + public fun connect(onReceive: (ApiResult) -> Unit) public fun disconnect() } @@ -23,7 +23,7 @@ internal class AlertsRepository( ioDispatcher: CoroutineDispatcher, ) : IAlertsRepository, KoinComponent { private val channelOwner = - ChannelOwner( + ChannelOwner( socket, ioDispatcher, debugRepository, @@ -31,13 +31,13 @@ internal class AlertsRepository( ) internal var channel: PhoenixChannel? by channelOwner::channel - override fun connect(onReceive: (ApiResult) -> Unit) { + override fun connect(onReceive: (ApiResult) -> Unit) { channelOwner.connect( AlertsChannel, AlertsChannel::parseMessage, { when (it) { - is ApiResult.Ok -> println("Received ${it.data.alerts.size} alerts") + is ApiResult.Ok -> println("Received ${it.data.update.size} alerts") else -> {} } onReceive(it) @@ -54,20 +54,20 @@ internal class AlertsRepository( public class MockAlertsRepository @DefaultArgumentInterop.Enabled internal constructor( - private val result: ApiResult, + private val result: ApiResult, private val onConnect: () -> Unit = {}, private val onDisconnect: () -> Unit = {}, ) : IAlertsRepository { @DefaultArgumentInterop.Enabled public constructor( - response: AlertsStreamDataResponse = AlertsStreamDataResponse(emptyMap()), + response: AlertsStreamUpdateResponse = AlertsStreamUpdateResponse(emptyList(), emptyMap()), onConnect: () -> Unit = {}, onDisconnect: () -> Unit = {}, ) : this(ApiResult.Ok(response), onConnect, onDisconnect) - private var receiveCallback: ((ApiResult) -> Unit)? = null + private var receiveCallback: ((ApiResult) -> Unit)? = null - override fun connect(onReceive: (ApiResult) -> Unit) { + override fun connect(onReceive: (ApiResult) -> Unit) { receiveCallback = onReceive onConnect() onReceive(result) @@ -77,7 +77,7 @@ internal constructor( onDisconnect() } - internal fun receiveResult(result: ApiResult) { + internal fun receiveResult(result: ApiResult) { receiveCallback?.let { it(result) } } } diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/usecases/AlertsUsecase.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/usecases/AlertsUsecase.kt index d8e81b7266..12908636a4 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/usecases/AlertsUsecase.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/usecases/AlertsUsecase.kt @@ -2,6 +2,7 @@ package com.mbta.tid.mbta_app.usecases import co.touchlab.skie.configuration.annotations.DefaultArgumentInterop import com.mbta.tid.mbta_app.model.response.AlertsStreamDataResponse +import com.mbta.tid.mbta_app.model.response.AlertsStreamUpdateResponse import com.mbta.tid.mbta_app.model.response.ApiResult import com.mbta.tid.mbta_app.repositories.IAlertsRepository import com.mbta.tid.mbta_app.repositories.IGlobalRepository @@ -21,17 +22,22 @@ constructor( private val globalUpdateDispatcher: CoroutineDispatcher = Dispatchers.IO, ) : KoinComponent { - private var lastOkResult: ApiResult.Ok? = null + private var currentAlerts: AlertsStreamDataResponse? = null + private var globalState = globalRepository.state private var globalUpdateJob: Job? = null public fun connect(onReceive: (ApiResult) -> Unit) { - fun injectAndReceive(result: ApiResult) { + fun injectAndReceive(result: ApiResult) { val injectedResult = - if (result is ApiResult.Ok) { - lastOkResult = result - result.copy(result.data.injectFacilities(globalState.value)) - } else result + when (result) { + is ApiResult.Ok -> + ApiResult.Ok( + result.data.mergeInto(currentAlerts).injectFacilities(globalState.value) + ) + + is ApiResult.Error -> ApiResult.Error(result.code, result.message) + } onReceive(injectedResult) } alertsRepository.connect(::injectAndReceive) @@ -40,8 +46,8 @@ constructor( globalUpdateJob = CoroutineScope(globalUpdateDispatcher).launch { globalState.collect { global -> - lastOkResult?.let { result -> - onReceive(result.copy(result.data.injectFacilities(global))) + currentAlerts?.let { alerts -> + onReceive(ApiResult.Ok(alerts.injectFacilities(global))) } } }