Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5983a73
feat: add profile fetching from pubky
ben-kaufman Mar 5, 2026
b5f211c
Merge branch 'master' into feat/pubky-profile
ben-kaufman Mar 5, 2026
8d5bac7
fix build
ben-kaufman Mar 5, 2026
1bdd233
Merge branch 'feat/pubky-profile' of https://github.com/synonymdev/bi…
ben-kaufman Mar 5, 2026
1484f26
fix paykit version
ben-kaufman Mar 5, 2026
06a618d
fixes
ben-kaufman Mar 6, 2026
7ff4b1e
fixes
ben-kaufman Mar 6, 2026
13bd697
fixes
ben-kaufman Mar 6, 2026
359eddc
fixes
ben-kaufman Mar 6, 2026
ec754c1
detekt fix
ben-kaufman Mar 6, 2026
9ede70f
fixes
ben-kaufman Mar 6, 2026
ea2a78e
fix comment
ben-kaufman Mar 10, 2026
fcb20c9
Merge branch 'master' into feat/pubky-profile
ovitrif Mar 10, 2026
5100c97
fix feedback comments
ben-kaufman Mar 11, 2026
1985f9e
Merge branch 'master' into feat/pubky-profile
ben-kaufman Mar 11, 2026
8656cb7
claude fixes
ben-kaufman Mar 11, 2026
9b3a0e8
feat: migrate pubky profile fetching from paykit to bitkitcore
ben-kaufman Mar 11, 2026
97b8e70
feat: add Pubky contacts screen with intro flow and contact detail view
ben-kaufman Mar 11, 2026
4be2306
fixes
ben-kaufman Mar 11, 2026
6634eca
Merge branch 'master' into feat/pubky-profile
ben-kaufman Mar 12, 2026
c3b548c
Merge branch 'master' into feat/pubky-profile
ovitrif Mar 12, 2026
e0527c7
fixes
ben-kaufman Mar 15, 2026
63cfa6e
Merge remote-tracking branch 'origin/master' into feat/pubky-profile
ben-kaufman Mar 16, 2026
78d16f7
fixes
ben-kaufman Mar 16, 2026
1f0bba0
fixes
ben-kaufman Mar 16, 2026
7c4fdad
fix detekt
ben-kaufman Mar 16, 2026
e52430d
feat: add coil with pubky image fetcher
ovitrif Mar 13, 2026
b0383b4
refactor: migrate pubky images to coil
ovitrif Mar 13, 2026
ae06f53
feat: add crossfade and spring pop to pubky images
ovitrif Mar 13, 2026
d16bbe1
refactor: remove modifiers trailing comma
ovitrif Mar 13, 2026
6ae1a82
refactor: use AsyncImage vs. SubcomposeAsyncImage
ovitrif Mar 16, 2026
94b2b5a
fix: address PR review remarks
ovitrif Mar 16, 2026
9a07040
refactor: extract ActionButton and LinkRow into shared components, fi…
ben-kaufman Mar 17, 2026
a3ac562
Merge branch 'master' into feat/pubky-profile
ben-kaufman Mar 17, 2026
64e8017
Merge branch 'feat/pubky-profile' into feat/pubky-async-image
ben-kaufman Mar 17, 2026
0ce5b99
Merge pull request #846 from synonymdev/feat/pubky-async-image
ben-kaufman Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ dependencies {
implementation(libs.bouncycastle.provider.jdk)
implementation(libs.ldk.node.android) { exclude(group = "net.java.dev.jna", module = "jna") }
implementation(libs.bitkit.core)
implementation(libs.paykit)
implementation(libs.vss.client)
// Firebase
implementation(platform(libs.firebase.bom))
Expand Down Expand Up @@ -266,6 +267,9 @@ dependencies {
implementation(libs.charts)
implementation(libs.haze)
implementation(libs.haze.materials)
// Image Loading
implementation(platform(libs.coil.bom))
implementation(libs.coil.compose)
// Compose Navigation
implementation(libs.navigation.compose)
androidTestImplementation(libs.navigation.testing)
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
<intent>
<action android:name="android.settings.APPLICATION_DETAILS_SETTINGS" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="pubkyauth" />
</intent>
</queries>

<uses-feature
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/to/bitkit/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import coil3.ImageLoader
import coil3.SingletonImageLoader
import dagger.hilt.android.HiltAndroidApp
import to.bitkit.env.Env
import javax.inject.Inject
Expand All @@ -16,13 +18,17 @@ internal open class App : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory

@Inject
lateinit var imageLoader: ImageLoader

override val workManagerConfiguration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()

override fun onCreate() {
super.onCreate()
SingletonImageLoader.setSafe { imageLoader }
currentActivity = CurrentActivity().also { registerActivityLifecycleCallbacks(it) }
Env.initAppStoragePath(filesDir.absolutePath)
}
Expand Down
50 changes: 50 additions & 0 deletions app/src/main/java/to/bitkit/data/PubkyImageFetcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package to.bitkit.data

import coil3.ImageLoader
import coil3.Uri
import coil3.decode.DataSource
import coil3.decode.ImageSource
import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import okio.Buffer
import org.json.JSONObject
import to.bitkit.services.PubkyService
import to.bitkit.utils.Logger

private const val TAG = "PubkyImageFetcher"
private const val PUBKY_SCHEME = "pubky://"

class PubkyImageFetcher(
private val uri: String,
private val options: Options,
private val pubkyService: PubkyService,
) : Fetcher {

override suspend fun fetch(): FetchResult {
val data = pubkyService.fetchFile(uri)
val blobData = resolveImageData(data)
val source = ImageSource(Buffer().apply { write(blobData) }, options.fileSystem)
return SourceFetchResult(source, null, dataSource = DataSource.NETWORK)
}

private suspend fun resolveImageData(data: ByteArray): ByteArray = runCatching {
val json = JSONObject(String(data))
val src = json.optString("src", "")
if (src.isNotEmpty() && src.startsWith(PUBKY_SCHEME)) {
Logger.debug("File descriptor found, fetching blob from '$src'", context = TAG)
pubkyService.fetchFile(src)
} else {
data
}
}.getOrDefault(data)

class Factory(private val pubkyService: PubkyService) : Fetcher.Factory<Uri> {
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
val uri = data.toString()
if (!uri.startsWith(PUBKY_SCHEME)) return null
return PubkyImageFetcher(uri, options, pubkyService)
}
}
}
39 changes: 39 additions & 0 deletions app/src/main/java/to/bitkit/data/PubkyStore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package to.bitkit.data

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.dataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import to.bitkit.data.serializers.PubkyStoreSerializer
import javax.inject.Inject
import javax.inject.Singleton

private val Context.pubkyDataStore: DataStore<PubkyStoreData> by dataStore(
fileName = "pubky.json",
serializer = PubkyStoreSerializer,
)

@Singleton
class PubkyStore @Inject constructor(
@ApplicationContext private val context: Context,
) {
private val store = context.pubkyDataStore

val data: Flow<PubkyStoreData> = store.data

suspend fun update(transform: (PubkyStoreData) -> PubkyStoreData) {
store.updateData(transform)
}

suspend fun reset() {
store.updateData { PubkyStoreData() }
}
}

@Serializable
data class PubkyStoreData(
val cachedName: String? = null,
val cachedImageUri: String? = null,
)
1 change: 1 addition & 0 deletions app/src/main/java/to/bitkit/data/SettingsStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ data class SettingsData(
val hasSeenSavingsIntro: Boolean = false,
val hasSeenShopIntro: Boolean = false,
val hasSeenProfileIntro: Boolean = false,
val hasSeenContactsIntro: Boolean = false,
val quickPayIntroSeen: Boolean = false,
val bgPaymentsIntroSeen: Boolean = false,
val isQuickPayEnabled: Boolean = false,
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/to/bitkit/data/keychain/Keychain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ class Keychain @Inject constructor(
BIP39_PASSPHRASE,
PIN,
PIN_ATTEMPTS_REMAINING,
PAYKIT_SESSION,
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package to.bitkit.data.serializers

import androidx.datastore.core.Serializer
import to.bitkit.data.PubkyStoreData
import to.bitkit.di.json
import to.bitkit.utils.Logger
import java.io.InputStream
import java.io.OutputStream

object PubkyStoreSerializer : Serializer<PubkyStoreData> {
private const val TAG = "PubkyStoreSerializer"

override val defaultValue: PubkyStoreData = PubkyStoreData()

override suspend fun readFrom(input: InputStream): PubkyStoreData {
return runCatching {
json.decodeFromString<PubkyStoreData>(input.readBytes().decodeToString())
}.getOrElse {
Logger.error("Failed to deserialize PubkyStoreData", it, context = TAG)
defaultValue
}
}

override suspend fun writeTo(t: PubkyStoreData, output: OutputStream) {
output.write(json.encodeToString(t).encodeToByteArray())
}
}
41 changes: 41 additions & 0 deletions app/src/main/java/to/bitkit/di/ImageModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package to.bitkit.di

import android.content.Context
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.memory.MemoryCache
import coil3.request.crossfade
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import to.bitkit.data.PubkyImageFetcher
import to.bitkit.services.PubkyService
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ImageModule {

@Provides
@Singleton
fun provideImageLoader(
@ApplicationContext context: Context,
pubkyService: PubkyService,
): ImageLoader = ImageLoader.Builder(context)
.crossfade(true)
.components { add(PubkyImageFetcher.Factory(pubkyService)) }
.memoryCache {
MemoryCache.Builder()
.maxSizePercent(context, percent = 0.15)
.build()
}
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("pubky-images"))
.build()
}
.build()
}
10 changes: 10 additions & 0 deletions app/src/main/java/to/bitkit/env/Env.kt
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,16 @@ internal object Env {
const val BITREFILL_APP = "Bitkit"
const val BITREFILL_REF = "AL6dyZYt"

val pubkyCapabilities: String
get() {
val prefix = when (network) {
Network.BITCOIN -> ""
else -> "staging."
}
return "/pub/${prefix}paykit.app/v0/:rw," +
"/pub/${prefix}pubky.app/profile.json:rw,/pub/${prefix}pubky.app/follows/:rw"
}

val rnBackupServerHost: String
get() = when (network) {
Network.BITCOIN -> "https://blocktank.synonym.to/backups-ldk"
Expand Down
55 changes: 55 additions & 0 deletions app/src/main/java/to/bitkit/models/PubkyProfile.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package to.bitkit.models

import to.bitkit.ext.ellipsisMiddle
import com.synonym.bitkitcore.PubkyProfile as CorePubkyProfile

data class PubkyProfileLink(val label: String, val url: String)

data class PubkyProfile(
val publicKey: String,
val name: String,
val bio: String,
val imageUrl: String?,
val links: List<PubkyProfileLink>,
val status: String?,
) {
companion object {
private const val TRUNCATED_PK_LENGTH = 11

fun fromFfi(publicKey: String, ffiProfile: CorePubkyProfile): PubkyProfile {
return PubkyProfile(
publicKey = publicKey,
name = ffiProfile.name,
bio = ffiProfile.bio ?: "",
imageUrl = ffiProfile.image,
links = ffiProfile.links.orEmpty().map { PubkyProfileLink(label = it.title, url = it.url) },
status = ffiProfile.status,
)
}

fun placeholder(publicKey: String) = PubkyProfile(
publicKey = publicKey,
name = publicKey.ellipsisMiddle(TRUNCATED_PK_LENGTH),
bio = "",
imageUrl = null,
links = emptyList(),
status = null,
)

fun forDisplay(
publicKey: String,
name: String?,
imageUrl: String?,
) = PubkyProfile(
publicKey = publicKey,
name = name ?: publicKey.ellipsisMiddle(TRUNCATED_PK_LENGTH),
bio = "",
imageUrl = imageUrl,
links = emptyList(),
status = null,
)
}

val truncatedPublicKey: String
get() = publicKey.ellipsisMiddle(TRUNCATED_PK_LENGTH)
}
4 changes: 2 additions & 2 deletions app/src/main/java/to/bitkit/models/Suggestion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ enum class Suggestion(
PROFILE(
title = R.string.cards__slashtagsProfile__title,
description = R.string.cards__slashtagsProfile__description,
color = Colors.Brand24,
icon = R.drawable.crown
color = Colors.PubkyGreen24,
icon = R.drawable.crown,
),
SHOP(
title = R.string.cards__shop__title,
Expand Down
Loading
Loading