Files
buzz/mobile/ios/Runner/NativeTabler.swift
Tom Brow beec055300 feat(mobile): move Activity to a Home bell and align Home spacing (#9212)
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>
2026-11-06 12:10:11 -08:00

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
]
}