Skip to content

Commit 0a0d9cc

Browse files
ovitrifclaude
andcommitted
refactor: migrate pubky images to coil
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 619cd8a commit 0a0d9cc

File tree

8 files changed

+153
-255
lines changed

8 files changed

+153
-255
lines changed

app/src/main/java/to/bitkit/data/PubkyImageCache.kt

Lines changed: 0 additions & 58 deletions
This file was deleted.

app/src/main/java/to/bitkit/data/PubkyFetcher.kt renamed to app/src/main/java/to/bitkit/data/PubkyImageFetcher.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import org.json.JSONObject
1212
import to.bitkit.services.PubkyService
1313
import to.bitkit.utils.Logger
1414

15-
private const val TAG = "PubkyFetcher"
15+
private const val TAG = "PubkyImageFetcher"
1616
private const val PUBKY_SCHEME = "pubky://"
1717

18-
class PubkyFetcher(
18+
class PubkyImageFetcher(
1919
private val uri: String,
2020
private val options: Options,
2121
private val pubkyService: PubkyService,
@@ -42,7 +42,7 @@ class PubkyFetcher(
4242
class Factory(private val pubkyService: PubkyService) : Fetcher.Factory<String> {
4343
override fun create(data: String, options: Options, imageLoader: ImageLoader): Fetcher? {
4444
if (!data.startsWith(PUBKY_SCHEME)) return null
45-
return PubkyFetcher(data, options, pubkyService)
45+
return PubkyImageFetcher(data, options, pubkyService)
4646
}
4747
}
4848
}

app/src/main/java/to/bitkit/di/ImageModule.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import dagger.Provides
1010
import dagger.hilt.InstallIn
1111
import dagger.hilt.android.qualifiers.ApplicationContext
1212
import dagger.hilt.components.SingletonComponent
13-
import to.bitkit.data.PubkyFetcher
13+
import to.bitkit.data.PubkyImageFetcher
1414
import to.bitkit.services.PubkyService
1515
import javax.inject.Singleton
1616

@@ -24,7 +24,7 @@ object ImageModule {
2424
@ApplicationContext context: Context,
2525
pubkyService: PubkyService,
2626
): ImageLoader = ImageLoader.Builder(context)
27-
.components { add(PubkyFetcher.Factory(pubkyService)) }
27+
.components { add(PubkyImageFetcher.Factory(pubkyService)) }
2828
.memoryCache {
2929
MemoryCache.Builder()
3030
.maxSizePercent(context, percent = 0.15)

app/src/main/java/to/bitkit/repositories/PubkyRepo.kt

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package to.bitkit.repositories
22

3-
import android.graphics.Bitmap
3+
import coil3.ImageLoader
44
import kotlinx.coroutines.CoroutineDispatcher
55
import kotlinx.coroutines.CoroutineScope
66
import kotlinx.coroutines.SupervisorJob
@@ -18,8 +18,6 @@ import kotlinx.coroutines.flow.update
1818
import kotlinx.coroutines.launch
1919
import kotlinx.coroutines.sync.Mutex
2020
import kotlinx.coroutines.withContext
21-
import org.json.JSONObject
22-
import to.bitkit.data.PubkyImageCache
2321
import to.bitkit.data.PubkyStore
2422
import to.bitkit.data.keychain.Keychain
2523
import to.bitkit.di.IoDispatcher
@@ -36,7 +34,7 @@ class PubkyRepo @Inject constructor(
3634
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
3735
private val pubkyService: PubkyService,
3836
private val keychain: Keychain,
39-
private val imageCache: PubkyImageCache,
37+
private val imageLoader: ImageLoader,
4038
private val pubkyStore: PubkyStore,
4139
) {
4240
companion object {
@@ -251,37 +249,25 @@ class PubkyRepo @Inject constructor(
251249
withContext(ioDispatcher) { pubkyService.forceSignOut() }
252250
}.also {
253251
runCatching { withContext(ioDispatcher) { keychain.delete(Keychain.Key.PAYKIT_SESSION.name) } }
254-
runCatching { withContext(ioDispatcher) { imageCache.clear() } }
252+
evictPubkyImages()
255253
runCatching { withContext(ioDispatcher) { pubkyStore.reset() } }
256254
_publicKey.update { null }
257255
_profile.update { null }
258256
_contacts.update { emptyList() }
259257
_authState.update { PubkyAuthState.Idle }
260258
}
261259

262-
fun cachedImage(uri: String): Bitmap? = imageCache.memoryImage(uri)
263-
264-
suspend fun fetchImage(uri: String): Result<Bitmap> = runCatching {
265-
withContext(ioDispatcher) {
266-
imageCache.image(uri)?.let { return@withContext it }
267-
268-
val data = pubkyService.fetchFile(uri)
269-
val blobData = resolveImageData(data)
270-
imageCache.decodeAndStore(blobData, uri).getOrThrow()
260+
private fun evictPubkyImages() {
261+
imageLoader.memoryCache?.let { cache ->
262+
cache.keys.filter { it.key.startsWith(PUBKY_SCHEME) }.forEach { cache.remove(it) }
263+
}
264+
val imageUris = buildList {
265+
_profile.value?.imageUrl?.let { add(it) }
266+
addAll(_contacts.value.mapNotNull { it.imageUrl })
267+
}
268+
imageLoader.diskCache?.let { cache ->
269+
imageUris.forEach { cache.remove(it) }
271270
}
272-
}
273-
274-
private suspend fun resolveImageData(data: ByteArray): ByteArray {
275-
return runCatching {
276-
val json = JSONObject(String(data))
277-
val src = json.optString("src", "")
278-
if (src.isNotEmpty() && src.startsWith(PUBKY_SCHEME)) {
279-
Logger.debug("File descriptor found, fetching blob from: '$src'", context = TAG)
280-
pubkyService.fetchFile(src)
281-
} else {
282-
data
283-
}
284-
}.getOrDefault(data)
285271
}
286272

287273
private suspend fun cacheMetadata(profile: PubkyProfile) {
Lines changed: 28 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,59 @@
11
package to.bitkit.ui.components
22

3-
import android.graphics.Bitmap
4-
import androidx.compose.foundation.Image
53
import androidx.compose.foundation.background
64
import androidx.compose.foundation.layout.Box
75
import androidx.compose.foundation.layout.size
86
import androidx.compose.foundation.shape.CircleShape
97
import androidx.compose.material3.CircularProgressIndicator
108
import androidx.compose.material3.Icon
119
import androidx.compose.runtime.Composable
12-
import androidx.compose.runtime.LaunchedEffect
13-
import androidx.compose.runtime.getValue
1410
import androidx.compose.ui.Alignment
1511
import androidx.compose.ui.Modifier
1612
import androidx.compose.ui.draw.clip
17-
import androidx.compose.ui.graphics.asImageBitmap
1813
import androidx.compose.ui.layout.ContentScale
1914
import androidx.compose.ui.res.painterResource
2015
import androidx.compose.ui.unit.Dp
2116
import androidx.compose.ui.unit.dp
22-
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
23-
import androidx.lifecycle.ViewModel
24-
import androidx.lifecycle.compose.collectAsStateWithLifecycle
25-
import androidx.lifecycle.viewModelScope
26-
import dagger.hilt.android.lifecycle.HiltViewModel
27-
import kotlinx.coroutines.flow.MutableStateFlow
28-
import kotlinx.coroutines.flow.asStateFlow
29-
import kotlinx.coroutines.flow.update
30-
import kotlinx.coroutines.launch
17+
import coil3.compose.SubcomposeAsyncImage
3118
import to.bitkit.R
32-
import to.bitkit.repositories.PubkyRepo
3319
import to.bitkit.ui.theme.Colors
34-
import to.bitkit.utils.Logger
35-
import javax.inject.Inject
36-
37-
private const val TAG = "PubkyImage"
38-
39-
@HiltViewModel
40-
class PubkyImageViewModel @Inject constructor(
41-
private val pubkyRepo: PubkyRepo,
42-
) : ViewModel() {
43-
44-
private val _images = MutableStateFlow<Map<String, PubkyImageState>>(emptyMap())
45-
val images = _images.asStateFlow()
46-
47-
fun loadImage(uri: String) {
48-
val current = _images.value[uri]
49-
if (current is PubkyImageState.Loaded || current is PubkyImageState.Loading) return
50-
51-
val cached = pubkyRepo.cachedImage(uri)
52-
if (cached != null) {
53-
_images.update { it + (uri to PubkyImageState.Loaded(cached)) }
54-
return
55-
}
56-
57-
_images.update { it + (uri to PubkyImageState.Loading) }
58-
viewModelScope.launch {
59-
pubkyRepo.fetchImage(uri)
60-
.onSuccess { bitmap ->
61-
_images.update { it + (uri to PubkyImageState.Loaded(bitmap)) }
62-
}
63-
.onFailure {
64-
Logger.error("Failed to load pubky image '$uri'", it, context = TAG)
65-
_images.update { it + (uri to PubkyImageState.Failed) }
66-
}
67-
}
68-
}
69-
}
70-
71-
sealed interface PubkyImageState {
72-
data object Loading : PubkyImageState
73-
data class Loaded(val bitmap: Bitmap) : PubkyImageState
74-
data object Failed : PubkyImageState
75-
}
7620

7721
@Composable
7822
fun PubkyImage(
7923
uri: String,
8024
size: Dp,
8125
modifier: Modifier = Modifier,
8226
) {
83-
val viewModel: PubkyImageViewModel = hiltViewModel()
84-
val images by viewModel.images.collectAsStateWithLifecycle()
85-
val state = images[uri]
86-
87-
LaunchedEffect(uri) {
88-
viewModel.loadImage(uri)
89-
}
90-
91-
Box(
92-
contentAlignment = Alignment.Center,
93-
modifier = modifier
94-
.size(size)
95-
.clip(CircleShape)
96-
) {
97-
when (state) {
98-
is PubkyImageState.Loaded -> {
99-
Image(
100-
bitmap = state.bitmap.asImageBitmap(),
101-
contentDescription = null,
102-
contentScale = ContentScale.Crop,
103-
modifier = Modifier.matchParentSize()
104-
)
105-
}
106-
is PubkyImageState.Failed -> {
107-
Box(
108-
contentAlignment = Alignment.Center,
109-
modifier = Modifier
110-
.matchParentSize()
111-
.background(Colors.Gray5, CircleShape)
112-
) {
113-
Icon(
114-
painter = painterResource(R.drawable.ic_user_square),
115-
contentDescription = null,
116-
tint = Colors.White32,
117-
modifier = Modifier.size(size / 2)
118-
)
119-
}
120-
}
121-
else -> {
27+
SubcomposeAsyncImage(
28+
model = uri,
29+
contentDescription = null,
30+
contentScale = ContentScale.Crop,
31+
loading = {
32+
Box(contentAlignment = Alignment.Center) {
12233
CircularProgressIndicator(
12334
strokeWidth = 2.dp,
12435
color = Colors.White32,
125-
modifier = Modifier.size(size / 3)
36+
modifier = Modifier.size(size / 3),
12637
)
12738
}
128-
}
129-
}
39+
},
40+
error = {
41+
Box(
42+
contentAlignment = Alignment.Center,
43+
modifier = Modifier
44+
.matchParentSize()
45+
.background(Colors.Gray5, CircleShape),
46+
) {
47+
Icon(
48+
painter = painterResource(R.drawable.ic_user_square),
49+
contentDescription = null,
50+
tint = Colors.White32,
51+
modifier = Modifier.size(size / 2),
52+
)
53+
}
54+
},
55+
modifier = modifier
56+
.size(size)
57+
.clip(CircleShape),
58+
)
13059
}

0 commit comments

Comments
 (0)