-
Notifications
You must be signed in to change notification settings - Fork 7
[MOB-3582] AI Shortcut Access Point on NTP #932
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a011cce
565a7b5
2ccaaee
60a4f52
ae049c8
d676693
87dd53c
a727439
e2d5007
6585715
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
// This Source Code Form is subject to the terms of the Mozilla Public | ||
// License, v. 2.0. If a copy of the MPL was not distributed with this | ||
// file, You can obtain one at http://mozilla.org/MPL/2.0/ | ||
|
||
import UIKit | ||
import SwiftUI | ||
import Common | ||
import Ecosia | ||
|
||
protocol NTPMultiPurposeEcosiaHeaderDelegate: AnyObject { | ||
func multiPurposeEcosiaHeaderDidRequestAISearch() | ||
} | ||
|
||
/// NTP header cell containing multiple Ecosia-specific actions like AI search | ||
@available(iOS 16.0, *) | ||
final class NTPMultiPurposeEcosiaHeader: UICollectionViewCell, ThemeApplicable, ReusableCell { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand the descriptiveness, but to be honest I think |
||
|
||
// MARK: - Properties | ||
private var hostingController: UIHostingController<AnyView>? | ||
private var viewModel: NTPMultiPurposeEcosiaHeaderViewModel? | ||
|
||
// MARK: - Init | ||
override init(frame: CGRect) { | ||
super.init(frame: frame) | ||
setup() | ||
} | ||
|
||
required init?(coder: NSCoder) { | ||
super.init(coder: coder) | ||
setup() | ||
} | ||
|
||
// MARK: - Setup | ||
|
||
private func setup() { | ||
// Create a placeholder hosting controller - will be configured later | ||
let hostingController = UIHostingController(rootView: AnyView(EmptyView())) | ||
hostingController.view.backgroundColor = UIColor.clear | ||
hostingController.view.translatesAutoresizingMaskIntoConstraints = false | ||
|
||
contentView.addSubview(hostingController.view) | ||
self.hostingController = hostingController | ||
|
||
NSLayoutConstraint.activate([ | ||
hostingController.view.topAnchor.constraint(equalTo: contentView.topAnchor), | ||
hostingController.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), | ||
hostingController.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), | ||
hostingController.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) | ||
]) | ||
} | ||
|
||
// MARK: - Public Methods | ||
|
||
func configure(with viewModel: NTPMultiPurposeEcosiaHeaderViewModel, windowUUID: WindowUUID) { | ||
self.viewModel = viewModel | ||
|
||
// Update the SwiftUI view with the new view model | ||
let swiftUIView = NTPMultiPurposeEcosiaHeaderView( | ||
viewModel: viewModel, | ||
windowUUID: windowUUID | ||
) | ||
|
||
hostingController?.rootView = AnyView(swiftUIView) | ||
} | ||
|
||
// MARK: - Theming | ||
func applyTheme(theme: Theme) { | ||
// Theme is handled by the SwiftUI view | ||
} | ||
} | ||
|
||
// MARK: - SwiftUI Multi-Purpose Header View | ||
@available(iOS 16.0, *) | ||
struct NTPMultiPurposeEcosiaHeaderView: View { | ||
@ObservedObject var viewModel: NTPMultiPurposeEcosiaHeaderViewModel | ||
let windowUUID: WindowUUID | ||
|
||
var body: some View { | ||
HStack { | ||
Spacer() | ||
|
||
// AI Search Button positioned on the right | ||
// Ecosia: Use theme colors from view model (following FeedbackView pattern) | ||
EcosiaAISearchButton( | ||
backgroundColor: viewModel.buttonBackgroundColor, | ||
iconColor: viewModel.buttonIconColor, | ||
onTap: handleAISearchTap | ||
) | ||
} | ||
.padding(.leading, .ecosia.space._m) | ||
.padding(.trailing, .ecosia.space._m) | ||
.onReceive(NotificationCenter.default.publisher(for: .ThemeDidChange)) { notification in | ||
// Ecosia: Listen to theme changes (following FeedbackView pattern) | ||
guard let uuid = notification.windowUUID, uuid == windowUUID else { return } | ||
let themeManager = AppContainer.shared.resolve() as ThemeManager | ||
viewModel.applyTheme(theme: themeManager.getCurrentTheme(for: windowUUID)) | ||
} | ||
} | ||
|
||
private func handleAISearchTap() { | ||
viewModel.openAISearch() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
// This Source Code Form is subject to the terms of the Mozilla Public | ||
// License, v. 2.0. If a copy of the MPL was not distributed with this | ||
// file, You can obtain one at http://mozilla.org/MPL/2.0/ | ||
|
||
import Common | ||
import Foundation | ||
import Shared | ||
import SwiftUI | ||
import Ecosia | ||
|
||
final class NTPMultiPurposeEcosiaHeaderViewModel: ObservableObject { | ||
struct UX { | ||
static let topInset: CGFloat = 24 | ||
} | ||
|
||
// MARK: - Properties | ||
private let windowUUID: WindowUUID | ||
internal weak var delegate: NTPMultiPurposeEcosiaHeaderDelegate? | ||
internal var theme: Theme | ||
|
||
// MARK: - Theme Properties (following FeedbackView pattern) | ||
@Published var buttonBackgroundColor = Color.gray.opacity(0.2) | ||
@Published var buttonIconColor = Color.primary | ||
|
||
// MARK: - Initialization | ||
init(theme: Theme, | ||
windowUUID: WindowUUID, | ||
delegate: NTPMultiPurposeEcosiaHeaderDelegate? = nil) { | ||
self.theme = theme | ||
self.windowUUID = windowUUID | ||
self.delegate = delegate | ||
|
||
// Apply initial theme | ||
applyTheme(theme: theme) | ||
} | ||
|
||
// MARK: - Theme Handling | ||
func applyTheme(theme: Theme) { | ||
self.theme = theme | ||
buttonBackgroundColor = Color(theme.colors.ecosia.backgroundElevation1) | ||
buttonIconColor = Color(theme.colors.ecosia.textPrimary) | ||
} | ||
Comment on lines
+38
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels a bit bad to me that this is on the view model, but I remember this was already the case for the latest feedback view implementation. Can you remind me why that is? Couldn't we leave handling colors as a responsibility of the view similar to how |
||
|
||
// MARK: - Public Methods | ||
|
||
func openAISearch() { | ||
delegate?.multiPurposeEcosiaHeaderDidRequestAISearch() | ||
Analytics.shared.aiSearchNTPButtonTapped() | ||
} | ||
} | ||
|
||
// MARK: HomeViewModelProtocol | ||
extension NTPMultiPurposeEcosiaHeaderViewModel: HomepageViewModelProtocol, FeatureFlaggable { | ||
var sectionType: HomepageSectionType { | ||
return .multiPurposeEcosiaHeader | ||
} | ||
|
||
var headerViewModel: LabelButtonHeaderViewModel { | ||
return .emptyHeader | ||
} | ||
|
||
func section(for traitCollection: UITraitCollection, size: CGSize) -> NSCollectionLayoutSection { | ||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), | ||
heightDimension: .estimated(64)) | ||
let item = NSCollectionLayoutItem(layoutSize: itemSize) | ||
|
||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), | ||
heightDimension: .estimated(64)) | ||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1) | ||
|
||
let section = NSCollectionLayoutSection(group: group) | ||
|
||
section.contentInsets = NSDirectionalEdgeInsets( | ||
top: UX.topInset, | ||
leading: 0, | ||
bottom: 0, | ||
trailing: 0) | ||
|
||
return section | ||
} | ||
|
||
func numberOfItemsInSection() -> Int { | ||
return 1 | ||
} | ||
|
||
var isEnabled: Bool { | ||
AISearchMVPExperiment.isEnabled | ||
} | ||
|
||
func setTheme(theme: Theme) { | ||
// Ecosia: Use applyTheme for consistency (following FeedbackView pattern) | ||
applyTheme(theme: theme) | ||
} | ||
|
||
func refreshData(for traitCollection: UITraitCollection, size: CGSize, isPortrait: Bool, device: UIUserInterfaceIdiom) { | ||
// No data refresh needed for multi-purpose header | ||
} | ||
} | ||
|
||
extension NTPMultiPurposeEcosiaHeaderViewModel: HomepageSectionHandler { | ||
|
||
func configure(_ cell: UICollectionViewCell, at indexPath: IndexPath) -> UICollectionViewCell { | ||
if #available(iOS 16.0, *) { | ||
guard let multiPurposeHeaderCell = cell as? NTPMultiPurposeEcosiaHeader else { return cell } | ||
multiPurposeHeaderCell.configure(with: self, windowUUID: windowUUID) | ||
return multiPurposeHeaderCell | ||
} | ||
return cell | ||
} | ||
|
||
func didSelectItem(at indexPath: IndexPath, homePanelDelegate: HomePanelDelegate?, libraryPanelDelegate: LibraryPanelDelegate?) { | ||
// This cell handles its own button actions, no cell selection needed | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you check with Product if no problem not supporting iOS 15? I see no issue and to be honest will likely soon be the case for the FF base, just checking.