refactor: migrate Base64Image off java.util.Base64

Phase 2 of the iOS plan — first of ~9 small migrations to clear the
java.* imports out of commons/commonMain. Replaces java.util.Base64
with kotlin.io.encoding.Base64 (stdlib, KMP-clean). The two callers
(Android Base64Fetcher, Desktop DesktopBase64Fetcher) use the public
parse() signature only, which is unchanged.

Also documents the full Phase 2 audit in
amethyst/plans/2026-05-24-ios-support.md: out of 335 commonMain files,
21 are real iOS blockers grouped into ~10 small mergeable PRs. The
remaining 183 androidx.compose users and 7 androidx.lifecycle users
already map to JetBrains Compose Multiplatform / AndroidX KMP and need
no work.
This commit is contained in:
Claude
2026-05-24 18:07:25 +00:00
parent b27fc34786
commit 78ef4fa672
2 changed files with 51 additions and 5 deletions
+46 -1
View File
@@ -1,7 +1,7 @@
# iOS Support for Amethyst
**Date:** 2026-05-24
**Status:** Draft — not yet started
**Status:** Phase 1 complete; Phase 2 in flight
**Owner:** TBD
Plan to incrementally bring Amethyst to iOS by extending the existing
@@ -166,6 +166,51 @@ shared ViewModel and state class.
Kotlin/Native by default — code that explicitly references it needs a
`KmpDispatchers.IO` shim. Audit before Phase 2 starts.
### Phase 2 audit (2026-05-24): commons/commonMain iOS-blocker inventory
Audit of all 335 .kt files in `commons/src/commonMain/`. Better than feared
— most files are already KMP-clean. The actual blockers are 21 files
across ~6 distinct concerns. Each row below is a small mergeable PR.
**By blocker category:**
| Blocker | Files | Fix |
|---|---|---|
| `java.util.Base64` | 1 (`Base64Image.kt`) | `kotlin.io.encoding.Base64` (stdlib since 1.8) |
| `AtomicLong` / `AtomicInteger` | 2 (`ChessLobbyState.kt`, `SigningState.kt`) | `kotlinx.atomicfu.atomic` |
| `ConcurrentHashMap` | 4 (`ChessRelayFetchHelper.kt`, `ChessEventCollector.kt`, `ComposeSubscriptionManager.kt`, `MutableComposeSubscriptionManager.kt`) | `androidx.collection.MutableScatterMap` (KMP) — synchronization most likely already provided by enclosing scope; audit per file |
| `SortedSet` + `ConcurrentSkipListSet` | 2 (`EventListMatchingFilter.kt`, `NoteListMatchingFilter.kt`) | Switch to `mutableListOf` + sort-on-access, or `androidx.collection.MutableScatterSet` with manual order |
| `WeakReference` | 5 (`Channel.kt`, `Chatroom.kt`, `MarmotGroupChatroom.kt`, `UserRelaysCache.kt`, **+1**) | `expect class KmpWeakReference<T>` actuals: JVM `java.lang.ref.WeakReference`; iOS `kotlin.native.ref.WeakReference` |
| `BigDecimal` | 1 (`Note.kt`) | Either KMP bignum lib (`com.ionspin:bignum`) or move the BigDecimal-using helper to `jvmAndroid` and stub on iOS |
| `java.io.File` | 1 (`MediaContentModels.kt`) | Replace with `String` path, or `okio.Path` |
| `java.net.URI` / `MalformedURLException` | 2 (`RichTextParser.kt`, `UrlInfoItem.kt`) | KMP URL lib (`io.ktor:ktor-http`) or stay JVM via expect/actual `parseUrl()` |
| `java.nio.charset.Charset` | 1 (`HtmlCharsetParser.kt`) | `kotlin.text.Charsets` for UTF-8/16; for arbitrary charsets, expect/actual |
| `:nestsClient` project dep | 2 (`NestViewModel.kt`, `ActiveSubscription.kt`) | Move both files to `jvmAndroid` source set (audio rooms are Phase 5 anyway) |
| `com.halilibo.richtext.*` | 1 (`RenderMarkdown.kt`) | Verify iOS artifact; if missing, move to `jvmAndroid` until Phase 3 markdown decision |
**Files that look scary but aren't:**
- 183 files import `androidx.compose.*` — these all map to JetBrains Compose
Multiplatform's iOS artifacts (identical package paths). No work needed.
- 7 files import `androidx.lifecycle.*` — KMP since 2.8.0. No work needed.
- 0 files import `coil3.network.okhttp` (Coil network is already isolated).
- 0 files import `javax.*` or `android.*` directly from commonMain.
**Recommended PR order** (ascending cost, descending obviousness):
1. ✅ Phase 1 complete (gates + iOS CI for quartz, Jackson migration).
2. **Base64** (1 file, ~2 LOC change). Demonstrates the pattern.
3. **Atomics** (2 files, atomicfu plugin + ~10 LOC).
4. **ConcurrentHashMap** (4 files; needs concurrency audit per file).
5. **WeakReference** (5 files + 1 new expect/actual).
6. **`:nestsClient` files → jvmAndroid** (2 files; pure source-set move).
7. **`RenderMarkdown.kt` → jvmAndroid OR iOS verification** (1 file; depends on lib check).
8. **URL parsing** (2 files; either ktor-http dep or expect/actual).
9. **Charsets, BigDecimal, File** (3 files; small per-file decisions).
10. After ~9 lands: add iOS targets to `:commons`, expect failures to be down
to ~zero, run `compileKotlinIosSimulatorArm64` to confirm.
11. Then proceed with the original Phase 2 plan items (Ktor for HTTP,
SecureKeyStore expect/actual, etc.) for the cross-cutting deps.
---
## Phase 3 — Compose Multiplatform UI on iOS
@@ -20,19 +20,20 @@
*/
package com.vitorpamplona.amethyst.commons.richtext
import java.util.Base64
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
object Base64Image {
val pattern = Patterns.BASE64_IMAGE
fun isBase64(content: String): Boolean = Patterns.BASE64_IMAGE.matches(content)
@OptIn(ExperimentalEncodingApi::class)
fun parse(content: String): ByteArray {
val matcher = pattern.find(content)
if (matcher != null) {
val base64String = matcher.groups[2]?.value
val byteArray = Base64.getDecoder().decode(base64String)
return byteArray
val base64String = matcher.groups[2]?.value ?: ""
return Base64.decode(base64String)
}
throw Exception("Unable to convert base64 to image $content")