mirror of
https://github.com/block/buzz.git
synced 2026-11-11 20:01:09 +00:00
The UI-only part of #9186 by @klopez4212, split out so it can land without the Activity sync changes. - The Inbox page becomes Activity, with header controls instead of menus: a type menu (All, Threads, Mentions, DMs, Reminders, Drafts), a sender menu (Anyone, People, Agents) and an Unread toggle. All of them filter the feed the app already loads. - Activity and Search get a top edge fade. Bulk mark-as-read and the "New" divider are gone. - DM thread replies group with their DM row, so the DMs filter includes them. Left out, for a separate sync PR: the Activity history cache, subscriptions to every joined channel, the refresh coalescer, thread-follow evidence, Reactions and Solo rows, and the read-state changes. The Activity provider and read-state writes are unchanged from main. One fix on top: the iOS filter labels pass an unscaled font size, since the native button already applies Dynamic Type. Before, large text was scaled twice. ### Validation - Kenny's added tests for the header controls (`tabs_journey`) and `TopEdgeFade`, adjusted for the removed Reactions option. Added tests for the DM filter, marking a DM row with replies under two thread roots read, and sender filtering per event. - Full mobile suite: see the latest CI run. Locally, 11 push and notification-toggle tests fail identically on an unchanged checkout. - Automated simulator run (iPhone 17 Pro, iOS 27) of a build combining this PR with its sibling split: Home rows measure 44 pt, the bell opens Activity and shows its dot after a row is marked unread, type/sender/Unread filters each narrow the feed, a row opens its conversation, Search results scroll under the fade, and filter labels fit at the largest text size. Screenshots are in Buzz rather than here because they show internal test data. Android was covered only by widget tests. ### Before merge - [x] Automated simulator run of Activity filters, row taps, read and unread, and Search, with screenshots. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@squareup.com> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
5.2 KiB
Swift
129 lines
5.2 KiB
Swift
import UIKit
|
|
import CoreText
|
|
import Flutter
|
|
|
|
/// Renders the same bundled Tabler font used by Flutter into template images.
|
|
/// Legacy symbol keys are kept at the platform-channel boundary only.
|
|
enum NativeTabler {
|
|
private static let fontName: String? = {
|
|
let key = FlutterDartProject.lookupKey(forAsset: "assets/fonts/TablerIcons.ttf")
|
|
let appBundle = Bundle(url: Bundle.main.bundleURL.appendingPathComponent("Frameworks/App.framework"))
|
|
guard let url = appBundle?.url(forResource: key, withExtension: nil) ?? Bundle.main.url(forResource: key, withExtension: nil),
|
|
let provider = CGDataProvider(url: url as CFURL),
|
|
let font = CGFont(provider) else { return nil }
|
|
CTFontManagerRegisterGraphicsFont(font, nil)
|
|
return font.postScriptName as String?
|
|
}()
|
|
|
|
static func image(_ symbol: String, size: CGFloat = 20) -> UIImage? {
|
|
if symbol == "agent" { return agentImage(size: size) }
|
|
guard let code = glyphs[symbol], let scalar = UnicodeScalar(code),
|
|
let name = fontName, let font = UIFont(name: name, size: size) else {
|
|
assertionFailure("Missing Tabler icon: \(symbol)")
|
|
return nil
|
|
}
|
|
let text = String(scalar) as NSString
|
|
let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: UIColor.black]
|
|
let measured = text.size(withAttributes: attributes)
|
|
return UIGraphicsImageRenderer(size: CGSize(width: size, height: size)).image { _ in
|
|
text.draw(at: CGPoint(x: (size - measured.width) / 2, y: (size - measured.height) / 2), withAttributes: attributes)
|
|
}.withRenderingMode(.alwaysTemplate)
|
|
}
|
|
|
|
/// Matches Flutter's AgentNavigationIcon and the desktop agent mark.
|
|
private static func agentImage(size: CGFloat) -> UIImage {
|
|
UIGraphicsImageRenderer(size: CGSize(width: size, height: size)).image { renderer in
|
|
renderer.cgContext.scaleBy(x: size / 24, y: size / 24)
|
|
UIColor.black.setStroke()
|
|
UIColor.black.setFill()
|
|
let head = UIBezierPath(roundedRect: CGRect(x: 4.75, y: 7, width: 14.5, height: 13.25), cornerRadius: 4.5)
|
|
head.lineWidth = 2
|
|
head.stroke()
|
|
let lines = UIBezierPath()
|
|
lines.lineWidth = 2
|
|
lines.lineCapStyle = .round
|
|
for (start, end) in [
|
|
(CGPoint(x: 12, y: 7), CGPoint(x: 12, y: 2.5)),
|
|
(CGPoint(x: 3.25, y: 11.5), CGPoint(x: 3.25, y: 15.75)),
|
|
(CGPoint(x: 20.75, y: 11.5), CGPoint(x: 20.75, y: 15.75))
|
|
] {
|
|
lines.move(to: start)
|
|
lines.addLine(to: end)
|
|
}
|
|
lines.stroke()
|
|
for x in [9.0, 15.0] {
|
|
UIBezierPath(ovalIn: CGRect(x: x - 1.4, y: 11.85, width: 2.8, height: 2.8)).fill()
|
|
}
|
|
}.withRenderingMode(.alwaysTemplate)
|
|
}
|
|
|
|
private static let glyphs: [String: UInt32] = [
|
|
"arrow.down": 0xea16, // arrow-down
|
|
"xmark": 0xeb55, // x
|
|
"xmark.circle.fill": 0xea6a, // circle-x
|
|
"chevron.down": 0xea5f, // chevron-down
|
|
"chevron.backward": 0xea60, // chevron-left
|
|
"chevron.left": 0xea60, // chevron-left
|
|
"chevron.right": 0xea61, // chevron-right
|
|
"camera": 0xea54, // camera
|
|
"photo.on.rectangle": 0xfd4a, // images — matches BuzzIcons.images
|
|
"photo.on.rectangle.angled": 0xfd4a, // images — matches BuzzIcons.images
|
|
"paintpalette": 0xeb01, // palette
|
|
"drop": 0xea97, // droplet
|
|
"drop.fill": 0xea97, // droplet
|
|
"face.smiling": 0xeaf7, // mood-smile
|
|
"person": 0xeb4d, // user
|
|
"person.crop.circle": 0xeb4d, // user
|
|
"arrow.left.arrow.right": 0xf1f4, // arrows-left-right
|
|
"person.badge.plus": 0xeb4b, // user-plus
|
|
"gearshape": 0xeb20, // settings
|
|
"person.2": 0xebf2, // users
|
|
"rectangle.stack": 0xeb0a, // photo
|
|
"crop": 0xea85, // crop
|
|
"arrow.triangle.2.circlepath.camera": 0xee22, // camera-rotate
|
|
"circle": 0xea6b, // circle
|
|
"circle.fill": 0xea6b, // circle
|
|
"sun.max": 0xeb30, // sun
|
|
"moon": 0xeaf8, // moon
|
|
"ellipsis": 0xea95, // dots
|
|
"arrowshape.turn.up.left": 0xeb77, // arrow-back-up
|
|
"circle.lefthalf.filled": 0xf4a3, // sun-moon
|
|
"magnifyingglass": 0xeb1c, // search
|
|
"tray": 0xeac4, // inbox
|
|
"sparkles": 0xf6d7, // sparkles
|
|
"clock": 0xea70, // clock
|
|
"leaf": 0xed4f, // leaf
|
|
"fork.knife": 0xeeff, // tools-kitchen-2
|
|
"figure.run": 0xec82, // run
|
|
"airplane": 0xeb6f, // plane
|
|
"lightbulb": 0xea51, // bulb
|
|
"heart": 0xeabe, // heart
|
|
"flag": 0xeaa6, // flag
|
|
"video": 0xed22, // video
|
|
"mic": 0xeaf0, // microphone
|
|
"doc": 0xeaa4, // file
|
|
"plus": 0xeb0b, // plus
|
|
"building.2.crop.circle": 0xebf6, // building-community
|
|
"workspace": 0xecd7, // cube-3d-sphere
|
|
"folder.badge.plus": 0xeaab, // folder-plus
|
|
"headphones": 0xeabd, // headphones
|
|
"line.3.horizontal.decrease": 0xeaa5, // filter
|
|
"square.and.pencil": 0xea98, // edit
|
|
"pencil": 0xea98, // edit
|
|
"doc.on.doc": 0xea7a, // copy
|
|
"trash": 0xeb41, // trash
|
|
"pin": 0xec9c, // pin
|
|
"pin.slash": 0xed5f, // pinned-off
|
|
"square.and.arrow.up": 0xeb21, // share
|
|
"link": 0xeade, // link
|
|
"checkmark": 0xea5e, // check
|
|
"checkmark.circle": 0xea67, // circle-check
|
|
"bell": 0xea35, // bell
|
|
"bell.slash": 0xece9, // bell-off
|
|
"number": 0xeabc, // hash — matches BuzzIcons.hash
|
|
"bubble.left": 0xeaef, // message
|
|
"bubble.left.and.bubble.right": 0xeb6c, // messages
|
|
"arrow.uturn.backward": 0xeb77, // arrow-back-up
|
|
]
|
|
}
|