diff --git a/Pachyderm/Utilities/NotificationGroup.swift b/Pachyderm/Utilities/NotificationGroup.swift new file mode 100644 index 00000000..7907b3c7 --- /dev/null +++ b/Pachyderm/Utilities/NotificationGroup.swift @@ -0,0 +1,42 @@ +// +// NotificationGroup.swift +// Pachyderm +// +// Created by Shadowfacts on 9/5/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import Foundation + +public class NotificationGroup { + public let notificationIDs: [String] + public let id: String + public let kind: Notification.Kind + + init?(notifications: [Notification]) { + guard !notifications.isEmpty else { return nil } + self.notificationIDs = notifications.map { $0.id } + self.id = notifications.first!.id + self.kind = notifications.first!.kind + } + + public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] { + return notifications.reduce(into: [[Notification]]()) { (groups, notification) in + if allowedTypes.contains(notification.kind), + let lastGroup = groups.last, + let firstStatus = lastGroup.first, + firstStatus.kind == notification.kind, + firstStatus.status?.id == notification.status?.id { + + groups[groups.count - 1].append(notification) + } else { + groups.append([notification]) + } + }.map { + NotificationGroup(notifications: $0)! + } + } + +} + +extension NotificationGroup: Identifiable {} diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 7a3e6c84..2e9392f6 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -86,8 +86,6 @@ D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; }; D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; }; 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 */; }; @@ -147,6 +145,9 @@ D68632AB21ED8319008C716E /* GMImagePickerController.h in Headers */ = {isa = PBXBuildFile; fileRef = D686329121ED8319008C716E /* GMImagePickerController.h */; settings = {ATTRIBUTES = (Public, ); }; }; D68632AC21ED8319008C716E /* GMImagePicker.strings in Resources */ = {isa = PBXBuildFile; fileRef = D686329321ED8319008C716E /* GMImagePicker.strings */; }; D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */; }; + D6A3BC7923218E9200FD64D5 /* NotificationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */; }; + D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */; }; + D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A3BC7B232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib */; }; D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */; }; D6A5FAFB217B86CE003DB2D9 /* OnboardingViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A5FAFA217B86CE003DB2D9 /* OnboardingViewController.xib */; }; D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */; }; @@ -327,8 +328,6 @@ D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = ""; }; D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; 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 = ""; }; @@ -387,6 +386,9 @@ D686329121ED8319008C716E /* GMImagePickerController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GMImagePickerController.h; sourceTree = ""; }; D686329421ED8319008C716E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = GMImagePicker.strings; sourceTree = ""; }; D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSegment.swift; sourceTree = ""; }; + D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationGroup.swift; sourceTree = ""; }; + D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionNotificationGroupTableViewCell.swift; sourceTree = ""; }; + D6A3BC7B232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ActionNotificationGroupTableViewCell.xib; sourceTree = ""; }; D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeViewController.xib; sourceTree = ""; }; D6A5FAFA217B86CE003DB2D9 /* OnboardingViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OnboardingViewController.xib; sourceTree = ""; }; D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = ""; }; @@ -783,10 +785,10 @@ D641C78C213DD937004B4513 /* Notifications */ = { isa = PBXGroup; children = ( - D641C778213CAC56004B4513 /* ActionNotificationTableViewCell.xib */, - D641C776213CAA9E004B4513 /* ActionNotificationTableViewCell.swift */, D641C77A213CB017004B4513 /* FollowNotificationTableViewCell.xib */, D641C77C213CB024004B4513 /* FollowNotificationTableViewCell.swift */, + D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */, + D6A3BC7B232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib */, ); path = Notifications; sourceTree = ""; @@ -934,6 +936,7 @@ children = ( D6E6F26221603F8B006A8599 /* CharacterCounter.swift */, D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */, + D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */, ); path = Utilities; sourceTree = ""; @@ -1335,8 +1338,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - D641C779213CAC56004B4513 /* ActionNotificationTableViewCell.xib in Resources */, D627FF7D217E958900CC0648 /* DraftTableViewCell.xib in Resources */, + D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */, D667E5E921349EE50057A976 /* ProfileHeaderTableViewCell.xib in Resources */, D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */, D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */, @@ -1411,6 +1414,7 @@ D6109A032145722C00432DC2 /* RegisteredApplication.swift in Sources */, D6109A0921458C4A00432DC2 /* Empty.swift in Sources */, D6285B4F21EA695800FE4B39 /* StatusContentType.swift in Sources */, + D6A3BC7923218E9200FD64D5 /* NotificationGroup.swift in Sources */, D61099DF2144C11400432DC2 /* MastodonError.swift in Sources */, D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */, D61099D62144B4B200432DC2 /* FormAttachment.swift in Sources */, @@ -1493,7 +1497,6 @@ D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */, D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */, D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */, - D641C777213CAA9E004B4513 /* ActionNotificationTableViewCell.swift in Sources */, D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */, D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */, D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, @@ -1504,6 +1507,7 @@ D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */, D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */, D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */, + D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */, D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */, 04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */, D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */, diff --git a/Tusker/Screens/Notifications/NotificationsTableViewController.swift b/Tusker/Screens/Notifications/NotificationsTableViewController.swift index a743753e..dbe9a217 100644 --- a/Tusker/Screens/Notifications/NotificationsTableViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsTableViewController.swift @@ -11,7 +11,13 @@ import Pachyderm class NotificationsTableViewController: EnhancedTableViewController { - var timelineSegments: [TimelineSegment] = [] { + let statusCell = "statusCell" + let actionGroupCell = "actionGroupCell" + let followCell = "followCell" + + let groupTypes = [Notification.Kind.favourite, .reblog] + + var groups: [NotificationGroup] = [] { didSet { DispatchQueue.main.async { self.tableView.reloadData() @@ -42,9 +48,9 @@ class NotificationsTableViewController: EnhancedTableViewController { 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") + tableView.register(UINib(nibName: "StatusTableViewCell", bundle: nil), forCellReuseIdentifier: statusCell) + tableView.register(UINib(nibName: "ActionNotificationGroupTableViewCell", bundle: nil), forCellReuseIdentifier: actionGroupCell) + tableView.register(UINib(nibName: "FollowNotificationTableViewCell", bundle: nil), forCellReuseIdentifier: followCell) tableView.prefetchDataSource = self @@ -52,7 +58,10 @@ class NotificationsTableViewController: EnhancedTableViewController { MastodonController.client.run(request) { result in guard case let .success(notifications, pagination) = result else { fatalError() } - self.timelineSegments.append(TimelineSegment(objects: notifications)) + let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes) + + self.groups.append(contentsOf: groups) + MastodonCache.addAll(notifications: notifications) MastodonCache.addAll(statuses: notifications.compactMap { $0.status }) MastodonCache.addAll(accounts: notifications.map { $0.account }) @@ -67,48 +76,59 @@ class NotificationsTableViewController: EnhancedTableViewController { // MARK: - Table view data source override func numberOfSections(in tableView: UITableView) -> Int { - return timelineSegments.count + return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return timelineSegments[section].count + return groups.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let notificationID = timelineSegments[indexPath.section][indexPath.row] - guard let notification = MastodonCache.notification(for: notificationID) else { fatalError() } + let group = groups[indexPath.row] - switch notification.kind { + switch group.kind { case .mention: - guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() } + guard let notification = MastodonCache.notification(for: group.notificationIDs.first!), + let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? StatusTableViewCell else { + fatalError() + } cell.updateUI(statusID: notification.status!.id) 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) + guard let cell = tableView.dequeueReusableCell(withIdentifier: actionGroupCell, for: indexPath) as? ActionNotificationGroupTableViewCell else { fatalError() } + cell.updateUI(group: group) cell.delegate = self return cell + case .follow: - guard let cell = tableView.dequeueReusableCell(withIdentifier: "followCell", for: indexPath) as? FollowNotificationTableViewCell else { fatalError() } + guard let notification = MastodonCache.notification(for: group.notificationIDs.first!) else { fatalError() } + guard let cell = tableView.dequeueReusableCell(withIdentifier: followCell, for: indexPath) as? FollowNotificationTableViewCell else { fatalError() } cell.updateUI(for: notification) cell.delegate = self return cell +// guard let cell = tableView.dequeueReusableCell(withIdentifier: "followGroupCell", for: indexPath) as? FollowNotificationGroupTableViewCell else { fatalError() } +// cell.updateUI(notificationGroup: group) +// cell.delegate = self +// return cell } } override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - if indexPath.section == timelineSegments.count - 1, - indexPath.row == timelineSegments[indexPath.section].count - 1 { + if indexPath.row == groups.count - 1 { guard let older = older else { return } let request = MastodonController.client.getNotifications(range: older) MastodonController.client.run(request) { result in guard case let .success(newNotifications, pagination) = result else { fatalError() } - self.timelineSegments[self.timelineSegments.count - 1].append(objects: newNotifications) + let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes) + + self.groups.append(contentsOf: groups) + MastodonCache.addAll(notifications: newNotifications) MastodonCache.addAll(statuses: newNotifications.compactMap { $0.status }) MastodonCache.addAll(accounts: newNotifications.map { $0.account }) @@ -137,7 +157,10 @@ class NotificationsTableViewController: EnhancedTableViewController { MastodonController.client.run(request) { result in guard case let .success(newNotifications, pagination) = result else { fatalError() } - self.timelineSegments[0].insertAtBeginning(objects: newNotifications) + let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes) + + self.groups.insert(contentsOf: groups, at: 0) + MastodonCache.addAll(notifications: newNotifications) MastodonCache.addAll(statuses: newNotifications.compactMap { $0.status }) MastodonCache.addAll(accounts: newNotifications.map { $0.account }) @@ -160,17 +183,19 @@ extension NotificationsTableViewController: StatusTableViewCellDelegate {} extension NotificationsTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { - let notificationID = timelineSegments[indexPath.section][indexPath.row] - guard let notification = MastodonCache.notification(for: notificationID) else { continue } - ImageCache.avatars.get(notification.account.avatar, completion: nil) + for notificationID in groups[indexPath.row].notificationIDs { + guard let notification = MastodonCache.notification(for: notificationID) else { continue } + ImageCache.avatars.get(notification.account.avatar, completion: nil) + } } } func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { - let notificationID = timelineSegments[indexPath.section][indexPath.row] - guard let notification = MastodonCache.notification(for: notificationID) else { continue } - ImageCache.avatars.cancel(notification.account.url) + for notificationID in groups[indexPath.row].notificationIDs { + guard let notification = MastodonCache.notification(for: notificationID) else { continue } + ImageCache.avatars.cancel(notification.account.avatar) + } } } } diff --git a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift new file mode 100644 index 00000000..69405e4b --- /dev/null +++ b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift @@ -0,0 +1,142 @@ +// +// ActionNotificationGroupTableViewCell.swift +// Tusker +// +// Created by Shadowfacts on 9/5/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm +import SwiftSoup + +class ActionNotificationGroupTableViewCell: UITableViewCell { + + var delegate: StatusTableViewCellDelegate? + + @IBOutlet weak var actionImageView: UIImageView! + @IBOutlet weak var actionAvatarStackView: UIStackView! + @IBOutlet weak var timestampLabel: UILabel! + @IBOutlet weak var actionLabel: UILabel! + @IBOutlet weak var statusContentLabel: UILabel! + + var group: NotificationGroup! + var statusID: String! + + var authorAvatarURL: URL? + var updateTimestampWorkItem: DispatchWorkItem? + + override func awakeFromNib() { + super.awakeFromNib() + + NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) + } + + @objc func updateUIForPreferences() { + let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account } + updateActionLabel(people: people) + + for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews { + imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView) + } + } + + func updateUI(group: NotificationGroup) { + guard group.kind == .favourite || group.kind == .reblog else { + fatalError("Invalid notification type \(group.kind) for ActionNotificationGroupTableViewCell") + } + self.group = group + + guard let firstNotification = MastodonCache.notification(for: group.notificationIDs.first!) else { fatalError() } + let status = firstNotification.status! + self.statusID = status.id + + updateUIForPreferences() + + switch group.kind { + case .favourite: + actionImageView.image = UIImage(systemName: "star.fill") + case .reblog: + actionImageView.image = UIImage(systemName: "repeat") + default: + fatalError() + } + + let people = group.notificationIDs.compactMap(MastodonCache.notification(for:)).map { $0.account } + + actionAvatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + for account in people { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.layer.masksToBounds = true + imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 + ImageCache.avatars.get(account.avatar) { (data) in + guard let data = data, self.group.id == group.id else { return } + DispatchQueue.main.async { + imageView.image = UIImage(data: data) + } + } + actionAvatarStackView.addArrangedSubview(imageView) + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: 30), + imageView.heightAnchor.constraint(equalToConstant: 30) + ]) + } + + updateTimestamp() + + updateActionLabel(people: people) + + let doc = try! SwiftSoup.parse(status.content) + statusContentLabel.text = try! doc.text() + } + + func updateTimestamp() { + guard let id = group.notificationIDs.first, + let notification = MastodonCache.notification(for: id) else { + fatalError("Missing cached status") + } + + 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(block: self.updateTimestamp) + DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) + } else { + updateTimestampWorkItem = nil + } + } + + func updateActionLabel(people: [Account]) { + let verb: String + switch group.kind { + case .favourite: + verb = "Favorited" + case .reblog: + verb = "Reblogged" + default: + fatalError() + } + let peopleStr: String + // todo: figure out how to localize this + switch people.count { + case 1: + peopleStr = people.first!.realDisplayName + case 2: + peopleStr = people.first!.realDisplayName + " and " + people.last!.realDisplayName + default: + peopleStr = people.dropLast().map { $0.realDisplayName }.joined(separator: ", ") + ", and " + people.last!.realDisplayName + } + actionLabel.text = "\(verb) by \(peopleStr)" + } + +} diff --git a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.xib b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.xib new file mode 100644 index 00000000..e43117ce --- /dev/null +++ b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.xib @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift deleted file mode 100644 index 253bb598..00000000 --- a/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift +++ /dev/null @@ -1,207 +0,0 @@ -// -// ActionNotificationTableViewCell.swift -// Tusker -// -// Created by Shadowfacts on 9/2/18. -// Copyright © 2018 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm - -class ActionNotificationTableViewCell: UITableViewCell { - - var delegate: StatusTableViewCellDelegate? { - didSet { - contentLabel.navigationDelegate = delegate - } - } - - @IBOutlet weak var displayNameLabel: UILabel! - @IBOutlet weak var usernameLabel: UILabel! - @IBOutlet weak var contentLabel: StatusContentLabel! - @IBOutlet weak var avatarContainerView: UIView! - @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: Pachyderm.Notification! - var statusID: String! - - 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 - - NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) - } - - @objc func updateUIForPreferences() { - guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } - - 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.kind { - case .favourite: - verb = "Liked" - case .reblog: - verb = "Reblogged" - default: - fatalError("Invalid notification type \(notification.kind) for ActionNotificationTableViewCell") - } - actionLabel.text = "\(verb) by \(notification.account.realDisplayName)" - } - - func updateUI(for notification: Pachyderm.Notification) { - guard notification.kind == .favourite || notification.kind == .reblog else { - fatalError("Invalid notification type \(notification.kind) for ActionNotificationTableViewCell") - } - self.notification = notification - let status = notification.status! - self.statusID = status.id - - updateUIForPreferences() - - usernameLabel.text = "@\(status.account.acct)" - opAvatarImageView.image = nil - opAvatarURL = status.account.avatar - ImageCache.avatars.get(status.account.avatar) { (data) in - guard let data = data else { return } - DispatchQueue.main.async { - self.opAvatarImageView.image = UIImage(data: data) - self.opAvatarURL = nil - } - } - actionAvatarImageView.image = nil - actionAvatarURL = notification.account.avatar - ImageCache.avatars.get(notification.account.avatar) { (data) in - guard let data = data else { return } - DispatchQueue.main.async { - self.actionAvatarImageView.image = UIImage(data: data) - self.actionAvatarURL = nil - } - } - updateTimestamp() - let attachments = status.attachments.filter({ $0.kind == .image }) - if attachments.count > 0 { - attachmentsView.isHidden = false - for attachment in attachments { - let url = attachment.textURL ?? attachment.url - let label = UILabel() - label.textColor = .secondaryLabel - - 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.append(NSAttributedString(string: "\(url.lastPathComponent)")) - text.addAttribute(.foregroundColor, value: UIColor.systemBlue, range: NSRange(location: 0, length: 2)) - - label.attributedText = text - attachmentsView.addArrangedSubview(label) - } - } else { - attachmentsView.isHidden = true - } - - contentLabel.setTextFromHtml(status.content) - } - - func updateTimestamp() { - guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } - - 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 { - ImageCache.avatars.cancel(url) - } - if let url = actionAvatarURL { - ImageCache.avatars.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: statusID) - } - } - - @objc func accountPressed() { - delegate?.selected(account: notification.status!.account.id) - } - - @objc func actionPressed() { - delegate?.selected(account: notification.account.id) - } - -} - -extension ActionNotificationTableViewCell: MenuPreviewProvider { - func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { - if avatarContainerView.frame.contains(location) { - let accountID = notification.account.id - return (content: { ProfileTableViewController(accountID: accountID) }, actions: { self.actionsForProfile(accountID: accountID) }) - } else if contentLabel.frame.contains(location), - let link = contentLabel.getLink(atPoint: contentLabel.convert(location, from: self)) { - return ( - content: { self.contentLabel.getViewController(forLink: link.url, inRange: link.range) }, - actions: { - let text = (self.contentLabel.text! as NSString).substring(with: link.range) - if let mention = self.contentLabel.getMention(for: link.url, text: text) { - return self.actionsForProfile(accountID: mention.id) - } else if let hashtag = self.contentLabel.getHashtag(for: link.url, text: text) { - return self.actionsForHashtag(hashtag) - } else { - return self.actionsForURL(link.url) - } - } - ) - } - return (content: { ConversationTableViewController(for: self.statusID) }, actions: { self.actionsForStatus(statusID: self.statusID) }) - } -} diff --git a/Tusker/Views/Notifications/ActionNotificationTableViewCell.xib b/Tusker/Views/Notifications/ActionNotificationTableViewCell.xib deleted file mode 100644 index f2b48f3a..00000000 --- a/Tusker/Views/Notifications/ActionNotificationTableViewCell.xib +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tusker/Views/Status/StatusTableViewCell.xib b/Tusker/Views/Status/StatusTableViewCell.xib index 2f08d02e..04fea672 100644 --- a/Tusker/Views/Status/StatusTableViewCell.xib +++ b/Tusker/Views/Status/StatusTableViewCell.xib @@ -1,11 +1,10 @@ - + - + - @@ -20,7 +19,7 @@ @@ -48,13 +47,13 @@ - + @@ -81,7 +80,7 @@