Compare commits

...

55 Commits

Author SHA1 Message Date
Shadowfacts 273b74ddfb
Bump build number and update changelog 2020-08-15 22:10:44 -04:00
Shadowfacts ae055f1ffd
Remove debug code 2020-08-15 18:00:47 -04:00
Shadowfacts eef9b96a1a
Fix crash when showing profile for uncached account 2020-08-15 18:00:18 -04:00
Shadowfacts 29aed65b99
Fix crash if profile header view outlives VC 2020-08-15 17:59:14 -04:00
Shadowfacts 090746f292
Disallow opening universal links from Open in Safari context menu action 2020-08-15 17:48:58 -04:00
Shadowfacts af300a3559
Remove unused TuskerNavigationDelegate customization points 2020-08-15 17:47:33 -04:00
Shadowfacts 79eb23ef5d
Remove unused preference 2020-08-15 17:43:31 -04:00
Shadowfacts 60565f9625
Fix crash if status table view cell outlives VC 2020-08-15 17:37:56 -04:00
Shadowfacts 70bedf17a8
Set app category 2020-08-15 17:36:23 -04:00
Shadowfacts 392e51eb3e
Remove unnecessary prefernces change notification 2020-08-15 17:31:24 -04:00
Shadowfacts 86d5a73c85
Change menu item order
Open in Safari should be the closest to the user's finger when tapping a
menu button
2020-08-15 17:20:09 -04:00
Shadowfacts eaefa366b7
Fix displaying images on iOS 14 2020-08-15 17:03:02 -04:00
Shadowfacts 79b23127e9
Fix crash on refreshing 2020-08-15 14:15:38 -04:00
Shadowfacts f9b85c87b4
Fix crash on launch due to unloaded sidebar VC 2020-08-15 13:55:47 -04:00
Shadowfacts 260bedcf10
Fix retain cycle between status cells and menu actions 2020-07-07 23:23:39 -04:00
Shadowfacts fe09c5e522
Switch asset picker to use diffable data sources 2020-07-06 18:16:18 -04:00
Shadowfacts 985d30a401 Add background to image descriptions so they're visible against light backgrounds
Closes #102
2020-07-06 17:48:19 -04:00
Shadowfacts 794594805c Prevent needlessly prefetching non-image attachments 2020-07-06 00:00:55 -04:00
Shadowfacts 1c708732f2 Exclude iOS 14-specific code from compilation on Xcode 11 to allow building for TestFlight 2020-07-06 00:00:51 -04:00
Shadowfacts db30471011 Fix not being able to refresh timelines 2020-07-05 16:30:16 -04:00
Shadowfacts 2825345c7e Add switching between Posts, Posts and Replies, and Media pages of user profiles
Closes #103
2020-07-05 16:17:56 -04:00
Shadowfacts f3d01c47c3 Merge branch 'develop-xcode-12' into ios-14 2020-07-04 11:21:00 -04:00
Shadowfacts caab5e357a Fix crash loading audio attachment uploaded on Mastodon
Closes #104
2020-07-03 22:13:49 -04:00
Shadowfacts 2916d7a72d Add tapping the active tab bar item to scroll to top
Closes #106
2020-07-03 19:36:52 -04:00
Shadowfacts d190636fbd Fix Preferences button not appearing (again) 2020-07-03 19:36:08 -04:00
Shadowfacts 4e4701ead5 Use SwiftSoup from SPM instead of Git submodule 2020-07-03 19:09:58 -04:00
Shadowfacts b07efc150c Use App Group for user defaults 2020-07-03 18:54:21 -04:00
Shadowfacts 19fa12391d Fix Preferences button not appearing 2020-07-03 18:53:19 -04:00
Shadowfacts c55ea2e005 More link context menu preview tweaks 2020-07-03 18:52:35 -04:00
Shadowfacts 47dc00ab8f Fix sometimes broken masking of text view link preview animations 2020-07-03 18:52:23 -04:00
Shadowfacts fdcdbced38 Limit context menu previews in ContentTextView to link's text line rects 2020-07-03 18:50:37 -04:00
Shadowfacts e70a84274e Fix showing instance public timeline 2020-07-03 18:50:22 -04:00
Shadowfacts 641ab765a7 Fix crash when displaying search results 2020-07-03 18:50:05 -04:00
Shadowfacts 986fc5b833 Prevent crash when displaying accounts with no pinned statuses 2020-07-03 18:49:55 -04:00
Shadowfacts cf5b97d9c8 Fix crash showing custom instance on iOS 14 2020-07-03 18:49:28 -04:00
Shadowfacts 7f0fd119c5 Use App Group for user defaults 2020-07-03 18:45:37 -04:00
Shadowfacts b2c7735256 Fix Preferences button not appearing 2020-07-03 18:44:38 -04:00
Shadowfacts 1d815d6cd6 More link context menu preview tweaks 2020-07-03 17:01:52 -04:00
Shadowfacts f86d3a0ed1 Fix sometimes broken masking of text view link preview animations 2020-07-01 00:01:36 -04:00
Shadowfacts 864fd77ecc Sync active tab and navigation stack between split view/tab bar controllers 2020-06-29 22:21:03 -04:00
Shadowfacts 78da04162f Fix missing file from project.pbxproj 2020-06-29 21:47:11 -04:00
Shadowfacts 40a742139b Fix menu state getting out of sync with bookmarked/muted state 2020-06-27 13:13:04 -04:00
Shadowfacts 8bbc572fa7 Replace more with share button for timeline status swipe actions 2020-06-27 10:47:31 -04:00
Shadowfacts 2a8e970738 Use context menus as primary actions for 'More Actions' buttons on >= iOS 14 2020-06-27 00:22:14 -04:00
Shadowfacts 3abb5972b9 Limit context menu previews in ContentTextView to link's text line rects 2020-06-25 10:42:46 -04:00
Shadowfacts 0c06d91f6b Fix showing instance public timeline 2020-06-24 16:41:01 -04:00
Shadowfacts 6cf6db6a8d Add sidebar on iPadOS 14 2020-06-24 16:40:45 -04:00
Shadowfacts fb11e36467 Fix crash when displaying search results 2020-06-24 15:42:56 -04:00
Shadowfacts 0fa87e9177 Prevent crash when displaying accounts with no pinned statuses 2020-06-23 22:21:50 -04:00
Shadowfacts 5cb84e271a Prefer ephemeral sessions in ASWebAuthneticationSession 2020-06-23 21:35:14 -04:00
Shadowfacts 50f1a9a7de Change ComposeDrawingViewController to use drawingPolicy on iOS 14 2020-06-23 19:33:14 -04:00
Shadowfacts 154fc7cd02 Fix ASWebAuthenticationSession usage in Catalyst 2020-06-23 19:32:30 -04:00
Shadowfacts 01d765fa45 Enable Catalyst 2020-06-23 19:32:04 -04:00
Shadowfacts 04aad1252a Use SwiftSoup from SPM instead of Git submodule 2020-06-23 19:31:32 -04:00
Shadowfacts 43779e42df Fix crash showing custom instance on iOS 14 2020-06-23 19:27:34 -04:00
55 changed files with 2453 additions and 876 deletions

3
.gitmodules vendored
View File

@ -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

View File

@ -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

@ -1 +1 @@
Subproject commit ed572f53ce58b8e23499abeb3a926033cbe480f7
Subproject commit 9b1a6461aa3b5f66cb0ed3a50c5523db0b4fb007

View File

@ -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 {

View File

@ -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

View File

@ -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" */;

View File

@ -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>

View File

@ -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"
}
}
]
},

View File

@ -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))
}
}

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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

View File

@ -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() {

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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

View File

@ -46,6 +46,8 @@ class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentV
}
override public func display(_ layer: CALayer) {
super.display(layer)
updateImageIfNeeded()
}
}

View File

@ -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
}

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -12,6 +12,12 @@ 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 {
return .portrait
@ -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
}
}
}

View File

@ -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)
}

View File

@ -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) {

View File

@ -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()
}

View File

@ -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")
}

View File

@ -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)
}
// }
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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()
}
}

View File

@ -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> {

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -63,3 +63,11 @@ class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDel
}
}
extension SegmentedPageViewController: TabBarScrollableViewController {
func tabBarScrollToTop() {
if let scrollableVC = pageControllers[currentIndex] as? TabBarScrollableViewController {
scrollableVC.tabBarScrollToTop()
}
}
}

View File

@ -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()
}

View File

@ -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>

View File

@ -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(),
]
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
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?) {

View File

@ -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) }
)
}

View File

@ -177,6 +177,8 @@ class AttachmentView: UIImageView, GIFAnimatable {
}
override func display(_ layer: CALayer) {
super.display(layer)
updateImageIfNeeded()
}

View File

@ -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

View File

@ -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)
}

View File

@ -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 []
})

View File

@ -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>

View File

@ -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!
@ -38,32 +46,45 @@ class ProfileHeaderTableViewCell: UITableViewCell {
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,11 +116,14 @@ 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
}
}
}
@ -130,46 +158,60 @@ class ProfileHeaderTableViewCell: UITableViewCell {
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 }
}

View File

@ -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>

View File

@ -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) }
)
}

View File

@ -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])
}

View File

@ -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
}

View File

@ -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)
}