// // TimelineTableViewController.swift // elpha-ios // // Created by Dwayne Harris on 10/1/18. // Copyright © 2018 Elpha. All rights reserved. // import AlamofireImage import CoreData import MastodonKit import UIKit class TimelineTableViewController: UITableViewController { let fetchLimit = 50 var loading: Bool = false { didSet { DispatchQueue.main.async { if self.loading { UIApplication.shared.isNetworkActivityIndicatorVisible = true self.refreshControl?.beginRefreshing() } else { UIApplication.shared.isNetworkActivityIndicatorVisible = false self.refreshControl?.endRefreshing() } } } } override func viewDidLoad() { super.viewDidLoad() let composeButtonItem = UIBarButtonItem(image: UIImage(named: "Compose"), style: .plain, target: self, action: #selector(compose)) navigationItem.rightBarButtonItem = composeButtonItem navigationItem.title = "Home" refreshControl?.addTarget(self, action: #selector(self.fetchTimelineWithDefaultRange), for: .valueChanged) fetchTimelineWithDefaultRange() } @objc func compose() { let alertController = UIAlertController(title: "Compose", message: "Toot", preferredStyle: .alert) alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) present(alertController, animated: true) } func createDefaultTimelines(account: AccountMO) { let context = CoreDataManager.shared.getContext() let timelineNames = [ "Home", "Local", "Federated", ] timelineNames.forEach { timelineName in let timeline = TimelineMO(context: context) timeline.name = timelineName timeline.account = account } CoreDataManager.shared.saveContext() } func fetchStatuses(request: Request<[Status]>, forTimeline timeline: TimelineMO) { if let client = AuthenticationManager.shared.getMKClientForSelectedSession() { print("Running request \(request)") client.run(request) { result in switch result { case .success(let remoteStatuses, _): DispatchQueue.main.async { let context = CoreDataManager.shared.getContext() let statuses = remoteStatuses.compactMap { status in return MastodonDataManager.upsertStatus(status) } for (index, status) in statuses.enumerated() { if index == 0 { let boundary = TimelineBoundaryMO(context: context) boundary.statusID = status.id! boundary.start = true boundary.createdAt = Date() timeline.mutableSetValue(forKey: "boundaries").add(boundary) } else if index == statuses.count { let boundary = TimelineBoundaryMO(context: context) boundary.statusID = status.id! boundary.start = false boundary.createdAt = Date() timeline.mutableSetValue(forKey: "boundaries").add(boundary) } else { let predicate = NSPredicate(format: "statusID != %@", status.id!) timeline.mutableSetValue(forKey: "boundaries").filter(using: predicate) } } timeline.addToStatuses(NSSet(array: statuses)) CoreDataManager.shared.saveContext() self.loading = false self.tableView.reloadData() } case .failure(let error): print("\(error)") } } } } @objc func fetchTimelineWithDefaultRange() { fetchTimeline(withRange: .limit(fetchLimit)) } func fetchTimeline(withRange requestRange: RequestRange) { guard let session = AuthenticationManager.shared.selectedSession, let account = session.account else { return } if session.selectedTimeline == nil { if let timelines = account.timelines, timelines.count == 0 { createDefaultTimelines(account: account) } let request = NSFetchRequest(entityName: "Timeline") request.predicate = NSPredicate(format: "name == %@", "Home") do { let results = try CoreDataManager.shared.getContext().fetch(request) session.selectedTimeline = results.first CoreDataManager.shared.saveContext() } catch { print("\(error)") } } loading = true if let selectedTimeline = session.selectedTimeline { var request: Request<[Status]> switch selectedTimeline.name { case "Home": request = Timelines.home(range: requestRange) case "Local": request = Timelines.public(local: true, range: requestRange) case "Federated": request = Timelines.public(local: false, range: requestRange) case let tag: request = Timelines.tag(tag!, range: requestRange) } fetchStatuses(request: request, forTimeline: selectedTimeline) } } func getTimelineRequest() -> NSFetchRequest? { guard let session = AuthenticationManager.shared.selectedSession, let selectedTimeline = session.selectedTimeline else { return nil } let request = NSFetchRequest(entityName: "Timeline") request.predicate = NSPredicate(format: "name == %@", selectedTimeline.name!) return request } func getTimelineStatuses() -> [StatusMO]? { guard let request = getTimelineRequest() else { return [] } do { let response = try CoreDataManager.shared.getContext().fetch(request) guard let timeline = response.first, let statuses = timeline.statuses else { return [] } let sortDescriptor = NSSortDescriptor(key: "createdAt", ascending: false) return statuses.sortedArray(using: [sortDescriptor]) as? [StatusMO] } catch { print("\(error)") return [] } } func getTimelineStatusesCount() -> Int { guard let statuses = getTimelineStatuses() else { return 0 } return statuses.count } func getTimelineBoundaries() -> NSSet? { guard let session = AuthenticationManager.shared.selectedSession, let timeline = session.selectedTimeline else { return nil } return timeline.boundaries } override func numberOfSections(in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return getTimelineStatusesCount() } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "TimelineTableViewCell", for: indexPath) as? TimelineTableViewCell else { fatalError("Unable to find reusable cell") } cell.topDividerView.isHidden = false cell.topLoadMoreView.isHidden = true cell.boostView.isHidden = true cell.replyView.isHidden = true cell.bottomLoadMoreView.isHidden = true cell.bottomDividerView.isHidden = false cell.attachmentImageView.isHidden = true if let statuses = getTimelineStatuses() { let status = statuses[indexPath.row] let boundaries = getTimelineBoundaries() ?? NSSet() let avatarFilter = AspectScaledToFillSizeWithRoundedCornersFilter( size: CGSize(width: 40.0, height: 40.0), radius: 20.0, divideRadiusByImageScale: true ) if indexPath.row != 0 { if let boundary = boundaries.filtered(using: NSPredicate(format: "statusID = %@", status.id!)).first as? TimelineBoundaryMO { if boundary.start { if let previousBoundary = boundaries.filtered(using: NSPredicate(format: "statusID = %@", statuses[indexPath.row - 1])).first as? TimelineBoundaryMO { if !previousBoundary.start { cell.topDividerView.isHidden = true cell.topLoadMoreView.isHidden = false } } } } } if indexPath.row < statuses.count - 1 { if let boundary = boundaries.filtered(using: NSPredicate(format: "statusID = %@", status.id!)).first as? TimelineBoundaryMO { if !boundary.start { if let nextBoundary = boundaries.filtered(using: NSPredicate(format: "statusID = %@", statuses[indexPath.row + 1])).first as? TimelineBoundaryMO { if nextBoundary.start { cell.bottomDividerView.isHidden = true cell.bottomLoadMoreView.isHidden = false } } } } } if let reblog = status.reblog { cell.boostView.isHidden = false if let account = reblog.account { cell.avatarImageView.af_setImage(withURL: account.avatarURL!, filter: avatarFilter) cell.displayNameLabel.text = account.displayName cell.usernameLabel.text = account.acct } if let account = status.account { cell.boostAvatarImageView.af_setImage(withURL: account.avatarURL!, filter: avatarFilter) cell.boostDisplayNameLabel.text = account.displayName cell.boostUsernameLabel.text = account.acct } } else { if let account = status.account { cell.avatarImageView.af_setImage(withURL: account.avatarURL!, filter: avatarFilter) cell.displayNameLabel.text = account.displayName cell.usernameLabel.text = account.acct } } if let replyAccountID = status.inReplyToAccountID { if let replyAccount = MastodonDataManager.getAccountByID(replyAccountID) { cell.replyView.isHidden = false cell.replyAvatarImageView.af_setImage(withURL: replyAccount.avatarURL!, filter: avatarFilter) cell.replyDisplayNameLabel.text = replyAccount.displayName cell.replyUsernameLabel.text = replyAccount.acct } } if let content = status.content { do { let styledContent = " \(content)" let attributedText = try NSAttributedString( data: styledContent.data(using: String.Encoding.unicode, allowLossyConversion: true)!, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil ) cell.contentLabel.attributedText = attributedText } catch { print("\(error)") } } if let attachments = status.attachments, let attachment = attachments.anyObject() as? AttachmentMO { cell.attachmentImageView.isHidden = false cell.attachmentImageView.af_setImage(withURL: attachment.url!) } cell.timestampLabel.text = status.createdAt!.timeAgo() cell.repliesLabel.text = "0" cell.boostsLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.reblogsCount), number: .decimal) cell.favoritesLabel.text = NumberFormatter.localizedString(from: NSNumber(value: status.favouritesCount), number: .decimal) if status.reblogged { cell.boostsImageView.image = UIImage(named: "Boost Bold") } else { cell.boostsImageView.image = UIImage(named: "Boost Regular") } if status.favourited { cell.favoritesImageView.image = UIImage(named: "Star Filled") } else { cell.favoritesImageView.image = UIImage(named: "Star Regular") } if indexPath.row == statuses.count - 1 && !loading { fetchTimeline(withRange: .max(id: status.id!, limit: fetchLimit)) } return cell } return cell } }