Merge branch 'main' into claude/auto-load-transactions-scroll-KC8of
This commit is contained in:
Regular → Executable
@@ -16,7 +16,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "./gradlew spotlessApply",
|
"command": "./gradlew spotlessApply 2>/dev/null || spotless-apply",
|
||||||
"timeout": 120
|
"timeout": 120
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Build Benchmark APK
|
name: Build APK For Claude
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -43,14 +43,27 @@ jobs:
|
|||||||
name: Play Benchmark APK
|
name: Play Benchmark APK
|
||||||
path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk
|
path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk
|
||||||
|
|
||||||
- name: Comment on commit with APK link
|
- name: Comment on PR with APK link
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
const artifactId = `${{ steps.upload.outputs.artifact-id }}`;
|
||||||
await github.rest.repos.createCommitComment({
|
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({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
commit_sha: context.sha,
|
head: `${context.repo.owner}:${branch}`,
|
||||||
body: `📦 **Benchmark APK ready!**\n\nDownload it from the [workflow run artifacts](${runUrl}#artifacts).`
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -119,11 +119,12 @@ object ShareHelper {
|
|||||||
bytesRead >= 12 && matchesMagicNumbers(header, 4, MOV_FTYP) -> detectMp4OrMov(header)
|
bytesRead >= 12 && matchesMagicNumbers(header, 4, MOV_FTYP) -> detectMp4OrMov(header)
|
||||||
|
|
||||||
// MP4/MOV alternative: moov, mdat, or free at offset 4
|
// MP4/MOV alternative: moov, mdat, or free at offset 4
|
||||||
bytesRead >= 8 && (
|
bytesRead >= 8 &&
|
||||||
matchesMagicNumbers(header, 4, MOV_MOOV) ||
|
(
|
||||||
matchesMagicNumbers(header, 4, MOV_MDAT) ||
|
matchesMagicNumbers(header, 4, MOV_MOOV) ||
|
||||||
matchesMagicNumbers(header, 4, MOV_FREE)
|
matchesMagicNumbers(header, 4, MOV_MDAT) ||
|
||||||
) -> "mp4"
|
matchesMagicNumbers(header, 4, MOV_FREE)
|
||||||
|
) -> "mp4"
|
||||||
|
|
||||||
else -> defaultExtension
|
else -> defaultExtension
|
||||||
}
|
}
|
||||||
|
|||||||
+88
-2
@@ -403,7 +403,16 @@ class ChatNewMessageViewModel :
|
|||||||
|
|
||||||
accountViewModel.launchSigner {
|
accountViewModel.launchSigner {
|
||||||
if (nip17) {
|
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
|
uploadsWaitingToBeSent += it
|
||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
onceUploaded()
|
onceUploaded()
|
||||||
@@ -428,7 +437,19 @@ class ChatNewMessageViewModel :
|
|||||||
|
|
||||||
accountViewModel.launchSigner {
|
accountViewModel.launchSigner {
|
||||||
if (nip17) {
|
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)
|
ChatFileSender(room, account).sendNIP17(it)
|
||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
onceUploaded()
|
onceUploaded()
|
||||||
@@ -443,6 +464,69 @@ class ChatNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encrypted upload error state for retry dialog
|
||||||
|
var encryptedUploadErrorTitle by mutableStateOf<String?>(null)
|
||||||
|
var encryptedUploadErrorMessage by mutableStateOf<String?>(null)
|
||||||
|
var pendingRetryMode by mutableStateOf<RetryMode?>(null)
|
||||||
|
var pendingRetryOnError by mutableStateOf<((String, String) -> Unit)?>(null)
|
||||||
|
var pendingRetryContext by mutableStateOf<Context?>(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?) {
|
private suspend fun innerSendPost(draftTag: String?) {
|
||||||
val room = room ?: return
|
val room = room ?: return
|
||||||
|
|
||||||
@@ -563,6 +647,8 @@ class ChatNewMessageViewModel :
|
|||||||
uploadsWaitingToBeSent = emptyList()
|
uploadsWaitingToBeSent = emptyList()
|
||||||
uploadState?.reset()
|
uploadState?.reset()
|
||||||
|
|
||||||
|
dismissEncryptedUploadError()
|
||||||
|
|
||||||
iMetaAttachments.reset()
|
iMetaAttachments.reset()
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
|
|||||||
+9
@@ -302,6 +302,15 @@ fun GroupDMScreenContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
postViewModel.encryptedUploadErrorTitle?.let { title ->
|
||||||
|
EncryptedUploadErrorDialog(
|
||||||
|
title = title,
|
||||||
|
message = postViewModel.encryptedUploadErrorMessage ?: "",
|
||||||
|
onDismiss = postViewModel::dismissEncryptedUploadError,
|
||||||
|
onRetryWithoutEncryption = postViewModel::retryWithoutEncryption,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
@@ -27,8 +27,10 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -116,6 +118,15 @@ fun PrivateMessageEditFieldRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
channelScreenModel.encryptedUploadErrorTitle?.let { title ->
|
||||||
|
EncryptedUploadErrorDialog(
|
||||||
|
title = title,
|
||||||
|
message = channelScreenModel.encryptedUploadErrorMessage ?: "",
|
||||||
|
onDismiss = channelScreenModel::dismissEncryptedUploadError,
|
||||||
|
onRetryWithoutEncryption = channelScreenModel::retryWithoutEncryption,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = EditFieldModifier,
|
modifier = EditFieldModifier,
|
||||||
) {
|
) {
|
||||||
@@ -217,3 +228,36 @@ fun KeyboardLeadingIcon(
|
|||||||
ToggleNip17Button(channelScreenModel, accountViewModel)
|
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))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+30
@@ -24,11 +24,14 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
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.nip04Dm.messages.PrivateDmEvent
|
||||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||||
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||||
|
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||||
|
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||||
import com.vitorpamplona.quartz.utils.ciphers.AESGCM
|
import com.vitorpamplona.quartz.utils.ciphers.AESGCM
|
||||||
|
|
||||||
class ChatFileSender(
|
class ChatFileSender(
|
||||||
@@ -39,6 +42,8 @@ class ChatFileSender(
|
|||||||
uploads.forEach {
|
uploads.forEach {
|
||||||
if (it.cipher != null) {
|
if (it.cipher != null) {
|
||||||
sendNIP17(it.result, it.caption, it.contentWarningReason, it.cipher)
|
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
|
// NIP 04
|
||||||
// ------
|
// ------
|
||||||
|
|||||||
+1
@@ -74,5 +74,6 @@ fun RoomChatFileUploadDialog(
|
|||||||
onCancel,
|
onCancel,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
nav,
|
nav,
|
||||||
|
isNip17 = channelScreenModel.nip17,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-4
@@ -39,20 +39,68 @@ class ChatFileUploader(
|
|||||||
suspend fun justUploadNIP17(
|
suspend fun justUploadNIP17(
|
||||||
viewState: ChatFileUploadState,
|
viewState: ChatFileUploadState,
|
||||||
onError: (title: String, message: String) -> Unit,
|
onError: (title: String, message: String) -> Unit,
|
||||||
|
onEncryptedUploadError: (title: String, message: String) -> Unit,
|
||||||
context: Context,
|
context: Context,
|
||||||
onceUploaded: suspend (List<SuccessfulUploads>) -> Unit,
|
onceUploaded: suspend (List<SuccessfulUploads>) -> Unit,
|
||||||
) {
|
) {
|
||||||
val orchestrator = viewState.multiOrchestrator ?: return
|
val orchestrator = viewState.multiOrchestrator ?: return
|
||||||
viewState.mediaUploadTracker.startUpload(orchestrator.hasNonMedia())
|
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<SuccessfulUploads>) -> Unit,
|
||||||
|
) {
|
||||||
|
val orchestrator = viewState.multiOrchestrator ?: return
|
||||||
|
viewState.mediaUploadTracker.startUpload(orchestrator.hasNonMedia())
|
||||||
|
|
||||||
val results =
|
val results =
|
||||||
orchestrator.uploadEncrypted(
|
orchestrator.upload(
|
||||||
viewState.caption,
|
viewState.caption,
|
||||||
viewState.contentWarningReason,
|
viewState.contentWarningReason,
|
||||||
MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider),
|
MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider),
|
||||||
cipher,
|
|
||||||
viewState.selectedServer,
|
viewState.selectedServer,
|
||||||
account,
|
account,
|
||||||
context,
|
context,
|
||||||
@@ -62,7 +110,7 @@ class ChatFileUploader(
|
|||||||
val list =
|
val list =
|
||||||
results.successful.mapNotNull { state ->
|
results.successful.mapNotNull { state ->
|
||||||
if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||||
SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, cipher)
|
SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, null)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -81,6 +81,7 @@ fun ChatFileUploadDialog(
|
|||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
|
isNip17: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ fun ChatFileUploadDialog(
|
|||||||
) {
|
) {
|
||||||
Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp, bottom = 10.dp)) {
|
Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp, bottom = 10.dp)) {
|
||||||
Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) {
|
Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) {
|
||||||
ImageVideoPostChat(state, accountViewModel)
|
ImageVideoPostChat(state, accountViewModel, isNip17)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,6 +145,7 @@ fun ChatFileUploadDialog(
|
|||||||
private fun ImageVideoPostChat(
|
private fun ImageVideoPostChat(
|
||||||
fileUploadState: ChatFileUploadState,
|
fileUploadState: ChatFileUploadState,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
|
isNip17: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val fileServers by accountViewModel.account.blossomServers.hostNameFlow
|
val fileServers by accountViewModel.account.blossomServers.hostNameFlow
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
@@ -190,6 +192,16 @@ private fun ImageVideoPostChat(
|
|||||||
onCheckedChange = fileUploadState::updateContentWarning,
|
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) {
|
SettingsRow(R.string.file_server, R.string.file_server_description) {
|
||||||
TextSpinner(
|
TextSpinner(
|
||||||
label = "",
|
label = "",
|
||||||
|
|||||||
+3
@@ -55,6 +55,8 @@ class ChatFileUploadState(
|
|||||||
// 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED
|
// 0 = Low, 1 = Medium, 2 = High, 3=UNCOMPRESSED
|
||||||
var mediaQualitySlider by mutableIntStateOf(1)
|
var mediaQualitySlider by mutableIntStateOf(1)
|
||||||
|
|
||||||
|
var encryptFiles by mutableStateOf(true)
|
||||||
|
|
||||||
fun load(uris: ImmutableList<SelectedMedia>) {
|
fun load(uris: ImmutableList<SelectedMedia>) {
|
||||||
reset()
|
reset()
|
||||||
this.multiOrchestrator = MultiOrchestrator(uris)
|
this.multiOrchestrator = MultiOrchestrator(uris)
|
||||||
@@ -70,6 +72,7 @@ class ChatFileUploadState(
|
|||||||
mediaUploadTracker.finishUpload()
|
mediaUploadTracker.finishUpload()
|
||||||
caption = ""
|
caption = ""
|
||||||
selectedServer = defaultServer
|
selectedServer = defaultServer
|
||||||
|
encryptFiles = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteMediaToUpload(selected: SelectedMediaProcessing) {
|
fun deleteMediaToUpload(selected: SelectedMediaProcessing) {
|
||||||
|
|||||||
+4
-2
@@ -297,8 +297,10 @@ fun ChessLobbyContent(
|
|||||||
val userPubkey = accountViewModel.account.userProfile().pubkeyHex
|
val userPubkey = accountViewModel.account.userProfile().pubkeyHex
|
||||||
|
|
||||||
val hasContent =
|
val hasContent =
|
||||||
activeGames.isNotEmpty() || spectatingGames.isNotEmpty() ||
|
activeGames.isNotEmpty() ||
|
||||||
publicGames.isNotEmpty() || challenges.isNotEmpty()
|
spectatingGames.isNotEmpty() ||
|
||||||
|
publicGames.isNotEmpty() ||
|
||||||
|
challenges.isNotEmpty()
|
||||||
|
|
||||||
if (!hasContent) {
|
if (!hasContent) {
|
||||||
// Empty state - use LazyColumn so pull-to-refresh works
|
// Empty state - use LazyColumn so pull-to-refresh works
|
||||||
|
|||||||
+2
-1
@@ -83,7 +83,8 @@ class HashtagFeedFilter(
|
|||||||
event is PrivateDmEvent ||
|
event is PrivateDmEvent ||
|
||||||
event is PollNoteEvent ||
|
event is PollNoteEvent ||
|
||||||
event is AudioHeaderEvent
|
event is AudioHeaderEvent
|
||||||
) && event.isTaggedHash(hashTag)
|
) &&
|
||||||
|
event.isTaggedHash(hashTag)
|
||||||
|
|
||||||
fun acceptableViaScope(
|
fun acceptableViaScope(
|
||||||
event: Event?,
|
event: Event?,
|
||||||
|
|||||||
+2
-1
@@ -1047,7 +1047,8 @@ open class ShortNotePostViewModel :
|
|||||||
(
|
(
|
||||||
!wantsPoll ||
|
!wantsPoll ||
|
||||||
(
|
(
|
||||||
pollOptions.isNotEmpty() && pollOptions.all { it.value.label.isNotEmpty() } &&
|
pollOptions.isNotEmpty() &&
|
||||||
|
pollOptions.all { it.value.label.isNotEmpty() } &&
|
||||||
closedAt > TimeUtils.oneMinuteFromNow()
|
closedAt > TimeUtils.oneMinuteFromNow()
|
||||||
)
|
)
|
||||||
) &&
|
) &&
|
||||||
|
|||||||
+7
-6
@@ -77,12 +77,13 @@ class UserProfileGalleryFeedFilter(
|
|||||||
val noteEvent = it.event
|
val noteEvent = it.event
|
||||||
return (
|
return (
|
||||||
(
|
(
|
||||||
it.event?.pubKey == user.pubkeyHex && (
|
it.event?.pubKey == user.pubkeyHex &&
|
||||||
noteEvent is PictureEvent ||
|
(
|
||||||
noteEvent is RegularVideoEvent ||
|
noteEvent is PictureEvent ||
|
||||||
(noteEvent is ReplaceableVideoEvent && it is AddressableNote) ||
|
noteEvent is RegularVideoEvent ||
|
||||||
(noteEvent is ProfileGalleryEntryEvent && noteEvent.hasUrl() && noteEvent.hasFromEvent())
|
(noteEvent is ReplaceableVideoEvent && it is AddressableNote) ||
|
||||||
)
|
(noteEvent is ProfileGalleryEntryEvent && noteEvent.hasUrl() && noteEvent.hasFromEvent())
|
||||||
|
)
|
||||||
) // && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET))
|
) // && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET))
|
||||||
) &&
|
) &&
|
||||||
params.match(noteEvent, it.relays) &&
|
params.match(noteEvent, it.relays) &&
|
||||||
|
|||||||
@@ -1216,6 +1216,13 @@
|
|||||||
<string name="compression_cancelled">Compression Cancelled</string>
|
<string name="compression_cancelled">Compression Cancelled</string>
|
||||||
<string name="compression_returned_null">Compression failed to return a file</string>
|
<string name="compression_returned_null">Compression failed to return a file</string>
|
||||||
|
|
||||||
|
<string name="encrypt_files_label">Encrypt files</string>
|
||||||
|
<string name="encrypt_files_description">Encrypt files before uploading for privacy. Some servers may not accept encrypted files on free accounts.</string>
|
||||||
|
<string name="failed_to_upload_encrypted_media_title">Encrypted upload failed</string>
|
||||||
|
<string name="failed_to_upload_encrypted_media_message">Many servers do not accept encrypted files on free accounts. You can retry without encryption.</string>
|
||||||
|
<string name="retry_without_encryption">Retry without encryption</string>
|
||||||
|
<string name="upload_without_encryption_warning">Warning: Without encryption, anyone with the file link can see the content.</string>
|
||||||
|
|
||||||
<string name="media_compression_quality_label">Media Quality</string>
|
<string name="media_compression_quality_label">Media Quality</string>
|
||||||
<string name="media_compression_quality_explainer">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.</string>
|
<string name="media_compression_quality_explainer">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.</string>
|
||||||
<string name="media_compression_quality_low">Low</string>
|
<string name="media_compression_quality_low">Low</string>
|
||||||
|
|||||||
+5
-2
@@ -345,8 +345,11 @@ private fun ChessLobby(
|
|||||||
listState: LazyListState = rememberLazyListState(),
|
listState: LazyListState = rememberLazyListState(),
|
||||||
) {
|
) {
|
||||||
val hasContent =
|
val hasContent =
|
||||||
activeGames.isNotEmpty() || spectatingGames.isNotEmpty() ||
|
activeGames.isNotEmpty() ||
|
||||||
publicGames.isNotEmpty() || challenges.isNotEmpty() || completedGames.isNotEmpty()
|
spectatingGames.isNotEmpty() ||
|
||||||
|
publicGames.isNotEmpty() ||
|
||||||
|
challenges.isNotEmpty() ||
|
||||||
|
completedGames.isNotEmpty()
|
||||||
|
|
||||||
if (!hasContent) {
|
if (!hasContent) {
|
||||||
// Empty state
|
// Empty state
|
||||||
|
|||||||
+2
-1
@@ -678,7 +678,8 @@ class QueryBuilder(
|
|||||||
val search: String? = null,
|
val search: String? = null,
|
||||||
) {
|
) {
|
||||||
fun isSimpleSearch() =
|
fun isSimpleSearch() =
|
||||||
search != null && search.isNotEmpty() &&
|
search != null &&
|
||||||
|
search.isNotEmpty() &&
|
||||||
(nonDTagsIn == null || nonDTagsIn.isEmpty()) &&
|
(nonDTagsIn == null || nonDTagsIn.isEmpty()) &&
|
||||||
(nonDTagsAll == null || nonDTagsAll.isEmpty())
|
(nonDTagsAll == null || nonDTagsAll.isEmpty())
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -301,7 +301,8 @@ class NamecoinNameResolver(
|
|||||||
pubkey = rootMatch.content
|
pubkey = rootMatch.content
|
||||||
}
|
}
|
||||||
|
|
||||||
firstEntry != null && firstEntry.value is JsonPrimitive &&
|
firstEntry != null &&
|
||||||
|
firstEntry.value is JsonPrimitive &&
|
||||||
isValidPubkey((firstEntry.value as JsonPrimitive).content) -> {
|
isValidPubkey((firstEntry.value as JsonPrimitive).content) -> {
|
||||||
resolvedLocalPart = firstEntry.key
|
resolvedLocalPart = firstEntry.key
|
||||||
pubkey = (firstEntry.value as JsonPrimitive).content
|
pubkey = (firstEntry.value as JsonPrimitive).content
|
||||||
|
|||||||
+6
-3
@@ -217,9 +217,12 @@ object ChessStateReconstructor {
|
|||||||
return fen1 == fen2 // Fallback to exact match
|
return fen1 == fen2 // Fallback to exact match
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts1[0] == parts2[0] && // Board position
|
return parts1[0] == parts2[0] &&
|
||||||
parts1[1] == parts2[1] && // Active color
|
// Board position
|
||||||
parts1[2] == parts2[2] && // Castling rights
|
parts1[1] == parts2[1] &&
|
||||||
|
// Active color
|
||||||
|
parts1[2] == parts2[2] &&
|
||||||
|
// Castling rights
|
||||||
parts1[3] == parts2[3] // En passant
|
parts1[3] == parts2[3] // En passant
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -212,7 +212,8 @@ actual class ChessEngine {
|
|||||||
board.getPiece(toSquare) != Piece.NONE ||
|
board.getPiece(toSquare) != Piece.NONE ||
|
||||||
(
|
(
|
||||||
pt == com.github.bhlangonijr.chesslib.PieceType.PAWN &&
|
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) {
|
if (pt != com.github.bhlangonijr.chesslib.PieceType.PAWN) {
|
||||||
|
|||||||
+2
-1
@@ -261,7 +261,8 @@ class NamecoinNameResolverTest {
|
|||||||
pubkey = rootMatch.content
|
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)
|
(firstEntry.value as kotlinx.serialization.json.JsonPrimitive)
|
||||||
.content
|
.content
|
||||||
.matches(Regex("^[0-9a-fA-F]{64}$")) -> {
|
.matches(Regex("^[0-9a-fA-F]{64}$")) -> {
|
||||||
|
|||||||
Reference in New Issue
Block a user