mirror of
https://github.com/block/buzz.git
synced 2026-10-31 22:41:12 +00:00
fix(mobile): tighten avatar shape guard and iOS agent fallback
The iOS Add members fallback tile drew agents as a 30% rounded rect while their loaded avatar was a squircle. It now draws the same squircle. The geometry test now rejects the old rounded rect, and the source guard also flags the old 30%/60% agent radius idiom. Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Signed-off-by: Tom Brow <tomb@squareup.com>
This commit is contained in:
@@ -281,8 +281,8 @@ final class NativeAddMembersViewController: UITableViewController, UISearchResul
|
|||||||
let size = CGSize(width: 40, height: 40)
|
let size = CGSize(width: 40, height: 40)
|
||||||
return UIGraphicsImageRenderer(size: size).image { _ in
|
return UIGraphicsImageRenderer(size: size).image { _ in
|
||||||
(Self.color(user["avatarBackground"]) ?? .tertiarySystemFill).setFill()
|
(Self.color(user["avatarBackground"]) ?? .tertiarySystemFill).setFill()
|
||||||
UIBezierPath(roundedRect: CGRect(origin: .zero, size: size),
|
let rect = CGRect(origin: .zero, size: size)
|
||||||
cornerRadius: user["agent"] as? Bool == true ? 12 : 20).fill()
|
(user["agent"] as? Bool == true ? Self.agentSquircle(rect) : UIBezierPath(ovalIn: rect)).fill()
|
||||||
let text = (user["initial"] as? String ?? "?") as NSString
|
let text = (user["initial"] as? String ?? "?") as NSString
|
||||||
let attributes: [NSAttributedString.Key: Any] = [
|
let attributes: [NSAttributedString.Key: Any] = [
|
||||||
.font: UIFont.systemFont(ofSize: 18, weight: .medium),
|
.font: UIFont.systemFont(ofSize: 18, weight: .medium),
|
||||||
@@ -293,6 +293,21 @@ final class NativeAddMembersViewController: UITableViewController, UISearchResul
|
|||||||
}.withRenderingMode(.alwaysOriginal)
|
}.withRenderingMode(.alwaysOriginal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mirrors avatar_shape.dart and desktop's avatar-squircle.svg.
|
||||||
|
private static func agentSquircle(_ rect: CGRect) -> UIBezierPath {
|
||||||
|
func at(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
|
||||||
|
CGPoint(x: rect.minX + rect.width * x, y: rect.minY + rect.height * y)
|
||||||
|
}
|
||||||
|
let path = UIBezierPath()
|
||||||
|
path.move(to: at(0.5, 0))
|
||||||
|
path.addCurve(to: at(1, 0.5), controlPoint1: at(0.93, 0), controlPoint2: at(1, 0.07))
|
||||||
|
path.addCurve(to: at(0.5, 1), controlPoint1: at(1, 0.93), controlPoint2: at(0.93, 1))
|
||||||
|
path.addCurve(to: at(0, 0.5), controlPoint1: at(0.07, 1), controlPoint2: at(0, 0.93))
|
||||||
|
path.addCurve(to: at(0.5, 0), controlPoint1: at(0, 0.07), controlPoint2: at(0.07, 0))
|
||||||
|
path.close()
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
private static func color(_ value: Any?) -> UIColor? {
|
private static func color(_ value: Any?) -> UIColor? {
|
||||||
guard let number = value as? NSNumber else { return nil }
|
guard let number = value as? NSNumber else { return nil }
|
||||||
let argb = number.uint32Value
|
let argb = number.uint32Value
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ void main() {
|
|||||||
(const Offset(92, 8), false),
|
(const Offset(92, 8), false),
|
||||||
(const Offset(92, 92), false),
|
(const Offset(92, 92), false),
|
||||||
(const Offset(8, 92), false),
|
(const Offset(8, 92), false),
|
||||||
|
// Inside the old 30% rounded rect, outside the squircle.
|
||||||
|
(const Offset(1.9, 22), false),
|
||||||
|
(const Offset(98.1, 78), false),
|
||||||
]) {
|
]) {
|
||||||
expect(path.contains(point), inside, reason: '$point');
|
expect(path.contains(point), inside, reason: '$point');
|
||||||
}
|
}
|
||||||
@@ -33,7 +36,9 @@ void main() {
|
|||||||
test('avatar shapes come only from avatarShape', () {
|
test('avatar shapes come only from avatarShape', () {
|
||||||
// Agent avatars drifted into circles and 30% rounded rects because each
|
// Agent avatars drifted into circles and 30% rounded rects because each
|
||||||
// surface picked its own shape. Shape decisions keyed on agent identity,
|
// surface picked its own shape. Shape decisions keyed on agent identity,
|
||||||
// and person shapes on agent-only surfaces, must use avatar_shape.dart.
|
// the old agent radius idiom, and person shapes on agent-only surfaces
|
||||||
|
// must use avatar_shape.dart. Best effort: it matches the patterns that
|
||||||
|
// produced past drift, not every possible spelling.
|
||||||
final inlineAgentShape = RegExp(
|
final inlineAgentShape = RegExp(
|
||||||
r'\b(isAgent|isBot|agent)\b\s*\?\s*(const\s+)?'
|
r'\b(isAgent|isBot|agent)\b\s*\?\s*(const\s+)?'
|
||||||
r'(BoxShape|BorderRadius|Radius|CircleBorder|RoundedRectangleBorder|'
|
r'(BoxShape|BorderRadius|Radius|CircleBorder|RoundedRectangleBorder|'
|
||||||
@@ -43,6 +48,10 @@ void main() {
|
|||||||
r'if\s*\(\s*!?\s*(isAgent|isBot|agent)\s*\)\s*return\s+'
|
r'if\s*\(\s*!?\s*(isAgent|isBot|agent)\s*\)\s*return\s+'
|
||||||
r'(ClipOval|ClipRRect|CircleAvatar)\b',
|
r'(ClipOval|ClipRRect|CircleAvatar)\b',
|
||||||
);
|
);
|
||||||
|
// The old hand-rolled agent radius: 30% of a size, or 60% of a radius.
|
||||||
|
final agentRadius = RegExp(
|
||||||
|
r'Radius\.circular\(\s*[\w.()+\s]*\*\s*0\.[36]\s*,?\s*\)',
|
||||||
|
);
|
||||||
final personShape = RegExp(r'\b(ClipOval|CircleAvatar|BoxShape\.circle)\b');
|
final personShape = RegExp(r'\b(ClipOval|CircleAvatar|BoxShape\.circle)\b');
|
||||||
final violations = <String>[];
|
final violations = <String>[];
|
||||||
for (final file in Directory('lib').listSync(recursive: true)) {
|
for (final file in Directory('lib').listSync(recursive: true)) {
|
||||||
@@ -53,6 +62,7 @@ void main() {
|
|||||||
for (final match in [
|
for (final match in [
|
||||||
...inlineAgentShape.allMatches(source),
|
...inlineAgentShape.allMatches(source),
|
||||||
...inlineAgentClip.allMatches(source),
|
...inlineAgentClip.allMatches(source),
|
||||||
|
...agentRadius.allMatches(source),
|
||||||
if (path.startsWith('lib/features/agents/'))
|
if (path.startsWith('lib/features/agents/'))
|
||||||
...personShape.allMatches(source),
|
...personShape.allMatches(source),
|
||||||
]) {
|
]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user