Skip to content

Commit a9489ca

Browse files
ben-kaufmanclaude
andcommitted
fix: address pr review and claude.md compliance
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 60b7ff4 commit a9489ca

14 files changed

Lines changed: 219 additions & 90 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class PubkyImageFetcher(
3333
val json = JSONObject(String(data))
3434
val src = json.optString("src", "")
3535
if (src.isNotEmpty() && src.startsWith(PUBKY_SCHEME)) {
36-
Logger.debug("File descriptor found, fetching blob from '$src'", context = TAG)
36+
Logger.debug("Found file descriptor, fetching blob from '$src'", context = TAG)
3737
pubkyService.fetchFile(src)
3838
} else {
3939
data

app/src/main/java/to/bitkit/models/PubkyAuthRequest.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package to.bitkit.models
22

3+
import androidx.compose.runtime.Immutable
4+
5+
@Immutable
36
data class PubkyAuthPermission(
47
val path: String,
58
val accessLevel: String,

app/src/main/java/to/bitkit/models/PubkyProfile.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package to.bitkit.models
22

3+
import androidx.compose.runtime.Immutable
4+
import androidx.compose.runtime.Stable
35
import kotlinx.serialization.Serializable
46
import kotlinx.serialization.json.Json
57
import to.bitkit.ext.ellipsisMiddle
68
import com.synonym.bitkitcore.PubkyProfile as CorePubkyProfile
79

10+
@Immutable
811
data class PubkyProfileLink(val label: String, val url: String)
912

13+
@Stable
1014
data class PubkyProfile(
1115
val publicKey: String,
1216
val name: String,

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

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import to.bitkit.models.PubkyProfile
3232
import to.bitkit.models.PubkyProfileData
3333
import to.bitkit.models.PubkyProfileLink
3434
import to.bitkit.services.PubkyService
35+
import to.bitkit.utils.AppError
3536
import to.bitkit.utils.Logger
3637
import java.io.ByteArrayOutputStream
3738
import javax.inject.Inject
@@ -40,6 +41,11 @@ import kotlin.math.min
4041

4142
enum class PubkyAuthState { Idle, Authenticating, Authenticated }
4243

44+
sealed class PubkyContactError(message: String) : AppError(message) {
45+
data object CannotAddSelf : PubkyContactError("Cannot add your own pubky as a contact")
46+
data object InvalidFormat : PubkyContactError("Invalid pubky key format")
47+
}
48+
4349
@Suppress("TooManyFunctions")
4450
@Singleton
4551
class PubkyRepo @Inject constructor(
@@ -56,6 +62,8 @@ class PubkyRepo @Inject constructor(
5662
private const val PUBKY_SCHEME = "pubky://"
5763
private const val AVATAR_MAX_SIZE = 400
5864
private const val AVATAR_QUALITY = 80
65+
private const val PUBKY_KEY_LENGTH = 52
66+
private val Z_BASE_32_REGEX = Regex("^[ybndrfg8ejkmcpqxot1uwisza345h769]+$")
5967
}
6068

6169
private val scope = CoroutineScope(ioDispatcher + SupervisorJob())
@@ -137,7 +145,7 @@ class PubkyRepo @Inject constructor(
137145
}.getOrNull() ?: return
138146

139147
when (result) {
140-
is InitResult.NoSession -> Logger.debug("No saved paykit session found", context = TAG)
148+
is InitResult.NoSession -> Logger.debug("Found no saved paykit session", context = TAG)
141149
is InitResult.Restored -> {
142150
_publicKey.update { result.publicKey }
143151
_authState.update { PubkyAuthState.Authenticated }
@@ -157,7 +165,7 @@ class PubkyRepo @Inject constructor(
157165
}.getOrNull()
158166

159167
if (secretKeyHex.isNullOrEmpty()) {
160-
Logger.warn("No secret key available for re-sign-in recovery", context = TAG)
168+
Logger.warn("Skipped re-sign-in recovery, no secret key available", context = TAG)
161169
return InitResult.RestorationFailed
162170
}
163171

@@ -264,7 +272,7 @@ class PubkyRepo @Inject constructor(
264272
val json = pubkyService.fetchFileString(uri)
265273
PubkyProfileData.decode(json).toPubkyProfile(publicKey)
266274
}.onFailure {
267-
Logger.debug("No bitkit profile found, falling back to FFI", context = TAG)
275+
Logger.debug("Falling back to FFI, no bitkit profile found", context = TAG)
268276
}.getOrNull()
269277

270278
suspend fun fetchRemoteProfile(publicKey: String): Result<PubkyProfile?> = runCatching {
@@ -373,7 +381,7 @@ class PubkyRepo @Inject constructor(
373381
"No session available"
374382
}
375383
writeProfile(session, name, bio, links, tags, imageUrl)
376-
val pk = requireNotNull(_publicKey.value)
384+
val pk = requireNotNull(_publicKey.value) { "No public key available" }
377385
val profile = PubkyProfile(
378386
publicKey = pk,
379387
name = name,
@@ -500,10 +508,14 @@ class PubkyRepo @Inject constructor(
500508
}
501509

502510
suspend fun fetchContactProfile(publicKey: String): Result<PubkyProfile> {
503-
val prefixedKey = publicKey.ensurePubkyPrefix()
511+
val prefixedKey = runCatching { requireAddableContactPublicKey(publicKey) }
512+
.getOrElse { return Result.failure(it) }
504513
return fetchRemoteProfile(prefixedKey)
505514
.map { it ?: PubkyProfile.placeholder(prefixedKey) }
506-
.recover {
515+
.recoverCatching {
516+
if (!it.isMissingPubkyData()) {
517+
throw it
518+
}
507519
Logger.warn("Falling back to placeholder contact '$prefixedKey'", it, context = TAG)
508520
PubkyProfile.placeholder(prefixedKey)
509521
}
@@ -514,7 +526,7 @@ class PubkyRepo @Inject constructor(
514526
val session = requireNotNull(keychain.loadString(Keychain.Key.PAYKIT_SESSION.name)) {
515527
"No session available"
516528
}
517-
val prefixedKey = publicKey.ensurePubkyPrefix()
529+
val prefixedKey = requireAddableContactPublicKey(publicKey)
518530
val profile = existingProfile?.copy(publicKey = prefixedKey) ?: run {
519531
val ffiProfile = pubkyService.getProfile(prefixedKey)
520532
PubkyProfile.fromFfi(prefixedKey, ffiProfile)
@@ -734,20 +746,45 @@ class PubkyRepo @Inject constructor(
734746
_authState.update { PubkyAuthState.Idle }
735747
}
736748

749+
private fun requireAddableContactPublicKey(publicKey: String): String {
750+
val prefixedKey = publicKey.trim().ensurePubkyPrefix()
751+
val strippedKey = prefixedKey.removePrefix(PUBKY_PREFIX)
752+
if (strippedKey.length != PUBKY_KEY_LENGTH || !Z_BASE_32_REGEX.matches(strippedKey)) {
753+
throw PubkyContactError.InvalidFormat
754+
}
755+
if (_publicKey.value == prefixedKey) {
756+
throw PubkyContactError.CannotAddSelf
757+
}
758+
return prefixedKey
759+
}
760+
737761
private fun String.ensurePubkyPrefix(): String =
738762
if (startsWith(PUBKY_PREFIX)) this else "$PUBKY_PREFIX$this"
739763

740764
private fun Throwable.isMissingPubkyDirectory(): Boolean {
741-
val fullMessage = buildString {
765+
if (isMissingPubkyData()) {
766+
return true
767+
}
768+
769+
val fullMessage = buildErrorMessage()
770+
return fullMessage.contains("directory not found", ignoreCase = true)
771+
}
772+
773+
private fun Throwable.isMissingPubkyData(): Boolean {
774+
val fullMessage = buildErrorMessage()
775+
return fullMessage.contains("404") ||
776+
fullMessage.contains("not found", ignoreCase = true) ||
777+
fullMessage.contains("missing", ignoreCase = true)
778+
}
779+
780+
private fun Throwable.buildErrorMessage(): String =
781+
buildString {
742782
append(message.orEmpty())
743783
cause?.message?.takeIf { it.isNotBlank() }?.let {
744784
append(" ")
745785
append(it)
746786
}
747787
}
748-
return fullMessage.contains("directory not found", ignoreCase = true) ||
749-
(fullMessage.contains("404") && fullMessage.contains("not found", ignoreCase = true))
750-
}
751788

752789
// endregion
753790
}

app/src/main/java/to/bitkit/ui/components/AddLinkSheet.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import androidx.compose.runtime.mutableStateOf
1818
import androidx.compose.runtime.remember
1919
import androidx.compose.runtime.setValue
2020
import androidx.compose.ui.Modifier
21+
import androidx.compose.ui.platform.LocalFocusManager
22+
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
2123
import androidx.compose.ui.res.painterResource
2224
import androidx.compose.ui.res.stringResource
2325
import androidx.compose.ui.tooling.preview.Preview
@@ -46,9 +48,19 @@ fun AddLinkSheet(
4648
var label by remember { mutableStateOf("") }
4749
var url by remember { mutableStateOf("") }
4850
var showSuggestions by remember { mutableStateOf(false) }
51+
val focusManager = LocalFocusManager.current
52+
val keyboardController = LocalSoftwareKeyboardController.current
53+
54+
val dismissKeyboard = {
55+
focusManager.clearFocus()
56+
keyboardController?.hide()
57+
}
4958

5059
BottomSheet(
51-
onDismissRequest = onDismiss,
60+
onDismissRequest = {
61+
dismissKeyboard()
62+
onDismiss()
63+
},
5264
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
5365
modifier = Modifier.imePadding()
5466
) {
@@ -57,6 +69,7 @@ fun AddLinkSheet(
5769
title = stringResource(R.string.profile__suggestions_to_add),
5870
suggestions = LINK_SUGGESTIONS,
5971
onSelect = {
72+
dismissKeyboard()
6073
label = it
6174
showSuggestions = false
6275
},
@@ -69,7 +82,10 @@ fun AddLinkSheet(
6982
onLabelChange = { label = it },
7083
onUrlChange = { url = it },
7184
onShowSuggestions = { showSuggestions = true },
72-
onSave = { onSave(label, url) },
85+
onSave = {
86+
dismissKeyboard()
87+
onSave(label, url)
88+
},
7389
isSaveEnabled = label.isNotBlank() && url.isNotBlank(),
7490
)
7591
}

app/src/main/java/to/bitkit/ui/components/AddTagSheet.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import androidx.compose.runtime.mutableStateOf
1515
import androidx.compose.runtime.remember
1616
import androidx.compose.runtime.setValue
1717
import androidx.compose.ui.Modifier
18+
import androidx.compose.ui.platform.LocalFocusManager
19+
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
1820
import androidx.compose.ui.res.painterResource
1921
import androidx.compose.ui.res.stringResource
2022
import androidx.compose.ui.tooling.preview.Preview
@@ -40,9 +42,19 @@ fun AddTagSheet(
4042
) {
4143
var tag by remember { mutableStateOf("") }
4244
var showSuggestions by remember { mutableStateOf(false) }
45+
val focusManager = LocalFocusManager.current
46+
val keyboardController = LocalSoftwareKeyboardController.current
47+
48+
val dismissKeyboard = {
49+
focusManager.clearFocus()
50+
keyboardController?.hide()
51+
}
4352

4453
BottomSheet(
45-
onDismissRequest = onDismiss,
54+
onDismissRequest = {
55+
dismissKeyboard()
56+
onDismiss()
57+
},
4658
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
4759
modifier = Modifier.imePadding()
4860
) {
@@ -51,6 +63,7 @@ fun AddTagSheet(
5163
title = stringResource(R.string.profile__suggestions_to_add),
5264
suggestions = TAG_SUGGESTIONS,
5365
onSelect = {
66+
dismissKeyboard()
5467
tag = it
5568
showSuggestions = false
5669
},
@@ -61,7 +74,10 @@ fun AddTagSheet(
6174
tag = tag,
6275
onTagChange = { tag = it },
6376
onShowSuggestions = { showSuggestions = true },
64-
onSave = { onSave(tag) },
77+
onSave = {
78+
dismissKeyboard()
79+
onSave(tag)
80+
},
6581
isSaveEnabled = tag.isNotBlank(),
6682
)
6783
}

app/src/main/java/to/bitkit/ui/components/ProfileEditForm.kt

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.FlowRow
77
import androidx.compose.foundation.layout.Row
88
import androidx.compose.foundation.layout.fillMaxSize
99
import androidx.compose.foundation.layout.fillMaxWidth
10+
import androidx.compose.foundation.layout.imePadding
1011
import androidx.compose.foundation.layout.padding
1112
import androidx.compose.foundation.layout.size
1213
import androidx.compose.foundation.rememberScrollState
@@ -34,6 +35,8 @@ import to.bitkit.ui.theme.AppTextStyles
3435
import to.bitkit.ui.theme.AppThemeSurface
3536
import to.bitkit.ui.theme.Colors
3637

38+
private const val BIO_MAX_LENGTH = 160
39+
3740
@OptIn(ExperimentalLayoutApi::class)
3841
@Composable
3942
fun ProfileEditForm(
@@ -54,13 +57,20 @@ fun ProfileEditForm(
5457
isSaveEnabled: Boolean,
5558
modifier: Modifier = Modifier,
5659
avatarContent: @Composable () -> Unit = {},
60+
publicKeyLabel: String? = null,
61+
footerNote: String? = null,
62+
showFooterNote: Boolean = true,
5763
onDelete: (() -> Unit)? = null,
5864
deleteLabel: String = "",
5965
) {
66+
val resolvedPublicKeyLabel = publicKeyLabel ?: stringResource(R.string.profile__your_pubky)
67+
val resolvedFooterNote = footerNote ?: stringResource(R.string.profile__edit_public_note)
68+
6069
Column(
6170
horizontalAlignment = Alignment.CenterHorizontally,
6271
modifier = modifier
6372
.fillMaxSize()
73+
.imePadding()
6474
.verticalScroll(rememberScrollState())
6575
.padding(horizontal = 32.dp)
6676
) {
@@ -81,7 +91,7 @@ fun ProfileEditForm(
8191
VerticalSpacer(12.dp)
8292

8393
Text13Up(
84-
text = stringResource(R.string.profile__your_pubky),
94+
text = resolvedPublicKeyLabel,
8595
color = Colors.White64,
8696
)
8797
VerticalSpacer(4.dp)
@@ -101,7 +111,7 @@ fun ProfileEditForm(
101111
VerticalSpacer(8.dp)
102112
TextInput(
103113
value = bio,
104-
onValueChange = onBioChange,
114+
onValueChange = { onBioChange(it.take(BIO_MAX_LENGTH)) },
105115
placeholder = stringResource(R.string.profile__edit_bio_placeholder),
106116
minLines = 2,
107117
maxLines = 4,
@@ -191,10 +201,12 @@ fun ProfileEditForm(
191201
}
192202

193203
VerticalSpacer(16.dp)
194-
BodyS(
195-
text = stringResource(R.string.profile__edit_public_note),
196-
color = Colors.White64,
197-
)
204+
if (showFooterNote) {
205+
BodyS(
206+
text = resolvedFooterNote,
207+
color = Colors.White64,
208+
)
209+
}
198210

199211
if (onDelete != null) {
200212
VerticalSpacer(16.dp)

0 commit comments

Comments
 (0)