eleven / ocho Public

ios: the row-height cache key counts replies, so a new thread link fits (#1481)

A message row is measured once and cached by content key — process-global
AND persisted across launches — so anything that draws must appear in that
key or the row keeps a stale frame forever.

Two things draw and were missing: replyCount (the root's "N Replies" tally)
and, since #1479, threadReplyCount (the same line on a quoted reply's card).
A thread growing 1 -> 2 adds ~15pt to a row whose key never moved: the
reconfigure notices the change, then height(for:) hands back the pre-link
height and FeedMeasuredLayout lays the row out clipped. Nothing self-corrects
(no estimated self-sizing), and warm() restores the same stale entry on the
next launch, so the row stays wrong until the text, reactions or attachments
change. Deleting a reply left the mirror gap.

The tally half was already broken on main before the card link existed; one
line fixes both. The cache is stamped with the binary, so no migration.


Claude-Session: https://claude.ai/code/session_01EXy6kzmYE9rChy3KgciJvZ
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by codex agent, operated by paul@keymail.dev c04a73f5f3ae856657de95b6669520de38a71308 parent 0d87a74
1 file changed, +7 −0
  • ios/LChat/Views/StockChatFeed.swift +7 −0
diff --git a/ios/LChat/Views/StockChatFeed.swift b/ios/LChat/Views/StockChatFeed.swift
index 2c2ca64..1c4af78 100644
--- a/ios/LChat/Views/StockChatFeed.swift
+++ b/ios/LChat/Views/StockChatFeed.swift
@@ -451,9 +451,16 @@ struct StockChatFeed: UIViewControllerRepresentable {
// so persisted keys silently never matched across real launches.
var textHash: UInt64 = 0xcbf29ce484222325
for b in m.text.utf8 { textHash = (textHash ^ UInt64(b)) &* 0x100000001b3 }
+ // Both counts are in the key because both DRAW: replyCount is the
+ // root's "N Replies" tally, threadReplyCount the same line on a
+ // quoted reply's card. A row whose thread grows 1 -> 2 gains ~15pt
+ // it never had, and this cache outlives the reconfigure that
+ // noticed the change (process-global, and persisted), so without
+ // them the row keeps its pre-link frame and lays out clipped.
return "\(id)|\(m.at)|\(m.edited)|\(textHash)|" +
"\(m.reactions.count)|\(m.attachments.count)|" +
"\(m.reply != nil)|\(m.action)|\(hooks.foldsAsMuted(m))|" +
+ "\(m.replyCount)|\(m.threadReplyCount)|" +
"\(sep)|\(startsRun)|\(width)|\(hooks.chatFontSize)|" +
"\(hooks.themeName)|\(hooks.isGroup)"
}