Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/ios/CovenCave/CovenCave/Views/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ struct ChatView: View {
onReply: { beginReply($0) },
operatorName: app.operatorDisplayName,
operatorAvatarURL: app.operatorAvatarURL)
.equatable()
.id(message.id)
// New bubbles settle in with a soft rise-and-fade
// (native Messages behaviour) instead of popping;
Expand Down
26 changes: 26 additions & 0 deletions apps/ios/CovenCave/CovenCave/Views/MessageBubble.swift
Original file line number Diff line number Diff line change
Expand Up @@ -458,3 +458,29 @@ struct SuggestionPills: View {
.padding(.top, 6)
}
}

// Skip re-rendering a bubble when its render-affecting inputs are unchanged.
// ChatView's composer keeps `draft` in the same view body as the message list,
// so every keystroke invalidates ChatView.body and would otherwise re-diff
// every bubble (each hosts a WKWebView) — the cause of slow typing on long
// threads. Closures are excluded (they capture fresh state each render but
// don't change what's drawn); optional-action closures are compared by presence
// since that toggles which buttons appear. Environment values and the complete
// familiar model are included because they directly control colours, markdown
// styling, and the familiar attribution row.
extension MessageBubble: Equatable {
static func == (lhs: MessageBubble, rhs: MessageBubble) -> Bool {
lhs.message == rhs.message
&& lhs.isGroup == rhs.isGroup
&& lhs.familiar == rhs.familiar
&& lhs.isLast == rhs.isLast
&& lhs.operatorName == rhs.operatorName
&& lhs.operatorAvatarURL == rhs.operatorAvatarURL
&& lhs.colorScheme == rhs.colorScheme
&& lhs.chrome == rhs.chrome
&& (lhs.onRetry == nil) == (rhs.onRetry == nil)
&& (lhs.onReply == nil) == (rhs.onReply == nil)
&& (lhs.onOpenReader == nil) == (rhs.onOpenReader == nil)
&& (lhs.onForward == nil) == (rhs.onForward == nil)
}
}
59 changes: 59 additions & 0 deletions scripts/ios-message-bubble-equatable.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const chatView = fs.readFileSync(
path.join(root, "apps/ios/CovenCave/CovenCave/Views/ChatView.swift"),
"utf8",
);
const messageBubble = fs.readFileSync(
path.join(root, "apps/ios/CovenCave/CovenCave/Views/MessageBubble.swift"),
"utf8",
);
const runner = fs.readFileSync(path.join(root, "scripts/run-tests.mjs"), "utf8");

assert.match(
chatView,
/MessageBubble\([\s\S]*?\)\s*\.equatable\(\)\s*\.id\(message\.id\)/,
"ChatView should skip re-rendering bubbles whose render inputs are unchanged",
);

assert.match(
messageBubble,
/extension MessageBubble: Equatable/,
"MessageBubble should define its render-input equality contract",
);

assert.match(
messageBubble,
/lhs\.familiar == rhs\.familiar/,
"familiar presentation changes should invalidate an equatable bubble",
);

assert.match(
messageBubble,
/lhs\.colorScheme == rhs\.colorScheme/,
"light and dark appearance changes should invalidate an equatable bubble",
);

assert.match(
messageBubble,
/lhs\.chrome == rhs\.chrome/,
"theme palette changes should invalidate an equatable bubble",
);

assert.doesNotMatch(
messageBubble,
/lhs\.familiar\?\.id == rhs\.familiar\?\.id/,
"familiar equality must not ignore refreshed names, avatars, or colours",
);

assert.match(
runner,
/"scripts\/ios-message-bubble-equatable\.test\.mjs"/,
"mobile test suite should run the message-bubble equality regression",
);

console.log("ios-message-bubble-equatable.test.mjs: ok");
1 change: 1 addition & 0 deletions scripts/run-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,7 @@ export const SUITES = {
"scripts/ios-markdown-accent.test.mjs",
"scripts/ios-message-forwarding.test.mjs",
"scripts/ios-message-retry.test.mjs",
"scripts/ios-message-bubble-equatable.test.mjs",
"scripts/ios-chat-draft-lag.test.mjs",
"scripts/ios-chat-thread-no-search.test.mjs",
"scripts/ios-development-code-title.test.mjs",
Expand Down
Loading