import LChatCore import SwiftUI // One chat row's content — extracted so the UIKit list (ThreadListCollection) can // render it inside a UICollectionViewListCell via UIHostingConfiguration. Pure // presentation; the cell owns the highlight/selection chrome. struct ThreadRow: View { let thread: ChatThread var body: some View { HStack(spacing: 12) { // The main list finally shows app identity: Eleven's own threads // wear the brand tile, apps their mark (docs/icons.md), and the // emoji glyph stays the fallback for anything unmapped. Avatar(name: thread.title, size: 44, source: thread.avatar, photo: thread.isEleven ? ElevenBrand.icon : nil, glyph: thread.glyph, symbol: AppIdentity.symbol(for: thread.appId), mark: AppIdentity.mark(for: thread.appId)) VStack(alignment: .leading, spacing: 2) { HStack { Text(thread.title).font(.headline).lineLimit(1) if let badge = thread.badge { AgentBadge(badge) } Spacer() HStack(spacing: 4) { if thread.pinned { Image(systemName: "pin.fill") .font(.caption2) .foregroundStyle(.secondary) .accessibilityLabel("Pinned") } if thread.lastAt > 0 { Text(relTime(thread.lastAt)).font(.caption).foregroundStyle(.secondary) } } } // Always rendered, preview or not: two reserved lines keep // every row the same height (à la Mail/Messages — a list of // mixed heights read as jumbled next to app rows with no // recent message). The web keeps its 2-line clamp. HStack(spacing: 6) { Text(thread.preview) .font(.subheadline).foregroundStyle(.secondary) .lineLimit(2, reservesSpace: true) Spacer() // A muted chat never escalates: the slashed bell replaces // the unread dot — the state stays visible (never absence // alone), it just doesn't shout. Web-list parity. if thread.isMuted { Image(systemName: "bell.slash.fill") .font(.caption) .foregroundStyle(.secondary) .accessibilityLabel("Muted") } else if thread.unread { Circle().fill(Color.accentColor).frame(width: 9, height: 9) } } } } } } // relTime moved to LChatCore (Format) — the Mac thread list uses it too.