Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions mobile/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
4A71C0032F40200100A17E01 /* NativeAttachmentPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A71C0042F40200100A17E01 /* NativeAttachmentPopover.swift */; };
4A71C0052F40300100A17E01 /* NativeAttachmentPopoverCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A71C0062F40300100A17E01 /* NativeAttachmentPopoverCoordinator.swift */; };
4A71C0072F40400100A17E01 /* ConcentricSheetSurface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A71C0082F40400100A17E01 /* ConcentricSheetSurface.swift */; };
4A71C0092F40500100A17E01 /* JumpToLatestGlassButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A71C00A2F40500100A17E01 /* JumpToLatestGlassButton.swift */; };
4A71C00B2F40600100A17E01 /* StickyDateGlassHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A71C00C2F40600100A17E01 /* StickyDateGlassHeader.swift */; };
331C809D294A63AB00263BE5 /* UIKitEncoded.png in Resources */ = {isa = PBXBuildFile; fileRef = 331C809C294A618700263BE5 /* UIKitEncoded.png */; };
331C809F294A63AB00263BE5 /* UIKitEncoded.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 331C809E294A618700263BE5 /* UIKitEncoded.jpg */; };
33ADD70AB275E0EC81295559 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8906419FB4E98B4B12B7A56F /* Pods_Runner.framework */; };
Expand Down Expand Up @@ -59,6 +61,8 @@
4A71C0042F40200100A17E01 /* NativeAttachmentPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeAttachmentPopover.swift; sourceTree = "<group>"; };
4A71C0062F40300100A17E01 /* NativeAttachmentPopoverCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeAttachmentPopoverCoordinator.swift; sourceTree = "<group>"; };
4A71C0082F40400100A17E01 /* ConcentricSheetSurface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcentricSheetSurface.swift; sourceTree = "<group>"; };
4A71C00A2F40500100A17E01 /* JumpToLatestGlassButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JumpToLatestGlassButton.swift; sourceTree = "<group>"; };
4A71C00C2F40600100A17E01 /* StickyDateGlassHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickyDateGlassHeader.swift; sourceTree = "<group>"; };
331C809C294A618700263BE5 /* UIKitEncoded.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = UIKitEncoded.png; sourceTree = "<group>"; };
331C809E294A618700263BE5 /* UIKitEncoded.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = UIKitEncoded.jpg; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -182,6 +186,8 @@
4A71C0042F40200100A17E01 /* NativeAttachmentPopover.swift */,
4A71C0062F40300100A17E01 /* NativeAttachmentPopoverCoordinator.swift */,
4A71C0082F40400100A17E01 /* ConcentricSheetSurface.swift */,
4A71C00A2F40500100A17E01 /* JumpToLatestGlassButton.swift */,
4A71C00C2F40600100A17E01 /* StickyDateGlassHeader.swift */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
Expand Down Expand Up @@ -417,6 +423,8 @@
4A71C0032F40200100A17E01 /* NativeAttachmentPopover.swift in Sources */,
4A71C0052F40300100A17E01 /* NativeAttachmentPopoverCoordinator.swift in Sources */,
4A71C0072F40400100A17E01 /* ConcentricSheetSurface.swift in Sources */,
4A71C0092F40500100A17E01 /* JumpToLatestGlassButton.swift in Sources */,
4A71C00B2F40600100A17E01 /* StickyDateGlassHeader.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
);
Expand Down
18 changes: 18 additions & 0 deletions mobile/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,24 @@ import UserNotifications
}
}

if let jumpToLatestGlassRegistrar = engineBridge.pluginRegistry.registrar(
forPlugin: "BuzzJumpToLatestGlassButton"
) {
jumpToLatestGlassRegistrar.register(
JumpToLatestGlassButtonFactory(messenger: messenger),
withId: "buzz/jump_to_latest_glass"
)
}

if let stickyDateGlassRegistrar = engineBridge.pluginRegistry.registrar(
forPlugin: "BuzzStickyDateGlassHeader"
) {
stickyDateGlassRegistrar.register(
StickyDateGlassHeaderFactory(messenger: messenger),
withId: "buzz/sticky_date_glass"
)
}

let nativeAttachmentRegistrar = engineBridge.pluginRegistry.registrar(
forPlugin: "BuzzNativeAttachmentPopover"
)
Expand Down
125 changes: 125 additions & 0 deletions mobile/ios/Runner/JumpToLatestGlassButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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
Comment thread
klopez4212 marked this conversation as resolved.
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)
}
}
131 changes: 131 additions & 0 deletions mobile/ios/Runner/StickyDateGlassHeader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import Flutter
import UIKit

private final class StickyDateGlassView: UIVisualEffectView {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.height / 2
}
}

final class StickyDateGlassHeaderFactory: 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 {
StickyDateGlassHeaderPlatformView(
frame: frame,
viewIdentifier: viewId,
arguments: args,
messenger: messenger
)
}
}

final class StickyDateGlassHeaderPlatformView: NSObject, FlutterPlatformView {
private let glassView: StickyDateGlassView
private let channel: FlutterMethodChannel
private let dateLabel = UILabel()

init(
frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?,
messenger: FlutterBinaryMessenger
) {
let arguments = args as? [String: Any]
let text = arguments?["label"] as? String ?? ""
channel = FlutterMethodChannel(
name: "buzz/sticky_date_glass/\(viewId)",
binaryMessenger: messenger
)

if #available(iOS 26.0, *) {
let glassEffect = UIGlassEffect(style: .regular)
glassEffect.isInteractive = false
glassView = StickyDateGlassView(effect: glassEffect)
} else {
glassView = StickyDateGlassView(
effect: UIBlurEffect(style: .systemMaterial)
)
}

super.init()

glassView.frame = frame
glassView.isOpaque = false
glassView.isUserInteractionEnabled = false
glassView.clipsToBounds = true
glassView.layer.cornerCurve = .continuous
applyBrightness(from: arguments?["brightness"])

dateLabel.translatesAutoresizingMaskIntoConstraints = false
dateLabel.text = text
dateLabel.textAlignment = .center
dateLabel.textColor = .secondaryLabel
dateLabel.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(
for: UIFont.systemFont(ofSize: 14, weight: .medium)
)
dateLabel.adjustsFontForContentSizeCategory = true
dateLabel.numberOfLines = 1
dateLabel.lineBreakMode = .byTruncatingTail
dateLabel.isAccessibilityElement = false

glassView.contentView.addSubview(dateLabel)
NSLayoutConstraint.activate([
dateLabel.leadingAnchor.constraint(
equalTo: glassView.contentView.leadingAnchor,
constant: 12
),
dateLabel.trailingAnchor.constraint(
equalTo: glassView.contentView.trailingAnchor,
constant: -12
),
dateLabel.centerYAnchor.constraint(
equalTo: glassView.contentView.centerYAnchor
),
])

channel.setMethodCallHandler { [weak self] call, result in
switch call.method {
case "setLabel":
guard let text = call.arguments as? String else {
result(FlutterMethodNotImplemented)
return
}
self?.dateLabel.text = text
result(nil)
case "setBrightness":
self?.applyBrightness(from: call.arguments)
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
}

func view() -> UIView {
glassView
}

private func applyBrightness(from value: Any?) {
let interfaceStyle: UIUserInterfaceStyle = value as? String == "dark" ? .dark : .light
glassView.overrideUserInterfaceStyle = interfaceStyle
}

deinit {
channel.setMethodCallHandler(nil)
}
}
5 changes: 3 additions & 2 deletions mobile/lib/features/channels/channel_detail_page.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'dart:async';
import 'dart:math' show min;
import 'dart:math' show max, min;

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show ScrollDirection;
Expand Down Expand Up @@ -45,7 +45,7 @@ import 'day_divider.dart';
import 'dm_channel_labels.dart';
import 'ephemeral_channel_display.dart';
import 'ime_metrics_settle_observer.dart';
import 'latest_message_button.dart';
import 'jump_to_latest_button.dart';
import 'members_sheet.dart';
import 'message_actions.dart';
import 'message_long_press_region.dart';
Expand All @@ -58,6 +58,7 @@ import 'reaction_row.dart';
import 'send_message_provider.dart';
import '../profile/user_profile_sheet.dart';
import 'small_avatar.dart';
import 'sticky_date_header.dart';
import 'thread_detail_page.dart';
import 'timeline_message.dart';

Expand Down
Loading
Loading