mirror of
https://github.com/block/buzz.git
synced 2026-11-07 09:52:03 +00:00
The UI-only part of #9193 by @klopez4212, split out so it can land without the Activity sync changes. - Activity moves from a row in the Home list to a bell button in the Home header, with an unread dot driven by the existing Activity feed and read state. - Home filter capsules match native iOS and Android widths, with a subtle outline on Android when the header collapses. - Home section spacing is tighter. Left out: the reconnect rewrite in #9193's last commit (live events applied in place, no rebuild on reconnect, fatal fallback reads). The Activity provider is unchanged from main. Channel and Solo rows keep their bottom padding so they stay at the 44 pt minimum tap height. ### Validation - Kenny's added tests: `inbox_unread_provider_test` (unread dot) and the Home page bell and filter width tests. - Full mobile suite: all pass except 11 push and notification-toggle tests that fail identically on an unchanged checkout in this environment. - 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 Home, the bell, Activity and channel navigation, 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>
128 lines
5.2 KiB
Swift
128 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.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
|
|
]
|
|
}
|