Files
buzz/mobile/ios/Runner/JumpToLatestGlassButton.swift
klopez4212 417eea2230 Polish mobile timeline navigation (#5874)
## Summary
- add sticky date headers that crossfade with in-timeline dates
- replace the Latest pill with a centered down-arrow control
- animate the control out from its bottom-center anchor
<img width="630" height="1368"
alt="2AF1BB3F-B295-4084-8A65-90AED60B58B5"
src="https://github.com/user-attachments/assets/505de450-66f5-4628-8c59-34b5511ffb5c"
/>

## Validation
- `bin/just mobile-check`
- `bin/just mobile-test` (1,359 tests)
- Android debug and signed iOS release builds

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
2026-08-18 18:56:58 +01:00

126 lines
3.5 KiB
Swift

import Flutter
import UIKit
final class JumpToLatestGlassButtonFactory: NSObject, FlutterPlatformViewFactory {
private let messenger: FlutterBinaryMessenger
init(messenger: FlutterBinaryMessenger) {
self.messenger = messenger
super.init()
}
func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
FlutterStandardMessageCodec.sharedInstance()
}
func create(
withFrame frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?
) -> FlutterPlatformView {
JumpToLatestGlassButtonPlatformView(
frame: frame,
viewIdentifier: viewId,
arguments: args,
messenger: messenger
)
}
}
private final class JumpToLatestGlassButton: UIButton {
private static let hitTargetExpansion: CGFloat = 4
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
bounds
.insetBy(
dx: -Self.hitTargetExpansion,
dy: -Self.hitTargetExpansion
)
.contains(point)
}
}
final class JumpToLatestGlassButtonPlatformView: NSObject, FlutterPlatformView {
private let containerView: UIView
private let channel: FlutterMethodChannel
private let button = JumpToLatestGlassButton(type: .system)
init(
frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?,
messenger: FlutterBinaryMessenger
) {
containerView = UIView(frame: frame)
channel = FlutterMethodChannel(
name: "buzz/jump_to_latest_glass/\(viewId)",
binaryMessenger: messenger
)
super.init()
containerView.backgroundColor = .clear
containerView.isOpaque = false
applyBrightness(from: args)
var configuration: UIButton.Configuration
if #available(iOS 26.0, *) {
configuration = .glass()
} else {
configuration = .gray()
configuration.baseBackgroundColor = UIColor.secondarySystemBackground
}
configuration.cornerStyle = .capsule
configuration.baseForegroundColor = .label
configuration.image = UIImage(
systemName: "arrow.down",
withConfiguration: UIImage.SymbolConfiguration(
pointSize: 16,
weight: .semibold
)
)
button.configuration = configuration
button.accessibilityLabel = "Jump to latest message"
button.translatesAutoresizingMaskIntoConstraints = false
button.addAction(
UIAction { [weak self] _ in
self?.channel.invokeMethod("pressed", arguments: nil)
},
for: .touchUpInside
)
channel.setMethodCallHandler { [weak self] call, result in
guard call.method == "setBrightness" else {
result(FlutterMethodNotImplemented)
return
}
self?.applyBrightness(from: call.arguments)
result(nil)
}
containerView.addSubview(button)
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
button.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
button.widthAnchor.constraint(equalToConstant: 40),
button.heightAnchor.constraint(equalToConstant: 40),
])
}
func view() -> UIView {
containerView
}
private func applyBrightness(from value: Any?) {
let brightness = (value as? [String: Any])?["brightness"] as? String
?? value as? String
let interfaceStyle: UIUserInterfaceStyle = brightness == "dark" ? .dark : .light
containerView.overrideUserInterfaceStyle = interfaceStyle
button.overrideUserInterfaceStyle = interfaceStyle
button.setNeedsUpdateConfiguration()
}
deinit {
channel.setMethodCallHandler(nil)
}
}