diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 879c4bf9..7eb72767 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -69,7 +69,6 @@ D6109A11214607D500432DC2 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A10214607D500432DC2 /* Timeline.swift */; }; D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */; }; D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; }; - D6163F2C21AA0AF1008DAC41 /* MyProfileTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6163F2B21AA0AF1008DAC41 /* MyProfileTableViewController.swift */; }; D61AC1D3232E928600C54D2D /* InstanceSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D2232E928600C54D2D /* InstanceSelector.swift */; }; D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; }; D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; }; @@ -120,6 +119,11 @@ D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */; }; D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; }; D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */; }; + D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0424B0227D00F5412E /* ProfileViewController.swift */; }; + D6412B0724B0237700F5412E /* ProfileStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0624B0237700F5412E /* ProfileStatusesViewController.swift */; }; + D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0824B0291E00F5412E /* MyProfileViewController.swift */; }; + D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */; }; + D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */; }; D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */; }; D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; }; D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6434EB2215B1856001A919A /* XCBRequest.swift */; }; @@ -151,9 +155,6 @@ D667383C23299340000A2373 /* InstanceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667383B23299340000A2373 /* InstanceType.swift */; }; D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; }; D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */; }; - D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */; }; - D667E5E921349EE50057A976 /* ProfileHeaderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */; }; - D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */; }; D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */; }; D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */; }; D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; }; @@ -380,7 +381,6 @@ D6109A10214607D500432DC2 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = ""; }; D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = ""; }; D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = ""; }; - D6163F2B21AA0AF1008DAC41 /* MyProfileTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTableViewController.swift; sourceTree = ""; }; D61AC1D2232E928600C54D2D /* InstanceSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelector.swift; sourceTree = ""; }; D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = ""; }; D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = ""; }; @@ -430,6 +430,11 @@ D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectImageButton.swift; sourceTree = ""; }; D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = ""; }; D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScrollableViewController.swift; sourceTree = ""; }; + D6412B0424B0227D00F5412E /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; + D6412B0624B0237700F5412E /* ProfileStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusesViewController.swift; sourceTree = ""; }; + D6412B0824B0291E00F5412E /* MyProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileViewController.swift; sourceTree = ""; }; + D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderView.xib; sourceTree = ""; }; + D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = ""; }; D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = ""; }; D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = ""; }; D6434EB2215B1856001A919A /* XCBRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequest.swift; sourceTree = ""; }; @@ -465,9 +470,6 @@ D667383B23299340000A2373 /* InstanceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceType.swift; sourceTree = ""; }; D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = ""; }; D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TimelineStatusTableViewCell.xib; sourceTree = ""; }; - D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTableViewController.swift; sourceTree = ""; }; - D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderTableViewCell.xib; sourceTree = ""; }; - D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderTableViewCell.swift; sourceTree = ""; }; D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Delegates.swift"; sourceTree = ""; }; D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTableViewController.swift; sourceTree = ""; }; D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = ""; }; @@ -930,8 +932,9 @@ D641C784213DD819004B4513 /* Profile */ = { isa = PBXGroup; children = ( - D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */, - D6163F2B21AA0AF1008DAC41 /* MyProfileTableViewController.swift */, + D6412B0424B0227D00F5412E /* ProfileViewController.swift */, + D6412B0624B0237700F5412E /* ProfileStatusesViewController.swift */, + D6412B0824B0291E00F5412E /* MyProfileViewController.swift */, ); path = Profile; sourceTree = ""; @@ -1008,8 +1011,8 @@ D641C78B213DD92F004B4513 /* Profile Header */ = { isa = PBXGroup; children = ( - D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */, - D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */, + D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */, + D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */, ); path = "Profile Header"; sourceTree = ""; @@ -1513,9 +1516,6 @@ LastSwiftMigration = 1020; TestTargetID = D6D4DDCB212518A000E1C4BB; }; - D68E526124A3F9AF0054355A = { - CreatedOnToolsVersion = 12.0; - }; D6D4DDCB212518A000E1C4BB = { CreatedOnToolsVersion = 10.0; LastSwiftMigration = 1020; @@ -1590,7 +1590,7 @@ D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */, D627FF7D217E958900CC0648 /* DraftTableViewCell.xib in Resources */, D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */, - D667E5E921349EE50057A976 /* ProfileHeaderTableViewCell.xib in Resources */, + D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */, D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */, D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */, D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */, @@ -1744,6 +1744,7 @@ D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, 0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */, + D6412B0724B0237700F5412E /* ProfileStatusesViewController.swift in Sources */, D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */, D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */, D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */, @@ -1823,6 +1824,7 @@ D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */, D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */, D620483423D3801D008A63EF /* LinkTextView.swift in Sources */, + D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */, D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */, D627943523A5525100D38C68 /* StatusActivity.swift in Sources */, D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */, @@ -1855,10 +1857,10 @@ D620483223D2A6A3008A63EF /* CompositionState.swift in Sources */, D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */, D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */, - D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */, D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */, 04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */, D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */, + D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */, D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */, D64D8CA92463B494006B0BAA /* CachedDictionary.swift in Sources */, D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */, @@ -1874,12 +1876,11 @@ D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */, D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */, D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */, - D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */, D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */, + D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */, D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */, D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */, D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */, - D6163F2C21AA0AF1008DAC41 /* MyProfileTableViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 3bb2a0e5..f0691566 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -105,7 +105,7 @@ extension MainTabBarViewController { case .explore: return ExploreViewController(mastodonController: mastodonController) case .myProfile: - return MyProfileTableViewController(mastodonController: mastodonController) + return MyProfileViewController(mastodonController: mastodonController) } } } diff --git a/Tusker/Screens/Profile/MyProfileTableViewController.swift b/Tusker/Screens/Profile/MyProfileViewController.swift similarity index 85% rename from Tusker/Screens/Profile/MyProfileTableViewController.swift rename to Tusker/Screens/Profile/MyProfileViewController.swift index f5ee6779..c229c8b9 100644 --- a/Tusker/Screens/Profile/MyProfileTableViewController.swift +++ b/Tusker/Screens/Profile/MyProfileViewController.swift @@ -1,5 +1,5 @@ // -// MyProfileTableViewController.swift +// MyProfileViewController.swift // Tusker // // Created by Shadowfacts on 11/24/18. @@ -8,7 +8,7 @@ import UIKit -class MyProfileTableViewController: ProfileTableViewController { +class MyProfileViewController: ProfileViewController { init(mastodonController: MastodonController) { super.init(accountID: nil, mastodonController: mastodonController) @@ -17,7 +17,9 @@ class MyProfileTableViewController: ProfileTableViewController { tabBarItem.image = UIImage(systemName: "person.fill") mastodonController.getOwnAccount { (account) in - self.accountID = account.id + DispatchQueue.main.async { + self.accountID = account.id + } _ = ImageCache.avatars.get(account.avatar, completion: { [weak self] (data) in guard let self = self, let data = data, let image = UIImage(data: data) else { return } @@ -33,22 +35,18 @@ class MyProfileTableViewController: ProfileTableViewController { } } - required init?(coder aDecoder: NSCoder) { + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() - + navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Preferences", style: .plain, target: self, action: #selector(preferencesPressed)) } @objc func preferencesPressed() { present(PreferencesNavigationController(mastodonController: mastodonController), animated: true) } - - @objc func closePreferences() { - dismiss(animated: true) - } } diff --git a/Tusker/Screens/Profile/ProfileStatusesViewController.swift b/Tusker/Screens/Profile/ProfileStatusesViewController.swift new file mode 100644 index 00000000..ff93ca27 --- /dev/null +++ b/Tusker/Screens/Profile/ProfileStatusesViewController.swift @@ -0,0 +1,319 @@ +// +// ProfileStatusesViewController.swift +// Tusker +// +// Created by Shadowfacts on 7/3/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm + +class ProfileStatusesViewController: EnhancedTableViewController { + + weak var mastodonController: MastodonController! + + private(set) var headerView: ProfileHeaderView! + + var accountID: String! + + let kind: Kind + + private var pinnedStatuses: [(id: String, state: StatusState)] = [] + private var timelineSegments: [[(id: String, state: StatusState)]] = [] + + private var older: RequestRange? + private var newer: RequestRange? + + private var loaded = false + + init(accountID: String?, kind: Kind, mastodonController: MastodonController) { + self.accountID = accountID + self.kind = kind + self.mastodonController = mastodonController + + super.init(style: .plain) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + + refreshControl = UIRefreshControl() + refreshControl!.addTarget(self, action: #selector(refreshStatuses(_:)), for: .valueChanged) + + tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = 140 + + tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell") + + tableView.prefetchDataSource = self + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if !loaded, + let accountID = accountID, + let account = mastodonController.persistentContainer.account(for: accountID) { + loaded = true + updateUI(account: account) + } + } + + private func updateUI(account: AccountMO) { + if kind == .statuses { + getPinnedStatuses { (response) in + guard case let .success(statuses, _) = response else { + // todo: error message + return + } + if statuses.isEmpty { return } + + self.mastodonController.persistentContainer.addAll(statuses: statuses) { + self.pinnedStatuses = statuses.map { ($0.id, .unknown) } + let indexPaths = (0..) { + let request: Request<[Status]> + switch kind { + case .statuses: + request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: false, excludeReplies: true) + case .withReplies: + request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: false, excludeReplies: false) + case .onlyMedia: + request = Account.getStatuses(accountID, range: range, onlyMedia: true, pinned: false, excludeReplies: false) + } + mastodonController.run(request, completion: completion) + } + + private func getPinnedStatuses(completion: @escaping Client.Callback<[Status]>) { + let request = Account.getStatuses(accountID, range: .default, onlyMedia: false, pinned: true, excludeReplies: false) + mastodonController.run(request, completion: completion) + } + + // MARK: Interaction + + @objc func refreshStatuses(_ sender: UIRefreshControl) { + guard let newer = newer else { return } + + getStatuses(for: newer) { (response) in + guard case let .success(newStatuses, pagination) = response else { + // todo: error message + return + } + + self.mastodonController.persistentContainer.addAll(statuses: newStatuses) { + // if there's no newer request range (because no statuses were returned), + // we don't want to change the current newer pagination, so that we can + // continue to load statuses newer than whatever was last loaded + if let newer = pagination?.newer { + self.newer = newer + } + + let indexPaths = (0.. (id: String, state: StatusState) in + let state: StatusState + if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) { + state = oldState + } else { + state = .unknown + } + return (status.id, state) + } + DispatchQueue.main.async { + self.pinnedStatuses = pinnedStatuses + UIView.performWithoutAnimation { + self.tableView.reloadSections(IndexSet(integer: 0), with: .none) + } + } + } + } + } + } + + // MARK: Table view data source + + override func numberOfSections(in tableView: UITableView) -> Int { + // 1 for pinned, rest for timeline + return 1 + timelineSegments.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == 0 { + return pinnedStatuses.count + } else { + return timelineSegments[section - 1].count + } + } + + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell + + cell.delegate = self + + if indexPath.section == 0 { + cell.showPinned = true + let (id, state) = pinnedStatuses[indexPath.row] + cell.updateUI(statusID: id, state: state) + } else { + cell.showPinned = false + let (id, state) = timelineSegments[indexPath.section - 1][indexPath.row] + cell.updateUI(statusID: id, state: state) + } + + return cell + } + + override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + // todo: if scrolling up, remove statuses at bottom like timeline VC + + // load older statuses if at bottom + if timelineSegments.count > 0, + indexPath.section == timelineSegments.count, + indexPath.row == timelineSegments[indexPath.section - 1].count - 1 { + guard let older = older else { return } + + getStatuses(for: older) { (response) in + guard case let .success(newStatuses, pagination) = response else { + // todo: error message + return + } + + self.mastodonController.persistentContainer.addAll(statuses: newStatuses) { + // if there is no older request range, we want to set ours to nil + // otherwise we would end up loading the same statuses again + self.older = pagination?.older + + DispatchQueue.main.async { + let start = self.timelineSegments[indexPath.section - 1].count + let indexPaths = (0.. Bool { + return true + } + + override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration() + } + + override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration() + } + +} + +extension ProfileStatusesViewController { + enum Kind { + case statuses, withReplies, onlyMedia + } +} + +extension ProfileStatusesViewController: StatusTableViewCellDelegate { + var apiController: MastodonController { mastodonController } + + func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) { + // causes the table view to recalculate the cell heights + tableView.beginUpdates() + tableView.endUpdates() + } +} + +extension ProfileStatusesViewController: UITableViewDataSourcePrefetching { + func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { + for indexPath in indexPaths { + let statusID: String + if indexPath.section == 0 { + statusID = pinnedStatuses[indexPath.row].id + } else { + statusID = timelineSegments[indexPath.section - 1][indexPath.row].id + } + guard let status = mastodonController.persistentContainer.status(for: statusID) else { continue } + _ = ImageCache.avatars.get(status.account.avatar, completion: nil) + for attachment in status.attachments { + _ = ImageCache.attachments.get(attachment.url, completion: nil) + } + } + } + + func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { + for indexPath in indexPaths { + let statusID: String + if indexPath.section == 0 { + statusID = pinnedStatuses[indexPath.row].id + } else { + statusID = timelineSegments[indexPath.section - 1][indexPath.row].id + } + guard let status = mastodonController.persistentContainer.status(for: statusID) else { continue } + ImageCache.avatars.cancelWithoutCallback(status.account.avatar) + for attachment in status.attachments { + ImageCache.avatars.cancelWithoutCallback(attachment.url) + } + } + } +} diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift deleted file mode 100644 index 50e509a0..00000000 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ /dev/null @@ -1,355 +0,0 @@ -// -// ProfileTableViewController.swift -// Tusker -// -// Created by Shadowfacts on 8/27/18. -// Copyright © 2018 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm -import SafariServices - -class ProfileTableViewController: EnhancedTableViewController { - - weak var mastodonController: MastodonController! - - var accountID: String! - - var pinnedStatuses: [(id: String, state: StatusState)] = [] - var timelineSegments: [[(id: String, state: StatusState)]] = [] - - var older: RequestRange? - var newer: RequestRange? - - private var loadingVC: LoadingViewController? = nil - private var loaded = false - - init(accountID: String?, mastodonController: MastodonController) { - self.mastodonController = mastodonController - - self.accountID = accountID - - super.init(style: .plain) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemeneted") - } - - deinit { - if let id = accountID, let container = mastodonController?.persistentContainer { - container.backgroundContext.perform { - container.account(for: id, in: container.backgroundContext)?.decrementReferenceCount() - } - } - } - - override func viewDidLoad() { - super.viewDidLoad() - - self.refreshControl = UIRefreshControl() - refreshControl!.addTarget(self, action: #selector(refreshStatuses(_:)), for: .valueChanged) - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composePressed(_:))) - - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = 140 - - tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell") - tableView.register(UINib(nibName: "ProfileHeaderTableViewCell", bundle: nil), forCellReuseIdentifier: "headerCell") - - tableView.prefetchDataSource = self - - if accountID == nil { - loadingVC = LoadingViewController() - embedChild(loadingVC!) - } - - NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - if !loaded, let accountID = accountID { - loaded = true - loadingVC?.removeViewAndController() - loadingVC = nil - - if mastodonController.persistentContainer.account(for: accountID) != nil { - updateAccountUI() - } else { - loadingVC = LoadingViewController() - embedChild(loadingVC!) - let request = Client.getAccount(id: accountID) - mastodonController.run(request) { (response) in - guard case let .success(account, _) = response else { - let alert = UIAlertController(title: "Something Went Wrong", message: "Couldn't load the selected account", preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in - self.navigationController!.popViewController(animated: true) - })) - DispatchQueue.main.async { - self.present(alert, animated: true) - } - return - } - self.mastodonController.persistentContainer.addOrUpdate(account: account, incrementReferenceCount: true) { (_) in - DispatchQueue.main.async { - self.updateAccountUI() - self.tableView.reloadData() - } - } - } - } - } - } - - func updateAccountUI() { - updateUIForPreferences() - - getStatuses(onlyPinned: true) { (response) in - guard case let .success(statuses, _) = response else { fatalError() } - if statuses.isEmpty { return } - - self.mastodonController.persistentContainer.addAll(statuses: statuses) { - self.pinnedStatuses = statuses.map { ($0.id, .unknown) } - let indexPaths = (0..) { - let request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: onlyPinned, excludeReplies: !Preferences.shared.showRepliesInProfiles) - mastodonController.run(request, completion: completion) - } - - func sendMessageMentioning() { - guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } - let vc = UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct, mastodonController: mastodonController)) - present(vc, animated: true) - } - - // MARK: - Table view data source - - override func numberOfSections(in tableView: UITableView) -> Int { - // 1 section for header, 1 section for pinned, rest for timeline - return 2 + timelineSegments.count - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if section == 0 { - return accountID == nil || mastodonController.persistentContainer.account(for: accountID) == nil ? 0 : 1 - } else if section == 1 { - return pinnedStatuses.count - } else { - return timelineSegments[section - 2].count - } - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - switch indexPath.section { - case 0: - guard let cell = tableView.dequeueReusableCell(withIdentifier: "headerCell", for: indexPath) as? ProfileHeaderTableViewCell else { fatalError() } - cell.selectionStyle = .none - cell.delegate = self - cell.updateUI(for: accountID) - return cell - case 1: - guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } - let (id, state) = pinnedStatuses[indexPath.row] - cell.showPinned = true - cell.delegate = self - cell.updateUI(statusID: id, state: state) - return cell - default: - guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() } - let (id, state) = timelineSegments[indexPath.section - 2][indexPath.row] - cell.delegate = self - cell.updateUI(statusID: id, state: state) - return cell - } - } - - // MARK: - Table view delegate - - override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - // todo: if scrolling up, remove statuses at bottom like timeline VC - - // load older statuses if at bottom - if timelineSegments.count > 0 && indexPath.section - 1 == timelineSegments.count && indexPath.row == timelineSegments[indexPath.section - 2].count - 1 { - guard let older = older else { return } - - getStatuses(for: older) { response in - guard case let .success(newStatuses, pagination) = response else { fatalError() } - - self.mastodonController.persistentContainer.addAll(statuses: newStatuses) { - self.older = pagination?.older - - DispatchQueue.main.async { - let start = self.timelineSegments[indexPath.section - 2].count - let indexPaths = (0.. Bool { - return true - } - - override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration() - } - - override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration() - } - - @objc func refreshStatuses(_ sender: Any) { - guard let newer = newer else { return } - - getStatuses(for: newer) { response in - guard case let .success(newStatuses, pagination) = response else { fatalError() } - - self.mastodonController.persistentContainer.addAll(statuses: newStatuses) { - if let newer = pagination?.newer { - self.newer = newer - } - - let indexPaths = (0.. 1 { - let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id - guard let status = mastodonController.persistentContainer.status(for: statusID) else { continue } - _ = ImageCache.avatars.get(status.account.avatar, completion: nil) - for attachment in status.attachments { - _ = ImageCache.attachments.get(attachment.url, completion: nil) - } - } - } - - func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { - for indexPath in indexPaths where indexPath.section > 1 { - let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id - guard let status = mastodonController.persistentContainer.status(for: statusID) else { continue } - ImageCache.avatars.cancelWithoutCallback(status.account.avatar) - for attachment in status.attachments { - ImageCache.attachments.cancelWithoutCallback(attachment.url) - } - } - } -} diff --git a/Tusker/Screens/Profile/ProfileViewController.swift b/Tusker/Screens/Profile/ProfileViewController.swift new file mode 100644 index 00000000..545984ae --- /dev/null +++ b/Tusker/Screens/Profile/ProfileViewController.swift @@ -0,0 +1,193 @@ +// +// ProfileViewController.swift +// Tusker +// +// Created by Shadowfacts on 7/3/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm +import Combine + +class ProfileViewController: UIPageViewController { + + weak var mastodonController: MastodonController! + + // todo: does this still need to be settable? + var accountID: String! { + didSet { + updateAccountUI() + pageControllers.forEach { $0.accountID = accountID } + } + } + + private var accountUpdater: Cancellable? + + private(set) var currentIndex: Int! + let pageControllers: [ProfileStatusesViewController] + var currentViewController: ProfileStatusesViewController { + pageControllers[currentIndex] + } + + private var headerView: ProfileHeaderView! + + init(accountID: String?, mastodonController: MastodonController) { + self.accountID = accountID + self.mastodonController = mastodonController + + self.pageControllers = [ + ProfileStatusesViewController(accountID: accountID, kind: .statuses, mastodonController: mastodonController), + ProfileStatusesViewController(accountID: accountID, kind: .withReplies, mastodonController: mastodonController), + ProfileStatusesViewController(accountID: accountID, kind: .onlyMedia, mastodonController: mastodonController) + ] + + super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + + headerView = ProfileHeaderView.create() + headerView.delegate = self + headerView.updateUI(for: accountID) + + selectPage(at: 0, animated: false) + + currentViewController.tableView.tableHeaderView = headerView + + NSLayoutConstraint.activate([ + headerView.widthAnchor.constraint(equalTo: view.widthAnchor), + ]) + + accountUpdater = mastodonController.persistentContainer.accountSubject + .filter { [weak self] in $0 == self?.accountID } + .receive(on: DispatchQueue.main) + .sink { [weak self] (_) in self?.updateAccountUI() } + } + + private func updateAccountUI() { + guard let account = mastodonController.persistentContainer.account(for: accountID) else { return } + navigationItem.title = account.displayNameWithoutCustomEmoji + } + + private func selectPage(at index: Int, animated: Bool, completion: ((Bool) -> Void)? = nil) { + let direction: UIPageViewController.NavigationDirection = currentIndex == nil || index - currentIndex > 0 ? .forward : .reverse + currentIndex = index + + guard let old = viewControllers?.first as? ProfileStatusesViewController else { + // if old doesn't exist, we're selecting the initial view controller, so moving the header around isn't necessary + // since it will be added in viewDidLoad + setViewControllers([pageControllers[index]], direction: direction, animated: animated, completion: completion) + return + } + let new = pageControllers[index] + + let headerHeight = self.headerView.bounds.height + + // Store old's content offset so it can be transferred to new + let prevOldContentOffset = old.tableView.contentOffset + // Remove the header, inset the table content by the same amount, and adjust the offset so the cells don't move + old.tableView.tableHeaderView = nil + old.tableView.contentInset = UIEdgeInsets(top: headerHeight, left: 0, bottom: 0, right: 0) + old.tableView.contentOffset.y -= headerHeight + + // Add the header to ourself temporarily, and constrain it to the same position it was in + self.view.addSubview(self.headerView) + let tempTopConstraint = self.headerView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: -(prevOldContentOffset.y + old.tableView.safeAreaInsets.top)) + NSLayoutConstraint.activate([ + self.headerView.widthAnchor.constraint(equalTo: self.view.widthAnchor), + tempTopConstraint + ]) + + // Setup the inset in new, in case it hasn't been already + new.tableView.contentInset = UIEdgeInsets(top: headerHeight, left: 0, bottom: 0, right: 0) + // Match the scroll positions + new.tableView.contentOffset = old.tableView.contentOffset + + // Actually switch pages + setViewControllers([pageControllers[index]], direction: direction, animated: animated) { (finished) in + // Defer everything one run-loop iteration, otherwise altering the tableView's contentInset/Offset causes it to jump around during the animation + DispatchQueue.main.async { + // Move the header to the new table view + new.tableView.tableHeaderView = self.headerView + // Remove the inset, and set the offset back to old's original one, prior to removing the header + new.tableView.contentInset = .zero + new.tableView.contentOffset = prevOldContentOffset + + // Deactivate the top constraint, otherwise it sticks around + tempTopConstraint.isActive = false + // Re-add the width constraint since it was removed by re-parenting the view + // Why was the width constraint removed, but the top one not? Good question, I have no idea. + NSLayoutConstraint.activate([ + self.headerView.widthAnchor.constraint(equalTo: self.view.widthAnchor) + ]) + + // Layout and update the table view, otherwise the content jumps around when first scrolling it, + // if old was not scrolled all the way to the top + new.tableView.layoutIfNeeded() + UIView.performWithoutAnimation { + new.tableView.performBatchUpdates(nil, completion: nil) + } + + completion?(finished) + } + } + } + +} + +extension ProfileViewController: TuskerNavigationDelegate { + var apiController: MastodonController { mastodonController } +} + +extension ProfileViewController: ProfileHeaderViewDelegate { + func profileHeader(_ view: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int) { + // disable user interaction on segmented control while switching pages to prevent + // race condition from trying to switch to multiple pages simultaneously + view.pagesSegmentedControl.isUserInteractionEnabled = false + selectPage(at: newIndex, animated: true) { (finished) in + view.pagesSegmentedControl.isUserInteractionEnabled = true + } + } + + func profileHeader(_ view: ProfileHeaderView, showMoreOptionsFor accountID: String, sourceView: UIView) { + let account = mastodonController.persistentContainer.account(for: accountID)! + + func showActivityController(activities: [UIActivity]) { + let activityController = UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: activities) + activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: account.url) + activityController.popoverPresentationController?.sourceView = sourceView + self.present(activityController, animated: true) + } + + if account.id == mastodonController.account.id { + showActivityController(activities: [OpenInSafariActivity()]) + } else { + let request = Client.getRelationships(accounts: [account.id]) + mastodonController.run(request) { (response) in + var customActivities: [UIActivity] = [OpenInSafariActivity()] + if case let .success(results, _) = response, let relationship = results.first { + let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity() + customActivities.insert(toggleFollowActivity, at: 0) + } + + DispatchQueue.main.async { + showActivityController(activities: customActivities) + } + } + } + } +} + +extension ProfileViewController: TabBarScrollableViewController { + func tabBarScrollToTop() { + pageControllers[currentIndex].tabBarScrollToTop() + } +} diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index 7d4bd0f2..e96f76ab 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -67,16 +67,16 @@ extension TuskerNavigationDelegate where Self: UIViewController { func selected(account accountID: String) { // don't open if the account is the same as the current one - if let profileController = self as? ProfileTableViewController, + if let profileController = self as? ProfileViewController, profileController.accountID == accountID { return } - show(ProfileTableViewController(accountID: accountID, mastodonController: apiController), sender: self) + show(ProfileViewController(accountID: accountID, mastodonController: apiController), sender: self) } func selected(mention: Mention) { - show(ProfileTableViewController(accountID: mention.id, mastodonController: apiController), sender: self) + show(ProfileViewController(accountID: mention.id, mastodonController: apiController), sender: self) } func selected(tag: Hashtag) { diff --git a/Tusker/Views/Account Cell/AccountTableViewCell.swift b/Tusker/Views/Account Cell/AccountTableViewCell.swift index 7159fcf9..c8e1d331 100644 --- a/Tusker/Views/Account Cell/AccountTableViewCell.swift +++ b/Tusker/Views/Account Cell/AccountTableViewCell.swift @@ -83,7 +83,7 @@ extension AccountTableViewCell: MenuPreviewProvider { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { guard let mastodonController = mastodonController else { return nil } return ( - content: { ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController) }, + content: { ProfileViewController(accountID: self.accountID, mastodonController: mastodonController) }, actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) } ) } diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index 30155acf..3b90e3c4 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -222,7 +222,7 @@ class ContentTextView: LinkTextView { let text = (self.text as NSString).substring(with: range) if let mention = getMention(for: url, text: text) { - return ProfileTableViewController(accountID: mention.id, mastodonController: mastodonController!) + return ProfileViewController(accountID: mention.id, mastodonController: mastodonController!) } else if let tag = getHashtag(for: url, text: text) { return HashtagTimelineViewController(for: tag, mastodonController: mastodonController!) } else { diff --git a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift index 8d211a0f..1303f433 100644 --- a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift @@ -147,7 +147,7 @@ extension FollowNotificationGroupTableViewCell: MenuPreviewProvider { let accountIDs = self.group.notifications.map { $0.account.id } return (content: { if accountIDs.count == 1 { - return ProfileTableViewController(accountID: accountIDs.first!, mastodonController: mastodonController) + return ProfileViewController(accountID: accountIDs.first!, mastodonController: mastodonController) } else { return AccountListTableViewController(accountIDs: accountIDs, mastodonController: mastodonController) } diff --git a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift index e25597d6..ff0fb6b7 100644 --- a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift @@ -152,7 +152,7 @@ extension FollowRequestNotificationTableViewCell: MenuPreviewProvider { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { guard let mastodonController = mastodonController else { return nil } return (content: { - return ProfileTableViewController(accountID: self.account.id, mastodonController: mastodonController) + return ProfileViewController(accountID: self.account.id, mastodonController: mastodonController) }, actions: { return [] }) diff --git a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.xib b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.xib deleted file mode 100644 index 8a165a56..00000000 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.xib +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift b/Tusker/Views/Profile Header/ProfileHeaderView.swift similarity index 70% rename from Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift rename to Tusker/Views/Profile Header/ProfileHeaderView.swift index 0ed38707..651f767c 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderView.swift @@ -1,27 +1,35 @@ // -// ProfileHeaderTableViewCell.swift +// ProfileHeaderView.swift // Tusker // -// Created by Shadowfacts on 8/27/18. -// Copyright © 2018 Shadowfacts. All rights reserved. +// Created by Shadowfacts on 7/4/20. +// Copyright © 2020 Shadowfacts. All rights reserved. // import UIKit import Pachyderm import Combine -protocol ProfileHeaderTableViewCellDelegate: TuskerNavigationDelegate { - func showMoreOptions(cell: ProfileHeaderTableViewCell) +protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate { + func profileHeader(_ headerView: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int) + + func profileHeader(_ headerView: ProfileHeaderView, showMoreOptionsFor accountID: String, sourceView: UIView) } -class ProfileHeaderTableViewCell: UITableViewCell { +class ProfileHeaderView: UIView { - weak var delegate: ProfileHeaderTableViewCellDelegate? + static func create() -> ProfileHeaderView { + let nib = UINib(nibName: "ProfileHeaderView", bundle: .main) + return nib.instantiate(withOwner: nil, options: nil).first as! ProfileHeaderView + } + + weak var delegate: ProfileHeaderViewDelegate? var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var headerImageView: UIImageView! @IBOutlet weak var avatarContainerView: UIView! @IBOutlet weak var avatarImageView: UIImageView! + @IBOutlet weak var moreButton: VisualEffectImageButton! @IBOutlet weak var displayNameLabel: EmojiLabel! @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var followsYouLabel: UILabel! @@ -29,7 +37,7 @@ class ProfileHeaderTableViewCell: UITableViewCell { @IBOutlet weak var fieldsStackView: UIStackView! @IBOutlet weak var fieldNamesStackView: UIStackView! @IBOutlet weak var fieldValuesStackView: UIStackView! - @IBOutlet weak var moreButton: VisualEffectImageButton! + @IBOutlet weak var pagesSegmentedControl: UISegmentedControl! var accountID: String! @@ -37,18 +45,40 @@ class ProfileHeaderTableViewCell: UITableViewCell { var headerRequest: ImageCache.Request? private var accountUpdater: Cancellable? - + + override var frame: CGRect { + didSet { + print("set header frame for \(self)") + } + } + + override var bounds: CGRect { + didSet { + print("set header bounds for \(self)") + } + } + + deinit { + avatarRequest?.cancel() + headerRequest?.cancel() + } + override func awakeFromNib() { + super.awakeFromNib() + avatarContainerView.layer.masksToBounds = true avatarImageView.layer.masksToBounds = true avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(avatarPressed))) avatarImageView.isUserInteractionEnabled = true + headerImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(headerPressed))) headerImageView.isUserInteractionEnabled = true moreButton.layer.cornerRadius = 16 moreButton.layer.masksToBounds = true + NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) + if #available(iOS 13.4, *) { moreButton.addInteraction(UIPointerInteraction(delegate: self)) } @@ -56,15 +86,14 @@ class ProfileHeaderTableViewCell: UITableViewCell { moreButton.showsMenuAsPrimaryAction = true moreButton.isContextMenuInteractionEnabled = true } - - NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) } func updateUI(for accountID: String) { - guard accountID != self.accountID else { return } self.accountID = accountID - guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID)") } + guard let account = mastodonController.persistentContainer.account(for: accountID) else { + fatalError("Missing cached account \(accountID)") + } updateUIForPreferences() @@ -96,17 +125,20 @@ class ProfileHeaderTableViewCell: UITableViewCell { // don't show relationship label for the user's own account if accountID != mastodonController.account.id { let request = Client.getRelationships(accounts: [accountID]) - mastodonController.run(request) { (response) in - if case let .success(results, _) = response, let relationship = results.first { - DispatchQueue.main.async { - self.followsYouLabel.isHidden = !relationship.followedBy - } + mastodonController.run(request) { [weak self] (response) in + guard let self = self, + case let .success(results, _) = response, + let relationship = results.first else { + return + } + DispatchQueue.main.async { + self.followsYouLabel.isHidden = !relationship.followedBy } } } fieldsStackView.isHidden = account.fields.isEmpty - + fieldNamesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } fieldValuesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } for field in account.fields { @@ -118,7 +150,7 @@ class ProfileHeaderTableViewCell: UITableViewCell { nameLabel.lineBreakMode = .byWordWrapping nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) fieldNamesStackView.addArrangedSubview(nameLabel) - + let valueTextView = ContentTextView() valueTextView.isSelectable = false valueTextView.font = .systemFont(ofSize: 17) @@ -132,54 +164,63 @@ class ProfileHeaderTableViewCell: UITableViewCell { nameLabel.heightAnchor.constraint(equalTo: valueTextView.heightAnchor).isActive = true } - + if accountUpdater == nil { accountUpdater = mastodonController.persistentContainer.accountSubject - .filter { [unowned self] in $0 == self.accountID } + .filter { [weak self] in $0 == self?.accountID } .receive(on: DispatchQueue.main) - .sink { [unowned self] in self.updateUI(for: $0) } + .sink { [weak self] in self?.updateUI(for: $0) } } } - - @objc func updateUIForPreferences() { - guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } + + @objc private func updateUIForPreferences() { + guard let account = mastodonController.persistentContainer.account(for: accountID) else { + fatalError("Missing cached account \(accountID!)") + } avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView) avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) displayNameLabel.updateForAccountDisplayName(account: account) } - override func prepareForReuse() { - avatarRequest?.cancel() - headerRequest?.cancel() - } + // MARK: Interaction @IBAction func morePressed(_ sender: Any) { - delegate?.showMoreOptions(cell: self) + guard #available(iOS 14.0, *) else { + // can't use TuskerNavigationDelegate method, because it doesn't know about the (un)follow activity + delegate?.profileHeader(self, showMoreOptionsFor: accountID, sourceView: moreButton) + return + } } @objc func avatarPressed() { - guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } + guard let account = mastodonController.persistentContainer.account(for: accountID) else { + fatalError("Missing cached account \(accountID!)") + } delegate?.showLoadingLargeImage(url: account.avatar, cache: .avatars, description: nil, animatingFrom: avatarImageView) } @objc func headerPressed() { - guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } + guard let account = mastodonController.persistentContainer.account(for: accountID) else { + fatalError("Missing cached account \(accountID!)") + } delegate?.showLoadingLargeImage(url: account.header, cache: .headers, description: nil, animatingFrom: headerImageView) } + @IBAction func postsSegmentedControlChanged(_ sender: UISegmentedControl) { + delegate?.profileHeader(self, selectedPostsIndexChangedTo: sender.selectedSegmentIndex) + } + } @available(iOS 13.4, *) -extension ProfileHeaderTableViewCell: UIPointerInteractionDelegate { +extension ProfileHeaderView: UIPointerInteractionDelegate { func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? { let preview = UITargetedPreview(view: moreButton) return UIPointerStyle(effect: .lift(preview), shape: .none) } } -extension ProfileHeaderTableViewCell: MenuPreviewProvider { - var navigationDelegate: TuskerNavigationDelegate? { - delegate - } +extension ProfileHeaderView: MenuPreviewProvider { + var navigationDelegate: TuskerNavigationDelegate? { delegate } } diff --git a/Tusker/Views/Profile Header/ProfileHeaderView.xib b/Tusker/Views/Profile Header/ProfileHeaderView.xib new file mode 100644 index 00000000..940071c7 --- /dev/null +++ b/Tusker/Views/Profile Header/ProfileHeaderView.xib @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index affc26e4..41d544d3 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -378,7 +378,7 @@ extension BaseStatusTableViewCell: MenuPreviewProvider { guard let mastodonController = mastodonController else { return nil } if avatarImageView.frame.contains(location) { return ( - content: { ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController) }, + content: { ProfileViewController(accountID: self.accountID, mastodonController: mastodonController) }, actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) } ) } diff --git a/Tusker/XCallbackURL/XCBActions.swift b/Tusker/XCallbackURL/XCBActions.swift index 4e5a40a6..8c0601dc 100644 --- a/Tusker/XCallbackURL/XCBActions.swift +++ b/Tusker/XCallbackURL/XCBActions.swift @@ -252,7 +252,7 @@ struct XCBActions { static func showAccount(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) { getAccount(from: request, session: session) { (account) in DispatchQueue.main.async { - let vc = ProfileTableViewController(accountID: account.id, mastodonController: mastodonController) + let vc = ProfileViewController(accountID: account.id, mastodonController: mastodonController) show(vc) } } @@ -307,7 +307,7 @@ struct XCBActions { if silent ?? false { performAction(account) } else { - let vc = ProfileTableViewController(accountID: account.id, mastodonController: mastodonController) + let vc = ProfileViewController(accountID: account.id, mastodonController: mastodonController) DispatchQueue.main.async { show(vc) }