diff --git a/app/src/main/java/uk/ewancroft/inkwell/data/remote/AtProtoApi.kt b/app/src/main/java/uk/ewancroft/inkwell/data/remote/AtProtoApi.kt index 43eb85e..79c9668 100644 --- a/app/src/main/java/uk/ewancroft/inkwell/data/remote/AtProtoApi.kt +++ b/app/src/main/java/uk/ewancroft/inkwell/data/remote/AtProtoApi.kt @@ -8,12 +8,15 @@ import okhttp3.* import okhttp3.MediaType.Companion.toMediaType import okhttp3.logging.HttpLoggingInterceptor import java.util.concurrent.TimeUnit +import uk.ewancroft.inkwell.data.model.AtUri as AtUriModel +import uk.ewancroft.inkwell.data.model.BlueskyProfile +import uk.ewancroft.inkwell.data.model.StrongRef /** * Low-level AT Protocol HTTP client. * * Handles authenticated (OAuth DPoP) and unauthenticated requests to PDS - * servers and the public Bluesky API. Mirror's Inkwell iOS LoginStateManager's + * servers and the public Bluesky API. Mirrors Inkwell iOS LoginStateManager's * authenticatedData/unauthenticatedData methods. */ class AtProtoClient( @@ -37,19 +40,16 @@ class AtProtoClient( collection: String, limit: Int = 25, cursor: String? = null - ): ListRecordsResponse { - val base = if (did == currentDid()) pdsUrl else resolvePdsUrl(did) + ): JsonObject { + val base = if (did == currentDid) pdsUrl else resolvePdsUrl(did) + ?: "https://public.api.bsky.app" val url = "$base/xrpc/com.atproto.repo.listRecords".toHttpUrl { addQueryParameter("repo", did) addQueryParameter("collection", collection) addQueryParameter("limit", limit.toString()) cursor?.let { addQueryParameter("cursor", it) } } - - val request = Request.Builder().url(url).get() - .addAuthIfOwn(did) - .build() - + val request = Request.Builder().url(url).get().build() val response = client.newCall(request).execute() return json.decodeFromString(response.body!!.string()) } @@ -65,28 +65,26 @@ class AtProtoClient( suspend fun resolveHandle(handle: String): String { val url = "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=$handle" val response = client.newCall(Request.Builder().url(url).get().build()).execute() - val body = json.decodeFromString(response.body!!.string()) - return body["did"]!!.toString().trim('"') + val body: JsonObject = json.decodeFromString(response.body!!.string()) + return body["did"]!!.jsonPrimitive.content } /** Creates a record in the user's repository (authenticated). */ suspend fun createRecord( collection: String, - record: kotlinx.serialization.json.JsonObject, + record: JsonObject, repo: String ): StrongRef { val base = pdsUrl ?: throw IllegalStateException("Not authenticated") val url = "$base/xrpc/com.atproto.repo.createRecord" - val body = json.encodeToString(kotlinx.serialization.json.JsonObject::class.java, buildJsonObject { + val body = json.encodeToString(JsonObject.serializer(), buildJsonObject { put("repo", repo) put("collection", collection) put("record", record) }) - val request = Request.Builder().url(url).post(body.toRequestBody(jsonMediaType)) .header("Authorization", "Bearer $bearerToken") .build() - val response = client.newCall(request).execute() return json.decodeFromString(response.body!!.string()) } @@ -95,61 +93,39 @@ class AtProtoClient( suspend fun deleteRecord(collection: String, rkey: String, repo: String) { val base = pdsUrl ?: throw IllegalStateException("Not authenticated") val url = "$base/xrpc/com.atproto.repo.deleteRecord" - val body = json.encodeToString(kotlinx.serialization.json.JsonObject::class.java, buildJsonObject { + val body = json.encodeToString(JsonObject.serializer(), buildJsonObject { put("repo", repo) put("collection", collection) put("rkey", rkey) }) - val request = Request.Builder().url(url).post(body.toRequestBody(jsonMediaType)) .header("Authorization", "Bearer $bearerToken") .build() - client.newCall(request).execute().close() } /** Fetches a single record by AT-URI. */ - suspend fun getRecord(uri: String): RecordEntry { - val parsed = requireNotNull(AtUri.parse(uri)) - val base = if (parsed.did == currentDid()) pdsUrl else resolvePdsUrl(parsed.did) + suspend fun getRecord(uri: String): JsonObject { + val parsed = requireNotNull(AtUriModel.parse(uri)) + val base = if (parsed.did == currentDid) pdsUrl else resolvePdsUrl(parsed.did) + ?: "https://public.api.bsky.app" val url = "$base/xrpc/com.atproto.repo.getRecord".toHttpUrl { addQueryParameter("repo", parsed.did) addQueryParameter("collection", parsed.collection) addQueryParameter("rkey", parsed.recordKey) } - - val request = Request.Builder().url(url).get() - .addAuthIfOwn(parsed.did) - .build() - + val request = Request.Builder().url(url).get().build() val response = client.newCall(request).execute() return json.decodeFromString(response.body!!.string()) } - private fun currentDid(): String? = null // Set by session state - private fun resolvePdsUrl(did: String): String { - // In production: resolve via plc.directory. For now, use public API domain. - return "https://public.api.bsky.app" - } + private val currentDid: String? = null // Set by session state - private fun Request.Builder.addAuthIfOwn(did: String): Request.Builder { - if (did == currentDid() && bearerToken != null) { - header("Authorization", "Bearer $bearerToken") - } - return this - } + private fun resolvePdsUrl(did: String): String? = null // resolve via plc.directory } private fun String.toHttpUrl(block: HttpUrl.Builder.() -> Unit): HttpUrl { - val builder = requireNotNull(HttpUrl.parse(this)) { "Invalid base URL: $this" } - .newBuilder() + val builder = requireNotNull(HttpUrl.parse(this)) { "Invalid base URL: $this" }.newBuilder() builder.block() return builder.build() } - -// Re-exports -typealias BlueskyProfile = uk.ewancroft.inkwell.data.model.BlueskyProfile -typealias ListRecordsResponse = uk.ewancroft.inkwell.data.model.ListRecordsResponse -typealias RecordEntry = uk.ewancroft.inkwell.data.model.RecordEntry -typealias StrongRef = uk.ewancroft.inkwell.data.model.StrongRef -typealias AtUri = uk.ewancroft.inkwell.data.model.AtUri diff --git a/app/src/main/java/uk/ewancroft/inkwell/ui/reader/ReaderScreen.kt b/app/src/main/java/uk/ewancroft/inkwell/ui/reader/ReaderScreen.kt index bbb015e..6557306 100644 --- a/app/src/main/java/uk/ewancroft/inkwell/ui/reader/ReaderScreen.kt +++ b/app/src/main/java/uk/ewancroft/inkwell/ui/reader/ReaderScreen.kt @@ -24,7 +24,7 @@ import androidx.compose.material.icons.outlined.* @OptIn(ExperimentalMaterial3Api::class) @Composable fun ReaderScreen() { - var selectedTab by remember { mutableIntStateOf(0) } + var selectedTab by remember { mutableStateOf(0) } val tabs = listOf("Following", "Yours") Column { diff --git a/app/src/main/java/uk/ewancroft/inkwell/ui/theme/Theme.kt b/app/src/main/java/uk/ewancroft/inkwell/ui/theme/Theme.kt index b0f6fc9..d7bdfde 100644 --- a/app/src/main/java/uk/ewancroft/inkwell/ui/theme/Theme.kt +++ b/app/src/main/java/uk/ewancroft/inkwell/ui/theme/Theme.kt @@ -107,7 +107,12 @@ fun InkwellTheme( } private fun uk.ewancroft.inkwell.data.model.ColorValue.toColor(): Color = - Color(red = r, green = g, blue = b, alpha = a?.coerceIn(0, 100)?.div(100f) ?: 1f) + Color( + red = r / 255f, + green = g / 255f, + blue = b / 255f, + alpha = (a ?: 100) / 100f + ) private fun RgbColor.toColor(): Color = - Color(red = r, green = g, blue = b) + Color(red = r / 255f, green = g / 255f, blue = b / 255f)