From 997a59dc23fdf09360eb7a679d812e1dfd6e251a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 14:30:26 +0000 Subject: [PATCH 01/13] feat: add file encryption toggle for encrypted DMs with retry on failure Add an "Encrypt files" toggle (default: on) to the file upload dialog in NIP-17 encrypted DMs. When encrypted upload fails, show an informative dialog explaining that many servers don't accept encrypted files on free accounts, with an option to retry without encryption. The retry dialog warns that without encryption anyone with the link can see the content. https://claude.ai/code/session_012xvKzHrZPq3ZTAzBN2LvrR --- .../privateDM/send/ChatNewMessageViewModel.kt | 89 ++++++++++++++++++- .../chats/privateDM/send/NewGroupDMScreen.kt | 9 ++ .../send/PrivateMessageEditFieldRow.kt | 44 +++++++++ .../send/upload/ChatFileUploadDialog.kt | 1 + .../privateDM/send/upload/ChatFileUploader.kt | 56 +++++++++++- .../chats/utils/ChatFileUploadDialog.kt | 14 ++- .../chats/utils/ChatFileUploadState.kt | 3 + amethyst/src/main/res/values/strings.xml | 7 ++ 8 files changed, 216 insertions(+), 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 8ae6448bc..26c3d530c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -403,7 +403,16 @@ class ChatNewMessageViewModel : accountViewModel.launchSigner { if (nip17) { - ChatFileUploader(account).justUploadNIP17(uploadState, onError, context) { + ChatFileUploader(account).justUploadNIP17( + uploadState, + onError, + onEncryptedUploadError = { title, message -> + encryptedUploadErrorTitle = title + encryptedUploadErrorMessage = message + pendingRetryMode = RetryMode.HOLD + }, + context, + ) { uploadsWaitingToBeSent += it draftTag.newVersion() onceUploaded() @@ -428,7 +437,19 @@ class ChatNewMessageViewModel : accountViewModel.launchSigner { if (nip17) { - ChatFileUploader(account).justUploadNIP17(uploadState, onError, context) { + ChatFileUploader(account).justUploadNIP17( + uploadState, + onError, + onEncryptedUploadError = { title, message -> + encryptedUploadErrorTitle = title + encryptedUploadErrorMessage = message + pendingRetryMode = RetryMode.SEND + pendingRetryOnError = onError + pendingRetryContext = context + pendingRetryOnceUploaded = onceUploaded + }, + context, + ) { ChatFileSender(room, account).sendNIP17(it) draftTag.newVersion() onceUploaded() @@ -443,6 +464,68 @@ class ChatNewMessageViewModel : } } + // Encrypted upload error state for retry dialog + var encryptedUploadErrorTitle by mutableStateOf(null) + var encryptedUploadErrorMessage by mutableStateOf(null) + var pendingRetryMode by mutableStateOf(null) + var pendingRetryOnError by mutableStateOf<((String, String) -> Unit)?>(null) + var pendingRetryContext by mutableStateOf(null) + var pendingRetryOnceUploaded by mutableStateOf<(() -> Unit)?>(null) + + enum class RetryMode { HOLD, SEND } + + fun dismissEncryptedUploadError() { + encryptedUploadErrorTitle = null + encryptedUploadErrorMessage = null + pendingRetryMode = null + pendingRetryOnError = null + pendingRetryContext = null + pendingRetryOnceUploaded = null + } + + fun retryWithoutEncryption() { + val mode = pendingRetryMode ?: return + val onError = pendingRetryOnError + val context = pendingRetryContext + val onceUploaded = pendingRetryOnceUploaded + val room = room + val uploadState = uploadState + + dismissEncryptedUploadError() + + if (uploadState == null || context == null) return + + uploadState.encryptFiles = false + + accountViewModel.launchSigner { + when (mode) { + RetryMode.HOLD -> { + ChatFileUploader(account).justUploadNIP17Unencrypted( + uploadState, + onError ?: accountViewModel.toastManager::toast, + context, + ) { + uploadsWaitingToBeSent += it + draftTag.newVersion() + onceUploaded?.invoke() + } + } + RetryMode.SEND -> { + if (room == null) return@launchSigner + ChatFileUploader(account).justUploadNIP17Unencrypted( + uploadState, + onError ?: accountViewModel.toastManager::toast, + context, + ) { + ChatFileSender(room, account).sendNIP17(it) + draftTag.newVersion() + onceUploaded?.invoke() + } + } + } + } + } + private suspend fun innerSendPost(draftTag: String?) { val room = room ?: return @@ -563,6 +646,8 @@ class ChatNewMessageViewModel : uploadsWaitingToBeSent = emptyList() uploadState?.reset() + dismissEncryptedUploadError() + iMetaAttachments.reset() emojiSuggestions?.reset() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index c8f1bf6c9..117f96d36 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -302,6 +302,15 @@ fun GroupDMScreenContent( ) } } + + postViewModel.encryptedUploadErrorTitle?.let { title -> + EncryptedUploadErrorDialog( + title = title, + message = postViewModel.encryptedUploadErrorMessage ?: "", + onDismiss = postViewModel::dismissEncryptedUploadError, + onRetryWithoutEncryption = postViewModel::retryWithoutEncryption, + ) + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index 2615ac2ec..a0b45b9d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -27,8 +27,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -114,6 +116,15 @@ fun PrivateMessageEditFieldRow( } } + channelScreenModel.encryptedUploadErrorTitle?.let { title -> + EncryptedUploadErrorDialog( + title = title, + message = channelScreenModel.encryptedUploadErrorMessage ?: "", + onDismiss = channelScreenModel::dismissEncryptedUploadError, + onRetryWithoutEncryption = channelScreenModel::retryWithoutEncryption, + ) + } + Column( modifier = EditFieldModifier, ) { @@ -215,3 +226,36 @@ fun KeyboardLeadingIcon( ToggleNip17Button(channelScreenModel, accountViewModel) } } + +@Composable +fun EncryptedUploadErrorDialog( + title: String, + message: String, + onDismiss: () -> Unit, + onRetryWithoutEncryption: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(title) }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(message) + Text( + stringRes(R.string.upload_without_encryption_warning), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + } + }, + confirmButton = { + TextButton(onClick = onRetryWithoutEncryption) { + Text(stringRes(R.string.retry_without_encryption)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringRes(R.string.cancel)) + } + }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt index 1dca30b9f..d5c06cfcf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt @@ -74,5 +74,6 @@ fun RoomChatFileUploadDialog( onCancel, accountViewModel, nav, + isNip17 = channelScreenModel.nip17, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt index 33a9e456e..2770d85f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt @@ -39,20 +39,68 @@ class ChatFileUploader( suspend fun justUploadNIP17( viewState: ChatFileUploadState, onError: (title: String, message: String) -> Unit, + onEncryptedUploadError: (title: String, message: String) -> Unit, context: Context, onceUploaded: suspend (List) -> Unit, ) { val orchestrator = viewState.multiOrchestrator ?: return viewState.mediaUploadTracker.startUpload(orchestrator.hasNonMedia()) - val cipher = AESGCM() + if (viewState.encryptFiles) { + val cipher = AESGCM() + + val results = + orchestrator.uploadEncrypted( + viewState.caption, + viewState.contentWarningReason, + MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), + cipher, + viewState.selectedServer, + account, + context, + ) + + if (results.allGood) { + val list = + results.successful.mapNotNull { state -> + if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, cipher) + } else { + null + } + } + + onceUploaded(list) + viewState.reset() + } else { + val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct() + + onEncryptedUploadError( + stringRes(context, R.string.failed_to_upload_encrypted_media_title), + stringRes(context, R.string.failed_to_upload_encrypted_media_message) + "\n\n" + errorMessages.joinToString(".\n"), + ) + } + } else { + justUploadNIP17Unencrypted(viewState, onError, context, onceUploaded) + } + + viewState.mediaUploadTracker.finishUpload() + } + + suspend fun justUploadNIP17Unencrypted( + viewState: ChatFileUploadState, + onError: (title: String, message: String) -> Unit, + context: Context, + onceUploaded: suspend (List) -> Unit, + ) { + val orchestrator = viewState.multiOrchestrator ?: return + viewState.mediaUploadTracker.startUpload(orchestrator.hasNonMedia()) val results = - orchestrator.uploadEncrypted( + orchestrator.upload( viewState.caption, viewState.contentWarningReason, MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), - cipher, viewState.selectedServer, account, context, @@ -62,7 +110,7 @@ class ChatFileUploader( val list = results.successful.mapNotNull { state -> if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, cipher) + SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, null) } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt index f1ce97525..c05fa0c4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt @@ -81,6 +81,7 @@ fun ChatFileUploadDialog( onCancel: () -> Unit, accountViewModel: AccountViewModel, nav: INav, + isNip17: Boolean = false, ) { val scrollState = rememberScrollState() @@ -132,7 +133,7 @@ fun ChatFileUploadDialog( ) { Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp, bottom = 10.dp)) { Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) { - ImageVideoPostChat(state, accountViewModel) + ImageVideoPostChat(state, accountViewModel, isNip17) } } } @@ -144,6 +145,7 @@ fun ChatFileUploadDialog( private fun ImageVideoPostChat( fileUploadState: ChatFileUploadState, accountViewModel: AccountViewModel, + isNip17: Boolean = false, ) { val fileServers by accountViewModel.account.blossomServers.hostNameFlow .collectAsState() @@ -190,6 +192,16 @@ private fun ImageVideoPostChat( onCheckedChange = fileUploadState::updateContentWarning, ) + if (isNip17) { + SettingSwitchItem( + title = R.string.encrypt_files_label, + description = R.string.encrypt_files_description, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + checked = fileUploadState.encryptFiles, + onCheckedChange = { fileUploadState.encryptFiles = it }, + ) + } + SettingsRow(R.string.file_server, R.string.file_server_description) { TextSpinner( label = "", diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt index 32105a29c..1e6717ae0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt @@ -55,6 +55,8 @@ class ChatFileUploadState( // 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED var mediaQualitySlider by mutableIntStateOf(1) + var encryptFiles by mutableStateOf(true) + fun load(uris: ImmutableList) { reset() this.multiOrchestrator = MultiOrchestrator(uris) @@ -70,6 +72,7 @@ class ChatFileUploadState( mediaUploadTracker.finishUpload() caption = "" selectedServer = defaultServer + encryptFiles = true } fun deleteMediaToUpload(selected: SelectedMediaProcessing) { diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 36ce30142..c649661e2 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1215,6 +1215,13 @@ Compression Cancelled Compression failed to return a file + Encrypt files + Encrypt files before uploading for privacy. Some servers may not accept encrypted files on free accounts. + Encrypted upload failed + Many servers do not accept encrypted files on free accounts. You can retry without encryption. + Retry without encryption + Warning: Without encryption, anyone with the file link can see the content. + Media Quality Select Low quality to compress your media to a smaller file with less quality, High quality to compress to a larger file with higher quality or Uncompressed to upload the media without compression. Low From c56665d4ddcaadc539d68e14dbd21354c6d63ac4 Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 16 Mar 2026 10:51:24 +0100 Subject: [PATCH 02/13] spotlessApply --- .../loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 26c3d530c..44d7f8c75 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -510,6 +510,7 @@ class ChatNewMessageViewModel : onceUploaded?.invoke() } } + RetryMode.SEND -> { if (room == null) return@launchSigner ChatFileUploader(account).justUploadNIP17Unencrypted( From 39e15a89295ab2bf642181488c1f9868d0ede1b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 03:23:00 +0000 Subject: [PATCH 03/13] feat: comment benchmark APK link on open PRs Add PR commenting to the build-benchmark-apk workflow. When a build completes, it now finds any open PR for the branch and posts a comment with a direct download link to the benchmark APK artifact. https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf --- .github/workflows/build-benchmark-apk.yml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml index a9e338b31..e3efc3674 100644 --- a/.github/workflows/build-benchmark-apk.yml +++ b/.github/workflows/build-benchmark-apk.yml @@ -48,9 +48,30 @@ jobs: with: script: | const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const artifactUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/artifacts/${{ steps.upload.outputs.artifact-id }}`; + const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${artifactUrl})\n\nOr view all artifacts in the [workflow run](${runUrl}#artifacts).`; + await github.rest.repos.createCommitComment({ owner: context.repo.owner, repo: context.repo.repo, commit_sha: context.sha, - body: `📦 **Benchmark APK ready!**\n\nDownload it from the [workflow run artifacts](${runUrl}#artifacts).` + body }); + + // Find open PRs for this branch and comment on them + const branch = context.ref.replace('refs/heads/', ''); + const { data: prs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + head: `${context.repo.owner}:${branch}`, + state: 'open' + }); + + for (const pr of prs) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body + }); + } From af12d13d67229650bcd97825b10f9225dbaa5201 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 03:34:53 +0000 Subject: [PATCH 04/13] fix: use artifact-url output for direct APK download link The manually constructed URL was invalid. Use the artifact-url output from upload-artifact which provides the correct download URL. https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf --- .github/workflows/build-benchmark-apk.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml index e3efc3674..54db24f5e 100644 --- a/.github/workflows/build-benchmark-apk.yml +++ b/.github/workflows/build-benchmark-apk.yml @@ -48,7 +48,7 @@ jobs: with: script: | const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const artifactUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/artifacts/${{ steps.upload.outputs.artifact-id }}`; + const artifactUrl = `${{ steps.upload.outputs.artifact-url }}`; const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${artifactUrl})\n\nOr view all artifacts in the [workflow run](${runUrl}#artifacts).`; await github.rest.repos.createCommitComment({ From 1c4f9769b12c2e494ff8e5ef5c5ad633c4accb0c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 03:43:10 +0000 Subject: [PATCH 05/13] fix: session-start hook fails silently when dl.google.com is blocked The Claude Code web egress proxy does not include dl.google.com in its allowlist, causing the session-start hook to abort entirely (due to set -euo pipefail) when Android SDK downloads fail. This cascading failure also prevented local.properties creation and ANDROID_HOME export. Additionally, JAVA_TOOL_OPTIONS contains nonProxyHosts entries for *.google.com and *.googleapis.com, which causes the JVM to bypass the proxy for Google Maven, resulting in connection failures. Changes: - Remove set -euo pipefail; use per-section error handling instead - Fix JAVA_TOOL_OPTIONS: strip *.google.com from nonProxyHosts - Fix no_proxy: remove *.google.com and *.googleapis.com entries - Move local.properties and env exports before SDK download section - Make SDK downloads non-fatal with clear warning messages - Install standalone ktlint as fallback for spotlessApply - Create spotless-apply wrapper script for proxy-blocked environments - Update Stop hook to fall back to spotless-apply when Gradle fails https://claude.ai/code/session_01HeWiRdKDTnMGfEBUegk3d1 --- .claude/hooks/session-start.sh | 232 ++++++++++++++++++++++++--------- .claude/settings.json | 2 +- 2 files changed, 170 insertions(+), 64 deletions(-) diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index 35cddfb3b..b17f6e97b 100644 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -1,12 +1,59 @@ #!/bin/bash # Session start hook: Configure proxy auth, SSL trust, and Android SDK for Claude Code on the web -set -euo pipefail # Only run in remote (web) environments if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then exit 0 fi +# --- Fix no_proxy: remove *.google.com and *.googleapis.com so traffic routes through proxy --- +# The default no_proxy in Claude Code web includes *.google.com and *.googleapis.com, +# which causes JVM/curl to bypass the proxy for Google Maven (dl.google.com). +# These domains MUST go through the proxy for authenticated egress to work. +fix_no_proxy() { + local var="$1" + local val="${!var:-}" + if [ -n "$val" ]; then + # Remove *.google.com, *.googleapis.com entries from the comma-separated list + local fixed + fixed=$(echo "$val" | tr ',' '\n' | grep -v -E '^\*\.google\.com$|^\*\.googleapis\.com$' | tr '\n' ',' | sed 's/,$//') + export "$var=$fixed" + fi +} + +fix_no_proxy no_proxy +fix_no_proxy NO_PROXY + +# --- Fix JAVA_TOOL_OPTIONS: remove nonProxyHosts that bypass Google domains --- +# JAVA_TOOL_OPTIONS may contain -Dhttp.nonProxyHosts=...|*.google.com|*.googleapis.com +# which prevents the JVM from routing Google Maven traffic through the proxy. +if [ -n "${JAVA_TOOL_OPTIONS:-}" ]; then + # Remove the nonProxyHosts entries for google.com and googleapis.com from the JVM options + # Strategy: rewrite the -Dhttp.nonProxyHosts value to exclude google patterns + JAVA_TOOL_OPTIONS=$(echo "$JAVA_TOOL_OPTIONS" | sed -E ' + s/-Dhttp\.nonProxyHosts=[^ ]*/\n&\n/g + ' | while IFS= read -r line; do + if [[ "$line" == -Dhttp.nonProxyHosts=* ]]; then + # Extract the value, remove google patterns, reconstruct + val="${line#-Dhttp.nonProxyHosts=}" + fixed=$(echo "$val" | tr '|' '\n' | grep -v -E '^\*\.google\.com$|^\*\.googleapis\.com$' | tr '\n' '|' | sed 's/|$//') + echo "-Dhttp.nonProxyHosts=$fixed" + else + echo "$line" + fi + done | tr '\n' ' ' | sed 's/ */ /g; s/^ //; s/ $//') + export JAVA_TOOL_OPTIONS +fi + +# Export fixed env vars for the session +if [ -n "${CLAUDE_ENV_FILE:-}" ]; then + echo "export no_proxy='${no_proxy:-}'" >> "$CLAUDE_ENV_FILE" + echo "export NO_PROXY='${NO_PROXY:-}'" >> "$CLAUDE_ENV_FILE" + echo "export JAVA_TOOL_OPTIONS='$JAVA_TOOL_OPTIONS'" >> "$CLAUDE_ENV_FILE" +fi + +echo "Fixed proxy bypass settings (no_proxy, JAVA_TOOL_OPTIONS)" >&2 + # --- Proxy credentials: configure Maven/Gradle if authenticated proxy is set --- proxy="${https_proxy:-${HTTPS_PROXY:-}}" if [ -n "$proxy" ] && echo "$proxy" | grep -q '@'; then @@ -99,68 +146,11 @@ if [ -n "$ANTHROPIC_CA_PEM" ]; then rm -f "$TMPCA" fi +# --- Project setup: local.properties and env vars (before SDK download so these always run) --- ANDROID_SDK_DIR="/root/android-sdk" -SDK_REPO_BASE="https://dl.google.com/android/repository" - -# Install Android SDK packages by downloading directly with curl -# (sdkmanager cannot reach the SDK repository through the proxy) -install_sdk_package() { - local zip_url="$1" - local dest_dir="$2" - local inner_dir="$3" # top-level dir inside the zip - - if [ -d "$dest_dir" ]; then - return 0 - fi - - echo "Downloading $zip_url..." - local TMP_ZIP - TMP_ZIP=$(mktemp /tmp/sdk-pkg.XXXXXX.zip) - curl -fsSL "$zip_url" -o "$TMP_ZIP" - - local TMP_DIR - TMP_DIR=$(mktemp -d) - unzip -q "$TMP_ZIP" -d "$TMP_DIR" - rm -f "$TMP_ZIP" - - mkdir -p "$(dirname "$dest_dir")" - mv "$TMP_DIR/$inner_dir" "$dest_dir" - rm -rf "$TMP_DIR" - echo "Installed to $dest_dir" -} - -# Install Android platform 36 -install_sdk_package \ - "$SDK_REPO_BASE/platform-36_r02.zip" \ - "$ANDROID_SDK_DIR/platforms/android-36" \ - "android-36" - -# Install build-tools 36.0.0 (zip uses "android-16" as inner dir name) -install_sdk_package \ - "$SDK_REPO_BASE/build-tools_r36_linux.zip" \ - "$ANDROID_SDK_DIR/build-tools/36.0.0" \ - "android-16" - -# Install platform-tools -install_sdk_package \ - "$SDK_REPO_BASE/platform-tools_r37.0.0-linux.zip" \ - "$ANDROID_SDK_DIR/platform-tools" \ - "platform-tools" - -# Accept SDK licenses (create license files manually) -echo "Writing SDK license files..." -mkdir -p "$ANDROID_SDK_DIR/licenses" -# android-sdk-license -echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_SDK_DIR/licenses/android-sdk-license" -echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" >> "$ANDROID_SDK_DIR/licenses/android-sdk-license" -# android-sdk-preview-license -echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" -echo -e "\n504667f4c0de7af1a06de9f4b1727b84351f2910" >> "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" -# intel-android-extra-license -echo -e "\nd975f751698a77b662f1254ddbeed3901e976f5a" > "$ANDROID_SDK_DIR/licenses/intel-android-extra-license" +REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-/home/user/amethyst}")" # Create local.properties if missing -REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-/home/user/Amber}")" LOCAL_PROPS="$REPO_ROOT/local.properties" if [ ! -f "$LOCAL_PROPS" ]; then echo "sdk.dir=$ANDROID_SDK_DIR" > "$LOCAL_PROPS" @@ -174,7 +164,123 @@ if [ -n "${CLAUDE_ENV_FILE:-}" ]; then echo "export PATH=\$PATH:$ANDROID_SDK_DIR/platform-tools" >> "$CLAUDE_ENV_FILE" fi -cd "$CLAUDE_PROJECT_DIR" -./gradlew --version > /dev/null 2>&1 +# --- Install standalone ktlint (fallback for spotlessApply when Gradle can't resolve AGP) --- +# The egress proxy may block dl.google.com (Google Maven), preventing Gradle from resolving +# the Android Gradle Plugin. Since spotlessApply depends on Gradle's plugin resolution, +# we install ktlint standalone as a reliable alternative. +KTLINT_VERSION="1.5.0" +KTLINT_BIN="/usr/local/bin/ktlint" +if [ ! -x "$KTLINT_BIN" ]; then + echo "Installing ktlint $KTLINT_VERSION..." >&2 + if curl -fsSL "https://github.com/pinterest/ktlint/releases/download/${KTLINT_VERSION}/ktlint" -o "$KTLINT_BIN" 2>/dev/null; then + chmod +x "$KTLINT_BIN" + echo "Installed ktlint $KTLINT_VERSION to $KTLINT_BIN" >&2 + else + echo "WARNING: Failed to install ktlint (GitHub may be unreachable)" >&2 + fi +fi -echo "Android SDK setup complete." \ No newline at end of file +# Create a spotless-apply wrapper that mimics the Gradle spotless configuration +cat > /usr/local/bin/spotless-apply << 'SPOTLESS_SCRIPT' +#!/bin/bash +# Standalone spotless-apply: runs ktlint with the same settings as the Gradle spotless plugin. +# Use this when ./gradlew spotlessApply fails due to proxy/network issues. +set -euo pipefail + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "$REPO_ROOT" + +if ! command -v ktlint &>/dev/null; then + echo "ERROR: ktlint not found. Install it first." >&2 + exit 1 +fi + +echo "Running ktlint format on Kotlin sources..." +# Find all Kotlin source files matching the spotless target pattern (src/**/*.kt) +# Exclude build directories +ktlint --format "**/*.kt" 2>&1 | grep -v "^$" || true + +echo "Done. Files formatted with ktlint." +SPOTLESS_SCRIPT +chmod +x /usr/local/bin/spotless-apply + +# --- Android SDK download (non-fatal: warns if dl.google.com is blocked) --- +SDK_REPO_BASE="https://dl.google.com/android/repository" + +install_sdk_package() { + local zip_url="$1" + local dest_dir="$2" + local inner_dir="$3" # top-level dir inside the zip + + if [ -d "$dest_dir" ]; then + return 0 + fi + + echo "Downloading $zip_url..." >&2 + local TMP_ZIP + TMP_ZIP=$(mktemp /tmp/sdk-pkg.XXXXXX.zip) + if ! curl -fsSL "$zip_url" -o "$TMP_ZIP" 2>/dev/null; then + rm -f "$TMP_ZIP" + echo "WARNING: Failed to download $zip_url (dl.google.com may be blocked by proxy)" >&2 + echo " -> Add dl.google.com to the egress proxy allowlist to fix this" >&2 + return 1 + fi + + local TMP_DIR + TMP_DIR=$(mktemp -d) + unzip -q "$TMP_ZIP" -d "$TMP_DIR" + rm -f "$TMP_ZIP" + + mkdir -p "$(dirname "$dest_dir")" + mv "$TMP_DIR/$inner_dir" "$dest_dir" + rm -rf "$TMP_DIR" + echo "Installed to $dest_dir" >&2 +} + +sdk_ok=true + +# Install Android platform 36 +if ! install_sdk_package \ + "$SDK_REPO_BASE/platform-36_r02.zip" \ + "$ANDROID_SDK_DIR/platforms/android-36" \ + "android-36"; then + sdk_ok=false +fi + +# Install build-tools 36.0.0 (zip uses "android-16" as inner dir name) +if ! install_sdk_package \ + "$SDK_REPO_BASE/build-tools_r36_linux.zip" \ + "$ANDROID_SDK_DIR/build-tools/36.0.0" \ + "android-16"; then + sdk_ok=false +fi + +# Install platform-tools +if ! install_sdk_package \ + "$SDK_REPO_BASE/platform-tools_r37.0.0-linux.zip" \ + "$ANDROID_SDK_DIR/platform-tools" \ + "platform-tools"; then + sdk_ok=false +fi + +if [ "$sdk_ok" = true ]; then + # Accept SDK licenses (create license files manually) + echo "Writing SDK license files..." >&2 + mkdir -p "$ANDROID_SDK_DIR/licenses" + echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_SDK_DIR/licenses/android-sdk-license" + echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" >> "$ANDROID_SDK_DIR/licenses/android-sdk-license" + echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" + echo -e "\n504667f4c0de7af1a06de9f4b1727b84351f2910" >> "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" + echo -e "\nd975f751698a77b662f1254ddbeed3901e976f5a" > "$ANDROID_SDK_DIR/licenses/intel-android-extra-license" + echo "Android SDK setup complete." >&2 +else + echo "WARNING: Android SDK download failed. dl.google.com is likely blocked by the egress proxy." >&2 + echo " -> Gradle builds requiring AGP will fail until dl.google.com is added to the proxy allowlist." >&2 + echo " -> Use 'spotless-apply' (standalone ktlint) instead of './gradlew spotlessApply'." >&2 +fi + +# Warm up Gradle wrapper (non-fatal) +cd "$REPO_ROOT" +./gradlew --version > /dev/null 2>&1 || true + +echo "Session start hook complete." >&2 diff --git a/.claude/settings.json b/.claude/settings.json index 80496cbba..4ab4d1726 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -16,7 +16,7 @@ "hooks": [ { "type": "command", - "command": "./gradlew spotlessApply", + "command": "./gradlew spotlessApply 2>/dev/null || spotless-apply", "timeout": 120 } ] From 883a2f995e22dc35cb848c390065be1cf09cf2a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 03:43:19 +0000 Subject: [PATCH 06/13] style: apply ktlint formatting fixes https://claude.ai/code/session_01HeWiRdKDTnMGfEBUegk3d1 --- .../amethyst/ui/components/ShareHelper.kt | 11 ++++++----- .../ui/screen/loggedIn/chess/ChessLobbyScreen.kt | 6 ++++-- .../loggedIn/hashtag/dal/HashtagFeedFilter.kt | 3 ++- .../screen/loggedIn/home/ShortNotePostViewModel.kt | 3 ++- .../gallery/dal/UserProfileGalleryFeedFilter.kt | 13 +++++++------ .../amethyst/desktop/chess/ChessScreen.kt | 7 +++++-- .../quartz/nip01Core/store/sqlite/QueryBuilder.kt | 3 ++- .../namecoin/NamecoinNameResolver.kt | 3 ++- .../quartz/nip64Chess/ChessStateReconstructor.kt | 9 ++++++--- .../quartz/nip64Chess/ChessEngine.jvmAndroid.kt | 3 ++- .../nip05/namecoin/NamecoinNameResolverTest.kt | 3 ++- 11 files changed, 40 insertions(+), 24 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ShareHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ShareHelper.kt index 32aeaa679..90bad3c7f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ShareHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ShareHelper.kt @@ -119,11 +119,12 @@ object ShareHelper { bytesRead >= 12 && matchesMagicNumbers(header, 4, MOV_FTYP) -> detectMp4OrMov(header) // MP4/MOV alternative: moov, mdat, or free at offset 4 - bytesRead >= 8 && ( - matchesMagicNumbers(header, 4, MOV_MOOV) || - matchesMagicNumbers(header, 4, MOV_MDAT) || - matchesMagicNumbers(header, 4, MOV_FREE) - ) -> "mp4" + bytesRead >= 8 && + ( + matchesMagicNumbers(header, 4, MOV_MOOV) || + matchesMagicNumbers(header, 4, MOV_MDAT) || + matchesMagicNumbers(header, 4, MOV_FREE) + ) -> "mp4" else -> defaultExtension } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt index ad9ad0e22..96a2502eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt @@ -297,8 +297,10 @@ fun ChessLobbyContent( val userPubkey = accountViewModel.account.userProfile().pubkeyHex val hasContent = - activeGames.isNotEmpty() || spectatingGames.isNotEmpty() || - publicGames.isNotEmpty() || challenges.isNotEmpty() + activeGames.isNotEmpty() || + spectatingGames.isNotEmpty() || + publicGames.isNotEmpty() || + challenges.isNotEmpty() if (!hasContent) { // Empty state - use LazyColumn so pull-to-refresh works diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt index 9a95a0e46..e9c468e94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt @@ -83,7 +83,8 @@ class HashtagFeedFilter( event is PrivateDmEvent || event is PollNoteEvent || event is AudioHeaderEvent - ) && event.isTaggedHash(hashTag) + ) && + event.isTaggedHash(hashTag) fun acceptableViaScope( event: Event?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 4b63c9df4..87fd5939a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -1047,7 +1047,8 @@ open class ShortNotePostViewModel : ( !wantsPoll || ( - pollOptions.isNotEmpty() && pollOptions.all { it.value.label.isNotEmpty() } && + pollOptions.isNotEmpty() && + pollOptions.all { it.value.label.isNotEmpty() } && closedAt > TimeUtils.oneMinuteFromNow() ) ) && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt index 0a8f6f31d..0e943641d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt @@ -77,12 +77,13 @@ class UserProfileGalleryFeedFilter( val noteEvent = it.event return ( ( - it.event?.pubKey == user.pubkeyHex && ( - noteEvent is PictureEvent || - noteEvent is RegularVideoEvent || - (noteEvent is ReplaceableVideoEvent && it is AddressableNote) || - (noteEvent is ProfileGalleryEntryEvent && noteEvent.hasUrl() && noteEvent.hasFromEvent()) - ) + it.event?.pubKey == user.pubkeyHex && + ( + noteEvent is PictureEvent || + noteEvent is RegularVideoEvent || + (noteEvent is ReplaceableVideoEvent && it is AddressableNote) || + (noteEvent is ProfileGalleryEntryEvent && noteEvent.hasUrl() && noteEvent.hasFromEvent()) + ) ) // && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) ) && params.match(noteEvent, it.relays) && diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt index efc578373..2668756b8 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt @@ -345,8 +345,11 @@ private fun ChessLobby( listState: LazyListState = rememberLazyListState(), ) { val hasContent = - activeGames.isNotEmpty() || spectatingGames.isNotEmpty() || - publicGames.isNotEmpty() || challenges.isNotEmpty() || completedGames.isNotEmpty() + activeGames.isNotEmpty() || + spectatingGames.isNotEmpty() || + publicGames.isNotEmpty() || + challenges.isNotEmpty() || + completedGames.isNotEmpty() if (!hasContent) { // Empty state diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt index c876c7b96..8ab91cfae 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt @@ -678,7 +678,8 @@ class QueryBuilder( val search: String? = null, ) { fun isSimpleSearch() = - search != null && search.isNotEmpty() && + search != null && + search.isNotEmpty() && (nonDTagsIn == null || nonDTagsIn.isEmpty()) && (nonDTagsAll == null || nonDTagsAll.isEmpty()) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/NamecoinNameResolver.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/NamecoinNameResolver.kt index 72abf7546..4d4a44aa8 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/NamecoinNameResolver.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/NamecoinNameResolver.kt @@ -301,7 +301,8 @@ class NamecoinNameResolver( pubkey = rootMatch.content } - firstEntry != null && firstEntry.value is JsonPrimitive && + firstEntry != null && + firstEntry.value is JsonPrimitive && isValidPubkey((firstEntry.value as JsonPrimitive).content) -> { resolvedLocalPart = firstEntry.key pubkey = (firstEntry.value as JsonPrimitive).content diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessStateReconstructor.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessStateReconstructor.kt index 997e9921c..4dd69fbd4 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessStateReconstructor.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessStateReconstructor.kt @@ -217,9 +217,12 @@ object ChessStateReconstructor { return fen1 == fen2 // Fallback to exact match } - return parts1[0] == parts2[0] && // Board position - parts1[1] == parts2[1] && // Active color - parts1[2] == parts2[2] && // Castling rights + return parts1[0] == parts2[0] && + // Board position + parts1[1] == parts2[1] && + // Active color + parts1[2] == parts2[2] && + // Castling rights parts1[3] == parts2[3] // En passant } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessEngine.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessEngine.jvmAndroid.kt index d9b192d9d..dbd0bbb27 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessEngine.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessEngine.jvmAndroid.kt @@ -212,7 +212,8 @@ actual class ChessEngine { board.getPiece(toSquare) != Piece.NONE || ( pt == com.github.bhlangonijr.chesslib.PieceType.PAWN && - epTarget != Square.NONE && toSquare == epTarget + epTarget != Square.NONE && + toSquare == epTarget ) if (pt != com.github.bhlangonijr.chesslib.PieceType.PAWN) { diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip05/namecoin/NamecoinNameResolverTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip05/namecoin/NamecoinNameResolverTest.kt index 52822c49c..0c975e7c3 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip05/namecoin/NamecoinNameResolverTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip05/namecoin/NamecoinNameResolverTest.kt @@ -261,7 +261,8 @@ class NamecoinNameResolverTest { pubkey = rootMatch.content } - firstEntry != null && firstEntry.value is kotlinx.serialization.json.JsonPrimitive && + firstEntry != null && + firstEntry.value is kotlinx.serialization.json.JsonPrimitive && (firstEntry.value as kotlinx.serialization.json.JsonPrimitive) .content .matches(Regex("^[0-9a-fA-F]{64}$")) -> { From 6bc927147c5e592e7140e77bd127647341f477e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 03:47:47 +0000 Subject: [PATCH 07/13] fix: use nightly.link for direct APK download without auth GitHub artifact URLs require authentication and don't provide direct file downloads. nightly.link proxies artifact downloads, giving a direct download link that works for anyone without GitHub login. https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf --- .github/workflows/build-benchmark-apk.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml index 54db24f5e..beaddc41b 100644 --- a/.github/workflows/build-benchmark-apk.yml +++ b/.github/workflows/build-benchmark-apk.yml @@ -48,8 +48,9 @@ jobs: with: script: | const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const artifactUrl = `${{ steps.upload.outputs.artifact-url }}`; - const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${artifactUrl})\n\nOr view all artifacts in the [workflow run](${runUrl}#artifacts).`; + const artifactName = encodeURIComponent('Play Benchmark APK'); + const nightlyUrl = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/${artifactName}.zip`; + const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${nightlyUrl})\n\nOr view all artifacts in the [workflow run](${runUrl}#artifacts).`; await github.rest.repos.createCommitComment({ owner: context.repo.owner, From 61dc903e9a5cc21bb011dc573108301d47cea47a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 04:03:18 +0000 Subject: [PATCH 08/13] fix: use artifact ID for direct download URL instead of nightly.link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the artifact-id output from upload-artifact to construct a direct GitHub artifact download URL. No third-party service needed — the URL points to the artifact on GitHub which triggers a download when clicked. https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf --- .github/workflows/build-benchmark-apk.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml index beaddc41b..1f19c0e67 100644 --- a/.github/workflows/build-benchmark-apk.yml +++ b/.github/workflows/build-benchmark-apk.yml @@ -47,10 +47,10 @@ jobs: uses: actions/github-script@v7 with: script: | + const artifactId = `${{ steps.upload.outputs.artifact-id }}`; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const artifactName = encodeURIComponent('Play Benchmark APK'); - const nightlyUrl = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/${artifactName}.zip`; - const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${nightlyUrl})\n\nOr view all artifacts in the [workflow run](${runUrl}#artifacts).`; + const downloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/artifacts/${artifactId}`; + const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${downloadUrl})\n\nOr view all artifacts in the [workflow run](${runUrl}#artifacts).`; await github.rest.repos.createCommitComment({ owner: context.repo.owner, From 706d282681198321ed817e117b53dcaab7dd54aa Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 04:05:33 +0000 Subject: [PATCH 09/13] fix: direct APK download via prerelease asset, PR comment only - Upload APK as a GitHub prerelease asset for direct .apk download (no zip wrapping like artifacts) - Remove commit comment, only comment on open PRs - Clean up existing release with same tag before creating https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf --- .github/workflows/build-benchmark-apk.yml | 69 ++++++++++++++++++----- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml index 1f19c0e67..ba9c7ee3c 100644 --- a/.github/workflows/build-benchmark-apk.yml +++ b/.github/workflows/build-benchmark-apk.yml @@ -36,30 +36,69 @@ jobs: - name: Build Benchmark APK run: ./gradlew assemblePlayBenchmark - - name: Upload Play Benchmark APK + - name: Upload APK to prerelease id: upload - uses: actions/upload-artifact@v6 - with: - name: Play Benchmark APK - path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk - - - name: Comment on commit with APK link uses: actions/github-script@v7 with: script: | - const artifactId = `${{ steps.upload.outputs.artifact-id }}`; - const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const downloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/artifacts/${artifactId}`; - const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${downloadUrl})\n\nOr view all artifacts in the [workflow run](${runUrl}#artifacts).`; + const fs = require('fs'); + const apkPath = 'amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk'; + const sha = context.sha.substring(0, 7); + const tag = `benchmark-${sha}`; - await github.rest.repos.createCommitComment({ + // Delete existing release with this tag if it exists + try { + const { data: existing } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag + }); + await github.rest.repos.deleteRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: existing.id + }); + await github.rest.git.deleteRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `tags/${tag}` + }); + } catch (e) { + // Release doesn't exist yet, that's fine + } + + const { data: release } = await github.rest.repos.createRelease({ owner: context.repo.owner, repo: context.repo.repo, - commit_sha: context.sha, - body + tag_name: tag, + target_commitish: context.sha, + name: `Benchmark APK (${sha})`, + body: `Auto-generated benchmark APK for commit ${context.sha}`, + prerelease: true }); - // Find open PRs for this branch and comment on them + const apkData = fs.readFileSync(apkPath); + const { data: asset } = await github.rest.repos.uploadReleaseAsset({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: release.id, + name: 'amethyst-play-universal-benchmark.apk', + data: apkData, + headers: { + 'content-type': 'application/vnd.android.package-archive', + 'content-length': apkData.length + } + }); + + core.setOutput('apk-url', asset.browser_download_url); + + - name: Comment on PR with APK link + uses: actions/github-script@v7 + with: + script: | + const apkUrl = `${{ steps.upload.outputs.apk-url }}`; + const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${apkUrl})`; + const branch = context.ref.replace('refs/heads/', ''); const { data: prs } = await github.rest.pulls.list({ owner: context.repo.owner, From dbf5d6954c73eafb61f847fe70a15a4c6ea41e58 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 04:08:30 +0000 Subject: [PATCH 10/13] fix: revert to artifact zip download, PR comment only Use upload-artifact with artifact-id for direct zip download URL. Remove prerelease approach and commit comments. https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf --- .github/workflows/build-benchmark-apk.yml | 62 +++-------------------- 1 file changed, 7 insertions(+), 55 deletions(-) diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml index ba9c7ee3c..1326569d9 100644 --- a/.github/workflows/build-benchmark-apk.yml +++ b/.github/workflows/build-benchmark-apk.yml @@ -36,68 +36,20 @@ jobs: - name: Build Benchmark APK run: ./gradlew assemblePlayBenchmark - - name: Upload APK to prerelease + - name: Upload Play Benchmark APK id: upload - uses: actions/github-script@v7 + uses: actions/upload-artifact@v6 with: - script: | - const fs = require('fs'); - const apkPath = 'amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk'; - const sha = context.sha.substring(0, 7); - const tag = `benchmark-${sha}`; - - // Delete existing release with this tag if it exists - try { - const { data: existing } = await github.rest.repos.getReleaseByTag({ - owner: context.repo.owner, - repo: context.repo.repo, - tag - }); - await github.rest.repos.deleteRelease({ - owner: context.repo.owner, - repo: context.repo.repo, - release_id: existing.id - }); - await github.rest.git.deleteRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: `tags/${tag}` - }); - } catch (e) { - // Release doesn't exist yet, that's fine - } - - const { data: release } = await github.rest.repos.createRelease({ - owner: context.repo.owner, - repo: context.repo.repo, - tag_name: tag, - target_commitish: context.sha, - name: `Benchmark APK (${sha})`, - body: `Auto-generated benchmark APK for commit ${context.sha}`, - prerelease: true - }); - - const apkData = fs.readFileSync(apkPath); - const { data: asset } = await github.rest.repos.uploadReleaseAsset({ - owner: context.repo.owner, - repo: context.repo.repo, - release_id: release.id, - name: 'amethyst-play-universal-benchmark.apk', - data: apkData, - headers: { - 'content-type': 'application/vnd.android.package-archive', - 'content-length': apkData.length - } - }); - - core.setOutput('apk-url', asset.browser_download_url); + name: Play Benchmark APK + path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk - name: Comment on PR with APK link uses: actions/github-script@v7 with: script: | - const apkUrl = `${{ steps.upload.outputs.apk-url }}`; - const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${apkUrl})`; + const artifactId = `${{ steps.upload.outputs.artifact-id }}`; + const downloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/artifacts/${artifactId}`; + const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${downloadUrl})`; const branch = context.ref.replace('refs/heads/', ''); const { data: prs } = await github.rest.pulls.list({ From 9bf3086230c25f5f5dc19a52e0311e849e31b3f6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 17 Mar 2026 09:25:33 -0400 Subject: [PATCH 11/13] Make the session-start executable by default --- .claude/hooks/session-start.sh | 0 .github/workflows/build-benchmark-apk.yml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 .claude/hooks/session-start.sh diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh old mode 100644 new mode 100755 diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml index 1326569d9..0a65ca6c1 100644 --- a/.github/workflows/build-benchmark-apk.yml +++ b/.github/workflows/build-benchmark-apk.yml @@ -1,4 +1,4 @@ -name: Build Benchmark APK +name: Build APK For Claude on: push: From baa55ab7f9c271dead14c88597f13be37bdd64a2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 17 Mar 2026 09:55:56 -0400 Subject: [PATCH 12/13] reverts to amber's session start --- .claude/hooks/session-start.sh | 232 +++++++++------------------------ 1 file changed, 63 insertions(+), 169 deletions(-) diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index b17f6e97b..35cddfb3b 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -1,59 +1,12 @@ #!/bin/bash # Session start hook: Configure proxy auth, SSL trust, and Android SDK for Claude Code on the web +set -euo pipefail # Only run in remote (web) environments if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then exit 0 fi -# --- Fix no_proxy: remove *.google.com and *.googleapis.com so traffic routes through proxy --- -# The default no_proxy in Claude Code web includes *.google.com and *.googleapis.com, -# which causes JVM/curl to bypass the proxy for Google Maven (dl.google.com). -# These domains MUST go through the proxy for authenticated egress to work. -fix_no_proxy() { - local var="$1" - local val="${!var:-}" - if [ -n "$val" ]; then - # Remove *.google.com, *.googleapis.com entries from the comma-separated list - local fixed - fixed=$(echo "$val" | tr ',' '\n' | grep -v -E '^\*\.google\.com$|^\*\.googleapis\.com$' | tr '\n' ',' | sed 's/,$//') - export "$var=$fixed" - fi -} - -fix_no_proxy no_proxy -fix_no_proxy NO_PROXY - -# --- Fix JAVA_TOOL_OPTIONS: remove nonProxyHosts that bypass Google domains --- -# JAVA_TOOL_OPTIONS may contain -Dhttp.nonProxyHosts=...|*.google.com|*.googleapis.com -# which prevents the JVM from routing Google Maven traffic through the proxy. -if [ -n "${JAVA_TOOL_OPTIONS:-}" ]; then - # Remove the nonProxyHosts entries for google.com and googleapis.com from the JVM options - # Strategy: rewrite the -Dhttp.nonProxyHosts value to exclude google patterns - JAVA_TOOL_OPTIONS=$(echo "$JAVA_TOOL_OPTIONS" | sed -E ' - s/-Dhttp\.nonProxyHosts=[^ ]*/\n&\n/g - ' | while IFS= read -r line; do - if [[ "$line" == -Dhttp.nonProxyHosts=* ]]; then - # Extract the value, remove google patterns, reconstruct - val="${line#-Dhttp.nonProxyHosts=}" - fixed=$(echo "$val" | tr '|' '\n' | grep -v -E '^\*\.google\.com$|^\*\.googleapis\.com$' | tr '\n' '|' | sed 's/|$//') - echo "-Dhttp.nonProxyHosts=$fixed" - else - echo "$line" - fi - done | tr '\n' ' ' | sed 's/ */ /g; s/^ //; s/ $//') - export JAVA_TOOL_OPTIONS -fi - -# Export fixed env vars for the session -if [ -n "${CLAUDE_ENV_FILE:-}" ]; then - echo "export no_proxy='${no_proxy:-}'" >> "$CLAUDE_ENV_FILE" - echo "export NO_PROXY='${NO_PROXY:-}'" >> "$CLAUDE_ENV_FILE" - echo "export JAVA_TOOL_OPTIONS='$JAVA_TOOL_OPTIONS'" >> "$CLAUDE_ENV_FILE" -fi - -echo "Fixed proxy bypass settings (no_proxy, JAVA_TOOL_OPTIONS)" >&2 - # --- Proxy credentials: configure Maven/Gradle if authenticated proxy is set --- proxy="${https_proxy:-${HTTPS_PROXY:-}}" if [ -n "$proxy" ] && echo "$proxy" | grep -q '@'; then @@ -146,11 +99,68 @@ if [ -n "$ANTHROPIC_CA_PEM" ]; then rm -f "$TMPCA" fi -# --- Project setup: local.properties and env vars (before SDK download so these always run) --- ANDROID_SDK_DIR="/root/android-sdk" -REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-/home/user/amethyst}")" +SDK_REPO_BASE="https://dl.google.com/android/repository" + +# Install Android SDK packages by downloading directly with curl +# (sdkmanager cannot reach the SDK repository through the proxy) +install_sdk_package() { + local zip_url="$1" + local dest_dir="$2" + local inner_dir="$3" # top-level dir inside the zip + + if [ -d "$dest_dir" ]; then + return 0 + fi + + echo "Downloading $zip_url..." + local TMP_ZIP + TMP_ZIP=$(mktemp /tmp/sdk-pkg.XXXXXX.zip) + curl -fsSL "$zip_url" -o "$TMP_ZIP" + + local TMP_DIR + TMP_DIR=$(mktemp -d) + unzip -q "$TMP_ZIP" -d "$TMP_DIR" + rm -f "$TMP_ZIP" + + mkdir -p "$(dirname "$dest_dir")" + mv "$TMP_DIR/$inner_dir" "$dest_dir" + rm -rf "$TMP_DIR" + echo "Installed to $dest_dir" +} + +# Install Android platform 36 +install_sdk_package \ + "$SDK_REPO_BASE/platform-36_r02.zip" \ + "$ANDROID_SDK_DIR/platforms/android-36" \ + "android-36" + +# Install build-tools 36.0.0 (zip uses "android-16" as inner dir name) +install_sdk_package \ + "$SDK_REPO_BASE/build-tools_r36_linux.zip" \ + "$ANDROID_SDK_DIR/build-tools/36.0.0" \ + "android-16" + +# Install platform-tools +install_sdk_package \ + "$SDK_REPO_BASE/platform-tools_r37.0.0-linux.zip" \ + "$ANDROID_SDK_DIR/platform-tools" \ + "platform-tools" + +# Accept SDK licenses (create license files manually) +echo "Writing SDK license files..." +mkdir -p "$ANDROID_SDK_DIR/licenses" +# android-sdk-license +echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_SDK_DIR/licenses/android-sdk-license" +echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" >> "$ANDROID_SDK_DIR/licenses/android-sdk-license" +# android-sdk-preview-license +echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" +echo -e "\n504667f4c0de7af1a06de9f4b1727b84351f2910" >> "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" +# intel-android-extra-license +echo -e "\nd975f751698a77b662f1254ddbeed3901e976f5a" > "$ANDROID_SDK_DIR/licenses/intel-android-extra-license" # Create local.properties if missing +REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-/home/user/Amber}")" LOCAL_PROPS="$REPO_ROOT/local.properties" if [ ! -f "$LOCAL_PROPS" ]; then echo "sdk.dir=$ANDROID_SDK_DIR" > "$LOCAL_PROPS" @@ -164,123 +174,7 @@ if [ -n "${CLAUDE_ENV_FILE:-}" ]; then echo "export PATH=\$PATH:$ANDROID_SDK_DIR/platform-tools" >> "$CLAUDE_ENV_FILE" fi -# --- Install standalone ktlint (fallback for spotlessApply when Gradle can't resolve AGP) --- -# The egress proxy may block dl.google.com (Google Maven), preventing Gradle from resolving -# the Android Gradle Plugin. Since spotlessApply depends on Gradle's plugin resolution, -# we install ktlint standalone as a reliable alternative. -KTLINT_VERSION="1.5.0" -KTLINT_BIN="/usr/local/bin/ktlint" -if [ ! -x "$KTLINT_BIN" ]; then - echo "Installing ktlint $KTLINT_VERSION..." >&2 - if curl -fsSL "https://github.com/pinterest/ktlint/releases/download/${KTLINT_VERSION}/ktlint" -o "$KTLINT_BIN" 2>/dev/null; then - chmod +x "$KTLINT_BIN" - echo "Installed ktlint $KTLINT_VERSION to $KTLINT_BIN" >&2 - else - echo "WARNING: Failed to install ktlint (GitHub may be unreachable)" >&2 - fi -fi +cd "$CLAUDE_PROJECT_DIR" +./gradlew --version > /dev/null 2>&1 -# Create a spotless-apply wrapper that mimics the Gradle spotless configuration -cat > /usr/local/bin/spotless-apply << 'SPOTLESS_SCRIPT' -#!/bin/bash -# Standalone spotless-apply: runs ktlint with the same settings as the Gradle spotless plugin. -# Use this when ./gradlew spotlessApply fails due to proxy/network issues. -set -euo pipefail - -REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" -cd "$REPO_ROOT" - -if ! command -v ktlint &>/dev/null; then - echo "ERROR: ktlint not found. Install it first." >&2 - exit 1 -fi - -echo "Running ktlint format on Kotlin sources..." -# Find all Kotlin source files matching the spotless target pattern (src/**/*.kt) -# Exclude build directories -ktlint --format "**/*.kt" 2>&1 | grep -v "^$" || true - -echo "Done. Files formatted with ktlint." -SPOTLESS_SCRIPT -chmod +x /usr/local/bin/spotless-apply - -# --- Android SDK download (non-fatal: warns if dl.google.com is blocked) --- -SDK_REPO_BASE="https://dl.google.com/android/repository" - -install_sdk_package() { - local zip_url="$1" - local dest_dir="$2" - local inner_dir="$3" # top-level dir inside the zip - - if [ -d "$dest_dir" ]; then - return 0 - fi - - echo "Downloading $zip_url..." >&2 - local TMP_ZIP - TMP_ZIP=$(mktemp /tmp/sdk-pkg.XXXXXX.zip) - if ! curl -fsSL "$zip_url" -o "$TMP_ZIP" 2>/dev/null; then - rm -f "$TMP_ZIP" - echo "WARNING: Failed to download $zip_url (dl.google.com may be blocked by proxy)" >&2 - echo " -> Add dl.google.com to the egress proxy allowlist to fix this" >&2 - return 1 - fi - - local TMP_DIR - TMP_DIR=$(mktemp -d) - unzip -q "$TMP_ZIP" -d "$TMP_DIR" - rm -f "$TMP_ZIP" - - mkdir -p "$(dirname "$dest_dir")" - mv "$TMP_DIR/$inner_dir" "$dest_dir" - rm -rf "$TMP_DIR" - echo "Installed to $dest_dir" >&2 -} - -sdk_ok=true - -# Install Android platform 36 -if ! install_sdk_package \ - "$SDK_REPO_BASE/platform-36_r02.zip" \ - "$ANDROID_SDK_DIR/platforms/android-36" \ - "android-36"; then - sdk_ok=false -fi - -# Install build-tools 36.0.0 (zip uses "android-16" as inner dir name) -if ! install_sdk_package \ - "$SDK_REPO_BASE/build-tools_r36_linux.zip" \ - "$ANDROID_SDK_DIR/build-tools/36.0.0" \ - "android-16"; then - sdk_ok=false -fi - -# Install platform-tools -if ! install_sdk_package \ - "$SDK_REPO_BASE/platform-tools_r37.0.0-linux.zip" \ - "$ANDROID_SDK_DIR/platform-tools" \ - "platform-tools"; then - sdk_ok=false -fi - -if [ "$sdk_ok" = true ]; then - # Accept SDK licenses (create license files manually) - echo "Writing SDK license files..." >&2 - mkdir -p "$ANDROID_SDK_DIR/licenses" - echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_SDK_DIR/licenses/android-sdk-license" - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" >> "$ANDROID_SDK_DIR/licenses/android-sdk-license" - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" - echo -e "\n504667f4c0de7af1a06de9f4b1727b84351f2910" >> "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" - echo -e "\nd975f751698a77b662f1254ddbeed3901e976f5a" > "$ANDROID_SDK_DIR/licenses/intel-android-extra-license" - echo "Android SDK setup complete." >&2 -else - echo "WARNING: Android SDK download failed. dl.google.com is likely blocked by the egress proxy." >&2 - echo " -> Gradle builds requiring AGP will fail until dl.google.com is added to the proxy allowlist." >&2 - echo " -> Use 'spotless-apply' (standalone ktlint) instead of './gradlew spotlessApply'." >&2 -fi - -# Warm up Gradle wrapper (non-fatal) -cd "$REPO_ROOT" -./gradlew --version > /dev/null 2>&1 || true - -echo "Session start hook complete." >&2 +echo "Android SDK setup complete." \ No newline at end of file From 4770f1eb90bc527c30d3fe3d44209d5697b1d5c9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 17 Mar 2026 11:43:24 -0400 Subject: [PATCH 13/13] Fixes sending url without cipher. --- .../privateDM/send/upload/ChatFileSender.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt index 7ec7bd245..9164a505b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt @@ -24,11 +24,14 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments +import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning +import com.vitorpamplona.quartz.nip92IMeta.imetas import com.vitorpamplona.quartz.utils.ciphers.AESGCM class ChatFileSender( @@ -39,6 +42,8 @@ class ChatFileSender( uploads.forEach { if (it.cipher != null) { sendNIP17(it.result, it.caption, it.contentWarningReason, it.cipher) + } else { + sendNIP17AsHiddenLink(it.result, it.caption, it.contentWarningReason) } } } @@ -70,6 +75,31 @@ class ChatFileSender( ) } + suspend fun sendNIP17AsHiddenLink( + result: UploadOrchestrator.OrchestratorResult.ServerResult, + caption: String?, + contentWarningReason: String?, + ) { + val iMetaAttachments = IMetaAttachments() + iMetaAttachments.add(result, caption, contentWarningReason) + + val toUsers = chatroom.users.map { LocalCache.getOrCreateUser(it).toPTag() } + + val template = + ChatMessageEvent.build(result.url, toUsers) { + references(listOf(result.url)) + + if (!caption.isNullOrEmpty()) { + alt(caption) + } + contentWarningReason?.let { contentWarning(it) } + + imetas(iMetaAttachments.filterIsIn(setOf(result.url))) + } + + account.sendNip17PrivateMessage(template) + } + // ------ // NIP 04 // ------