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 <noreply@anthropic.com>
This commit is contained in:
-20
@@ -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<HlsPublishState> = _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")
|
||||
}
|
||||
|
||||
-6
@@ -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() }
|
||||
},
|
||||
|
||||
-1
@@ -38,7 +38,6 @@ sealed class HlsPublishState {
|
||||
data class Success(
|
||||
val eventId: String,
|
||||
val masterUrl: String,
|
||||
val noteEventId: String? = null,
|
||||
) : HlsPublishState()
|
||||
|
||||
data class Failure(
|
||||
|
||||
+20
-9
@@ -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,
|
||||
|
||||
+1
-2
@@ -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<ServerName?>(null)
|
||||
|
||||
private val _state = MutableStateFlow<HlsPublishState>(HlsPublishState.Idle)
|
||||
@@ -147,7 +147,6 @@ open class NewHlsVideoViewModel : ViewModel() {
|
||||
codec = codec,
|
||||
server = server,
|
||||
durationSeconds = sourceMetadata?.durationSeconds,
|
||||
crossPostAsNote = crossPostAsNote,
|
||||
)
|
||||
|
||||
currentJob =
|
||||
|
||||
@@ -2112,8 +2112,9 @@
|
||||
<string name="hls_view_note">View note</string>
|
||||
<string name="hls_done">Done</string>
|
||||
<string name="hls_try_again">Try again</string>
|
||||
<string name="hls_cross_post_as_note">Cross-post as note</string>
|
||||
<string name="hls_cross_post_as_note_explainer">Also publish a regular Nostr note linking to the video so it shows in the home feed.</string>
|
||||
<string name="hls_draft_note_after_upload">Draft note after upload</string>
|
||||
<string name="hls_draft_note_after_upload_explainer">Open the note composer pre-filled with the title, description and video link so you can tweak it before posting.</string>
|
||||
<string name="hls_draft_note_button">Draft note</string>
|
||||
|
||||
<string name="pack_actions_dialog_title">Pack Actions</string>
|
||||
<string name="list_actions_dialog_title">List Actions</string>
|
||||
|
||||
-72
@@ -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<String>()
|
||||
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<String>()
|
||||
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() } },
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user