Dactyloidae/mobile/ios/Extensions/SendTo/ClientPickerViewController.swift

320 lines
13 KiB
Swift
Raw Normal View History

2026-06-26 21:04:09 -07:00
/* 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 Shared
import Storage
import SnapKit
protocol ClientPickerViewControllerDelegate {
func clientPickerViewControllerDidCancel(_ clientPickerViewController: ClientPickerViewController)
func clientPickerViewController(_ clientPickerViewController: ClientPickerViewController, didPickClients clients: [RemoteClient])
}
struct ClientPickerViewControllerUX {
static let TableHeaderRowHeight = CGFloat(50)
static let TableHeaderTextFont = UIFont.systemFont(ofSize: 16)
static let TableHeaderTextColor = UIColor.gray
static let TableHeaderTextPaddingLeft = CGFloat(20)
static let DeviceRowTintColor = UIColor(red: 0.427, green: 0.800, blue: 0.102, alpha: 1.0)
static let DeviceRowHeight = CGFloat(50)
static let DeviceRowTextFont = UIFont.systemFont(ofSize: 16)
static let DeviceRowTextPaddingLeft = CGFloat(72)
static let DeviceRowTextPaddingRight = CGFloat(50)
}
/// The ClientPickerViewController displays a list of clients associated with the provided Account.
/// The user can select a number of devices and hit the Send button.
/// This viewcontroller does not implement any specific business logic that needs to happen with the selected clients.
/// That is up to it's delegate, who can listen for cancellation and success events.
class ClientPickerViewController: UITableViewController {
var profile: Profile?
var profileNeedsShutdown = true
var clientPickerDelegate: ClientPickerViewControllerDelegate?
var reloading = true
var clients: [RemoteClient] = []
var selectedClients = NSMutableSet()
// ShareItem has been added as we are now using this class outside of the ShareTo extension to provide Share To functionality
// And in this case we need to be able to store the item we are sharing as we may not have access to the
// url later. Currently used only when sharing an item from the Tab Tray from a Preview Action.
var shareItem: ShareItem?
override func viewDidLoad() {
super.viewDidLoad()
title = NSLocalizedString("Send Tab", tableName: "SendTo", comment: "Title of the dialog that allows you to send a tab to a different device")
refreshControl = UIRefreshControl()
refreshControl?.addTarget(self, action: #selector(ClientPickerViewController.refresh), for: UIControlEvents.valueChanged)
navigationItem.leftBarButtonItem = UIBarButtonItem(
title: Strings.SendToCancelButton,
style: .plain,
target: self,
action: #selector(ClientPickerViewController.cancel)
)
tableView.register(ClientPickerTableViewHeaderCell.self, forCellReuseIdentifier: ClientPickerTableViewHeaderCell.CellIdentifier)
tableView.register(ClientPickerTableViewCell.self, forCellReuseIdentifier: ClientPickerTableViewCell.CellIdentifier)
tableView.register(ClientPickerNoClientsTableViewCell.self, forCellReuseIdentifier: ClientPickerNoClientsTableViewCell.CellIdentifier)
tableView.tableFooterView = UIView(frame: CGRect.zero)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let refreshControl = refreshControl {
refreshControl.beginRefreshing()
let height = -(refreshControl.bounds.size.height + (self.navigationController?.navigationBar.bounds.size.height ?? 0))
self.tableView.contentOffset = CGPoint(x: 0, y: height)
}
reloadClients()
}
override func numberOfSections(in tableView: UITableView) -> Int {
if clients.count == 0 {
return 1
} else {
return 2
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if clients.count == 0 {
return 1
} else {
if section == 0 {
return 1
} else {
return clients.count
}
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell
if clients.count > 0 {
if indexPath.section == 0 {
cell = tableView.dequeueReusableCell(withIdentifier: ClientPickerTableViewHeaderCell.CellIdentifier, for: indexPath) as! ClientPickerTableViewHeaderCell
} else {
let clientCell = tableView.dequeueReusableCell(withIdentifier: ClientPickerTableViewCell.CellIdentifier, for: indexPath) as! ClientPickerTableViewCell
clientCell.nameLabel.text = clients[indexPath.row].name
clientCell.clientType = clients[indexPath.row].type == "mobile" ? ClientType.Mobile : ClientType.Desktop
clientCell.checked = selectedClients.contains(indexPath)
cell = clientCell
}
} else {
if reloading == false {
cell = tableView.dequeueReusableCell(withIdentifier: ClientPickerNoClientsTableViewCell.CellIdentifier, for: indexPath) as! ClientPickerNoClientsTableViewCell
} else {
cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "ClientCell")
}
}
return cell
}
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
return indexPath.section != 0
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if clients.count > 0 && indexPath.section == 1 {
tableView.deselectRow(at: indexPath, animated: true)
if selectedClients.contains(indexPath) {
selectedClients.remove(indexPath)
} else {
selectedClients.add(indexPath)
}
tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.none)
navigationItem.rightBarButtonItem?.isEnabled = (selectedClients.count != 0)
}
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if clients.count > 0 {
if indexPath.section == 0 {
return ClientPickerViewControllerUX.TableHeaderRowHeight
} else {
return ClientPickerViewControllerUX.DeviceRowHeight
}
} else {
return tableView.frame.height
}
}
fileprivate func reloadClients() {
// If we were not given a profile, open the default profile. This happens in case we are called from an app
// extension. That also means that we need to shut down the profile, otherwise the app extension will be
// terminated when it goes into the background.
if self.profile == nil {
self.profile = BrowserProfile(localName: "profile")
self.profileNeedsShutdown = true
}
guard let profile = self.profile else {
return
}
// Re-open the profile it was shutdown. This happens when we run from an app extension, where we must
// make sure that the profile is only open for brief moments of time.
if profile.isShutdown {
profile.reopen()
}
reloading = true
profile.getClients().upon({ result in
withExtendedLifetime(profile) {
// If we are running from an app extension then make sure we shut down the profile as soon as we are
// done with it.
if self.profileNeedsShutdown {
profile.shutdown()
}
self.reloading = false
guard let c = result.successValue else {
return
}
self.clients = c
DispatchQueue.main.async {
if self.clients.count == 0 {
self.navigationItem.rightBarButtonItem = nil
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Send", tableName: "SendTo", comment: "Navigation bar button to Send the current page to a device"), style: UIBarButtonItemStyle.done, target: self, action: #selector(ClientPickerViewController.send))
self.navigationItem.rightBarButtonItem?.isEnabled = false
}
self.selectedClients.removeAllObjects()
self.tableView.reloadData()
self.refreshControl?.endRefreshing()
}
}
})
}
func refresh() {
reloadClients()
}
func cancel() {
clientPickerDelegate?.clientPickerViewControllerDidCancel(self)
}
func send() {
var clients = [RemoteClient]()
for indexPath in selectedClients {
clients.append(self.clients[(indexPath as AnyObject).row])
}
clientPickerDelegate?.clientPickerViewController(self, didPickClients: clients)
// Replace the Send button with a loading indicator since it takes a while to sync
// up our changes to the server.
let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 25, height: 25))
loadingIndicator.color = .darkGray
loadingIndicator.startAnimating()
let customBarButton = UIBarButtonItem(customView: loadingIndicator)
self.navigationItem.rightBarButtonItem = customBarButton
}
}
class ClientPickerTableViewHeaderCell: UITableViewCell {
static let CellIdentifier = "ClientPickerTableViewSectionHeader"
let nameLabel = UILabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addSubview(nameLabel)
nameLabel.font = ClientPickerViewControllerUX.TableHeaderTextFont
nameLabel.text = NSLocalizedString("Available devices:", tableName: "SendTo", comment: "Header for the list of devices table")
nameLabel.textColor = ClientPickerViewControllerUX.TableHeaderTextColor
nameLabel.snp.makeConstraints { (make) -> Void in
make.left.equalTo(ClientPickerViewControllerUX.TableHeaderTextPaddingLeft)
make.centerY.equalTo(self)
make.right.equalTo(self)
}
preservesSuperviewLayoutMargins = false
layoutMargins = UIEdgeInsets.zero
separatorInset = UIEdgeInsets.zero
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
public enum ClientType: String {
case Mobile = "deviceTypeMobile"
case Desktop = "deviceTypeDesktop"
}
class ClientPickerTableViewCell: UITableViewCell {
static let CellIdentifier = "ClientPickerTableViewCell"
var nameLabel: UILabel
var checked: Bool = false {
didSet {
self.accessoryType = checked ? UITableViewCellAccessoryType.checkmark : UITableViewCellAccessoryType.none
}
}
var clientType: ClientType = ClientType.Mobile {
didSet {
self.imageView?.image = UIImage(named: clientType.rawValue)
}
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
nameLabel = UILabel()
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(nameLabel)
nameLabel.font = ClientPickerViewControllerUX.DeviceRowTextFont
nameLabel.numberOfLines = 2
nameLabel.lineBreakMode = NSLineBreakMode.byWordWrapping
self.tintColor = ClientPickerViewControllerUX.DeviceRowTintColor
self.preservesSuperviewLayoutMargins = false
self.selectionStyle = UITableViewCellSelectionStyle.none
}
override func layoutSubviews() {
super.layoutSubviews()
nameLabel.snp.makeConstraints { (make) -> Void in
make.left.equalTo(ClientPickerViewControllerUX.DeviceRowTextPaddingLeft)
make.centerY.equalTo(self.snp.centerY)
make.right.equalTo(self.snp.right).offset(-ClientPickerViewControllerUX.DeviceRowTextPaddingRight)
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ClientPickerNoClientsTableViewCell: UITableViewCell {
static let CellIdentifier = "ClientPickerNoClientsTableViewCell"
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupHelpView(contentView,
introText: NSLocalizedString("You dont have any other devices connected to this Firefox Account available to sync.", tableName: "SendTo", comment: "Error message shown in the remote tabs panel"),
showMeText: "") // TODO We used to have a 'show me how to ...' text here. But, we cannot open web pages from the extension. So this is clear for now until we decide otherwise.
// Move the separator off screen
separatorInset = UIEdgeInsets(top: 0, left: 1000, bottom: 0, right: 0)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}