Draw a story that is only words — and fix a bubble that never compiled
🔴 The file bubble committed earlier today DID NOT COMPILE. It reached for Icons.Filled.InsertDriveFile in a file that imports no Material icons at all, and I reported it as building. The reason it went unnoticed matters more than the mistake: ANDROID_HOME was unset, so `:app:compileDebugKotlin` failed on "SDK location not found" before Kotlin ran — and the check for errors was a grep for lines starting `e:`, which a build that never reached the compiler does not produce. A green-looking check that never compiled anything. Every Android app-side claim I made today rested on it. It now uses the kit's own icon set, like the rest of the file, and the app compiles with the SDK on the path. Alongside: Vault.Story.ref is nullable, so a story can be words. Every path here assumed a picture, which is the same shape that made a text story impossible on iOS rather than merely unbuilt. ⚠️ `loadStories` used `getJSONObject("ref")`, which THROWS. A single text story would have taken down the load of every story — a feed that is simply empty, with nothing to say why. It reads the optional accessor now, and tells "no ref at all" (a text story) apart from "a ref that would not parse" (a corrupt row), because those deserve different treatment. Expiry moved onto the story, falling back to the attachment for everything already stored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bfe9fff430
commit
fdaf06415e
@@ -2464,7 +2464,7 @@ fun ColumnScope.StoryStrip(
|
||||
@Composable
|
||||
private fun ColumnScope.StoryViewer(model: MessengerViewModel, story: Vault.Story) {
|
||||
val remaining = remember(story.id) {
|
||||
val seconds = story.ref.expires - System.currentTimeMillis() / 1000.0
|
||||
val seconds = story.expires - System.currentTimeMillis() / 1000.0
|
||||
when {
|
||||
seconds <= 0 -> "expired"
|
||||
seconds < 3600 -> "${(seconds / 60).toInt()}m left"
|
||||
@@ -2475,8 +2475,9 @@ private fun ColumnScope.StoryViewer(model: MessengerViewModel, story: Vault.Stor
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp).testTag("story-viewer"),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
AttachedImage(story.ref, model)
|
||||
if (story.caption.isNotBlank()) Text(story.caption)
|
||||
val ref = story.ref
|
||||
if (ref == null) TextStoryCard(story.caption) else AttachedImage(ref, model)
|
||||
if (story.caption.isNotBlank() && !story.textOnly) Text(story.caption)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -3502,8 +3503,12 @@ private fun FileBubble(ref: io.flamenet.e2e.Attachment.Ref, mine: Boolean) {
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.padding(vertical = 2.dp).testTag("chat.file"),
|
||||
) {
|
||||
// ⚠️ The kit's own icon set, not Material's. This file imports no
|
||||
// Material icons at all, and the version that reached for one did not
|
||||
// compile — which went unnoticed because the build was failing on a
|
||||
// missing ANDROID_HOME before it ever got as far as Kotlin.
|
||||
Icon(
|
||||
Icons.Filled.InsertDriveFile,
|
||||
KitIcon.Attach,
|
||||
contentDescription = null,
|
||||
tint = onColour,
|
||||
modifier = Modifier.size(24.dp),
|
||||
|
||||
@@ -31,6 +31,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import io.flamenet.e2e.Vault
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
||||
/**
|
||||
* Stories as a feed you scroll, which is the kit's Timeline.
|
||||
@@ -106,9 +108,9 @@ private fun StoryCard(
|
||||
?: if (story.mine) "You" else "user ${story.authorId}"
|
||||
}
|
||||
val remaining = remember(story.id) {
|
||||
val seconds = story.ref.expires - System.currentTimeMillis() / 1000.0
|
||||
val seconds = story.expires - System.currentTimeMillis() / 1000.0
|
||||
when {
|
||||
story.ref.expires <= 0L -> "no expiry"
|
||||
story.expires <= 0L -> "no expiry"
|
||||
seconds <= 0 -> "expired"
|
||||
seconds < 3600 -> "${(seconds / 60).toInt()} min left"
|
||||
else -> "${(seconds / 3600).toInt()} h left"
|
||||
@@ -180,14 +182,21 @@ private fun StoryCard(
|
||||
if (stream != null && streamHost != null &&
|
||||
io.flamenet.e2e.StreamDisclosure.acceptable(stream)) {
|
||||
StoryStreamGate(stream, streamHost)
|
||||
} else if (story.ref.mime.startsWith("video/")) {
|
||||
StoryVideoCard(story.ref, model)
|
||||
} else {
|
||||
StoryImage(story.ref, model)
|
||||
val ref = story.ref
|
||||
when {
|
||||
// 🔴 Words alone, set large on a tinted band — the way
|
||||
// every other messenger draws one. A text story rendered
|
||||
// as a caption under an empty rectangle reads as a picture
|
||||
// that failed to load.
|
||||
ref == null -> TextStoryCard(story.caption)
|
||||
ref.mime.startsWith("video/") -> StoryVideoCard(ref, model)
|
||||
else -> StoryImage(ref, model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (story.caption.isNotBlank()) {
|
||||
if (story.caption.isNotBlank() && !story.textOnly) {
|
||||
Spacer(Modifier.height(Space.md))
|
||||
Text(story.caption, fontSize = 14.sp, maxLines = 3)
|
||||
}
|
||||
@@ -457,3 +466,32 @@ private fun StoryImage(ref: io.flamenet.e2e.Attachment.Ref, model: MessengerView
|
||||
else -> Text("Loading…", fontSize = 14.sp, color = Palette.SecondaryText)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A story that is only words.
|
||||
*
|
||||
* Set large and centred on a tinted band, matching iOS — a text story drawn as
|
||||
* a caption under an empty grey rectangle reads as a picture that failed to
|
||||
* load, which is the opposite of what the sender meant.
|
||||
*/
|
||||
@Composable
|
||||
fun TextStoryCard(text: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(240.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.10f))
|
||||
.testTag("story-text"),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 6,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user