From 453bdab2022d2ec2449649dfde82fc4c4a19b920 Mon Sep 17 00:00:00 2001 From: davotoula Date: Tue, 14 Apr 2026 21:57:55 +0200 Subject: [PATCH] refactor(hls): replace auto kind-1 cross-post with editable draft handoff The auto-publish behaviour from the previous commit silently sent a kind-1 note the user could not edit before it hit the relays. Replace it with a handoff into Amethyst's existing NewShortNote composer pre-filled with the title, description and master playlist URL so the author can tweak the text, add hashtags/mentions, and send when ready. Changes: - HlsPublishOrchestrator drops the signAndPublishNote callback and stops publishing any kind-1 itself. HlsPublishState.Success drops the noteEventId field. - HlsPublishRequest drops crossPostAsNote (the orchestrator no longer cares about the toggle). - NewHlsVideoViewModel renames crossPostAsNote -> draftNoteAfterUpload and the screen renames the switch to "Draft note after upload". - SuccessBody now reads vm.draftNoteAfterUpload: when on, it offers a "Draft note" primary button that navigates to Route.NewShortNote with the message parameter filled via a small buildDraftNoteText helper (title + description + masterUrl, blanks collapsed). The existing short-note composer accepts message as its initial text. When off, the button reverts to "Done". - Production factory wiring drops the TextNoteEvent import + closure. - Orchestrator tests drop the two cross-post cases; a single signAndPublish callback is now enough. Co-Authored-By: Claude Opus 4.5 --- .../video/hls/HlsPublishOrchestrator.kt | 20 ------ .../hls/HlsPublishOrchestratorFactory.kt | 6 -- .../loggedIn/video/hls/HlsPublishState.kt | 1 - .../loggedIn/video/hls/NewHlsVideoScreen.kt | 29 +++++--- .../video/hls/NewHlsVideoViewModel.kt | 3 +- amethyst/src/main/res/values/strings.xml | 5 +- .../uploads/hls/HlsPublishOrchestratorTest.kt | 72 ------------------- 7 files changed, 24 insertions(+), 112 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt index 9c4798e1d..d315a11fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt @@ -41,7 +41,6 @@ data class HlsPublishRequest( val codec: VideoCodec, val server: ServerName, val durationSeconds: Int? = null, - val crossPostAsNote: Boolean = true, ) /** @@ -61,7 +60,6 @@ class HlsPublishOrchestrator( ) -> HlsBundle, private val buildUploader: (ServerName) -> HlsBlobUploader, private val signAndPublish: suspend (HlsVideoEventTemplate) -> String, - private val signAndPublishNote: suspend (content: String) -> String, private val workDirFactory: () -> File, ) { val state: StateFlow = _state @@ -98,19 +96,10 @@ class HlsPublishOrchestrator( ) val eventId = signAndPublish(template) - val noteEventId = - if (request.crossPostAsNote) { - val content = buildCompanionNoteContent(request, uploadResult.masterUrl) - signAndPublishNote(content) - } else { - null - } - _state.value = HlsPublishState.Success( eventId = eventId, masterUrl = uploadResult.masterUrl, - noteEventId = noteEventId, ) } catch (e: CancellationException) { _state.value = HlsPublishState.Failure(message = "Cancelled") @@ -125,13 +114,4 @@ class HlsPublishOrchestrator( } private fun contentWarningOrNull(request: HlsPublishRequest): String? = if (request.sensitiveContent) request.contentWarningReason else null - - private fun buildCompanionNoteContent( - request: HlsPublishRequest, - masterUrl: String, - ): String = - listOf(request.title, request.description, masterUrl) - .map { it.trim() } - .filter { it.isNotEmpty() } - .joinToString("\n\n") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestratorFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestratorFactory.kt index 2810c7aab..659a124c0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestratorFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestratorFactory.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.uploads.hls.HlsBlobUploaderFactory import com.vitorpamplona.amethyst.service.uploads.hls.HlsTranscoder import com.vitorpamplona.amethyst.service.uploads.hls.HlsVideoEventTemplate -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import kotlinx.coroutines.flow.MutableStateFlow import java.io.File @@ -68,11 +67,6 @@ fun createProductionHlsPublishOrchestrator( account.sendAutomatic(signed) signed.id }, - signAndPublishNote = { content -> - val signed = account.signer.sign(TextNoteEvent.build(content)) - account.sendAutomatic(signed) - signed.id - }, workDirFactory = { File(context.cacheDir, "hls-${System.currentTimeMillis()}").apply { mkdirs() } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishState.kt index 8b782938e..da48161d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishState.kt @@ -38,7 +38,6 @@ sealed class HlsPublishState { data class Success( val eventId: String, val masterUrl: String, - val noteEventId: String? = null, ) : HlsPublishState() data class Failure( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoScreen.kt index c9a2a578b..cb51acc81 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoScreen.kt @@ -321,25 +321,26 @@ private fun FormFields(vm: NewHlsVideoViewModel) { Spacer(Modifier.height(8.dp)) - // Cross-post as kind-1 note toggle + // Draft-a-note-after-upload toggle — opens the existing short-note composer prefilled + // with the title, description and master playlist URL; the user edits and posts. Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { Column(modifier = Modifier.weight(1f)) { Text( - text = stringResource(R.string.hls_cross_post_as_note), + text = stringResource(R.string.hls_draft_note_after_upload), style = MaterialTheme.typography.bodyLarge, ) Text( - text = stringResource(R.string.hls_cross_post_as_note_explainer), + text = stringResource(R.string.hls_draft_note_after_upload_explainer), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } Switch( - checked = vm.crossPostAsNote, - onCheckedChange = { vm.crossPostAsNote = it }, + checked = vm.draftNoteAfterUpload, + onCheckedChange = { vm.draftNoteAfterUpload = it }, ) } @@ -613,16 +614,16 @@ private fun SuccessBody( ) Spacer(Modifier.height(16.dp)) - val noteId = state.noteEventId - if (noteId != null) { + if (vm.draftNoteAfterUpload) { Button( onClick = { + val draft = buildDraftNoteText(vm.title, vm.description, state.masterUrl) vm.reset() - nav.nav(Route.Note(noteId)) + nav.nav(Route.NewShortNote(message = draft)) }, modifier = Modifier.fillMaxWidth(), ) { - Text(stringResource(R.string.hls_view_note)) + Text(stringResource(R.string.hls_draft_note_button)) } Spacer(Modifier.height(8.dp)) OutlinedButton( @@ -648,6 +649,16 @@ private fun SuccessBody( } } +private fun buildDraftNoteText( + title: String, + description: String, + masterUrl: String, +): String = + listOf(title, description, masterUrl) + .map { it.trim() } + .filter { it.isNotEmpty() } + .joinToString("\n\n") + @Composable private fun FailureBody( vm: NewHlsVideoViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoViewModel.kt index 72db7b442..c7816c86f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoViewModel.kt @@ -63,7 +63,7 @@ open class NewHlsVideoViewModel : ViewModel() { var sensitiveContent by mutableStateOf(false) var contentWarningReason by mutableStateOf("") var useH265 by mutableStateOf(true) - var crossPostAsNote by mutableStateOf(true) + var draftNoteAfterUpload by mutableStateOf(true) var selectedServer by mutableStateOf(null) private val _state = MutableStateFlow(HlsPublishState.Idle) @@ -147,7 +147,6 @@ open class NewHlsVideoViewModel : ViewModel() { codec = codec, server = server, durationSeconds = sourceMetadata?.durationSeconds, - crossPostAsNote = crossPostAsNote, ) currentJob = diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 837b8b0d1..99c580a52 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2112,8 +2112,9 @@ View note Done Try again - Cross-post as note - Also publish a regular Nostr note linking to the video so it shows in the home feed. + Draft note after upload + Open the note composer pre-filled with the title, description and video link so you can tweak it before posting. + Draft note Pack Actions List Actions diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsPublishOrchestratorTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsPublishOrchestratorTest.kt index 74c93d5fc..fbce8d2c3 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsPublishOrchestratorTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsPublishOrchestratorTest.kt @@ -98,7 +98,6 @@ class HlsPublishOrchestratorTest { description: String = "A test clip", sensitive: Boolean = false, warningReason: String = "", - crossPostAsNote: Boolean = false, ) = HlsPublishRequest( title = title, description = description, @@ -106,7 +105,6 @@ class HlsPublishOrchestratorTest { contentWarningReason = warningReason, codec = VideoCodec.H265, server = server, - crossPostAsNote = crossPostAsNote, ) @Test @@ -121,7 +119,6 @@ class HlsPublishOrchestratorTest { publishedTemplates += tpl "signed-event-id" }, - signAndPublishNote = { "note-id" }, workDirFactory = { File(workDir, "work").apply { mkdirs() } }, ) @@ -164,7 +161,6 @@ class HlsPublishOrchestratorTest { capturedDuringPublish += orchestrator.state.value "event-id" }, - signAndPublishNote = { "note-id" }, workDirFactory = { File(workDir, "work").apply { mkdirs() } }, ) @@ -188,7 +184,6 @@ class HlsPublishOrchestratorTest { runTranscode = { _, _, _ -> throw RuntimeException("decode failed") }, buildUploader = { CannedUploader() }, signAndPublish = { "never" }, - signAndPublishNote = { "note-id" }, workDirFactory = { File(workDir, "work").apply { mkdirs() } }, ) @@ -209,7 +204,6 @@ class HlsPublishOrchestratorTest { HlsBlobUploader { _, _ -> throw RuntimeException("server 500") } }, signAndPublish = { "never" }, - signAndPublishNote = { "note-id" }, workDirFactory = { File(workDir, "work").apply { mkdirs() } }, ) @@ -228,7 +222,6 @@ class HlsPublishOrchestratorTest { runTranscode = { _, _, _ -> fakeBundle() }, buildUploader = { CannedUploader() }, signAndPublish = { throw RuntimeException("relay rejected") }, - signAndPublishNote = { "note-id" }, workDirFactory = { File(workDir, "work").apply { mkdirs() } }, ) @@ -251,7 +244,6 @@ class HlsPublishOrchestratorTest { captured += tpl "event-id" }, - signAndPublishNote = { "note-id" }, workDirFactory = { File(workDir, "work").apply { mkdirs() } }, ) @@ -290,7 +282,6 @@ class HlsPublishOrchestratorTest { captured += tpl "event-id" }, - signAndPublishNote = { "note-id" }, workDirFactory = { File(workDir, "work").apply { mkdirs() } }, ) @@ -299,68 +290,6 @@ class HlsPublishOrchestratorTest { assertTrue(captured.single() is HlsVideoEventTemplate.Vertical) } - @Test - fun crossPostAsNoteInvokesSignAndPublishNoteWithTitleDescriptionAndMasterUrl() { - val capturedNoteContent = mutableListOf() - val orchestrator = - HlsPublishOrchestrator( - _state = MutableStateFlow(HlsPublishState.Idle), - runTranscode = { _, _, _ -> fakeBundle() }, - buildUploader = { CannedUploader() }, - signAndPublish = { "video-event-id" }, - signAndPublishNote = { content -> - capturedNoteContent += content - "note-event-id" - }, - workDirFactory = { File(workDir, "work").apply { mkdirs() } }, - ) - - runBlocking { - orchestrator.publish( - newRequest( - title = "Sunset", - description = "Golden hour", - crossPostAsNote = true, - ), - ) - } - - assertEquals(1, capturedNoteContent.size) - val note = capturedNoteContent[0] - assertTrue("note missing title: $note", note.contains("Sunset")) - assertTrue("note missing description: $note", note.contains("Golden hour")) - assertTrue("note missing master url: $note", note.contains("https://cdn.test/")) - - val final = orchestrator.state.value as HlsPublishState.Success - assertEquals("note-event-id", final.noteEventId) - assertEquals("video-event-id", final.eventId) - } - - @Test - fun crossPostDisabledLeavesNoteEventIdNull() { - val noteCallbackInvocations = mutableListOf() - val orchestrator = - HlsPublishOrchestrator( - _state = MutableStateFlow(HlsPublishState.Idle), - runTranscode = { _, _, _ -> fakeBundle() }, - buildUploader = { CannedUploader() }, - signAndPublish = { "video-event-id" }, - signAndPublishNote = { - noteCallbackInvocations += it - "should-not-be-used" - }, - workDirFactory = { File(workDir, "work").apply { mkdirs() } }, - ) - - runBlocking { - orchestrator.publish(newRequest(crossPostAsNote = false)) - } - - assertEquals(0, noteCallbackInvocations.size) - val final = orchestrator.state.value as HlsPublishState.Success - assertEquals(null, final.noteEventId) - } - @Test fun resetRestoresIdleState() { val orchestrator = @@ -369,7 +298,6 @@ class HlsPublishOrchestratorTest { runTranscode = { _, _, _ -> throw RuntimeException("boom") }, buildUploader = { CannedUploader() }, signAndPublish = { "never" }, - signAndPublishNote = { "note-id" }, workDirFactory = { File(workDir, "work").apply { mkdirs() } }, )