From b3f5ba7341a797bdeb5a3197b11b99b61304f54a Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 3 Sep 2018 16:54:03 -0400 Subject: [PATCH] Add notifications screen --- Artwork/Icons/Download.svg | 91 ++++++++ Artwork/Icons/Link.svg | 78 +++++++ Tusker.xcodeproj/project.pbxproj | 28 +++ .../Link.imageset/Contents.json | 25 +++ Tusker/Assets.xcassets/Link.imageset/Link.pdf | 70 ++++++ Tusker/Storyboards/Notifications.storyboard | 55 +++++ Tusker/Storyboards/Timeline.storyboard | 2 +- .../ConversationViewController.swift | 6 +- .../MainTabBarViewController.swift | 1 + .../NotificationsTableViewController.swift | 140 ++++++++++++ .../ProfileTableViewController.swift | 6 - .../TimelineTableViewController.swift | 5 - .../ActionNotificationTableViewCell.swift | 201 ++++++++++++++++++ .../Views/ActionNotificationTableViewCell.xib | 136 ++++++++++++ .../FollowNotificationTableViewCell.swift | 97 +++++++++ .../Views/FollowNotificationTableViewCell.xib | 94 ++++++++ Tusker/Views/InlineTextAttachment.swift | 21 ++ Tusker/Views/ProfileHeaderTableViewCell.swift | 4 +- Tusker/Views/StatusTableViewCell.swift | 18 +- 19 files changed, 1051 insertions(+), 27 deletions(-) create mode 100644 Artwork/Icons/Download.svg create mode 100644 Artwork/Icons/Link.svg create mode 100644 Tusker/Assets.xcassets/Link.imageset/Contents.json create mode 100644 Tusker/Assets.xcassets/Link.imageset/Link.pdf create mode 100644 Tusker/Storyboards/Notifications.storyboard create mode 100644 Tusker/View Controllers/NotificationsTableViewController.swift create mode 100644 Tusker/Views/ActionNotificationTableViewCell.swift create mode 100644 Tusker/Views/ActionNotificationTableViewCell.xib create mode 100644 Tusker/Views/FollowNotificationTableViewCell.swift create mode 100644 Tusker/Views/FollowNotificationTableViewCell.xib create mode 100644 Tusker/Views/InlineTextAttachment.swift 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 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + 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 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + 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 ded45eaa..59bcf473 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 {