Compare commits
55 Commits
a5a2cd147e
...
273b74ddfb
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 273b74ddfb | |
Shadowfacts | ae055f1ffd | |
Shadowfacts | eef9b96a1a | |
Shadowfacts | 29aed65b99 | |
Shadowfacts | 090746f292 | |
Shadowfacts | af300a3559 | |
Shadowfacts | 79eb23ef5d | |
Shadowfacts | 60565f9625 | |
Shadowfacts | 70bedf17a8 | |
Shadowfacts | 392e51eb3e | |
Shadowfacts | 86d5a73c85 | |
Shadowfacts | eaefa366b7 | |
Shadowfacts | 79b23127e9 | |
Shadowfacts | f9b85c87b4 | |
Shadowfacts | 260bedcf10 | |
Shadowfacts | fe09c5e522 | |
Shadowfacts | 985d30a401 | |
Shadowfacts | 794594805c | |
Shadowfacts | 1c708732f2 | |
Shadowfacts | db30471011 | |
Shadowfacts | 2825345c7e | |
Shadowfacts | f3d01c47c3 | |
Shadowfacts | caab5e357a | |
Shadowfacts | 2916d7a72d | |
Shadowfacts | d190636fbd | |
Shadowfacts | 4e4701ead5 | |
Shadowfacts | b07efc150c | |
Shadowfacts | 19fa12391d | |
Shadowfacts | c55ea2e005 | |
Shadowfacts | 47dc00ab8f | |
Shadowfacts | fdcdbced38 | |
Shadowfacts | e70a84274e | |
Shadowfacts | 641ab765a7 | |
Shadowfacts | 986fc5b833 | |
Shadowfacts | cf5b97d9c8 | |
Shadowfacts | 7f0fd119c5 | |
Shadowfacts | b2c7735256 | |
Shadowfacts | 1d815d6cd6 | |
Shadowfacts | f86d3a0ed1 | |
Shadowfacts | 864fd77ecc | |
Shadowfacts | 78da04162f | |
Shadowfacts | 40a742139b | |
Shadowfacts | 8bbc572fa7 | |
Shadowfacts | 2a8e970738 | |
Shadowfacts | 3abb5972b9 | |
Shadowfacts | 0c06d91f6b | |
Shadowfacts | 6cf6db6a8d | |
Shadowfacts | fb11e36467 | |
Shadowfacts | 0fa87e9177 | |
Shadowfacts | 5cb84e271a | |
Shadowfacts | 50f1a9a7de | |
Shadowfacts | 154fc7cd02 | |
Shadowfacts | 01d765fa45 | |
Shadowfacts | 04aad1252a | |
Shadowfacts | 43779e42df |
|
@ -1,6 +1,3 @@
|
|||
[submodule "SwiftSoup"]
|
||||
path = SwiftSoup
|
||||
url = git://github.com/scinfu/SwiftSoup.git
|
||||
[submodule "Cache"]
|
||||
path = Cache
|
||||
url = git@github.com:hyperoslo/Cache.git
|
||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -1,5 +1,32 @@
|
|||
# Changelog
|
||||
|
||||
## 2020.1 (7)
|
||||
This is the first update since WWDC and the introduction of iOS 14. As such, most of the focus has been on fixing iOS 14-specific problems. However, there are still a couple new features, both for those on the iOS 14 beta and those not.
|
||||
|
||||
Features/Improvements:
|
||||
- Add toggle between Posts, Posts and Replies, and Media on user profiles
|
||||
- Remove 'Show Replies in Profiles' preference
|
||||
- Limit link preview animation to only link text
|
||||
- Add additional context menu actions for statuses, accounts, and hashtags
|
||||
- Add semi-translucent background to image descriptions, so they're legible against light images
|
||||
- iPadOS 14: Add sidebar
|
||||
- When using multitasking on iPad and switching in and out of "compact" mode, the active tab as well as the navigation history for all tabs will be transferred between the sidebar and tab bar modes.
|
||||
- iOS 14: Use context menus on status/account '...' buttons
|
||||
- iOS 14: Replace 'More' status swipe action with 'Share'
|
||||
|
||||
Bugfixes:
|
||||
- Fix crash when attempting to change post visibility on iPad
|
||||
- Fix attachment view corners not being rounded
|
||||
- Fix crash when viewing instance public timelines
|
||||
- Fix Preferences button not appearing on My Profile tab
|
||||
- Fix tapping current tab bar item not scrolling to top
|
||||
- Fix crash showing audio attachments on Mastodon
|
||||
- Fix timeline refreshing forever
|
||||
- Set app category (fixes usage not being categorized correctly under Screen Time)
|
||||
- iOS 14: Fix crash when searching for instances
|
||||
- iOS 14: Fix crash when displaying accounts with no pinned posts
|
||||
- iOS 14: Fix crash when displaying search results
|
||||
|
||||
## 2020.1 (6)
|
||||
This is the pre-WWDC update with lots of bugfixes and some small features. There will likely be another build this week to fix any pressing issues that arise from iOS 14.
|
||||
|
||||
|
|
2
Gifu
2
Gifu
|
@ -1 +1 @@
|
|||
Subproject commit ed572f53ce58b8e23499abeb3a926033cbe480f7
|
||||
Subproject commit 9b1a6461aa3b5f66cb0ed3a50c5523db0b4fb007
|
|
@ -13,7 +13,7 @@ public class Attachment: Codable {
|
|||
public let kind: Kind
|
||||
public let url: URL
|
||||
public let remoteURL: URL?
|
||||
public let previewURL: URL
|
||||
public let previewURL: URL?
|
||||
public let textURL: URL?
|
||||
public let meta: Metadata?
|
||||
public let description: String?
|
||||
|
@ -30,11 +30,11 @@ public class Attachment: Codable {
|
|||
self.id = try container.decode(String.self, forKey: .id)
|
||||
self.kind = try container.decode(Kind.self, forKey: .kind)
|
||||
self.url = try container.decode(URL.self, forKey: .url)
|
||||
self.previewURL = try container.decode(URL.self, forKey: .previewURL)
|
||||
self.remoteURL = try? container.decode(URL.self, forKey: .remoteURL)
|
||||
self.textURL = try? container.decode(URL.self, forKey: .textURL)
|
||||
self.meta = try? container.decode(Metadata.self, forKey: .meta)
|
||||
self.description = try? container.decode(String.self, forKey: .description)
|
||||
self.previewURL = try? container.decode(URL?.self, forKey: .previewURL)
|
||||
self.remoteURL = try? container.decode(URL?.self, forKey: .remoteURL)
|
||||
self.textURL = try? container.decode(URL?.self, forKey: .textURL)
|
||||
self.meta = try? container.decode(Metadata?.self, forKey: .meta)
|
||||
self.description = try? container.decode(String?.self, forKey: .description)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public class List: Decodable {
|
||||
public class List: Decodable, Equatable, Hashable {
|
||||
public let id: String
|
||||
public let title: String
|
||||
|
||||
|
@ -16,6 +16,14 @@ public class List: Decodable {
|
|||
return .list(id: id)
|
||||
}
|
||||
|
||||
public static func ==(lhs: List, rhs: List) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
|
||||
public static func getAccounts(_ list: List, range: RequestRange = .default) -> Request<[Account]> {
|
||||
var request = Request<[Account]>(method: .get, path: "/api/v1/lists/\(list.id)/accounts")
|
||||
request.range = range
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit f445c9067d28346e828e615e2b43cb07b20bca35
|
|
@ -21,6 +21,7 @@
|
|||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
|
||||
D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */; };
|
||||
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; };
|
||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; };
|
||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.swift */; };
|
||||
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; };
|
||||
|
@ -68,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 */; };
|
||||
|
@ -116,7 +116,14 @@
|
|||
D63F9C6B241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */; };
|
||||
D63F9C6C241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */; };
|
||||
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; };
|
||||
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 */; };
|
||||
|
@ -137,7 +144,6 @@
|
|||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
|
||||
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; };
|
||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
|
||||
D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; };
|
||||
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; };
|
||||
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */; };
|
||||
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */; };
|
||||
|
@ -149,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 */; };
|
||||
|
@ -174,7 +177,10 @@
|
|||
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
|
||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
|
||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
|
||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
|
||||
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525C24A3E8F00054355A /* SearchViewController.swift */; };
|
||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
|
||||
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */; };
|
||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; };
|
||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; };
|
||||
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; };
|
||||
|
@ -226,7 +232,6 @@
|
|||
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */; };
|
||||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; };
|
||||
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */; };
|
||||
D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
|
||||
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
|
||||
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
||||
|
@ -250,6 +255,8 @@
|
|||
D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; };
|
||||
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
|
||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; };
|
||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; };
|
||||
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; };
|
||||
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F1F84C2193B56E00F5FE67 /* Cache.swift */; };
|
||||
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */; };
|
||||
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; };
|
||||
|
@ -303,7 +310,6 @@
|
|||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
D61099C12144B0CC00432DC2 /* Pachyderm.framework in Embed Frameworks */,
|
||||
D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */,
|
||||
D6BC874621961F73006163F1 /* Gifu.framework in Embed Frameworks */,
|
||||
0461A3912163CBAE00C0A807 /* Cache.framework in Embed Frameworks */,
|
||||
);
|
||||
|
@ -375,7 +381,6 @@
|
|||
D6109A10214607D500432DC2 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = "<group>"; };
|
||||
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D6163F2B21AA0AF1008DAC41 /* MyProfileTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTableViewController.swift; sourceTree = "<group>"; };
|
||||
D61AC1D2232E928600C54D2D /* InstanceSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelector.swift; sourceTree = "<group>"; };
|
||||
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; };
|
||||
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -422,7 +427,14 @@
|
|||
D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeAttachmentTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
|
||||
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectImageButton.swift; sourceTree = "<group>"; };
|
||||
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
|
||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScrollableViewController.swift; sourceTree = "<group>"; };
|
||||
D6412B0424B0227D00F5412E /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
||||
D6412B0624B0237700F5412E /* ProfileStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusesViewController.swift; sourceTree = "<group>"; };
|
||||
D6412B0824B0291E00F5412E /* MyProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileViewController.swift; sourceTree = "<group>"; };
|
||||
D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderView.xib; sourceTree = "<group>"; };
|
||||
D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = "<group>"; };
|
||||
D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
|
||||
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
|
||||
D6434EB2215B1856001A919A /* XCBRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequest.swift; sourceTree = "<group>"; };
|
||||
|
@ -458,9 +470,6 @@
|
|||
D667383B23299340000A2373 /* InstanceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceType.swift; sourceTree = "<group>"; };
|
||||
D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; };
|
||||
D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TimelineStatusTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTableViewController.swift; sourceTree = "<group>"; };
|
||||
D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Delegates.swift"; sourceTree = "<group>"; };
|
||||
D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTableViewController.swift; sourceTree = "<group>"; };
|
||||
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
|
||||
|
@ -483,7 +492,10 @@
|
|||
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; };
|
||||
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
|
||||
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
|
||||
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
|
||||
D68E525C24A3E8F00054355A /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
|
||||
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
|
||||
D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Helpers.swift"; sourceTree = "<group>"; };
|
||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; };
|
||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
|
||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedDataManager.swift; sourceTree = "<group>"; };
|
||||
|
@ -530,7 +542,6 @@
|
|||
D6BC9DB4232D4CE3002CA326 /* NotificationsMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsMode.swift; sourceTree = "<group>"; };
|
||||
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = "<group>"; };
|
||||
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = "<group>"; };
|
||||
D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
|
||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -556,16 +567,18 @@
|
|||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = "<group>"; };
|
||||
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
|
||||
D6E4885C24A2890C0011C13E /* Tusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tusker.entitlements; sourceTree = "<group>"; };
|
||||
D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = "<group>"; };
|
||||
D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = "<group>"; };
|
||||
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
|
||||
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; };
|
||||
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitViewController.swift; sourceTree = "<group>"; };
|
||||
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarViewController.swift; sourceTree = "<group>"; };
|
||||
D6F1F84C2193B56E00F5FE67 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; };
|
||||
D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterViewController.swift; sourceTree = "<group>"; };
|
||||
D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CrashReporterViewController.xib; sourceTree = "<group>"; };
|
||||
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
|
||||
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
|
||||
D6F98BD523AE951F008A4DAC /* Swifter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Swifter.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -590,10 +603,10 @@
|
|||
files = (
|
||||
D61099C02144B0CC00432DC2 /* Pachyderm.framework in Frameworks */,
|
||||
D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */,
|
||||
D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */,
|
||||
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */,
|
||||
D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */,
|
||||
0461A3902163CBAE00C0A807 /* Cache.framework in Frameworks */,
|
||||
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -899,7 +912,10 @@
|
|||
D641C782213DD7F0004B4513 /* Main */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */,
|
||||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
||||
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */,
|
||||
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */,
|
||||
);
|
||||
path = Main;
|
||||
sourceTree = "<group>";
|
||||
|
@ -916,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 = "<group>";
|
||||
|
@ -994,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 = "<group>";
|
||||
|
@ -1031,7 +1048,6 @@
|
|||
D65F613523AFD65900F3CFD3 /* Ambassador.framework */,
|
||||
D65F613023AE99E000F3CFD3 /* Ambassador.framework */,
|
||||
D65F612D23AE990C00F3CFD3 /* Embassy.framework */,
|
||||
D6F98BD523AE951F008A4DAC /* Swifter.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1064,6 +1080,7 @@
|
|||
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */,
|
||||
D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */,
|
||||
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */,
|
||||
D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1194,6 +1211,7 @@
|
|||
D6BC9DD8232D8BCA002CA326 /* Search */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D68E525C24A3E8F00054355A /* SearchViewController.swift */,
|
||||
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */,
|
||||
);
|
||||
path = Search;
|
||||
|
@ -1211,6 +1229,7 @@
|
|||
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
|
||||
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
|
||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
||||
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */,
|
||||
D67C57A721E2649B00C3118B /* Account Detail */,
|
||||
D67C57B021E28F9400C3118B /* Compose Status Reply */,
|
||||
D626494023C122C800612E6E /* Asset Picker */,
|
||||
|
@ -1240,6 +1259,7 @@
|
|||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
||||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1259,7 +1279,6 @@
|
|||
children = (
|
||||
D6BC874421961F73006163F1 /* Gifu.framework */,
|
||||
0461A38F2163CBAE00C0A807 /* Cache.framework */,
|
||||
D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */,
|
||||
D61099AC2144B0CC00432DC2 /* Pachyderm */,
|
||||
D61099B92144B0CC00432DC2 /* PachydermTests */,
|
||||
D6D4DDCE212518A000E1C4BB /* Tusker */,
|
||||
|
@ -1285,6 +1304,7 @@
|
|||
D6D4DDCE212518A000E1C4BB /* Tusker */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6E4885C24A2890C0011C13E /* Tusker.entitlements */,
|
||||
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
||||
D6AC956623C4347E008C9946 /* SceneDelegate.swift */,
|
||||
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
|
||||
|
@ -1433,6 +1453,7 @@
|
|||
packageProductDependencies = (
|
||||
D6B0539E23BD2BA300A066FA /* SheetController */,
|
||||
D69CCBBE249E6EFD000AF167 /* CrashReporter */,
|
||||
D60CFFDA24A290BA00D00083 /* SwiftSoup */,
|
||||
);
|
||||
productName = Tusker;
|
||||
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
|
||||
|
@ -1482,7 +1503,7 @@
|
|||
D6D4DDC4212518A000E1C4BB /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1000;
|
||||
LastSwiftUpdateCheck = 1200;
|
||||
LastUpgradeCheck = 1020;
|
||||
ORGANIZATIONNAME = Shadowfacts;
|
||||
TargetAttributes = {
|
||||
|
@ -1529,6 +1550,7 @@
|
|||
packageReferences = (
|
||||
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */,
|
||||
D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */,
|
||||
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
||||
);
|
||||
productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -1568,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 */,
|
||||
|
@ -1702,9 +1724,11 @@
|
|||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
||||
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */,
|
||||
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */,
|
||||
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
|
||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
|
||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
||||
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
|
||||
D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */,
|
||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
||||
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
|
||||
|
@ -1720,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 */,
|
||||
|
@ -1736,6 +1761,7 @@
|
|||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
||||
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
|
||||
D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */,
|
||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
|
||||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
||||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */,
|
||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
||||
|
@ -1753,6 +1779,7 @@
|
|||
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
||||
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
|
||||
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */,
|
||||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
|
||||
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
|
||||
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */,
|
||||
|
@ -1775,6 +1802,7 @@
|
|||
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */,
|
||||
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */,
|
||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
|
||||
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */,
|
||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
||||
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
|
||||
|
@ -1796,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 */,
|
||||
|
@ -1828,14 +1857,15 @@
|
|||
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 */,
|
||||
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */,
|
||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
||||
0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */,
|
||||
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
||||
|
@ -1846,11 +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;
|
||||
};
|
||||
|
@ -2140,23 +2170,27 @@
|
|||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2020.1;
|
||||
OTHER_LDFLAGS = "";
|
||||
"OTHER_SWIFT_FLAGS[sdk=iphone*14*]" = "$(inherited) -D SDK_IOS_14";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
@ -2165,22 +2199,26 @@
|
|||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2020.1;
|
||||
"OTHER_SWIFT_FLAGS[sdk=iphone*14*]" = "$(inherited) -D SDK_IOS_14";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
@ -2326,6 +2364,14 @@
|
|||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/scinfu/SwiftSoup.git";
|
||||
requirement = {
|
||||
kind = upToNextMinorVersion;
|
||||
minimumVersion = 2.3.2;
|
||||
};
|
||||
};
|
||||
D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/microsoft/plcrashreporter";
|
||||
|
@ -2345,6 +2391,11 @@
|
|||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
D60CFFDA24A290BA00D00083 /* SwiftSoup */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
||||
productName = SwiftSoup;
|
||||
};
|
||||
D69CCBBE249E6EFD000AF167 /* CrashReporter */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */;
|
||||
|
|
|
@ -10,9 +10,6 @@
|
|||
<FileRef
|
||||
location = "group:Cache/Cache.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:SwiftSoup/SwiftSoup.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Gifu/Gifu.xcodeproj">
|
||||
</FileRef>
|
||||
|
|
|
@ -9,6 +9,24 @@
|
|||
"revision": "4637a7854de2cc5c354d46fb931d74bdbc2c043e",
|
||||
"version": "1.7.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SheetController",
|
||||
"repositoryURL": "https://git.shadowfacts.net/shadowfacts/SheetController.git",
|
||||
"state": {
|
||||
"branch": "master",
|
||||
"revision": "6926446c4e15eb7f4513c4c00df9279553b330be",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftSoup",
|
||||
"repositoryURL": "https://github.com/scinfu/SwiftSoup.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "774dc9c7213085db8aa59595e27c1cd22e428904",
|
||||
"version": "2.3.2"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
//
|
||||
// UIBezierPath+Helpers.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 6/25/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
// TODO: write unit tests for this
|
||||
extension UIBezierPath {
|
||||
|
||||
/// Create a new UIBezierPath that wraps around the given array of rectangles.
|
||||
/// This is not a convex hull aglorithm. What this does is it takes a set of rectangles
|
||||
/// and draws a line around the outer borders of the combined shape.
|
||||
convenience init(wrappingAround rects: [CGRect]) {
|
||||
precondition(rects.count > 0)
|
||||
|
||||
if rects.count == 1 {
|
||||
self.init(rect: rects.first!)
|
||||
return
|
||||
}
|
||||
|
||||
let rects = rects.sorted { $0.minY < $1.minY }
|
||||
|
||||
self.init()
|
||||
|
||||
// start at the top left corner
|
||||
self.move(to: CGPoint(x: rects.first!.minX, y: rects.first!.minY))
|
||||
|
||||
// walk down the left side
|
||||
var prevLeft = rects.first!.minX
|
||||
for rect in rects where !rect.minX.isEqual(to: prevLeft) {
|
||||
self.addLine(to: CGPoint(x: prevLeft, y: rect.minY))
|
||||
self.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
|
||||
prevLeft = rect.minX
|
||||
}
|
||||
|
||||
// ensure at the bottom left if not already
|
||||
let bottomLeft = CGPoint(x: rects.last!.minX, y: rects.last!.maxY)
|
||||
if !self.currentPoint.equalTo(bottomLeft) {
|
||||
self.addLine(to: bottomLeft)
|
||||
}
|
||||
|
||||
// across the bottom of the last rect
|
||||
self.addLine(to: CGPoint(x: rects.last!.maxX, y: rects.last!.maxY))
|
||||
|
||||
// walk up the right side
|
||||
var prevRight = rects.last!.maxX
|
||||
for rect in rects.reversed() where !rect.maxX.isEqual(to: prevRight) {
|
||||
self.addLine(to: CGPoint(x: prevRight, y: rect.maxY))
|
||||
self.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
|
||||
prevRight = rect.maxX
|
||||
}
|
||||
|
||||
// ensure at the top right if not already
|
||||
let topRight = CGPoint(x: rects.first!.maxX, y: rects.first!.minY)
|
||||
if !self.currentPoint.equalTo(topRight) {
|
||||
self.addLine(to: topRight)
|
||||
}
|
||||
|
||||
// across the top of the first rect
|
||||
self.addLine(to: CGPoint(x: rects.first!.minX, y: rects.first!.minY))
|
||||
}
|
||||
|
||||
}
|
|
@ -31,6 +31,8 @@
|
|||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.social-networking</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
|
|
@ -30,7 +30,20 @@ class LocalData: ObservableObject {
|
|||
]
|
||||
}
|
||||
} else {
|
||||
defaults = UserDefaults()
|
||||
defaults = UserDefaults(suiteName: "group.space.vaccor.Tusker")!
|
||||
tryMigrateOldDefaults()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove me before public beta
|
||||
private func tryMigrateOldDefaults() {
|
||||
let old = UserDefaults()
|
||||
if let accounts = old.array(forKey: accountsKey) as? [[String: String]],
|
||||
let mostRecentAccount = old.string(forKey: mostRecentAccountKey) {
|
||||
defaults.setValue(accounts, forKey: accountsKey)
|
||||
defaults.setValue(mostRecentAccount, forKey: mostRecentAccountKey)
|
||||
old.removeObject(forKey: accountsKey)
|
||||
old.removeObject(forKey: mostRecentAccountKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ class Preferences: Codable, ObservableObject {
|
|||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.theme = try container.decode(UIUserInterfaceStyle.self, forKey: .theme)
|
||||
self.showRepliesInProfiles = try container.decode(Bool.self, forKey: .showRepliesInProfiles)
|
||||
self.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle)
|
||||
self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames)
|
||||
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
|
||||
|
@ -68,7 +67,6 @@ class Preferences: Codable, ObservableObject {
|
|||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(theme, forKey: .theme)
|
||||
try container.encode(showRepliesInProfiles, forKey: .showRepliesInProfiles)
|
||||
try container.encode(avatarStyle, forKey: .avatarStyle)
|
||||
try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames)
|
||||
try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon)
|
||||
|
@ -96,7 +94,6 @@ class Preferences: Codable, ObservableObject {
|
|||
|
||||
// MARK: Appearance
|
||||
@Published var theme = UIUserInterfaceStyle.unspecified
|
||||
@Published var showRepliesInProfiles = false
|
||||
@Published var avatarStyle = AvatarStyle.roundRect
|
||||
@Published var hideCustomEmojiInUsernames = false
|
||||
@Published var showIsStatusReplyIcon = false
|
||||
|
@ -128,7 +125,6 @@ class Preferences: Codable, ObservableObject {
|
|||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case theme
|
||||
case showRepliesInProfiles
|
||||
case avatarStyle
|
||||
case hideCustomEmojiInUsernames
|
||||
case showIsStatusReplyIcon
|
||||
|
|
|
@ -157,8 +157,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
mastodonController.getOwnAccount()
|
||||
mastodonController.getOwnInstance()
|
||||
|
||||
let tabBarController = MainTabBarViewController(mastodonController: mastodonController)
|
||||
window!.rootViewController = tabBarController
|
||||
let rootController: UIViewController
|
||||
#if SDK_IOS_14
|
||||
if #available(iOS 14.0, *) {
|
||||
rootController = MainSplitViewController(mastodonController: mastodonController)
|
||||
} else {
|
||||
rootController = MainTabBarViewController(mastodonController: mastodonController)
|
||||
}
|
||||
#else
|
||||
rootController = MainTabBarViewController(mastodonController: mastodonController)
|
||||
#endif
|
||||
window!.rootViewController = rootController
|
||||
}
|
||||
|
||||
func showOnboardingUI() {
|
||||
|
|
|
@ -22,20 +22,22 @@ class AssetCollectionViewController: UICollectionViewController {
|
|||
|
||||
weak var delegate: AssetCollectionViewControllerDelegate?
|
||||
|
||||
var flowLayout: UICollectionViewFlowLayout {
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
private var flowLayout: UICollectionViewFlowLayout {
|
||||
return collectionViewLayout as! UICollectionViewFlowLayout
|
||||
}
|
||||
|
||||
var availableWidth: CGFloat!
|
||||
var thumbnailSize: CGSize!
|
||||
private var availableWidth: CGFloat!
|
||||
private var thumbnailSize: CGSize!
|
||||
|
||||
let imageManager = PHCachingImageManager()
|
||||
var fetchResult: PHFetchResult<PHAsset>!
|
||||
private let imageManager = PHCachingImageManager()
|
||||
private var fetchResult: PHFetchResult<PHAsset>!
|
||||
|
||||
var selectedAssets: [PHAsset] {
|
||||
return collectionView.indexPathsForSelectedItems?.map({ (indexPath) in
|
||||
fetchResult.object(at: indexPath.row - 1)
|
||||
}) ?? []
|
||||
return collectionView.indexPathsForSelectedItems?.compactMap { (indexPath) in
|
||||
guard case let .asset(asset) = dataSource.itemIdentifier(for: indexPath) else { return nil }
|
||||
return asset
|
||||
} ?? []
|
||||
}
|
||||
|
||||
init() {
|
||||
|
@ -71,14 +73,42 @@ class AssetCollectionViewController: UICollectionViewController {
|
|||
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
|
||||
collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier)
|
||||
|
||||
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
|
||||
switch item {
|
||||
case .showCamera:
|
||||
return collectionView.dequeueReusableCell(withReuseIdentifier: cameraReuseIdentifier, for: indexPath)
|
||||
case let .asset(asset):
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! AssetCollectionViewCell
|
||||
|
||||
cell.updateUI(asset: asset)
|
||||
self.imageManager.requestImage(for: asset, targetSize: self.thumbnailSize, contentMode: .aspectFill, options: nil) { (image, _) in
|
||||
guard let image = image else { return }
|
||||
DispatchQueue.main.async {
|
||||
guard cell.assetIdentifier == asset.localIdentifier else { return }
|
||||
cell.thumbnailImage = image
|
||||
}
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
})
|
||||
|
||||
let options = PHFetchOptions()
|
||||
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
|
||||
fetchResult = fetchAssets(with: options)
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.assets])
|
||||
var items: [Item] = [.showCamera]
|
||||
fetchResult.enumerateObjects { (asset, _, _) in
|
||||
items.append(.asset(asset))
|
||||
}
|
||||
snapshot.appendItems(items)
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
collectionView.allowsMultipleSelection = true
|
||||
setEditing(true, animated: false)
|
||||
|
||||
updateItemsSelected()
|
||||
updateItemsSelectedCount()
|
||||
|
||||
if let singleFingerPanGesture = collectionView.gestureRecognizers?.first(where: {
|
||||
$0.name == "multi-select.singleFingerPanGesture"
|
||||
|
@ -115,43 +145,12 @@ class AssetCollectionViewController: UICollectionViewController {
|
|||
return PHAsset.fetchAssets(with: options)
|
||||
}
|
||||
|
||||
func updateItemsSelected() {
|
||||
func updateItemsSelectedCount() {
|
||||
let selected = collectionView.indexPathsForSelectedItems?.count ?? 0
|
||||
|
||||
navigationItem.title = "\(selected) selected"
|
||||
}
|
||||
|
||||
// MARK: UICollectionViewDataSource
|
||||
|
||||
override func numberOfSections(in collectionView: UICollectionView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return fetchResult.count + 1
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
if indexPath.row == 0 {
|
||||
return collectionView.dequeueReusableCell(withReuseIdentifier: cameraReuseIdentifier, for: indexPath)
|
||||
} else {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! AssetCollectionViewCell
|
||||
|
||||
let asset = fetchResult.object(at: indexPath.row - 1)
|
||||
|
||||
cell.updateUI(asset: asset)
|
||||
imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil) { (image, _) in
|
||||
guard let image = image else { return }
|
||||
DispatchQueue.main.async {
|
||||
guard cell.assetIdentifier == asset.localIdentifier else { return }
|
||||
cell.thumbnailImage = image
|
||||
}
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UICollectionViewDelegate
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool {
|
||||
|
@ -159,36 +158,34 @@ class AssetCollectionViewController: UICollectionViewController {
|
|||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||
if indexPath.row > 0,
|
||||
let delegate = delegate {
|
||||
let asset = fetchResult.object(at: indexPath.row - 1)
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return false }
|
||||
if let delegate = delegate,
|
||||
case let .asset(asset) = item {
|
||||
return delegate.shouldSelectAsset(asset)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
if indexPath.row == 0 {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||
switch item {
|
||||
case .showCamera:
|
||||
collectionView.deselectItem(at: indexPath, animated: false)
|
||||
delegate?.captureFromCamera()
|
||||
} else {
|
||||
updateItemsSelected()
|
||||
case .asset(_):
|
||||
updateItemsSelectedCount()
|
||||
}
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
|
||||
updateItemsSelected()
|
||||
updateItemsSelectedCount()
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
if indexPath.row == 0 {
|
||||
return nil
|
||||
} else {
|
||||
let asset = fetchResult.object(at: indexPath.row - 1)
|
||||
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in
|
||||
return AssetPreviewViewController(asset: asset)
|
||||
}, actionProvider: nil)
|
||||
}
|
||||
guard case let .asset(asset) = dataSource.itemIdentifier(for: indexPath) else { return nil }
|
||||
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in
|
||||
return AssetPreviewViewController(asset: asset)
|
||||
}, actionProvider: nil)
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
|
@ -210,3 +207,13 @@ class AssetCollectionViewController: UICollectionViewController {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension AssetCollectionViewController {
|
||||
enum Section: Hashable {
|
||||
case assets
|
||||
}
|
||||
enum Item: Hashable {
|
||||
case showCamera
|
||||
case asset(PHAsset)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -149,20 +149,6 @@ class BookmarksTableViewController: EnhancedTableViewController {
|
|||
return config
|
||||
}
|
||||
|
||||
override func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] {
|
||||
guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { return [] }
|
||||
return [
|
||||
UIAction(title: NSLocalizedString("Unbookmark", comment: "unbookmark action title"), image: UIImage(systemName: "bookmark.fill"), identifier: .init("unbookmark"), discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
|
||||
let request = Status.unbookmark(status.id)
|
||||
self.mastodonController.run(request) { (response) in
|
||||
guard case let .success(newStatus, _) = response else { fatalError() }
|
||||
self.mastodonController.persistentContainer.addOrUpdate(status: newStatus, incrementReferenceCount: false)
|
||||
self.statuses.remove(at: indexPath.row)
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension BookmarksTableViewController: StatusTableViewCellDelegate {
|
||||
|
|
|
@ -58,7 +58,15 @@ class ComposeDrawingViewController: UIViewController {
|
|||
canvasView.drawing = initialDrawing
|
||||
}
|
||||
canvasView.delegate = self
|
||||
#if SDK_IOS_14
|
||||
if #available(iOS 14.0, *) {
|
||||
canvasView.drawingPolicy = .anyInput
|
||||
} else {
|
||||
canvasView.allowsFingerDrawing = true
|
||||
}
|
||||
#else
|
||||
canvasView.allowsFingerDrawing = true
|
||||
#endif
|
||||
canvasView.minimumZoomScale = 0.5
|
||||
canvasView.maximumZoomScale = 2
|
||||
canvasView.backgroundColor = .systemBackground
|
||||
|
@ -75,6 +83,7 @@ class ComposeDrawingViewController: UIViewController {
|
|||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
// todo: should the PKToolPicker be owned by this VC or something else?
|
||||
if let window = parent?.view.window, let toolPicker = PKToolPicker.shared(for: window) {
|
||||
toolPicker.setVisible(true, forFirstResponder: canvasView)
|
||||
toolPicker.addObserver(canvasView)
|
||||
|
|
|
@ -189,7 +189,7 @@ extension ConversationTableViewController: UITableViewDataSourcePrefetching {
|
|||
for indexPath in indexPaths {
|
||||
guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue }
|
||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
||||
for attachment in status.attachments {
|
||||
for attachment in status.attachments where attachment.kind == .image {
|
||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
||||
}
|
||||
}
|
||||
|
@ -199,7 +199,7 @@ extension ConversationTableViewController: UITableViewDataSourcePrefetching {
|
|||
for indexPath in indexPaths {
|
||||
guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue }
|
||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
||||
for attachment in status.attachments {
|
||||
for attachment in status.attachments where attachment.kind == .image {
|
||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ class ExploreViewController: EnhancedTableViewController {
|
|||
var resultsController: SearchResultsViewController!
|
||||
var searchController: UISearchController!
|
||||
|
||||
var searchControllerStatusOnAppearance: Bool? = nil
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
|
@ -90,9 +92,7 @@ class ExploreViewController: EnhancedTableViewController {
|
|||
snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags)
|
||||
snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances)
|
||||
// the initial, static items should not be displayed with an animation
|
||||
UIView.performWithoutAnimation {
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
||||
resultsController.exploreNavigationController = self.navigationController!
|
||||
|
@ -111,6 +111,18 @@ class ExploreViewController: EnhancedTableViewController {
|
|||
reloadLists()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
// this is a workaround for the issue that setting isActive on a search controller that is not visible
|
||||
// does not cause it to automatically become active once it becomes visible
|
||||
// see FB7814561
|
||||
if let active = searchControllerStatusOnAppearance {
|
||||
searchController.isActive = active
|
||||
searchControllerStatusOnAppearance = nil
|
||||
}
|
||||
}
|
||||
|
||||
func reloadLists() {
|
||||
let request = Client.getLists()
|
||||
mastodonController.run(request) { (response) in
|
||||
|
|
|
@ -46,6 +46,8 @@ class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentV
|
|||
}
|
||||
|
||||
override public func display(_ layer: CALayer) {
|
||||
super.display(layer)
|
||||
|
||||
updateImageIfNeeded()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,8 +86,9 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
|
||||
scrollView.delegate = self
|
||||
|
||||
if let imageDescription = imageDescription {
|
||||
descriptionLabel.text = imageDescription
|
||||
if let imageDescription = imageDescription,
|
||||
!imageDescription.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
descriptionLabel.text = imageDescription.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
} else {
|
||||
bottomControlsView.isHidden = true
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
|
@ -70,23 +71,25 @@
|
|||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rPa-Zu-T6g">
|
||||
<rect key="frame" x="0.0" y="630.5" width="375" height="36.5"/>
|
||||
<rect key="frame" x="0.0" y="622.5" width="375" height="44.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eo5-fc-RV8">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="20.5"/>
|
||||
<rect key="frame" x="16" y="8" width="343" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.5" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="eo5-fc-RV8" firstAttribute="top" secondItem="rPa-Zu-T6g" secondAttribute="top" constant="8" id="6n3-E0-2G6"/>
|
||||
<constraint firstAttribute="trailing" secondItem="eo5-fc-RV8" secondAttribute="trailing" constant="16" id="6uL-vY-tqk"/>
|
||||
<constraint firstItem="eo5-fc-RV8" firstAttribute="leading" secondItem="rPa-Zu-T6g" secondAttribute="leading" constant="16" id="KIF-vw-K7n"/>
|
||||
<constraint firstAttribute="height" secondItem="eo5-fc-RV8" secondAttribute="height" constant="16" id="bt3-XT-WzC"/>
|
||||
<constraint firstAttribute="bottom" secondItem="eo5-fc-RV8" secondAttribute="bottom" constant="16" id="v43-mS-tyR"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="w1g-VC-Ll9"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<gestureRecognizers/>
|
||||
<constraints>
|
||||
|
@ -101,7 +104,6 @@
|
|||
<constraint firstItem="Skj-xq-AgQ" firstAttribute="height" secondItem="BJw-5C-9nT" secondAttribute="height" id="jvz-QW-n9c"/>
|
||||
<constraint firstItem="kHo-B9-R7a" firstAttribute="top" secondItem="BJw-5C-9nT" secondAttribute="top" id="n1O-C3-yQR"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="w1g-VC-Ll9"/>
|
||||
<point key="canvasLocation" x="-164" y="476"/>
|
||||
</view>
|
||||
</objects>
|
||||
|
|
|
@ -0,0 +1,383 @@
|
|||
//
|
||||
// MainSidebarViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 6/24/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
#if SDK_IOS_14
|
||||
@available(iOS 14.0, *)
|
||||
protocol MainSidebarViewControllerDelegate: class {
|
||||
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item)
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class MainSidebarViewController: UIViewController {
|
||||
|
||||
private weak var mastodonController: MastodonController!
|
||||
|
||||
weak var sidebarDelegate: MainSidebarViewControllerDelegate?
|
||||
|
||||
private var collectionView: UICollectionView!
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
|
||||
var allItems: [Item] {
|
||||
[
|
||||
.tab(.timelines),
|
||||
.tab(.notifications),
|
||||
.tab(.myProfile),
|
||||
] + exploreTabItems
|
||||
}
|
||||
|
||||
var exploreTabItems: [Item] {
|
||||
var items: [Item] = [.search, .bookmarks]
|
||||
let snapshot = dataSource.snapshot()
|
||||
for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) {
|
||||
items.append(.list(list))
|
||||
}
|
||||
for case let .savedHashtag(hashtag) in snapshot.itemIdentifiers(inSection: .savedHashtags) {
|
||||
items.append(.savedHashtag(hashtag))
|
||||
}
|
||||
for case let .savedInstance(instance) in snapshot.itemIdentifiers(inSection: .savedInstances) {
|
||||
items.append(.savedInstance(instance))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
private(set) var previouslySelectedItem: Item?
|
||||
var selectedItem: Item? {
|
||||
guard let indexPath = collectionView.indexPathsForSelectedItems?.first else {
|
||||
return nil
|
||||
}
|
||||
return dataSource.itemIdentifier(for: indexPath)
|
||||
}
|
||||
|
||||
private(set) var itemLastSelectedTimestamps = [Item: Date]()
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
navigationItem.title = "Tusker"
|
||||
navigationItem.largeTitleDisplayMode = .always
|
||||
navigationController!.navigationBar.prefersLargeTitles = true
|
||||
|
||||
let layout = UICollectionViewCompositionalLayout.list(using: .init(appearance: .sidebar))
|
||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
collectionView.backgroundColor = .systemGroupedBackground
|
||||
collectionView.delegate = self
|
||||
view.addSubview(collectionView)
|
||||
|
||||
dataSource = createDataSource()
|
||||
|
||||
applyInitialSnapshot()
|
||||
|
||||
select(item: .tab(.timelines), animated: false)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
|
||||
}
|
||||
|
||||
func select(item: Item, animated: Bool) {
|
||||
guard let indexPath = dataSource.indexPath(for: item) else { return }
|
||||
collectionView.selectItem(at: indexPath, animated: animated, scrollPosition: .top)
|
||||
itemLastSelectedTimestamps[item] = Date()
|
||||
}
|
||||
|
||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
let listCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { (cell, indexPath, item) in
|
||||
var config = cell.defaultContentConfiguration()
|
||||
config.text = item.title
|
||||
if let imageName = item.imageName {
|
||||
config.image = UIImage(systemName: imageName)
|
||||
}
|
||||
cell.contentConfiguration = config
|
||||
}
|
||||
|
||||
let outlineHeaderCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { (cell, indexPath, item) in
|
||||
var config = cell.defaultContentConfiguration()
|
||||
config.attributedText = NSAttributedString(string: item.title, attributes: [
|
||||
.font: UIFont.boldSystemFont(ofSize: 21)
|
||||
])
|
||||
cell.contentConfiguration = config
|
||||
cell.accessories = [.outlineDisclosure(options: .init(style: .header))]
|
||||
}
|
||||
|
||||
return UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
|
||||
if item.hasChildren {
|
||||
return collectionView.dequeueConfiguredReusableCell(using: outlineHeaderCell, for: indexPath, item: item)
|
||||
} else {
|
||||
return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func applyInitialSnapshot() {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections(Section.allCases)
|
||||
snapshot.appendItems([
|
||||
.tab(.timelines),
|
||||
.tab(.notifications),
|
||||
.search,
|
||||
.bookmarks,
|
||||
.tab(.myProfile)
|
||||
], toSection: .tabs)
|
||||
snapshot.appendItems([
|
||||
.tab(.compose)
|
||||
], toSection: .compose)
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
reloadLists()
|
||||
reloadSavedHashtags()
|
||||
reloadSavedInstances()
|
||||
}
|
||||
|
||||
private func reloadLists() {
|
||||
let request = Client.getLists()
|
||||
mastodonController.run(request) { [weak self] (response) in
|
||||
guard let self = self, case let .success(lists, _) = response else { return }
|
||||
|
||||
var exploreSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
||||
exploreSnapshot.append([.listsHeader])
|
||||
exploreSnapshot.expand([.listsHeader])
|
||||
exploreSnapshot.append(lists.map { .list($0) }, to: .listsHeader)
|
||||
exploreSnapshot.append([.addList], to: .listsHeader)
|
||||
DispatchQueue.main.async {
|
||||
self.dataSource.apply(exploreSnapshot, to: .lists)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func reloadSavedHashtags() {
|
||||
var hashtagsSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
||||
hashtagsSnapshot.append([.savedHashtagsHeader])
|
||||
hashtagsSnapshot.expand([.savedHashtagsHeader])
|
||||
let sortedHashtags = SavedDataManager.shared.sortedHashtags(for: mastodonController.accountInfo!)
|
||||
hashtagsSnapshot.append(sortedHashtags.map { .savedHashtag($0) }, to: .savedHashtagsHeader)
|
||||
hashtagsSnapshot.append([.addSavedHashtag], to: .savedHashtagsHeader)
|
||||
self.dataSource.apply(hashtagsSnapshot, to: .savedHashtags, animatingDifferences: false)
|
||||
}
|
||||
|
||||
@objc private func reloadSavedInstances() {
|
||||
var instancesSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
||||
instancesSnapshot.append([.savedInstancesHeader])
|
||||
instancesSnapshot.expand([.savedInstancesHeader])
|
||||
let sortedInstances = SavedDataManager.shared.savedInstances(for: mastodonController.accountInfo!)
|
||||
instancesSnapshot.append(sortedInstances.map { .savedInstance($0) }, to: .savedInstancesHeader)
|
||||
instancesSnapshot.append([.addSavedInstance], to: .savedInstancesHeader)
|
||||
self.dataSource.apply(instancesSnapshot, to: .savedInstances, animatingDifferences: false)
|
||||
}
|
||||
|
||||
// todo: deduplicate with ExploreViewController
|
||||
private func showAddList() {
|
||||
let alert = UIAlertController(title: "New List", message: "Choose a title for your new list", preferredStyle: .alert)
|
||||
alert.addTextField(configurationHandler: nil)
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||
alert.addAction(UIAlertAction(title: "Create List", style: .default, handler: { (_) in
|
||||
guard let title = alert.textFields?.first?.text else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
let request = Client.createList(title: title)
|
||||
self.mastodonController.run(request) { (response) in
|
||||
guard case let .success(list, _) = response else { fatalError() }
|
||||
|
||||
self.reloadLists()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.sidebarDelegate?.sidebar(self, didSelectItem: .list(list))
|
||||
}
|
||||
}
|
||||
}))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
// todo: deduplicate with ExploreViewController
|
||||
private func showAddSavedHashtag() {
|
||||
let navController = EnhancedNavigationViewController(rootViewController: AddSavedHashtagViewController(mastodonController: mastodonController))
|
||||
present(navController, animated: true)
|
||||
}
|
||||
|
||||
// todo: deduplicate with ExploreViewController
|
||||
private func showAddSavedInstance() {
|
||||
let findController = FindInstanceViewController(parentMastodonController: mastodonController)
|
||||
findController.instanceTimelineDelegate = self
|
||||
let navController = EnhancedNavigationViewController(rootViewController: findController)
|
||||
present(navController, animated: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MainSidebarViewController {
|
||||
enum Section: Int, Hashable, CaseIterable {
|
||||
case tabs
|
||||
case compose
|
||||
case lists
|
||||
case savedHashtags
|
||||
case savedInstances
|
||||
}
|
||||
enum Item: Hashable {
|
||||
case tab(MainTabBarViewController.Tab)
|
||||
case search, bookmarks
|
||||
case listsHeader, list(List), addList
|
||||
case savedHashtagsHeader, savedHashtag(Hashtag), addSavedHashtag
|
||||
case savedInstancesHeader, savedInstance(URL), addSavedInstance
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case let .tab(tab):
|
||||
return tab.title
|
||||
case .search:
|
||||
return "Search"
|
||||
case .bookmarks:
|
||||
return "Bookmarks"
|
||||
case .listsHeader:
|
||||
return "Lists"
|
||||
case let .list(list):
|
||||
return list.title
|
||||
case .addList:
|
||||
return "New List..."
|
||||
case .savedHashtagsHeader:
|
||||
return "Saved Hashtags"
|
||||
case let .savedHashtag(hashtag):
|
||||
return hashtag.name
|
||||
case .addSavedHashtag:
|
||||
return "Save Hashtag..."
|
||||
case .savedInstancesHeader:
|
||||
return "Saved Instances"
|
||||
case let .savedInstance(url):
|
||||
return url.host!
|
||||
case .addSavedInstance:
|
||||
return "Find An Instance..."
|
||||
}
|
||||
}
|
||||
|
||||
var imageName: String? {
|
||||
switch self {
|
||||
case let .tab(tab):
|
||||
return tab.imageName
|
||||
case .search:
|
||||
return "magnifyingglass"
|
||||
case .bookmarks:
|
||||
return "bookmark"
|
||||
case .list(_):
|
||||
return "list.bullet"
|
||||
case .savedHashtag(_):
|
||||
return "number"
|
||||
case .savedInstance(_):
|
||||
return "globe"
|
||||
case .listsHeader, .savedHashtagsHeader, .savedInstancesHeader:
|
||||
return nil
|
||||
case .addList, .addSavedHashtag, .addSavedInstance:
|
||||
return "plus"
|
||||
}
|
||||
}
|
||||
|
||||
var hasChildren: Bool {
|
||||
switch self {
|
||||
case .listsHeader, .savedHashtagsHeader, .savedInstancesHeader:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension MainTabBarViewController.Tab {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .timelines:
|
||||
return "Home"
|
||||
case .notifications:
|
||||
return "Notifications"
|
||||
case .compose:
|
||||
return "Compose"
|
||||
case .explore:
|
||||
return "Explore"
|
||||
case .myProfile:
|
||||
return "My Profile"
|
||||
}
|
||||
}
|
||||
|
||||
var imageName: String? {
|
||||
switch self {
|
||||
case .timelines:
|
||||
return "house"
|
||||
case .notifications:
|
||||
return "bell"
|
||||
case .compose:
|
||||
return "pencil"
|
||||
case .explore:
|
||||
return "magnifyingglass"
|
||||
case .myProfile:
|
||||
// todo: use user avatar image
|
||||
return "person"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MainSidebarViewController: UICollectionViewDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||
previouslySelectedItem = selectedItem
|
||||
return true
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
return
|
||||
}
|
||||
itemLastSelectedTimestamps[item] = Date()
|
||||
if [MainSidebarViewController.Item.tab(.compose), .addList, .addSavedHashtag, .addSavedInstance].contains(item) {
|
||||
if let previous = previouslySelectedItem, let indexPath = dataSource.indexPath(for: previous) {
|
||||
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically)
|
||||
}
|
||||
switch item {
|
||||
case .tab(.compose):
|
||||
sidebarDelegate?.sidebarRequestPresentCompose(self)
|
||||
case .addList:
|
||||
showAddList()
|
||||
case .addSavedHashtag:
|
||||
showAddSavedHashtag()
|
||||
case .addSavedInstance:
|
||||
showAddSavedInstance()
|
||||
default:
|
||||
fatalError("unreachable")
|
||||
}
|
||||
} else {
|
||||
sidebarDelegate?.sidebar(self, didSelectItem: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MainSidebarViewController: InstanceTimelineViewControllerDelegate {
|
||||
func didSaveInstance(url: URL) {
|
||||
dismiss(animated: true) {
|
||||
self.sidebarDelegate?.sidebar(self, didSelectItem: .savedInstance(url))
|
||||
}
|
||||
}
|
||||
|
||||
func didUnsaveInstance(url: URL) {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,326 @@
|
|||
//
|
||||
// MainSplitViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 6/23/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
#if SDK_IOS_14
|
||||
@available(iOS 14.0, *)
|
||||
class MainSplitViewController: UISplitViewController {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
private var sidebar: MainSidebarViewController!
|
||||
|
||||
// Keep track of navigation stacks per-item so that we can only ever use a single navigation controller
|
||||
private var navigationStacks: [MainSidebarViewController.Item: [UIViewController]] = [:]
|
||||
|
||||
private var tabBarViewController: MainTabBarViewController!
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(style: .doubleColumn)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
preferredDisplayMode = .oneBesideSecondary
|
||||
preferredSplitBehavior = .tile
|
||||
presentsWithGesture = false
|
||||
showsSecondaryOnlyButton = false
|
||||
delegate = self
|
||||
|
||||
sidebar = MainSidebarViewController(mastodonController: mastodonController)
|
||||
sidebar.sidebarDelegate = self
|
||||
setViewController(sidebar, for: .primary)
|
||||
|
||||
setViewController(EnhancedNavigationViewController(), for: .secondary)
|
||||
select(item: .tab(.timelines))
|
||||
|
||||
tabBarViewController = MainTabBarViewController(mastodonController: mastodonController)
|
||||
setViewController(tabBarViewController, for: .compact)
|
||||
}
|
||||
|
||||
func select(item: MainSidebarViewController.Item) {
|
||||
let nav = viewController(for: .secondary) as! UINavigationController
|
||||
nav.viewControllers = getOrCreateNavigationStack(item: item)
|
||||
}
|
||||
|
||||
func getOrCreateNavigationStack(item: MainSidebarViewController.Item) -> [UIViewController] {
|
||||
if let existing = navigationStacks[item], existing.count > 0 {
|
||||
return existing
|
||||
} else {
|
||||
let new = [item.createRootViewController(mastodonController)!]
|
||||
navigationStacks[item] = new
|
||||
return new
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||
/// Transfer the navigation stack for a sidebar item to a destination navgiation controller.
|
||||
/// - Parameter dropFirst: Remove the first view controller from the item's navigation stack before transferring.
|
||||
/// - Parameter append: Append the item's navigation stack to the destination nav controller's instead of replacing it.
|
||||
private func transferNavigationStack(from item: MainSidebarViewController.Item, to destination: UINavigationController, dropFirst: Bool = false, append: Bool = false) {
|
||||
var itemNavStack: [UIViewController]
|
||||
if item == sidebar.selectedItem {
|
||||
let detailNav = viewController(for: .secondary) as! UINavigationController
|
||||
itemNavStack = detailNav.viewControllers
|
||||
} else {
|
||||
itemNavStack = navigationStacks[item] ?? []
|
||||
navigationStacks.removeValue(forKey: item)
|
||||
}
|
||||
if itemNavStack.isEmpty {
|
||||
itemNavStack = [item.createRootViewController(mastodonController)!]
|
||||
}
|
||||
|
||||
if dropFirst {
|
||||
itemNavStack.remove(at: 0)
|
||||
}
|
||||
|
||||
if append {
|
||||
destination.viewControllers += itemNavStack
|
||||
} else {
|
||||
destination.viewControllers = itemNavStack
|
||||
}
|
||||
}
|
||||
|
||||
func splitViewControllerDidCollapse(_ svc: UISplitViewController) {
|
||||
// on iPhones, the sidebar VC is never loaded, but since this method is still called, we can't do anything
|
||||
guard sidebar.isViewLoaded else { return }
|
||||
|
||||
// Transfer the nav stacks for all the sidebar items that map 1 <-> 1 with tabs
|
||||
for tab in [MainTabBarViewController.Tab.timelines, .notifications, .myProfile] {
|
||||
let tabNav = tabBarViewController.viewController(for: tab) as! UINavigationController
|
||||
transferNavigationStack(from: .tab(tab), to: tabNav)
|
||||
}
|
||||
|
||||
// Since several sidebar items map to the single Explore tab, we only transfer the
|
||||
// navigation stack of the most-recently used one.
|
||||
let mostRecentExploreItem: (MainSidebarViewController.Item, Date)? =
|
||||
sidebar.exploreTabItems.compactMap {
|
||||
if let timestamp = sidebar.itemLastSelectedTimestamps[$0] {
|
||||
return ($0, timestamp)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}.min {
|
||||
$0.1 > $1.1
|
||||
}
|
||||
if let mostRecentExploreItem = mostRecentExploreItem?.0,
|
||||
mostRecentExploreItem != .search {
|
||||
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
||||
// Pop back to root, so we're appending to the Explore VC instead of some other VC
|
||||
exploreNav.popToRootViewController(animated: false)
|
||||
// Append so we don't replace the Explore VC
|
||||
transferNavigationStack(from: mostRecentExploreItem, to: exploreNav, append: true)
|
||||
}
|
||||
|
||||
// Switch the tab bar to focus the same item as the sidebar has selected
|
||||
switch sidebar.selectedItem! {
|
||||
case let .tab(tab):
|
||||
// sidebar items that map 1 <-> 1 can be transferred directly
|
||||
tabBarViewController.select(tab: tab)
|
||||
|
||||
case .search:
|
||||
// Search sidebar item maps to the Explore tab with the search controller/results visible
|
||||
// The nav stack can't be copied directly, since the split VC uses a different SearchViewController
|
||||
// so that explore items aren't shown multiple times.
|
||||
|
||||
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
||||
let explore: ExploreViewController
|
||||
if let existing = exploreNav.viewControllers.first as? ExploreViewController {
|
||||
explore = existing
|
||||
exploreNav.popToRootViewController(animated: false)
|
||||
} else {
|
||||
// If the Explore tab hasn't been loaded before, it's root view controller won't be loaded yet, so create and add it manually.
|
||||
explore = ExploreViewController(mastodonController: mastodonController)
|
||||
exploreNav.viewControllers = [explore]
|
||||
}
|
||||
// Make sure viewDidLoad is called so that the searchController/resultsController have been initialized
|
||||
explore.loadViewIfNeeded()
|
||||
|
||||
let nav = viewController(for: .secondary) as! UINavigationController
|
||||
let search = nav.viewControllers.first as! SearchViewController
|
||||
// Copy the search query from the search VC to the Explore VC's search controller.
|
||||
let query = search.searchController.searchBar.text ?? ""
|
||||
explore.searchController.searchBar.text = query
|
||||
// Instruct the explore controller to show its search controller immediately upon its first appearance.
|
||||
// explore.searchController.isActive can't be set directly, see FB7814561
|
||||
explore.searchControllerStatusOnAppearance = !query.isEmpty
|
||||
// Copy the results from the search VC's results controller to avoid the delay introduced by an extra network request
|
||||
explore.resultsController.loadResults(from: search.resultsController)
|
||||
|
||||
// Transfer the navigation stack, dropping the search VC, to keep anything the user has opened
|
||||
transferNavigationStack(from: .search, to: exploreNav, dropFirst: true, append: true)
|
||||
|
||||
tabBarViewController.select(tab: .explore)
|
||||
|
||||
case .bookmarks, .list(_), .savedHashtag(_), .savedInstance(_):
|
||||
tabBarViewController.select(tab: .explore)
|
||||
// Make sure the Explore VC doesn't show it's search bar when it appears, in case the user was previously
|
||||
// in compact mode and performing a search.
|
||||
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
||||
let explore = exploreNav.viewControllers.first as! ExploreViewController
|
||||
explore.searchControllerStatusOnAppearance = false
|
||||
|
||||
case .listsHeader, .addList, .savedHashtagsHeader, .addSavedHashtag, .savedInstancesHeader, .addSavedInstance:
|
||||
// These items are not selectable in the sidebar collection view, so this code is unreachable.
|
||||
fatalError("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
/// Transfer a navigation stack from a navigation controller belonging to the tab bar VC to a sidebar item.
|
||||
/// - Parameter skipFirst:The number of view controllers that should be skipped from the source navigation controller.
|
||||
/// - Parameter prepend: An optional view controller to prepend to the beginning of the navigation stack being moved.
|
||||
private func transferNavigationStack(from navController: UINavigationController, to item: MainSidebarViewController.Item, skipFirst: Int = 0, prepend: UIViewController? = nil) {
|
||||
let viewControllersToMove = navController.viewControllers.dropFirst(skipFirst)
|
||||
navController.viewControllers.removeLast(navController.viewControllers.count - skipFirst)
|
||||
|
||||
if let prepend = prepend {
|
||||
navigationStacks[item] = [prepend] + viewControllersToMove
|
||||
} else {
|
||||
navigationStacks[item] = Array(viewControllersToMove)
|
||||
}
|
||||
}
|
||||
|
||||
func splitViewControllerDidExpand(_ svc: UISplitViewController) {
|
||||
// For each sidebar item, transfer the existing navigation stasck from the tab bar controller to ourself.
|
||||
var exploreItem: MainSidebarViewController.Item?
|
||||
for tab in MainTabBarViewController.Tab.allCases {
|
||||
guard let tabNavController = tabBarViewController.viewController(for: tab) as? UINavigationController else { continue }
|
||||
let tabNavigationStack = tabNavController.viewControllers
|
||||
|
||||
switch tab {
|
||||
case .timelines, .notifications, .myProfile:
|
||||
// Items that map 1 <-> 1 to tabs can be transferred directly.
|
||||
let item = MainSidebarViewController.Item.tab(tab)
|
||||
transferNavigationStack(from: tabNavController, to: item)
|
||||
|
||||
case .explore:
|
||||
// The Explore tab is more complicated since it encapsulates a bunch of screens which have top-level sidebar items.
|
||||
|
||||
var toPrepend: UIViewController? = nil
|
||||
|
||||
// If the tab navigation stack has only one item or the search controller is active, it corresponds to the Search item
|
||||
// For other items, the 2nd VC in the nav stack determines which sidebar item they map to.
|
||||
// Search screen has special considerations, all others can be transferred directly.
|
||||
if tabNavigationStack.count == 1 || ((tabNavigationStack.first as? ExploreViewController)?.searchController.isActive ?? false) {
|
||||
exploreItem = .search
|
||||
let searchVC = SearchViewController(mastodonController: mastodonController)
|
||||
searchVC.loadViewIfNeeded()
|
||||
let explore = tabNavigationStack.first as! ExploreViewController
|
||||
if let exploreSearchControler = explore.searchController,
|
||||
let query = exploreSearchControler.searchBar.text {
|
||||
// Transfer query to search VC
|
||||
searchVC.searchController.searchBar.text = query
|
||||
// If there is a query, make the search VC activate itself upon appearing
|
||||
searchVC.searchControllerStatusOnAppearance = !query.isEmpty
|
||||
// Transfer the results from the explore VC, to avoid an extra network request
|
||||
searchVC.resultsController.loadResults(from: explore.resultsController)
|
||||
}
|
||||
// Insert the new search VC at the beginning of the new search nav stack
|
||||
toPrepend = searchVC
|
||||
} else if tabNavigationStack[1] is BookmarksTableViewController {
|
||||
exploreItem = .bookmarks
|
||||
} else if let listVC = tabNavigationStack[1] as? ListTimelineViewController {
|
||||
exploreItem = .list(listVC.list)
|
||||
} else if let hashtagVC = tabNavigationStack[1] as? HashtagTimelineViewController {
|
||||
exploreItem = .savedHashtag(hashtagVC.hashtag)
|
||||
} else if let instanceVC = tabNavigationStack[1] as? InstanceTimelineViewController {
|
||||
exploreItem = .savedInstance(instanceVC.instanceURL)
|
||||
}
|
||||
transferNavigationStack(from: tabNavController, to: exploreItem!, skipFirst: 1, prepend: toPrepend)
|
||||
|
||||
case .compose:
|
||||
// The compose tab can't be activated, this is unreachable.
|
||||
fatalError("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer the selected tab from the tab bar VC to the sidebar
|
||||
switch tabBarViewController.selectedTab {
|
||||
case .timelines, .notifications, .myProfile:
|
||||
// These tabs map 1 <-> 1 with sidebar items
|
||||
let item = MainSidebarViewController.Item.tab(tabBarViewController.selectedTab)
|
||||
sidebar.select(item: item, animated: false)
|
||||
select(item: item)
|
||||
|
||||
case .explore:
|
||||
// If the explore tab is active, the sidebar item is determined above when transferring the explore VC's nav stack
|
||||
sidebar.select(item: exploreItem!, animated: false)
|
||||
select(item: exploreItem!)
|
||||
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
||||
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController) {
|
||||
presentCompose()
|
||||
}
|
||||
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item) {
|
||||
let nav = viewController(for: .secondary) as! UINavigationController
|
||||
if let previous = sidebar.previouslySelectedItem {
|
||||
navigationStacks[previous] = nav.viewControllers
|
||||
}
|
||||
select(item: item)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
fileprivate extension MainSidebarViewController.Item {
|
||||
func createRootViewController(_ mastodonController: MastodonController) -> UIViewController? {
|
||||
switch self {
|
||||
case let .tab(tab):
|
||||
return tab.createViewController(mastodonController)
|
||||
case .search:
|
||||
return SearchViewController(mastodonController: mastodonController)
|
||||
case .bookmarks:
|
||||
return BookmarksTableViewController(mastodonController: mastodonController)
|
||||
case let .list(list):
|
||||
return ListTimelineViewController(for: list, mastodonController: mastodonController)
|
||||
case let .savedHashtag(hashtag):
|
||||
return HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController)
|
||||
case let .savedInstance(url):
|
||||
return InstanceTimelineViewController(for: url, parentMastodonController: mastodonController)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MainSplitViewController: TuskerRootViewController {
|
||||
func presentCompose() {
|
||||
let compose = ComposeViewController(mastodonController: mastodonController)
|
||||
let navigationController = EnhancedNavigationViewController(rootViewController: compose)
|
||||
navigationController.presentationController?.delegate = compose
|
||||
present(navigationController, animated: true)
|
||||
}
|
||||
|
||||
func select(tab: MainTabBarViewController.Tab) {
|
||||
if tab == .compose {
|
||||
presentCompose()
|
||||
} else {
|
||||
select(item: .tab(tab))
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -11,6 +11,12 @@ import UIKit
|
|||
class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
private var composePlaceholder: UIViewController!
|
||||
|
||||
var selectedTab: Tab {
|
||||
return Tab(rawValue: selectedIndex)!
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
|
@ -35,12 +41,16 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
|||
|
||||
self.delegate = self
|
||||
|
||||
composePlaceholder = UIViewController()
|
||||
composePlaceholder.title = "Compose"
|
||||
composePlaceholder.tabBarItem.image = UIImage(systemName: "pencil")
|
||||
|
||||
viewControllers = [
|
||||
embedInNavigationController(TimelinesPageViewController(mastodonController: mastodonController)),
|
||||
embedInNavigationController(NotificationsPageViewController(mastodonController: mastodonController)),
|
||||
ComposeViewController(mastodonController: mastodonController),
|
||||
embedInNavigationController(ExploreViewController(mastodonController: mastodonController)),
|
||||
embedInNavigationController(MyProfileTableViewController(mastodonController: mastodonController)),
|
||||
embedInNavigationController(Tab.timelines.createViewController(mastodonController)),
|
||||
embedInNavigationController(Tab.notifications.createViewController(mastodonController)),
|
||||
composePlaceholder,
|
||||
embedInNavigationController(Tab.explore.createViewController(mastodonController)),
|
||||
embedInNavigationController(Tab.myProfile.createViewController(mastodonController)),
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -53,35 +63,50 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
|||
}
|
||||
|
||||
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
|
||||
if viewController is ComposeViewController {
|
||||
if viewController == composePlaceholder {
|
||||
presentCompose()
|
||||
return false
|
||||
}
|
||||
if viewController == viewControllers![selectedIndex],
|
||||
let nav = viewController as? UINavigationController,
|
||||
nav.viewControllers.count == 1,
|
||||
let scrollableVC = nav.viewControllers.first as? TabBarScrollableViewController {
|
||||
scrollableVC.tabBarScrollToTop()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func presentCompose() {
|
||||
let compose = ComposeViewController(mastodonController: mastodonController)
|
||||
let navigationController = embedInNavigationController(compose)
|
||||
navigationController.presentationController?.delegate = compose
|
||||
present(navigationController, animated: true)
|
||||
func setViewController(_ viewController: UIViewController, forTab tab: Tab) {
|
||||
viewControllers![tab.rawValue] = viewController
|
||||
}
|
||||
|
||||
func viewController(for tab: Tab) -> UIViewController {
|
||||
return viewControllers![tab.rawValue]
|
||||
}
|
||||
}
|
||||
|
||||
extension MainTabBarViewController {
|
||||
enum Tab: Int {
|
||||
enum Tab: Int, Hashable, CaseIterable {
|
||||
case timelines
|
||||
case notifications
|
||||
case compose
|
||||
case explore
|
||||
case myProfile
|
||||
}
|
||||
|
||||
func select(tab: Tab) {
|
||||
if tab == .compose {
|
||||
presentCompose()
|
||||
} else {
|
||||
selectedIndex = tab.rawValue
|
||||
|
||||
func createViewController(_ mastodonController: MastodonController) -> UIViewController {
|
||||
switch self {
|
||||
case .timelines:
|
||||
return TimelinesPageViewController(mastodonController: mastodonController)
|
||||
case .notifications:
|
||||
return NotificationsPageViewController(mastodonController: mastodonController)
|
||||
case .compose:
|
||||
return ComposeViewController(mastodonController: mastodonController)
|
||||
case .explore:
|
||||
return ExploreViewController(mastodonController: mastodonController)
|
||||
case .myProfile:
|
||||
return MyProfileViewController(mastodonController: mastodonController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,3 +118,20 @@ extension MainTabBarViewController {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MainTabBarViewController: TuskerRootViewController {
|
||||
func presentCompose() {
|
||||
let compose = ComposeViewController(mastodonController: mastodonController)
|
||||
let navigationController = embedInNavigationController(compose)
|
||||
navigationController.presentationController?.delegate = compose
|
||||
present(navigationController, animated: true)
|
||||
}
|
||||
|
||||
func select(tab: Tab) {
|
||||
if tab == .compose {
|
||||
presentCompose()
|
||||
} else {
|
||||
selectedIndex = tab.rawValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// TuskerRootViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 6/24/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol TuskerRootViewController: UIViewController {
|
||||
func presentCompose()
|
||||
func select(tab: MainTabBarViewController.Tab)
|
||||
}
|
|
@ -118,7 +118,9 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
let request = Client.getInstance()
|
||||
client.run(request) { (response) in
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .selected))
|
||||
if snapshot.indexOfSection(.selected) != nil {
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .selected))
|
||||
}
|
||||
|
||||
if case let .success(instance, _) = response {
|
||||
if !snapshot.sectionIdentifiers.contains(.selected) {
|
||||
|
|
|
@ -48,19 +48,17 @@ extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate
|
|||
let mastodonController = MastodonController(instanceURL: instanceURL)
|
||||
mastodonController.registerApp { (clientID, clientSecret) in
|
||||
|
||||
let callbackURL = "tusker://oauth"
|
||||
|
||||
var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)!
|
||||
components.path = "/oauth/authorize"
|
||||
components.queryItems = [
|
||||
URLQueryItem(name: "client_id", value: clientID),
|
||||
URLQueryItem(name: "response_type", value: "code"),
|
||||
URLQueryItem(name: "scope", value: "read write follow"),
|
||||
URLQueryItem(name: "redirect_uri", value: callbackURL)
|
||||
URLQueryItem(name: "redirect_uri", value: "tusker://oauth")
|
||||
]
|
||||
let authorizeURL = components.url!
|
||||
|
||||
self.authenticationSession = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: callbackURL) { url, error in
|
||||
self.authenticationSession = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: "tusker") { url, error in
|
||||
guard error == nil,
|
||||
let url = url,
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
|
@ -84,6 +82,8 @@ extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate
|
|||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
// Prefer ephemeral sessions to make it easier to sign into multiple accounts on the same instance.
|
||||
self.authenticationSession!.prefersEphemeralWebBrowserSession = true
|
||||
self.authenticationSession!.presentationContextProvider = self
|
||||
self.authenticationSession!.start()
|
||||
}
|
||||
|
|
|
@ -30,9 +30,6 @@ struct AppearancePrefsView : View {
|
|||
Text("Light").tag(UIUserInterfaceStyle.light)
|
||||
Text("Dark").tag(UIUserInterfaceStyle.dark)
|
||||
}
|
||||
Toggle(isOn: $preferences.showRepliesInProfiles) {
|
||||
Text("Show Replies in Profiles")
|
||||
}
|
||||
Toggle(isOn: useCircularAvatars) {
|
||||
Text("Use Circular Avatars")
|
||||
}
|
||||
|
|
|
@ -77,10 +77,6 @@ struct PreferencesView: View {
|
|||
}
|
||||
.listStyle(GroupedListStyle())
|
||||
.navigationBarTitle(Text("Preferences"), displayMode: .inline)
|
||||
.onDisappear {
|
||||
// todo: this onDisappear callback is not called in beta 4, check again in beta 5
|
||||
NotificationCenter.default.post(name: .preferencesChanged, object: nil)
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// MyProfileTableViewController.swift
|
||||
// MyProfileViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 11/24/18.
|
||||
|
@ -7,9 +7,8 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
class MyProfileTableViewController: ProfileTableViewController {
|
||||
class MyProfileViewController: ProfileViewController {
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
super.init(accountID: nil, mastodonController: mastodonController)
|
||||
|
@ -17,9 +16,10 @@ class MyProfileTableViewController: ProfileTableViewController {
|
|||
title = "My Profile"
|
||||
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,28 +33,20 @@ class MyProfileTableViewController: ProfileTableViewController {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Preferences", style: .plain, target: self, action: #selector(preferencesPressed))
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,321 @@
|
|||
//
|
||||
// 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?
|
||||
|
||||
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) {
|
||||
updateUI(account: account)
|
||||
}
|
||||
}
|
||||
|
||||
func updateUI(account: AccountMO) {
|
||||
guard !loaded else { return }
|
||||
loaded = true
|
||||
|
||||
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..<statuses.count).map { IndexPath(row: $0, section: 0) }
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getStatuses { (response) in
|
||||
guard case let .success(statuses, pagination) = response else {
|
||||
// todo: error message
|
||||
return
|
||||
}
|
||||
if statuses.isEmpty { return }
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
self.timelineSegments.append(statuses.map { ($0.id, .unknown) })
|
||||
|
||||
self.older = pagination?.older
|
||||
self.newer = pagination?.newer
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertSections(IndexSet(integer: 1), with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getStatuses(for range: RequestRange = .default, completion: @escaping Client.Callback<[Status]>) {
|
||||
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..<newStatuses.count).map { IndexPath(row: $0, section: 1) }
|
||||
DispatchQueue.main.async {
|
||||
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||
}
|
||||
|
||||
self.refreshControl!.endRefreshing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if kind == .statuses {
|
||||
getPinnedStatuses { (response) in
|
||||
guard case let .success(newPinnedStatuses, _) = response else {
|
||||
// todo: error message
|
||||
return
|
||||
}
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newPinnedStatuses) {
|
||||
let oldPinnedStatuses = self.pinnedStatuses
|
||||
let pinnedStatuses = newPinnedStatuses.map { (status) -> (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..<newStatuses.count).map { IndexPath(row: start + $0, section: indexPath.section) }
|
||||
self.timelineSegments[indexPath.section - 1].append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> 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 where attachment.kind == .image {
|
||||
_ = 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 where attachment.kind == .image {
|
||||
ImageCache.avatars.cancelWithoutCallback(attachment.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,354 +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)
|
||||
|
||||
self.refreshControl = UIRefreshControl()
|
||||
refreshControl!.addTarget(self, action: #selector(refreshStatuses(_:)), for: .valueChanged)
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composePressed(_:)))
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
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() }
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
self.pinnedStatuses = statuses.map { ($0.id, .unknown) }
|
||||
let indexPaths = (0..<statuses.count).map { IndexPath(row: $0, section: 1) }
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getStatuses() { response in
|
||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
self.timelineSegments.append(statuses.map { ($0.id, .unknown) })
|
||||
|
||||
self.older = pagination?.older
|
||||
self.newer = pagination?.newer
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertSections(IndexSet(integer: 2), with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func updateUIForPreferences() {
|
||||
guard let accountID = accountID, let account = mastodonController.persistentContainer.account(for: accountID) else { return }
|
||||
navigationItem.title = account.displayNameWithoutCustomEmoji
|
||||
}
|
||||
|
||||
func getStatuses(for range: RequestRange = .default, onlyPinned: Bool = false, completion: @escaping Client.Callback<[Status]>) {
|
||||
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..<newStatuses.count).map { IndexPath(row: start + $0, section: indexPath.section) }
|
||||
self.timelineSegments[indexPath.section - 2].append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> 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..<newStatuses.count).map { IndexPath(row: $0, section: 2) }
|
||||
DispatchQueue.main.async {
|
||||
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||
}
|
||||
|
||||
self.refreshControl?.endRefreshing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getStatuses(onlyPinned: true) { (response) in
|
||||
guard case let .success(newPinnedStatuses, _) = response else { fatalError() }
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newPinnedStatuses) {
|
||||
let oldPinnedStatuses = self.pinnedStatuses
|
||||
var pinnedStatuses = [(id: String, state: StatusState)]()
|
||||
for status in newPinnedStatuses {
|
||||
let state: StatusState
|
||||
if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) {
|
||||
state = oldState
|
||||
} else {
|
||||
state = .unknown
|
||||
}
|
||||
pinnedStatuses.append((status.id, state))
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.pinnedStatuses = pinnedStatuses
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.reloadSections(IndexSet(integer: 1), with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func composePressed(_ sender: Any) {
|
||||
sendMessageMentioning()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ProfileTableViewController: StatusTableViewCellDelegate {
|
||||
var apiController: MastodonController { mastodonController }
|
||||
|
||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||
// causes the table view to recalculate the cell heights
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
|
||||
func showMoreOptions(cell: ProfileHeaderTableViewCell) {
|
||||
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 = cell.moreButtonVisualEffectView
|
||||
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 ProfileTableViewController: UITableViewDataSourcePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt 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.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
//
|
||||
// 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")
|
||||
}
|
||||
|
||||
deinit {
|
||||
mastodonController.persistentContainer.account(for: accountID)?.decrementReferenceCount()
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = .systemBackground
|
||||
|
||||
headerView = ProfileHeaderView.create()
|
||||
headerView.delegate = self
|
||||
|
||||
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() }
|
||||
|
||||
if mastodonController.persistentContainer.account(for: accountID) != nil {
|
||||
headerView.updateUI(for: accountID)
|
||||
} else {
|
||||
let req = Client.getAccount(id: accountID)
|
||||
mastodonController.run(req) { [weak self] (response) in
|
||||
guard let self = self else { return }
|
||||
guard case let .success(account, _) = response else { fatalError() }
|
||||
self.mastodonController.persistentContainer.addOrUpdate(account: account, incrementReferenceCount: true) { (account) in
|
||||
DispatchQueue.main.async {
|
||||
self.headerView.updateUI(for: self.accountID)
|
||||
self.pageControllers.forEach {
|
||||
$0.updateUI(account: account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ extension SearchResultsViewControllerDelegate {
|
|||
|
||||
class SearchResultsViewController: EnhancedTableViewController {
|
||||
|
||||
let mastodonController: MastodonController!
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
weak var exploreNavigationController: UINavigationController?
|
||||
weak var delegate: SearchResultsViewControllerDelegate?
|
||||
|
@ -109,6 +109,15 @@ class SearchResultsViewController: EnhancedTableViewController {
|
|||
return super.targetViewController(forAction: action, sender: sender)
|
||||
}
|
||||
|
||||
func loadResults(from source: SearchResultsViewController) {
|
||||
currentQuery = source.currentQuery
|
||||
if let sourceDataSource = source.dataSource {
|
||||
dataSource.apply(sourceDataSource.snapshot())
|
||||
}
|
||||
// todo: check if the search needs to be performed before searching
|
||||
// performSearch(query: currentQuery)
|
||||
}
|
||||
|
||||
func performSearch(query: String?) {
|
||||
guard let query = query, !query.isEmpty else {
|
||||
self.dataSource.apply(NSDiffableDataSourceSnapshot<Section, Item>())
|
||||
|
@ -134,14 +143,18 @@ class SearchResultsViewController: EnhancedTableViewController {
|
|||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
|
||||
self.mastodonController.persistentContainer.performBatchUpdates({ (context, addAccounts, addStatuses) in
|
||||
oldSnapshot.itemIdentifiers(inSection: .accounts).forEach { (item) in
|
||||
guard case let .account(id) = item else { return }
|
||||
self.mastodonController.persistentContainer.account(for: id, in: context)?.decrementReferenceCount()
|
||||
if oldSnapshot.indexOfSection(.accounts) != nil {
|
||||
oldSnapshot.itemIdentifiers(inSection: .accounts).forEach { (item) in
|
||||
guard case let .account(id) = item else { return }
|
||||
self.mastodonController.persistentContainer.account(for: id, in: context)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
|
||||
oldSnapshot.itemIdentifiers(inSection: .statuses).forEach { (item) in
|
||||
guard case let .status(id, _) = item else { return }
|
||||
self.mastodonController.persistentContainer.status(for: id, in: context)?.decrementReferenceCount()
|
||||
if oldSnapshot.indexOfSection(.statuses) != nil {
|
||||
oldSnapshot.itemIdentifiers(inSection: .statuses).forEach { (item) in
|
||||
guard case let .status(id, _) = item else { return }
|
||||
self.mastodonController.persistentContainer.status(for: id, in: context)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
|
||||
if self.onlySections.contains(.accounts) && !results.accounts.isEmpty {
|
||||
|
@ -208,6 +221,20 @@ extension SearchResultsViewController {
|
|||
case account(String)
|
||||
case hashtag(Hashtag)
|
||||
case status(String, StatusState)
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case let .account(id):
|
||||
hasher.combine("account")
|
||||
hasher.combine(id)
|
||||
case let .hashtag(hashtag):
|
||||
hasher.combine("hashtag")
|
||||
hasher.combine(hashtag.url)
|
||||
case let .status(id, _):
|
||||
hasher.combine("status")
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
//
|
||||
// SearchViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 6/24/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SearchViewController: UIViewController {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
var resultsController: SearchResultsViewController!
|
||||
var searchController: UISearchController!
|
||||
|
||||
var searchControllerStatusOnAppearance: Bool? = nil
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
title = NSLocalizedString("Search", comment: "search tab title")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
||||
resultsController.exploreNavigationController = self.navigationController
|
||||
searchController = UISearchController(searchResultsController: resultsController)
|
||||
searchController.searchResultsUpdater = resultsController
|
||||
searchController.searchBar.autocapitalizationType = .none
|
||||
searchController.searchBar.delegate = resultsController
|
||||
definesPresentationContext = true
|
||||
|
||||
navigationItem.searchController = searchController
|
||||
navigationItem.hidesSearchBarWhenScrolling = false
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
// this is a workaround for the issue that setting isActive on a search controller that is not visible
|
||||
// does not cause it to automatically become active once it becomes visible
|
||||
// see FB7814561
|
||||
if let active = searchControllerStatusOnAppearance {
|
||||
searchController.isActive = active
|
||||
searchControllerStatusOnAppearance = nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -213,15 +213,16 @@ class TimelineTableViewController: EnhancedTableViewController {
|
|||
let request = Client.getStatuses(timeline: timeline, range: newer)
|
||||
mastodonController.run(request) { response in
|
||||
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
||||
self.newer = pagination?.newer
|
||||
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
||||
|
||||
// If there is no new newer pagination, don't reset it, so that the user can continue refreshing for more recent statuses
|
||||
// Otherwise, when no new statuses were loaded, it would get reset and the the user would be unable to refresh
|
||||
if let newer = pagination?.newer {
|
||||
self.newer = newer
|
||||
}
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
||||
DispatchQueue.main.async {
|
||||
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
||||
let newIndexPaths = (0..<newStatuses.count).map {
|
||||
IndexPath(row: $0, section: 0)
|
||||
}
|
||||
|
@ -259,7 +260,7 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
|||
for indexPath in indexPaths {
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID(for: indexPath)) else { continue }
|
||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
||||
for attachment in status.attachments {
|
||||
for attachment in status.attachments where attachment.kind == .image {
|
||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
||||
}
|
||||
}
|
||||
|
@ -272,7 +273,7 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
|||
guard indexPath.section < timelineSegments.count, indexPath.row < timelineSegments[indexPath.section].count,
|
||||
let status = mastodonController.persistentContainer.status(for: statusID(for: indexPath)) else { continue }
|
||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
||||
for attachment in status.attachments {
|
||||
for attachment in status.attachments where attachment.kind == .image {
|
||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,3 +95,13 @@ extension EnhancedTableViewController {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension EnhancedTableViewController: TabBarScrollableViewController {
|
||||
func tabBarScrollToTop() {
|
||||
if scrollViewShouldScrollToTop(tableView) {
|
||||
let topOffset = CGPoint(x: 0, y: -tableView.adjustedContentInset.top)
|
||||
tableView.setContentOffset(topOffset, animated: true)
|
||||
scrollViewDidScrollToTop(tableView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,9 +10,9 @@ import UIKit
|
|||
import SafariServices
|
||||
import Pachyderm
|
||||
|
||||
protocol MenuPreviewProvider {
|
||||
protocol MenuPreviewProvider: class {
|
||||
|
||||
typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIAction])
|
||||
typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIMenuElement])
|
||||
|
||||
var navigationDelegate: TuskerNavigationDelegate? { get }
|
||||
|
||||
|
@ -28,57 +28,167 @@ extension MenuPreviewProvider {
|
|||
|
||||
private var mastodonController: MastodonController? { navigationDelegate?.apiController }
|
||||
|
||||
func actionsForProfile(accountID: String, sourceView: UIView?) -> [UIAction] {
|
||||
// Default no-op implementation
|
||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionsForProfile(accountID: String, sourceView: UIView?) -> [UIMenuElement] {
|
||||
guard let mastodonController = mastodonController,
|
||||
let account = mastodonController.persistentContainer.account(for: accountID) else { return [] }
|
||||
return [
|
||||
createAction(identifier: "sendmessage", title: "Send Message", systemImageName: "envelope", handler: { (_) in
|
||||
|
||||
var actionsSection: [UIMenuElement] = [
|
||||
createAction(identifier: "sendmessage", title: "Send Message", systemImageName: "envelope", handler: { [weak self] (_) in
|
||||
guard let self = self else { return }
|
||||
self.navigationDelegate?.compose(mentioning: account.acct)
|
||||
}),
|
||||
createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in
|
||||
self.navigationDelegate?.selected(url: account.url)
|
||||
}),
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in
|
||||
]
|
||||
|
||||
// todo: handle pre-iOS 14
|
||||
#if SDK_IOS_14
|
||||
if accountID != mastodonController.account.id,
|
||||
#available(iOS 14.0, *) {
|
||||
actionsSection.append(UIDeferredMenuElement({ (elementHandler) in
|
||||
guard let mastodonController = self.mastodonController else {
|
||||
elementHandler([])
|
||||
return
|
||||
}
|
||||
let request = Client.getRelationships(accounts: [account.id])
|
||||
mastodonController.run(request) { [weak self] (response) in
|
||||
if let self = self,
|
||||
case let .success(results, _) = response,
|
||||
let relationship = results.first {
|
||||
let following = relationship.following
|
||||
DispatchQueue.main.async {
|
||||
elementHandler([
|
||||
self.createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.minus", handler: { (_) in
|
||||
let request = (following ? Account.unfollow : Account.follow)(accountID)
|
||||
mastodonController.run(request) { (_) in
|
||||
}
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
#endif
|
||||
|
||||
let shareSection = [
|
||||
openInSafariAction(url: account.url),
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
||||
guard let self = self else { return }
|
||||
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
||||
})
|
||||
]
|
||||
|
||||
return [
|
||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection),
|
||||
]
|
||||
}
|
||||
|
||||
func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] {
|
||||
return [
|
||||
createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in
|
||||
self.navigationDelegate?.selected(url: url)
|
||||
}),
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in
|
||||
openInSafariAction(url: url),
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
||||
guard let self = self else { return }
|
||||
self.navigationDelegate?.showMoreOptions(forURL: url, sourceView: sourceView)
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
func actionsForHashtag(_ hashtag: Hashtag, sourceView: UIView?) -> [UIAction] {
|
||||
return actionsForURL(hashtag.url, sourceView: sourceView)
|
||||
func actionsForHashtag(_ hashtag: Hashtag, sourceView: UIView?) -> [UIMenuElement] {
|
||||
let account = mastodonController!.accountInfo!
|
||||
let saved = SavedDataManager.shared.isSaved(hashtag: hashtag, for: account)
|
||||
|
||||
let actionsSection = [
|
||||
createAction(identifier: "save", title: saved ? "Unsave Hashtag" : "Save Hashtag", systemImageName: "number", handler: { (_) in
|
||||
if saved {
|
||||
SavedDataManager.shared.remove(hashtag: hashtag, for: account)
|
||||
} else {
|
||||
SavedDataManager.shared.add(hashtag: hashtag, for: account)
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
let shareSection = actionsForURL(hashtag.url, sourceView: sourceView)
|
||||
|
||||
return [
|
||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection),
|
||||
]
|
||||
}
|
||||
|
||||
func actionsForStatus(statusID: String, sourceView: UIView?) -> [UIAction] {
|
||||
func actionsForStatus(statusID: String, sourceView: UIView?) -> [UIMenuElement] {
|
||||
guard let mastodonController = mastodonController,
|
||||
let status = mastodonController.persistentContainer.status(for: statusID) else { return [] }
|
||||
return [
|
||||
createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { (_) in
|
||||
let bookmarked = status.bookmarked ?? false
|
||||
let muted = status.muted
|
||||
|
||||
var actionsSection = [
|
||||
createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { [weak self] (_) in
|
||||
guard let self = self else { return }
|
||||
self.navigationDelegate?.reply(to: statusID)
|
||||
}),
|
||||
createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in
|
||||
self.navigationDelegate?.selected(url: status.url!)
|
||||
createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in
|
||||
guard let self = self else { return }
|
||||
let request = (bookmarked ? Status.unbookmark : Status.bookmark)(statusID)
|
||||
self.mastodonController?.run(request) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||
}
|
||||
}
|
||||
}),
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in
|
||||
self.navigationDelegate?.showMoreOptions(forStatus: statusID, sourceView: sourceView)
|
||||
createAction(identifier: "mute", title: muted ? "Unmute" : "Mute", systemImageName: muted ? "speaker" : "speaker.slash", handler: { [weak self] (_) in
|
||||
guard let self = self else { return }
|
||||
let request = (muted ? Status.unmuteConversation : Status.muteConversation)(statusID)
|
||||
self.mastodonController?.run(request) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
if mastodonController.account != nil && mastodonController.account.id == status.account.id {
|
||||
let pinned = status.pinned ?? false
|
||||
actionsSection.append(createAction(identifier: "", title: pinned ? "Unpin" : "Pin", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
|
||||
guard let self = self else { return }
|
||||
let request = (pinned ? Status.unpin : Status.pin)(statusID)
|
||||
self.mastodonController?.run(request, completion: { [weak self] (response) in
|
||||
guard let self = self else { return }
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
let shareSection = [
|
||||
openInSafariAction(url: status.url!),
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
||||
guard let self = self else { return }
|
||||
self.navigationDelegate?.showMoreOptions(forStatus: statusID, sourceView: sourceView)
|
||||
}),
|
||||
]
|
||||
|
||||
return [
|
||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection),
|
||||
]
|
||||
}
|
||||
|
||||
private func createAction(identifier: String, title: String, systemImageName: String, handler: @escaping UIActionHandler) -> UIAction {
|
||||
return UIAction(title: title, image: UIImage(systemName: systemImageName), identifier: UIAction.Identifier(identifier), discoverabilityTitle: nil, attributes: [], state: .off, handler: handler)
|
||||
}
|
||||
|
||||
private func openInSafariAction(url: URL) -> UIAction {
|
||||
return createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in
|
||||
self.navigationDelegate?.selected(url: url, allowUniversalLinks: false)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension LargeImageViewController: CustomPreviewPresenting {
|
||||
|
|
|
@ -63,3 +63,11 @@ class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDel
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension SegmentedPageViewController: TabBarScrollableViewController {
|
||||
func tabBarScrollToTop() {
|
||||
if let scrollableVC = pageControllers[currentIndex] as? TabBarScrollableViewController {
|
||||
scrollableVC.tabBarScrollToTop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// TabBarScrollableViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 7/3/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol TabBarScrollableViewController: UIViewController {
|
||||
func tabBarScrollToTop()
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.space.vaccor.Tusker</string>
|
||||
</array>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.photos-library</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -10,52 +10,11 @@ import UIKit
|
|||
import SafariServices
|
||||
import Pachyderm
|
||||
|
||||
protocol TuskerNavigationDelegate: class {
|
||||
|
||||
protocol TuskerNavigationDelegate: UIViewController {
|
||||
var apiController: MastodonController { get }
|
||||
|
||||
func show(_ vc: UIViewController)
|
||||
|
||||
func selected(account accountID: String)
|
||||
|
||||
func selected(mention: Mention)
|
||||
|
||||
func selected(tag: Hashtag)
|
||||
|
||||
func selected(url: URL)
|
||||
|
||||
func selected(status statusID: String)
|
||||
|
||||
func selected(status statusID: String, state: StatusState)
|
||||
|
||||
func compose()
|
||||
|
||||
func compose(mentioning: String?)
|
||||
|
||||
func reply(to statusID: String)
|
||||
|
||||
func reply(to statusID: String, mentioningAcct: String?)
|
||||
|
||||
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController
|
||||
|
||||
func showLoadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView)
|
||||
|
||||
func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController
|
||||
|
||||
func showGallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int)
|
||||
|
||||
func showMoreOptions(forStatus statusID: String, sourceView: UIView?)
|
||||
|
||||
func showMoreOptions(forAccount accountID: String, sourceView: UIView?)
|
||||
|
||||
func showMoreOptions(forURL url: URL, sourceView: UIView?)
|
||||
|
||||
func showFollowedByList(accountIDs: [String])
|
||||
|
||||
func statusActionAccountList(action: StatusActionAccountListTableViewController.ActionType, statusID: String, statusState state: StatusState, accountIDs: [String]?) -> StatusActionAccountListTableViewController
|
||||
}
|
||||
|
||||
extension TuskerNavigationDelegate where Self: UIViewController {
|
||||
extension TuskerNavigationDelegate {
|
||||
|
||||
func show(_ vc: UIViewController) {
|
||||
if vc is LargeImageViewController || vc is GalleryViewController || vc is SFSafariViewController {
|
||||
|
@ -67,23 +26,23 @@ 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) {
|
||||
show(HashtagTimelineViewController(for: tag, mastodonController: apiController), sender: self)
|
||||
}
|
||||
|
||||
func selected(url: URL) {
|
||||
func selected(url: URL, allowUniversalLinks: Bool = true) {
|
||||
func openSafari() {
|
||||
if Preferences.shared.useInAppSafari {
|
||||
let config = SFSafariViewController.Configuration()
|
||||
|
@ -94,7 +53,7 @@ extension TuskerNavigationDelegate where Self: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
if (Preferences.shared.openLinksInApps) {
|
||||
if allowUniversalLinks && Preferences.shared.openLinksInApps {
|
||||
UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { (success) in
|
||||
if (!success) {
|
||||
openSafari()
|
||||
|
@ -177,32 +136,41 @@ extension TuskerNavigationDelegate where Self: UIViewController {
|
|||
guard let status = apiController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
|
||||
guard let url = status.url else { fatalError("Missing url for status \(statusID)") }
|
||||
|
||||
var customActivites: [UIActivity] = [
|
||||
OpenInSafariActivity(),
|
||||
(status.bookmarked ?? false) ? UnbookmarkStatusActivity() : BookmarkStatusActivity(),
|
||||
status.muted ? UnmuteConversationActivity() : MuteConversationActivity(),
|
||||
]
|
||||
// on iOS 14+, all these custom actions are in the context menu and don't need to be in the share sheet
|
||||
if #available(iOS 14.0, *) {
|
||||
return UIActivityViewController(activityItems: [url, StatusActivityItemSource(status)], applicationActivities: nil)
|
||||
} else {
|
||||
var customActivites: [UIActivity] = [
|
||||
OpenInSafariActivity(),
|
||||
(status.bookmarked ?? false) ? UnbookmarkStatusActivity() : BookmarkStatusActivity(),
|
||||
status.muted ? UnmuteConversationActivity() : MuteConversationActivity(),
|
||||
]
|
||||
|
||||
if apiController.account != nil, status.account.id == apiController.account.id {
|
||||
let pinned = status.pinned ?? false
|
||||
customActivites.insert(pinned ? UnpinStatusActivity() : PinStatusActivity(), at: 1)
|
||||
if apiController.account != nil, status.account.id == apiController.account.id {
|
||||
let pinned = status.pinned ?? false
|
||||
customActivites.insert(pinned ? UnpinStatusActivity() : PinStatusActivity(), at: 1)
|
||||
}
|
||||
|
||||
let activityController = UIActivityViewController(activityItems: [url, StatusActivityItemSource(status)], applicationActivities: customActivites)
|
||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: url)
|
||||
return activityController
|
||||
}
|
||||
|
||||
let activityController = UIActivityViewController(activityItems: [url, StatusActivityItemSource(status)], applicationActivities: customActivites)
|
||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: url)
|
||||
return activityController
|
||||
}
|
||||
|
||||
private func moreOptions(forAccount accountID: String) -> UIActivityViewController {
|
||||
guard let account = apiController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
|
||||
|
||||
let customActivities: [UIActivity] = [
|
||||
OpenInSafariActivity(),
|
||||
]
|
||||
|
||||
let activityController = UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: customActivities)
|
||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: account.url)
|
||||
return activityController
|
||||
if #available(iOS 14.0, *) {
|
||||
return UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: nil)
|
||||
} else {
|
||||
let customActivities: [UIActivity] = [
|
||||
OpenInSafariActivity(),
|
||||
]
|
||||
|
||||
let activityController = UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: customActivities)
|
||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: account.url)
|
||||
return activityController
|
||||
}
|
||||
}
|
||||
|
||||
func showMoreOptions(forStatus statusID: String, sourceView: UIView?) {
|
||||
|
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
|
|
|
@ -177,6 +177,8 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
|||
}
|
||||
|
||||
override func display(_ layer: CALayer) {
|
||||
super.display(layer)
|
||||
|
||||
updateImageIfNeeded()
|
||||
}
|
||||
|
||||
|
|
|
@ -22,11 +22,19 @@ class ContentTextView: LinkTextView {
|
|||
var defaultFont: UIFont = .systemFont(ofSize: 17)
|
||||
var defaultColor: UIColor = .label
|
||||
|
||||
// The link range currently being previewed
|
||||
private var currentPreviewedLinkRange: NSRange?
|
||||
// The preview created in the previewForHighlighting method, so that we can use the same one in previewForDismissing.
|
||||
private weak var currentTargetedPreview: UITargetedPreview?
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
delegate = self
|
||||
|
||||
// Disable layer masking, otherwise the context menu opening animation
|
||||
// may be clipped if it's at an edge of the text view
|
||||
layer.masksToBounds = false
|
||||
addInteraction(UIContextMenuInteraction(delegate: self))
|
||||
|
||||
textDragInteraction?.isEnabled = false
|
||||
|
@ -214,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 {
|
||||
|
@ -253,12 +261,15 @@ extension ContentTextView: MenuPreviewProvider {
|
|||
extension ContentTextView: UIContextMenuInteractionDelegate {
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||
if let (link, range) = getLinkAtPoint(location) {
|
||||
// Store the previewed link range for use in the previewForHighlighting method
|
||||
currentPreviewedLinkRange = range
|
||||
|
||||
let preview: UIContextMenuContentPreviewProvider = {
|
||||
self.getViewController(forLink: link, inRange: range)
|
||||
}
|
||||
let actions: UIContextMenuActionProvider = { (_) in
|
||||
let text = (self.text as NSString).substring(with: range)
|
||||
let actions: [UIAction]
|
||||
let actions: [UIMenuElement]
|
||||
if let mention = self.getMention(for: link, text: text) {
|
||||
actions = self.actionsForProfile(accountID: mention.id, sourceView: self)
|
||||
} else if let tag = self.getHashtag(for: link, text: text) {
|
||||
|
@ -268,11 +279,73 @@ extension ContentTextView: UIContextMenuInteractionDelegate {
|
|||
}
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
|
||||
}
|
||||
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: preview, actionProvider: actions)
|
||||
} else {
|
||||
currentPreviewedLinkRange = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
// If there isn't a link range, use the default system-generated preview.
|
||||
guard let range = currentPreviewedLinkRange else {
|
||||
return nil
|
||||
}
|
||||
currentPreviewedLinkRange = nil
|
||||
|
||||
// Determine the line rects that the link takes up in the coordinate space of this view.
|
||||
var rects = [CGRect]()
|
||||
layoutManager.enumerateEnclosingRects(forGlyphRange: range, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0), in: textContainer) { (rect, stop) in
|
||||
rects.append(rect)
|
||||
}
|
||||
|
||||
// Try to create a snapshot view of this view to disply as the preview.
|
||||
// If a snapshot view cannot be created, we bail and use the system-provided preview.
|
||||
guard let snapshot = self.snapshotView(afterScreenUpdates: false) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mask the snapshot layer to only show the text of the link, and nothing else.
|
||||
// By default, the system-applied mask is too wide and other content may seep in.
|
||||
let path = UIBezierPath(wrappingAround: rects)
|
||||
let maskLayer = CAShapeLayer()
|
||||
maskLayer.path = path.cgPath
|
||||
snapshot.layer.mask = maskLayer
|
||||
|
||||
// The preview parameters describe how the preview view is shown inside the preview.
|
||||
let parameters = UIPreviewParameters(textLineRects: rects as [NSValue])
|
||||
|
||||
// Calculate the smallest rect enclosing all of the text line rects, in the coordinate space of this view.
|
||||
var minX: CGFloat = .greatestFiniteMagnitude, maxX: CGFloat = -.greatestFiniteMagnitude, minY: CGFloat = .greatestFiniteMagnitude, maxY: CGFloat = -.greatestFiniteMagnitude
|
||||
for rect in rects {
|
||||
minX = min(rect.minX, minX)
|
||||
maxX = max(rect.maxX, maxX)
|
||||
minY = min(rect.minY, minY)
|
||||
maxY = max(rect.maxY, maxY)
|
||||
}
|
||||
// The center point of the the minimum enclosing rect in our coordinate space is the point where the
|
||||
// center of the preview should be, since that's also in this view's coordinate space.
|
||||
let rectsCenter = CGPoint(x: (minX + maxX) / 2, y: (minY + maxY) / 2)
|
||||
|
||||
// The preview target describes how the preview is positioned.
|
||||
let target = UIPreviewTarget(container: self, center: rectsCenter)
|
||||
|
||||
// Create a dummy containerview for the snapshot view, since using a view with a CALayer mask and UIPreviewParameters(textLineRects:)
|
||||
// causes the mask to be ignored. See FB7832297
|
||||
let snapshotContainer = UIView(frame: snapshot.bounds)
|
||||
snapshotContainer.addSubview(snapshot)
|
||||
|
||||
let preview = UITargetedPreview(view: snapshotContainer, parameters: parameters, target: target)
|
||||
currentTargetedPreview = preview
|
||||
return preview
|
||||
}
|
||||
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForDismissingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
// Use the same preview for dismissing as was used for highlighting, so that the link animates back to the original position.
|
||||
return currentTargetedPreview
|
||||
}
|
||||
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||
if let viewController = animator.previewViewController {
|
||||
animator.preferredCommitStyle = .pop
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 []
|
||||
})
|
||||
|
|
|
@ -1,182 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ProfileHeaderTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="296"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Fw7-OL-iy5">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="150"/>
|
||||
<accessibility key="accessibilityConfiguration" label="User Profile Banner">
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="150" id="y43-4B-slK"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="KyB-ey-l11">
|
||||
<rect key="frame" x="16" y="90" width="120" height="120"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="tH8-sR-DHC">
|
||||
<rect key="frame" x="2" y="2" width="116" height="116"/>
|
||||
<accessibility key="accessibilityConfiguration" label="User Avatar">
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
<gestureRecognizers/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="116" id="k9v-I0-Aoz"/>
|
||||
<constraint firstAttribute="height" constant="116" id="sz5-86-5Iq"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="tH8-sR-DHC" firstAttribute="centerX" secondItem="KyB-ey-l11" secondAttribute="centerX" id="KT6-FP-LsA"/>
|
||||
<constraint firstAttribute="height" constant="120" id="LVm-OC-cGm"/>
|
||||
<constraint firstAttribute="width" constant="120" id="Obt-ZN-POD"/>
|
||||
<constraint firstItem="tH8-sR-DHC" firstAttribute="centerY" secondItem="KyB-ey-l11" secondAttribute="centerY" id="nYu-RE-MfA"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="10" translatesAutoresizingMaskIntoConstraints="NO" id="LjK-72-Bez" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="144" y="158" width="215" height="24"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="MIj-OR-NOR">
|
||||
<rect key="frame" x="144" y="190" width="215" height="18"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="DfO-uD-UNI">
|
||||
<rect key="frame" x="16" y="218" width="343" height="12"/>
|
||||
<subviews>
|
||||
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Follows you" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="a32-1a-xXZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="75.5" height="0.0"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bnc-3t-t7t" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="337" height="12"/>
|
||||
<string key="text">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.</string>
|
||||
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="sHU-GU-klv">
|
||||
<rect key="frame" x="16" y="238" width="343" height="50"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="pV2-Mz-54W">
|
||||
<rect key="frame" x="0.0" y="0.0" width="167.5" height="50"/>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="oza-9d-8v4">
|
||||
<rect key="frame" x="175.5" y="0.0" width="167.5" height="50"/>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="oza-9d-8v4" firstAttribute="width" relation="lessThanOrEqual" secondItem="pV2-Mz-54W" secondAttribute="width" multiplier="2" id="LHm-6k-LyV"/>
|
||||
<constraint firstItem="oza-9d-8v4" firstAttribute="width" relation="greaterThanOrEqual" secondItem="pV2-Mz-54W" secondAttribute="width" multiplier="0.5" id="Zbr-l3-Lff"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mQY-XN-PfZ">
|
||||
<rect key="frame" x="335" y="110" width="32" height="32"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="0Ol-1d-la6">
|
||||
<rect key="frame" x="0.0" y="0.0" width="32" height="32"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="t0d-eE-mbc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="32" height="32"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="TgJ-FF-QyB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="32" height="32"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ellipsis" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="cLs-dC-SWU">
|
||||
<rect key="frame" x="2" y="12.5" width="28" height="7"/>
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="24"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<gestureRecognizers/>
|
||||
<constraints>
|
||||
<constraint firstItem="cLs-dC-SWU" firstAttribute="leading" secondItem="TgJ-FF-QyB" secondAttribute="leading" constant="2" id="7nV-7d-GAY"/>
|
||||
<constraint firstAttribute="bottom" secondItem="cLs-dC-SWU" secondAttribute="bottom" constant="2" id="8sP-mZ-ZSQ"/>
|
||||
<constraint firstAttribute="trailing" secondItem="cLs-dC-SWU" secondAttribute="trailing" constant="2" id="iBQ-oA-yOm"/>
|
||||
<constraint firstItem="cLs-dC-SWU" firstAttribute="top" secondItem="TgJ-FF-QyB" secondAttribute="top" constant="2" id="jSB-2f-sZF"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<vibrancyEffect style="label">
|
||||
<blurEffect style="prominent"/>
|
||||
</vibrancyEffect>
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="t0d-eE-mbc" firstAttribute="leading" secondItem="0Ol-1d-la6" secondAttribute="leading" id="6Py-U4-Jlo"/>
|
||||
<constraint firstAttribute="bottom" secondItem="t0d-eE-mbc" secondAttribute="bottom" id="OT5-Yh-eiG"/>
|
||||
<constraint firstAttribute="trailing" secondItem="t0d-eE-mbc" secondAttribute="trailing" id="a8T-dS-dc8"/>
|
||||
<constraint firstItem="t0d-eE-mbc" firstAttribute="top" secondItem="0Ol-1d-la6" secondAttribute="top" id="xKq-qM-vmk"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="32" id="Zye-sh-FIH"/>
|
||||
<constraint firstAttribute="width" constant="32" id="hpF-s0-dbt"/>
|
||||
</constraints>
|
||||
<blurEffect style="prominent"/>
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Fw7-OL-iy5" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="0fI-0y-cXG"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="DfO-uD-UNI" secondAttribute="trailing" constant="16" id="ASp-mh-SFv"/>
|
||||
<constraint firstItem="KyB-ey-l11" firstAttribute="centerY" secondItem="Fw7-OL-iy5" secondAttribute="bottom" id="AXr-6X-FJ8"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="MIj-OR-NOR" secondAttribute="trailing" constant="16" id="AwT-Vi-CLa"/>
|
||||
<constraint firstItem="LjK-72-Bez" firstAttribute="leading" secondItem="KyB-ey-l11" secondAttribute="trailing" constant="8" id="CIO-tn-hJC"/>
|
||||
<constraint firstItem="LjK-72-Bez" firstAttribute="top" secondItem="Fw7-OL-iy5" secondAttribute="bottom" constant="8" id="Kvl-sz-Lv3"/>
|
||||
<constraint firstItem="DfO-uD-UNI" firstAttribute="top" secondItem="KyB-ey-l11" secondAttribute="bottom" constant="8" id="Ljh-8x-yI3"/>
|
||||
<constraint firstItem="Fw7-OL-iy5" firstAttribute="trailing" secondItem="vUN-kp-3ea" secondAttribute="trailing" id="LqH-lE-AIe"/>
|
||||
<constraint firstItem="KyB-ey-l11" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="NN7-5B-k1Q"/>
|
||||
<constraint firstItem="MIj-OR-NOR" firstAttribute="bottom" secondItem="tH8-sR-DHC" secondAttribute="bottom" id="PhQ-El-olR"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="mQY-XN-PfZ" secondAttribute="trailing" constant="8" id="TZn-8m-0Wq"/>
|
||||
<constraint firstItem="sHU-GU-klv" firstAttribute="top" secondItem="DfO-uD-UNI" secondAttribute="bottom" constant="8" id="Vza-1s-qbG"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="sHU-GU-klv" secondAttribute="trailing" id="XJa-zP-Ma2"/>
|
||||
<constraint firstItem="sHU-GU-klv" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leadingMargin" id="cSX-WD-2aJ"/>
|
||||
<constraint firstItem="Fw7-OL-iy5" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="d1j-6d-hBb"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="LjK-72-Bez" secondAttribute="trailing" constant="16" id="hn9-c3-iNH"/>
|
||||
<constraint firstItem="MIj-OR-NOR" firstAttribute="leading" secondItem="KyB-ey-l11" secondAttribute="trailing" constant="8" id="iG7-yZ-9u3"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="sHU-GU-klv" secondAttribute="bottom" constant="8" id="iRf-l0-ZZX"/>
|
||||
<constraint firstItem="MIj-OR-NOR" firstAttribute="top" relation="greaterThanOrEqual" secondItem="LjK-72-Bez" secondAttribute="bottom" id="nMM-6t-bjX"/>
|
||||
<constraint firstItem="DfO-uD-UNI" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="pqd-E3-Aw4"/>
|
||||
<constraint firstItem="LjK-72-Bez" firstAttribute="top" secondItem="mQY-XN-PfZ" secondAttribute="bottom" constant="16" id="rTO-fy-u0V"/>
|
||||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<connections>
|
||||
<outlet property="avatarContainerView" destination="KyB-ey-l11" id="45s-jV-l8L"/>
|
||||
<outlet property="avatarImageView" destination="tH8-sR-DHC" id="6ll-yL-g1o"/>
|
||||
<outlet property="displayNameLabel" destination="LjK-72-Bez" id="nIU-ey-H1C"/>
|
||||
<outlet property="fieldNamesStackView" destination="pV2-Mz-54W" id="xfG-60-K0s"/>
|
||||
<outlet property="fieldValuesStackView" destination="oza-9d-8v4" id="UIS-KM-5fR"/>
|
||||
<outlet property="fieldsStackView" destination="sHU-GU-klv" id="Gli-Gf-Ubh"/>
|
||||
<outlet property="followsYouLabel" destination="a32-1a-xXZ" id="phY-0L-NnN"/>
|
||||
<outlet property="headerImageView" destination="Fw7-OL-iy5" id="6sv-E5-D73"/>
|
||||
<outlet property="moreButtonVisualEffectView" destination="mQY-XN-PfZ" id="t7l-wg-nj0"/>
|
||||
<outlet property="noteTextView" destination="bnc-3t-t7t" id="dV2-7U-gSd"/>
|
||||
<outlet property="usernameLabel" destination="MIj-OR-NOR" id="e1I-N7-rKx"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="40.799999999999997" y="110.64467766116942"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
||||
</resources>
|
||||
</document>
|
|
@ -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 moreButtonVisualEffectView: UIVisualEffectView!
|
||||
@IBOutlet weak var pagesSegmentedControl: UISegmentedControl!
|
||||
|
||||
var accountID: String!
|
||||
|
||||
|
@ -37,33 +45,46 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
|||
var headerRequest: ImageCache.Request?
|
||||
|
||||
private var accountUpdater: Cancellable?
|
||||
|
||||
|
||||
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
|
||||
moreButtonVisualEffectView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(morePressed)))
|
||||
|
||||
let maskLayer = CAShapeLayer()
|
||||
maskLayer.frame = moreButtonVisualEffectView.bounds
|
||||
maskLayer.path = CGPath(ellipseIn: moreButtonVisualEffectView.bounds, transform: nil)
|
||||
moreButtonVisualEffectView.layer.mask = maskLayer
|
||||
|
||||
if #available(iOS 13.4, *) {
|
||||
moreButtonVisualEffectView.addInteraction(UIPointerInteraction(delegate: self))
|
||||
}
|
||||
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))
|
||||
}
|
||||
#if SDK_IOS_14
|
||||
if #available(iOS 14.0, *) {
|
||||
moreButton.showsMenuAsPrimaryAction = true
|
||||
moreButton.isContextMenuInteractionEnabled = true
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
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 mastodonController = mastodonController else { return }
|
||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
|
||||
fatalError("Missing cached account \(accountID)")
|
||||
}
|
||||
|
||||
updateUIForPreferences()
|
||||
|
||||
|
@ -84,6 +105,10 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
if #available(iOS 14.0, *) {
|
||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForProfile(accountID: accountID, sourceView: moreButton))
|
||||
}
|
||||
|
||||
noteTextView.navigationDelegate = delegate
|
||||
noteTextView.setTextFromHtml(account.note)
|
||||
noteTextView.setEmojis(account.emojis)
|
||||
|
@ -91,17 +116,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 {
|
||||
|
@ -113,7 +141,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)
|
||||
|
@ -127,49 +155,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
|
||||
|
||||
@objc func morePressed() {
|
||||
delegate?.showMoreOptions(cell: self)
|
||||
@IBAction func morePressed(_ sender: Any) {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ProfileHeaderTableViewCell: UIPointerInteractionDelegate {
|
||||
@available(iOS 13.4, *)
|
||||
@available(iOS 13.4, *)
|
||||
extension ProfileHeaderView: UIPointerInteractionDelegate {
|
||||
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
|
||||
let preview = UITargetedPreview(view: moreButtonVisualEffectView)
|
||||
let rect = CGRect(x: moreButtonVisualEffectView.frame.minX - 4, y: moreButtonVisualEffectView.frame.minY - 4, width: moreButtonVisualEffectView.frame.width + 8, height: moreButtonVisualEffectView.frame.height + 8)
|
||||
return UIPointerStyle(effect: .highlight(preview), shape: .roundedRect(rect, radius: 4))
|
||||
let preview = UITargetedPreview(view: moreButton)
|
||||
return UIPointerStyle(effect: .lift(preview), shape: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileHeaderView: MenuPreviewProvider {
|
||||
var navigationDelegate: TuskerNavigationDelegate? { delegate }
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="iN0-l3-epB" customClass="ProfileHeaderView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="dgG-dR-lSv">
|
||||
<rect key="frame" x="0.0" y="44" width="414" height="150"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="150" id="aCE-CA-XWm"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wT9-2J-uSY">
|
||||
<rect key="frame" x="16" y="134" width="120" height="120"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="TkY-oK-if4">
|
||||
<rect key="frame" x="2" y="2" width="116" height="116"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="116" id="eDg-Vc-o8R"/>
|
||||
<constraint firstAttribute="width" constant="116" id="r63-z8-eBH"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="120" id="6Dt-Ml-clv"/>
|
||||
<constraint firstItem="TkY-oK-if4" firstAttribute="centerY" secondItem="wT9-2J-uSY" secondAttribute="centerY" id="NYH-4p-7zT"/>
|
||||
<constraint firstAttribute="width" constant="120" id="PYe-ej-mRj"/>
|
||||
<constraint firstItem="TkY-oK-if4" firstAttribute="centerX" secondItem="wT9-2J-uSY" secondAttribute="centerX" id="ozz-sa-gSc"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="10" translatesAutoresizingMaskIntoConstraints="NO" id="vcl-Gl-kXl" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="144" y="202" width="254" height="24"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bRJ-Xf-kc9" customClass="VisualEffectImageButton" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="374" y="154" width="32" height="32"/>
|
||||
<viewLayoutGuide key="safeArea" id="kQa-ou-pQz"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" button="YES"/>
|
||||
</accessibility>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="32" id="1lX-9R-x90"/>
|
||||
<constraint firstAttribute="height" constant="32" id="Vp8-v8-Lr1"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="image" keyPath="image" value="ellipsis" catalog="system"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="morePressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="Td6-rw-Xvr"/>
|
||||
</connections>
|
||||
</view>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="u4P-3i-gEq">
|
||||
<rect key="frame" x="16" y="262" width="398" height="600"/>
|
||||
<subviews>
|
||||
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Follows you" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UF8-nI-KVj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="75.5" height="0.0"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" horizontalHuggingPriority="249" verticalHuggingPriority="251" horizontalCompressionResistancePriority="749" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1O8-2P-Gbf" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="382" height="186.5"/>
|
||||
<string key="text">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.</string>
|
||||
<color key="textColor" systemColor="labelColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" distribution="fillProportionally" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="sp4-Zb-00B">
|
||||
<rect key="frame" x="0.0" y="194.5" width="382" height="358"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="aqG-FA-so5">
|
||||
<rect key="frame" x="0.0" y="0.0" width="186.5" height="358"/>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="EfH-Dj-Jmn">
|
||||
<rect key="frame" x="194.5" y="0.0" width="187.5" height="358"/>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="EfH-Dj-Jmn" firstAttribute="width" relation="greaterThanOrEqual" secondItem="aqG-FA-so5" secondAttribute="width" multiplier="0.5" id="2hZ-pF-C9b"/>
|
||||
<constraint firstItem="EfH-Dj-Jmn" firstAttribute="width" relation="lessThanOrEqual" secondItem="aqG-FA-so5" secondAttribute="width" multiplier="2" id="Lir-Ff-z0m"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="n1M-vM-Cj0">
|
||||
<rect key="frame" x="0.0" y="560.5" width="382" height="32"/>
|
||||
<segments>
|
||||
<segment title="Posts"/>
|
||||
<segment title="Posts and Replies"/>
|
||||
<segment title="Media"/>
|
||||
</segments>
|
||||
<connections>
|
||||
<action selector="postsSegmentedControlChanged:" destination="iN0-l3-epB" eventType="valueChanged" id="D6y-ZM-DwU"/>
|
||||
</connections>
|
||||
</segmentedControl>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5ja-fK-Fqz">
|
||||
<rect key="frame" x="0.0" y="599.5" width="398" height="0.5"/>
|
||||
<color key="backgroundColor" systemColor="separatorColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="0.5" id="VwS-gV-q8M"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="n1M-vM-Cj0" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" constant="-16" id="9Ds-zl-acc"/>
|
||||
<constraint firstItem="sp4-Zb-00B" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" constant="-16" id="Qum-qT-goH"/>
|
||||
<constraint firstItem="5ja-fK-Fqz" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" id="azv-le-93y"/>
|
||||
<constraint firstItem="1O8-2P-Gbf" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" constant="-16" id="hnA-3G-B9B"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1C3-Pd-QiL">
|
||||
<rect key="frame" x="144" y="234" width="254" height="18"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="1C3-Pd-QiL" firstAttribute="leading" secondItem="wT9-2J-uSY" secondAttribute="trailing" constant="8" id="23a-no-Gjj"/>
|
||||
<constraint firstItem="dgG-dR-lSv" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="6Q0-q5-Ju6"/>
|
||||
<constraint firstItem="wT9-2J-uSY" firstAttribute="centerY" secondItem="dgG-dR-lSv" secondAttribute="bottom" id="7gb-T3-Xe7"/>
|
||||
<constraint firstItem="vcl-Gl-kXl" firstAttribute="top" secondItem="dgG-dR-lSv" secondAttribute="bottom" constant="8" symbolic="YES" id="7ss-Mf-YYH"/>
|
||||
<constraint firstItem="vcl-Gl-kXl" firstAttribute="leading" secondItem="wT9-2J-uSY" secondAttribute="trailing" constant="8" id="8ho-WU-MxW"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="u4P-3i-gEq" secondAttribute="bottom" id="9zc-N2-mfI"/>
|
||||
<constraint firstItem="bRJ-Xf-kc9" firstAttribute="bottom" secondItem="dgG-dR-lSv" secondAttribute="bottom" constant="-8" id="AXS-bG-20Q"/>
|
||||
<constraint firstItem="1C3-Pd-QiL" firstAttribute="bottom" secondItem="TkY-oK-if4" secondAttribute="bottom" id="OpB-YM-gyu"/>
|
||||
<constraint firstItem="dgG-dR-lSv" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="VD1-yc-KSa"/>
|
||||
<constraint firstItem="wT9-2J-uSY" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="16" id="WNS-AR-ff2"/>
|
||||
<constraint firstItem="bRJ-Xf-kc9" firstAttribute="trailing" secondItem="vUN-kp-3ea" secondAttribute="trailing" constant="-8" id="ZB4-ys-9zP"/>
|
||||
<constraint firstItem="1C3-Pd-QiL" firstAttribute="top" relation="greaterThanOrEqual" secondItem="vcl-Gl-kXl" secondAttribute="bottom" id="d0z-X6-Sig"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="vcl-Gl-kXl" secondAttribute="trailing" constant="16" id="e38-Od-kPg"/>
|
||||
<constraint firstItem="u4P-3i-gEq" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="16" id="hgl-UR-o3W"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="dgG-dR-lSv" secondAttribute="trailing" id="j0d-hY-815"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="1C3-Pd-QiL" secondAttribute="trailing" constant="16" id="pcH-vi-2zH"/>
|
||||
<constraint firstAttribute="trailing" secondItem="u4P-3i-gEq" secondAttribute="trailing" id="ph6-NT-A02"/>
|
||||
<constraint firstItem="u4P-3i-gEq" firstAttribute="top" secondItem="wT9-2J-uSY" secondAttribute="bottom" constant="8" id="tKQ-6d-Z55"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="avatarContainerView" destination="wT9-2J-uSY" id="yEm-h7-tfq"/>
|
||||
<outlet property="avatarImageView" destination="TkY-oK-if4" id="bSJ-7z-j4w"/>
|
||||
<outlet property="displayNameLabel" destination="vcl-Gl-kXl" id="64n-a9-my0"/>
|
||||
<outlet property="fieldNamesStackView" destination="aqG-FA-so5" id="prA-n7-blZ"/>
|
||||
<outlet property="fieldValuesStackView" destination="EfH-Dj-Jmn" id="LMk-Hn-EkY"/>
|
||||
<outlet property="fieldsStackView" destination="sp4-Zb-00B" id="eyx-GF-2Wf"/>
|
||||
<outlet property="followsYouLabel" destination="UF8-nI-KVj" id="dTe-DQ-eJV"/>
|
||||
<outlet property="headerImageView" destination="dgG-dR-lSv" id="HXT-v4-2iX"/>
|
||||
<outlet property="moreButton" destination="bRJ-Xf-kc9" id="zIN-pz-L7y"/>
|
||||
<outlet property="noteTextView" destination="1O8-2P-Gbf" id="yss-zZ-uQ5"/>
|
||||
<outlet property="pagesSegmentedControl" destination="n1M-vM-Cj0" id="TCU-ku-YZN"/>
|
||||
<outlet property="usernameLabel" destination="1C3-Pd-QiL" id="57b-LQ-3pM"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="-590" y="117"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
||||
<systemColor name="labelColor">
|
||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
<systemColor name="secondaryLabelColor">
|
||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="separatorColor">
|
||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
|
@ -87,6 +87,12 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!]
|
||||
attachmentsView.isAccessibilityElement = true
|
||||
|
||||
#if SDK_IOS_14
|
||||
if #available(iOS 14.0, *) {
|
||||
moreButton.showsMenuAsPrimaryAction = true
|
||||
}
|
||||
#endif
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||
}
|
||||
|
||||
|
@ -96,7 +102,8 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
.filter { [unowned self] in $0 == self.statusID }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [unowned self] in
|
||||
if let status = self.mastodonController.persistentContainer.status(for: $0) {
|
||||
if let mastodonController = mastodonController,
|
||||
let status = mastodonController.persistentContainer.status(for: $0) {
|
||||
self.updateStatusState(status: status)
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +114,8 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
.filter { [unowned self] in $0 == self.accountID }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [unowned self] in
|
||||
if let account = self.mastodonController.persistentContainer.account(for: $0) {
|
||||
if let mastodonController = mastodonController,
|
||||
let account = mastodonController.persistentContainer.account(for: $0) {
|
||||
self.updateUI(account: account)
|
||||
}
|
||||
}
|
||||
|
@ -143,7 +151,11 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
}
|
||||
|
||||
let reblogDisabled: Bool
|
||||
switch mastodonController.instance.instanceType {
|
||||
switch mastodonController.instance?.instanceType {
|
||||
case nil:
|
||||
// todo: this handle a race condition in instance public timelines
|
||||
// a somewhat better solution would be waiting to load the timeline until after the instance is loaded
|
||||
reblogDisabled = true
|
||||
case .mastodon:
|
||||
reblogDisabled = status.visibility == .private || status.visibility == .direct
|
||||
case .pleroma:
|
||||
|
@ -190,6 +202,13 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
} else {
|
||||
reblogButton.accessibilityLabel = NSLocalizedString("Reblog", comment: "reblog button accessibility label")
|
||||
}
|
||||
|
||||
#if SDK_IOS_14
|
||||
if #available(iOS 14.0, *) {
|
||||
// keep menu in sync with changed states e.g. bookmarked, muted
|
||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForStatus(statusID: statusID, sourceView: moreButton))
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func updateUI(account: AccountMO) {
|
||||
|
@ -365,7 +384,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) }
|
||||
)
|
||||
}
|
||||
|
|
|
@ -257,11 +257,25 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
|||
}
|
||||
reply.image = UIImage(systemName: "arrowshape.turn.up.left.fill")
|
||||
reply.backgroundColor = tintColor
|
||||
let more = UIContextualAction(style: .normal, title: "More") { (action, view, completion) in
|
||||
|
||||
let moreTitle: String
|
||||
let moreImage: UIImage
|
||||
// on iOS 14+, more actions are in the context menu so display this as 'Share'
|
||||
if #available(iOS 14.0, *) {
|
||||
moreTitle = "Share"
|
||||
// Bold to more closely match the other action symbols
|
||||
let config = UIImage.SymbolConfiguration(weight: .bold)
|
||||
moreImage = UIImage(systemName: "square.and.arrow.up")!.applyingSymbolConfiguration(config)!
|
||||
} else {
|
||||
moreTitle = "More"
|
||||
moreImage = UIImage(systemName: "ellipsis.circle.fill")!
|
||||
}
|
||||
|
||||
let more = UIContextualAction(style: .normal, title: moreTitle) { (action, view, completion) in
|
||||
completion(true)
|
||||
self.delegate?.showMoreOptions(forStatus: self.statusID, sourceView: self)
|
||||
}
|
||||
more.image = UIImage(systemName: "ellipsis.circle.fill")
|
||||
more.image = moreImage
|
||||
more.backgroundColor = .lightGray
|
||||
return UISwipeActionsConfiguration(actions: [reply, more])
|
||||
}
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
//
|
||||
// VisualEffectImageButton.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 6/26/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class VisualEffectImageButton: UIControl {
|
||||
|
||||
@IBInspectable
|
||||
var image: UIImage! {
|
||||
didSet {
|
||||
imageView?.image = image
|
||||
}
|
||||
}
|
||||
|
||||
var menu: UIMenu?
|
||||
|
||||
private(set) var imageView: UIImageView!
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
let blur = UIBlurEffect(style: .prominent)
|
||||
let blurView = UIVisualEffectView(effect: blur)
|
||||
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||
let vibrancy = UIVibrancyEffect(blurEffect: blur, style: .label)
|
||||
let vibrancyView = UIVisualEffectView(effect: vibrancy)
|
||||
vibrancyView.translatesAutoresizingMaskIntoConstraints = false
|
||||
imageView = UIImageView(image: self.image)
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
vibrancyView.contentView.addSubview(imageView)
|
||||
blurView.contentView.addSubview(vibrancyView)
|
||||
addSubview(blurView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
blurView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||
blurView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||
blurView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
blurView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||
vibrancyView.leadingAnchor.constraint(equalTo: blurView.leadingAnchor),
|
||||
vibrancyView.trailingAnchor.constraint(equalTo: blurView.trailingAnchor),
|
||||
vibrancyView.topAnchor.constraint(equalTo: blurView.topAnchor),
|
||||
vibrancyView.bottomAnchor.constraint(equalTo: blurView.bottomAnchor),
|
||||
imageView.leadingAnchor.constraint(equalTo: vibrancyView.leadingAnchor, constant: 2),
|
||||
imageView.trailingAnchor.constraint(equalTo: vibrancyView.trailingAnchor, constant: -2),
|
||||
imageView.topAnchor.constraint(equalTo: vibrancyView.topAnchor, constant: 2),
|
||||
imageView.bottomAnchor.constraint(equalTo: vibrancyView.bottomAnchor, constant: -2),
|
||||
])
|
||||
|
||||
#if SDK_IOS_14
|
||||
addInteraction(UIContextMenuInteraction(delegate: self))
|
||||
#endif
|
||||
|
||||
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
|
||||
}
|
||||
|
||||
@objc private func onTap() {
|
||||
sendActions(for: .touchUpInside)
|
||||
}
|
||||
|
||||
#if SDK_IOS_14
|
||||
override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||
guard let menu = menu else { return nil }
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in
|
||||
return menu
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue