[ABANDONED] Mastodon iOS client.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

335 lines
14 KiB

//
// 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<TimelineMO>(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<TimelineMO>? {
guard let session = AuthenticationManager.shared.selectedSession, let selectedTimeline = session.selectedTimeline else {
return nil
}
let request = NSFetchRequest<TimelineMO>(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 = "<style>html * { font-size: 15px; color: #170c49; font-family: -apple-system } html, body { margin: 0; padding: 0 }</style> \(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
}
}