mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-23 21:03:08 +09:00
610 lines
27 KiB
Swift
610 lines
27 KiB
Swift
/* 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 Account
|
||
import Shared
|
||
import SnapKit
|
||
import Storage
|
||
import Sync
|
||
import XCGLogger
|
||
|
||
private let log = Logger.browserLogger
|
||
|
||
private struct RemoteTabsPanelUX {
|
||
static let HeaderHeight = SiteTableViewControllerUX.RowHeight // Not HeaderHeight!
|
||
static let RowHeight = SiteTableViewControllerUX.RowHeight
|
||
static let HeaderBackgroundColor = UIColor(rgb: 0xf8f8f8)
|
||
|
||
static let EmptyStateTitleTextColor = UIColor.darkGray
|
||
|
||
static let EmptyStateInstructionsTextColor = UIColor.gray
|
||
static let EmptyStateInstructionsWidth = 170
|
||
static let EmptyStateTopPaddingInBetweenItems: CGFloat = 15 // UX TODO I set this to 8 so that it all fits on landscape
|
||
static let EmptyStateSignInButtonColor = UIColor(red: 0.3, green: 0.62, blue: 1, alpha: 1)
|
||
static let EmptyStateSignInButtonTitleColor = UIColor.white
|
||
static let EmptyStateSignInButtonCornerRadius: CGFloat = 4
|
||
static let EmptyStateSignInButtonHeight = 44
|
||
static let EmptyStateSignInButtonWidth = 200
|
||
|
||
// Backup and active strings added in Bug 1205294.
|
||
static let EmptyStateInstructionsSyncTabsPasswordsBookmarksString = NSLocalizedString("Sync your tabs, bookmarks, passwords and more.", comment: "Text displayed when the Sync home panel is empty, describing the features provided by Sync to invite the user to log in.")
|
||
|
||
static let EmptyStateInstructionsSyncTabsPasswordsString = NSLocalizedString("Sync your tabs, passwords and more.", comment: "Text displayed when the Sync home panel is empty, describing the features provided by Sync to invite the user to log in.")
|
||
|
||
static let EmptyStateInstructionsGetTabsBookmarksPasswordsString = NSLocalizedString("Get your open tabs, bookmarks, and passwords from your other devices.", comment: "A re-worded offer about Sync, displayed when the Sync home panel is empty, that emphasizes one-way data transfer, not syncing.")
|
||
|
||
static let HistoryTableViewHeaderChevronInset: CGFloat = 10
|
||
static let HistoryTableViewHeaderChevronSize: CGFloat = 20
|
||
static let HistoryTableViewHeaderChevronLineWidth: CGFloat = 3.0
|
||
}
|
||
|
||
private let RemoteClientIdentifier = "RemoteClient"
|
||
private let RemoteTabIdentifier = "RemoteTab"
|
||
|
||
class RemoteTabsPanel: UIViewController, HomePanel {
|
||
weak var homePanelDelegate: HomePanelDelegate?
|
||
fileprivate lazy var tableViewController: RemoteTabsTableViewController = RemoteTabsTableViewController()
|
||
fileprivate lazy var historyBackButton: HistoryBackButton = {
|
||
let button = HistoryBackButton()
|
||
button.addTarget(self, action: #selector(RemoteTabsPanel.historyBackButtonWasTapped), for: .touchUpInside)
|
||
return button
|
||
}()
|
||
var profile: Profile!
|
||
|
||
init() {
|
||
super.init(nibName: nil, bundle: nil)
|
||
NotificationCenter.default.addObserver(self, selector: #selector(RemoteTabsPanel.notificationReceived(_:)), name: NotificationFirefoxAccountChanged, object: nil)
|
||
NotificationCenter.default.addObserver(self, selector: #selector(RemoteTabsPanel.notificationReceived(_:)), name: NotificationProfileDidFinishSyncing, object: nil)
|
||
}
|
||
|
||
required init?(coder aDecoder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
|
||
tableViewController.profile = profile
|
||
tableViewController.remoteTabsPanel = self
|
||
|
||
view.backgroundColor = UIConstants.PanelBackgroundColor
|
||
|
||
addChildViewController(tableViewController)
|
||
self.view.addSubview(tableViewController.view)
|
||
self.view.addSubview(historyBackButton)
|
||
|
||
historyBackButton.snp.makeConstraints { make in
|
||
make.top.left.right.equalTo(self.view)
|
||
make.height.equalTo(50)
|
||
make.bottom.equalTo(tableViewController.view.snp.top)
|
||
}
|
||
|
||
tableViewController.view.snp.makeConstraints { make in
|
||
make.top.equalTo(historyBackButton.snp.bottom)
|
||
make.left.right.bottom.equalTo(self.view)
|
||
}
|
||
|
||
tableViewController.didMove(toParentViewController: self)
|
||
}
|
||
|
||
func notificationReceived(_ notification: Notification) {
|
||
switch notification.name {
|
||
case NotificationFirefoxAccountChanged, NotificationProfileDidFinishSyncing:
|
||
DispatchQueue.main.async {
|
||
print(notification.name)
|
||
self.tableViewController.refreshTabs()
|
||
}
|
||
break
|
||
default:
|
||
// no need to do anything at all
|
||
log.warning("Received unexpected notification \(notification.name)")
|
||
break
|
||
}
|
||
}
|
||
|
||
@objc fileprivate func historyBackButtonWasTapped(_ gestureRecognizer: UITapGestureRecognizer) {
|
||
_ = self.navigationController?.popViewController(animated: true)
|
||
}
|
||
}
|
||
|
||
enum RemoteTabsError {
|
||
case notLoggedIn
|
||
case noClients
|
||
case noTabs
|
||
case failedToSync
|
||
|
||
func localizedString() -> String {
|
||
switch self {
|
||
case .notLoggedIn:
|
||
return "" // This does not have a localized string because we have a whole specific screen for it.
|
||
case .noClients:
|
||
return Strings.EmptySyncedTabsPanelNullStateDescription
|
||
case .noTabs:
|
||
return NSLocalizedString("You don’t have any tabs open in Firefox on your other devices.", comment: "Error message in the remote tabs panel")
|
||
case .failedToSync:
|
||
return NSLocalizedString("There was a problem accessing tabs from your other devices. Try again in a few moments.", comment: "Error message in the remote tabs panel")
|
||
}
|
||
}
|
||
}
|
||
|
||
protocol RemoteTabsPanelDataSource: UITableViewDataSource, UITableViewDelegate {
|
||
}
|
||
|
||
class RemoteTabsPanelClientAndTabsDataSource: NSObject, RemoteTabsPanelDataSource {
|
||
weak var homePanel: HomePanel?
|
||
fileprivate var clientAndTabs: [ClientAndTabs]
|
||
|
||
init(homePanel: HomePanel, clientAndTabs: [ClientAndTabs]) {
|
||
self.homePanel = homePanel
|
||
self.clientAndTabs = clientAndTabs
|
||
}
|
||
|
||
func numberOfSections(in tableView: UITableView) -> Int {
|
||
return self.clientAndTabs.count
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||
return self.clientAndTabs[section].tabs.count
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||
return RemoteTabsPanelUX.HeaderHeight
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||
let clientTabs = self.clientAndTabs[section]
|
||
let client = clientTabs.client
|
||
let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: RemoteClientIdentifier) as! TwoLineHeaderFooterView
|
||
view.frame = CGRect(x: 0, y: 0, width: tableView.frame.width, height: RemoteTabsPanelUX.HeaderHeight)
|
||
view.textLabel?.text = client.name
|
||
view.contentView.backgroundColor = RemoteTabsPanelUX.HeaderBackgroundColor
|
||
|
||
/*
|
||
* A note on timestamps.
|
||
* We have access to two timestamps here: the timestamp of the remote client record,
|
||
* and the set of timestamps of the client's tabs.
|
||
* Neither is "last synced". The client record timestamp changes whenever the remote
|
||
* client uploads its record (i.e., infrequently), but also whenever another device
|
||
* sends a command to that client -- which can be much later than when that client
|
||
* last synced.
|
||
* The client's tabs haven't necessarily changed, but it can still have synced.
|
||
* Ideally, we should save and use the modified time of the tabs record itself.
|
||
* This will be the real time that the other client uploaded tabs.
|
||
*/
|
||
|
||
let timestamp = clientTabs.approximateLastSyncTime()
|
||
let label = NSLocalizedString("Last synced: %@", comment: "Remote tabs last synced time. Argument is the relative date string.")
|
||
view.detailTextLabel?.text = String(format: label, Date.fromTimestamp(timestamp).toRelativeTimeString())
|
||
|
||
let image: UIImage?
|
||
if client.type == "desktop" {
|
||
image = UIImage(named: "deviceTypeDesktop")
|
||
image?.accessibilityLabel = NSLocalizedString("computer", comment: "Accessibility label for Desktop Computer (PC) image in remote tabs list")
|
||
} else {
|
||
image = UIImage(named: "deviceTypeMobile")
|
||
image?.accessibilityLabel = NSLocalizedString("mobile device", comment: "Accessibility label for Mobile Device image in remote tabs list")
|
||
}
|
||
view.imageView.image = image
|
||
|
||
view.mergeAccessibilityLabels()
|
||
return view
|
||
}
|
||
|
||
fileprivate func tabAtIndexPath(_ indexPath: IndexPath) -> RemoteTab {
|
||
return clientAndTabs[indexPath.section].tabs[indexPath.item]
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||
let cell = tableView.dequeueReusableCell(withIdentifier: RemoteTabIdentifier, for: indexPath) as! TwoLineTableViewCell
|
||
let tab = tabAtIndexPath(indexPath)
|
||
cell.setLines(tab.title, detailText: tab.URL.absoluteString)
|
||
// TODO: Bug 1144765 - Populate image with cached favicons.
|
||
return cell
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||
tableView.deselectRow(at: indexPath, animated: false)
|
||
let tab = tabAtIndexPath(indexPath)
|
||
if let homePanel = self.homePanel {
|
||
// It's not a bookmark, so let's call it Typed (which means History, too).
|
||
homePanel.homePanelDelegate?.homePanel(homePanel, didSelectURL: tab.URL, visitType: VisitType.typed)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: -
|
||
|
||
class RemoteTabsPanelErrorDataSource: NSObject, RemoteTabsPanelDataSource {
|
||
weak var homePanel: HomePanel?
|
||
var error: RemoteTabsError
|
||
var notLoggedCell: UITableViewCell?
|
||
|
||
init(homePanel: HomePanel, error: RemoteTabsError) {
|
||
self.homePanel = homePanel
|
||
self.error = error
|
||
self.notLoggedCell = nil
|
||
}
|
||
|
||
func numberOfSections(in tableView: UITableView) -> Int {
|
||
return 1
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||
return 1
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||
if let cell = self.notLoggedCell {
|
||
cell.updateConstraints()
|
||
}
|
||
return tableView.bounds.height
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
||
// Making the footer height as small as possible because it will disable button tappability if too high.
|
||
return 1
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||
return UIView()
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||
switch error {
|
||
case .notLoggedIn:
|
||
let cell = RemoteTabsNotLoggedInCell(homePanel: homePanel)
|
||
self.notLoggedCell = cell
|
||
return cell
|
||
default:
|
||
let cell = RemoteTabsErrorCell(error: self.error)
|
||
self.notLoggedCell = nil
|
||
return cell
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// MARK: -
|
||
|
||
class RemoteTabsErrorCell: UITableViewCell {
|
||
static let Identifier = "RemoteTabsErrorCell"
|
||
|
||
init(error: RemoteTabsError) {
|
||
super.init(style: .default, reuseIdentifier: RemoteTabsErrorCell.Identifier)
|
||
|
||
separatorInset = UIEdgeInsets(top: 0, left: 1000, bottom: 0, right: 0)
|
||
|
||
let containerView = UIView()
|
||
contentView.addSubview(containerView)
|
||
|
||
let imageView = UIImageView()
|
||
imageView.image = UIImage(named: "emptySync")
|
||
containerView.addSubview(imageView)
|
||
imageView.snp.makeConstraints { (make) -> Void in
|
||
make.top.equalTo(containerView)
|
||
make.centerX.equalTo(containerView)
|
||
}
|
||
|
||
let titleLabel = UILabel()
|
||
titleLabel.font = DynamicFontHelper.defaultHelper.DeviceFont
|
||
titleLabel.text = Strings.EmptySyncedTabsPanelStateTitle
|
||
titleLabel.textAlignment = NSTextAlignment.center
|
||
titleLabel.textColor = RemoteTabsPanelUX.EmptyStateTitleTextColor
|
||
containerView.addSubview(titleLabel)
|
||
|
||
let instructionsLabel = UILabel()
|
||
instructionsLabel.font = DynamicFontHelper.defaultHelper.DeviceFontSmallLight
|
||
instructionsLabel.text = error.localizedString()
|
||
instructionsLabel.textAlignment = NSTextAlignment.center
|
||
instructionsLabel.textColor = RemoteTabsPanelUX.EmptyStateInstructionsTextColor
|
||
instructionsLabel.numberOfLines = 0
|
||
containerView.addSubview(instructionsLabel)
|
||
|
||
titleLabel.snp.makeConstraints { make in
|
||
make.top.equalTo(imageView.snp.bottom).offset(RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems)
|
||
make.centerX.equalTo(imageView)
|
||
}
|
||
|
||
instructionsLabel.snp.makeConstraints { make in
|
||
make.top.equalTo(titleLabel.snp.bottom).offset(RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems / 2)
|
||
make.centerX.equalTo(containerView)
|
||
make.width.equalTo(RemoteTabsPanelUX.EmptyStateInstructionsWidth)
|
||
}
|
||
|
||
containerView.snp.makeConstraints { make in
|
||
// Let the container wrap around the content
|
||
make.top.equalTo(imageView.snp.top)
|
||
make.left.bottom.right.equalTo(instructionsLabel)
|
||
// And then center it in the overlay view that sits on top of the UITableView
|
||
make.centerX.equalTo(contentView)
|
||
|
||
// Sets proper top constraint for iPhone 6 in portait and for iPad.
|
||
make.centerY.equalTo(contentView.snp.centerY).offset(HomePanelUX.EmptyTabContentOffset).priority(100)
|
||
|
||
// Sets proper top constraint for iPhone 4, 5 in portrait.
|
||
make.top.greaterThanOrEqualTo(contentView.snp.top).offset(20).priority(1000)
|
||
}
|
||
}
|
||
|
||
required init?(coder aDecoder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
}
|
||
|
||
// MARK: -
|
||
|
||
class RemoteTabsNotLoggedInCell: UITableViewCell {
|
||
static let Identifier = "RemoteTabsNotLoggedInCell"
|
||
var homePanel: HomePanel?
|
||
var instructionsLabel: UILabel
|
||
var signInButton: UIButton
|
||
var titleLabel: UILabel
|
||
var emptyStateImageView: UIImageView
|
||
|
||
init(homePanel: HomePanel?) {
|
||
let titleLabel = UILabel()
|
||
let instructionsLabel = UILabel()
|
||
let signInButton = UIButton()
|
||
let imageView = UIImageView()
|
||
|
||
self.instructionsLabel = instructionsLabel
|
||
self.signInButton = signInButton
|
||
self.titleLabel = titleLabel
|
||
self.emptyStateImageView = imageView
|
||
|
||
super.init(style: .default, reuseIdentifier: RemoteTabsErrorCell.Identifier)
|
||
|
||
self.homePanel = homePanel
|
||
let createAnAccountButton = UIButton(type: .system)
|
||
|
||
imageView.image = UIImage(named: "emptySync")
|
||
contentView.addSubview(imageView)
|
||
|
||
titleLabel.font = DynamicFontHelper.defaultHelper.DeviceFont
|
||
titleLabel.text = Strings.EmptySyncedTabsPanelStateTitle
|
||
titleLabel.textAlignment = NSTextAlignment.center
|
||
titleLabel.textColor = RemoteTabsPanelUX.EmptyStateTitleTextColor
|
||
contentView.addSubview(titleLabel)
|
||
|
||
instructionsLabel.font = DynamicFontHelper.defaultHelper.DeviceFontSmallLight
|
||
instructionsLabel.text = Strings.EmptySyncedTabsPanelStateDescription
|
||
instructionsLabel.textAlignment = NSTextAlignment.center
|
||
instructionsLabel.textColor = RemoteTabsPanelUX.EmptyStateInstructionsTextColor
|
||
instructionsLabel.numberOfLines = 0
|
||
contentView.addSubview(instructionsLabel)
|
||
|
||
signInButton.backgroundColor = RemoteTabsPanelUX.EmptyStateSignInButtonColor
|
||
signInButton.setTitle(NSLocalizedString("Sign in", comment: "See http://mzl.la/1Qtkf0j"), for: UIControlState())
|
||
signInButton.setTitleColor(RemoteTabsPanelUX.EmptyStateSignInButtonTitleColor, for: UIControlState())
|
||
signInButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.subheadline)
|
||
signInButton.layer.cornerRadius = RemoteTabsPanelUX.EmptyStateSignInButtonCornerRadius
|
||
signInButton.clipsToBounds = true
|
||
signInButton.addTarget(self, action: #selector(RemoteTabsNotLoggedInCell.SELsignIn), for: UIControlEvents.touchUpInside)
|
||
contentView.addSubview(signInButton)
|
||
|
||
createAnAccountButton.setTitle(NSLocalizedString("Create an account", comment: "See http://mzl.la/1Qtkf0j"), for: UIControlState())
|
||
createAnAccountButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.caption1)
|
||
createAnAccountButton.addTarget(self, action: #selector(RemoteTabsNotLoggedInCell.SELcreateAnAccount), for: UIControlEvents.touchUpInside)
|
||
contentView.addSubview(createAnAccountButton)
|
||
|
||
imageView.snp.makeConstraints { (make) -> Void in
|
||
make.centerX.equalTo(instructionsLabel)
|
||
|
||
// Sets proper top constraint for iPhone 6 in portait and for iPad.
|
||
make.centerY.equalTo(contentView).offset(HomePanelUX.EmptyTabContentOffset + 30).priority(100)
|
||
|
||
// Sets proper top constraint for iPhone 4, 5 in portrait.
|
||
make.top.greaterThanOrEqualTo(contentView.snp.top).priority(1000)
|
||
}
|
||
|
||
titleLabel.snp.makeConstraints { make in
|
||
make.top.equalTo(imageView.snp.bottom).offset(RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems)
|
||
make.centerX.equalTo(imageView)
|
||
}
|
||
|
||
createAnAccountButton.snp.makeConstraints { (make) -> Void in
|
||
make.centerX.equalTo(signInButton)
|
||
make.top.equalTo(signInButton.snp.bottom).offset(RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems)
|
||
}
|
||
}
|
||
|
||
required init?(coder aDecoder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
@objc fileprivate func SELsignIn() {
|
||
if let homePanel = self.homePanel {
|
||
homePanel.homePanelDelegate?.homePanelDidRequestToSignIn(homePanel)
|
||
}
|
||
}
|
||
|
||
@objc fileprivate func SELcreateAnAccount() {
|
||
if let homePanel = self.homePanel {
|
||
homePanel.homePanelDelegate?.homePanelDidRequestToCreateAccount(homePanel)
|
||
}
|
||
}
|
||
|
||
override func updateConstraints() {
|
||
if UIInterfaceOrientationIsLandscape(UIApplication.shared.statusBarOrientation) && !(DeviceInfo.deviceModel().range(of: "iPad") != nil) {
|
||
instructionsLabel.snp.remakeConstraints { make in
|
||
make.top.equalTo(titleLabel.snp.bottom).offset(RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems)
|
||
make.width.equalTo(RemoteTabsPanelUX.EmptyStateInstructionsWidth)
|
||
|
||
// Sets proper landscape layout for bigger phones: iPhone 6 and on.
|
||
make.left.lessThanOrEqualTo(contentView.snp.left).offset(80).priority(100)
|
||
|
||
// Sets proper landscape layout for smaller phones: iPhone 4 & 5.
|
||
make.right.lessThanOrEqualTo(contentView.snp.centerX).offset(-30).priority(1000)
|
||
}
|
||
|
||
signInButton.snp.remakeConstraints { make in
|
||
make.height.equalTo(RemoteTabsPanelUX.EmptyStateSignInButtonHeight)
|
||
make.width.equalTo(RemoteTabsPanelUX.EmptyStateSignInButtonWidth)
|
||
make.centerY.equalTo(emptyStateImageView).offset(2*RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems)
|
||
|
||
// Sets proper landscape layout for bigger phones: iPhone 6 and on.
|
||
make.right.greaterThanOrEqualTo(contentView.snp.right).offset(-70).priority(100)
|
||
|
||
// Sets proper landscape layout for smaller phones: iPhone 4 & 5.
|
||
make.left.greaterThanOrEqualTo(contentView.snp.centerX).offset(10).priority(1000)
|
||
}
|
||
} else {
|
||
instructionsLabel.snp.remakeConstraints { make in
|
||
make.top.equalTo(titleLabel.snp.bottom).offset(RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems)
|
||
make.centerX.equalTo(contentView)
|
||
make.width.equalTo(RemoteTabsPanelUX.EmptyStateInstructionsWidth)
|
||
}
|
||
|
||
signInButton.snp.remakeConstraints { make in
|
||
make.centerX.equalTo(contentView)
|
||
make.top.equalTo(instructionsLabel.snp.bottom).offset(RemoteTabsPanelUX.EmptyStateTopPaddingInBetweenItems)
|
||
make.height.equalTo(RemoteTabsPanelUX.EmptyStateSignInButtonHeight)
|
||
make.width.equalTo(RemoteTabsPanelUX.EmptyStateSignInButtonWidth)
|
||
}
|
||
}
|
||
super.updateConstraints()
|
||
}
|
||
}
|
||
|
||
fileprivate class RemoteTabsTableViewController: UITableViewController {
|
||
weak var remoteTabsPanel: RemoteTabsPanel?
|
||
var profile: Profile!
|
||
var tableViewDelegate: RemoteTabsPanelDataSource? {
|
||
didSet {
|
||
tableView.dataSource = tableViewDelegate
|
||
tableView.delegate = tableViewDelegate
|
||
}
|
||
}
|
||
|
||
fileprivate lazy var longPressRecognizer: UILongPressGestureRecognizer = {
|
||
return UILongPressGestureRecognizer(target: self, action: #selector(RemoteTabsTableViewController.longPress(_:)))
|
||
}()
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
tableView.addGestureRecognizer(longPressRecognizer)
|
||
tableView.register(TwoLineHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: RemoteClientIdentifier)
|
||
tableView.register(TwoLineTableViewCell.self, forCellReuseIdentifier: RemoteTabIdentifier)
|
||
|
||
tableView.rowHeight = RemoteTabsPanelUX.RowHeight
|
||
tableView.separatorInset = UIEdgeInsets.zero
|
||
|
||
tableView.delegate = nil
|
||
tableView.dataSource = nil
|
||
|
||
refreshControl = UIRefreshControl()
|
||
}
|
||
|
||
override func viewWillAppear(_ animated: Bool) {
|
||
super.viewWillAppear(animated)
|
||
refreshControl?.addTarget(self, action: #selector(RemoteTabsTableViewController.refreshTabs), for: .valueChanged)
|
||
refreshTabs()
|
||
}
|
||
|
||
override func viewDidDisappear(_ animated: Bool) {
|
||
super.viewDidDisappear(animated)
|
||
refreshControl?.removeTarget(self, action: #selector(RemoteTabsTableViewController.refreshTabs), for: .valueChanged)
|
||
}
|
||
|
||
fileprivate func startRefreshing() {
|
||
if let refreshControl = self.refreshControl {
|
||
let height = -refreshControl.bounds.size.height
|
||
tableView.setContentOffset(CGPoint(x: 0, y: height), animated: true)
|
||
refreshControl.beginRefreshing()
|
||
}
|
||
}
|
||
|
||
func endRefreshing() {
|
||
if self.refreshControl?.isRefreshing ?? false {
|
||
self.refreshControl?.endRefreshing()
|
||
}
|
||
|
||
self.tableView.isScrollEnabled = true
|
||
self.tableView.reloadData()
|
||
}
|
||
|
||
func updateDelegateClientAndTabData(_ clientAndTabs: [ClientAndTabs]) {
|
||
guard let remoteTabsPanel = remoteTabsPanel else { return }
|
||
if clientAndTabs.count == 0 {
|
||
self.tableViewDelegate = RemoteTabsPanelErrorDataSource(homePanel: remoteTabsPanel, error: .noClients)
|
||
} else {
|
||
let nonEmptyClientAndTabs = clientAndTabs.filter { $0.tabs.count > 0 }
|
||
if nonEmptyClientAndTabs.count == 0 {
|
||
self.tableViewDelegate = RemoteTabsPanelErrorDataSource(homePanel: remoteTabsPanel, error: .noTabs)
|
||
} else {
|
||
self.tableViewDelegate = RemoteTabsPanelClientAndTabsDataSource(homePanel: remoteTabsPanel, clientAndTabs: nonEmptyClientAndTabs)
|
||
tableView.allowsSelection = true
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc fileprivate func refreshTabs() {
|
||
guard let remoteTabsPanel = remoteTabsPanel else { return }
|
||
|
||
assert(Thread.isMainThread)
|
||
|
||
tableView.isScrollEnabled = false
|
||
tableView.allowsSelection = false
|
||
tableView.tableFooterView = UIView(frame: CGRect.zero)
|
||
|
||
// Short circuit if the user is not logged in
|
||
if !profile.hasSyncableAccount() {
|
||
self.tableViewDelegate = RemoteTabsPanelErrorDataSource(homePanel: remoteTabsPanel, error: .notLoggedIn)
|
||
self.endRefreshing()
|
||
return
|
||
}
|
||
|
||
self.profile.getCachedClientsAndTabs().uponQueue(DispatchQueue.main) { result in
|
||
if let clientAndTabs = result.successValue {
|
||
self.updateDelegateClientAndTabData(clientAndTabs)
|
||
}
|
||
|
||
// Fetch the tabs from the cloud if it has been more than 5 seconds since the last sync.
|
||
let lastSyncTime = self.profile.prefs.timestampForKey(PrefsKeys.KeyLastRemoteTabSyncTime) ?? 0
|
||
if Date.now() > lastSyncTime && Date.now() - lastSyncTime > OneSecondInMilliseconds * 5 {
|
||
self.startRefreshing()
|
||
self.profile.getClientsAndTabs().uponQueue(DispatchQueue.main) { result in
|
||
// We set the last sync time to now, regardless of whether the sync was successful, to avoid trying to sync over
|
||
// and over again in cases whether the client is unable to sync (e.g. when there is no network connectivity).
|
||
self.profile.prefs.setTimestamp(Date.now(), forKey: PrefsKeys.KeyLastRemoteTabSyncTime)
|
||
if let clientAndTabs = result.successValue {
|
||
self.updateDelegateClientAndTabData(clientAndTabs)
|
||
}
|
||
self.endRefreshing()
|
||
}
|
||
} else {
|
||
// If we failed before and didn't sync, show the failure delegate
|
||
if let _ = result.failureValue {
|
||
self.tableViewDelegate = RemoteTabsPanelErrorDataSource(homePanel: remoteTabsPanel, error: .failedToSync)
|
||
}
|
||
|
||
self.endRefreshing()
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc fileprivate func longPress(_ longPressGestureRecognizer: UILongPressGestureRecognizer) {
|
||
guard longPressGestureRecognizer.state == UIGestureRecognizerState.began else { return }
|
||
let touchPoint = longPressGestureRecognizer.location(in: tableView)
|
||
guard let indexPath = tableView.indexPathForRow(at: touchPoint) else { return }
|
||
presentContextMenu(for: indexPath)
|
||
}
|
||
}
|
||
|
||
extension RemoteTabsTableViewController: HomePanelContextMenu {
|
||
func presentContextMenu(for site: Site, with indexPath: IndexPath, completionHandler: @escaping () -> PhotonActionSheet?) {
|
||
guard let contextMenu = completionHandler() else { return }
|
||
self.present(contextMenu, animated: true, completion: nil)
|
||
}
|
||
|
||
func getSiteDetails(for indexPath: IndexPath) -> Site? {
|
||
guard let tab = (tableViewDelegate as? RemoteTabsPanelClientAndTabsDataSource)?.tabAtIndexPath(indexPath) else { return nil }
|
||
return Site(url: String(describing: tab.URL), title: tab.title)
|
||
}
|
||
|
||
func getContextMenuActions(for site: Site, with indexPath: IndexPath) -> [PhotonActionSheetItem]? {
|
||
return getDefaultContextMenuActions(for: site, homePanelDelegate: remoteTabsPanel?.homePanelDelegate)
|
||
}
|
||
}
|