Understand a file somebody sent, rather than showing a line of text
iOS can now send a document from Files. This build treated `file` as a kind from a newer peer: legible, because a file envelope always carries a caption naming the document, but only as text. The engine now knows the kind, parses the additive `fn`/`cls`/`cmp` keys, and can assemble them — so sending works in both directions, not one. 🔴 The interop test parses an envelope produced by the iOS build rather than by this engine. Two implementations of one format that each only test against themselves agree right up to the day somebody sends a real file, and the symptom is "[unsupported attachment]" with no clue why. It also pins the two rules that keep older peers working: a file envelope is never captionless, and a photo envelope still carries none of the new keys. ⚠️ The bubble shows the classification as a MARKING. It says how the sender had it filed; it does not mean this device was cleared for it, and whoever can read the message already has the key. ⚠️ No "Save to Files" on this side yet — this build has no Files of its own, so the button would be one that cannot work. Showing what arrived is the honest half of parity while the other half is built. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
df00bea388
commit
688a519e0c
@@ -2148,8 +2148,13 @@ private fun Bubble(
|
||||
// to show and a duration that matters; an image has the
|
||||
// reverse. Routing on the kind rather than trying to render
|
||||
// both with one composable.
|
||||
if (ref.kind == io.flamenet.e2e.Attachment.AUDIO) VoiceBubble(ref, model)
|
||||
else AttachedImage(ref, model)
|
||||
when (ref.kind) {
|
||||
io.flamenet.e2e.Attachment.AUDIO -> VoiceBubble(ref, model)
|
||||
// A document, not a picture: AttachedImage would download
|
||||
// a PDF and draw nothing.
|
||||
io.flamenet.e2e.Attachment.FILE -> FileBubble(ref, line.mine)
|
||||
else -> AttachedImage(ref, model)
|
||||
}
|
||||
}
|
||||
if (line.text.isNotEmpty()) {
|
||||
Text(
|
||||
@@ -3474,3 +3479,83 @@ fun ColumnScope.MailboxesSection(
|
||||
tag = "confirm-add-mailbox",
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A document that arrived in a conversation.
|
||||
*
|
||||
* 🔴 **The classification is shown, and it is a marking.** It says how the
|
||||
* sender had it filed — it does not mean this device was cleared for it, and
|
||||
* the row must not imply that. Whoever can read the message already has the
|
||||
* key.
|
||||
*
|
||||
* ⚠️ No "Save to Files" here yet: this build has no Files of its own, so
|
||||
* offering one would be a button that cannot work. It shows what arrived
|
||||
* rather than pretending to more, which is the honest half of parity while the
|
||||
* other half is built.
|
||||
*/
|
||||
@Composable
|
||||
private fun FileBubble(ref: io.flamenet.e2e.Attachment.Ref, mine: Boolean) {
|
||||
val onColour = if (mine) MaterialTheme.colorScheme.onPrimary
|
||||
else MaterialTheme.colorScheme.onSurface
|
||||
Row(
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.padding(vertical = 2.dp).testTag("chat.file"),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.InsertDriveFile,
|
||||
contentDescription = null,
|
||||
tint = onColour,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
ref.filename.ifEmpty { "A file" },
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = onColour,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(
|
||||
android.text.format.Formatter.formatShortFileSize(
|
||||
LocalContext.current, ref.bytes.toLong()),
|
||||
fontSize = 12.sp,
|
||||
color = onColour.copy(alpha = 0.75f),
|
||||
)
|
||||
// Only when there is something to say: an Unclassified file in
|
||||
// no areas needs no chip announcing it.
|
||||
if (ref.classification != 0 || ref.compartments.isNotEmpty()) {
|
||||
Text(
|
||||
classificationName(ref.classification).uppercase(),
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = onColour.copy(alpha = 0.9f),
|
||||
modifier = Modifier
|
||||
.background(
|
||||
onColour.copy(alpha = 0.14f),
|
||||
RoundedCornerShape(4.dp),
|
||||
)
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||
.testTag("chat.file.classification"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ Hard-coded, matching iOS's local vocabulary, because there is no server to
|
||||
* ask on either side any more. An ordinal outside the set is shown as itself
|
||||
* rather than guessed at — inventing "Secret" for a level this deployment calls
|
||||
* something else would be worse than a number.
|
||||
*/
|
||||
private fun classificationName(ordinal: Int): String = when (ordinal) {
|
||||
0 -> "Unclassified"
|
||||
1 -> "Internal"
|
||||
2 -> "Confidential"
|
||||
3 -> "Restricted"
|
||||
else -> "Level $ordinal"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user