Dwayne Harris
6 years ago
12 changed files with 779 additions and 121 deletions
-
16elpha-ios.xcodeproj/project.pbxproj
-
97elpha-ios/AuthenticateViewController.swift
-
13elpha-ios/AuthenticationManager.swift
-
137elpha-ios/Base.lproj/Main.storyboard
-
77elpha-ios/Date+TimeAgo.swift
-
75elpha-ios/Elpha.xcdatamodeld/Elpha.xcdatamodel/contents
-
6elpha-ios/InstancesDataManager.swift
-
8elpha-ios/InstancesTableViewCell.swift
-
12elpha-ios/InstancesTableViewController.swift
-
231elpha-ios/MastodonDataManager.swift
-
18elpha-ios/TimelineTableViewCell.swift
-
210elpha-ios/TimelineTableViewController.swift
@ -0,0 +1,77 @@ |
|||
// |
|||
// Date+TimeAgo.swift |
|||
// elpha-ios |
|||
// |
|||
// Created by Dwayne Harris on 10/1/18. |
|||
// Copyright © 2018 Elpha. All rights reserved. |
|||
// |
|||
|
|||
import Foundation |
|||
|
|||
extension Date { |
|||
func timeAgo() -> String { |
|||
let calendar = Calendar.current |
|||
let now = Date() |
|||
let earliest = self < now ? self : now |
|||
let latest = self > now ? self : now |
|||
|
|||
let unitFlags: Set<Calendar.Component> = [.second, .minute, .hour, .day, .weekOfMonth, .month, .year] |
|||
let components: DateComponents = calendar.dateComponents(unitFlags, from: earliest, to: latest) |
|||
|
|||
if let year = components.year { |
|||
if (year >= 2) { |
|||
return "\(year) years" |
|||
} else if (year >= 1) { |
|||
return "Last year" |
|||
} |
|||
} |
|||
|
|||
if let month = components.month { |
|||
if (month >= 2) { |
|||
return "\(month) months" |
|||
} else if (month >= 1) { |
|||
return "Last month" |
|||
} |
|||
} |
|||
|
|||
if let weekOfMonth = components.weekOfMonth { |
|||
if (weekOfMonth >= 2) { |
|||
return "\(weekOfMonth) weeks" |
|||
} else if (weekOfMonth >= 1) { |
|||
return "Last week" |
|||
} |
|||
} |
|||
|
|||
if let day = components.day { |
|||
if (day >= 2) { |
|||
return "\(day) days" |
|||
} else if (day >= 1) { |
|||
return "Yesterday" |
|||
} |
|||
} |
|||
|
|||
if let hour = components.hour { |
|||
if (hour >= 2) { |
|||
return "\(hour) hours" |
|||
} else if (hour >= 1) { |
|||
return "An hour ago" |
|||
} |
|||
} |
|||
|
|||
if let minute = components.minute { |
|||
if (minute >= 2) { |
|||
return "\(minute) mins" |
|||
} else if (minute >= 1) { |
|||
return "A minute ago" |
|||
} |
|||
} |
|||
|
|||
if let second = components.second { |
|||
if (second >= 3) { |
|||
return "\(second) secs" |
|||
} |
|||
} |
|||
|
|||
return "Just now" |
|||
} |
|||
} |
@ -0,0 +1,231 @@ |
|||
// |
|||
// MastodonDataManager.swift |
|||
// elpha-ios |
|||
// |
|||
// Created by Dwayne Harris on 10/1/18. |
|||
// Copyright © 2018 Elpha. All rights reserved. |
|||
// |
|||
|
|||
import CoreData |
|||
import Foundation |
|||
import MastodonKit |
|||
|
|||
public class MastodonDataManager { |
|||
static func upsertAccount(_ remoteAccount: Account) -> AccountMO? { |
|||
func saveAccount(_ account: AccountMO) -> AccountMO? { |
|||
account.id = remoteAccount.id |
|||
account.username = remoteAccount.username |
|||
account.acct = remoteAccount.acct |
|||
account.displayName = remoteAccount.displayName |
|||
account.note = remoteAccount.note |
|||
account.url = remoteAccount.url |
|||
account.avatarURL = URL(string: remoteAccount.avatar) |
|||
account.avatarStaticURL = URL(string: remoteAccount.avatarStatic) |
|||
account.headerURL = URL(string: remoteAccount.header) |
|||
account.headerStaticURL = URL(string: remoteAccount.headerStatic) |
|||
account.locked = remoteAccount.locked |
|||
account.createdAt = remoteAccount.createdAt |
|||
account.followersCount = Int32(remoteAccount.followersCount) |
|||
account.followingCount = Int32(remoteAccount.followingCount) |
|||
account.statusesCount = Int32(remoteAccount.statusesCount) |
|||
|
|||
return account |
|||
} |
|||
|
|||
let context = CoreDataManager.shared.getContext() |
|||
let request = NSFetchRequest<AccountMO>(entityName: "Account") |
|||
request.predicate = NSPredicate(format: "acct == %@", remoteAccount.acct) |
|||
|
|||
do { |
|||
let results = try context.fetch(request) |
|||
if let account = results.first { |
|||
return saveAccount(account) |
|||
} else { |
|||
return saveAccount(AccountMO(context: context)) |
|||
} |
|||
} catch { |
|||
print("\(error)") |
|||
return nil |
|||
} |
|||
} |
|||
|
|||
static func upsertAttachment(_ remoteAttachment: Attachment) -> AttachmentMO? { |
|||
func saveAttachment(_ attachment: AttachmentMO) -> AttachmentMO? { |
|||
attachment.id = remoteAttachment.id |
|||
attachment.type = remoteAttachment.type.rawValue |
|||
attachment.url = URL(string: remoteAttachment.url) |
|||
attachment.previewURL = URL(string: remoteAttachment.previewURL) |
|||
attachment.remoteURL = remoteAttachment.remoteURL |
|||
attachment.textURL = remoteAttachment.textURL |
|||
|
|||
return attachment |
|||
} |
|||
|
|||
let context = CoreDataManager.shared.getContext() |
|||
let request = NSFetchRequest<AttachmentMO>(entityName: "Attachment") |
|||
request.predicate = NSPredicate(format: "id == %@", remoteAttachment.id) |
|||
|
|||
do { |
|||
let results = try context.fetch(request) |
|||
if let attachment = results.first { |
|||
return saveAttachment(attachment) |
|||
} else { |
|||
return saveAttachment(AttachmentMO(context: context)) |
|||
} |
|||
} catch { |
|||
print("\(error)") |
|||
return nil |
|||
} |
|||
} |
|||
|
|||
static func upsertMention(_ remoteMention: Mention) -> MentionMO? { |
|||
func saveMention(_ mention: MentionMO) -> MentionMO? { |
|||
mention.id = remoteMention.id |
|||
mention.username = remoteMention.username |
|||
mention.acct = remoteMention.acct |
|||
mention.url = URL(string: remoteMention.url) |
|||
|
|||
return mention |
|||
} |
|||
|
|||
let context = CoreDataManager.shared.getContext() |
|||
let request = NSFetchRequest<MentionMO>(entityName: "Mention") |
|||
request.predicate = NSPredicate(format: "id == %@", remoteMention.id) |
|||
|
|||
do { |
|||
let results = try context.fetch(request) |
|||
if let mention = results.first { |
|||
return saveMention(mention) |
|||
} else { |
|||
return saveMention(MentionMO(context: context)) |
|||
} |
|||
} catch { |
|||
print("\(error)") |
|||
return nil |
|||
} |
|||
} |
|||
|
|||
static func upsertTag(_ remoteTag: Tag) -> TagMO? { |
|||
func saveTag(_ tag: TagMO) -> TagMO? { |
|||
tag.name = remoteTag.name |
|||
tag.url = URL(string: remoteTag.url) |
|||
|
|||
return tag |
|||
} |
|||
|
|||
let context = CoreDataManager.shared.getContext() |
|||
let request = NSFetchRequest<TagMO>(entityName: "Tag") |
|||
request.predicate = NSPredicate(format: "name == %@", remoteTag.name) |
|||
|
|||
do { |
|||
let results = try context.fetch(request) |
|||
if let tag = results.first { |
|||
return saveTag(tag) |
|||
} else { |
|||
return saveTag(TagMO(context: context)) |
|||
} |
|||
} catch { |
|||
print("\(error)") |
|||
return nil |
|||
} |
|||
} |
|||
|
|||
static func upsertApp(_ remoteApp: Application) -> AppMO? { |
|||
func saveApp(_ app: AppMO) -> AppMO? { |
|||
app.name = remoteApp.name |
|||
|
|||
if let website = remoteApp.website { |
|||
app.website = URL(string: website) |
|||
} |
|||
|
|||
return app |
|||
} |
|||
|
|||
let context = CoreDataManager.shared.getContext() |
|||
let request = NSFetchRequest<AppMO>(entityName: "App") |
|||
request.predicate = NSPredicate(format: "name == %@", remoteApp.name) |
|||
|
|||
do { |
|||
let results = try context.fetch(request) |
|||
if let tag = results.first { |
|||
return saveApp(tag) |
|||
} else { |
|||
return saveApp(AppMO(context: context)) |
|||
} |
|||
} catch { |
|||
print("\(error)") |
|||
return nil |
|||
} |
|||
} |
|||
|
|||
static func upsertStatus(remoteStatus: Status) -> StatusMO? { |
|||
func saveStatus(_ status: StatusMO) throws -> StatusMO? { |
|||
status.id = remoteStatus.id |
|||
status.uri = URL(string: remoteStatus.uri) |
|||
status.url = remoteStatus.url |
|||
status.account = MastodonDataManager.upsertAccount(remoteStatus.account) |
|||
status.inReplyToID = remoteStatus.inReplyToID |
|||
status.inReplyToAccountID = remoteStatus.inReplyToAccountID |
|||
status.content = remoteStatus.content |
|||
status.createdAt = remoteStatus.createdAt |
|||
status.reblogsCount = Int32(remoteStatus.reblogsCount) |
|||
status.favouritesCount = Int32(remoteStatus.favouritesCount) |
|||
status.reblogged = remoteStatus.reblogged ?? false |
|||
status.favourited = remoteStatus.favourited ?? false |
|||
status.sensitive = remoteStatus.sensitive ?? false |
|||
status.spoilerText = remoteStatus.spoilerText |
|||
status.visibility = remoteStatus.visibility.rawValue |
|||
|
|||
if let app = remoteStatus.application { |
|||
status.app = MastodonDataManager.upsertApp(app) |
|||
} |
|||
|
|||
remoteStatus.mediaAttachments.forEach { attachment in |
|||
if let attachment = MastodonDataManager.upsertAttachment(attachment) { |
|||
status.mutableSetValue(forKey: "attachments").add(attachment) |
|||
} |
|||
} |
|||
|
|||
remoteStatus.mentions.forEach { mention in |
|||
if let mention = MastodonDataManager.upsertMention(mention) { |
|||
status.mutableSetValue(forKey: "mentions").add(mention) |
|||
} |
|||
} |
|||
|
|||
remoteStatus.tags.forEach { tag in |
|||
if let tag = MastodonDataManager.upsertTag(tag) { |
|||
status.mutableSetValue(forKey: "tags").add(tag) |
|||
} |
|||
} |
|||
|
|||
return status |
|||
} |
|||
|
|||
let context = CoreDataManager.shared.getContext() |
|||
let request = NSFetchRequest<StatusMO>(entityName: "Status") |
|||
request.predicate = NSPredicate(format: "id == %@", remoteStatus.id) |
|||
|
|||
do { |
|||
let results = try context.fetch(request) |
|||
if let status = results.first { |
|||
return try saveStatus(status) |
|||
} else { |
|||
return try saveStatus(StatusMO(context: context)) |
|||
} |
|||
} catch { |
|||
print("\(error)") |
|||
return nil |
|||
} |
|||
} |
|||
|
|||
static func saveClient(id: String, clientID: String, clientSecret: String, url: String) -> ClientMO { |
|||
let client = ClientMO(context: CoreDataManager.shared.getContext()) |
|||
client.id = id |
|||
client.clientID = clientID |
|||
client.clientSecret = clientSecret |
|||
client.url = url |
|||
|
|||
CoreDataManager.shared.saveContext() |
|||
return client |
|||
} |
|||
} |
@ -0,0 +1,18 @@ |
|||
// |
|||
// TimelineTableViewCell.swift |
|||
// elpha-ios |
|||
// |
|||
// Created by Dwayne Harris on 10/1/18. |
|||
// Copyright © 2018 Elpha. All rights reserved. |
|||
// |
|||
|
|||
import Foundation |
|||
import UIKit |
|||
|
|||
class TimelineTableViewCell: UITableViewCell { |
|||
@IBOutlet var avatarImageView: UIImageView! |
|||
@IBOutlet var displayNameLabel: UILabel! |
|||
@IBOutlet var acctLabel: UILabel! |
|||
@IBOutlet var timestampLabel: UILabel! |
|||
@IBOutlet var contentLabel: UILabel! |
|||
} |
@ -0,0 +1,210 @@ |
|||
// |
|||
// TimelineTableViewController.swift |
|||
// elpha-ios |
|||
// |
|||
// Created by Dwayne Harris on 10/1/18. |
|||
// Copyright © 2018 Elpha. All rights reserved. |
|||
// |
|||
|
|||
import AlamofireImage |
|||
import CoreData |
|||
import Foundation |
|||
import MastodonKit |
|||
import UIKit |
|||
|
|||
class TimelineTableViewController: UITableViewController { |
|||
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() |
|||
|
|||
refreshControl?.addTarget(self, action: #selector(self.fetchTimeline), for: .valueChanged) |
|||
|
|||
let statusesCount = getTimelineStatusesCount() |
|||
fetchTimeline() |
|||
|
|||
if statusesCount == 0 { |
|||
self.tableView.reloadData() |
|||
} else { |
|||
print("More data to load...") |
|||
self.tableView.reloadData() |
|||
} |
|||
} |
|||
|
|||
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() { |
|||
client.run(request) { result in |
|||
switch result { |
|||
case .success(let remoteStatuses, _): |
|||
let statuses = remoteStatuses.compactMap { status in |
|||
return MastodonDataManager.upsertStatus(remoteStatus: status) |
|||
} |
|||
|
|||
timeline.addToStatuses(NSSet(array: statuses)) |
|||
|
|||
CoreDataManager.shared.saveContext() |
|||
case .failure(let error): |
|||
print("\(error)") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@objc func fetchTimeline() { |
|||
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 |
|||
defer { |
|||
loading = false |
|||
} |
|||
|
|||
if let selectedTimeline = session.selectedTimeline { |
|||
switch selectedTimeline.name { |
|||
case "Home": |
|||
fetchStatuses(request: Timelines.home(), forTimeline: selectedTimeline) |
|||
case "Local": |
|||
fetchStatuses(request: Timelines.public(local: true), forTimeline: selectedTimeline) |
|||
case "Federated": |
|||
fetchStatuses(request: Timelines.public(local: false), forTimeline: selectedTimeline) |
|||
default: |
|||
print("Tags not implemented") |
|||
} |
|||
} |
|||
} |
|||
|
|||
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 |
|||
} |
|||
|
|||
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") |
|||
} |
|||
|
|||
if let statuses = getTimelineStatuses() { |
|||
let status = statuses[indexPath.row] |
|||
if let account = status.account { |
|||
let filter = AspectScaledToFillSizeWithRoundedCornersFilter( |
|||
size: CGSize(width: 40.0, height: 40.0), |
|||
radius: 20.0, |
|||
divideRadiusByImageScale: true |
|||
) |
|||
|
|||
cell.avatarImageView.af_setImage(withURL: account.avatarURL!, filter: filter) |
|||
cell.displayNameLabel.text = account.displayName |
|||
cell.acctLabel.text = account.acct |
|||
} |
|||
|
|||
if let content = status.content { |
|||
do { |
|||
let styledContent = "<style>html * { font-size: 16px; color: #170c49; font-family: -apple-system }</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)") |
|||
} |
|||
} |
|||
|
|||
cell.timestampLabel.text = status.createdAt!.timeAgo() |
|||
|
|||
return cell |
|||
} |
|||
|
|||
return cell |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue