diff --git a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/MainActivity.kt b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/MainActivity.kt index 93b906413..304a36753 100644 --- a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/MainActivity.kt @@ -49,11 +49,6 @@ class MainActivity : ComponentActivity() { // cheap and we only block once per Activity creation (including // the post-language-swap recreate() path below). Without this, // recreate() would briefly flash the old locale before settling. - // - // The 2s timeout + catch-all is defence against a stalled or - // corrupted DataStore: we'd rather boot in system language than - // leave the Activity stuck before super.onCreate(), which would - // hang the whole app with no visible error. runBlocking { val tag = try { diff --git a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt index e9fc4acd2..0c4e6a917 100644 --- a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt +++ b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/DesktopApp.kt @@ -51,10 +51,6 @@ fun main(args: Array) { // language swaps surface as a "restart required" snackbar from the // Tweaks screen; this block just covers the cold-start path so // users see their chosen language immediately on next launch. - // - // Timeout guards against a stalled DataStore read blocking window - // creation and deep-link dispatch — we fall back to system language - // rather than hang the launch. runBlocking { val koin = GlobalContext.get() val tweaksRepo = koin.get() diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt index 1f27ec388..80b6d8414 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt @@ -155,6 +155,7 @@ val coreModule = get() BackendApiClient( proxyConfigFlow = ProxyManager.configFlow(ProxyScope.DISCOVERY), + tokenStore = get(), ) } // NOTE: the reviewer asked for a Koin onClose hook to call diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt index 838d59ea5..c5a908a3d 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt @@ -6,6 +6,7 @@ import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest import io.ktor.client.request.get +import io.ktor.client.request.header import io.ktor.client.request.parameter import io.ktor.http.isSuccess import io.ktor.serialization.kotlinx.json.json @@ -27,6 +28,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import zed.rainxch.core.data.data_source.TokenStore import zed.rainxch.core.data.dto.BackendExploreResponse import zed.rainxch.core.data.dto.BackendRepoResponse import zed.rainxch.core.data.dto.BackendSearchResponse @@ -42,6 +44,7 @@ import kotlin.coroutines.cancellation.CancellationException */ class BackendApiClient( proxyConfigFlow: StateFlow, + private val tokenStore: TokenStore, ) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val mutex = Mutex() @@ -110,12 +113,14 @@ class BackendApiClient( offset: Int = 0, ): Result = safeCall { + val token = currentUserGithubToken() val response = httpClient.get("search") { parameter("q", query) if (platform != null) parameter("platform", platform) if (sort != null) parameter("sort", sort) parameter("limit", limit) parameter("offset", offset) + if (token != null) header(X_GITHUB_TOKEN_HEADER, token) } if (response.status.isSuccess()) { Result.success(response.body()) @@ -130,11 +135,13 @@ class BackendApiClient( page: Int = 1, ): Result = safeCall { + val token = currentUserGithubToken() val response = httpClient.get("search/explore") { parameter("q", query) if (platform != null) parameter("platform", platform) parameter("page", page) timeout { requestTimeoutMillis = 20_000 } + if (token != null) header(X_GITHUB_TOKEN_HEADER, token) } if (response.status.isSuccess()) { Result.success(response.body()) @@ -143,6 +150,14 @@ class BackendApiClient( } } + private suspend fun currentUserGithubToken(): String? = + try { + tokenStore.currentToken()?.accessToken?.trim()?.takeIf { it.isNotEmpty() } + } catch (e: CancellationException) { + throw e + } catch (_: Exception) { + null + } suspend fun getRepo(owner: String, name: String): Result = safeCall { val response = httpClient.get("repo/$owner/$name") @@ -180,6 +195,7 @@ class BackendApiClient( companion object { private const val BASE_URL = "https://api.github-store.org/v1/" + private const val X_GITHUB_TOKEN_HEADER = "X-GitHub-Token" } } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt index 36ad81b10..4d5aff5cf 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt @@ -214,10 +214,6 @@ class TweaksRepositoryImpl( override fun getAppLanguage(): Flow = preferences.data.map { prefs -> - // Treat blank *or* unknown tags as "unset" — guards against - // stale writes from older builds that shipped a language - // we no longer bundle resources for, which would otherwise - // pin the UI to an unresolvable locale. prefs[APP_LANGUAGE_KEY] ?.trim() ?.takeIf { it.isNotEmpty() && AppLanguages.containsTag(it) } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index e05ae7438..2d625542a 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -752,22 +752,9 @@ class TweaksViewModel( } is TweaksAction.OnAppLanguageSelected -> { - // Skip the write + restart prompt when the user re-picks - // the language that's already active — tapping the - // current option shouldn't look like a change on - // Desktop (would fire a spurious "restart to apply" - // snackbar) or churn DataStore on Android. if (action.tag == _state.value.selectedAppLanguage) return viewModelScope.launch { tweaksRepository.setAppLanguage(action.tag) - // Android: `MainActivity` is subscribed to the - // same preference flow and calls `recreate()` on - // change — no extra nudging needed. Desktop has - // no recreate-equivalent, so we surface a - // "restart to apply" prompt; the user's choice is - // already persisted and will take effect on the - // next launch (or they can restart now via the - // snackbar action). if (getPlatform() != Platform.ANDROID) { _events.send(TweaksEvent.OnAppLanguageChangeRequiresRestart) }