Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4fc3955
feat(mobile): add inline profile photo capture
klopez4212 Aug 24, 2026
dbb4bab
fix(mobile): refine profile camera motion
klopez4212 Aug 24, 2026
6295fef
fix(mobile): polish profile camera capture motion
klopez4212 Aug 24, 2026
2594add
fix(mobile): smooth camera state handoffs
klopez4212 Aug 24, 2026
5f663a8
fix(mobile): stabilize emoji editor layout
klopez4212 Aug 24, 2026
5a59509
fix(mobile): refine profile camera controls
klopez4212 Aug 24, 2026
9ea6ada
fix(mobile): polish avatar capture feedback
klopez4212 Aug 24, 2026
c4b1373
fix(mobile): crossfade camera exit controls
klopez4212 Aug 24, 2026
8c44b1c
fix(mobile): complete camera exit handoff
klopez4212 Aug 24, 2026
be11508
fix(mobile): synchronize camera preview expansion
klopez4212 Aug 24, 2026
1cfd1c8
fix(mobile): scale avatar through camera handoff
klopez4212 Aug 24, 2026
4de4751
refactor(mobile): keep avatar capture within size limit
klopez4212 Aug 24, 2026
eb92b3d
fix(mobile): resize profile camera shutter
klopez4212 Aug 24, 2026
82b5943
fix(mobile): enlarge profile camera shutter
klopez4212 Aug 24, 2026
287186c
feat(mobile): animate profile camera flips
klopez4212 Aug 24, 2026
6d29b94
fix(mobile): polish profile camera selection
klopez4212 Aug 24, 2026
0b4c15a
fix(mobile): lock profile capture to portrait
klopez4212 Aug 24, 2026
ff3cdab
fix(mobile): preserve animated avatar save handoff
klopez4212 Aug 24, 2026
9a6554d
fix(mobile): hide capture placeholder during processing
klopez4212 Aug 24, 2026
5728239
fix(mobile): save latest animated avatar framing
klopez4212 Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mobile/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:screenOrientation="portrait"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep portrait locking inside profile capture

When users rotate the device anywhere outside the profile editor, this activity-level setting now forces every Android screen to remain portrait; the corresponding Info.plist change also removes both previously supported landscape orientations on iOS. The camera controllers already lock their capture orientation, so any required UI-orientation lock should be applied only while a profile capture widget is mounted and restored afterward rather than changing the orientation contract for channels, forums, media viewing, and the rest of the app.

Useful? React with 👍 / 👎.

android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
Expand Down
2 changes: 0 additions & 2 deletions mobile/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
184 changes: 150 additions & 34 deletions mobile/ios/Runner/JumpToLatestGlassButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,18 @@ final class NavigationGlassButtonFactory: NSObject, FlutterPlatformViewFactory {
}

final class NavigationGlassButtonPlatformView: NSObject, FlutterPlatformView {
private static let shutterIconRatio: CGFloat = 80.0 / 115.0
private static let shutterInsetRatio: CGFloat = 20.0 / 115.0
private let containerView: UIView
private let channel: FlutterMethodChannel
private let button = NavigationGlassButton(type: .system)
private let activityIndicator = UIActivityIndicatorView(style: .medium)
private var buttonLabel: String?
private var buttonIconName = "chevron.backward"
private var contentIcon = "back"
private var buttonImage: UIImage?
private var isBusy = false
private var controlSize: CGFloat = 40

init(
frame: CGRect,
Expand All @@ -196,12 +204,11 @@ final class NavigationGlassButtonPlatformView: NSObject, FlutterPlatformView {
(arguments?["hitTargetWidth"] as? NSNumber)?.doubleValue ?? 48
let hitTargetHeight =
(arguments?["hitTargetHeight"] as? NSNumber)?.doubleValue ?? 48
let label = arguments?["label"] as? String
buttonLabel = label
let icon = arguments?["icon"] as? String
let symbolName = icon == "close" ? "xmark" : "chevron.backward"
let controlWidth =
(arguments?["controlWidth"] as? NSNumber)?.doubleValue ?? 40
controlSize =
(arguments?["controlSize"] as? NSNumber)?.doubleValue ?? 40
let fillWidth = arguments?["fillWidth"] as? Bool ?? false

var configuration: UIButton.Configuration
if #available(iOS 26.0, *) {
Expand All @@ -211,33 +218,15 @@ final class NavigationGlassButtonPlatformView: NSObject, FlutterPlatformView {
configuration.baseBackgroundColor = UIColor.secondarySystemBackground
}
configuration.cornerStyle = .capsule
if let label {
configuration.title = label
configuration.titleLineBreakMode = .byClipping
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer {
incoming in
var outgoing = incoming
let preferred = UIFont.preferredFont(forTextStyle: .subheadline)
outgoing.font = UIFont.systemFont(ofSize: preferred.pointSize, weight: .semibold)
return outgoing
}
} else {
configuration.image = UIImage(
systemName: symbolName,
withConfiguration: UIImage.SymbolConfiguration(
pointSize: 17,
weight: .semibold
)
)
}
button.configuration = configuration
applyContent(from: arguments)
button.titleLabel?.numberOfLines = 1
button.titleLabel?.lineBreakMode = .byClipping
button.hitTargetInsets = UIEdgeInsets(
top: max(0, (hitTargetHeight - 40) / 2),
left: max(0, buttonCenterX - 20),
bottom: max(0, (hitTargetHeight - 40) / 2),
right: max(0, hitTargetWidth - buttonCenterX - 20)
top: max(0, (hitTargetHeight - controlSize) / 2),
left: max(0, buttonCenterX - controlWidth / 2),
bottom: max(0, (hitTargetHeight - controlSize) / 2),
right: max(0, hitTargetWidth - buttonCenterX - controlWidth / 2)
)
button.accessibilityLabel = arguments?["accessibilityLabel"] as? String ?? "Back"
button.translatesAutoresizingMaskIntoConstraints = false
Expand All @@ -248,8 +237,24 @@ final class NavigationGlassButtonPlatformView: NSObject, FlutterPlatformView {
for: .touchUpInside
)

activityIndicator.translatesAutoresizingMaskIntoConstraints = false
activityIndicator.hidesWhenStopped = true
activityIndicator.isUserInteractionEnabled = false
button.addSubview(activityIndicator)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: button.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: button.centerYAnchor),
activityIndicator.widthAnchor.constraint(equalToConstant: 24),
activityIndicator.heightAnchor.constraint(equalToConstant: 24),
])

applyAppearance(from: args)
channel.setMethodCallHandler { [weak self] call, result in
if call.method == "setContent" {
self?.setContent(from: call.arguments)
result(nil)
return
}
guard call.method == "setAppearance" else {
result(FlutterMethodNotImplemented)
return
Expand All @@ -260,14 +265,19 @@ final class NavigationGlassButtonPlatformView: NSObject, FlutterPlatformView {

containerView.addSubview(button)
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
button.heightAnchor.constraint(equalToConstant: controlSize),
])
if fillWidth {
button.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).isActive = true
button.widthAnchor.constraint(equalTo: containerView.widthAnchor).isActive = true
} else {
button.centerXAnchor.constraint(
equalTo: containerView.leadingAnchor,
constant: buttonCenterX
),
button.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
button.widthAnchor.constraint(equalToConstant: controlWidth),
button.heightAnchor.constraint(equalToConstant: 40),
])
).isActive = true
button.widthAnchor.constraint(equalToConstant: controlWidth).isActive = true
}
}

func view() -> UIView {
Expand Down Expand Up @@ -331,24 +341,130 @@ final class NavigationGlassButtonPlatformView: NSObject, FlutterPlatformView {
let foregroundColor = colorValue.map(Self.color(from:))
let enabled = arguments?["enabled"] as? Bool ?? true
let busy = arguments?["busy"] as? Bool ?? false
let selected = arguments?["selected"] as? Bool ?? false

containerView.overrideUserInterfaceStyle = interfaceStyle
button.overrideUserInterfaceStyle = interfaceStyle
button.isEnabled = enabled
button.configuration?.showsActivityIndicator = busy
button.configuration?.title = busy ? nil : buttonLabel
button.isSelected = selected
isBusy = busy
if selected {
button.accessibilityTraits.insert(.selected)
} else {
button.accessibilityTraits.remove(.selected)
}
button.configuration?.showsActivityIndicator = false
if let foregroundColor {
// Glass uses the view tint for its selected treatment. Keep it aligned
// with the Buzz theme instead of falling back to the system blue tint.
button.tintColor = foregroundColor
button.configuration?.baseForegroundColor = foregroundColor
activityIndicator.color = foregroundColor
}
if #unavailable(iOS 26.0) {
button.configuration?.baseBackgroundColor = Self.fallbackBackgroundColor(
foregroundColor: foregroundColor,
interfaceStyle: interfaceStyle
)
}
updateDisplayedContent()
button.setNeedsUpdateConfiguration()
}

private func applyContent(from value: Any?) {
let arguments = value as? [String: Any]
let icon = arguments?["icon"] as? String
contentIcon = icon ?? "back"
buttonLabel = arguments?["label"] as? String
if let accessibilityLabel = arguments?["accessibilityLabel"] as? String {
button.accessibilityLabel = accessibilityLabel
}
switch icon {
case "close": buttonIconName = "xmark"
case "camera": buttonIconName = "camera"
case "photoLibrary": buttonIconName = "photo.on.rectangle.angled"
case "palette": buttonIconName = "paintpalette"
case "emoji": buttonIconName = "face.smiling"
case "person": buttonIconName = "person"
case "frame": buttonIconName = "rectangle.stack"
case "rotateCamera": buttonIconName = "arrow.triangle.2.circlepath.camera"
case "shutter": buttonIconName = "circle.fill"
default: buttonIconName = "chevron.backward"
}
if buttonLabel != nil {
buttonImage = nil
button.configuration?.contentInsets = NSDirectionalEdgeInsets(
top: 8,
leading: 8,
bottom: 8,
trailing: 8
)
button.configuration?.titleLineBreakMode = .byClipping
button.configuration?.titleTextAttributesTransformer =
UIConfigurationTextAttributesTransformer { incoming in
var outgoing = incoming
let preferred = UIFont.preferredFont(forTextStyle: .subheadline)
outgoing.font = UIFont.systemFont(
ofSize: preferred.pointSize,
weight: .semibold
)
return outgoing
}
} else {
button.configuration?.titleTextAttributesTransformer = nil
let iconInset: CGFloat = icon == "shutter"
? controlSize * Self.shutterInsetRatio
: 8
button.configuration?.contentInsets = NSDirectionalEdgeInsets(
top: iconInset,
leading: iconInset,
bottom: iconInset,
trailing: iconInset
)
let pointSize: CGFloat = icon == "shutter"
? controlSize * Self.shutterIconRatio
: 17
buttonImage = UIImage(
systemName: buttonIconName,
withConfiguration: UIImage.SymbolConfiguration(
pointSize: pointSize,
weight: .semibold
)
)
}
updateDisplayedContent()
button.setNeedsUpdateConfiguration()
}

private func setContent(from value: Any?) {
let arguments = value as? [String: Any]
let nextIcon = arguments?["icon"] as? String ?? "back"
let nextLabel = arguments?["label"] as? String
guard nextIcon != contentIcon || nextLabel != buttonLabel else {
applyContent(from: value)
return
}
let duration = UIAccessibility.isReduceMotionEnabled ? 0 : 0.12
UIView.transition(
with: button,
duration: duration,
options: [.transitionCrossDissolve, .beginFromCurrentState, .allowAnimatedContent],
animations: { [weak self] in self?.applyContent(from: value) }
)
}

private func updateDisplayedContent() {
if isBusy {
button.configuration?.title = nil
button.configuration?.image = nil
activityIndicator.startAnimating()
} else {
button.configuration?.title = buttonLabel
button.configuration?.image = buttonLabel == nil ? buttonImage : nil
activityIndicator.stopAnimating()
}
}

private static func color(from value: UInt32) -> UIColor {
let alpha = CGFloat((value >> 24) & 0xFF) / 255
let red = CGFloat((value >> 16) & 0xFF) / 255
Expand Down
Loading
Loading