Compare commits
39 Commits
3fdeb51353
...
1e7bfac13c
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 1e7bfac13c | |
Shadowfacts | 6e92633793 | |
Shadowfacts | e4ff632dcb | |
Shadowfacts | b0ebef2cfd | |
Shadowfacts | bbb8707cb7 | |
Shadowfacts | 6a927e4092 | |
Shadowfacts | 13cdb5d8c7 | |
Shadowfacts | 9f0883d0cb | |
Shadowfacts | eba2e17479 | |
Shadowfacts | 5d1c95621b | |
Shadowfacts | 02ba45fa34 | |
Shadowfacts | 9d5c004ec4 | |
Shadowfacts | 37e90229c2 | |
Shadowfacts | 73aceda97f | |
Shadowfacts | 669d55500a | |
Shadowfacts | f44d127110 | |
Shadowfacts | bcc023a127 | |
Shadowfacts | 122cce3bc7 | |
Shadowfacts | 949162bcab | |
Shadowfacts | 4ed862120c | |
Shadowfacts | f9411d706b | |
Shadowfacts | 8f61b0b9a6 | |
Shadowfacts | cdffda5593 | |
Shadowfacts | d1c45a87e6 | |
Shadowfacts | 2761c05a01 | |
Shadowfacts | e7800249af | |
Shadowfacts | 2e88b266d9 | |
Shadowfacts | 0b008489f7 | |
Shadowfacts | de67327f6d | |
Shadowfacts | 04a6fe807e | |
Shadowfacts | 6dee0957ea | |
Shadowfacts | c12d2db258 | |
Shadowfacts | 27b39b79e6 | |
Shadowfacts | d7aa3f1617 | |
Shadowfacts | 69c2faf0e1 | |
Shadowfacts | 678ed4959b | |
Shadowfacts | 0bdcda1b23 | |
Shadowfacts | 74a30d27e8 | |
Shadowfacts | f0e2bb8db6 |
|
@ -1,6 +1,3 @@
|
|||
[submodule "Cache"]
|
||||
path = Cache
|
||||
url = git@github.com:hyperoslo/Cache.git
|
||||
[submodule "Gifu"]
|
||||
path = Gifu
|
||||
url = git://github.com/kaishin/Gifu.git
|
||||
|
|
24
CHANGELOG.md
24
CHANGELOG.md
|
@ -1,5 +1,29 @@
|
|||
# Changelog
|
||||
|
||||
## 2021.1 (17)
|
||||
The main improvement this build is a complete overhaul of the Conversation screen, along with fixes for a few different crashes.
|
||||
|
||||
Features/Improvements:
|
||||
- Group replies by thread on Conversation screen
|
||||
- Adding Trending Hashtags and Profile Directory to Explore screen (Mastodon only)
|
||||
|
||||
Bugfixes:
|
||||
- Fix crash when editing List members
|
||||
- Fix crash when re-opening Preferences after switching accounts
|
||||
- Fix crash when refreshing profiles in some circumstances
|
||||
|
||||
## 2021.1 (16)
|
||||
This build fixes a number of crashes and significantly improves performance on older devices.
|
||||
|
||||
Features/Improvements:
|
||||
- Significantly improve performance when scrolling through timelines
|
||||
|
||||
Bugfixes:
|
||||
- Fix crash when timeline or profile went offscreen
|
||||
- Fix crash when refreshing profile too quickly
|
||||
- iPadOS: Fix secondary windows not respecting theme preference
|
||||
- Fix refreshes breaking after a refresh which did not return new results
|
||||
|
||||
## 2020.1 (15)
|
||||
There are a whole bunch of new features in this release, in addition to a slew of bugfixes. The big ticket items are multi-window support on iPadOS and fast account switching on iPhone (fast account switching will be supported on iPads in a future build).
|
||||
|
||||
|
|
1
Cache
1
Cache
|
@ -1 +0,0 @@
|
|||
Subproject commit 8c42c575cf28b2ff0e780c9728721e9a8891c92e
|
|
@ -323,7 +323,7 @@ public class Client {
|
|||
return request
|
||||
}
|
||||
|
||||
// MARK: - Trends
|
||||
// MARK: - Instance
|
||||
public static func getTrends(limit: Int? = nil) -> Request<[Hashtag]> {
|
||||
let parameters: [Parameter]
|
||||
if let limit = limit {
|
||||
|
@ -334,6 +334,20 @@ public class Client {
|
|||
return Request<[Hashtag]>(method: .get, path: "/api/v1/trends", queryParameters: parameters)
|
||||
}
|
||||
|
||||
public static func getFeaturedProfiles(local: Bool, order: DirectoryOrder, offset: Int? = nil, limit: Int? = nil) -> Request<[Account]> {
|
||||
var parameters = [
|
||||
"order" => order.rawValue,
|
||||
"local" => local,
|
||||
]
|
||||
if let offset = offset {
|
||||
parameters.append("offset" => offset)
|
||||
}
|
||||
if let limit = limit {
|
||||
parameters.append("limit" => limit)
|
||||
}
|
||||
return Request<[Account]>(method: .get, path: "/api/v1/directory", queryParameters: parameters)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Client {
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// DirectoryOrder.swift
|
||||
// Pachyderm
|
||||
//
|
||||
// Created by Shadowfacts on 2/6/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum DirectoryOrder: String {
|
||||
case active
|
||||
case new
|
||||
}
|
|
@ -14,12 +14,15 @@
|
|||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; };
|
||||
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; };
|
||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4222B301470021BD04 /* AppearancePrefsView.swift */; };
|
||||
0461A3902163CBAE00C0A807 /* Cache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0461A38F2163CBAE00C0A807 /* Cache.framework */; };
|
||||
0461A3912163CBAE00C0A807 /* Cache.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0461A38F2163CBAE00C0A807 /* Cache.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D14BAE22B34A2800642648 /* GalleryViewController.swift */; };
|
||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
|
||||
D600613E25D07E170067FAD6 /* ProfileDirectoryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D600613D25D07E170067FAD6 /* ProfileDirectoryFilterView.swift */; };
|
||||
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; };
|
||||
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */; };
|
||||
D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */; };
|
||||
D6093FB725BE0CF3004811E6 /* HashtagHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* HashtagHistoryView.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 */; };
|
||||
|
@ -95,7 +98,6 @@
|
|||
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943623A552C200D38C68 /* BookmarkStatusActivity.swift */; };
|
||||
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943823A553B600D38C68 /* UnbookmarkStatusActivity.swift */; };
|
||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */; };
|
||||
D627943E23A564D400D38C68 /* ExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943D23A564D400D38C68 /* ExploreViewController.swift */; };
|
||||
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */; };
|
||||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944923A6AD6100D38C68 /* BookmarksTableViewController.swift */; };
|
||||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */; };
|
||||
|
@ -112,6 +114,7 @@
|
|||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; };
|
||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
|
||||
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
|
||||
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; };
|
||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
|
||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
|
||||
D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.swift */; };
|
||||
|
@ -194,6 +197,11 @@
|
|||
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.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 */; };
|
||||
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; };
|
||||
D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */; };
|
||||
D693A72C25CF8D15003A14E2 /* DirectoryOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72B25CF8D15003A14E2 /* DirectoryOrder.swift */; };
|
||||
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; };
|
||||
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */; };
|
||||
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 */; };
|
||||
|
@ -225,6 +233,10 @@
|
|||
D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; };
|
||||
D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; };
|
||||
D6A5BB3123BBAD87003BF21D /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */; };
|
||||
D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */; };
|
||||
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C10E25B62D2400298D0F /* DiskCache.swift */; };
|
||||
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C11425B62E9700298D0F /* CacheExpiry.swift */; };
|
||||
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */; };
|
||||
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */; };
|
||||
D6ACE1AC240C3BAD004EA8E2 /* Ambassador.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F613023AE99E000F3CFD3 /* Ambassador.framework */; };
|
||||
D6ACE1AD240C3BAD004EA8E2 /* Embassy.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F612D23AE990C00F3CFD3 /* Embassy.framework */; };
|
||||
|
@ -265,16 +277,17 @@
|
|||
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
||||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */; };
|
||||
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */; };
|
||||
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C82B5425C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift */; };
|
||||
D6C82B5725C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6C82B5525C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib */; };
|
||||
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D862139E62700CB5196 /* LargeImageViewController.swift */; };
|
||||
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D882139E6EC00CB5196 /* AttachmentView.swift */; };
|
||||
D6C99FC724FACFAB005C74D3 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C99FC624FACFAB005C74D3 /* ActivityIndicatorView.swift */; };
|
||||
D6C99FCB24FADC91005C74D3 /* MainComposeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */; };
|
||||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */; };
|
||||
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */; };
|
||||
D6D3F4C424FDB6B700EC4A6A /* View+ConditionalModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */; };
|
||||
D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D3FDDF24F41B8400FF50A5 /* ComposeContainerView.swift */; };
|
||||
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D3FDE124F46A8D00FF50A5 /* ComposeUIState.swift */; };
|
||||
D6D4CC91250D2C3100FCCF8D /* UIAccessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4CC90250D2C3100FCCF8D /* UIAccessibility.swift */; };
|
||||
D6D4CC94250DB86A00FCCF8D /* ComposeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4CC93250DB86A00FCCF8D /* ComposeTests.swift */; };
|
||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */; };
|
||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD6212518A200E1C4BB /* Assets.xcassets */; };
|
||||
|
@ -288,11 +301,11 @@
|
|||
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; };
|
||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
|
||||
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */; };
|
||||
D6E426812532814100C02E1C /* MaybeLazyStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426802532814100C02E1C /* MaybeLazyStack.swift */; };
|
||||
D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */; };
|
||||
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; };
|
||||
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */; };
|
||||
D6E426B9253382B300C02E1C /* SearchResultType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B8253382B300C02E1C /* SearchResultType.swift */; };
|
||||
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D6E57FA525C26FAB00341037 /* Localizable.stringsdict */; };
|
||||
D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; };
|
||||
D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; };
|
||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */; };
|
||||
|
@ -301,7 +314,6 @@
|
|||
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EE63FA2551F7F60065485C /* StatusCollapseButton.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 */; };
|
||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
|
||||
|
@ -355,7 +367,6 @@
|
|||
files = (
|
||||
D61099C12144B0CC00432DC2 /* Pachyderm.framework in Embed Frameworks */,
|
||||
D6BC874621961F73006163F1 /* Gifu.framework in Embed Frameworks */,
|
||||
0461A3912163CBAE00C0A807 /* Cache.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -375,6 +386,11 @@
|
|||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
|
||||
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
|
||||
D600613D25D07E170067FAD6 /* ProfileDirectoryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDirectoryFilterView.swift; sourceTree = "<group>"; };
|
||||
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSearchResultsViewController.swift; sourceTree = "<group>"; };
|
||||
D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrendingHashtagTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D6093FB625BE0CF3004811E6 /* HashtagHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagHistoryView.swift; sourceTree = "<group>"; };
|
||||
D60A4FFB238B726A008AC647 /* StatusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusState.swift; sourceTree = "<group>"; };
|
||||
D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D60E2F232442372B005F8713 /* StatusMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMO.swift; sourceTree = "<group>"; };
|
||||
|
@ -451,7 +467,6 @@
|
|||
D627943623A552C200D38C68 /* BookmarkStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkStatusActivity.swift; sourceTree = "<group>"; };
|
||||
D627943823A553B600D38C68 /* UnbookmarkStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnbookmarkStatusActivity.swift; sourceTree = "<group>"; };
|
||||
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigableTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D627943D23A564D400D38C68 /* ExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreViewController.swift; sourceTree = "<group>"; };
|
||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BasicTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D627944923A6AD6100D38C68 /* BookmarksTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksTableViewController.swift; sourceTree = "<group>"; };
|
||||
D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTimelineViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -468,6 +483,7 @@
|
|||
D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; };
|
||||
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
|
||||
D6311C4F25B3765B00B27539 /* ImageDataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataCache.swift; sourceTree = "<group>"; };
|
||||
D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; };
|
||||
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
|
||||
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = "<group>"; };
|
||||
|
@ -553,6 +569,11 @@
|
|||
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseEmojiLabel.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>"; };
|
||||
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; };
|
||||
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDirectoryViewController.swift; sourceTree = "<group>"; };
|
||||
D693A72B25CF8D15003A14E2 /* DirectoryOrder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryOrder.swift; sourceTree = "<group>"; };
|
||||
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FeaturedProfileCollectionViewCell.xib; 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>"; };
|
||||
|
@ -583,6 +604,10 @@
|
|||
D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTests.swift; sourceTree = "<group>"; };
|
||||
D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = "<group>"; };
|
||||
D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = "<group>"; };
|
||||
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTablePrefetching.swift; sourceTree = "<group>"; };
|
||||
D6A6C10E25B62D2400298D0F /* DiskCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskCache.swift; sourceTree = "<group>"; };
|
||||
D6A6C11425B62E9700298D0F /* CacheExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheExpiry.swift; sourceTree = "<group>"; };
|
||||
D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
|
||||
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSceneDelegate.swift; sourceTree = "<group>"; };
|
||||
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; };
|
||||
D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMesasgeActivity.swift; sourceTree = "<group>"; };
|
||||
|
@ -619,16 +644,17 @@
|
|||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
|
||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
||||
D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsContainerView.swift; sourceTree = "<group>"; };
|
||||
D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreViewController.swift; sourceTree = "<group>"; };
|
||||
D6C82B5425C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandThreadTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D6C82B5525C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ExpandThreadTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageViewController.swift; sourceTree = "<group>"; };
|
||||
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
|
||||
D6C99FC624FACFAB005C74D3 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = "<group>"; };
|
||||
D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainComposeTextView.swift; sourceTree = "<group>"; };
|
||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionHelper.swift; sourceTree = "<group>"; };
|
||||
D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryPlayerViewController.swift; sourceTree = "<group>"; };
|
||||
D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ConditionalModifier.swift"; sourceTree = "<group>"; };
|
||||
D6D3FDDF24F41B8400FF50A5 /* ComposeContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeContainerView.swift; sourceTree = "<group>"; };
|
||||
D6D3FDE124F46A8D00FF50A5 /* ComposeUIState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeUIState.swift; sourceTree = "<group>"; };
|
||||
D6D4CC90250D2C3100FCCF8D /* UIAccessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAccessibility.swift; sourceTree = "<group>"; };
|
||||
D6D4CC93250DB86A00FCCF8D /* ComposeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTests.swift; sourceTree = "<group>"; };
|
||||
D6D4DDCC212518A000E1C4BB /* Tusker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tusker.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
|
@ -648,12 +674,12 @@
|
|||
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>"; };
|
||||
D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAutocompleteView.swift; sourceTree = "<group>"; };
|
||||
D6E426802532814100C02E1C /* MaybeLazyStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeLazyStack.swift; sourceTree = "<group>"; };
|
||||
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzyMatcher.swift; sourceTree = "<group>"; };
|
||||
D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzyMatcherTests.swift; sourceTree = "<group>"; };
|
||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiImageView.swift; sourceTree = "<group>"; };
|
||||
D6E426B8253382B300C02E1C /* SearchResultType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultType.swift; sourceTree = "<group>"; };
|
||||
D6E4885C24A2890C0011C13E /* Tusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tusker.entitlements; sourceTree = "<group>"; };
|
||||
D6E57FA425C26FAB00341037 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; 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>"; };
|
||||
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextField.swift; sourceTree = "<group>"; };
|
||||
|
@ -662,7 +688,6 @@
|
|||
D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCollapseButton.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>"; };
|
||||
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
|
||||
|
@ -693,7 +718,6 @@
|
|||
D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */,
|
||||
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */,
|
||||
D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */,
|
||||
0461A3902163CBAE00C0A807 /* Cache.framework in Frameworks */,
|
||||
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -804,6 +828,7 @@
|
|||
D61099E6214561FF00432DC2 /* Attachment.swift */,
|
||||
D61099E82145658300432DC2 /* Card.swift */,
|
||||
D61099EA2145661700432DC2 /* ConversationContext.swift */,
|
||||
D693A72B25CF8D15003A14E2 /* DirectoryOrder.swift */,
|
||||
D61099E22144C38900432DC2 /* Emoji.swift */,
|
||||
D61099EC2145664800432DC2 /* Filter.swift */,
|
||||
D6109A0021456B0800432DC2 /* Hashtag.swift */,
|
||||
|
@ -830,6 +855,9 @@
|
|||
children = (
|
||||
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */,
|
||||
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */,
|
||||
D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */,
|
||||
D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */,
|
||||
D6093FB625BE0CF3004811E6 /* HashtagHistoryView.swift */,
|
||||
);
|
||||
path = "Hashtag Cell";
|
||||
sourceTree = "<group>";
|
||||
|
@ -895,9 +923,15 @@
|
|||
D627943C23A5635D00D38C68 /* Explore */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D627943D23A564D400D38C68 /* ExploreViewController.swift */,
|
||||
D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */,
|
||||
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */,
|
||||
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */,
|
||||
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */,
|
||||
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */,
|
||||
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
|
||||
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
|
||||
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
|
||||
D600613D25D07E170067FAD6 /* ProfileDirectoryFilterView.swift */,
|
||||
);
|
||||
path = Explore;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1025,6 +1059,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */,
|
||||
D6C82B5425C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift */,
|
||||
D6C82B5525C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib */,
|
||||
);
|
||||
path = Conversation;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1183,7 +1219,6 @@
|
|||
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */,
|
||||
D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */,
|
||||
D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */,
|
||||
D6D4CC90250D2C3100FCCF8D /* UIAccessibility.swift */,
|
||||
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */,
|
||||
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */,
|
||||
);
|
||||
|
@ -1346,10 +1381,8 @@
|
|||
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
|
||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
||||
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */,
|
||||
D6C99FC624FACFAB005C74D3 /* ActivityIndicatorView.swift */,
|
||||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
||||
D6E426802532814100C02E1C /* MaybeLazyStack.swift */,
|
||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
||||
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
|
||||
D67C57A721E2649B00C3118B /* Account Detail */,
|
||||
|
@ -1385,6 +1418,7 @@
|
|||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
||||
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
||||
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
||||
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1443,6 +1477,7 @@
|
|||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
||||
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
||||
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
|
||||
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */,
|
||||
D67B506B250B28FF00FAECFB /* Vendor */,
|
||||
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
||||
D6757A7A2157E00100721E32 /* XCallbackURL */,
|
||||
|
@ -1491,7 +1526,10 @@
|
|||
D6F1F84E2193B9BE00F5FE67 /* Caching */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6F1F84C2193B56E00F5FE67 /* Cache.swift */,
|
||||
D6A6C11425B62E9700298D0F /* CacheExpiry.swift */,
|
||||
D6A6C10E25B62D2400298D0F /* DiskCache.swift */,
|
||||
D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */,
|
||||
D6311C4F25B3765B00B27539 /* ImageDataCache.swift */,
|
||||
04DACE8D212CC7CC009840C4 /* ImageCache.swift */,
|
||||
);
|
||||
path = Caching;
|
||||
|
@ -1717,19 +1755,23 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */,
|
||||
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */,
|
||||
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
|
||||
D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */,
|
||||
D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */,
|
||||
D627FF7D217E958900CC0648 /* DraftTableViewCell.xib in Resources */,
|
||||
D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */,
|
||||
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
|
||||
D6C82B5725C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib in Resources */,
|
||||
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
||||
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */,
|
||||
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */,
|
||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
||||
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */,
|
||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */,
|
||||
D6969EA4240DD28D002843CE /* UnknownNotificationTableViewCell.xib in Resources */,
|
||||
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
|
||||
D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */,
|
||||
D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */,
|
||||
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */,
|
||||
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
|
||||
|
@ -1808,6 +1850,7 @@
|
|||
D61099D02144B2D700432DC2 /* Method.swift in Sources */,
|
||||
D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */,
|
||||
D61099FB214569F600432DC2 /* Report.swift in Sources */,
|
||||
D693A72C25CF8D15003A14E2 /* DirectoryOrder.swift in Sources */,
|
||||
D61099F92145698900432DC2 /* Relationship.swift in Sources */,
|
||||
D61099E12144C1DC00432DC2 /* Account.swift in Sources */,
|
||||
D61099E92145658300432DC2 /* Card.swift in Sources */,
|
||||
|
@ -1850,6 +1893,7 @@
|
|||
D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */,
|
||||
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */,
|
||||
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
|
||||
D600613E25D07E170067FAD6 /* ProfileDirectoryFilterView.swift in Sources */,
|
||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
||||
|
@ -1857,6 +1901,7 @@
|
|||
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */,
|
||||
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */,
|
||||
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
|
||||
D6093FB725BE0CF3004811E6 /* HashtagHistoryView.swift in Sources */,
|
||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
|
||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
||||
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
|
||||
|
@ -1877,7 +1922,6 @@
|
|||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
||||
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */,
|
||||
D6C99FC724FACFAB005C74D3 /* ActivityIndicatorView.swift in Sources */,
|
||||
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */,
|
||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
||||
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
||||
|
@ -1889,6 +1933,7 @@
|
|||
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */,
|
||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
||||
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
|
||||
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
|
||||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
|
||||
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
|
||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
|
||||
|
@ -1910,7 +1955,7 @@
|
|||
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
||||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
||||
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
||||
D6E426812532814100C02E1C /* MaybeLazyStack.swift in Sources */,
|
||||
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
||||
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
|
||||
D6A3BC852321F6C100FD64D5 /* AccountListTableViewController.swift in Sources */,
|
||||
|
@ -1933,6 +1978,7 @@
|
|||
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */,
|
||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
|
||||
D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.swift in Sources */,
|
||||
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */,
|
||||
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
||||
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */,
|
||||
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */,
|
||||
|
@ -1941,10 +1987,13 @@
|
|||
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
|
||||
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
||||
D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */,
|
||||
D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */,
|
||||
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
|
||||
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */,
|
||||
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
||||
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
||||
|
@ -1970,8 +2019,6 @@
|
|||
D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */,
|
||||
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */,
|
||||
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */,
|
||||
D6D4CC91250D2C3100FCCF8D /* UIAccessibility.swift in Sources */,
|
||||
D627943E23A564D400D38C68 /* ExploreViewController.swift in Sources */,
|
||||
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */,
|
||||
D622757A24EE21D900B82A16 /* ComposeAttachmentRow.swift in Sources */,
|
||||
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */,
|
||||
|
@ -1983,6 +2030,7 @@
|
|||
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
||||
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */,
|
||||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
||||
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */,
|
||||
|
@ -2009,6 +2057,7 @@
|
|||
D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */,
|
||||
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
||||
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */,
|
||||
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */,
|
||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
||||
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
|
||||
|
@ -2018,6 +2067,7 @@
|
|||
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */,
|
||||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
|
||||
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */,
|
||||
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */,
|
||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
|
||||
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */,
|
||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
||||
|
@ -2034,7 +2084,6 @@
|
|||
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */,
|
||||
D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */,
|
||||
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
||||
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */,
|
||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
||||
0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */,
|
||||
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
||||
|
@ -2043,6 +2092,7 @@
|
|||
D626493F23C101C500612E6E /* AlbumAssetCollectionViewController.swift in Sources */,
|
||||
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */,
|
||||
D6C99FCB24FADC91005C74D3 /* MainComposeTextView.swift in Sources */,
|
||||
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
|
||||
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */,
|
||||
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */,
|
||||
D677284E24ECC01D00C732D3 /* Draft.swift in Sources */,
|
||||
|
@ -2052,6 +2102,8 @@
|
|||
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
|
||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
||||
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
|
||||
D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */,
|
||||
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
|
||||
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
||||
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */,
|
||||
|
@ -2121,6 +2173,14 @@
|
|||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
D6E57FA425C26FAB00341037 /* en */,
|
||||
);
|
||||
name = Localizable.stringsdict;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
|
@ -2275,7 +2335,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
|
@ -2331,7 +2391,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -2349,16 +2409,16 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2020.1;
|
||||
MARKETING_VERSION = 2021.1;
|
||||
OTHER_LDFLAGS = "";
|
||||
"OTHER_SWIFT_FLAGS[sdk=iphone*14*]" = "$(inherited) -D SDK_IOS_14";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker;
|
||||
|
@ -2378,16 +2438,16 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2020.1;
|
||||
MARKETING_VERSION = 2021.1;
|
||||
"OTHER_SWIFT_FLAGS[sdk=iphone*14*]" = "$(inherited) -D SDK_IOS_14";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
|
@ -7,9 +7,6 @@
|
|||
<FileRef
|
||||
location = "group:BlankSlate.xcappdata">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Cache/Cache.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Gifu/Gifu.xcodeproj">
|
||||
</FileRef>
|
||||
|
|
|
@ -29,7 +29,7 @@ class AccountActivityItemSource: NSObject, UIActivityItemSource {
|
|||
metadata.originalURL = account.url
|
||||
metadata.url = account.url
|
||||
metadata.title = "\(account.displayName) (@\(account.username)@\(account.url.host!)"
|
||||
if let data = ImageCache.avatars.get(account.avatar),
|
||||
if let data = ImageCache.avatars.getData(account.avatar),
|
||||
let image = UIImage(data: data) {
|
||||
metadata.iconProvider = NSItemProvider(object: image)
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ class StatusActivityItemSource: NSObject, UIActivityItemSource {
|
|||
let doc = try! SwiftSoup.parse(status.content)
|
||||
let content = try! doc.text()
|
||||
metadata.title = "\(status.account.displayName): \"\(content)\""
|
||||
if let data = ImageCache.avatars.get(status.account.avatar),
|
||||
if let data = ImageCache.avatars.getData(status.account.avatar),
|
||||
let image = UIImage(data: data) {
|
||||
metadata.iconProvider = NSItemProvider(object: image)
|
||||
}
|
||||
|
|
|
@ -56,6 +56,9 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
window = UIWindow(windowScene: windowScene)
|
||||
window!.rootViewController = nav
|
||||
window!.makeKeyAndVisible()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil)
|
||||
themePrefChanged()
|
||||
}
|
||||
|
||||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
|
@ -109,4 +112,8 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
@objc private func close() {
|
||||
closeWindow()
|
||||
}
|
||||
|
||||
@objc private func themePrefChanged() {
|
||||
window?.overrideUserInterfaceStyle = Preferences.shared.theme
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
//
|
||||
// Cache.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 11/7/18.
|
||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Cache
|
||||
|
||||
/// Wrapper around Cache library that provides an API for transparently using any storage type
|
||||
enum Cache<T> {
|
||||
case memory(MemoryStorage<T>)
|
||||
case disk(DiskStorage<T>)
|
||||
case hybrid(HybridStorage<T>)
|
||||
|
||||
@available(*, deprecated, message: "disk-based caches synchronously interact with the file system. Avoid using if possible.")
|
||||
func existsObject(forKey key: String) throws -> Bool {
|
||||
switch self {
|
||||
case let .memory(memory):
|
||||
return try memory.existsObject(forKey: key)
|
||||
case let .disk(disk):
|
||||
return try disk.existsObject(forKey: key)
|
||||
case let .hybrid(hybrid):
|
||||
return try hybrid.existsObject(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func object(forKey key: String) throws -> T {
|
||||
switch self {
|
||||
case let .memory(memory):
|
||||
return try memory.object(forKey: key)
|
||||
case let .disk(disk):
|
||||
return try disk.object(forKey: key)
|
||||
case let .hybrid(hybrid):
|
||||
return try hybrid.object(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func setObject(_ object: T, forKey key: String, expiry: Expiry? = nil) throws {
|
||||
switch self {
|
||||
case let .memory(memory):
|
||||
memory.setObject(object, forKey: key, expiry: expiry)
|
||||
case let .disk(disk):
|
||||
try disk.setObject(object, forKey: key, expiry: expiry)
|
||||
case let .hybrid(hybrid):
|
||||
try hybrid.setObject(object, forKey: key, expiry: expiry)
|
||||
}
|
||||
}
|
||||
|
||||
func removeAll() throws {
|
||||
switch self {
|
||||
case let .memory(memory):
|
||||
memory.removeAll()
|
||||
case let .disk(disk):
|
||||
try disk.removeAll()
|
||||
case let .hybrid(hybrid):
|
||||
try hybrid.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// CacheExpiry.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/18/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum CacheExpiry {
|
||||
case never
|
||||
case seconds(TimeInterval)
|
||||
case date(Date)
|
||||
|
||||
var date: Date {
|
||||
switch self {
|
||||
case .never:
|
||||
return .distantFuture
|
||||
case let .seconds(seconds):
|
||||
return Date().addingTimeInterval(seconds)
|
||||
case let .date(date):
|
||||
return date
|
||||
}
|
||||
}
|
||||
|
||||
var isExpired: Bool {
|
||||
return date.timeIntervalSinceNow < 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
//
|
||||
// DiskCache.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/18/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
struct DiskCacheTransformer<T> {
|
||||
let toData: (T) throws -> Data
|
||||
let fromData: (Data) throws -> T
|
||||
}
|
||||
|
||||
class DiskCache<T> {
|
||||
|
||||
let fileManager: FileManager
|
||||
let path: String
|
||||
let defaultExpiry: CacheExpiry
|
||||
let transformer: DiskCacheTransformer<T>
|
||||
|
||||
private var fileStates = [String: FileState]()
|
||||
|
||||
init(name: String, defaultExpiry: CacheExpiry, transformer: DiskCacheTransformer<T>, fileManager: FileManager = .default) throws {
|
||||
self.defaultExpiry = defaultExpiry
|
||||
self.transformer = transformer
|
||||
self.fileManager = fileManager
|
||||
|
||||
let cacheDir = try fileManager.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
|
||||
self.path = cacheDir.appendingPathComponent(name, isDirectory: true).path
|
||||
|
||||
try createDirectory()
|
||||
}
|
||||
|
||||
private func createDirectory() throws {
|
||||
if !fileManager.fileExists(atPath: path) {
|
||||
try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeFileName(for key: String) -> String {
|
||||
let ext = (key as NSString).pathExtension
|
||||
|
||||
let digest = Insecure.MD5.hash(data: key.data(using: .utf8)!)
|
||||
let hash = digest.map { String($0, radix: 16) }.joined()
|
||||
|
||||
if ext.isEmpty {
|
||||
return hash
|
||||
} else {
|
||||
return "\(hash).\(ext)"
|
||||
}
|
||||
}
|
||||
|
||||
private func makeFilePath(for key: String) -> String {
|
||||
return (path as NSString).appendingPathComponent(makeFileName(for: key))
|
||||
}
|
||||
|
||||
private func fileState(forKey key: String) -> FileState {
|
||||
return fileStates[key] ?? .unknown
|
||||
}
|
||||
|
||||
func setObject(_ object: T, forKey key: String) throws {
|
||||
let data = try transformer.toData(object)
|
||||
let path = makeFilePath(for: key)
|
||||
guard fileManager.createFile(atPath: path, contents: data, attributes: [.modificationDate: defaultExpiry.date]) else {
|
||||
throw Error.couldNotCreateFile
|
||||
}
|
||||
fileStates[key] = .exists
|
||||
}
|
||||
|
||||
func removeObject(forKey key: String) throws {
|
||||
let path = makeFilePath(for: key)
|
||||
try fileManager.removeItem(atPath: path)
|
||||
fileStates[key] = .doesNotExist
|
||||
}
|
||||
|
||||
func existsObject(forKey key: String) throws -> Bool {
|
||||
switch fileState(forKey: key) {
|
||||
case .exists:
|
||||
return true
|
||||
case .doesNotExist:
|
||||
return false
|
||||
case .unknown:
|
||||
let path = makeFilePath(for: key)
|
||||
guard fileManager.fileExists(atPath: path) else {
|
||||
return false
|
||||
}
|
||||
let attributes = try fileManager.attributesOfItem(atPath: path)
|
||||
guard let date = attributes[.modificationDate] as? Date else {
|
||||
throw Error.malformedFileAttributes
|
||||
}
|
||||
return date.timeIntervalSinceNow >= 0
|
||||
}
|
||||
}
|
||||
|
||||
func object(forKey key: String) throws -> T {
|
||||
let path = makeFilePath(for: key)
|
||||
let attributes = try fileManager.attributesOfItem(atPath: path)
|
||||
|
||||
guard let date = attributes[.modificationDate] as? Date else {
|
||||
throw Error.malformedFileAttributes
|
||||
}
|
||||
guard date.timeIntervalSinceNow >= 0 else {
|
||||
try fileManager.removeItem(atPath: path)
|
||||
fileStates[key] = .doesNotExist
|
||||
throw Error.expired
|
||||
}
|
||||
|
||||
let data = try Data(contentsOf: URL(fileURLWithPath: path, isDirectory: false))
|
||||
let object = try transformer.fromData(data)
|
||||
return object
|
||||
}
|
||||
|
||||
func removeAll() throws {
|
||||
try fileManager.removeItem(atPath: path)
|
||||
try createDirectory()
|
||||
fileStates.removeAll()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension DiskCache {
|
||||
enum Error: Swift.Error {
|
||||
case malformedFileAttributes
|
||||
case couldNotCreateFile
|
||||
case expired
|
||||
}
|
||||
}
|
||||
|
||||
extension DiskCache {
|
||||
enum FileState {
|
||||
case exists, doesNotExist, unknown
|
||||
}
|
||||
}
|
||||
|
||||
extension DiskCache where T == Data {
|
||||
convenience init(name: String, defaultExpiry: CacheExpiry) throws {
|
||||
try self.init(name: name, defaultExpiry: defaultExpiry, transformer: DiskCacheTransformer(toData: { $0 }, fromData: { $0 }))
|
||||
}
|
||||
}
|
|
@ -7,11 +7,10 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import Cache
|
||||
|
||||
class ImageCache {
|
||||
|
||||
static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24))
|
||||
static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24), desiredSize: CGSize(width: 50, height: 50))
|
||||
static let headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
|
||||
static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
|
||||
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
|
||||
|
@ -22,51 +21,36 @@ class ImageCache {
|
|||
private static let disableCaching = false
|
||||
#endif
|
||||
|
||||
private let cache: Cache<Data>
|
||||
private let cache: ImageDataCache
|
||||
|
||||
private var groups = MultiThreadDictionary<URL, RequestGroup>(name: "ImageCache request groups")
|
||||
|
||||
private var backgroundQueue = DispatchQueue(label: "ImageCache completion queue", qos: .default)
|
||||
|
||||
init(name: String, memoryExpiry expiry: Expiry) {
|
||||
let storage = MemoryStorage<Data>(config: MemoryConfig(expiry: expiry))
|
||||
self.cache = .memory(storage)
|
||||
init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) {
|
||||
// todo: might not always want to use UIScreen.main for this, e.g. Catalyst?
|
||||
let pixelSize = desiredSize?.applying(.init(scaleX: UIScreen.main.scale, y: UIScreen.main.scale))
|
||||
self.cache = ImageDataCache(name: name, memoryExpiry: memoryExpiry, diskExpiry: diskExpiry, storeOriginalDataInMemory: diskExpiry == nil, desiredPixelSize: pixelSize)
|
||||
}
|
||||
|
||||
init(name: String, diskExpiry expiry: Expiry) {
|
||||
let storage = try! DiskStorage<Data>(config: DiskConfig(name: name, expiry: expiry), transformer: TransformerFactory.forData())
|
||||
self.cache = .disk(storage)
|
||||
}
|
||||
|
||||
init(name: String, memoryExpiry: Expiry, diskExpiry: Expiry) {
|
||||
let memory = MemoryStorage<Data>(config: MemoryConfig(expiry: memoryExpiry))
|
||||
let disk = try! DiskStorage<Data>(config: DiskConfig(name: name, expiry: diskExpiry), transformer: TransformerFactory.forData())
|
||||
self.cache = .hybrid(HybridStorage(memoryStorage: memory, diskStorage: disk))
|
||||
}
|
||||
|
||||
func get(_ url: URL, completion: ((Data?) -> Void)?) -> Request? {
|
||||
func get(_ url: URL, loadOriginal: Bool = false, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
|
||||
let key = url.absoluteString
|
||||
if !ImageCache.disableCaching,
|
||||
// todo: calling object(forKey: key) does disk I/O and this method is often called from the main thread
|
||||
// in performance sensitive paths. a nice optimization to DiskStorage would be adding an internal cache
|
||||
// of the state (unknown/exists/does not exist) of whether or not objects exist on disk so that the slow, disk I/O
|
||||
// path can be avoided most of the time
|
||||
let data = try? cache.object(forKey: key) {
|
||||
let entry = try? cache.get(key, loadOriginal: loadOriginal) {
|
||||
if let completion = completion {
|
||||
backgroundQueue.async {
|
||||
completion?(data)
|
||||
completion(entry.data, entry.image)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
if let completion = completion, let group = groups[url] {
|
||||
if let group = groups[url] {
|
||||
if let completion = completion {
|
||||
return group.addCallback(completion)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
let group = RequestGroup(url: url) { (data) in
|
||||
if let data = data {
|
||||
try? self.cache.setObject(data, forKey: key)
|
||||
}
|
||||
self.groups.removeValueWithoutReturning(forKey: url)
|
||||
}
|
||||
groups[url] = group
|
||||
let group = createGroup(url: url)
|
||||
let request = group.addCallback(completion)
|
||||
group.run()
|
||||
return request
|
||||
|
@ -74,8 +58,34 @@ class ImageCache {
|
|||
}
|
||||
}
|
||||
|
||||
func get(_ url: URL) -> Data? {
|
||||
return try? cache.object(forKey: url.absoluteString)
|
||||
func fetchIfNotCached(_ url: URL) {
|
||||
// if caching is disabled, don't bother fetching since nothing will be done with the result
|
||||
guard !ImageCache.disableCaching else { return }
|
||||
|
||||
if !((try? cache.has(url.absoluteString)) ?? false),
|
||||
!groups.contains(key: url) {
|
||||
let group = createGroup(url: url)
|
||||
group.run()
|
||||
}
|
||||
}
|
||||
|
||||
private func createGroup(url: URL) -> RequestGroup {
|
||||
let group = RequestGroup(url: url) { (data, image) in
|
||||
if let data = data {
|
||||
try? self.cache.set(url.absoluteString, data: data, image: image)
|
||||
}
|
||||
self.groups.removeValueWithoutReturning(forKey: url)
|
||||
}
|
||||
groups[url] = group
|
||||
return group
|
||||
}
|
||||
|
||||
func getData(_ url: URL) -> Data? {
|
||||
return try? cache.getData(url.absoluteString)
|
||||
}
|
||||
|
||||
func get(_ url: URL, loadOriginal: Bool = false) -> ImageDataCache.Entry? {
|
||||
return try? cache.get(url.absoluteString, loadOriginal: loadOriginal)
|
||||
}
|
||||
|
||||
func cancelWithoutCallback(_ url: URL) {
|
||||
|
@ -88,11 +98,11 @@ class ImageCache {
|
|||
|
||||
private class RequestGroup {
|
||||
let url: URL
|
||||
private let onFinished: (Data?) -> Void
|
||||
private let onFinished: (Data?, UIImage?) -> Void
|
||||
private var task: URLSessionDataTask?
|
||||
private var requests = [Request]()
|
||||
|
||||
init(url: URL, onFinished: @escaping (Data?) -> Void) {
|
||||
init(url: URL, onFinished: @escaping (Data?, UIImage?) -> Void) {
|
||||
self.url = url
|
||||
self.onFinished = onFinished
|
||||
}
|
||||
|
@ -116,7 +126,7 @@ class ImageCache {
|
|||
task?.priority = max(1.0, URLSessionTask.defaultPriority + 0.1 * Float(requests.filter { !$0.cancelled }.count))
|
||||
}
|
||||
|
||||
func addCallback(_ completion: ((Data?) -> Void)?) -> Request {
|
||||
func addCallback(_ completion: ((Data?, UIImage?) -> Void)?) -> Request {
|
||||
let request = Request(callback: completion)
|
||||
requests.append(request)
|
||||
updatePriority()
|
||||
|
@ -141,21 +151,24 @@ class ImageCache {
|
|||
}
|
||||
|
||||
func complete(with data: Data?) {
|
||||
let image = data != nil ? UIImage(data: data!) : nil
|
||||
|
||||
requests.filter { !$0.cancelled }.forEach {
|
||||
if let callback = $0.callback {
|
||||
callback(data)
|
||||
callback(data, image)
|
||||
}
|
||||
}
|
||||
self.onFinished(data)
|
||||
|
||||
self.onFinished(data, image)
|
||||
}
|
||||
}
|
||||
|
||||
class Request {
|
||||
private weak var group: RequestGroup?
|
||||
private(set) var callback: ((Data?) -> Void)?
|
||||
private(set) var callback: ((Data?, UIImage?) -> Void)?
|
||||
private(set) var cancelled: Bool = false
|
||||
|
||||
init(callback: ((Data?) -> Void)?) {
|
||||
init(callback: ((Data?, UIImage?) -> Void)?) {
|
||||
self.callback = callback
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
//
|
||||
// ImageDataCache.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/16/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ImageDataCache {
|
||||
|
||||
private let memory: MemoryCache<Entry>
|
||||
private let disk: DiskCache<Data>?
|
||||
|
||||
private let storeOriginalDataInMemory: Bool
|
||||
private let desiredPixelSize: CGSize?
|
||||
|
||||
init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry?, storeOriginalDataInMemory: Bool, desiredPixelSize: CGSize?) {
|
||||
self.memory = MemoryCache(defaultExpiry: memoryExpiry)
|
||||
|
||||
if let diskExpiry = diskExpiry {
|
||||
self.disk = try! DiskCache(name: name, defaultExpiry: diskExpiry)
|
||||
} else {
|
||||
self.disk = nil
|
||||
}
|
||||
|
||||
self.storeOriginalDataInMemory = storeOriginalDataInMemory
|
||||
self.desiredPixelSize = desiredPixelSize
|
||||
}
|
||||
|
||||
func has(_ key: String) throws -> Bool {
|
||||
if memory.existsObject(forKey: key) {
|
||||
return true
|
||||
} else if let disk = self.disk,
|
||||
try disk.existsObject(forKey: key) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func get(_ key: String, loadOriginal: Bool) throws -> Entry? {
|
||||
if storeOriginalDataInMemory || !loadOriginal,
|
||||
let memoryEntry = try? memory.object(forKey: key) {
|
||||
return memoryEntry
|
||||
} else if let disk = self.disk,
|
||||
let data = try? disk.object(forKey: key),
|
||||
let image = UIImage(data: data) {
|
||||
return Entry(data: data, image: image)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getImage(_ key: String) throws -> UIImage? {
|
||||
return try get(key, loadOriginal: false)?.image
|
||||
}
|
||||
|
||||
func getData(_ key: String) throws -> Data? {
|
||||
return try get(key, loadOriginal: false)?.data
|
||||
}
|
||||
|
||||
func set(_ key: String, data: Data, image: UIImage?) throws {
|
||||
guard let image = scaleImageIfDesired(data: data) ?? image else { return }
|
||||
|
||||
let entry = Entry(data: storeOriginalDataInMemory ? data : nil, image: image)
|
||||
memory.setObject(entry, forKey: key)
|
||||
|
||||
if let disk = self.disk {
|
||||
try disk.setObject(data, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func removeAll() throws {
|
||||
memory.removeAll()
|
||||
try? disk?.removeAll()
|
||||
}
|
||||
|
||||
private func scaleImageIfDesired(data: Data) -> UIImage? {
|
||||
guard let desiredPixelSize = desiredPixelSize,
|
||||
let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceShouldCache: false] as CFDictionary) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let maxDimension = max(desiredPixelSize.width, desiredPixelSize.height)
|
||||
let downsampleOptions = [
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||
kCGImageSourceShouldCacheImmediately: true,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||
kCGImageSourceThumbnailMaxPixelSize: maxDimension
|
||||
] as CFDictionary
|
||||
|
||||
if let downsampled = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) {
|
||||
return UIImage(cgImage: downsampled)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ImageDataCache {
|
||||
struct Entry {
|
||||
let data: Data?
|
||||
let image: UIImage
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
//
|
||||
// MemoryCache.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/18/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class MemoryCache<T> {
|
||||
|
||||
private let cache = NSCache<NSString, Entry>()
|
||||
private let defaultExpiry: CacheExpiry
|
||||
|
||||
init(defaultExpiry: CacheExpiry) {
|
||||
self.defaultExpiry = defaultExpiry
|
||||
}
|
||||
|
||||
|
||||
func setObject(_ object: T, forKey key: String) {
|
||||
let entry = Entry(expiresAt: defaultExpiry.date, object: object)
|
||||
cache.setObject(entry, forKey: key as NSString)
|
||||
}
|
||||
|
||||
func removeObject(forKey key: String) {
|
||||
cache.removeObject(forKey: key as NSString)
|
||||
}
|
||||
|
||||
func existsObject(forKey key: String) -> Bool {
|
||||
return cache.object(forKey: key as NSString) != nil
|
||||
}
|
||||
|
||||
func object(forKey key: String) throws -> T {
|
||||
guard let entry = cache.object(forKey: key as NSString) else {
|
||||
throw Error.notFound
|
||||
}
|
||||
|
||||
guard entry.expiresAt.timeIntervalSinceNow >= 0 else {
|
||||
cache.removeObject(forKey: key as NSString)
|
||||
throw Error.expired
|
||||
}
|
||||
|
||||
return entry.object
|
||||
}
|
||||
|
||||
func removeAll() {
|
||||
cache.removeAllObjects()
|
||||
}
|
||||
}
|
||||
|
||||
extension MemoryCache {
|
||||
enum Error: Swift.Error {
|
||||
case notFound
|
||||
case expired
|
||||
}
|
||||
}
|
||||
|
||||
extension MemoryCache {
|
||||
class Entry {
|
||||
let expiresAt: Date
|
||||
let object: T
|
||||
|
||||
init(expiresAt: Date, object: T) {
|
||||
self.expiresAt = expiresAt
|
||||
self.object = object
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,6 +42,9 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
window = UIWindow(windowScene: windowScene)
|
||||
window!.rootViewController = nav
|
||||
window!.makeKeyAndVisible()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil)
|
||||
themePrefChanged()
|
||||
}
|
||||
|
||||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
|
@ -58,6 +61,10 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
return scene.userActivity
|
||||
}
|
||||
|
||||
@objc private func themePrefChanged() {
|
||||
window?.overrideUserInterfaceStyle = Preferences.shared.theme
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeSceneDelegate: ComposeHostingControllerDelegate {
|
||||
|
|
|
@ -115,6 +115,7 @@ class MastodonController: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
// todo: this should dedup requests
|
||||
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
|
||||
if let instance = self.instance {
|
||||
completion?(instance)
|
||||
|
|
|
@ -11,20 +11,13 @@ import UIKit
|
|||
struct MenuController {
|
||||
|
||||
static let composeCommand: UIKeyCommand = {
|
||||
let selector: Selector
|
||||
if #available(iOS 14.0, *) {
|
||||
selector = #selector(MainSplitViewController.presentCompose)
|
||||
} else {
|
||||
selector = #selector(MainTabBarViewController.presentCompose)
|
||||
}
|
||||
return UIKeyCommand(title: "Compose", action: selector, input: "n", modifierFlags: .command)
|
||||
return UIKeyCommand(title: "Compose", action: #selector(MainSplitViewController.presentCompose), input: "n", modifierFlags: .command)
|
||||
}()
|
||||
|
||||
static func refreshCommand(discoverabilityTitle: String?) -> UIKeyCommand {
|
||||
return UIKeyCommand(title: "Refresh", action: #selector(RefreshableViewController.refresh), input: "r", modifierFlags: .command, discoverabilityTitle: discoverabilityTitle)
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
static func sidebarCommand(item: MainSidebarViewController.Item, command: String) -> UIKeyCommand {
|
||||
let data: Any
|
||||
if case let .tab(tab) = item {
|
||||
|
@ -46,7 +39,6 @@ struct MenuController {
|
|||
)
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
static let sidebarItemKeyCommands: [UIKeyCommand] = [
|
||||
sidebarCommand(item: .tab(.timelines), command: "1"),
|
||||
sidebarCommand(item: .tab(.notifications), command: "2"),
|
||||
|
@ -92,25 +84,18 @@ struct MenuController {
|
|||
}
|
||||
|
||||
private static func buildSidebarShortcuts() -> UIMenu {
|
||||
let children: [UIMenuElement]
|
||||
if #available(iOS 14.0, *) {
|
||||
children = sidebarItemKeyCommands
|
||||
} else {
|
||||
children = []
|
||||
}
|
||||
return UIMenu(
|
||||
title: "",
|
||||
image: nil,
|
||||
identifier: nil,
|
||||
options: .displayInline,
|
||||
children: children
|
||||
children: sidebarItemKeyCommands
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MenuController {
|
||||
@available(iOS 14.0, *)
|
||||
class SidebarItem: NSObject, NSCopying {
|
||||
let item: MainSidebarViewController.Item
|
||||
|
||||
|
|
|
@ -24,6 +24,12 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
|||
return context
|
||||
}()
|
||||
|
||||
private(set) lazy var prefetchBackgroundContext: NSManagedObjectContext = {
|
||||
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
||||
context.parent = self.viewContext
|
||||
return context
|
||||
}()
|
||||
|
||||
let statusSubject = PassthroughSubject<String, Never>()
|
||||
let accountSubject = PassthroughSubject<String, Never>()
|
||||
let relationshipSubject = PassthroughSubject<String, Never>()
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
//
|
||||
// UIAccessibility.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 9/12/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIAccessibility {
|
||||
|
||||
static var prefersCrossFadeTransitionsBackwardsCompat: Bool {
|
||||
if #available(iOS 14.0, *) {
|
||||
return prefersCrossFadeTransitions
|
||||
} else {
|
||||
return isReduceMotionEnabled
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -12,6 +12,13 @@ extension UIWindowSceneDelegate {
|
|||
|
||||
func closeWindow(animation: UIWindowScene.DismissalAnimation = .standard, errorHandler: ((Error) -> Void)? = nil) {
|
||||
guard let session = self.window??.windowScene?.session else { return }
|
||||
// Hide the keyboard before dismissing window.
|
||||
// Calling resignFirstResponder() on the window does not work (always returns false).
|
||||
// Using UIApplication.shared.sendAction(#selector(resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
// may not work as desired if the window with focus is not the one being dismissed (in which case it's okay
|
||||
// if the keyboard remains visible).
|
||||
window??.endEditing(true)
|
||||
|
||||
let options = UIWindowSceneDestructionRequestOptions()
|
||||
options.windowDismissalAnimation = animation
|
||||
UIApplication.shared.requestSceneSessionDestruction(session, options: options, errorHandler: errorHandler)
|
||||
|
|
|
@ -14,6 +14,15 @@ struct ImageGrayscalifier {
|
|||
private static let context = CIContext()
|
||||
private static let cache = NSCache<NSURL, UIImage>()
|
||||
|
||||
static func convertIfNecessary(url: URL?, image: UIImage) -> UIImage? {
|
||||
if Preferences.shared.grayscaleImages,
|
||||
let source = image.cgImage {
|
||||
return convert(url: url, cgImage: source)
|
||||
} else {
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
static func convert(url: URL?, data: Data) -> UIImage? {
|
||||
if let url = url,
|
||||
let cached = cache.object(forKey: url as NSURL) {
|
||||
|
|
|
@ -142,7 +142,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
}
|
||||
|
||||
func activateAccount(_ account: LocalData.UserAccountInfo, animated: Bool) {
|
||||
|
||||
let oldMostRecentAccount = LocalData.shared.mostRecentAccountID
|
||||
LocalData.shared.setMostRecentAccount(account)
|
||||
window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account)
|
||||
|
||||
|
@ -150,7 +150,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
if let container = window?.rootViewController as? AccountSwitchingContainerViewController {
|
||||
let direction: AccountSwitchingContainerViewController.AnimationDirection
|
||||
if animated,
|
||||
let oldIndex = LocalData.shared.accounts.firstIndex(where: { $0.id == LocalData.shared.mostRecentAccountID }),
|
||||
let oldIndex = LocalData.shared.accounts.firstIndex(where: { $0.id == oldMostRecentAccount }),
|
||||
let newIndex = LocalData.shared.accounts.firstIndex(of: account) {
|
||||
direction = newIndex > oldIndex ? .upwards : .downwards
|
||||
} else {
|
||||
|
@ -176,11 +176,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
mastodonController.getOwnAccount()
|
||||
mastodonController.getOwnInstance()
|
||||
|
||||
if #available(iOS 14.0, *) {
|
||||
return MainSplitViewController(mastodonController: mastodonController)
|
||||
} else {
|
||||
return MainTabBarViewController(mastodonController: mastodonController)
|
||||
}
|
||||
}
|
||||
|
||||
func createOnboardingUI() -> UIViewController {
|
||||
|
|
|
@ -47,4 +47,12 @@ class MultiThreadDictionary<Key: Hashable, Value> {
|
|||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func contains(key: Key) -> Bool {
|
||||
var value: Bool!
|
||||
queue.sync {
|
||||
value = dict.keys.contains(key)
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@ class AccountListTableViewController: EnhancedTableViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
dragEnabled = true
|
||||
|
||||
tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell)
|
||||
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
|
|
|
@ -25,7 +25,7 @@ class AttachmentPreviewViewController: UIViewController {
|
|||
}
|
||||
|
||||
override func loadView() {
|
||||
if let data = ImageCache.attachments.get(attachment.url),
|
||||
if let data = ImageCache.attachments.getData(attachment.url),
|
||||
let image = UIImage(data: data) {
|
||||
let imageView: UIImageView
|
||||
if attachment.url.pathExtension == "gif" {
|
||||
|
|
|
@ -44,7 +44,7 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
|||
var animationGifData: Data? {
|
||||
let attachment = attachments[currentIndex]
|
||||
if attachment.url.pathExtension == "gif" {
|
||||
return ImageCache.attachments.get(attachment.url)
|
||||
return ImageCache.attachments.getData(attachment.url)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -162,24 +162,19 @@ extension BookmarksTableViewController: StatusTableViewCellDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
extension BookmarksTableViewController: UITableViewDataSourcePrefetching {
|
||||
extension BookmarksTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
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 where attachment.kind == .image {
|
||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
||||
}
|
||||
}
|
||||
let ids = indexPaths.map { statuses[$0.row].id }
|
||||
prefetchStatuses(with: ids)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
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 where attachment.kind == .image {
|
||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
||||
}
|
||||
}
|
||||
let ids: [String] = indexPaths.compactMap {
|
||||
guard $0.row < statuses.count else {
|
||||
return nil
|
||||
}
|
||||
return statuses[$0.row].id
|
||||
}
|
||||
cancelPrefetchingStatuses(with: ids)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,25 +34,11 @@ struct ComposeAttachmentRow: View {
|
|||
.contextMenu {
|
||||
if case .drawing(_) = attachment.data {
|
||||
Button(action: self.editDrawing) {
|
||||
if #available(iOS 14.0, *) {
|
||||
Label("Edit Drawing", systemImage: "hand.draw")
|
||||
} else {
|
||||
HStack {
|
||||
Text("Edit Drawing")
|
||||
Image(systemName: "hand.draw")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if attachment.data.type == .image {
|
||||
Button(action: self.recognizeText) {
|
||||
if #available(iOS 14.0, *) {
|
||||
Label("Recognize Text", systemImage: "doc.text.viewfinder")
|
||||
} else {
|
||||
HStack {
|
||||
Text("Recognize Text")
|
||||
Image(systemName: "doc.text.viewfinder")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,11 +51,7 @@ struct ComposeAttachmentRow: View {
|
|||
.fontSize(17)
|
||||
|
||||
case .recognizingText:
|
||||
if #available(iOS 14.0, *) {
|
||||
ProgressView()
|
||||
} else {
|
||||
ActivityIndicatorView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -45,14 +45,7 @@ struct ComposeAttachmentsList: View {
|
|||
}
|
||||
|
||||
Button(action: self.addAttachment) {
|
||||
if #available(iOS 14.0, *) {
|
||||
Label("Add photo or video", systemImage: addButtonImageName)
|
||||
} else {
|
||||
HStack {
|
||||
Image(systemName: addButtonImageName)
|
||||
Text("Add photo or video")
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(!canAddAttachment)
|
||||
.foregroundColor(.blue)
|
||||
|
@ -61,14 +54,7 @@ struct ComposeAttachmentsList: View {
|
|||
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
||||
|
||||
Button(action: self.createDrawing) {
|
||||
if #available(iOS 14.0, *) {
|
||||
Label("Draw something", systemImage: "hand.draw")
|
||||
} else {
|
||||
HStack(alignment: .lastTextBaseline) {
|
||||
Image(systemName: "hand.draw")
|
||||
Text("Draw something")
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(!canAddAttachment)
|
||||
.foregroundColor(.blue)
|
||||
|
|
|
@ -44,19 +44,6 @@ struct ComposeAutocompleteView: View {
|
|||
}
|
||||
}
|
||||
|
||||
fileprivate extension View {
|
||||
@ViewBuilder
|
||||
func iOS13OnlyPadding() -> some View {
|
||||
// on iOS 13, if the scroll view content's height changes after the view is added to the hierarchy,
|
||||
// it doesn't appear on screen until interactive keyboard dismissal is started and then cancelled :S
|
||||
if #available(iOS 14.0, *) {
|
||||
self
|
||||
} else {
|
||||
self.frame(height: 46)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ComposeAutocompleteMentionsView: View {
|
||||
@EnvironmentObject private var mastodonController: MastodonController
|
||||
@EnvironmentObject private var uiState: ComposeUIState
|
||||
|
@ -104,7 +91,6 @@ struct ComposeAutocompleteMentionsView: View {
|
|||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.iOS13OnlyPadding()
|
||||
}
|
||||
.onReceive(uiState.$autocompleteState.debounce(for: .milliseconds(250), scheduler: DispatchQueue.main), perform: queryChanged)
|
||||
.onDisappear {
|
||||
|
@ -333,7 +319,6 @@ struct ComposeAutocompleteHashtagsView: View {
|
|||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.iOS13OnlyPadding()
|
||||
}
|
||||
.onReceive(uiState.$autocompleteState.debounce(for: .milliseconds(250), scheduler: DispatchQueue.main), perform: queryChanged)
|
||||
.onDisappear {
|
||||
|
|
|
@ -44,17 +44,11 @@ struct ComposeAvatarImageView: View {
|
|||
|
||||
private func loadImage() {
|
||||
guard let url = url else { return }
|
||||
request = ImageCache.avatars.get(url) { (data) in
|
||||
if let data = data, let image = UIImage(data: data) {
|
||||
request = ImageCache.avatars.get(url) { (_, image) in
|
||||
DispatchQueue.main.async {
|
||||
self.request = nil
|
||||
self.avatarImage = image
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.request = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,11 +58,7 @@ class ComposeDrawingViewController: UIViewController {
|
|||
canvasView.drawing = initialDrawing
|
||||
}
|
||||
canvasView.delegate = self
|
||||
if #available(iOS 14.0, *) {
|
||||
canvasView.drawingPolicy = .anyInput
|
||||
} else {
|
||||
canvasView.allowsFingerDrawing = true
|
||||
}
|
||||
canvasView.minimumZoomScale = 0.5
|
||||
canvasView.maximumZoomScale = 2
|
||||
canvasView.backgroundColor = .systemBackground
|
||||
|
|
|
@ -27,7 +27,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
|
||||
private var cancellables = [AnyCancellable]()
|
||||
|
||||
private var keyboardHeight: CGFloat = 0
|
||||
private var toolbarHeight: CGFloat = 44
|
||||
|
||||
private var mainToolbar: UIToolbar!
|
||||
|
@ -115,13 +114,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
toolbar.isAccessibilityElement = true
|
||||
|
||||
let visibilityAction: Selector?
|
||||
if #available(iOS 14.0, *) {
|
||||
visibilityAction = nil
|
||||
} else {
|
||||
visibilityAction = #selector(visibilityButtonPressed(_:))
|
||||
}
|
||||
let visibilityItem = UIBarButtonItem(image: UIImage(systemName: draft.visibility.imageName), style: .plain, target: self, action: visibilityAction)
|
||||
let visibilityItem = UIBarButtonItem(image: UIImage(systemName: draft.visibility.imageName), style: .plain, target: nil, action: nil)
|
||||
visibilityBarButtonItems.append(visibilityItem)
|
||||
visibilityChanged(draft.visibility)
|
||||
|
||||
|
@ -135,7 +128,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
}
|
||||
|
||||
private func updateAdditionalSafeAreaInsets() {
|
||||
additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: toolbarHeight + keyboardHeight, right: 0)
|
||||
additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: toolbarHeight, right: 0)
|
||||
}
|
||||
|
||||
@objc private func keyboardWillShow(_ notification: Foundation.Notification) {
|
||||
|
@ -147,19 +140,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
|
||||
accessoryView.alpha = 1
|
||||
accessoryView.isHidden = false
|
||||
|
||||
// on iOS 14, SwiftUI safe area automatically includes the keyboard
|
||||
if #available(iOS 14.0, *) {
|
||||
} else {
|
||||
let userInfo = notification.userInfo!
|
||||
let frame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
|
||||
// temporarily reset add'l safe area insets so we can access the default inset
|
||||
additionalSafeAreaInsets = .zero
|
||||
// there are a few extra points that come from somewhere, it seems to be four
|
||||
// and without it, the autocomplete suggestions are cut off :S
|
||||
keyboardHeight = frame.height - view.safeAreaInsets.bottom - accessoryView.frame.height + 4
|
||||
updateAdditionalSafeAreaInsets()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func keyboardWillHide(_ notification: Foundation.Notification) {
|
||||
|
@ -192,13 +172,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
} completion: { (finished) in
|
||||
accessoryView.alpha = 1
|
||||
}
|
||||
|
||||
// on iOS 14, SwiftUI safe area automatically includes the keyboard
|
||||
if #available(iOS 14.0, *) {
|
||||
} else {
|
||||
keyboardHeight = 0
|
||||
updateAdditionalSafeAreaInsets()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func keyboardDidHide(_ notification: Foundation.Notification) {
|
||||
|
@ -214,7 +187,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
item.image = UIImage(systemName: newVisibility.imageName)
|
||||
item.image!.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
||||
item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
||||
if #available(iOS 14.0, *) {
|
||||
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
||||
let state = visibility == newVisibility ? UIMenuElement.State.on : .off
|
||||
return UIAction(title: visibility.displayName, image: UIImage(systemName: visibility.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { (_) in
|
||||
|
@ -224,7 +196,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
item.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: elements)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func canPaste(_ itemProviders: [NSItemProvider]) -> Bool {
|
||||
guard itemProviders.allSatisfy({ $0.canLoadObject(ofClass: CompositionAttachment.self) }) else { return false }
|
||||
|
@ -255,18 +226,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
draft.contentWarningEnabled = !draft.contentWarningEnabled
|
||||
}
|
||||
|
||||
@objc func visibilityButtonPressed(_ sender: UIBarButtonItem) {
|
||||
// if #available(iOS 14.0, *) {
|
||||
// } else {
|
||||
let alertController = UIAlertController(currentVisibility: draft.visibility) { (visibility) in
|
||||
guard let visibility = visibility else { return }
|
||||
self.draft.visibility = visibility
|
||||
}
|
||||
alertController.popoverPresentationController?.barButtonItem = sender
|
||||
present(alertController, animated: true)
|
||||
// }
|
||||
}
|
||||
|
||||
@objc func draftsButtonPresed() {
|
||||
let draftsVC = DraftsTableViewController(account: mastodonController.accountInfo!, exclude: draft)
|
||||
draftsVC.delegate = self
|
||||
|
|
|
@ -44,15 +44,10 @@ struct ComposeView: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
// the pre-iOS 14 API does not result in the correct pointer interactions for nav bar buttons, see FB8595468
|
||||
if #available(iOS 14.0, *) {
|
||||
mostOfTheBody.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) { cancelButton }
|
||||
ToolbarItem(placement: .confirmationAction) { postButton }
|
||||
}
|
||||
} else {
|
||||
mostOfTheBody.navigationBarItems(leading: cancelButton, trailing: postButton)
|
||||
}
|
||||
}
|
||||
|
||||
var mostOfTheBody: some View {
|
||||
|
@ -82,8 +77,6 @@ struct ComposeView: View {
|
|||
|
||||
@ViewBuilder
|
||||
var autocompleteSuggestions: some View {
|
||||
// on iOS 13, the transition causes SwiftUI to hang on the main thread when the view appears, so it's disabled
|
||||
if #available(iOS 14.0, *) {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
if let state = uiState.autocompleteState {
|
||||
|
@ -92,14 +85,6 @@ struct ComposeView: View {
|
|||
}
|
||||
.transition(.move(edge: .bottom))
|
||||
.animation(.default)
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
if let state = uiState.autocompleteState {
|
||||
ComposeAutocompleteView(autocompleteState: state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mainStack(outerMinY: CGFloat) -> some View {
|
||||
|
|
|
@ -45,15 +45,14 @@ class EmojiCollectionViewCell: UICollectionViewCell {
|
|||
func updateUI(emoji: Emoji) {
|
||||
currentEmojiShortcode = emoji.shortcode
|
||||
|
||||
imageRequest = ImageCache.emojis.get(emoji.url) { [weak self] (data) in
|
||||
if let data = data, let image = UIImage(data: data) {
|
||||
imageRequest = ImageCache.emojis.get(emoji.url) { [weak self] (_, image) in
|
||||
guard let image = image else { return }
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self, self.currentEmojiShortcode == emoji.shortcode else { return }
|
||||
self.emojiImageView.image = image
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
|
|
@ -69,13 +69,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
|
||||
uiState.autocompleteHandler = context.coordinator
|
||||
|
||||
let visibilityAction: Selector?
|
||||
if #available(iOS 14.0, *) {
|
||||
visibilityAction = nil
|
||||
} else {
|
||||
visibilityAction = #selector(ComposeHostingController.visibilityButtonPressed(_:))
|
||||
}
|
||||
let visibilityButton = UIBarButtonItem(image: UIImage(systemName: visibility.imageName), style: .plain, target: nil, action: visibilityAction)
|
||||
let visibilityButton = UIBarButtonItem(image: UIImage(systemName: visibility.imageName), style: .plain, target: nil, action: nil)
|
||||
updateVisibilityMenu(visibilityButton)
|
||||
let toolbar = UIToolbar()
|
||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -131,7 +125,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
}
|
||||
|
||||
private func updateVisibilityMenu(_ visibilityButton: UIBarButtonItem) {
|
||||
if #available(iOS 14.0, *) {
|
||||
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
||||
let state = visibility == self.visibility ? UIMenuElement.State.on : .off
|
||||
return UIAction(title: visibility.displayName, image: UIImage(systemName: visibility.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { (_) in
|
||||
|
@ -140,7 +133,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
}
|
||||
visibilityButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: elements)
|
||||
}
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||
uiView.text = text
|
||||
|
|
|
@ -7,25 +7,31 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import SafariServices
|
||||
import Pachyderm
|
||||
import CoreData
|
||||
|
||||
class ConversationNode {
|
||||
let status: StatusMO
|
||||
var children: [ConversationNode]
|
||||
|
||||
init(status: StatusMO) {
|
||||
self.status = status
|
||||
self.children = []
|
||||
}
|
||||
}
|
||||
|
||||
class ConversationTableViewController: EnhancedTableViewController {
|
||||
|
||||
static let showPostsImage = UIImage(systemName: "eye.fill")!
|
||||
static let hidePostsImage = UIImage(systemName: "eye.slash.fill")!
|
||||
|
||||
static let bottomSeparatorTag = 101
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
let mainStatusID: String
|
||||
let mainStatusState: StatusState
|
||||
var statuses: [(id: String, state: StatusState)] = [] {
|
||||
didSet {
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
private(set) var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||
|
||||
var showStatusesAutomatically = false
|
||||
var visibilityBarButtonItem: UIBarButtonItem!
|
||||
|
@ -46,7 +52,8 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
|
||||
deinit {
|
||||
guard let persistentContainer = mastodonController?.persistentContainer else { return }
|
||||
for (id, _) in statuses {
|
||||
let snapshot = dataSource.snapshot()
|
||||
for case let .status(id: id, state: _) in snapshot.itemIdentifiers {
|
||||
persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
|
@ -61,13 +68,76 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
|
||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
|
||||
tableView.register(UINib(nibName: "ConversationMainStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "mainStatusCell")
|
||||
tableView.register(UINib(nibName: "ExpandThreadTableViewCell", bundle: .main), forCellReuseIdentifier: "expandThreadCell")
|
||||
|
||||
tableView.prefetchDataSource = self
|
||||
|
||||
visibilityBarButtonItem = UIBarButtonItem(image: ConversationTableViewController.showPostsImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
||||
tableView.backgroundColor = .secondarySystemBackground
|
||||
// separators are disabled on the table view so we can re-add them ourselves
|
||||
// so they're not inserted in between statuses in the ame sub-thread
|
||||
tableView.separatorStyle = .none
|
||||
|
||||
dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||
switch item {
|
||||
case let .status(id: id, state: state):
|
||||
let rowsInSection = self.dataSource.tableView(tableView, numberOfRowsInSection: indexPath.section)
|
||||
let firstInSection = indexPath.row == 0
|
||||
let lastInSection = indexPath.row == rowsInSection - 1
|
||||
|
||||
let identifier = id == self.mainStatusID ? "mainStatusCell" : "statusCell"
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as! BaseStatusTableViewCell
|
||||
|
||||
if id == self.mainStatusID {
|
||||
cell.selectionStyle = .none
|
||||
}
|
||||
|
||||
cell.delegate = self
|
||||
cell.showStatusAutomatically = self.showStatusesAutomatically
|
||||
|
||||
if let cell = cell as? TimelineStatusTableViewCell {
|
||||
cell.showReplyIndicator = false
|
||||
}
|
||||
|
||||
cell.updateUI(statusID: id, state: state)
|
||||
|
||||
cell.setShowThreadLinks(prev: !firstInSection, next: !lastInSection)
|
||||
if lastInSection {
|
||||
if cell.viewWithTag(ConversationTableViewController.bottomSeparatorTag) == nil {
|
||||
let separator = UIView()
|
||||
separator.tag = ConversationTableViewController.bottomSeparatorTag
|
||||
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||
separator.backgroundColor = tableView.separatorColor
|
||||
cell.addSubview(separator)
|
||||
NSLayoutConstraint.activate([
|
||||
separator.heightAnchor.constraint(equalToConstant: 0.5),
|
||||
separator.bottomAnchor.constraint(equalTo: cell.bottomAnchor),
|
||||
separator.leftAnchor.constraint(equalTo: cell.leftAnchor, constant: cell.separatorInset.left),
|
||||
separator.rightAnchor.constraint(equalTo: cell.rightAnchor),
|
||||
])
|
||||
}
|
||||
} else {
|
||||
cell.viewWithTag(ConversationTableViewController.bottomSeparatorTag)?.removeFromSuperview()
|
||||
}
|
||||
|
||||
return cell
|
||||
|
||||
case let .expandThread(childThreads: childThreads):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "expandThreadCell", for: indexPath) as! ExpandThreadTableViewCell
|
||||
cell.updateUI(childThreads: childThreads)
|
||||
return cell
|
||||
}
|
||||
})
|
||||
|
||||
let initialImage = showStatusesAutomatically ? ConversationTableViewController.hidePostsImage : ConversationTableViewController.showPostsImage
|
||||
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
||||
navigationItem.rightBarButtonItem = visibilityBarButtonItem
|
||||
|
||||
statuses = [(mainStatusID, mainStatusState)]
|
||||
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.statuses])
|
||||
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
guard let mainStatus = self.mastodonController.persistentContainer.status(for: self.mainStatusID) else {
|
||||
fatalError("Missing cached status \(self.mainStatusID)")
|
||||
|
@ -81,19 +151,39 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
|
||||
let parents = self.getDirectParents(inReplyTo: mainStatusInReplyToID, from: context.ancestors)
|
||||
let parentStatuses = context.ancestors.filter { parents.contains($0.id) }
|
||||
self.mastodonController.persistentContainer.addAll(statuses: parentStatuses) {
|
||||
self.mastodonController.persistentContainer.addAll(statuses: context.descendants) {
|
||||
self.statuses = parents.map { ($0, .unknown) } + self.statuses + context.descendants.map { ($0.id, .unknown) }
|
||||
let indexPath = IndexPath(row: parents.count, section: 0)
|
||||
|
||||
// todo: should this really be blindly adding all the descendants?
|
||||
self.mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants) {
|
||||
DispatchQueue.main.async {
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
snapshot.insertItems(parents.map { .status(id: $0, state: .unknown) }, beforeItem: mainStatusItem)
|
||||
|
||||
// fetch all descendant status managed objects
|
||||
let descendantIDs = context.descendants.map(\.id)
|
||||
let request: NSFetchRequest<StatusMO> = StatusMO.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "id in %@", descendantIDs)
|
||||
|
||||
if let descendants = try? self.mastodonController.persistentContainer.viewContext.fetch(request) {
|
||||
// convert array of descendant statuses into tree of sub-threads
|
||||
let childThreads = self.getDescendantThreads(mainStatus: mainStatus, descendants: descendants)
|
||||
|
||||
// convert sub-threads into items for section and add to snapshot
|
||||
self.addFlattenedChildThreadsToSnapshot(childThreads, mainStatus: mainStatus, snapshot: &snapshot)
|
||||
}
|
||||
|
||||
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
||||
// ensure that the main status is on-screen after newly loaded statuses are added
|
||||
// todo: should this not happen if the user has already started scrolling (e.g. because the main status is very long)?
|
||||
if let indexPath = self.dataSource.indexPath(for: mainStatusItem) {
|
||||
self.tableView.scrollToRow(at: indexPath, at: .middle, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getDirectParents(inReplyTo inReplyToID: String?, from statuses: [Status]) -> [String] {
|
||||
private func getDirectParents(inReplyTo inReplyToID: String?, from statuses: [Status]) -> [String] {
|
||||
var statuses = statuses
|
||||
var parents = [String]()
|
||||
|
||||
|
@ -108,38 +198,99 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
return parents
|
||||
}
|
||||
|
||||
// MARK: - Table view data source
|
||||
private func getDescendantThreads(mainStatus: StatusMO, descendants: [StatusMO]) -> [ConversationNode] {
|
||||
var descendants = descendants
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
func removeAllInReplyTo(id: String) -> [StatusMO] {
|
||||
let statuses = descendants.filter { $0.inReplyToID == id }
|
||||
descendants.removeAll { $0.inReplyToID == id }
|
||||
return statuses
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return statuses.count
|
||||
var nodes: [String: ConversationNode] = [
|
||||
mainStatus.id: ConversationNode(status: mainStatus)
|
||||
]
|
||||
|
||||
var idsToCheck = [mainStatusID]
|
||||
|
||||
while !idsToCheck.isEmpty {
|
||||
let inReplyToID = idsToCheck.removeFirst()
|
||||
let nodeForID = nodes[inReplyToID]!
|
||||
|
||||
let inReply = removeAllInReplyTo(id: inReplyToID)
|
||||
for reply in inReply {
|
||||
idsToCheck.append(reply.id)
|
||||
|
||||
let replyNode = ConversationNode(status: reply)
|
||||
nodes[reply.id] = replyNode
|
||||
|
||||
nodeForID.children.append(replyNode)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let (id, state) = statuses[indexPath.row]
|
||||
return nodes[mainStatusID]!.children
|
||||
}
|
||||
|
||||
if id == mainStatusID {
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "mainStatusCell", for: indexPath) as? ConversationMainStatusTableViewCell else { fatalError() }
|
||||
cell.selectionStyle = .none
|
||||
cell.showStatusAutomatically = showStatusesAutomatically
|
||||
cell.delegate = self
|
||||
cell.updateUI(statusID: id, state: state)
|
||||
return cell
|
||||
private func addFlattenedChildThreadsToSnapshot(_ childThreads: [ConversationNode], mainStatus: StatusMO, snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
|
||||
var childThreads = childThreads
|
||||
|
||||
// child threads by the same author as the main status come first
|
||||
let pivotIndex = childThreads.partition(by: { $0.status.account.id != mainStatus.account.id })
|
||||
|
||||
// within each group, child threads are sorted chronologically
|
||||
childThreads[0..<pivotIndex].sort(by: { $0.status.createdAt < $1.status.createdAt })
|
||||
childThreads[pivotIndex...].sort(by: { $0.status.createdAt < $1.status.createdAt })
|
||||
|
||||
for node in childThreads {
|
||||
snapshot.appendSections([.childThread(firstStatusID: node.status.id)])
|
||||
snapshot.appendItems([.status(id: node.status.id, state: .unknown)])
|
||||
|
||||
var currentNode = node
|
||||
while true {
|
||||
let next: ConversationNode
|
||||
|
||||
if currentNode.children.count == 0 {
|
||||
break
|
||||
} else if currentNode.children.count == 1 {
|
||||
next = currentNode.children[0]
|
||||
} else {
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
|
||||
cell.showStatusAutomatically = showStatusesAutomatically
|
||||
cell.showReplyIndicator = false
|
||||
cell.delegate = self
|
||||
cell.updateUI(statusID: id, state: state)
|
||||
return cell
|
||||
let sameAuthorStatuses = currentNode.children.filter({ $0.status.account.id == node.status.account.id })
|
||||
if sameAuthorStatuses.count == 1 {
|
||||
next = sameAuthorStatuses[0]
|
||||
} else {
|
||||
snapshot.appendItems([.expandThread(childThreads: currentNode.children)])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
currentNode = next
|
||||
snapshot.appendItems([.status(id: next.status.id, state: .unknown)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(for indexPath: IndexPath) -> (id: String, state: StatusState)? {
|
||||
return self.dataSource.itemIdentifier(for: indexPath).flatMap { (item) in
|
||||
switch item {
|
||||
case let .status(id: id, state: state):
|
||||
return (id: id, state: state)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Table view delegate
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if case .expandThread = dataSource.itemIdentifier(for: indexPath),
|
||||
case let .status(id: id, state: state) = dataSource.itemIdentifier(for: IndexPath(row: indexPath.row - 1, section: indexPath.section)) {
|
||||
self.selected(status: id, state: state)
|
||||
} else {
|
||||
super.tableView(tableView, didSelectRowAt: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
@ -155,7 +306,8 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
@objc func toggleVisibilityButtonPressed() {
|
||||
showStatusesAutomatically = !showStatusesAutomatically
|
||||
|
||||
for (_, state) in statuses where state.collapsible == true {
|
||||
let snapshot = dataSource.snapshot()
|
||||
for case let .status(id: _, state: state) in snapshot.itemIdentifiers where state.collapsible == true {
|
||||
state.collapsed = !showStatusesAutomatically
|
||||
}
|
||||
|
||||
|
@ -179,6 +331,48 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
|
||||
}
|
||||
|
||||
extension ConversationTableViewController {
|
||||
enum Section: Hashable {
|
||||
case statuses
|
||||
case childThread(firstStatusID: String)
|
||||
}
|
||||
enum Item: Hashable {
|
||||
case status(id: String, state: StatusState)
|
||||
case expandThread(childThreads: [ConversationNode])
|
||||
|
||||
static func == (lhs: Item, rhs: Item) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.status(id: a, state: _), .status(id: b, state: _)):
|
||||
return a == b
|
||||
case let (.expandThread(childThreads: a), .expandThread(childThreads: b)):
|
||||
return zip(a, b).allSatisfy { $0.status.id == $1.status.id }
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case let .status(id: id, state: _):
|
||||
hasher.combine("status")
|
||||
hasher.combine(id)
|
||||
case let .expandThread(childThreads: children):
|
||||
hasher.combine("expandThread")
|
||||
hasher.combine(children.map(\.status.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationTableViewController: TuskerNavigationDelegate {
|
||||
func conversation(mainStatusID: String, state: StatusState) -> ConversationTableViewController {
|
||||
let vc = ConversationTableViewController(for: mainStatusID, state: state, mastodonController: mastodonController)
|
||||
// transfer show statuses automatically state when showing new conversation
|
||||
vc.showStatusesAutomatically = self.showStatusesAutomatically
|
||||
return vc
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationTableViewController: StatusTableViewCellDelegate {
|
||||
var apiController: MastodonController { mastodonController }
|
||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||
|
@ -188,24 +382,14 @@ extension ConversationTableViewController: StatusTableViewCellDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
extension ConversationTableViewController: UITableViewDataSourcePrefetching {
|
||||
extension ConversationTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
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 where attachment.kind == .image {
|
||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
||||
}
|
||||
}
|
||||
let ids: [String] = indexPaths.compactMap { item(for: $0)?.id }
|
||||
prefetchStatuses(with: ids)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
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 where attachment.kind == .image {
|
||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
||||
}
|
||||
}
|
||||
let ids: [String] = indexPaths.compactMap { item(for: $0)?.id }
|
||||
cancelPrefetchingStatuses(with: ids)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
//
|
||||
// ExpandThreadTableViewCell.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/30/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ExpandThreadTableViewCell: UITableViewCell {
|
||||
|
||||
@IBOutlet weak var avatarContainerView: UIView!
|
||||
@IBOutlet weak var avatarContainerWidthConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var replyCountLabel: UILabel!
|
||||
var avatarImageViews: [UIImageView] = []
|
||||
|
||||
private var avatarRequests: [ImageCache.Request] = []
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
let prevThreadLinkView = UIView()
|
||||
prevThreadLinkView.translatesAutoresizingMaskIntoConstraints = false
|
||||
prevThreadLinkView.backgroundColor = tintColor.withAlphaComponent(0.5)
|
||||
prevThreadLinkView.layer.cornerRadius = 2.5
|
||||
prevThreadLinkView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
contentView.addSubview(prevThreadLinkView)
|
||||
NSLayoutConstraint.activate([
|
||||
prevThreadLinkView.widthAnchor.constraint(equalToConstant: 5),
|
||||
prevThreadLinkView.centerXAnchor.constraint(equalTo: leadingAnchor, constant: 16 + 25),
|
||||
prevThreadLinkView.topAnchor.constraint(equalTo: topAnchor),
|
||||
prevThreadLinkView.bottomAnchor.constraint(equalTo: avatarContainerView.topAnchor, constant: -2),
|
||||
])
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||
}
|
||||
|
||||
func updateUI(childThreads: [ConversationNode]) {
|
||||
let format = NSLocalizedString("expand threads count", comment: "expand conversation threads button label")
|
||||
replyCountLabel.text = String.localizedStringWithFormat(format, childThreads.count)
|
||||
|
||||
let accounts = childThreads.map(\.status.account).uniques().prefix(3)
|
||||
|
||||
avatarImageViews.forEach { $0.removeFromSuperview() }
|
||||
avatarImageViews = []
|
||||
|
||||
avatarRequests = []
|
||||
|
||||
let avatarImageSize: CGFloat = 44 - 12
|
||||
|
||||
if accounts.count == 1 {
|
||||
avatarContainerWidthConstraint.constant = avatarImageSize
|
||||
} else {
|
||||
avatarContainerWidthConstraint.constant = CGFloat(accounts.count) * avatarImageSize * 3 / 4
|
||||
}
|
||||
|
||||
for (index, account) in accounts.enumerated() {
|
||||
let accountImageView = UIImageView()
|
||||
accountImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
accountImageView.contentMode = .scaleAspectFit
|
||||
accountImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * avatarImageSize
|
||||
accountImageView.layer.masksToBounds = true
|
||||
accountImageView.layer.borderWidth = 1
|
||||
accountImageView.layer.borderColor = UIColor.secondarySystemBackground.cgColor
|
||||
// need a solid background color so semi-transparent avatars don't look bad
|
||||
accountImageView.backgroundColor = .secondarySystemBackground
|
||||
avatarContainerView.addSubview(accountImageView)
|
||||
|
||||
avatarImageViews.append(accountImageView)
|
||||
|
||||
accountImageView.layer.zPosition = CGFloat(-index)
|
||||
|
||||
let xConstraint: NSLayoutConstraint
|
||||
if index == 0 {
|
||||
xConstraint = accountImageView.leadingAnchor.constraint(equalTo: avatarContainerView.leadingAnchor)
|
||||
} else if index == accounts.count - 1 {
|
||||
xConstraint = accountImageView.trailingAnchor.constraint(equalTo: avatarContainerView.trailingAnchor)
|
||||
} else {
|
||||
xConstraint = accountImageView.centerXAnchor.constraint(equalTo: avatarContainerView.centerXAnchor)
|
||||
}
|
||||
NSLayoutConstraint.activate([
|
||||
accountImageView.widthAnchor.constraint(equalToConstant: avatarImageSize),
|
||||
accountImageView.heightAnchor.constraint(equalToConstant: avatarImageSize),
|
||||
accountImageView.centerYAnchor.constraint(equalTo: avatarContainerView.centerYAnchor),
|
||||
xConstraint
|
||||
])
|
||||
|
||||
let req = ImageCache.avatars.get(account.avatar) { [weak accountImageView] (_, image) in
|
||||
DispatchQueue.main.async {
|
||||
accountImageView?.image = image
|
||||
}
|
||||
}
|
||||
if let req = req {
|
||||
avatarRequests.append(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func preferencesChanged() {
|
||||
avatarImageViews.forEach {
|
||||
$0.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: $0)
|
||||
}
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
avatarRequests.forEach { $0.cancel() }
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" 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="17703"/>
|
||||
<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"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="ExpandThreadTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="IXi-sc-YIw">
|
||||
<rect key="frame" x="16" y="0.0" width="173" height="44"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="eFB-F1-d3A">
|
||||
<rect key="frame" x="0.0" y="6" width="100" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="32" id="g9U-u7-718"/>
|
||||
<constraint firstAttribute="width" constant="100" id="tiI-Rj-gjh"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2 replies" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Dcm-ll-GeE">
|
||||
<rect key="frame" x="108" y="12" width="65" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" systemColor="systemBlueColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="jk2-uV-FdO"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kkt-hM-ScW">
|
||||
<rect key="frame" x="16" y="43.5" width="304" height="0.5"/>
|
||||
<color key="backgroundColor" systemColor="separatorColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="0.5" id="Fkq-bT-IYv"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="Kkt-hM-ScW" secondAttribute="bottom" id="AvY-H1-0YN"/>
|
||||
<constraint firstItem="Kkt-hM-ScW" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="16" id="E5g-hz-SLI"/>
|
||||
<constraint firstItem="IXi-sc-YIw" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="SRF-Zx-Y0R"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Kkt-hM-ScW" secondAttribute="trailing" id="YML-R1-ezq"/>
|
||||
<constraint firstItem="IXi-sc-YIw" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="iD5-Av-ORS"/>
|
||||
<constraint firstAttribute="bottom" secondItem="IXi-sc-YIw" secondAttribute="bottom" id="kpD-6Q-qKi"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||
<connections>
|
||||
<outlet property="avatarContainerView" destination="eFB-F1-d3A" id="xGo-40-nn7"/>
|
||||
<outlet property="avatarContainerWidthConstraint" destination="tiI-Rj-gjh" id="34n-ev-EKi"/>
|
||||
<outlet property="replyCountLabel" destination="Dcm-ll-GeE" id="E4m-xk-DiQ"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="132" y="132"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="secondarySystemBackgroundColor">
|
||||
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" 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="systemBlueColor">
|
||||
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
|
@ -9,12 +9,19 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class AddSavedHashtagViewController: SearchResultsViewController {
|
||||
class AddSavedHashtagViewController: EnhancedTableViewController {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
var resultsController: SearchResultsViewController!
|
||||
var searchController: UISearchController!
|
||||
|
||||
var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
super.init(mastodonController: mastodonController, resultTypes: [.hashtags])
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(style: .grouped)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
@ -24,14 +31,32 @@ class AddSavedHashtagViewController: SearchResultsViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
delegate = self
|
||||
title = NSLocalizedString("Search", comment: "search screen title")
|
||||
|
||||
searchController = UISearchController(searchResultsController: nil)
|
||||
searchController.obscuresBackgroundDuringPresentation = false
|
||||
tableView.register(UINib(nibName: "TrendingHashtagTableViewCell", bundle: .main), forCellReuseIdentifier: "trendingTagCell")
|
||||
tableView.rowHeight = 60 // 44 for content + 2 * 8 spacing
|
||||
|
||||
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||
switch item {
|
||||
case let .tag(hashtag):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "trendingTagCell", for: indexPath) as! TrendingHashtagTableViewCell
|
||||
cell.updateUI(hashtag: hashtag)
|
||||
return cell
|
||||
}
|
||||
})
|
||||
|
||||
resultsController = HashtagSearchResultsViewController(mastodonController: mastodonController)
|
||||
resultsController.delegate = self
|
||||
resultsController.exploreNavigationController = self.navigationController!
|
||||
|
||||
searchController = UISearchController(searchResultsController: resultsController)
|
||||
searchController.obscuresBackgroundDuringPresentation = true
|
||||
searchController.hidesNavigationBarDuringPresentation = false
|
||||
searchController.searchResultsUpdater = resultsController
|
||||
searchController.searchBar.autocapitalizationType = .none
|
||||
searchController.searchBar.placeholder = NSLocalizedString("Search for hashtags to save", comment: "add saved hashtag search field placeholder")
|
||||
searchController.searchBar.delegate = self
|
||||
searchController.searchBar.delegate = resultsController
|
||||
searchController.searchBar.showsCancelButton = false
|
||||
|
||||
definesPresentationContext = true
|
||||
|
||||
|
@ -41,11 +66,38 @@ class AddSavedHashtagViewController: SearchResultsViewController {
|
|||
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonPressed))
|
||||
}
|
||||
|
||||
override func performSearch(query: String?) {
|
||||
if let query = query, !query.starts(with: "#") {
|
||||
super.performSearch(query: "#\(query)")
|
||||
} else {
|
||||
super.performSearch(query: query)
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
let request = Client.getTrends(limit: 10)
|
||||
mastodonController.run(request) { (response) in
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
|
||||
guard case let .success(hashtags, _) = response,
|
||||
hashtags.count > 0 else {
|
||||
self.dataSource.apply(snapshot)
|
||||
return
|
||||
}
|
||||
|
||||
snapshot.appendSections([.trendingTags])
|
||||
snapshot.appendItems(hashtags.map { .tag($0) })
|
||||
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func selectHashtag(_ hashtag: Hashtag) {
|
||||
SavedDataManager.shared.add(hashtag: hashtag, for: mastodonController.accountInfo!)
|
||||
presentingViewController!.dismiss(animated: true)
|
||||
}
|
||||
|
||||
// MARK: - Table View Delegate
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
switch dataSource.itemIdentifier(for: indexPath) {
|
||||
case nil:
|
||||
return
|
||||
case let .tag(hashtag):
|
||||
selectHashtag(hashtag)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,9 +109,24 @@ class AddSavedHashtagViewController: SearchResultsViewController {
|
|||
|
||||
}
|
||||
|
||||
extension AddSavedHashtagViewController {
|
||||
enum Section {
|
||||
case trendingTags
|
||||
}
|
||||
|
||||
enum Item: Hashable {
|
||||
case tag(Hashtag)
|
||||
}
|
||||
|
||||
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
return NSLocalizedString("Trending Hashtags", comment: "trending hashtags seciton title")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AddSavedHashtagViewController: SearchResultsViewControllerDelegate {
|
||||
func selectedSearchResult(hashtag: Hashtag) {
|
||||
SavedDataManager.shared.add(hashtag: hashtag, for: mastodonController.accountInfo!)
|
||||
dismiss(animated: true)
|
||||
selectHashtag(hashtag)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,23 +10,22 @@ import UIKit
|
|||
import Combine
|
||||
import Pachyderm
|
||||
|
||||
class ExploreViewController: EnhancedTableViewController {
|
||||
class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
var dataSource: DataSource!
|
||||
private var collectionView: UICollectionView!
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
|
||||
var resultsController: SearchResultsViewController!
|
||||
var searchController: UISearchController!
|
||||
private(set) var resultsController: SearchResultsViewController!
|
||||
private(set) var searchController: UISearchController!
|
||||
|
||||
var searchControllerStatusOnAppearance: Bool? = nil
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(style: .insetGrouped)
|
||||
|
||||
dragEnabled = true
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
title = NSLocalizedString("Explore", comment: "explore tab title")
|
||||
tabBarItem.image = UIImage(systemName: "magnifyingglass")
|
||||
|
@ -39,63 +38,23 @@ class ExploreViewController: EnhancedTableViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: "basicCell")
|
||||
var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
|
||||
configuration.trailingSwipeActionsConfigurationProvider = self.trailingSwipeActionsForCell(at:)
|
||||
configuration.headerMode = .supplementary
|
||||
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
|
||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
collectionView.delegate = self
|
||||
collectionView.dragDelegate = self
|
||||
view.addSubview(collectionView)
|
||||
|
||||
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "basicCell", for: indexPath)
|
||||
dataSource = createDataSource()
|
||||
applyInitialSnapshot()
|
||||
|
||||
switch item {
|
||||
case .bookmarks:
|
||||
cell.imageView!.image = UIImage(systemName: "bookmark.fill")
|
||||
cell.textLabel!.text = NSLocalizedString("Bookmarks", comment: "bookmarks nav item title")
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
|
||||
case let .list(list):
|
||||
cell.imageView!.image = UIImage(systemName: "list.bullet")
|
||||
cell.textLabel!.text = list.title
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
|
||||
case .addList:
|
||||
cell.imageView!.image = UIImage(systemName: "plus")
|
||||
cell.textLabel!.text = NSLocalizedString("New List...", comment: "new list nav item title")
|
||||
cell.accessoryType = .none
|
||||
|
||||
case let .savedHashtag(hashtag):
|
||||
cell.imageView!.image = UIImage(systemName: "number")
|
||||
cell.textLabel!.text = hashtag.name
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
|
||||
case .addSavedHashtag:
|
||||
cell.imageView!.image = UIImage(systemName: "plus")
|
||||
cell.textLabel!.text = NSLocalizedString("Save Hashtag...", comment: "save hashtag nav item title")
|
||||
cell.accessoryType = .none
|
||||
|
||||
case let .savedInstance(url):
|
||||
cell.imageView!.image = UIImage(systemName: "globe")
|
||||
cell.textLabel!.text = url.host!
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
|
||||
case .findInstance:
|
||||
cell.imageView!.image = UIImage(systemName: "magnifyingglass")
|
||||
cell.textLabel!.text = NSLocalizedString("Find An Instance...", comment: "find instance nav item title")
|
||||
cell.accessoryType = .none
|
||||
if mastodonController.instance == nil {
|
||||
mastodonController.getOwnInstance(completion: self.ownInstanceLoaded(_:))
|
||||
}
|
||||
|
||||
return cell
|
||||
})
|
||||
dataSource.exploreController = self
|
||||
|
||||
let account = mastodonController.accountInfo!
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.bookmarks, .lists, .savedHashtags, .savedInstances])
|
||||
snapshot.appendItems([.bookmarks], toSection: .bookmarks)
|
||||
snapshot.appendItems([.addList], toSection: .lists)
|
||||
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
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
||||
resultsController.exploreNavigationController = self.navigationController!
|
||||
searchController = UISearchController(searchResultsController: resultsController)
|
||||
|
@ -109,8 +68,20 @@ class ExploreViewController: EnhancedTableViewController {
|
|||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(savedHashtagsChanged), name: .savedHashtagsChanged, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(savedInstancesChanged), name: .savedInstancesChanged, object: nil)
|
||||
}
|
||||
|
||||
reloadLists()
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
// Can't use UICollectionViewController's builtin version of this because it requires
|
||||
// the collection view layout be passed into the constructor. Swipe actions for list collection views
|
||||
// are created by passing a closure to the layout's configuration. This closure needs to capture
|
||||
// `self`, so it can't be passed into the super constructor.
|
||||
if let indexPaths = collectionView.indexPathsForSelectedItems {
|
||||
for indexPath in indexPaths {
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
|
@ -125,16 +96,82 @@ class ExploreViewController: EnhancedTableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
func reloadLists() {
|
||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { (headerView, collectionView, indexPath) in
|
||||
let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
|
||||
|
||||
var config = headerView.defaultContentConfiguration()
|
||||
config.text = section.label
|
||||
headerView.contentConfiguration = config
|
||||
}
|
||||
|
||||
let listCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { (cell, indexPath, item) in
|
||||
var config = cell.defaultContentConfiguration()
|
||||
config.text = item.label
|
||||
config.image = item.image
|
||||
cell.contentConfiguration = config
|
||||
|
||||
switch item {
|
||||
case .addList, .addSavedHashtag, .findInstance:
|
||||
cell.accessories = []
|
||||
default:
|
||||
cell.accessories = [.disclosureIndicator()]
|
||||
}
|
||||
}
|
||||
|
||||
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { (collectionView, indexPath, item) in
|
||||
return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: item)
|
||||
}
|
||||
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
|
||||
if elementKind == UICollectionView.elementKindSectionHeader {
|
||||
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return dataSource
|
||||
}
|
||||
|
||||
private func applyInitialSnapshot() {
|
||||
let account = mastodonController.accountInfo!
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections(Section.allCases.filter { $0 != .discover })
|
||||
snapshot.appendItems([.bookmarks], toSection: .bookmarks)
|
||||
if case .mastodon = mastodonController.instance?.instanceType {
|
||||
snapshot.insertSections([.discover], afterSection: .bookmarks)
|
||||
snapshot.appendItems([.trendingTags, .profileDirectory], toSection: .discover)
|
||||
}
|
||||
snapshot.appendItems([.addList], toSection: .lists)
|
||||
snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) }, toSection: .savedHashtags)
|
||||
snapshot.appendItems([.addSavedHashtag], toSection: .savedHashtags)
|
||||
snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) }, toSection: .savedInstances)
|
||||
snapshot.appendItems([.findInstance], toSection: .savedInstances)
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
reloadLists()
|
||||
}
|
||||
|
||||
private func ownInstanceLoaded(_ instance: Instance) {
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
if case .mastodon = instance.instanceType {
|
||||
snapshot.insertSections([.discover], afterSection: .bookmarks)
|
||||
snapshot.appendItems([.trendingTags, .profileDirectory], toSection: .discover)
|
||||
}
|
||||
self.dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
private func reloadLists() {
|
||||
let request = Client.getLists()
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(lists, _) = response else {
|
||||
fatalError()
|
||||
return
|
||||
}
|
||||
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .lists))
|
||||
snapshot.appendItems(lists.map { .list($0) } + [.addList], toSection: .lists)
|
||||
snapshot.appendItems(lists.map { .list($0) }, toSection: .lists)
|
||||
snapshot.appendItems([.addList], toSection: .lists)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.dataSource.apply(snapshot)
|
||||
|
@ -142,26 +179,31 @@ class ExploreViewController: EnhancedTableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
@objc func savedHashtagsChanged() {
|
||||
@objc private func savedHashtagsChanged() {
|
||||
let account = mastodonController.accountInfo!
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedHashtags))
|
||||
snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) } + [.addSavedHashtag], toSection: .savedHashtags)
|
||||
snapshot.appendItems(SavedDataManager.shared.sortedHashtags(for: account).map { .savedHashtag($0) }, toSection: .savedHashtags)
|
||||
snapshot.appendItems([.addSavedHashtag], toSection: .savedHashtags)
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
@objc func savedInstancesChanged() {
|
||||
@objc private func savedInstancesChanged() {
|
||||
let account = mastodonController.accountInfo!
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedInstances))
|
||||
snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) } + [.findInstance], toSection: .savedInstances)
|
||||
snapshot.appendItems(SavedDataManager.shared.savedInstances(for: account).map { .savedInstance($0) }, toSection: .savedInstances)
|
||||
snapshot.appendItems([.findInstance], toSection: .savedInstances)
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
func deleteList(_ list: List) {
|
||||
let title = String(format: NSLocalizedString("Are you sure want to delete the '%@' list?", comment: "delete list alert title"), list.title)
|
||||
private func deleteList(_ list: List, completion: @escaping (Bool) -> Void) {
|
||||
let titleFormat = NSLocalizedString("Are you sure you want to delete the '%@' list?", comment: "delete list alert title")
|
||||
let title = String(format: titleFormat, list.title)
|
||||
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "delete list alert cancel button"), style: .cancel, handler: nil))
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "delete list alert cancel button"), style: .cancel, handler: { (_) in
|
||||
completion(false)
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Delete List", comment: "delete list alert confirm button"), style: .destructive, handler: { (_) in
|
||||
|
||||
let request = List.delete(list)
|
||||
|
@ -174,6 +216,7 @@ class ExploreViewController: EnhancedTableViewController {
|
|||
snapshot.deleteItems([.list(list)])
|
||||
DispatchQueue.main.async {
|
||||
self.dataSource.apply(snapshot)
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
@ -190,9 +233,38 @@ class ExploreViewController: EnhancedTableViewController {
|
|||
SavedDataManager.shared.remove(instance: instanceURL, for: account)
|
||||
}
|
||||
|
||||
// MARK: - Table view delegate
|
||||
private func trailingSwipeActionsForCell(at indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let handler: UIContextualAction.Handler
|
||||
switch dataSource.itemIdentifier(for: indexPath) {
|
||||
case let .list(list):
|
||||
handler = { (_, _, completion) in
|
||||
self.deleteList(list, completion: completion)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
case let .savedHashtag(hashtag):
|
||||
handler = { (_, _, completion) in
|
||||
self.removeSavedHashtag(hashtag)
|
||||
completion(true)
|
||||
}
|
||||
|
||||
case let .savedInstance(url):
|
||||
handler = { (_, _, completion) in
|
||||
self.removeSavedInstance(url)
|
||||
completion(true)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
return UISwipeActionsConfiguration(actions: [
|
||||
UIContextualAction(style: .destructive, title: NSLocalizedString("Delete", comment: "delete swipe action title"), handler: handler)
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Collection View Delegate
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
switch dataSource.itemIdentifier(for: indexPath) {
|
||||
case nil:
|
||||
return
|
||||
|
@ -200,11 +272,17 @@ class ExploreViewController: EnhancedTableViewController {
|
|||
case .bookmarks:
|
||||
show(BookmarksTableViewController(mastodonController: mastodonController), sender: nil)
|
||||
|
||||
case .trendingTags:
|
||||
show(TrendingHashtagsViewController(mastodonController: mastodonController), sender: nil)
|
||||
|
||||
case .profileDirectory:
|
||||
show(ProfileDirectoryViewController(mastodonController: mastodonController), sender: nil)
|
||||
|
||||
case let .list(list):
|
||||
show(ListTimelineViewController(for: list, mastodonController: mastodonController), sender: nil)
|
||||
|
||||
case .addList:
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
let alert = UIAlertController(title: NSLocalizedString("New List", comment: "new list alert title"), message: NSLocalizedString("Choose a title for your new list", comment: "new list alert message"), preferredStyle: .alert)
|
||||
alert.addTextField(configurationHandler: nil)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "new list alert cancel button"), style: .cancel, handler: nil))
|
||||
|
@ -232,7 +310,7 @@ class ExploreViewController: EnhancedTableViewController {
|
|||
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
||||
|
||||
case .addSavedHashtag:
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
let navController = UINavigationController(rootViewController: AddSavedHashtagViewController(mastodonController: mastodonController))
|
||||
present(navController, animated: true)
|
||||
|
||||
|
@ -240,7 +318,7 @@ class ExploreViewController: EnhancedTableViewController {
|
|||
show(InstanceTimelineViewController(for: url, parentMastodonController: mastodonController), sender: nil)
|
||||
|
||||
case .findInstance:
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
let findController = FindInstanceViewController(parentMastodonController: mastodonController)
|
||||
findController.instanceTimelineDelegate = self
|
||||
let navController = UINavigationController(rootViewController: findController)
|
||||
|
@ -248,48 +326,36 @@ class ExploreViewController: EnhancedTableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
|
||||
return .delete
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
switch dataSource.itemIdentifier(for: indexPath) {
|
||||
case .bookmarks:
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: {
|
||||
return BookmarksTableViewController(mastodonController: self.mastodonController)
|
||||
}, actionProvider: nil)
|
||||
|
||||
case let .list(list):
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: {
|
||||
return ListTimelineViewController(for: list, mastodonController: self.mastodonController)
|
||||
}, actionProvider: nil)
|
||||
|
||||
case let .savedHashtag(hashtag):
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: {
|
||||
return HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
|
||||
}, actionProvider: nil)
|
||||
|
||||
case let .savedInstance(url):
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: {
|
||||
return InstanceTimelineViewController(for: url, parentMastodonController: self.mastodonController)
|
||||
}, actionProvider: nil)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ExploreViewController {
|
||||
enum Section: CaseIterable {
|
||||
case bookmarks
|
||||
case discover
|
||||
case lists
|
||||
case savedHashtags
|
||||
case savedInstances
|
||||
|
||||
var label: String? {
|
||||
switch self {
|
||||
case .bookmarks:
|
||||
return nil
|
||||
case .discover:
|
||||
return NSLocalizedString("Discover", comment: "discover section title")
|
||||
case .lists:
|
||||
return NSLocalizedString("Lists", comment: "explore lists section title")
|
||||
case .savedHashtags:
|
||||
return NSLocalizedString("Saved Hashtags", comment: "explore saved hashtags section title")
|
||||
case .savedInstances:
|
||||
return NSLocalizedString("Instance Timelines", comment: "explore instance timelines section title")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Item: Hashable {
|
||||
case bookmarks
|
||||
case trendingTags
|
||||
case profileDirectory
|
||||
case list(List)
|
||||
case addList
|
||||
case savedHashtag(Hashtag)
|
||||
|
@ -297,10 +363,60 @@ extension ExploreViewController {
|
|||
case savedInstance(URL)
|
||||
case findInstance
|
||||
|
||||
static func == (lhs: ExploreViewController.Item, rhs: ExploreViewController.Item) -> Bool {
|
||||
var label: String {
|
||||
switch self {
|
||||
case .bookmarks:
|
||||
return NSLocalizedString("Bookmarks", comment: "bookmarks nav item title")
|
||||
case .trendingTags:
|
||||
return NSLocalizedString("Trending Hashtags", comment: "trending hashtags nav item title")
|
||||
case .profileDirectory:
|
||||
return NSLocalizedString("Profile Directory", comment: "profile directory nav item title")
|
||||
case let .list(list):
|
||||
return list.title
|
||||
case .addList:
|
||||
return NSLocalizedString("New List...", comment: "new list nav item title")
|
||||
case let .savedHashtag(hashtag):
|
||||
return hashtag.name
|
||||
case .addSavedHashtag:
|
||||
return NSLocalizedString("Save Hashtag...", comment: "save hashtag nav item title")
|
||||
case let .savedInstance(url):
|
||||
return url.host!
|
||||
case .findInstance:
|
||||
return NSLocalizedString("Find An Instance...", comment: "find instance nav item title")
|
||||
}
|
||||
}
|
||||
|
||||
var image: UIImage {
|
||||
let name: String
|
||||
switch self {
|
||||
case .bookmarks:
|
||||
name = "bookmark.fill"
|
||||
case .trendingTags:
|
||||
name = "arrow.up.arrow.down"
|
||||
case .profileDirectory:
|
||||
name = "person.2.fill"
|
||||
case .list(_):
|
||||
name = "list.bullet"
|
||||
case .addList, .addSavedHashtag:
|
||||
name = "plus"
|
||||
case .savedHashtag(_):
|
||||
name = "number"
|
||||
case .savedInstance(_):
|
||||
name = "globe"
|
||||
case .findInstance:
|
||||
name = "magnifyingglass"
|
||||
}
|
||||
return UIImage(systemName: name)!
|
||||
}
|
||||
|
||||
static func == (lhs: Item, rhs: Item) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.bookmarks, .bookmarks):
|
||||
return true
|
||||
case (.trendingTags, .trendingTags):
|
||||
return true
|
||||
case (.profileDirectory, .profileDirectory):
|
||||
return true
|
||||
case let (.list(a), .list(b)):
|
||||
return a.id == b.id
|
||||
case (.addList, .addList):
|
||||
|
@ -317,10 +433,15 @@ extension ExploreViewController {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .bookmarks:
|
||||
hasher.combine("bookmarks")
|
||||
case .trendingTags:
|
||||
hasher.combine("trendingTags")
|
||||
case .profileDirectory:
|
||||
hasher.combine("profileDirectory")
|
||||
case let .list(list):
|
||||
hasher.combine("list")
|
||||
hasher.combine(list.id)
|
||||
|
@ -339,57 +460,6 @@ extension ExploreViewController {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
||||
|
||||
weak var exploreController: ExploreViewController?
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch section {
|
||||
case 1:
|
||||
return NSLocalizedString("Lists", comment: "explore lists section title")
|
||||
case 2:
|
||||
return NSLocalizedString("Saved Hashtags", comment: "explore saved hashtags section title")
|
||||
case 3:
|
||||
return NSLocalizedString("Instance Timelines", comment: "explore instance timelines section title")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
switch itemIdentifier(for: indexPath) {
|
||||
case .list(_):
|
||||
return true
|
||||
case .savedHashtag(_):
|
||||
return true
|
||||
case .savedInstance(_):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
|
||||
guard editingStyle == .delete,
|
||||
let exploreController = exploreController else {
|
||||
return
|
||||
}
|
||||
|
||||
switch itemIdentifier(for: indexPath) {
|
||||
case let .list(list):
|
||||
exploreController.deleteList(list)
|
||||
case let .savedHashtag(hashtag):
|
||||
exploreController.removeSavedHashtag(hashtag)
|
||||
case let .savedInstance(url):
|
||||
exploreController.removeSavedInstance(url)
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension ExploreViewController: InstanceTimelineViewControllerDelegate {
|
||||
|
@ -404,12 +474,13 @@ extension ExploreViewController: InstanceTimelineViewControllerDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
extension ExploreViewController {
|
||||
override func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||
extension ExploreViewController: UICollectionViewDragDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||
let accountID = mastodonController.accountInfo?.id else {
|
||||
return []
|
||||
}
|
||||
|
||||
let provider: NSItemProvider
|
||||
switch item {
|
||||
case .bookmarks:
|
||||
|
@ -425,11 +496,7 @@ extension ExploreViewController {
|
|||
case let .savedInstance(url):
|
||||
provider = NSItemProvider(object: url as NSURL)
|
||||
// todo: should dragging public timelines into new windows be supported?
|
||||
case .addList:
|
||||
return []
|
||||
case .addSavedHashtag:
|
||||
return []
|
||||
case .findInstance:
|
||||
case .trendingTags, .profileDirectory, .addList, .addSavedHashtag, .findInstance:
|
||||
return []
|
||||
}
|
||||
return [UIDragItem(itemProvider: provider)]
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
//
|
||||
// FeaturedProfileCollectionViewCell.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 2/6/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class FeaturedProfileCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
@IBOutlet weak var headerImageView: UIImageView!
|
||||
@IBOutlet weak var avatarContainerView: UIView!
|
||||
@IBOutlet weak var avatarImageView: UIImageView!
|
||||
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||
@IBOutlet weak var noteTextView: StatusContentTextView!
|
||||
|
||||
var account: Account?
|
||||
|
||||
private var avatarRequest: ImageCache.Request?
|
||||
private var headerRequest: ImageCache.Request?
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
|
||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
||||
|
||||
noteTextView.defaultFont = .systemFont(ofSize: 16)
|
||||
noteTextView.textContainer.lineBreakMode = .byTruncatingTail
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||
}
|
||||
|
||||
func updateUI(account: Account) {
|
||||
self.account = account
|
||||
|
||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||
|
||||
noteTextView.setTextFromHtml(account.note)
|
||||
noteTextView.setEmojis(account.emojis)
|
||||
|
||||
avatarImageView.image = nil
|
||||
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (_, image) in
|
||||
defer {
|
||||
self?.avatarRequest = nil
|
||||
}
|
||||
guard let self = self,
|
||||
let image = image,
|
||||
self.account?.id == account.id else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.avatarImageView.image = image
|
||||
}
|
||||
}
|
||||
|
||||
headerImageView.image = nil
|
||||
if let header = account.header {
|
||||
headerRequest = ImageCache.headers.get(header) { [weak self] (_, image) in
|
||||
defer {
|
||||
self?.headerRequest = nil
|
||||
}
|
||||
guard let self = self,
|
||||
let image = image,
|
||||
self.account?.id == account.id else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.headerImageView.image = image
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func preferencesChanged() {
|
||||
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
|
||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
||||
|
||||
if let account = account {
|
||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18121" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18091"/>
|
||||
<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"/>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="gTV-IL-0wX" customClass="FeaturedProfileCollectionViewCell" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="400" height="200"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||
<rect key="frame" x="0.0" y="0.0" width="400" height="200"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="bo4-Sd-caI">
|
||||
<rect key="frame" x="0.0" y="0.0" width="400" height="66"/>
|
||||
<color key="backgroundColor" systemColor="systemGray5Color"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="66" id="9Aa-Up-chJ"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RQe-uE-TEv">
|
||||
<rect key="frame" x="8" y="34" width="64" height="64"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="4wd-wq-Sh2">
|
||||
<rect key="frame" x="2" y="2" width="60" height="60"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="60" id="Xyl-Ry-J3r"/>
|
||||
<constraint firstAttribute="width" secondItem="4wd-wq-Sh2" secondAttribute="height" multiplier="1:1" id="YEc-fT-FRB"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="RQe-uE-TEv" secondAttribute="height" multiplier="1:1" id="4vR-IF-yS8"/>
|
||||
<constraint firstAttribute="width" secondItem="4wd-wq-Sh2" secondAttribute="width" constant="4" id="52Q-zq-k28"/>
|
||||
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerY" secondItem="RQe-uE-TEv" secondAttribute="centerY" id="Ped-H7-QtP"/>
|
||||
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerX" secondItem="RQe-uE-TEv" secondAttribute="centerX" id="bRk-uJ-JGg"/>
|
||||
</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" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="voW-Is-1b2" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="76" y="72" width="316" height="24"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bvj-F0-ggC" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="8" y="102" width="384" height="94"/>
|
||||
<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="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
</view>
|
||||
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="bvj-F0-ggC" firstAttribute="top" secondItem="RQe-uE-TEv" secondAttribute="bottom" constant="4" id="8Nc-FF-kRX"/>
|
||||
<constraint firstItem="bo4-Sd-caI" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="CJ1-Be-L45"/>
|
||||
<constraint firstAttribute="bottom" secondItem="bvj-F0-ggC" secondAttribute="bottom" constant="4" id="Hza-qE-Agk"/>
|
||||
<constraint firstItem="voW-Is-1b2" firstAttribute="bottom" secondItem="4wd-wq-Sh2" secondAttribute="bottom" id="N0l-fE-AAX"/>
|
||||
<constraint firstItem="RQe-uE-TEv" firstAttribute="centerY" secondItem="bo4-Sd-caI" secondAttribute="bottom" id="Ngh-DO-Q0X"/>
|
||||
<constraint firstItem="bvj-F0-ggC" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" constant="8" id="Rjq-1i-PV2"/>
|
||||
<constraint firstItem="voW-Is-1b2" firstAttribute="leading" secondItem="RQe-uE-TEv" secondAttribute="trailing" constant="4" id="WUb-3i-BFe"/>
|
||||
<constraint firstAttribute="trailing" secondItem="bvj-F0-ggC" secondAttribute="trailing" constant="8" id="ZrT-Wa-pbY"/>
|
||||
<constraint firstItem="voW-Is-1b2" firstAttribute="top" relation="greaterThanOrEqual" secondItem="bo4-Sd-caI" secondAttribute="bottom" id="g4l-yF-2wH"/>
|
||||
<constraint firstAttribute="trailing" secondItem="bo4-Sd-caI" secondAttribute="trailing" id="geb-Qa-zZp"/>
|
||||
<constraint firstAttribute="trailing" secondItem="voW-Is-1b2" secondAttribute="trailing" constant="8" id="l91-F6-kAL"/>
|
||||
<constraint firstItem="bo4-Sd-caI" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="tUr-Oy-nXN"/>
|
||||
<constraint firstItem="RQe-uE-TEv" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" constant="8" id="uZI-LM-bZW"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="avatarContainerView" destination="RQe-uE-TEv" id="tBI-fT-26P"/>
|
||||
<outlet property="avatarImageView" destination="4wd-wq-Sh2" id="rba-cv-8fb"/>
|
||||
<outlet property="displayNameLabel" destination="voW-Is-1b2" id="XVS-4d-PKx"/>
|
||||
<outlet property="headerImageView" destination="bo4-Sd-caI" id="YkL-Wi-BXb"/>
|
||||
<outlet property="noteTextView" destination="bvj-F0-ggC" id="Bbm-ai-bu1"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="535" y="428"/>
|
||||
</collectionViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="labelColor">
|
||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemGray5Color">
|
||||
<color red="0.89803921568627454" green="0.89803921568627454" blue="0.91764705882352937" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// HashtagSearchResultsViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/24/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class HashtagSearchResultsViewController: SearchResultsViewController {
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
super.init(mastodonController: mastodonController, resultTypes: [.hashtags])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func performSearch(query: String?) {
|
||||
if let query = query, !query.starts(with: "#") {
|
||||
super.performSearch(query: "#\(query)")
|
||||
} else {
|
||||
super.performSearch(query: query)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
//
|
||||
// ProfileDirectoryFilterView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 2/7/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class ProfileDirectoryFilterView: UICollectionReusableView {
|
||||
|
||||
var onFilterChanged: ((Scope, DirectoryOrder) -> Void)?
|
||||
|
||||
private var scope: UISegmentedControl!
|
||||
private var sort: UISegmentedControl!
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
|
||||
commonInit()
|
||||
}
|
||||
|
||||
private func commonInit() {
|
||||
scope = UISegmentedControl(items: ["Instance", NSLocalizedString("Everywhere", comment: "everywhere profile directory scope")])
|
||||
scope.selectedSegmentIndex = 0
|
||||
scope.addTarget(self, action: #selector(filterChanged), for: .valueChanged)
|
||||
|
||||
sort = UISegmentedControl(items: [
|
||||
NSLocalizedString("Active", comment: "active profile directory sort"),
|
||||
NSLocalizedString("New", comment: "new profile directory sort"),
|
||||
])
|
||||
sort.selectedSegmentIndex = 0
|
||||
sort.addTarget(self, action: #selector(filterChanged), for: .valueChanged)
|
||||
|
||||
let fromLabel = UILabel()
|
||||
fromLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
fromLabel.text = NSLocalizedString("From", comment: "profile directory scope label")
|
||||
fromLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
|
||||
let sortLabel = UILabel()
|
||||
sortLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
sortLabel.text = NSLocalizedString("Sort By", comment: "profile directory sort label")
|
||||
sortLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
|
||||
let labelContainer = UIView()
|
||||
labelContainer.addSubview(sortLabel)
|
||||
labelContainer.addSubview(fromLabel)
|
||||
|
||||
let controlStack = UIStackView(arrangedSubviews: [sort, scope])
|
||||
controlStack.axis = .vertical
|
||||
controlStack.spacing = 8
|
||||
|
||||
let blurEffect = UIBlurEffect(style: .systemChromeMaterial)
|
||||
let blurView = UIVisualEffectView(effect: blurEffect)
|
||||
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(blurView)
|
||||
|
||||
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect, style: .label)
|
||||
let vibrancyView = UIVisualEffectView(effect: vibrancyEffect)
|
||||
vibrancyView.translatesAutoresizingMaskIntoConstraints = false
|
||||
blurView.contentView.addSubview(vibrancyView)
|
||||
|
||||
let filterStack = UIStackView(arrangedSubviews: [
|
||||
labelContainer,
|
||||
controlStack,
|
||||
])
|
||||
filterStack.axis = .horizontal
|
||||
filterStack.spacing = 8
|
||||
filterStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
vibrancyView.contentView.addSubview(filterStack)
|
||||
|
||||
let separator = UIView()
|
||||
separator.backgroundColor = .separator
|
||||
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(separator)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
fromLabel.leadingAnchor.constraint(equalTo: labelContainer.leadingAnchor),
|
||||
fromLabel.trailingAnchor.constraint(equalTo: labelContainer.trailingAnchor),
|
||||
fromLabel.centerYAnchor.constraint(equalTo: scope.centerYAnchor),
|
||||
|
||||
sortLabel.leadingAnchor.constraint(equalTo: labelContainer.leadingAnchor),
|
||||
sortLabel.trailingAnchor.constraint(equalTo: labelContainer.trailingAnchor),
|
||||
sortLabel.centerYAnchor.constraint(equalTo: sort.centerYAnchor),
|
||||
|
||||
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
blurView.topAnchor.constraint(equalTo: topAnchor),
|
||||
blurView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
|
||||
vibrancyView.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor),
|
||||
vibrancyView.trailingAnchor.constraint(equalTo: blurView.contentView.trailingAnchor),
|
||||
vibrancyView.topAnchor.constraint(equalTo: blurView.contentView.topAnchor),
|
||||
vibrancyView.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor),
|
||||
|
||||
filterStack.leadingAnchor.constraint(equalToSystemSpacingAfter: vibrancyView.contentView.leadingAnchor, multiplier: 1),
|
||||
vibrancyView.contentView.trailingAnchor.constraint(equalToSystemSpacingAfter: filterStack.trailingAnchor, multiplier: 1),
|
||||
filterStack.topAnchor.constraint(equalToSystemSpacingBelow: vibrancyView.contentView.topAnchor, multiplier: 1),
|
||||
vibrancyView.contentView.bottomAnchor.constraint(equalToSystemSpacingBelow: filterStack.bottomAnchor, multiplier: 1),
|
||||
|
||||
separator.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
separator.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
separator.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
separator.heightAnchor.constraint(equalToConstant: 0.5),
|
||||
])
|
||||
}
|
||||
|
||||
func updateUI(mastodonController: MastodonController) {
|
||||
scope.setTitle(mastodonController.accountInfo!.instanceURL.host!, forSegmentAt: 0)
|
||||
}
|
||||
|
||||
@objc private func filterChanged() {
|
||||
let scope = Scope(rawValue: scope.selectedSegmentIndex)!
|
||||
let order = sort.selectedSegmentIndex == 0 ? DirectoryOrder.active : .new
|
||||
onFilterChanged?(scope, order)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ProfileDirectoryFilterView {
|
||||
enum Scope: Int, Equatable {
|
||||
case instance, everywhere
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
//
|
||||
// ProfileDirectoryViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 2/6/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class ProfileDirectoryViewController: UIViewController {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
private var collectionView: UICollectionView!
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
|
||||
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()
|
||||
|
||||
title = NSLocalizedString("Profile Directory", comment: "profile directory title")
|
||||
|
||||
let configuration = UICollectionViewCompositionalLayoutConfiguration()
|
||||
configuration.boundarySupplementaryItems = [
|
||||
NSCollectionLayoutBoundarySupplementaryItem(
|
||||
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100)),
|
||||
elementKind: "filter",
|
||||
alignment: .top
|
||||
)
|
||||
]
|
||||
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) in
|
||||
let itemHeight = NSCollectionLayoutDimension.absolute(200)
|
||||
let itemWidth: NSCollectionLayoutDimension
|
||||
if case .compact = layoutEnvironment.traitCollection.horizontalSizeClass {
|
||||
itemWidth = .fractionalWidth(1)
|
||||
} else {
|
||||
itemWidth = .absolute((layoutEnvironment.container.contentSize.width - 12) / 2)
|
||||
}
|
||||
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: itemWidth, heightDimension: itemHeight)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
let itemB = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: itemHeight)
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item, itemB])
|
||||
group.interItemSpacing = .flexible(4)
|
||||
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.interGroupSpacing = 4
|
||||
if case .compact = layoutEnvironment.traitCollection.horizontalSizeClass {
|
||||
section.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0)
|
||||
} else {
|
||||
section.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
|
||||
}
|
||||
return section
|
||||
}, configuration: configuration)
|
||||
|
||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
collectionView.backgroundColor = .secondarySystemBackground
|
||||
collectionView.register(UINib(nibName: "FeaturedProfileCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "featuredProfileCell")
|
||||
collectionView.register(ProfileDirectoryFilterView.self, forSupplementaryViewOfKind: "filter", withReuseIdentifier: "filter")
|
||||
collectionView.delegate = self
|
||||
collectionView.dragDelegate = self
|
||||
view.addSubview(collectionView)
|
||||
|
||||
dataSource = createDataSource()
|
||||
updateProfiles(local: true, order: .active)
|
||||
}
|
||||
|
||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { (collectionView, indexPath, item) in
|
||||
guard case let .account(account) = item else { fatalError() }
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "featuredProfileCell", for: indexPath) as! FeaturedProfileCollectionViewCell
|
||||
cell.updateUI(account: account)
|
||||
return cell
|
||||
}
|
||||
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
|
||||
guard elementKind == "filter" else {
|
||||
return nil
|
||||
}
|
||||
let filterView = collectionView.dequeueReusableSupplementaryView(ofKind: "filter", withReuseIdentifier: "filter", for: indexPath) as! ProfileDirectoryFilterView
|
||||
filterView.updateUI(mastodonController: self.mastodonController)
|
||||
filterView.onFilterChanged = { [weak self] (scope, order) in
|
||||
guard let self = self else { return }
|
||||
self.dataSource.apply(.init())
|
||||
self.updateProfiles(local: scope == .instance, order: order)
|
||||
}
|
||||
return filterView
|
||||
}
|
||||
return dataSource
|
||||
}
|
||||
|
||||
private func updateProfiles(local: Bool, order: DirectoryOrder) {
|
||||
let request = Client.getFeaturedProfiles(local: local, order: order)
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(accounts, _) = response else {
|
||||
return
|
||||
}
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.featuredProfiles])
|
||||
snapshot.appendItems(accounts.map { .account($0) })
|
||||
DispatchQueue.main.async {
|
||||
self.dataSource.apply(snapshot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ProfileDirectoryViewController {
|
||||
enum Section {
|
||||
case featuredProfiles
|
||||
}
|
||||
enum Item: Hashable {
|
||||
case account(Account)
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
guard case let .account(account) = self else { return }
|
||||
hasher.combine(account.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileDirectoryViewController: TuskerNavigationDelegate {
|
||||
var apiController: MastodonController { mastodonController }
|
||||
}
|
||||
|
||||
extension ProfileDirectoryViewController: MenuPreviewProvider {
|
||||
var navigationDelegate: TuskerNavigationDelegate? { self }
|
||||
}
|
||||
|
||||
extension ProfileDirectoryViewController: UICollectionViewDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||
case let .account(account) = item else {
|
||||
return
|
||||
}
|
||||
show(ProfileViewController(accountID: account.id, mastodonController: mastodonController), sender: nil)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||
case let .account(account) = item else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return UIContextMenuConfiguration(identifier: nil) {
|
||||
return ProfileViewController(accountID: account.id, mastodonController: self.mastodonController)
|
||||
} actionProvider: { (_) in
|
||||
let actions = self.actionsForProfile(accountID: account.id, sourceView: self.collectionView.cellForItem(at: indexPath))
|
||||
return UIMenu(children: actions)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||
if let viewController = animator.previewViewController {
|
||||
animator.preferredCommitStyle = .pop
|
||||
animator.addCompletion {
|
||||
self.show(viewController, sender: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileDirectoryViewController: UICollectionViewDragDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||
case let .account(account) = item,
|
||||
let currentAccountID = mastodonController.accountInfo?.id else {
|
||||
return []
|
||||
}
|
||||
let provider = NSItemProvider(object: account.url as NSURL)
|
||||
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
|
||||
provider.registerObject(activity, visibility: .all)
|
||||
return [UIDragItem(itemProvider: provider)]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
//
|
||||
// TrendingHashtagsViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 2/6/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class TrendingHashtagsViewController: EnhancedTableViewController {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
private var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(style: .grouped)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = NSLocalizedString("Trending Hashtags", comment: "trending hashtags screen title")
|
||||
|
||||
tableView.register(UINib(nibName: "TrendingHashtagTableViewCell", bundle: .main), forCellReuseIdentifier: "trendingTagCell")
|
||||
tableView.rowHeight = 60 // 44 for content + 2 * 8 spacing
|
||||
|
||||
// todo: enable drag
|
||||
|
||||
dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: tableView) { (tableView, indexPath, item) in
|
||||
switch item {
|
||||
case let .tag(hashtag):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "trendingTagCell", for: indexPath) as! TrendingHashtagTableViewCell
|
||||
cell.updateUI(hashtag: hashtag)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
let request = Client.getTrends(limit: 10)
|
||||
mastodonController.run(request) { (response) in
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
|
||||
guard case let .success(hashtags, _) = response,
|
||||
hashtags.count > 0 else {
|
||||
self.dataSource.apply(snapshot)
|
||||
return
|
||||
}
|
||||
|
||||
snapshot.appendSections([.trendingTags])
|
||||
snapshot.appendItems(hashtags.map { .tag($0) })
|
||||
self.dataSource.apply(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Table View Delegate
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||
case let .tag(hashtag) = item else {
|
||||
return
|
||||
}
|
||||
|
||||
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TrendingHashtagsViewController {
|
||||
enum Section {
|
||||
case trendingTags
|
||||
}
|
||||
enum Item: Hashable {
|
||||
case tag(Hashtag)
|
||||
}
|
||||
}
|
|
@ -54,7 +54,7 @@ class FastAccountSwitcherViewController: UIViewController {
|
|||
|
||||
view.isHidden = false
|
||||
|
||||
if UIAccessibility.prefersCrossFadeTransitionsBackwardsCompat {
|
||||
if UIAccessibility.prefersCrossFadeTransitions {
|
||||
view.alpha = 0
|
||||
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
|
||||
self.view.alpha = 1
|
||||
|
|
|
@ -87,8 +87,8 @@ class FastSwitchingAccountView: UIView {
|
|||
let controller = MastodonController.getForAccount(account)
|
||||
controller.getOwnAccount { [weak self] (result) in
|
||||
guard let self = self, case let .success(account) = result else { return }
|
||||
self.avatarRequest = ImageCache.avatars.get(account.avatar) { [weak avatarImageView] (data) in
|
||||
guard let avatarImageView = avatarImageView, let data = data, let image = UIImage(data: data) else { return }
|
||||
self.avatarRequest = ImageCache.avatars.get(account.avatar) { [weak avatarImageView] (_, image) in
|
||||
guard let avatarImageView = avatarImageView, let image = image else { return }
|
||||
DispatchQueue.main.async {
|
||||
avatarImageView.image = image
|
||||
}
|
||||
|
|
|
@ -85,19 +85,20 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
overrideUserInterfaceStyle = .dark
|
||||
view.backgroundColor = .black
|
||||
|
||||
if let data = cache.get(url) {
|
||||
createLargeImage(data: data, url: url)
|
||||
// always load full resolution from disk for large image, in case the cache is scaled
|
||||
if let entry = cache.get(url, loadOriginal: true) {
|
||||
createLargeImage(data: entry.data, image: entry.image, url: url)
|
||||
} else {
|
||||
createPreview()
|
||||
|
||||
loadingVC = LoadingViewController()
|
||||
embedChild(loadingVC!)
|
||||
imageRequest = cache.get(url) { [weak self] (data) in
|
||||
imageRequest = cache.get(url, loadOriginal: true) { [weak self] (data, image) in
|
||||
guard let self = self else { return }
|
||||
self.imageRequest = nil
|
||||
DispatchQueue.main.async {
|
||||
self.loadingVC?.removeViewAndController()
|
||||
self.createLargeImage(data: data!, url: self.url)
|
||||
self.createLargeImage(data: data!, image: image!, url: self.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -115,20 +116,13 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
}
|
||||
}
|
||||
|
||||
private func createLargeImage(data: Data, url: URL) {
|
||||
private func createLargeImage(data: Data?, image: UIImage, url: URL) {
|
||||
guard !loaded else { return }
|
||||
loaded = true
|
||||
|
||||
let image: UIImage?
|
||||
if Preferences.shared.grayscaleImages {
|
||||
image = ImageGrayscalifier.convert(url: url, data: data)
|
||||
} else {
|
||||
image = UIImage(data: data)
|
||||
}
|
||||
|
||||
if let image = image {
|
||||
if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) {
|
||||
let gifData = url.pathExtension == "gif" ? data : nil
|
||||
createLargeImage(image: image, gifData: gifData)
|
||||
createLargeImage(image: transformedImage, gifData: gifData)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ extension LargeImageAnimatableViewController {
|
|||
class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
|
||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
if UIAccessibility.prefersCrossFadeTransitionsBackwardsCompat {
|
||||
if UIAccessibility.prefersCrossFadeTransitions {
|
||||
return 0.2
|
||||
} else {
|
||||
return 0.4
|
||||
|
@ -51,7 +51,7 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
|
|||
return
|
||||
}
|
||||
|
||||
if UIAccessibility.prefersCrossFadeTransitionsBackwardsCompat {
|
||||
if UIAccessibility.prefersCrossFadeTransitions {
|
||||
animateCrossFadeTransition(using: transitionContext)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
|
|||
return
|
||||
}
|
||||
|
||||
if UIAccessibility.prefersCrossFadeTransitionsBackwardsCompat && !transitionContext.isInteractive {
|
||||
if UIAccessibility.prefersCrossFadeTransitions && !transitionContext.isInteractive {
|
||||
animateCrossFadeTransition(using: transitionContext)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ class EditListAccountsViewController: EnhancedTableViewController {
|
|||
guard case let .account(id) = item else { fatalError() }
|
||||
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "accountCell", for: indexPath) as! AccountTableViewCell
|
||||
cell.delegate = self
|
||||
cell.updateUI(accountID: id)
|
||||
return cell
|
||||
})
|
||||
|
@ -171,3 +172,7 @@ extension EditListAccountsViewController: SearchResultsViewControllerDelegate {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension EditListAccountsViewController: TuskerNavigationDelegate {
|
||||
var apiController: MastodonController { mastodonController }
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ class AccountSwitchingContainerViewController: UIViewController {
|
|||
embedChild(newRoot)
|
||||
|
||||
if direction != .none {
|
||||
if UIAccessibility.prefersCrossFadeTransitionsBackwardsCompat {
|
||||
if UIAccessibility.prefersCrossFadeTransitions {
|
||||
newRoot.view.alpha = 0
|
||||
|
||||
UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseInOut) {
|
||||
|
|
|
@ -9,13 +9,11 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
@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!
|
||||
|
@ -34,7 +32,7 @@ class MainSidebarViewController: UIViewController {
|
|||
}
|
||||
|
||||
var exploreTabItems: [Item] {
|
||||
var items: [Item] = [.search, .bookmarks]
|
||||
var items: [Item] = [.search, .bookmarks, .trendingTags, .profileDirectory]
|
||||
let snapshot = dataSource.snapshot()
|
||||
for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) {
|
||||
items.append(.list(list))
|
||||
|
@ -88,6 +86,10 @@ class MainSidebarViewController: UIViewController {
|
|||
|
||||
applyInitialSnapshot()
|
||||
|
||||
if mastodonController.instance == nil {
|
||||
mastodonController.getOwnInstance(completion: self.ownInstanceLoaded(_:))
|
||||
}
|
||||
|
||||
select(item: .tab(.timelines), animated: false)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil)
|
||||
|
@ -95,6 +97,8 @@ class MainSidebarViewController: UIViewController {
|
|||
}
|
||||
|
||||
func select(item: Item, animated: Bool) {
|
||||
// ensure view is loaded, since dataSource is created in viewDidLoad
|
||||
loadViewIfNeeded()
|
||||
guard let indexPath = dataSource.indexPath(for: item) else { return }
|
||||
collectionView.selectItem(at: indexPath, animated: animated, scrollPosition: .top)
|
||||
itemLastSelectedTimestamps[item] = Date()
|
||||
|
@ -130,7 +134,7 @@ class MainSidebarViewController: UIViewController {
|
|||
|
||||
private func applyInitialSnapshot() {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections(Section.allCases)
|
||||
snapshot.appendSections(Section.allCases.filter { $0 != .discover })
|
||||
snapshot.appendItems([
|
||||
.tab(.timelines),
|
||||
.tab(.notifications),
|
||||
|
@ -141,6 +145,13 @@ class MainSidebarViewController: UIViewController {
|
|||
snapshot.appendItems([
|
||||
.tab(.compose)
|
||||
], toSection: .compose)
|
||||
if case .mastodon = mastodonController.instance?.instanceType {
|
||||
snapshot.insertSections([.discover], afterSection: .compose)
|
||||
snapshot.appendItems([
|
||||
.trendingTags,
|
||||
.profileDirectory,
|
||||
], toSection: .discover)
|
||||
}
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
reloadLists()
|
||||
|
@ -148,6 +159,18 @@ class MainSidebarViewController: UIViewController {
|
|||
reloadSavedInstances()
|
||||
}
|
||||
|
||||
private func ownInstanceLoaded(_ instance: Instance) {
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
if case .mastodon = mastodonController.instance?.instanceType {
|
||||
snapshot.insertSections([.discover], afterSection: .compose)
|
||||
snapshot.appendItems([
|
||||
.trendingTags,
|
||||
.profileDirectory,
|
||||
], toSection: .discover)
|
||||
}
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
private func reloadLists() {
|
||||
let request = Client.getLists()
|
||||
mastodonController.run(request) { [weak self] (response) in
|
||||
|
@ -159,29 +182,47 @@ class MainSidebarViewController: UIViewController {
|
|||
exploreSnapshot.append(lists.map { .list($0) }, to: .listsHeader)
|
||||
exploreSnapshot.append([.addList], to: .listsHeader)
|
||||
DispatchQueue.main.async {
|
||||
self.dataSource.apply(exploreSnapshot, to: .lists)
|
||||
let selected = self.collectionView.indexPathsForSelectedItems?.first
|
||||
|
||||
self.dataSource.apply(exploreSnapshot, to: .lists) {
|
||||
if let selected = selected {
|
||||
self.collectionView.selectItem(at: selected, animated: false, scrollPosition: .centeredVertically)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func reloadSavedHashtags() {
|
||||
let selected = collectionView.indexPathsForSelectedItems?.first
|
||||
|
||||
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)
|
||||
self.dataSource.apply(hashtagsSnapshot, to: .savedHashtags, animatingDifferences: false) {
|
||||
if let selected = selected {
|
||||
self.collectionView.selectItem(at: selected, animated: false, scrollPosition: .centeredVertically)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func reloadSavedInstances() {
|
||||
let selected = collectionView.indexPathsForSelectedItems?.first
|
||||
|
||||
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)
|
||||
self.dataSource.apply(instancesSnapshot, to: .savedInstances, animatingDifferences: false) {
|
||||
if let selected = selected {
|
||||
self.collectionView.selectItem(at: selected, animated: false, scrollPosition: .centeredVertically)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo: deduplicate with ExploreViewController
|
||||
|
@ -250,11 +291,11 @@ class MainSidebarViewController: UIViewController {
|
|||
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MainSidebarViewController {
|
||||
enum Section: Int, Hashable, CaseIterable {
|
||||
case tabs
|
||||
case compose
|
||||
case discover
|
||||
case lists
|
||||
case savedHashtags
|
||||
case savedInstances
|
||||
|
@ -262,6 +303,7 @@ extension MainSidebarViewController {
|
|||
enum Item: Hashable {
|
||||
case tab(MainTabBarViewController.Tab)
|
||||
case search, bookmarks
|
||||
case trendingTags, profileDirectory
|
||||
case listsHeader, list(List), addList
|
||||
case savedHashtagsHeader, savedHashtag(Hashtag), addSavedHashtag
|
||||
case savedInstancesHeader, savedInstance(URL), addSavedInstance
|
||||
|
@ -274,6 +316,10 @@ extension MainSidebarViewController {
|
|||
return "Search"
|
||||
case .bookmarks:
|
||||
return "Bookmarks"
|
||||
case .trendingTags:
|
||||
return "Trending Hashtags"
|
||||
case .profileDirectory:
|
||||
return "Profile Directory"
|
||||
case .listsHeader:
|
||||
return "Lists"
|
||||
case let .list(list):
|
||||
|
@ -303,6 +349,10 @@ extension MainSidebarViewController {
|
|||
return "magnifyingglass"
|
||||
case .bookmarks:
|
||||
return "bookmark"
|
||||
case .trendingTags:
|
||||
return "arrow.up.arrow.down"
|
||||
case .profileDirectory:
|
||||
return "person.2.fill"
|
||||
case .list(_):
|
||||
return "list.bullet"
|
||||
case .savedHashtag(_):
|
||||
|
@ -360,7 +410,6 @@ fileprivate extension MainTabBarViewController.Tab {
|
|||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MainSidebarViewController: UICollectionViewDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||
previouslySelectedItem = selectedItem
|
||||
|
@ -395,7 +444,6 @@ extension MainSidebarViewController: UICollectionViewDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MainSidebarViewController: UICollectionViewDragDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||
|
@ -408,7 +456,6 @@ extension MainSidebarViewController: UICollectionViewDragDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MainSidebarViewController: InstanceTimelineViewControllerDelegate {
|
||||
func didSaveInstance(url: URL) {
|
||||
dismiss(animated: true) {
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class MainSplitViewController: UISplitViewController {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
@ -103,7 +102,6 @@ class MainSplitViewController: UISplitViewController {
|
|||
|
||||
}
|
||||
|
||||
@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.
|
||||
|
@ -205,7 +203,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
|||
|
||||
tabBarViewController.select(tab: .explore)
|
||||
|
||||
case .bookmarks, .list(_), .savedHashtag(_), .savedInstance(_):
|
||||
case .bookmarks, .trendingTags, .profileDirectory, .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.
|
||||
|
@ -279,6 +277,10 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
|||
exploreItem = .savedHashtag(hashtagVC.hashtag)
|
||||
} else if let instanceVC = tabNavigationStack[1] as? InstanceTimelineViewController {
|
||||
exploreItem = .savedInstance(instanceVC.instanceURL)
|
||||
} else if tabNavigationStack[1] is TrendingHashtagsViewController {
|
||||
exploreItem = .trendingTags
|
||||
} else if tabNavigationStack[1] is ProfileDirectoryViewController {
|
||||
exploreItem = .profileDirectory
|
||||
}
|
||||
transferNavigationStack(from: tabNavController, to: exploreItem!, skipFirst: 1, prepend: toPrepend)
|
||||
|
||||
|
@ -307,7 +309,6 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
||||
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController) {
|
||||
presentCompose()
|
||||
|
@ -322,7 +323,6 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
fileprivate extension MainSidebarViewController.Item {
|
||||
func createRootViewController(_ mastodonController: MastodonController) -> UIViewController? {
|
||||
switch self {
|
||||
|
@ -332,19 +332,22 @@ fileprivate extension MainSidebarViewController.Item {
|
|||
return SearchViewController(mastodonController: mastodonController)
|
||||
case .bookmarks:
|
||||
return BookmarksTableViewController(mastodonController: mastodonController)
|
||||
case .trendingTags:
|
||||
return TrendingHashtagsViewController(mastodonController: mastodonController)
|
||||
case .profileDirectory:
|
||||
return ProfileDirectoryViewController(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:
|
||||
case .listsHeader, .addList, .savedHashtagsHeader, .addSavedHashtag, .savedInstancesHeader, .addSavedInstance:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MainSplitViewController: TuskerRootViewController {
|
||||
@objc func presentCompose() {
|
||||
let vc = ComposeHostingController(mastodonController: mastodonController)
|
||||
|
@ -381,7 +384,6 @@ extension MainSplitViewController: TuskerRootViewController {
|
|||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MainSplitViewController: BackgroundableViewController {
|
||||
func sceneDidEnterBackground() {
|
||||
if traitCollection.horizontalSizeClass == .compact {
|
||||
|
|
|
@ -98,7 +98,9 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
|||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(newNotifications, pagination) = response else { fatalError() }
|
||||
|
||||
self.newer = pagination?.newer
|
||||
if let newer = pagination?.newer {
|
||||
self.newer = newer
|
||||
}
|
||||
|
||||
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||
|
||||
|
@ -211,7 +213,7 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
|||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
for notification in item(for: indexPath).notifications {
|
||||
_ = ImageCache.avatars.get(notification.account.avatar, completion: nil)
|
||||
ImageCache.avatars.fetchIfNotCached(notification.account.avatar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ struct AdvancedPrefsView : View {
|
|||
automationSection
|
||||
cachingSection
|
||||
}
|
||||
.insetOrGroupedListStyle()
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
.navigationBarTitle(Text("Advanced"))
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ struct AppearancePrefsView : View {
|
|||
accountsSection
|
||||
postsSection
|
||||
}
|
||||
.insetOrGroupedListStyle()
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
.navigationBarTitle(Text("Appearance"))
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ struct BehaviorPrefsView: View {
|
|||
linksSection
|
||||
contentWarningsSection
|
||||
}
|
||||
.insetOrGroupedListStyle()
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
.navigationBarTitle(Text("Behavior"))
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ struct ComposingPrefsView: View {
|
|||
composingSection
|
||||
replyingSection
|
||||
}
|
||||
.insetOrGroupedListStyle()
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
.navigationBarTitle("Composing")
|
||||
}
|
||||
|
||||
|
|
|
@ -38,8 +38,7 @@ struct LocalAccountAvatarView: View {
|
|||
let controller = MastodonController.getForAccount(localAccountInfo)
|
||||
controller.getOwnAccount { (result) in
|
||||
guard case let .success(account) = result else { return }
|
||||
_ = ImageCache.avatars.get(account.avatar) { (data) in
|
||||
if let data = data, let image = UIImage(data: data) {
|
||||
_ = ImageCache.avatars.get(account.avatar) { (_, image) in
|
||||
DispatchQueue.main.async {
|
||||
self.avatarImage = image
|
||||
}
|
||||
|
@ -47,7 +46,6 @@ struct LocalAccountAvatarView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//struct LocalAccountAvatarView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
|
|
|
@ -15,7 +15,7 @@ struct MediaPrefsView: View {
|
|||
List {
|
||||
viewingSection
|
||||
}
|
||||
.insetOrGroupedListStyle()
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
.navigationBarTitle("Media")
|
||||
}
|
||||
|
||||
|
|
|
@ -89,7 +89,7 @@ struct PreferencesView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.insetOrGroupedListStyle()
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
.navigationBarTitle(Text("Preferences"), displayMode: .inline)
|
||||
// }
|
||||
}
|
||||
|
@ -99,17 +99,6 @@ struct PreferencesView: View {
|
|||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
@ViewBuilder
|
||||
func insetOrGroupedListStyle() -> some View {
|
||||
if #available(iOS 14.0, *) {
|
||||
self.listStyle(InsetGroupedListStyle())
|
||||
} else {
|
||||
self.listStyle(GroupedListStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct PreferencesView_Previews : PreviewProvider {
|
||||
static var previews: some View {
|
||||
|
|
|
@ -14,7 +14,7 @@ struct SilentActionPrefs : View {
|
|||
List(Array(preferences.silentActions.keys), id: \.self) { source in
|
||||
SilentActionPermissionCell(source: source)
|
||||
}
|
||||
.insetOrGroupedListStyle()
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
// .navigationBarTitle("Silent Action Permissions")
|
||||
// see FB6838291
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ struct WellnessPrefsView: View {
|
|||
notificationsMode
|
||||
grayscaleImages
|
||||
}
|
||||
.insetOrGroupedListStyle()
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
.navigationBarTitle(Text("Digital Wellness"))
|
||||
}
|
||||
|
||||
|
|
|
@ -43,17 +43,10 @@ class MyProfileViewController: ProfileViewController {
|
|||
|
||||
private func setAvatarTabBarImage<Account: AccountProtocol>(account: Account) {
|
||||
let avatarURL = account.avatar
|
||||
_ = ImageCache.avatars.get(avatarURL, completion: { [weak self] (data) in
|
||||
guard let self = self, let data = data else { return }
|
||||
|
||||
let maybeGrayscale: UIImage?
|
||||
if Preferences.shared.grayscaleImages {
|
||||
maybeGrayscale = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||
} else {
|
||||
maybeGrayscale = UIImage(data: data)
|
||||
}
|
||||
|
||||
guard let image = maybeGrayscale else {
|
||||
_ = ImageCache.avatars.get(avatarURL, completion: { [weak self] (_, image) in
|
||||
guard let self = self,
|
||||
let image = image,
|
||||
let maybeGrayscale = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -63,7 +56,7 @@ class MyProfileViewController: ProfileViewController {
|
|||
let tabBarImage = UIGraphicsImageRenderer(size: size).image { (_) in
|
||||
let radius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
||||
UIBezierPath(roundedRect: rect, cornerRadius: radius).addClip()
|
||||
image.draw(in: rect)
|
||||
maybeGrayscale.draw(in: rect)
|
||||
}
|
||||
let alwaysOriginalImage = tabBarImage.withRenderingMode(.alwaysOriginal)
|
||||
self.tabBarItem.image = alwaysOriginalImage
|
||||
|
|
|
@ -145,7 +145,9 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
|
|||
return
|
||||
}
|
||||
|
||||
self.newer = pagination?.newer
|
||||
if let newer = pagination?.newer {
|
||||
self.newer = newer
|
||||
}
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
completion(statuses.map { ($0.id, .unknown) })
|
||||
|
@ -182,7 +184,8 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
|
|||
}
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newPinnedStatues) {
|
||||
let oldPinnedStatuses = self.sections[0]
|
||||
// if the user refreshes before the initial pinned statuses request completes, self.sections will be empty
|
||||
let oldPinnedStatuses = self.sections.isEmpty ? [] : self.sections[0]
|
||||
let pinnedStatues = newPinnedStatues.map { (status) -> TimelineEntry in
|
||||
let state: StatusState
|
||||
if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) {
|
||||
|
@ -193,12 +196,12 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
|
|||
return (status.id, state)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
if self.sections.count < 1 {
|
||||
self.sections.append(pinnedStatues)
|
||||
self.tableView.insertSections(IndexSet(integer: 0), with: .none)
|
||||
} else {
|
||||
self.sections[0] = pinnedStatues
|
||||
}
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
|
||||
}
|
||||
}
|
||||
|
@ -206,6 +209,7 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDatasource
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
|
@ -238,33 +242,20 @@ extension ProfileStatusesViewController: StatusTableViewCellDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching {
|
||||
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
let statusID = item(for: indexPath).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)
|
||||
}
|
||||
}
|
||||
let ids = indexPaths.map { item(for: $0).id }
|
||||
prefetchStatuses(with: ids)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
let statusID = item(for: indexPath).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)
|
||||
}
|
||||
let ids: [String] = indexPaths.compactMap {
|
||||
guard $0.section < sections.count,
|
||||
$0.row < sections[$0.section].count else {
|
||||
return nil
|
||||
}
|
||||
return item(for: $0).id
|
||||
}
|
||||
cancelPrefetchingStatuses(with: ids)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,13 +69,11 @@ class ProfileViewController: UIPageViewController {
|
|||
view.backgroundColor = .systemBackground
|
||||
|
||||
let composeButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composeMentioning))
|
||||
if #available(iOS 14.0, *) {
|
||||
composeButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
|
||||
UIAction(title: "Direct Message", image: UIImage(systemName: Status.Visibility.direct.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
|
||||
self.composeDirectMentioning()
|
||||
})
|
||||
])
|
||||
}
|
||||
navigationItem.rightBarButtonItem = composeButton
|
||||
|
||||
headerView = ProfileHeaderView.create()
|
||||
|
|
|
@ -132,6 +132,7 @@ class SearchResultsViewController: EnhancedTableViewController {
|
|||
activityIndicator.isHidden = false
|
||||
activityIndicator.startAnimating()
|
||||
|
||||
let resultTypes = self.resultTypes
|
||||
let request = Client.search(query: query, types: resultTypes, resolve: true, limit: 10)
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(results, _) = response else { fatalError() }
|
||||
|
@ -161,16 +162,16 @@ class SearchResultsViewController: EnhancedTableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
if !results.accounts.isEmpty {
|
||||
if !results.accounts.isEmpty && (resultTypes == nil || resultTypes!.contains(.accounts)) {
|
||||
snapshot.appendSections([.accounts])
|
||||
snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts)
|
||||
addAccounts(results.accounts)
|
||||
}
|
||||
if !results.hashtags.isEmpty {
|
||||
if !results.hashtags.isEmpty && (resultTypes == nil || resultTypes!.contains(.hashtags)) {
|
||||
snapshot.appendSections([.hashtags])
|
||||
snapshot.appendItems(results.hashtags.map { .hashtag($0) }, toSection: .hashtags)
|
||||
}
|
||||
if !results.statuses.isEmpty {
|
||||
if !results.statuses.isEmpty && (resultTypes == nil || resultTypes!.contains(.statuses)) {
|
||||
snapshot.appendSections([.statuses])
|
||||
snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses)
|
||||
addStatuses(results.statuses)
|
||||
|
|
|
@ -113,7 +113,11 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
|||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
||||
|
||||
self.newer = pagination?.newer
|
||||
// if there are no new statuses, pagination is nil
|
||||
// if we were to then overwrite self.newer, future refreshes would fail
|
||||
if let newer = pagination?.newer {
|
||||
self.newer = newer
|
||||
}
|
||||
|
||||
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
||||
completion(statuses.map { ($0.id, .unknown) })
|
||||
|
@ -146,32 +150,20 @@ extension TimelineTableViewController: StatusTableViewCellDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
||||
extension TimelineTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
guard let status = mastodonController.persistentContainer.status(for: item(for: indexPath).id) 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)
|
||||
}
|
||||
}
|
||||
let ids = indexPaths.map { item(for: $0).id }
|
||||
prefetchStatuses(with: ids)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
// todo: this means when removing cells, we can't cancel prefetching
|
||||
// is this an issue?
|
||||
guard indexPath.section < sections.count,
|
||||
indexPath.row < sections[indexPath.section].count,
|
||||
let status = mastodonController.persistentContainer.status(for: item(for: indexPath).id) else {
|
||||
continue
|
||||
}
|
||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
||||
for attachment in status.attachments where attachment.kind == .image {
|
||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
||||
}
|
||||
let ids: [String] = indexPaths.compactMap {
|
||||
guard $0.section < sections.count,
|
||||
$0.row < sections[$0.section].count else {
|
||||
return nil
|
||||
}
|
||||
return item(for: $0).id
|
||||
}
|
||||
cancelPrefetchingStatuses(with: ids)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ extension MenuPreviewProvider {
|
|||
guard mastodonController.loggedIn else {
|
||||
return [
|
||||
openInSafariAction(url: account.url),
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||
guard let self = self else { return }
|
||||
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
||||
})
|
||||
|
@ -54,14 +54,14 @@ extension MenuPreviewProvider {
|
|||
}),
|
||||
]
|
||||
|
||||
if accountID != mastodonController.account.id,
|
||||
#available(iOS 14.0, *) {
|
||||
if accountID != mastodonController.account.id {
|
||||
actionsSection.append(UIDeferredMenuElement({ (elementHandler) in
|
||||
guard let mastodonController = self.mastodonController else {
|
||||
elementHandler([])
|
||||
return
|
||||
}
|
||||
let request = Client.getRelationships(accounts: [account.id])
|
||||
// talk about callback hell :/
|
||||
mastodonController.run(request) { [weak self] (response) in
|
||||
if let self = self,
|
||||
case let .success(results, _) = response,
|
||||
|
@ -89,7 +89,7 @@ extension MenuPreviewProvider {
|
|||
|
||||
let shareSection = [
|
||||
openInSafariAction(url: account.url),
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||
guard let self = self else { return }
|
||||
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
||||
})
|
||||
|
@ -104,7 +104,7 @@ extension MenuPreviewProvider {
|
|||
func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] {
|
||||
return [
|
||||
openInSafariAction(url: url),
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||
guard let self = self else { return }
|
||||
self.navigationDelegate?.showMoreOptions(forURL: url, sourceView: sourceView)
|
||||
})
|
||||
|
@ -139,7 +139,7 @@ extension MenuPreviewProvider {
|
|||
guard mastodonController.loggedIn else {
|
||||
return [
|
||||
openInSafariAction(url: status.url!),
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||
guard let self = self else { return }
|
||||
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
||||
})
|
||||
|
@ -190,7 +190,7 @@ extension MenuPreviewProvider {
|
|||
|
||||
var shareSection = [
|
||||
openInSafariAction(url: status.url!),
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||
guard let self = self else { return }
|
||||
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
||||
}),
|
||||
|
@ -223,8 +223,8 @@ extension MenuPreviewProvider {
|
|||
}
|
||||
|
||||
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)
|
||||
return createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { [weak self] (_) in
|
||||
self?.navigationDelegate?.selected(url: url, allowUniversalLinks: false)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// StatusTablePrefetching.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/18/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
protocol StatusTablePrefetching: TuskerNavigationDelegate {
|
||||
}
|
||||
|
||||
extension StatusTablePrefetching {
|
||||
|
||||
func prefetchStatuses(with ids: [String]) {
|
||||
let context = apiController.persistentContainer.prefetchBackgroundContext
|
||||
context.perform {
|
||||
guard let statuses = getStatusesWith(ids: ids, in: context) else {
|
||||
return
|
||||
}
|
||||
for status in statuses {
|
||||
ImageCache.avatars.fetchIfNotCached(status.account.avatar)
|
||||
for attachment in status.attachments where attachment.kind == .image {
|
||||
ImageCache.attachments.fetchIfNotCached(attachment.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cancelPrefetchingStatuses(with ids: [String]) {
|
||||
let context = apiController.persistentContainer.prefetchBackgroundContext
|
||||
context.perform {
|
||||
guard let statuses = getStatusesWith(ids: ids, in: context) else {
|
||||
return
|
||||
}
|
||||
for status in statuses {
|
||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
||||
for attachment in status.attachments where attachment.kind == .image {
|
||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate func getStatusesWith(ids: [String], in context: NSManagedObjectContext) -> [StatusMO]? {
|
||||
let request: NSFetchRequest<StatusMO> = StatusMO.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "id IN %@", ids)
|
||||
return try? context.fetch(request)
|
||||
}
|
|
@ -117,7 +117,9 @@ class TimelineLikeTableViewController<Item>: EnhancedTableViewController, Refres
|
|||
}
|
||||
|
||||
private func pruneOffscreenRows() {
|
||||
guard let lastVisibleRow = lastLastVisibleRow else {
|
||||
guard let lastVisibleRow = lastLastVisibleRow,
|
||||
// never remove the last section
|
||||
sections.count - headerSectionsCount() > 1 else {
|
||||
return
|
||||
}
|
||||
let lastSectionIndex = sections.count - 1
|
||||
|
@ -134,11 +136,11 @@ class TimelineLikeTableViewController<Item>: EnhancedTableViewController, Refres
|
|||
}
|
||||
willRemoveRows(at: indexPathsToRemove)
|
||||
|
||||
sections.removeSubrange(sectionsToRemove)
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.deleteSections(IndexSet(sectionsToRemove), with: .none)
|
||||
}
|
||||
|
||||
sections.removeSubrange(sectionsToRemove)
|
||||
} else if lastVisibleRow.section == lastSectionIndex {
|
||||
let lastSection = sections.last!
|
||||
let lastRowIndex = lastSection.count - 1
|
||||
|
@ -146,7 +148,7 @@ class TimelineLikeTableViewController<Item>: EnhancedTableViewController, Refres
|
|||
if lastVisibleRow.row < lastRowIndex - pageSize {
|
||||
// if there are more than pageSize rows in the current section below the last visible one
|
||||
|
||||
let rowIndicesInLastSectionToRemove = (lastVisibleRow.row + 20)..<lastSection.count
|
||||
let rowIndicesInLastSectionToRemove = (lastVisibleRow.row + pageSize)..<lastSection.count
|
||||
|
||||
let indexPathsToRemove = rowIndicesInLastSectionToRemove.map {
|
||||
IndexPath(row: $0, section: lastSectionIndex)
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
@available(iOS 13.4, *)
|
||||
class TrackpadScrollGestureRecognizer: UIPanGestureRecognizer {
|
||||
|
||||
override init(target: Any?, action: Selector?) {
|
||||
|
|
|
@ -12,6 +12,8 @@ import Pachyderm
|
|||
|
||||
protocol TuskerNavigationDelegate: UIViewController {
|
||||
var apiController: MastodonController { get }
|
||||
|
||||
func conversation(mainStatusID: String, state: StatusState) -> ConversationTableViewController
|
||||
}
|
||||
|
||||
extension TuskerNavigationDelegate {
|
||||
|
@ -64,19 +66,16 @@ extension TuskerNavigationDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func conversation(mainStatusID: String, state: StatusState) -> ConversationTableViewController {
|
||||
return ConversationTableViewController(for: mainStatusID, state: state, mastodonController: apiController)
|
||||
}
|
||||
|
||||
func selected(status statusID: String) {
|
||||
self.selected(status: statusID, state: .unknown)
|
||||
}
|
||||
|
||||
func selected(status statusID: String, state: StatusState) {
|
||||
// todo: is this necessary? should the conversation main status cell prevent this
|
||||
// don't open if the conversation is the same as the current one
|
||||
if let conversationController = self as? ConversationTableViewController,
|
||||
conversationController.mainStatusID == statusID {
|
||||
return
|
||||
}
|
||||
|
||||
show(ConversationTableViewController(for: statusID, state: state, mastodonController: apiController), sender: self)
|
||||
show(conversation(mainStatusID: statusID, state: state), sender: self)
|
||||
}
|
||||
|
||||
func compose(editing draft: Draft) {
|
||||
|
@ -126,41 +125,13 @@ extension TuskerNavigationDelegate {
|
|||
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)") }
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
let activityController = UIActivityViewController(activityItems: [url, StatusActivityItemSource(status)], applicationActivities: customActivites)
|
||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(navigator: 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)") }
|
||||
|
||||
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(navigator: self, url: account.url)
|
||||
return activityController
|
||||
}
|
||||
}
|
||||
|
||||
func showMoreOptions(forStatus statusID: String, sourceView: UIView?) {
|
||||
|
|
|
@ -2,9 +2,17 @@
|
|||
/// From https://github.com/woltapp/blurhash/blob/b23214ddcab803fe1ec9a3e6b20558caf33a23a5/Swift/BlurHashDecode.swift
|
||||
import UIKit
|
||||
|
||||
// blurhashes are disabled in debug builds because this code is hideously slow when not optimized by the compiler
|
||||
#if DEBUG
|
||||
fileprivate let blurHashesEnabled = ProcessInfo.processInfo.environment.keys.contains("DEBUG_BLUR_HASH")
|
||||
#else
|
||||
fileprivate let blurHashesEnabled = true
|
||||
#endif
|
||||
|
||||
extension UIImage {
|
||||
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
|
||||
guard blurHash.count >= 6 else { return nil }
|
||||
guard blurHashesEnabled,
|
||||
blurHash.count >= 6 else { return nil }
|
||||
|
||||
let sizeFlag = String(blurHash[0]).decode83()
|
||||
let numY = (sizeFlag / 9) + 1
|
||||
|
|
|
@ -63,19 +63,16 @@ class AccountTableViewCell: UITableViewCell {
|
|||
let accountID = self.accountID
|
||||
|
||||
let avatarURL = account.avatar
|
||||
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
||||
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||
guard let self = self else { return }
|
||||
self.avatarRequest = nil
|
||||
|
||||
let image: UIImage?
|
||||
if self.isGrayscale {
|
||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||
} else {
|
||||
image = UIImage(data: data)
|
||||
}
|
||||
guard let image = image,
|
||||
self.accountID == accountID,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.avatarImageView.image = image
|
||||
self.avatarImageView.image = transformedImage
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -69,11 +69,11 @@ class LargeAccountDetailView: UIView {
|
|||
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||
usernameLabel.text = "@\(account.acct)"
|
||||
|
||||
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
||||
guard let self = self, let data = data else { return }
|
||||
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (_, image) in
|
||||
guard let self = self, let image = image else { return }
|
||||
self.avatarRequest = nil
|
||||
DispatchQueue.main.async {
|
||||
self.avatarImageView.image = UIImage(data: data)
|
||||
self.avatarImageView.image = image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,18 +24,11 @@ struct AccountDisplayNameLabel<Account: AccountProtocol>: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 14.0, *) {
|
||||
text
|
||||
.font(.system(size: CGFloat(fontSize), weight: .semibold))
|
||||
.onAppear(perform: self.loadEmojis)
|
||||
} else {
|
||||
text
|
||||
.font(.system(size: CGFloat(fontSize), weight: .semibold))
|
||||
}
|
||||
}
|
||||
|
||||
// embedding Image inside Text is only available on iOS 14
|
||||
@available(iOS 14.0, *)
|
||||
private func loadEmojis() {
|
||||
let fullRange = NSRange(account.displayName.startIndex..., in: account.displayName)
|
||||
let matches = emojiRegex.matches(in: account.displayName, options: [], range: fullRange)
|
||||
|
@ -54,9 +47,9 @@ struct AccountDisplayNameLabel<Account: AccountProtocol>: View {
|
|||
}
|
||||
|
||||
group.enter()
|
||||
let request = ImageCache.emojis.get(emoji.url) { (data) in
|
||||
let request = ImageCache.emojis.get(emoji.url) { (_, image) in
|
||||
defer { group.leave() }
|
||||
guard let data = data, let image = UIImage(data: data) else { return }
|
||||
guard let image = image else { return }
|
||||
|
||||
let size = CGSize(width: fontSize, height: fontSize)
|
||||
let renderer = UIGraphicsImageRenderer(size: size)
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
//
|
||||
// ActivityIndicatorView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/29/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ActivityIndicatorView: UIViewRepresentable {
|
||||
typealias UIViewType = UIActivityIndicatorView
|
||||
|
||||
func makeUIView(context: Context) -> UIActivityIndicatorView {
|
||||
let view = UIActivityIndicatorView()
|
||||
view.startAnimating()
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {
|
||||
}
|
||||
|
||||
}
|
|
@ -159,7 +159,7 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
|||
|
||||
func loadImage() {
|
||||
let attachmentURL = attachment.url
|
||||
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data) in
|
||||
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, _) in
|
||||
guard let self = self, let data = data else { return }
|
||||
self.attachmentRequest = nil
|
||||
if self.attachment.url.pathExtension == "gif" {
|
||||
|
|
|
@ -43,20 +43,13 @@ extension BaseEmojiLabel {
|
|||
foundEmojis = true
|
||||
|
||||
group.enter()
|
||||
let request = ImageCache.emojis.get(emoji.url) { (data) in
|
||||
let request = ImageCache.emojis.get(emoji.url) { (_, image) in
|
||||
defer { group.leave() }
|
||||
guard let data = data else {
|
||||
guard let image = image,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: emoji.url, image: image) else {
|
||||
return
|
||||
}
|
||||
let image: UIImage?
|
||||
if Preferences.shared.grayscaleImages {
|
||||
image = ImageGrayscalifier.convert(url: emoji.url, data: data)
|
||||
} else {
|
||||
image = UIImage(data: data)
|
||||
}
|
||||
if let image = image {
|
||||
emojiImages[emoji.shortcode] = image
|
||||
}
|
||||
emojiImages[emoji.shortcode] = transformedImage
|
||||
}
|
||||
if let request = request {
|
||||
emojiRequests.append(request)
|
||||
|
|
|
@ -63,20 +63,13 @@ class ContentTextView: LinkTextView {
|
|||
|
||||
for emoji in emojis {
|
||||
group.enter()
|
||||
_ = ImageCache.emojis.get(emoji.url) { (data) in
|
||||
_ = ImageCache.emojis.get(emoji.url) { (_, image) in
|
||||
defer { group.leave() }
|
||||
guard let data = data else {
|
||||
guard let image = image,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: emoji.url, image: image) else {
|
||||
return
|
||||
}
|
||||
let image: UIImage?
|
||||
if Preferences.shared.grayscaleImages {
|
||||
image = ImageGrayscalifier.convert(url: emoji.url, data: data)
|
||||
} else {
|
||||
image = UIImage(data: data)
|
||||
}
|
||||
if let image = image {
|
||||
emojiImages[emoji.shortcode] = image
|
||||
}
|
||||
emojiImages[emoji.shortcode] = transformedImage
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,16 +33,12 @@ struct CustomEmojiImageView: View {
|
|||
}
|
||||
|
||||
private func loadImage() {
|
||||
request = ImageCache.emojis.get(emoji.url) { (data) in
|
||||
if let data = data, let image = UIImage(data: data) {
|
||||
request = ImageCache.emojis.get(emoji.url) { (_, image) in
|
||||
DispatchQueue.main.async {
|
||||
self.request = nil
|
||||
if let image = image {
|
||||
self.image = image
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.request = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
//
|
||||
// HashtagHistoryView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/24/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class HashtagHistoryView: UIView {
|
||||
|
||||
private var history: [Hashtag.History]?
|
||||
|
||||
private let curveRadius: CGFloat = 10
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
createLayers()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
createLayers()
|
||||
}
|
||||
|
||||
func setHistory(_ history: [Hashtag.History]?) {
|
||||
if let history = history {
|
||||
self.history = history.sorted(by: { $0.day < $1.day })
|
||||
} else {
|
||||
self.history = nil
|
||||
}
|
||||
|
||||
createLayers()
|
||||
}
|
||||
|
||||
private func createLayers() {
|
||||
guard let history = history,
|
||||
history.count >= 2 else { return }
|
||||
|
||||
let maxUses = history.max(by: { $0.uses < $1.uses })!.uses
|
||||
|
||||
// remove old layers if this view is being re-used
|
||||
layer.sublayers?.forEach { $0.removeFromSuperlayer() }
|
||||
|
||||
let path = UIBezierPath()
|
||||
|
||||
let widthStep = bounds.width / CGFloat(history.count - 1)
|
||||
|
||||
let points: [CGPoint] = history.enumerated().map { (index, entry) in
|
||||
let x = CGFloat(index) * widthStep
|
||||
let yFrac = CGFloat(entry.uses) / CGFloat(maxUses)
|
||||
let y = (1 - yFrac) * bounds.height
|
||||
return CGPoint(x: x, y: y)
|
||||
}
|
||||
|
||||
var gapStartPoints = [CGPoint]()
|
||||
var gapEndPoints = [CGPoint]()
|
||||
|
||||
for (index, point) in points.enumerated().dropFirst().dropLast() {
|
||||
let prev = points[index - 1]
|
||||
let next = points[index + 1]
|
||||
|
||||
let a = atan((point.y - prev.y) / widthStep)
|
||||
let b = atan((next.y - point.y) / widthStep)
|
||||
let innerAngle = .pi - a - b
|
||||
|
||||
let gapDistance = curveRadius / sin(innerAngle / 2)
|
||||
|
||||
let x1 = point.x - cos(a) * gapDistance
|
||||
let y1 = point.y - sin(a) * gapDistance
|
||||
gapStartPoints.append(CGPoint(x: x1, y: y1))
|
||||
|
||||
let x2 = point.x + cos(b) * gapDistance
|
||||
let y2 = point.y + sin(b) * gapDistance
|
||||
gapEndPoints.append(CGPoint(x: x2, y: y2))
|
||||
}
|
||||
|
||||
path.move(to: points.first!)
|
||||
for (index, point) in points.dropFirst().dropLast().enumerated() {
|
||||
path.addLine(to: gapStartPoints[index])
|
||||
path.addQuadCurve(to: gapEndPoints[index], controlPoint: point)
|
||||
}
|
||||
path.addLine(to: points.last!)
|
||||
|
||||
let borderLayer = CAShapeLayer()
|
||||
// copy the border path so we can continue mutating the UIBezierPath to create the fill path
|
||||
borderLayer.path = path.cgPath.copy()!
|
||||
borderLayer.strokeColor = tintColor.cgColor
|
||||
borderLayer.fillColor = nil
|
||||
borderLayer.lineWidth = 2
|
||||
borderLayer.lineCap = .round
|
||||
|
||||
path.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
|
||||
path.addLine(to: CGPoint(x: 0, y: bounds.height))
|
||||
path.addLine(to: points.first!)
|
||||
|
||||
let fillLayer = CAShapeLayer()
|
||||
fillLayer.path = path.cgPath
|
||||
let fillColor = self.fillColor()
|
||||
fillLayer.strokeColor = fillColor
|
||||
fillLayer.fillColor = fillColor
|
||||
fillLayer.lineWidth = 2
|
||||
|
||||
layer.addSublayer(fillLayer)
|
||||
layer.addSublayer(borderLayer)
|
||||
}
|
||||
|
||||
// The non-transparent fill color.
|
||||
// We blend with the view's background color ourselves so that final color is non-transparent,
|
||||
// otherwise when the fill layer's border and fill overlap, there's a visibly darker patch
|
||||
// because transparent colors are being blended together.
|
||||
private func fillColor() -> CGColor {
|
||||
var backgroundRed: CGFloat = 0
|
||||
var backgroundGreen: CGFloat = 0
|
||||
var backgroundBlue: CGFloat = 0
|
||||
var tintRed: CGFloat = 0
|
||||
var tintGreen: CGFloat = 0
|
||||
var tintBlue: CGFloat = 0
|
||||
traitCollection.performAsCurrent {
|
||||
backgroundColor!.getRed(&backgroundRed, green: &backgroundGreen, blue: &backgroundBlue, alpha: nil)
|
||||
tintColor.getRed(&tintRed, green: &tintGreen, blue: &tintBlue, alpha: nil)
|
||||
}
|
||||
let blendedRed = (backgroundRed + tintRed) / 2
|
||||
let blendedGreen = (backgroundGreen + tintGreen) / 2
|
||||
let blendedBlue = (backgroundBlue + tintBlue) / 2
|
||||
return CGColor(red: blendedRed, green: blendedGreen, blue: blendedBlue, alpha: 1)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
|
@ -17,8 +18,8 @@
|
|||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="#hashtag" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vVg-1C-zJr">
|
||||
<rect key="frame" x="16" y="11" width="83" height="22"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<rect key="frame" x="16" y="11" width="71.5" height="22"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// TrendingHashtagTableViewCell.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/24/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class TrendingHashtagTableViewCell: UITableViewCell {
|
||||
|
||||
@IBOutlet weak var hashtagLabel: UILabel!
|
||||
@IBOutlet weak var peopleTodayLabel: UILabel!
|
||||
@IBOutlet weak var historyView: HashtagHistoryView!
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
}
|
||||
|
||||
func updateUI(hashtag: Hashtag) {
|
||||
hashtagLabel.text = "#\(hashtag.name)"
|
||||
historyView.setHistory(hashtag.history)
|
||||
historyView.isHidden = hashtag.history == nil || hashtag.history!.count < 2
|
||||
|
||||
if let history = hashtag.history {
|
||||
let sorted = history.sorted(by: { $0.day < $1.day })
|
||||
let lastTwo = sorted[(sorted.count - 2)...]
|
||||
let accounts = lastTwo.map(\.accounts).reduce(0, +)
|
||||
let uses = lastTwo.map(\.uses).reduce(0, +)
|
||||
|
||||
let format = NSLocalizedString("trending hashtag info", comment: "trending hashtag posts and people")
|
||||
peopleTodayLabel.text = String.localizedStringWithFormat(format, accounts, uses)
|
||||
peopleTodayLabel.isHidden = false
|
||||
} else {
|
||||
peopleTodayLabel.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18121" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18091"/>
|
||||
<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"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="TrendingHashtagTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="66"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="66"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="tEP-en-vHK">
|
||||
<rect key="frame" x="16" y="0.0" width="288" height="66"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="iCc-do-llt">
|
||||
<rect key="frame" x="0.0" y="15" width="180" height="36.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="#hashtag" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SIS-9e-Paj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="180" height="23"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="6 people today" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Kc5-BL-bmu">
|
||||
<rect key="frame" x="0.0" y="23" width="180" height="13.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Xrw-2v-ybZ" customClass="HashtagHistoryView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="188" y="11" width="100" height="44"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemGroupedBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="W4C-uw-zWg"/>
|
||||
<constraint firstAttribute="width" constant="100" id="XHb-vd-qNk"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="tEP-en-vHK" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="3Qd-rF-nGk"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="tEP-en-vHK" secondAttribute="trailing" id="Ws6-oZ-9Es"/>
|
||||
<constraint firstItem="tEP-en-vHK" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="if4-Ea-awg"/>
|
||||
<constraint firstAttribute="bottom" secondItem="tEP-en-vHK" secondAttribute="bottom" id="nTV-Ih-vTj"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<connections>
|
||||
<outlet property="hashtagLabel" destination="SIS-9e-Paj" id="1UK-Va-3rL"/>
|
||||
<outlet property="historyView" destination="Xrw-2v-ybZ" id="OIh-K9-gSk"/>
|
||||
<outlet property="peopleTodayLabel" destination="Kc5-BL-bmu" id="5L8-aO-zt4"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="132" y="132"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="secondarySystemGroupedBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
|
@ -60,8 +60,8 @@ class InstanceTableViewCell: UITableViewCell {
|
|||
private func updateThumbnail(url: URL) {
|
||||
thumbnailImageView.image = nil
|
||||
thumbnailURL = url
|
||||
thumbnailRequest = ImageCache.attachments.get(url) { [weak self] (data) in
|
||||
guard let self = self, self.thumbnailURL == url, let data = data, let image = UIImage(data: data) else { return }
|
||||
thumbnailRequest = ImageCache.attachments.get(url) { [weak self] (_, image) in
|
||||
guard let self = self, self.thumbnailURL == url, let image = image else { return }
|
||||
self.thumbnailRequest = nil
|
||||
DispatchQueue.main.async {
|
||||
self.thumbnailImageView.image = image
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
//
|
||||
// MaybeLazyStack.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 10/10/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MaybeLazyVStack<Content: View>: View {
|
||||
private let alignment: HorizontalAlignment
|
||||
private let spacing: CGFloat?
|
||||
private let content: Content
|
||||
|
||||
init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content) {
|
||||
self.alignment = alignment
|
||||
self.spacing = spacing
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
if #available(iOS 14.0, *) {
|
||||
LazyVStack(alignment: alignment, spacing: spacing) {
|
||||
content
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: alignment, spacing: spacing) {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MaybeLazyHStack<Content: View>: View {
|
||||
private let alignment: VerticalAlignment
|
||||
private let spacing: CGFloat?
|
||||
private let content: Content
|
||||
|
||||
init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content) {
|
||||
self.alignment = alignment
|
||||
self.spacing = spacing
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
if #available(iOS 14.0, *) {
|
||||
LazyHStack(alignment: alignment, spacing: spacing) {
|
||||
content
|
||||
}
|
||||
} else {
|
||||
HStack(alignment: alignment, spacing: spacing) {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -84,21 +84,20 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
|||
imageView.layer.masksToBounds = true
|
||||
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
||||
let avatarURL = account.avatar
|
||||
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||
guard let self = self, let data = data, self.group.id == group.id else { return }
|
||||
|
||||
let image: UIImage?
|
||||
if self.isGrayscale {
|
||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||
} else {
|
||||
image = UIImage(data: data)
|
||||
}
|
||||
|
||||
if let image = image {
|
||||
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||
guard let self = self else { return }
|
||||
guard let image = image,
|
||||
self.group.id == group.id,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||
DispatchQueue.main.async {
|
||||
self.avatarRequests.removeValue(forKey: account.id)
|
||||
imageView.image = image
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.avatarRequests.removeValue(forKey: account.id)
|
||||
imageView.image = transformedImage
|
||||
}
|
||||
}
|
||||
actionAvatarStackView.addArrangedSubview(imageView)
|
||||
|
@ -133,21 +132,20 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
|||
}
|
||||
|
||||
let avatarURL = account.avatar
|
||||
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||
guard let self = self, let data = data, self.group.id == groupID else { return }
|
||||
|
||||
let image: UIImage?
|
||||
if self.isGrayscale {
|
||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||
} else {
|
||||
image = UIImage(data: data)
|
||||
}
|
||||
|
||||
if let image = image {
|
||||
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||
guard let self = self else { return }
|
||||
guard let image = image,
|
||||
self.group.id == groupID,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||
DispatchQueue.main.async {
|
||||
self.avatarRequests.removeValue(forKey: account.id)
|
||||
imageView.image = image
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.avatarRequests.removeValue(forKey: account.id)
|
||||
imageView.image = transformedImage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,21 +65,17 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
|||
imageView.layer.masksToBounds = true
|
||||
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
||||
let avatarURL = account.avatar
|
||||
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||
guard let self = self, let data = data, self.group.id == group.id else { return }
|
||||
|
||||
let image: UIImage?
|
||||
if Preferences.shared.grayscaleImages {
|
||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||
} else {
|
||||
image = UIImage(data: data)
|
||||
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||
guard let self = self,
|
||||
let image = image,
|
||||
self.group.id == group.id,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||
return
|
||||
}
|
||||
|
||||
if let image = image {
|
||||
DispatchQueue.main.async {
|
||||
self.avatarRequests.removeValue(forKey: account.id)
|
||||
imageView.image = image
|
||||
}
|
||||
imageView.image = transformedImage
|
||||
}
|
||||
}
|
||||
avatarStackView.addArrangedSubview(imageView)
|
||||
|
@ -103,21 +99,20 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
|||
}
|
||||
|
||||
let avatarURL = account.avatar
|
||||
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||
guard let self = self, let data = data, self.group.id == groupID else { return }
|
||||
|
||||
let image: UIImage?
|
||||
if self.isGrayscale {
|
||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||
} else {
|
||||
image = UIImage(data: data)
|
||||
}
|
||||
|
||||
if let image = image {
|
||||
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||
guard let self = self else { return }
|
||||
guard let image = image,
|
||||
self.group.id == groupID,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||
DispatchQueue.main.async {
|
||||
self.avatarRequests.removeValue(forKey: account.id)
|
||||
imageView.image = image
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.avatarRequests.removeValue(forKey: account.id)
|
||||
imageView.image = transformedImage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18121" 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="17504.1"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18091"/>
|
||||
<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"/>
|
||||
|
@ -49,7 +48,7 @@
|
|||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="person.badge.plus.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="7gy-KD-YT1">
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="person.fill.badge.plus" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="7gy-KD-YT1">
|
||||
<rect key="frame" x="34" y="12.5" width="32" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="30" id="gvV-4g-2Xr"/>
|
||||
|
@ -76,7 +75,7 @@
|
|||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="person.badge.plus.fill" catalog="system" width="128" height="124"/>
|
||||
<image name="person.fill.badge.plus" catalog="system" width="128" height="124"/>
|
||||
<systemColor name="secondaryLabelColor">
|
||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
|
|
|
@ -68,21 +68,18 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
|||
actionLabel.setEmojis(account.emojis, identifier: account.id)
|
||||
}
|
||||
let avatarURL = account.avatar
|
||||
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||
guard let self = self, self.account == account, let data = data else { return }
|
||||
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||
guard let self = self else { return }
|
||||
self.avatarRequest = nil
|
||||
|
||||
let image: UIImage?
|
||||
if self.isGrayscale {
|
||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||
} else {
|
||||
image = UIImage(data: data)
|
||||
guard self.account == account,
|
||||
let image = image,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||
return
|
||||
}
|
||||
|
||||
if let image = image {
|
||||
DispatchQueue.main.async {
|
||||
self.avatarImageView.image = image
|
||||
}
|
||||
self.avatarImageView.image = transformedImage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,11 +74,9 @@ class ProfileHeaderView: UIView {
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||
|
||||
moreButton.addInteraction(UIPointerInteraction(delegate: self))
|
||||
if #available(iOS 14.0, *) {
|
||||
moreButton.showsMenuAsPrimaryAction = true
|
||||
moreButton.isContextMenuInteractionEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private func createObservers() {
|
||||
cancellables = []
|
||||
|
@ -110,9 +108,7 @@ class ProfileHeaderView: UIView {
|
|||
|
||||
updateImages(account: account)
|
||||
|
||||
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)
|
||||
|
@ -191,35 +187,32 @@ class ProfileHeaderView: UIView {
|
|||
|
||||
let accountID = account.id
|
||||
let avatarURL = account.avatar
|
||||
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
||||
// always load original for avatars, because ImageCache.avatars stores them scaled-down in memory
|
||||
avatarRequest = ImageCache.avatars.get(avatarURL, loadOriginal: true) { [weak self] (_, image) in
|
||||
guard let self = self,
|
||||
let image = image,
|
||||
self.accountID == accountID,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||
return
|
||||
}
|
||||
self.avatarRequest = nil
|
||||
|
||||
let image: UIImage?
|
||||
if self.isGrayscale {
|
||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||
} else {
|
||||
image = UIImage(data: data)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.avatarImageView.image = image
|
||||
self.avatarImageView.image = transformedImage
|
||||
}
|
||||
}
|
||||
if let header = account.header {
|
||||
headerRequest = ImageCache.headers.get(header) { [weak self] (data) in
|
||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
||||
headerRequest = ImageCache.headers.get(header) { [weak self] (_, image) in
|
||||
guard let self = self,
|
||||
let image = image,
|
||||
self.accountID == accountID,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: header, image: image) else {
|
||||
return
|
||||
}
|
||||
self.headerRequest = nil
|
||||
|
||||
let image: UIImage?
|
||||
if self.isGrayscale {
|
||||
image = ImageGrayscalifier.convert(url: header, data: data)
|
||||
} else {
|
||||
image = UIImage(data: data)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.headerImageView.image = image
|
||||
self.headerImageView.image = transformedImage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -227,14 +220,6 @@ class ProfileHeaderView: UIView {
|
|||
|
||||
// MARK: Interaction
|
||||
|
||||
@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 {
|
||||
return
|
||||
|
@ -256,7 +241,6 @@ class ProfileHeaderView: UIView {
|
|||
|
||||
}
|
||||
|
||||
@available(iOS 13.4, *)
|
||||
extension ProfileHeaderView: UIPointerInteractionDelegate {
|
||||
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
|
||||
let preview = UITargetedPreview(view: moreButton)
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18121" 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="17124"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18091"/>
|
||||
<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"/>
|
||||
|
@ -23,7 +22,7 @@
|
|||
<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">
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" 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"/>
|
||||
|
@ -58,9 +57,6 @@
|
|||
<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"/>
|
||||
|
|
|
@ -38,6 +38,8 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
@IBOutlet weak var favoriteButton: UIButton!
|
||||
@IBOutlet weak var reblogButton: UIButton!
|
||||
@IBOutlet weak var moreButton: UIButton!
|
||||
private(set) var prevThreadLinkView: UIView?
|
||||
private(set) var nextThreadLinkView: UIView?
|
||||
|
||||
var statusID: String!
|
||||
var accountID: String!
|
||||
|
@ -94,9 +96,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!]
|
||||
attachmentsView.isAccessibilityElement = true
|
||||
|
||||
if #available(iOS 14.0, *) {
|
||||
moreButton.showsMenuAsPrimaryAction = true
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||
|
||||
|
@ -211,11 +211,9 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
reblogButton.accessibilityLabel = NSLocalizedString("Reblog", comment: "reblog button accessibility label")
|
||||
}
|
||||
|
||||
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(status, sourceView: moreButton))
|
||||
}
|
||||
}
|
||||
|
||||
func updateUI(account: AccountMO) {
|
||||
usernameLabel.text = "@\(account.acct)"
|
||||
|
@ -260,18 +258,14 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
|
||||
let avatarURL = account.avatar
|
||||
let accountID = account.id
|
||||
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
||||
|
||||
let image: UIImage?
|
||||
if self.isGrayscale {
|
||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||
} else {
|
||||
image = UIImage(data: data)
|
||||
}
|
||||
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||
guard let self = self,
|
||||
let image = image,
|
||||
self.accountID == accountID,
|
||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.avatarImageView.image = image
|
||||
self.avatarImageView.image = transformedImage
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,6 +276,52 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||
}
|
||||
|
||||
func setShowThreadLinks(prev: Bool, next: Bool) {
|
||||
if prev {
|
||||
if let prevThreadLinkView = prevThreadLinkView {
|
||||
prevThreadLinkView.isHidden = false
|
||||
} else {
|
||||
let view = UIView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.backgroundColor = tintColor.withAlphaComponent(0.5)
|
||||
view.layer.cornerRadius = 2.5
|
||||
view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
prevThreadLinkView = view
|
||||
addSubview(view)
|
||||
NSLayoutConstraint.activate([
|
||||
view.widthAnchor.constraint(equalToConstant: 5),
|
||||
view.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor),
|
||||
view.topAnchor.constraint(equalTo: topAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: avatarImageView.topAnchor, constant: -2),
|
||||
])
|
||||
}
|
||||
} else {
|
||||
prevThreadLinkView?.isHidden = true
|
||||
}
|
||||
|
||||
if next {
|
||||
if let nextThreadLinkView = nextThreadLinkView {
|
||||
nextThreadLinkView.isHidden = false
|
||||
} else {
|
||||
let view = UIView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.backgroundColor = tintColor.withAlphaComponent(0.5)
|
||||
view.layer.cornerRadius = 2.5
|
||||
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
nextThreadLinkView = view
|
||||
addSubview(view)
|
||||
NSLayoutConstraint.activate([
|
||||
view.widthAnchor.constraint(equalToConstant: 5),
|
||||
view.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor),
|
||||
view.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 2),
|
||||
view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
}
|
||||
} else {
|
||||
nextThreadLinkView?.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue