diff --git a/Artwork/Icons/Download.svg b/Artwork/Icons/Download.svg
new file mode 100644
index 00000000..14faa937
--- /dev/null
+++ b/Artwork/Icons/Download.svg
@@ -0,0 +1,91 @@
+
+
+
+
diff --git a/Artwork/Icons/Link.svg b/Artwork/Icons/Link.svg
new file mode 100644
index 00000000..d59bf072
--- /dev/null
+++ b/Artwork/Icons/Link.svg
@@ -0,0 +1,78 @@
+
+
+
+
diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj
index bbf451a4..ffacf2ad 100644
--- a/Tusker.xcodeproj/project.pbxproj
+++ b/Tusker.xcodeproj/project.pbxproj
@@ -13,6 +13,13 @@
D6333B372137838300CE884A /* AttributedString+Trim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Trim.swift */; };
D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B762138D94E00CE884A /* ComposeMediaView.swift */; };
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
+ D641C771213CA9EC004B4513 /* Notifications.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D641C770213CA9EC004B4513 /* Notifications.storyboard */; };
+ D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */; };
+ D641C777213CAA9E004B4513 /* ActionNotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C776213CAA9E004B4513 /* ActionNotificationTableViewCell.swift */; };
+ D641C779213CAC56004B4513 /* ActionNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D641C778213CAC56004B4513 /* ActionNotificationTableViewCell.xib */; };
+ D641C77B213CB017004B4513 /* FollowNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D641C77A213CB017004B4513 /* FollowNotificationTableViewCell.xib */; };
+ D641C77D213CB024004B4513 /* FollowNotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77C213CB024004B4513 /* FollowNotificationTableViewCell.swift */; };
+ D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; };
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; };
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */; };
@@ -100,6 +107,13 @@
D6333B362137838300CE884A /* AttributedString+Trim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Trim.swift"; sourceTree = ""; };
D6333B762138D94E00CE884A /* ComposeMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMediaView.swift; sourceTree = ""; };
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = ""; };
+ D641C770213CA9EC004B4513 /* Notifications.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Notifications.storyboard; sourceTree = ""; };
+ D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = ""; };
+ D641C776213CAA9E004B4513 /* ActionNotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionNotificationTableViewCell.swift; sourceTree = ""; };
+ D641C778213CAC56004B4513 /* ActionNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ActionNotificationTableViewCell.xib; sourceTree = ""; };
+ D641C77A213CB017004B4513 /* FollowNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FollowNotificationTableViewCell.xib; sourceTree = ""; };
+ D641C77C213CB024004B4513 /* FollowNotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowNotificationTableViewCell.swift; sourceTree = ""; };
+ D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = ""; };
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageExpandAnimationController.swift; sourceTree = ""; };
D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageShrinkAnimationController.swift; sourceTree = ""; };
D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageInteractionController.swift; sourceTree = ""; };
@@ -227,6 +241,11 @@
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */,
D6333B762138D94E00CE884A /* ComposeMediaView.swift */,
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */,
+ D641C778213CAC56004B4513 /* ActionNotificationTableViewCell.xib */,
+ D641C776213CAA9E004B4513 /* ActionNotificationTableViewCell.swift */,
+ D641C77A213CB017004B4513 /* FollowNotificationTableViewCell.xib */,
+ D641C77C213CB024004B4513 /* FollowNotificationTableViewCell.swift */,
+ D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
);
path = Views;
sourceTree = "";
@@ -304,6 +323,7 @@
D663626721360E2C00C9CBA2 /* PreferencesTableViewController.swift */,
D66362702136338600C9CBA2 /* ComposeViewController.swift */,
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */,
+ D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */,
);
path = "View Controllers";
sourceTree = "";
@@ -319,6 +339,7 @@
D663626521360DD700C9CBA2 /* Preferences.storyboard */,
D663626E213632A000C9CBA2 /* Compose.storyboard */,
D6C94D842139DFD800CB5196 /* LargeImage.storyboard */,
+ D641C770213CA9EC004B4513 /* Notifications.storyboard */,
);
path = Storyboards;
sourceTree = "";
@@ -436,6 +457,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ D641C779213CAC56004B4513 /* ActionNotificationTableViewCell.xib in Resources */,
D667E5F32135BC260057A976 /* Conversation.storyboard in Resources */,
D667E5E921349EE50057A976 /* ProfileHeaderTableViewCell.xib in Resources */,
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
@@ -444,9 +466,11 @@
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */,
D6F953EE21251A0700CF0F2B /* Timeline.storyboard in Resources */,
D663626F213632A000C9CBA2 /* Compose.storyboard in Resources */,
+ D641C77B213CB017004B4513 /* FollowNotificationTableViewCell.xib in Resources */,
D6C94D852139DFD800CB5196 /* LargeImage.storyboard in Resources */,
D6D4DDD5212518A000E1C4BB /* Main.storyboard in Resources */,
D667E5E3213499F70057A976 /* Profile.storyboard in Resources */,
+ D641C771213CA9EC004B4513 /* Notifications.storyboard in Resources */,
D663626621360DD700C9CBA2 /* Preferences.storyboard in Resources */,
D667E5E12134937B0057A976 /* StatusTableViewCell.xib in Resources */,
);
@@ -490,9 +514,11 @@
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */,
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
+ D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
D6333B372137838300CE884A /* AttributedString+Trim.swift in Sources */,
D667E5EF2134C39F0057A976 /* StatusContentLabel.swift in Sources */,
+ D641C777213CAA9E004B4513 /* ActionNotificationTableViewCell.swift in Sources */,
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
D663626821360E2C00C9CBA2 /* PreferencesTableViewController.swift in Sources */,
D66362732136FFC600C9CBA2 /* UITextView+Placeholder.swift in Sources */,
@@ -501,6 +527,8 @@
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */,
D663626A2136163000C9CBA2 /* PreferencesAdaptive.swift in Sources */,
D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */,
+ D641C77D213CB024004B4513 /* FollowNotificationTableViewCell.swift in Sources */,
+ D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */,
D64A0CD32132153900640E3B /* HTMLContentLabel.swift in Sources */,
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */,
diff --git a/Tusker/Assets.xcassets/Link.imageset/Contents.json b/Tusker/Assets.xcassets/Link.imageset/Contents.json
new file mode 100644
index 00000000..c587ac7f
--- /dev/null
+++ b/Tusker/Assets.xcassets/Link.imageset/Contents.json
@@ -0,0 +1,25 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "Link.pdf",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template",
+ "preserves-vector-representation" : true
+ }
+}
\ No newline at end of file
diff --git a/Tusker/Assets.xcassets/Link.imageset/Link.pdf b/Tusker/Assets.xcassets/Link.imageset/Link.pdf
new file mode 100644
index 00000000..8d970876
--- /dev/null
+++ b/Tusker/Assets.xcassets/Link.imageset/Link.pdf
@@ -0,0 +1,70 @@
+%PDF-1.5
+%
+3 0 obj
+<< /Length 4 0 R
+ /Filter /FlateDecode
+>>
+stream
+x}RN1+؛HH
+GBP88znZqfq$
^?'|)T|x|D _ DU16VQJ[L83pG.cpȐ"
~imQ"P/-%:n)NdGEU9sby,2SβpF}]$չE;zC3Nc);U2'
+3ZQ̟W&:`cj7\J^_,yX-䶛FO;va +>:lGeuO\ф{!q+luۥ>
+endstream
+endobj
+4 0 obj
+ 330
+endobj
+2 0 obj
+<<
+ /ExtGState <<
+ /a0 << /CA 1 /ca 1 >>
+ >>
+>>
+endobj
+5 0 obj
+<< /Type /Page
+ /Parent 1 0 R
+ /MediaBox [ 0 0 280.629913 280.629944 ]
+ /Contents 3 0 R
+ /Group <<
+ /Type /Group
+ /S /Transparency
+ /I true
+ /CS /DeviceRGB
+ >>
+ /Resources 2 0 R
+>>
+endobj
+1 0 obj
+<< /Type /Pages
+ /Kids [ 5 0 R ]
+ /Count 1
+>>
+endobj
+6 0 obj
+<< /Creator (cairo 1.14.8 (http://cairographics.org))
+ /Producer (cairo 1.14.8 (http://cairographics.org))
+>>
+endobj
+7 0 obj
+<< /Type /Catalog
+ /Pages 1 0 R
+>>
+endobj
+xref
+0 8
+0000000000 65535 f
+0000000744 00000 n
+0000000444 00000 n
+0000000015 00000 n
+0000000422 00000 n
+0000000516 00000 n
+0000000809 00000 n
+0000000936 00000 n
+trailer
+<< /Size 8
+ /Root 7 0 R
+ /Info 6 0 R
+>>
+startxref
+988
+%%EOF
diff --git a/Tusker/Storyboards/Notifications.storyboard b/Tusker/Storyboards/Notifications.storyboard
new file mode 100644
index 00000000..25f4fb60
--- /dev/null
+++ b/Tusker/Storyboards/Notifications.storyboard
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tusker/Storyboards/Timeline.storyboard b/Tusker/Storyboards/Timeline.storyboard
index 830d3ad9..b2ce2806 100644
--- a/Tusker/Storyboards/Timeline.storyboard
+++ b/Tusker/Storyboards/Timeline.storyboard
@@ -33,7 +33,7 @@
-
+
diff --git a/Tusker/View Controllers/ConversationViewController.swift b/Tusker/View Controllers/ConversationViewController.swift
index 9b17ddea..679380c0 100644
--- a/Tusker/View Controllers/ConversationViewController.swift
+++ b/Tusker/View Controllers/ConversationViewController.swift
@@ -114,11 +114,9 @@ class ConversationViewController: UIViewController, UITableViewDataSource, UITab
}
}
- func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
let status = statuses[indexPath.row]
- guard status != mainStatus else { return }
- guard let cell = tableView.cellForRow(at: indexPath) as? StatusTableViewCell else { fatalError() }
- cell.didSelect()
+ return status == mainStatus ? nil : indexPath
}
}
diff --git a/Tusker/View Controllers/MainTabBarViewController.swift b/Tusker/View Controllers/MainTabBarViewController.swift
index 95afd1fc..8cd36ef1 100644
--- a/Tusker/View Controllers/MainTabBarViewController.swift
+++ b/Tusker/View Controllers/MainTabBarViewController.swift
@@ -17,6 +17,7 @@ class MainTabBarViewController: UITabBarController {
TimelineTableViewController.create(for: .home),
TimelineTableViewController.create(for: .federated),
TimelineTableViewController.create(for: .local),
+ NotificationsTableViewController.create(),
PreferencesTableViewController.create()
]
}
diff --git a/Tusker/View Controllers/NotificationsTableViewController.swift b/Tusker/View Controllers/NotificationsTableViewController.swift
new file mode 100644
index 00000000..34280528
--- /dev/null
+++ b/Tusker/View Controllers/NotificationsTableViewController.swift
@@ -0,0 +1,140 @@
+//
+// NotificationsTableViewController.swift
+// Tusker
+//
+// Created by Shadowfacts on 9/2/18.
+// Copyright © 2018 Shadowfacts. All rights reserved.
+//
+
+import UIKit
+import MastodonKit
+
+class NotificationsTableViewController: UITableViewController {
+
+ static func create() -> UIViewController {
+ guard let navigationController = UIStoryboard(name: "Notifications", bundle: nil).instantiateInitialViewController() as? UINavigationController else { fatalError() }
+ return navigationController
+ }
+
+ var notifications: [MastodonKit.Notification] = [] {
+ didSet {
+ DispatchQueue.main.async {
+ self.tableView.reloadData()
+ }
+ }
+ }
+
+ var newer: RequestRange?
+ var older: RequestRange?
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ // Uncomment the following line to preserve selection between presentations
+ // self.clearsSelectionOnViewWillAppear = false
+
+ // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
+ // self.navigationItem.rightBarButtonItem = self.editButtonItem
+
+ tableView.rowHeight = UITableView.automaticDimension
+ tableView.estimatedRowHeight = 140
+
+ tableView.register(UINib(nibName: "StatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
+ tableView.register(UINib(nibName: "ActionNotificationTableViewCell", bundle: nil), forCellReuseIdentifier: "actionCell")
+ tableView.register(UINib(nibName: "FollowNotificationTableViewCell", bundle: nil), forCellReuseIdentifier: "followCell")
+
+ let req = Notifications.all()
+ MastodonController.shared.client.run(req) { result in
+ guard case let .success(notifications, pagination) = result else { fatalError() }
+ self.notifications = notifications
+ self.newer = pagination?.previous
+ self.older = pagination?.next
+ }
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+
+ for cell in tableView.visibleCells {
+ if let cell = cell as? PreferencesAdaptive {
+ cell.updateUIForPreferences()
+ }
+ }
+ }
+
+ /*
+ // MARK: - Navigation
+
+ // In a storyboard-based application, you will often want to do a little preparation before navigation
+ override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+ // Get the new view controller using segue.destination.
+ // Pass the selected object to the new view controller.
+ }
+ */
+
+ // MARK: - Table view data source
+
+ override func numberOfSections(in tableView: UITableView) -> Int {
+ return 1
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return notifications.count
+ }
+
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let notification = notifications[indexPath.row]
+
+ switch notification.type {
+ case .mention:
+ guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() }
+ let status = notification.status!
+ cell.updateUI(for: status)
+ cell.delegate = self
+ return cell
+ case .favourite, .reblog:
+ guard let cell = tableView.dequeueReusableCell(withIdentifier: "actionCell", for: indexPath) as? ActionNotificationTableViewCell else { fatalError() }
+ cell.updateUI(for: notification)
+ cell.delegate = self
+ return cell
+ case .follow:
+ guard let cell = tableView.dequeueReusableCell(withIdentifier: "followCell", for: indexPath) as? FollowNotificationTableViewCell else { fatalError() }
+ cell.updateUI(for: notification)
+ cell.delegate = self
+ return cell
+ }
+ }
+
+
+ override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
+ if indexPath.row == notifications.count - 1 {
+ guard let older = older else { return }
+
+ let req = Notifications.all(range: older)
+ MastodonController.shared.client.run(req) { result in
+ guard case let .success(newNotifications, pagination) = result else { fatalError() }
+ self.older = pagination?.next
+ self.notifications.append(contentsOf: newNotifications)
+ }
+ }
+ }
+
+ @IBAction func refreshNotifications(_ sender: Any) {
+ guard let newer = newer else { return }
+
+ let req = Notifications.all(range: newer)
+ MastodonController.shared.client.run(req) { result in
+ guard case let .success(newNotifications, pagination) = result else { fatalError() }
+ self.newer = pagination?.previous
+ self.notifications.insert(contentsOf: newNotifications, at: 0)
+ DispatchQueue.main.async {
+ self.refreshControl?.endRefreshing()
+
+ // maintain the current position in the list (don't scroll to top)
+ self.tableView.scrollToRow(at: IndexPath(row: newNotifications.count, section: 0), at: .top, animated: false)
+ }
+ }
+ }
+
+}
diff --git a/Tusker/View Controllers/ProfileTableViewController.swift b/Tusker/View Controllers/ProfileTableViewController.swift
index 48e5a52b..4d0efe68 100644
--- a/Tusker/View Controllers/ProfileTableViewController.swift
+++ b/Tusker/View Controllers/ProfileTableViewController.swift
@@ -123,12 +123,6 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive {
}
}
- override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- guard indexPath.section == 1 else { return }
- guard let cell = tableView.cellForRow(at: indexPath) as? StatusTableViewCell else { fatalError() }
- cell.didSelect()
- }
-
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.section == 1 && indexPath.row == statuses.count - 1 {
guard let older = older else { return }
diff --git a/Tusker/View Controllers/TimelineTableViewController.swift b/Tusker/View Controllers/TimelineTableViewController.swift
index 69dbaf11..7a7911aa 100644
--- a/Tusker/View Controllers/TimelineTableViewController.swift
+++ b/Tusker/View Controllers/TimelineTableViewController.swift
@@ -103,11 +103,6 @@ class TimelineTableViewController: UITableViewController {
return cell
}
- override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- guard let cell = tableView.cellForRow(at: indexPath) as? StatusTableViewCell else { fatalError() }
- cell.didSelect()
- }
-
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == statuses.count - 1 {
guard let older = older else { return }
diff --git a/Tusker/Views/ActionNotificationTableViewCell.swift b/Tusker/Views/ActionNotificationTableViewCell.swift
new file mode 100644
index 00000000..23214b3a
--- /dev/null
+++ b/Tusker/Views/ActionNotificationTableViewCell.swift
@@ -0,0 +1,201 @@
+//
+// ActionNotificationTableViewCell.swift
+// Tusker
+//
+// Created by Shadowfacts on 9/2/18.
+// Copyright © 2018 Shadowfacts. All rights reserved.
+//
+
+import UIKit
+import MastodonKit
+
+class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive {
+
+ var delegate: StatusTableViewCellDelegate?
+
+ @IBOutlet weak var displayNameLabel: UILabel!
+ @IBOutlet weak var usernameLabel: UILabel!
+ @IBOutlet weak var contentLabel: StatusContentLabel!
+ @IBOutlet weak var opAvatarImageView: UIImageView!
+ @IBOutlet weak var actionAvatarImageView: UIImageView!
+ @IBOutlet weak var actionLabel: UILabel!
+ @IBOutlet weak var timestampLabel: UILabel!
+ @IBOutlet weak var attachmentsView: UIStackView!
+
+ var notification: MastodonKit.Notification!
+ var status: Status!
+
+ var opAvatarURL: URL?
+ var actionAvatarURL: URL?
+ var updateTimestampWorkItem: DispatchWorkItem?
+
+ override func awakeFromNib() {
+ displayNameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
+ displayNameLabel.isUserInteractionEnabled = true
+ usernameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
+ usernameLabel.isUserInteractionEnabled = true
+ opAvatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
+ opAvatarImageView.isUserInteractionEnabled = true
+ opAvatarImageView.layer.masksToBounds = true
+ actionAvatarImageView.layer.masksToBounds = true
+ actionLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(actionPressed)))
+ actionLabel.isUserInteractionEnabled = true
+ contentLabel.delegate = self
+ }
+
+ func updateUIForPreferences() {
+ opAvatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: opAvatarImageView)
+ actionAvatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: actionAvatarImageView)
+ displayNameLabel.text = status.account.realDisplayName
+
+ let verb: String
+ switch notification.type {
+ case .favourite:
+ verb = "Liked"
+ case .reblog:
+ verb = "Reblogged"
+ default:
+ fatalError("Invalid notification type \(notification.type) for ActionNotificationTableViewCell")
+ }
+ actionLabel.text = "\(verb) by \(notification.account.realDisplayName)"
+ }
+
+ func updateUI(for notification: MastodonKit.Notification) {
+ guard notification.type == .favourite || notification.type == .reblog else {
+ fatalError("Invalid notification type \(notification.type) for ActionNotificationTableViewCell")
+ }
+ self.notification = notification
+ self.status = notification.status!
+
+ updateUIForPreferences()
+
+ usernameLabel.text = "@\(status.account.acct)"
+ opAvatarImageView.image = nil
+ if let url = URL(string: status.account.avatar) {
+ opAvatarURL = url
+ AvatarCache.shared.get(url) { image in
+ DispatchQueue.main.async {
+ self.opAvatarImageView.image = image
+ self.opAvatarURL = nil
+ }
+ }
+ }
+ actionAvatarImageView.image = nil
+ if let url = URL(string: notification.account.avatar) {
+ actionAvatarURL = url
+ AvatarCache.shared.get(url) { image in
+ DispatchQueue.main.async {
+ self.actionAvatarImageView.image = image
+ self.actionAvatarURL = nil
+ }
+ }
+ }
+ updateTimestamp()
+ let attachments = status.mediaAttachments.filter({ $0.type == .image })
+ if attachments.count > 0 {
+ attachmentsView.isHidden = false
+ for attachment in attachments {
+ guard let url = URL(string: attachment.textURL ?? attachment.url) else { continue }
+ let label = UILabel()
+ label.textColor = .darkGray
+
+ let textAttachment = InlineTextAttachment()
+ textAttachment.image = UIImage(named: "Link")!
+ textAttachment.bounds = CGRect(x: 0, y: 0, width: label.font.pointSize, height: label.font.pointSize)
+ textAttachment.fontDescender = label.font.descender
+ let attachmentStr = NSAttributedString(attachment: textAttachment)
+ let text = NSMutableAttributedString(string: " ")
+ text.append(attachmentStr)
+ text.append(NSAttributedString(string: " "))
+// text.addAttribute(.font, value: UIFont.systemFont(ofSize: 0), range: NSRange(location: 0, length: text.length))
+
+ text.append(NSAttributedString(string: "\(url.lastPathComponent)"))
+ text.addAttribute(.foregroundColor, value: UIColor.red, range: NSRange(location: 0, length: 2))
+
+
+
+// let text = NSMutableAttributedString(string: " \(url.lastPathComponent)")
+// let imageAttachment = InlineTextAttachment()
+// imageAttachment.image = UIImage(named: "Link")!
+// imageAttachment.bounds = CGRect(x: 0, y: 0, width: label.font.pointSize, height: label.font.pointSize)
+// imageAttachment.fontDescender = label.font.descender
+// let imageStr = NSMutableAttributedString(attachment: imageAttachment)
+// imageStr.setAttributes([.foregroundColor: UIColor.darkGray], range: )
+// text.insert(imageStr, at: 0)
+
+ label.attributedText = text
+ attachmentsView.addArrangedSubview(label)
+ }
+ } else {
+ attachmentsView.isHidden = true
+ }
+
+ contentLabel.status = status
+ }
+
+ func updateTimestamp() {
+ timestampLabel.text = status.createdAt.timeAgoString()
+ let delay: DispatchTimeInterval?
+ switch status.createdAt.timeAgo().1 {
+ case .second:
+ delay = .seconds(10)
+ case .minute:
+ delay = .seconds(60)
+ default:
+ delay = nil
+ }
+ if let delay = delay {
+ updateTimestampWorkItem = DispatchWorkItem {
+ self.updateTimestamp()
+ }
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)
+ } else {
+ updateTimestampWorkItem = nil
+ }
+ }
+
+ override func prepareForReuse() {
+ if let url = opAvatarURL {
+ AvatarCache.shared.cancel(url)
+ }
+ if let url = actionAvatarURL {
+ AvatarCache.shared.cancel(url)
+ }
+ updateTimestampWorkItem?.cancel()
+ updateTimestampWorkItem = nil
+ attachmentsView.subviews.forEach { $0.removeFromSuperview() }
+ }
+
+ override func setSelected(_ selected: Bool, animated: Bool) {
+ super.setSelected(selected, animated: animated)
+
+ if selected {
+ delegate?.selected(status: status)
+ }
+ }
+
+ @objc func accountPressed() {
+ delegate?.selected(account: status.account)
+ }
+
+ @objc func actionPressed() {
+ delegate?.selected(account: notification.account)
+ }
+
+}
+
+extension ActionNotificationTableViewCell: HTMLContentLabelDelegate {
+
+ func selected(mention: Mention) {
+ delegate?.selected(mention: mention)
+ }
+
+ func selected(tag: Tag) {
+ delegate?.selected(tag: tag)
+ }
+
+ func selected(url: URL) {
+ delegate?.selected(url: url)
+ }
+
+}
diff --git a/Tusker/Views/ActionNotificationTableViewCell.xib b/Tusker/Views/ActionNotificationTableViewCell.xib
new file mode 100644
index 00000000..4d4b8c65
--- /dev/null
+++ b/Tusker/Views/ActionNotificationTableViewCell.xib
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tusker/Views/FollowNotificationTableViewCell.swift b/Tusker/Views/FollowNotificationTableViewCell.swift
new file mode 100644
index 00000000..7681404b
--- /dev/null
+++ b/Tusker/Views/FollowNotificationTableViewCell.swift
@@ -0,0 +1,97 @@
+//
+// FollowNotificationTableViewCell.swift
+// Tusker
+//
+// Created by Shadowfacts on 9/2/18.
+// Copyright © 2018 Shadowfacts. All rights reserved.
+//
+
+import UIKit
+import MastodonKit
+
+class FollowNotificationTableViewCell: UITableViewCell, PreferencesAdaptive {
+
+ var delegate: StatusTableViewCellDelegate?
+
+ @IBOutlet weak var followLabel: UILabel!
+ @IBOutlet weak var timestampLabel: UILabel!
+ @IBOutlet weak var avatarImageView: UIImageView!
+ @IBOutlet weak var displayNameLabel: UILabel!
+ @IBOutlet weak var usernameLabel: UILabel!
+
+ var notification: MastodonKit.Notification!
+ var account: Account!
+
+ var avatarURL: URL?
+ var updateTimestampWorkItem: DispatchWorkItem?
+
+ override func awakeFromNib() {
+ super.awakeFromNib()
+
+ avatarImageView.layer.masksToBounds = true
+ }
+
+ func updateUIForPreferences() {
+ avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
+ followLabel.text = "Followed by \(account.realDisplayName)"
+ displayNameLabel.text = account.realDisplayName
+ }
+
+ func updateUI(for notification: MastodonKit.Notification) {
+ self.notification = notification
+ self.account = notification.account
+
+ updateUIForPreferences()
+
+ usernameLabel.text = "@\(account.acct)"
+ avatarImageView.image = nil
+ if let url = URL(string: account.avatar) {
+ avatarURL = url
+ AvatarCache.shared.get(url) { image in
+ DispatchQueue.main.async {
+ self.avatarImageView.image = image
+ self.avatarURL = nil
+ }
+ }
+ }
+ updateTimestamp()
+ }
+
+ func updateTimestamp() {
+ timestampLabel.text = notification.createdAt.timeAgoString()
+ let delay: DispatchTimeInterval?
+ switch notification.createdAt.timeAgo().1 {
+ case .second:
+ delay = .seconds(10)
+ case .minute:
+ delay = .seconds(60)
+ default:
+ delay = nil
+ }
+ if let delay = delay {
+ updateTimestampWorkItem = DispatchWorkItem {
+ self.updateTimestamp()
+ }
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)
+ } else {
+ updateTimestampWorkItem = nil
+ }
+ }
+
+ override func prepareForReuse() {
+ if let url = avatarURL {
+ AvatarCache.shared.cancel(url)
+ }
+ updateTimestampWorkItem?.cancel()
+ updateTimestampWorkItem = nil
+ }
+
+ override func setSelected(_ selected: Bool, animated: Bool) {
+ super.setSelected(selected, animated: animated)
+
+ if selected {
+ delegate?.selected(account: account)
+ }
+ }
+
+}
diff --git a/Tusker/Views/FollowNotificationTableViewCell.xib b/Tusker/Views/FollowNotificationTableViewCell.xib
new file mode 100644
index 00000000..cff3d2db
--- /dev/null
+++ b/Tusker/Views/FollowNotificationTableViewCell.xib
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tusker/Views/InlineTextAttachment.swift b/Tusker/Views/InlineTextAttachment.swift
new file mode 100644
index 00000000..13d5a361
--- /dev/null
+++ b/Tusker/Views/InlineTextAttachment.swift
@@ -0,0 +1,21 @@
+//
+// InlineTextAttachment.swift
+// Tusker
+//
+// Created by Shadowfacts on 9/3/18.
+// Copyright © 2018 Shadowfacts. All rights reserved.
+//
+
+import UIKit
+
+class InlineTextAttachment: NSTextAttachment {
+
+ var fontDescender: CGFloat!
+
+ override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect {
+ var rect = super.attachmentBounds(for: textContainer, proposedLineFragment: lineFrag, glyphPosition: position, characterIndex: charIndex)
+ rect.origin.y = fontDescender
+ return rect
+ }
+
+}
diff --git a/Tusker/Views/ProfileHeaderTableViewCell.swift b/Tusker/Views/ProfileHeaderTableViewCell.swift
index 6b0fe1ae..47d7b5bb 100644
--- a/Tusker/Views/ProfileHeaderTableViewCell.swift
+++ b/Tusker/Views/ProfileHeaderTableViewCell.swift
@@ -66,9 +66,7 @@ class ProfileHeaderTableViewCell: UITableViewCell, PreferencesAdaptive {
}
if let url = URL(string: account.header) {
headerImageDownloadTask = URLSession.shared.dataTask(with: url) { data, response, error in
- guard error == nil,
- let data = data,
- let image = UIImage(data: data) else { return }
+ guard error == nil, let data = data, let image = UIImage(data: data) else { return }
DispatchQueue.main.async {
self.headerImageView.image = image
self.headerImageDownloadTask = nil
diff --git a/Tusker/Views/StatusTableViewCell.swift b/Tusker/Views/StatusTableViewCell.swift
index 9cf058df..0645a823 100644
--- a/Tusker/Views/StatusTableViewCell.swift
+++ b/Tusker/Views/StatusTableViewCell.swift
@@ -44,9 +44,7 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
var reblogger: Account?
var avatarURL: URL?
-
var updateTimestampWorkItem: DispatchWorkItem?
-
var attachmentDataTasks: [URLSessionDataTask] = []
override func awakeFromNib() {
@@ -61,6 +59,7 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
avatarImageView.layer.masksToBounds = true
attachmentsView.layer.cornerRadius = 5
attachmentsView.layer.masksToBounds = true
+ contentLabel.delegate = self
}
func updateUIForPreferences() {
@@ -102,7 +101,6 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
}
updateTimestamp()
- attachmentsView.subviews.forEach { $0.removeFromSuperview() }
let attachments = status.mediaAttachments.filter({ $0.type == .image })
if attachments.count > 0 {
attachmentsView.isHidden = false
@@ -132,7 +130,6 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
}
contentLabel.status = status
- contentLabel.delegate = self
}
func updateTimestamp() {
@@ -170,9 +167,18 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
updateTimestampWorkItem = nil
attachmentsView.subviews.forEach { view in
(view as? AttachmentView)?.task?.cancel()
+ view.removeFromSuperview()
}
}
+ override func setSelected(_ selected: Bool, animated: Bool) {
+ super.setSelected(selected, animated: animated)
+
+ if selected {
+ delegate?.selected(status: status)
+ }
+ }
+
@IBAction func replyPressed(_ sender: Any) {
delegate?.reply(to: status)
}
@@ -186,10 +192,6 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
delegate?.selected(account: reblogger)
}
- func didSelect() {
- delegate?.selected(status: status)
- }
-
}
extension StatusTableViewCell: HTMLContentLabelDelegate {