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"]
|
[submodule "Gifu"]
|
||||||
path = Gifu
|
path = Gifu
|
||||||
url = git://github.com/kaishin/Gifu.git
|
url = git://github.com/kaishin/Gifu.git
|
||||||
|
|
24
CHANGELOG.md
24
CHANGELOG.md
|
@ -1,5 +1,29 @@
|
||||||
# Changelog
|
# 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)
|
## 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).
|
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
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Trends
|
// MARK: - Instance
|
||||||
public static func getTrends(limit: Int? = nil) -> Request<[Hashtag]> {
|
public static func getTrends(limit: Int? = nil) -> Request<[Hashtag]> {
|
||||||
let parameters: [Parameter]
|
let parameters: [Parameter]
|
||||||
if let limit = limit {
|
if let limit = limit {
|
||||||
|
@ -334,6 +334,20 @@ public class Client {
|
||||||
return Request<[Hashtag]>(method: .get, path: "/api/v1/trends", queryParameters: parameters)
|
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 {
|
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 */; };
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; };
|
||||||
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; };
|
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; };
|
||||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4222B301470021BD04 /* AppearancePrefsView.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 */; };
|
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D14BAE22B34A2800642648 /* GalleryViewController.swift */; };
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
||||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
||||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.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 */; };
|
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; };
|
||||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; };
|
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; };
|
||||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.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 */; };
|
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943623A552C200D38C68 /* BookmarkStatusActivity.swift */; };
|
||||||
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943823A553B600D38C68 /* UnbookmarkStatusActivity.swift */; };
|
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943823A553B600D38C68 /* UnbookmarkStatusActivity.swift */; };
|
||||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943A23A55BA600D38C68 /* NavigableTableViewCell.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 */; };
|
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */; };
|
||||||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944923A6AD6100D38C68 /* BookmarksTableViewController.swift */; };
|
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944923A6AD6100D38C68 /* BookmarksTableViewController.swift */; };
|
||||||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944C23A9A03D00D38C68 /* ListTimelineViewController.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 */; };
|
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; };
|
||||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
|
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
|
||||||
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.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 */; };
|
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 */; };
|
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
|
||||||
D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.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 */; };
|
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */; };
|
||||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.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 */; };
|
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 */; };
|
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; };
|
||||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; };
|
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; };
|
||||||
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.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 */; };
|
D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; };
|
||||||
D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; };
|
D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; };
|
||||||
D6A5BB3123BBAD87003BF21D /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB3023BBAD87003BF21D /* JSONResponse.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 */; };
|
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */; };
|
||||||
D6ACE1AC240C3BAD004EA8E2 /* Ambassador.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F613023AE99E000F3CFD3 /* Ambassador.framework */; };
|
D6ACE1AC240C3BAD004EA8E2 /* Ambassador.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F613023AE99E000F3CFD3 /* Ambassador.framework */; };
|
||||||
D6ACE1AD240C3BAD004EA8E2 /* Embassy.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F612D23AE990C00F3CFD3 /* Embassy.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 */; };
|
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
||||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.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 */; };
|
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 */; };
|
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D862139E62700CB5196 /* LargeImageViewController.swift */; };
|
||||||
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D882139E6EC00CB5196 /* AttachmentView.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 */; };
|
D6C99FCB24FADC91005C74D3 /* MainComposeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */; };
|
||||||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */; };
|
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */; };
|
||||||
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.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 */; };
|
D6D3F4C424FDB6B700EC4A6A /* View+ConditionalModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */; };
|
||||||
D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D3FDDF24F41B8400FF50A5 /* ComposeContainerView.swift */; };
|
D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D3FDDF24F41B8400FF50A5 /* ComposeContainerView.swift */; };
|
||||||
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D3FDE124F46A8D00FF50A5 /* ComposeUIState.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 */; };
|
D6D4CC94250DB86A00FCCF8D /* ComposeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4CC93250DB86A00FCCF8D /* ComposeTests.swift */; };
|
||||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */; };
|
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */; };
|
||||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD6212518A200E1C4BB /* Assets.xcassets */; };
|
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 */; };
|
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; };
|
||||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
|
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
|
||||||
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4267625327FB400C02E1C /* ComposeAutocompleteView.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 */; };
|
D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */; };
|
||||||
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; };
|
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; };
|
||||||
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */; };
|
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */; };
|
||||||
D6E426B9253382B300C02E1C /* SearchResultType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B8253382B300C02E1C /* SearchResultType.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 */; };
|
D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; };
|
||||||
D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; };
|
D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; };
|
||||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.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 */; };
|
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */; };
|
||||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; };
|
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; };
|
||||||
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.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 */; };
|
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */; };
|
||||||
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; };
|
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; };
|
||||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
|
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
|
||||||
|
@ -355,7 +367,6 @@
|
||||||
files = (
|
files = (
|
||||||
D61099C12144B0CC00432DC2 /* Pachyderm.framework in Embed Frameworks */,
|
D61099C12144B0CC00432DC2 /* Pachyderm.framework in Embed Frameworks */,
|
||||||
D6BC874621961F73006163F1 /* Gifu.framework in Embed Frameworks */,
|
D6BC874621961F73006163F1 /* Gifu.framework in Embed Frameworks */,
|
||||||
0461A3912163CBAE00C0A807 /* Cache.framework in Embed Frameworks */,
|
|
||||||
);
|
);
|
||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -375,6 +386,11 @@
|
||||||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -693,7 +718,6 @@
|
||||||
D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */,
|
D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */,
|
||||||
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */,
|
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */,
|
||||||
D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */,
|
D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */,
|
||||||
0461A3902163CBAE00C0A807 /* Cache.framework in Frameworks */,
|
|
||||||
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
|
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -804,6 +828,7 @@
|
||||||
D61099E6214561FF00432DC2 /* Attachment.swift */,
|
D61099E6214561FF00432DC2 /* Attachment.swift */,
|
||||||
D61099E82145658300432DC2 /* Card.swift */,
|
D61099E82145658300432DC2 /* Card.swift */,
|
||||||
D61099EA2145661700432DC2 /* ConversationContext.swift */,
|
D61099EA2145661700432DC2 /* ConversationContext.swift */,
|
||||||
|
D693A72B25CF8D15003A14E2 /* DirectoryOrder.swift */,
|
||||||
D61099E22144C38900432DC2 /* Emoji.swift */,
|
D61099E22144C38900432DC2 /* Emoji.swift */,
|
||||||
D61099EC2145664800432DC2 /* Filter.swift */,
|
D61099EC2145664800432DC2 /* Filter.swift */,
|
||||||
D6109A0021456B0800432DC2 /* Hashtag.swift */,
|
D6109A0021456B0800432DC2 /* Hashtag.swift */,
|
||||||
|
@ -830,6 +855,9 @@
|
||||||
children = (
|
children = (
|
||||||
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */,
|
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */,
|
||||||
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */,
|
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */,
|
||||||
|
D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */,
|
||||||
|
D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */,
|
||||||
|
D6093FB625BE0CF3004811E6 /* HashtagHistoryView.swift */,
|
||||||
);
|
);
|
||||||
path = "Hashtag Cell";
|
path = "Hashtag Cell";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -895,9 +923,15 @@
|
||||||
D627943C23A5635D00D38C68 /* Explore */ = {
|
D627943C23A5635D00D38C68 /* Explore */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D627943D23A564D400D38C68 /* ExploreViewController.swift */,
|
D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */,
|
||||||
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */,
|
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */,
|
||||||
|
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */,
|
||||||
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */,
|
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */,
|
||||||
|
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */,
|
||||||
|
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
|
||||||
|
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
|
||||||
|
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
|
||||||
|
D600613D25D07E170067FAD6 /* ProfileDirectoryFilterView.swift */,
|
||||||
);
|
);
|
||||||
path = Explore;
|
path = Explore;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1025,6 +1059,8 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */,
|
D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */,
|
||||||
|
D6C82B5425C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift */,
|
||||||
|
D6C82B5525C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib */,
|
||||||
);
|
);
|
||||||
path = Conversation;
|
path = Conversation;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1183,7 +1219,6 @@
|
||||||
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */,
|
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */,
|
||||||
D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */,
|
D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */,
|
||||||
D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */,
|
D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */,
|
||||||
D6D4CC90250D2C3100FCCF8D /* UIAccessibility.swift */,
|
|
||||||
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */,
|
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */,
|
||||||
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */,
|
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */,
|
||||||
);
|
);
|
||||||
|
@ -1346,10 +1381,8 @@
|
||||||
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
|
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
|
||||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
||||||
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */,
|
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */,
|
||||||
D6C99FC624FACFAB005C74D3 /* ActivityIndicatorView.swift */,
|
|
||||||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
||||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
||||||
D6E426802532814100C02E1C /* MaybeLazyStack.swift */,
|
|
||||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
||||||
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
|
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
|
||||||
D67C57A721E2649B00C3118B /* Account Detail */,
|
D67C57A721E2649B00C3118B /* Account Detail */,
|
||||||
|
@ -1385,6 +1418,7 @@
|
||||||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
||||||
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
||||||
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
||||||
|
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */,
|
||||||
);
|
);
|
||||||
path = Utilities;
|
path = Utilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1443,6 +1477,7 @@
|
||||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
||||||
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
||||||
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
|
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
|
||||||
|
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */,
|
||||||
D67B506B250B28FF00FAECFB /* Vendor */,
|
D67B506B250B28FF00FAECFB /* Vendor */,
|
||||||
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
||||||
D6757A7A2157E00100721E32 /* XCallbackURL */,
|
D6757A7A2157E00100721E32 /* XCallbackURL */,
|
||||||
|
@ -1491,7 +1526,10 @@
|
||||||
D6F1F84E2193B9BE00F5FE67 /* Caching */ = {
|
D6F1F84E2193B9BE00F5FE67 /* Caching */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6F1F84C2193B56E00F5FE67 /* Cache.swift */,
|
D6A6C11425B62E9700298D0F /* CacheExpiry.swift */,
|
||||||
|
D6A6C10E25B62D2400298D0F /* DiskCache.swift */,
|
||||||
|
D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */,
|
||||||
|
D6311C4F25B3765B00B27539 /* ImageDataCache.swift */,
|
||||||
04DACE8D212CC7CC009840C4 /* ImageCache.swift */,
|
04DACE8D212CC7CC009840C4 /* ImageCache.swift */,
|
||||||
);
|
);
|
||||||
path = Caching;
|
path = Caching;
|
||||||
|
@ -1717,19 +1755,23 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */,
|
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */,
|
||||||
|
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */,
|
||||||
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
|
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
|
||||||
D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */,
|
D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */,
|
||||||
D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */,
|
D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */,
|
||||||
D627FF7D217E958900CC0648 /* DraftTableViewCell.xib in Resources */,
|
D627FF7D217E958900CC0648 /* DraftTableViewCell.xib in Resources */,
|
||||||
D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */,
|
D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */,
|
||||||
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
|
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
|
||||||
|
D6C82B5725C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib in Resources */,
|
||||||
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
||||||
|
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */,
|
||||||
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */,
|
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */,
|
||||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
||||||
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */,
|
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */,
|
||||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */,
|
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */,
|
||||||
D6969EA4240DD28D002843CE /* UnknownNotificationTableViewCell.xib in Resources */,
|
D6969EA4240DD28D002843CE /* UnknownNotificationTableViewCell.xib in Resources */,
|
||||||
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
|
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
|
||||||
|
D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */,
|
||||||
D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */,
|
D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */,
|
||||||
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */,
|
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */,
|
||||||
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
|
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
|
||||||
|
@ -1808,6 +1850,7 @@
|
||||||
D61099D02144B2D700432DC2 /* Method.swift in Sources */,
|
D61099D02144B2D700432DC2 /* Method.swift in Sources */,
|
||||||
D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */,
|
D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */,
|
||||||
D61099FB214569F600432DC2 /* Report.swift in Sources */,
|
D61099FB214569F600432DC2 /* Report.swift in Sources */,
|
||||||
|
D693A72C25CF8D15003A14E2 /* DirectoryOrder.swift in Sources */,
|
||||||
D61099F92145698900432DC2 /* Relationship.swift in Sources */,
|
D61099F92145698900432DC2 /* Relationship.swift in Sources */,
|
||||||
D61099E12144C1DC00432DC2 /* Account.swift in Sources */,
|
D61099E12144C1DC00432DC2 /* Account.swift in Sources */,
|
||||||
D61099E92145658300432DC2 /* Card.swift in Sources */,
|
D61099E92145658300432DC2 /* Card.swift in Sources */,
|
||||||
|
@ -1850,6 +1893,7 @@
|
||||||
D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */,
|
D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */,
|
||||||
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */,
|
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */,
|
||||||
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
|
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
|
||||||
|
D600613E25D07E170067FAD6 /* ProfileDirectoryFilterView.swift in Sources */,
|
||||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||||
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
||||||
|
@ -1857,6 +1901,7 @@
|
||||||
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */,
|
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */,
|
||||||
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */,
|
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */,
|
||||||
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
|
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
|
||||||
|
D6093FB725BE0CF3004811E6 /* HashtagHistoryView.swift in Sources */,
|
||||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
|
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
||||||
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
|
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
|
||||||
|
@ -1877,7 +1922,6 @@
|
||||||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
||||||
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */,
|
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */,
|
||||||
D6C99FC724FACFAB005C74D3 /* ActivityIndicatorView.swift in Sources */,
|
|
||||||
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */,
|
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */,
|
||||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
||||||
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
||||||
|
@ -1889,6 +1933,7 @@
|
||||||
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */,
|
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */,
|
||||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
||||||
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
|
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
|
||||||
|
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
|
||||||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
|
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
|
||||||
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
|
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
|
||||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
|
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
|
||||||
|
@ -1910,7 +1955,7 @@
|
||||||
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
||||||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
||||||
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
||||||
D6E426812532814100C02E1C /* MaybeLazyStack.swift in Sources */,
|
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
||||||
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
||||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
|
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
|
||||||
D6A3BC852321F6C100FD64D5 /* AccountListTableViewController.swift in Sources */,
|
D6A3BC852321F6C100FD64D5 /* AccountListTableViewController.swift in Sources */,
|
||||||
|
@ -1933,6 +1978,7 @@
|
||||||
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */,
|
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */,
|
||||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
|
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
|
||||||
D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.swift in Sources */,
|
D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.swift in Sources */,
|
||||||
|
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */,
|
||||||
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
||||||
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */,
|
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */,
|
||||||
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */,
|
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */,
|
||||||
|
@ -1941,10 +1987,13 @@
|
||||||
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
|
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
|
||||||
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
||||||
D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */,
|
D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */,
|
||||||
|
D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */,
|
||||||
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
|
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
|
||||||
|
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */,
|
||||||
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
||||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
||||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
||||||
|
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||||
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
||||||
|
@ -1970,8 +2019,6 @@
|
||||||
D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */,
|
D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */,
|
||||||
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */,
|
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */,
|
||||||
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */,
|
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */,
|
||||||
D6D4CC91250D2C3100FCCF8D /* UIAccessibility.swift in Sources */,
|
|
||||||
D627943E23A564D400D38C68 /* ExploreViewController.swift in Sources */,
|
|
||||||
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */,
|
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */,
|
||||||
D622757A24EE21D900B82A16 /* ComposeAttachmentRow.swift in Sources */,
|
D622757A24EE21D900B82A16 /* ComposeAttachmentRow.swift in Sources */,
|
||||||
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */,
|
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */,
|
||||||
|
@ -1983,6 +2030,7 @@
|
||||||
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
||||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
||||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
||||||
|
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */,
|
||||||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
||||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
||||||
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */,
|
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */,
|
||||||
|
@ -2009,6 +2057,7 @@
|
||||||
D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */,
|
D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */,
|
||||||
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
||||||
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */,
|
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */,
|
||||||
|
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */,
|
||||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
||||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
||||||
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
|
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
|
||||||
|
@ -2018,6 +2067,7 @@
|
||||||
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */,
|
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */,
|
||||||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
|
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
|
||||||
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */,
|
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */,
|
||||||
|
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */,
|
||||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
|
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
|
||||||
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */,
|
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */,
|
||||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
||||||
|
@ -2034,7 +2084,6 @@
|
||||||
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */,
|
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */,
|
||||||
D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */,
|
D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */,
|
||||||
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
||||||
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */,
|
|
||||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
||||||
0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */,
|
0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */,
|
||||||
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
||||||
|
@ -2043,6 +2092,7 @@
|
||||||
D626493F23C101C500612E6E /* AlbumAssetCollectionViewController.swift in Sources */,
|
D626493F23C101C500612E6E /* AlbumAssetCollectionViewController.swift in Sources */,
|
||||||
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */,
|
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */,
|
||||||
D6C99FCB24FADC91005C74D3 /* MainComposeTextView.swift in Sources */,
|
D6C99FCB24FADC91005C74D3 /* MainComposeTextView.swift in Sources */,
|
||||||
|
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
|
||||||
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */,
|
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */,
|
||||||
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */,
|
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */,
|
||||||
D677284E24ECC01D00C732D3 /* Draft.swift in Sources */,
|
D677284E24ECC01D00C732D3 /* Draft.swift in Sources */,
|
||||||
|
@ -2052,6 +2102,8 @@
|
||||||
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
|
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
|
||||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
||||||
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
|
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
|
||||||
|
D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */,
|
||||||
|
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
|
||||||
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
||||||
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
||||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */,
|
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */,
|
||||||
|
@ -2121,6 +2173,14 @@
|
||||||
name = LaunchScreen.storyboard;
|
name = LaunchScreen.storyboard;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
D6E57FA425C26FAB00341037 /* en */,
|
||||||
|
);
|
||||||
|
name = Localizable.stringsdict;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXVariantGroup section */
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
|
@ -2275,7 +2335,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
@ -2331,7 +2391,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -2349,16 +2409,16 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 15;
|
CURRENT_PROJECT_VERSION = 17;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.0;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2020.1;
|
MARKETING_VERSION = 2021.1;
|
||||||
OTHER_LDFLAGS = "";
|
OTHER_LDFLAGS = "";
|
||||||
"OTHER_SWIFT_FLAGS[sdk=iphone*14*]" = "$(inherited) -D SDK_IOS_14";
|
"OTHER_SWIFT_FLAGS[sdk=iphone*14*]" = "$(inherited) -D SDK_IOS_14";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker;
|
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker;
|
||||||
|
@ -2378,16 +2438,16 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 15;
|
CURRENT_PROJECT_VERSION = 17;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.0;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2020.1;
|
MARKETING_VERSION = 2021.1;
|
||||||
"OTHER_SWIFT_FLAGS[sdk=iphone*14*]" = "$(inherited) -D SDK_IOS_14";
|
"OTHER_SWIFT_FLAGS[sdk=iphone*14*]" = "$(inherited) -D SDK_IOS_14";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker;
|
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
|
|
@ -7,9 +7,6 @@
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "group:BlankSlate.xcappdata">
|
location = "group:BlankSlate.xcappdata">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
<FileRef
|
|
||||||
location = "group:Cache/Cache.xcodeproj">
|
|
||||||
</FileRef>
|
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "group:Gifu/Gifu.xcodeproj">
|
location = "group:Gifu/Gifu.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
|
|
@ -29,7 +29,7 @@ class AccountActivityItemSource: NSObject, UIActivityItemSource {
|
||||||
metadata.originalURL = account.url
|
metadata.originalURL = account.url
|
||||||
metadata.url = account.url
|
metadata.url = account.url
|
||||||
metadata.title = "\(account.displayName) (@\(account.username)@\(account.url.host!)"
|
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) {
|
let image = UIImage(data: data) {
|
||||||
metadata.iconProvider = NSItemProvider(object: image)
|
metadata.iconProvider = NSItemProvider(object: image)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ class StatusActivityItemSource: NSObject, UIActivityItemSource {
|
||||||
let doc = try! SwiftSoup.parse(status.content)
|
let doc = try! SwiftSoup.parse(status.content)
|
||||||
let content = try! doc.text()
|
let content = try! doc.text()
|
||||||
metadata.title = "\(status.account.displayName): \"\(content)\""
|
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) {
|
let image = UIImage(data: data) {
|
||||||
metadata.iconProvider = NSItemProvider(object: image)
|
metadata.iconProvider = NSItemProvider(object: image)
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,9 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
window = UIWindow(windowScene: windowScene)
|
window = UIWindow(windowScene: windowScene)
|
||||||
window!.rootViewController = nav
|
window!.rootViewController = nav
|
||||||
window!.makeKeyAndVisible()
|
window!.makeKeyAndVisible()
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil)
|
||||||
|
themePrefChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
func sceneWillResignActive(_ scene: UIScene) {
|
func sceneWillResignActive(_ scene: UIScene) {
|
||||||
|
@ -109,4 +112,8 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
@objc private func close() {
|
@objc private func close() {
|
||||||
closeWindow()
|
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 UIKit
|
||||||
import Cache
|
|
||||||
|
|
||||||
class ImageCache {
|
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 headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
|
||||||
static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
|
static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
|
||||||
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
|
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
|
private static let disableCaching = false
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private let cache: Cache<Data>
|
private let cache: ImageDataCache
|
||||||
|
|
||||||
private var groups = MultiThreadDictionary<URL, RequestGroup>(name: "ImageCache request groups")
|
private var groups = MultiThreadDictionary<URL, RequestGroup>(name: "ImageCache request groups")
|
||||||
|
|
||||||
private var backgroundQueue = DispatchQueue(label: "ImageCache completion queue", qos: .default)
|
private var backgroundQueue = DispatchQueue(label: "ImageCache completion queue", qos: .default)
|
||||||
|
|
||||||
init(name: String, memoryExpiry expiry: Expiry) {
|
init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) {
|
||||||
let storage = MemoryStorage<Data>(config: MemoryConfig(expiry: expiry))
|
// todo: might not always want to use UIScreen.main for this, e.g. Catalyst?
|
||||||
self.cache = .memory(storage)
|
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) {
|
func get(_ url: URL, loadOriginal: Bool = false, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
|
||||||
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? {
|
|
||||||
let key = url.absoluteString
|
let key = url.absoluteString
|
||||||
if !ImageCache.disableCaching,
|
if !ImageCache.disableCaching,
|
||||||
// todo: calling object(forKey: key) does disk I/O and this method is often called from the main thread
|
let entry = try? cache.get(key, loadOriginal: loadOriginal) {
|
||||||
// in performance sensitive paths. a nice optimization to DiskStorage would be adding an internal cache
|
if let completion = completion {
|
||||||
// of the state (unknown/exists/does not exist) of whether or not objects exist on disk so that the slow, disk I/O
|
backgroundQueue.async {
|
||||||
// path can be avoided most of the time
|
completion(entry.data, entry.image)
|
||||||
let data = try? cache.object(forKey: key) {
|
}
|
||||||
backgroundQueue.async {
|
|
||||||
completion?(data)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
if let completion = completion, let group = groups[url] {
|
if let group = groups[url] {
|
||||||
return group.addCallback(completion)
|
if let completion = completion {
|
||||||
} else {
|
return group.addCallback(completion)
|
||||||
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
|
return nil
|
||||||
|
} else {
|
||||||
|
let group = createGroup(url: url)
|
||||||
let request = group.addCallback(completion)
|
let request = group.addCallback(completion)
|
||||||
group.run()
|
group.run()
|
||||||
return request
|
return request
|
||||||
|
@ -74,8 +58,34 @@ class ImageCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func get(_ url: URL) -> Data? {
|
func fetchIfNotCached(_ url: URL) {
|
||||||
return try? cache.object(forKey: url.absoluteString)
|
// 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) {
|
func cancelWithoutCallback(_ url: URL) {
|
||||||
|
@ -88,11 +98,11 @@ class ImageCache {
|
||||||
|
|
||||||
private class RequestGroup {
|
private class RequestGroup {
|
||||||
let url: URL
|
let url: URL
|
||||||
private let onFinished: (Data?) -> Void
|
private let onFinished: (Data?, UIImage?) -> Void
|
||||||
private var task: URLSessionDataTask?
|
private var task: URLSessionDataTask?
|
||||||
private var requests = [Request]()
|
private var requests = [Request]()
|
||||||
|
|
||||||
init(url: URL, onFinished: @escaping (Data?) -> Void) {
|
init(url: URL, onFinished: @escaping (Data?, UIImage?) -> Void) {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.onFinished = onFinished
|
self.onFinished = onFinished
|
||||||
}
|
}
|
||||||
|
@ -116,7 +126,7 @@ class ImageCache {
|
||||||
task?.priority = max(1.0, URLSessionTask.defaultPriority + 0.1 * Float(requests.filter { !$0.cancelled }.count))
|
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)
|
let request = Request(callback: completion)
|
||||||
requests.append(request)
|
requests.append(request)
|
||||||
updatePriority()
|
updatePriority()
|
||||||
|
@ -141,21 +151,24 @@ class ImageCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
func complete(with data: Data?) {
|
func complete(with data: Data?) {
|
||||||
|
let image = data != nil ? UIImage(data: data!) : nil
|
||||||
|
|
||||||
requests.filter { !$0.cancelled }.forEach {
|
requests.filter { !$0.cancelled }.forEach {
|
||||||
if let callback = $0.callback {
|
if let callback = $0.callback {
|
||||||
callback(data)
|
callback(data, image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.onFinished(data)
|
|
||||||
|
self.onFinished(data, image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Request {
|
class Request {
|
||||||
private weak var group: RequestGroup?
|
private weak var group: RequestGroup?
|
||||||
private(set) var callback: ((Data?) -> Void)?
|
private(set) var callback: ((Data?, UIImage?) -> Void)?
|
||||||
private(set) var cancelled: Bool = false
|
private(set) var cancelled: Bool = false
|
||||||
|
|
||||||
init(callback: ((Data?) -> Void)?) {
|
init(callback: ((Data?, UIImage?) -> Void)?) {
|
||||||
self.callback = callback
|
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 = UIWindow(windowScene: windowScene)
|
||||||
window!.rootViewController = nav
|
window!.rootViewController = nav
|
||||||
window!.makeKeyAndVisible()
|
window!.makeKeyAndVisible()
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil)
|
||||||
|
themePrefChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
func sceneWillResignActive(_ scene: UIScene) {
|
func sceneWillResignActive(_ scene: UIScene) {
|
||||||
|
@ -58,6 +61,10 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
return scene.userActivity
|
return scene.userActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func themePrefChanged() {
|
||||||
|
window?.overrideUserInterfaceStyle = Preferences.shared.theme
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeSceneDelegate: ComposeHostingControllerDelegate {
|
extension ComposeSceneDelegate: ComposeHostingControllerDelegate {
|
||||||
|
|
|
@ -115,6 +115,7 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: this should dedup requests
|
||||||
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
|
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
|
||||||
if let instance = self.instance {
|
if let instance = self.instance {
|
||||||
completion?(instance)
|
completion?(instance)
|
||||||
|
|
|
@ -11,20 +11,13 @@ import UIKit
|
||||||
struct MenuController {
|
struct MenuController {
|
||||||
|
|
||||||
static let composeCommand: UIKeyCommand = {
|
static let composeCommand: UIKeyCommand = {
|
||||||
let selector: Selector
|
return UIKeyCommand(title: "Compose", action: #selector(MainSplitViewController.presentCompose), input: "n", modifierFlags: .command)
|
||||||
if #available(iOS 14.0, *) {
|
|
||||||
selector = #selector(MainSplitViewController.presentCompose)
|
|
||||||
} else {
|
|
||||||
selector = #selector(MainTabBarViewController.presentCompose)
|
|
||||||
}
|
|
||||||
return UIKeyCommand(title: "Compose", action: selector, input: "n", modifierFlags: .command)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static func refreshCommand(discoverabilityTitle: String?) -> UIKeyCommand {
|
static func refreshCommand(discoverabilityTitle: String?) -> UIKeyCommand {
|
||||||
return UIKeyCommand(title: "Refresh", action: #selector(RefreshableViewController.refresh), input: "r", modifierFlags: .command, discoverabilityTitle: discoverabilityTitle)
|
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 {
|
static func sidebarCommand(item: MainSidebarViewController.Item, command: String) -> UIKeyCommand {
|
||||||
let data: Any
|
let data: Any
|
||||||
if case let .tab(tab) = item {
|
if case let .tab(tab) = item {
|
||||||
|
@ -46,7 +39,6 @@ struct MenuController {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
static let sidebarItemKeyCommands: [UIKeyCommand] = [
|
static let sidebarItemKeyCommands: [UIKeyCommand] = [
|
||||||
sidebarCommand(item: .tab(.timelines), command: "1"),
|
sidebarCommand(item: .tab(.timelines), command: "1"),
|
||||||
sidebarCommand(item: .tab(.notifications), command: "2"),
|
sidebarCommand(item: .tab(.notifications), command: "2"),
|
||||||
|
@ -92,25 +84,18 @@ struct MenuController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func buildSidebarShortcuts() -> UIMenu {
|
private static func buildSidebarShortcuts() -> UIMenu {
|
||||||
let children: [UIMenuElement]
|
|
||||||
if #available(iOS 14.0, *) {
|
|
||||||
children = sidebarItemKeyCommands
|
|
||||||
} else {
|
|
||||||
children = []
|
|
||||||
}
|
|
||||||
return UIMenu(
|
return UIMenu(
|
||||||
title: "",
|
title: "",
|
||||||
image: nil,
|
image: nil,
|
||||||
identifier: nil,
|
identifier: nil,
|
||||||
options: .displayInline,
|
options: .displayInline,
|
||||||
children: children
|
children: sidebarItemKeyCommands
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MenuController {
|
extension MenuController {
|
||||||
@available(iOS 14.0, *)
|
|
||||||
class SidebarItem: NSObject, NSCopying {
|
class SidebarItem: NSObject, NSCopying {
|
||||||
let item: MainSidebarViewController.Item
|
let item: MainSidebarViewController.Item
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,12 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
return context
|
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 statusSubject = PassthroughSubject<String, Never>()
|
||||||
let accountSubject = PassthroughSubject<String, Never>()
|
let accountSubject = PassthroughSubject<String, Never>()
|
||||||
let relationshipSubject = 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) {
|
func closeWindow(animation: UIWindowScene.DismissalAnimation = .standard, errorHandler: ((Error) -> Void)? = nil) {
|
||||||
guard let session = self.window??.windowScene?.session else { return }
|
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()
|
let options = UIWindowSceneDestructionRequestOptions()
|
||||||
options.windowDismissalAnimation = animation
|
options.windowDismissalAnimation = animation
|
||||||
UIApplication.shared.requestSceneSessionDestruction(session, options: options, errorHandler: errorHandler)
|
UIApplication.shared.requestSceneSessionDestruction(session, options: options, errorHandler: errorHandler)
|
||||||
|
|
|
@ -14,6 +14,15 @@ struct ImageGrayscalifier {
|
||||||
private static let context = CIContext()
|
private static let context = CIContext()
|
||||||
private static let cache = NSCache<NSURL, UIImage>()
|
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? {
|
static func convert(url: URL?, data: Data) -> UIImage? {
|
||||||
if let url = url,
|
if let url = url,
|
||||||
let cached = cache.object(forKey: url as NSURL) {
|
let cached = cache.object(forKey: url as NSURL) {
|
||||||
|
|
|
@ -142,7 +142,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func activateAccount(_ account: LocalData.UserAccountInfo, animated: Bool) {
|
func activateAccount(_ account: LocalData.UserAccountInfo, animated: Bool) {
|
||||||
|
let oldMostRecentAccount = LocalData.shared.mostRecentAccountID
|
||||||
LocalData.shared.setMostRecentAccount(account)
|
LocalData.shared.setMostRecentAccount(account)
|
||||||
window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account)
|
window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account)
|
||||||
|
|
||||||
|
@ -150,7 +150,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
if let container = window?.rootViewController as? AccountSwitchingContainerViewController {
|
if let container = window?.rootViewController as? AccountSwitchingContainerViewController {
|
||||||
let direction: AccountSwitchingContainerViewController.AnimationDirection
|
let direction: AccountSwitchingContainerViewController.AnimationDirection
|
||||||
if animated,
|
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) {
|
let newIndex = LocalData.shared.accounts.firstIndex(of: account) {
|
||||||
direction = newIndex > oldIndex ? .upwards : .downwards
|
direction = newIndex > oldIndex ? .upwards : .downwards
|
||||||
} else {
|
} else {
|
||||||
|
@ -176,11 +176,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
mastodonController.getOwnAccount()
|
mastodonController.getOwnAccount()
|
||||||
mastodonController.getOwnInstance()
|
mastodonController.getOwnInstance()
|
||||||
|
|
||||||
if #available(iOS 14.0, *) {
|
return MainSplitViewController(mastodonController: mastodonController)
|
||||||
return MainSplitViewController(mastodonController: mastodonController)
|
|
||||||
} else {
|
|
||||||
return MainTabBarViewController(mastodonController: mastodonController)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createOnboardingUI() -> UIViewController {
|
func createOnboardingUI() -> UIViewController {
|
||||||
|
|
|
@ -47,4 +47,12 @@ class MultiThreadDictionary<Key: Hashable, Value> {
|
||||||
}
|
}
|
||||||
return 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() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
dragEnabled = true
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell)
|
tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell)
|
||||||
|
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
|
|
@ -25,7 +25,7 @@ class AttachmentPreviewViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadView() {
|
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 image = UIImage(data: data) {
|
||||||
let imageView: UIImageView
|
let imageView: UIImageView
|
||||||
if attachment.url.pathExtension == "gif" {
|
if attachment.url.pathExtension == "gif" {
|
||||||
|
|
|
@ -44,7 +44,7 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
||||||
var animationGifData: Data? {
|
var animationGifData: Data? {
|
||||||
let attachment = attachments[currentIndex]
|
let attachment = attachments[currentIndex]
|
||||||
if attachment.url.pathExtension == "gif" {
|
if attachment.url.pathExtension == "gif" {
|
||||||
return ImageCache.attachments.get(attachment.url)
|
return ImageCache.attachments.getData(attachment.url)
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,24 +162,19 @@ extension BookmarksTableViewController: StatusTableViewCellDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BookmarksTableViewController: UITableViewDataSourcePrefetching {
|
extension BookmarksTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
let ids = indexPaths.map { statuses[$0.row].id }
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue }
|
prefetchStatuses(with: ids)
|
||||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
|
||||||
for attachment in status.attachments where attachment.kind == .image {
|
|
||||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
let ids: [String] = indexPaths.compactMap {
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue }
|
guard $0.row < statuses.count else {
|
||||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
return nil
|
||||||
for attachment in status.attachments where attachment.kind == .image {
|
|
||||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
|
||||||
}
|
}
|
||||||
|
return statuses[$0.row].id
|
||||||
}
|
}
|
||||||
|
cancelPrefetchingStatuses(with: ids)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,25 +34,11 @@ struct ComposeAttachmentRow: View {
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
if case .drawing(_) = attachment.data {
|
if case .drawing(_) = attachment.data {
|
||||||
Button(action: self.editDrawing) {
|
Button(action: self.editDrawing) {
|
||||||
if #available(iOS 14.0, *) {
|
Label("Edit Drawing", systemImage: "hand.draw")
|
||||||
Label("Edit Drawing", systemImage: "hand.draw")
|
|
||||||
} else {
|
|
||||||
HStack {
|
|
||||||
Text("Edit Drawing")
|
|
||||||
Image(systemName: "hand.draw")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if attachment.data.type == .image {
|
} else if attachment.data.type == .image {
|
||||||
Button(action: self.recognizeText) {
|
Button(action: self.recognizeText) {
|
||||||
if #available(iOS 14.0, *) {
|
Label("Recognize Text", systemImage: "doc.text.viewfinder")
|
||||||
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)
|
.fontSize(17)
|
||||||
|
|
||||||
case .recognizingText:
|
case .recognizingText:
|
||||||
if #available(iOS 14.0, *) {
|
ProgressView()
|
||||||
ProgressView()
|
|
||||||
} else {
|
|
||||||
ActivityIndicatorView()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -45,14 +45,7 @@ struct ComposeAttachmentsList: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(action: self.addAttachment) {
|
Button(action: self.addAttachment) {
|
||||||
if #available(iOS 14.0, *) {
|
Label("Add photo or video", systemImage: addButtonImageName)
|
||||||
Label("Add photo or video", systemImage: addButtonImageName)
|
|
||||||
} else {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: addButtonImageName)
|
|
||||||
Text("Add photo or video")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.disabled(!canAddAttachment)
|
.disabled(!canAddAttachment)
|
||||||
.foregroundColor(.blue)
|
.foregroundColor(.blue)
|
||||||
|
@ -61,14 +54,7 @@ struct ComposeAttachmentsList: View {
|
||||||
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
||||||
|
|
||||||
Button(action: self.createDrawing) {
|
Button(action: self.createDrawing) {
|
||||||
if #available(iOS 14.0, *) {
|
Label("Draw something", systemImage: "hand.draw")
|
||||||
Label("Draw something", systemImage: "hand.draw")
|
|
||||||
} else {
|
|
||||||
HStack(alignment: .lastTextBaseline) {
|
|
||||||
Image(systemName: "hand.draw")
|
|
||||||
Text("Draw something")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.disabled(!canAddAttachment)
|
.disabled(!canAddAttachment)
|
||||||
.foregroundColor(.blue)
|
.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 {
|
struct ComposeAutocompleteMentionsView: View {
|
||||||
@EnvironmentObject private var mastodonController: MastodonController
|
@EnvironmentObject private var mastodonController: MastodonController
|
||||||
@EnvironmentObject private var uiState: ComposeUIState
|
@EnvironmentObject private var uiState: ComposeUIState
|
||||||
|
@ -104,7 +91,6 @@ struct ComposeAutocompleteMentionsView: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.iOS13OnlyPadding()
|
|
||||||
}
|
}
|
||||||
.onReceive(uiState.$autocompleteState.debounce(for: .milliseconds(250), scheduler: DispatchQueue.main), perform: queryChanged)
|
.onReceive(uiState.$autocompleteState.debounce(for: .milliseconds(250), scheduler: DispatchQueue.main), perform: queryChanged)
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
|
@ -333,7 +319,6 @@ struct ComposeAutocompleteHashtagsView: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.iOS13OnlyPadding()
|
|
||||||
}
|
}
|
||||||
.onReceive(uiState.$autocompleteState.debounce(for: .milliseconds(250), scheduler: DispatchQueue.main), perform: queryChanged)
|
.onReceive(uiState.$autocompleteState.debounce(for: .milliseconds(250), scheduler: DispatchQueue.main), perform: queryChanged)
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
|
|
|
@ -44,16 +44,10 @@ struct ComposeAvatarImageView: View {
|
||||||
|
|
||||||
private func loadImage() {
|
private func loadImage() {
|
||||||
guard let url = url else { return }
|
guard let url = url else { return }
|
||||||
request = ImageCache.avatars.get(url) { (data) in
|
request = ImageCache.avatars.get(url) { (_, image) in
|
||||||
if let data = data, let image = UIImage(data: data) {
|
DispatchQueue.main.async {
|
||||||
DispatchQueue.main.async {
|
self.request = nil
|
||||||
self.request = nil
|
self.avatarImage = image
|
||||||
self.avatarImage = image
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.request = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,11 +58,7 @@ class ComposeDrawingViewController: UIViewController {
|
||||||
canvasView.drawing = initialDrawing
|
canvasView.drawing = initialDrawing
|
||||||
}
|
}
|
||||||
canvasView.delegate = self
|
canvasView.delegate = self
|
||||||
if #available(iOS 14.0, *) {
|
canvasView.drawingPolicy = .anyInput
|
||||||
canvasView.drawingPolicy = .anyInput
|
|
||||||
} else {
|
|
||||||
canvasView.allowsFingerDrawing = true
|
|
||||||
}
|
|
||||||
canvasView.minimumZoomScale = 0.5
|
canvasView.minimumZoomScale = 0.5
|
||||||
canvasView.maximumZoomScale = 2
|
canvasView.maximumZoomScale = 2
|
||||||
canvasView.backgroundColor = .systemBackground
|
canvasView.backgroundColor = .systemBackground
|
||||||
|
|
|
@ -27,7 +27,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
|
|
||||||
private var cancellables = [AnyCancellable]()
|
private var cancellables = [AnyCancellable]()
|
||||||
|
|
||||||
private var keyboardHeight: CGFloat = 0
|
|
||||||
private var toolbarHeight: CGFloat = 44
|
private var toolbarHeight: CGFloat = 44
|
||||||
|
|
||||||
private var mainToolbar: UIToolbar!
|
private var mainToolbar: UIToolbar!
|
||||||
|
@ -115,13 +114,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
toolbar.isAccessibilityElement = true
|
toolbar.isAccessibilityElement = true
|
||||||
|
|
||||||
let visibilityAction: Selector?
|
let visibilityItem = UIBarButtonItem(image: UIImage(systemName: draft.visibility.imageName), style: .plain, target: nil, action: nil)
|
||||||
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)
|
|
||||||
visibilityBarButtonItems.append(visibilityItem)
|
visibilityBarButtonItems.append(visibilityItem)
|
||||||
visibilityChanged(draft.visibility)
|
visibilityChanged(draft.visibility)
|
||||||
|
|
||||||
|
@ -135,7 +128,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateAdditionalSafeAreaInsets() {
|
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) {
|
@objc private func keyboardWillShow(_ notification: Foundation.Notification) {
|
||||||
|
@ -147,19 +140,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
|
|
||||||
accessoryView.alpha = 1
|
accessoryView.alpha = 1
|
||||||
accessoryView.isHidden = false
|
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) {
|
@objc private func keyboardWillHide(_ notification: Foundation.Notification) {
|
||||||
|
@ -192,13 +172,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
} completion: { (finished) in
|
} completion: { (finished) in
|
||||||
accessoryView.alpha = 1
|
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) {
|
@objc private func keyboardDidHide(_ notification: Foundation.Notification) {
|
||||||
|
@ -214,15 +187,13 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
item.image = UIImage(systemName: newVisibility.imageName)
|
item.image = UIImage(systemName: newVisibility.imageName)
|
||||||
item.image!.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
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)
|
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 elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
let state = visibility == newVisibility ? UIMenuElement.State.on : .off
|
||||||
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
|
||||||
return UIAction(title: visibility.displayName, image: UIImage(systemName: visibility.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { (_) in
|
self.draft.visibility = visibility
|
||||||
self.draft.visibility = visibility
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
item.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: elements)
|
|
||||||
}
|
}
|
||||||
|
item.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: elements)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,18 +226,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
draft.contentWarningEnabled = !draft.contentWarningEnabled
|
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() {
|
@objc func draftsButtonPresed() {
|
||||||
let draftsVC = DraftsTableViewController(account: mastodonController.accountInfo!, exclude: draft)
|
let draftsVC = DraftsTableViewController(account: mastodonController.accountInfo!, exclude: draft)
|
||||||
draftsVC.delegate = self
|
draftsVC.delegate = self
|
||||||
|
|
|
@ -44,14 +44,9 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
// the pre-iOS 14 API does not result in the correct pointer interactions for nav bar buttons, see FB8595468
|
mostOfTheBody.toolbar {
|
||||||
if #available(iOS 14.0, *) {
|
ToolbarItem(placement: .cancellationAction) { cancelButton }
|
||||||
mostOfTheBody.toolbar {
|
ToolbarItem(placement: .confirmationAction) { postButton }
|
||||||
ToolbarItem(placement: .cancellationAction) { cancelButton }
|
|
||||||
ToolbarItem(placement: .confirmationAction) { postButton }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mostOfTheBody.navigationBarItems(leading: cancelButton, trailing: postButton)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,24 +77,14 @@ struct ComposeView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var autocompleteSuggestions: some View {
|
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
|
VStack(spacing: 0) {
|
||||||
if #available(iOS 14.0, *) {
|
Spacer()
|
||||||
VStack(spacing: 0) {
|
if let state = uiState.autocompleteState {
|
||||||
Spacer()
|
ComposeAutocompleteView(autocompleteState: state)
|
||||||
if let state = uiState.autocompleteState {
|
|
||||||
ComposeAutocompleteView(autocompleteState: state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.transition(.move(edge: .bottom))
|
|
||||||
.animation(.default)
|
|
||||||
} else {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
Spacer()
|
|
||||||
if let state = uiState.autocompleteState {
|
|
||||||
ComposeAutocompleteView(autocompleteState: state)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.transition(.move(edge: .bottom))
|
||||||
|
.animation(.default)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mainStack(outerMinY: CGFloat) -> some View {
|
func mainStack(outerMinY: CGFloat) -> some View {
|
||||||
|
|
|
@ -45,12 +45,11 @@ class EmojiCollectionViewCell: UICollectionViewCell {
|
||||||
func updateUI(emoji: Emoji) {
|
func updateUI(emoji: Emoji) {
|
||||||
currentEmojiShortcode = emoji.shortcode
|
currentEmojiShortcode = emoji.shortcode
|
||||||
|
|
||||||
imageRequest = ImageCache.emojis.get(emoji.url) { [weak self] (data) in
|
imageRequest = ImageCache.emojis.get(emoji.url) { [weak self] (_, image) in
|
||||||
if let data = data, let image = UIImage(data: data) {
|
guard let image = image else { return }
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self, self.currentEmojiShortcode == emoji.shortcode else { return }
|
guard let self = self, self.currentEmojiShortcode == emoji.shortcode else { return }
|
||||||
self.emojiImageView.image = image
|
self.emojiImageView.image = image
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,13 +69,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
|
|
||||||
uiState.autocompleteHandler = context.coordinator
|
uiState.autocompleteHandler = context.coordinator
|
||||||
|
|
||||||
let visibilityAction: Selector?
|
let visibilityButton = UIBarButtonItem(image: UIImage(systemName: visibility.imageName), style: .plain, target: nil, action: nil)
|
||||||
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)
|
|
||||||
updateVisibilityMenu(visibilityButton)
|
updateVisibilityMenu(visibilityButton)
|
||||||
let toolbar = UIToolbar()
|
let toolbar = UIToolbar()
|
||||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -131,15 +125,13 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateVisibilityMenu(_ visibilityButton: UIBarButtonItem) {
|
private func updateVisibilityMenu(_ visibilityButton: UIBarButtonItem) {
|
||||||
if #available(iOS 14.0, *) {
|
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
||||||
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
let state = visibility == self.visibility ? UIMenuElement.State.on : .off
|
||||||
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
|
||||||
return UIAction(title: visibility.displayName, image: UIImage(systemName: visibility.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { (_) in
|
self.uiState.draft.visibility = visibility
|
||||||
self.uiState.draft.visibility = visibility
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
visibilityButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: elements)
|
|
||||||
}
|
}
|
||||||
|
visibilityButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: elements)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: UITextView, context: Context) {
|
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||||
|
|
|
@ -7,25 +7,31 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import SafariServices
|
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
class ConversationNode {
|
||||||
|
let status: StatusMO
|
||||||
|
var children: [ConversationNode]
|
||||||
|
|
||||||
|
init(status: StatusMO) {
|
||||||
|
self.status = status
|
||||||
|
self.children = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ConversationTableViewController: EnhancedTableViewController {
|
class ConversationTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
static let showPostsImage = UIImage(systemName: "eye.fill")!
|
static let showPostsImage = UIImage(systemName: "eye.fill")!
|
||||||
static let hidePostsImage = UIImage(systemName: "eye.slash.fill")!
|
static let hidePostsImage = UIImage(systemName: "eye.slash.fill")!
|
||||||
|
|
||||||
|
static let bottomSeparatorTag = 101
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
let mainStatusID: String
|
let mainStatusID: String
|
||||||
let mainStatusState: StatusState
|
let mainStatusState: StatusState
|
||||||
var statuses: [(id: String, state: StatusState)] = [] {
|
private(set) var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||||
didSet {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.tableView.reloadData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var showStatusesAutomatically = false
|
var showStatusesAutomatically = false
|
||||||
var visibilityBarButtonItem: UIBarButtonItem!
|
var visibilityBarButtonItem: UIBarButtonItem!
|
||||||
|
@ -46,7 +52,8 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
guard let persistentContainer = mastodonController?.persistentContainer else { return }
|
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()
|
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: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
|
||||||
tableView.register(UINib(nibName: "ConversationMainStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "mainStatusCell")
|
tableView.register(UINib(nibName: "ConversationMainStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "mainStatusCell")
|
||||||
|
tableView.register(UINib(nibName: "ExpandThreadTableViewCell", bundle: .main), forCellReuseIdentifier: "expandThreadCell")
|
||||||
|
|
||||||
tableView.prefetchDataSource = self
|
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
|
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 {
|
guard let mainStatus = self.mastodonController.persistentContainer.status(for: self.mainStatusID) else {
|
||||||
fatalError("Missing cached status \(self.mainStatusID)")
|
fatalError("Missing cached status \(self.mainStatusID)")
|
||||||
|
@ -81,19 +151,39 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
let parents = self.getDirectParents(inReplyTo: mainStatusInReplyToID, from: context.ancestors)
|
let parents = self.getDirectParents(inReplyTo: mainStatusInReplyToID, from: context.ancestors)
|
||||||
let parentStatuses = context.ancestors.filter { parents.contains($0.id) }
|
let parentStatuses = context.ancestors.filter { parents.contains($0.id) }
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: parentStatuses) {
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: context.descendants) {
|
// todo: should this really be blindly adding all the descendants?
|
||||||
self.statuses = parents.map { ($0, .unknown) } + self.statuses + context.descendants.map { ($0.id, .unknown) }
|
self.mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants) {
|
||||||
let indexPath = IndexPath(row: parents.count, section: 0)
|
DispatchQueue.main.async {
|
||||||
DispatchQueue.main.async {
|
var snapshot = self.dataSource.snapshot()
|
||||||
self.tableView.scrollToRow(at: indexPath, at: .middle, animated: false)
|
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 statuses = statuses
|
||||||
var parents = [String]()
|
var parents = [String]()
|
||||||
|
|
||||||
|
@ -108,38 +198,99 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
return parents
|
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 {
|
func removeAllInReplyTo(id: String) -> [StatusMO] {
|
||||||
return 1
|
let statuses = descendants.filter { $0.inReplyToID == id }
|
||||||
|
descendants.removeAll { $0.inReplyToID == id }
|
||||||
|
return statuses
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes[mainStatusID]!.children
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
private func addFlattenedChildThreadsToSnapshot(_ childThreads: [ConversationNode], mainStatus: StatusMO, snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
|
||||||
return statuses.count
|
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 {
|
||||||
|
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)])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
func item(for indexPath: IndexPath) -> (id: String, state: StatusState)? {
|
||||||
let (id, state) = statuses[indexPath.row]
|
return self.dataSource.itemIdentifier(for: indexPath).flatMap { (item) in
|
||||||
|
switch item {
|
||||||
if id == mainStatusID {
|
case let .status(id: id, state: state):
|
||||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "mainStatusCell", for: indexPath) as? ConversationMainStatusTableViewCell else { fatalError() }
|
return (id: id, state: state)
|
||||||
cell.selectionStyle = .none
|
default:
|
||||||
cell.showStatusAutomatically = showStatusesAutomatically
|
return nil
|
||||||
cell.delegate = self
|
}
|
||||||
cell.updateUI(statusID: id, state: state)
|
|
||||||
return cell
|
|
||||||
} 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Table view delegate
|
// 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 {
|
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -155,7 +306,8 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
@objc func toggleVisibilityButtonPressed() {
|
@objc func toggleVisibilityButtonPressed() {
|
||||||
showStatusesAutomatically = !showStatusesAutomatically
|
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
|
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 {
|
extension ConversationTableViewController: StatusTableViewCellDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
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]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
let ids: [String] = indexPaths.compactMap { item(for: $0)?.id }
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue }
|
prefetchStatuses(with: ids)
|
||||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
|
||||||
for attachment in status.attachments where attachment.kind == .image {
|
|
||||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
let ids: [String] = indexPaths.compactMap { item(for: $0)?.id }
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statuses[indexPath.row].id) else { continue }
|
cancelPrefetchingStatuses(with: ids)
|
||||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
|
||||||
for attachment in status.attachments where attachment.kind == .image {
|
|
||||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class AddSavedHashtagViewController: SearchResultsViewController {
|
class AddSavedHashtagViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
|
var resultsController: SearchResultsViewController!
|
||||||
var searchController: UISearchController!
|
var searchController: UISearchController!
|
||||||
|
|
||||||
|
var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
super.init(mastodonController: mastodonController, resultTypes: [.hashtags])
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
super.init(style: .grouped)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -24,14 +31,32 @@ class AddSavedHashtagViewController: SearchResultsViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
delegate = self
|
title = NSLocalizedString("Search", comment: "search screen title")
|
||||||
|
|
||||||
searchController = UISearchController(searchResultsController: nil)
|
tableView.register(UINib(nibName: "TrendingHashtagTableViewCell", bundle: .main), forCellReuseIdentifier: "trendingTagCell")
|
||||||
searchController.obscuresBackgroundDuringPresentation = false
|
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.hidesNavigationBarDuringPresentation = false
|
||||||
|
searchController.searchResultsUpdater = resultsController
|
||||||
searchController.searchBar.autocapitalizationType = .none
|
searchController.searchBar.autocapitalizationType = .none
|
||||||
searchController.searchBar.placeholder = NSLocalizedString("Search for hashtags to save", comment: "add saved hashtag search field placeholder")
|
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
|
definesPresentationContext = true
|
||||||
|
|
||||||
|
@ -41,11 +66,38 @@ class AddSavedHashtagViewController: SearchResultsViewController {
|
||||||
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonPressed))
|
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonPressed))
|
||||||
}
|
}
|
||||||
|
|
||||||
override func performSearch(query: String?) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
if let query = query, !query.starts(with: "#") {
|
super.viewWillAppear(animated)
|
||||||
super.performSearch(query: "#\(query)")
|
|
||||||
} else {
|
let request = Client.getTrends(limit: 10)
|
||||||
super.performSearch(query: query)
|
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: SearchResultsViewControllerDelegate {
|
extension AddSavedHashtagViewController {
|
||||||
func selectedSearchResult(hashtag: Hashtag) {
|
enum Section {
|
||||||
SavedDataManager.shared.add(hashtag: hashtag, for: mastodonController.accountInfo!)
|
case trendingTags
|
||||||
dismiss(animated: true)
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
selectHashtag(hashtag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,23 +10,22 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class ExploreViewController: EnhancedTableViewController {
|
class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
var dataSource: DataSource!
|
private var collectionView: UICollectionView!
|
||||||
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
var resultsController: SearchResultsViewController!
|
private(set) var resultsController: SearchResultsViewController!
|
||||||
var searchController: UISearchController!
|
private(set) var searchController: UISearchController!
|
||||||
|
|
||||||
var searchControllerStatusOnAppearance: Bool? = nil
|
var searchControllerStatusOnAppearance: Bool? = nil
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
super.init(style: .insetGrouped)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
dragEnabled = true
|
|
||||||
|
|
||||||
title = NSLocalizedString("Explore", comment: "explore tab title")
|
title = NSLocalizedString("Explore", comment: "explore tab title")
|
||||||
tabBarItem.image = UIImage(systemName: "magnifyingglass")
|
tabBarItem.image = UIImage(systemName: "magnifyingglass")
|
||||||
|
@ -39,62 +38,22 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.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
|
dataSource = createDataSource()
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "basicCell", for: indexPath)
|
applyInitialSnapshot()
|
||||||
|
|
||||||
switch item {
|
if mastodonController.instance == nil {
|
||||||
case .bookmarks:
|
mastodonController.getOwnInstance(completion: self.ownInstanceLoaded(_:))
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = SearchResultsViewController(mastodonController: mastodonController)
|
||||||
resultsController.exploreNavigationController = self.navigationController!
|
resultsController.exploreNavigationController = self.navigationController!
|
||||||
|
@ -109,8 +68,20 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(savedHashtagsChanged), name: .savedHashtagsChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(savedHashtagsChanged), name: .savedHashtagsChanged, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(savedInstancesChanged), name: .savedInstancesChanged, 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) {
|
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()
|
let request = Client.getLists()
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
guard case let .success(lists, _) = response else {
|
guard case let .success(lists, _) = response else {
|
||||||
fatalError()
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var snapshot = self.dataSource.snapshot()
|
var snapshot = self.dataSource.snapshot()
|
||||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .lists))
|
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 {
|
DispatchQueue.main.async {
|
||||||
self.dataSource.apply(snapshot)
|
self.dataSource.apply(snapshot)
|
||||||
|
@ -142,26 +179,31 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func savedHashtagsChanged() {
|
@objc private func savedHashtagsChanged() {
|
||||||
let account = mastodonController.accountInfo!
|
let account = mastodonController.accountInfo!
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedHashtags))
|
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)
|
dataSource.apply(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func savedInstancesChanged() {
|
@objc private func savedInstancesChanged() {
|
||||||
let account = mastodonController.accountInfo!
|
let account = mastodonController.accountInfo!
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedInstances))
|
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)
|
dataSource.apply(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteList(_ list: List) {
|
private func deleteList(_ list: List, completion: @escaping (Bool) -> Void) {
|
||||||
let title = String(format: NSLocalizedString("Are you sure want to delete the '%@' list?", comment: "delete list alert title"), list.title)
|
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)
|
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
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Delete List", comment: "delete list alert confirm button"), style: .destructive, handler: { (_) in
|
||||||
|
|
||||||
let request = List.delete(list)
|
let request = List.delete(list)
|
||||||
|
@ -174,6 +216,7 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
snapshot.deleteItems([.list(list)])
|
snapshot.deleteItems([.list(list)])
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.dataSource.apply(snapshot)
|
self.dataSource.apply(snapshot)
|
||||||
|
completion(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
@ -190,9 +233,38 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
SavedDataManager.shared.remove(instance: instanceURL, for: account)
|
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) {
|
switch dataSource.itemIdentifier(for: indexPath) {
|
||||||
case nil:
|
case nil:
|
||||||
return
|
return
|
||||||
|
@ -200,11 +272,17 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
show(BookmarksTableViewController(mastodonController: mastodonController), sender: nil)
|
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):
|
case let .list(list):
|
||||||
show(ListTimelineViewController(for: list, mastodonController: mastodonController), sender: nil)
|
show(ListTimelineViewController(for: list, mastodonController: mastodonController), sender: nil)
|
||||||
|
|
||||||
case .addList:
|
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)
|
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.addTextField(configurationHandler: nil)
|
||||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "new list alert cancel button"), style: .cancel, handler: 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)
|
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
||||||
|
|
||||||
case .addSavedHashtag:
|
case .addSavedHashtag:
|
||||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
collectionView.deselectItem(at: indexPath, animated: true)
|
||||||
let navController = UINavigationController(rootViewController: AddSavedHashtagViewController(mastodonController: mastodonController))
|
let navController = UINavigationController(rootViewController: AddSavedHashtagViewController(mastodonController: mastodonController))
|
||||||
present(navController, animated: true)
|
present(navController, animated: true)
|
||||||
|
|
||||||
|
@ -240,7 +318,7 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
show(InstanceTimelineViewController(for: url, parentMastodonController: mastodonController), sender: nil)
|
show(InstanceTimelineViewController(for: url, parentMastodonController: mastodonController), sender: nil)
|
||||||
|
|
||||||
case .findInstance:
|
case .findInstance:
|
||||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
collectionView.deselectItem(at: indexPath, animated: true)
|
||||||
let findController = FindInstanceViewController(parentMastodonController: mastodonController)
|
let findController = FindInstanceViewController(parentMastodonController: mastodonController)
|
||||||
findController.instanceTimelineDelegate = self
|
findController.instanceTimelineDelegate = self
|
||||||
let navController = UINavigationController(rootViewController: findController)
|
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 {
|
extension ExploreViewController {
|
||||||
enum Section: CaseIterable {
|
enum Section: CaseIterable {
|
||||||
case bookmarks
|
case bookmarks
|
||||||
|
case discover
|
||||||
case lists
|
case lists
|
||||||
case savedHashtags
|
case savedHashtags
|
||||||
case savedInstances
|
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 {
|
enum Item: Hashable {
|
||||||
case bookmarks
|
case bookmarks
|
||||||
|
case trendingTags
|
||||||
|
case profileDirectory
|
||||||
case list(List)
|
case list(List)
|
||||||
case addList
|
case addList
|
||||||
case savedHashtag(Hashtag)
|
case savedHashtag(Hashtag)
|
||||||
|
@ -297,10 +363,60 @@ extension ExploreViewController {
|
||||||
case savedInstance(URL)
|
case savedInstance(URL)
|
||||||
case findInstance
|
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) {
|
switch (lhs, rhs) {
|
||||||
case (.bookmarks, .bookmarks):
|
case (.bookmarks, .bookmarks):
|
||||||
return true
|
return true
|
||||||
|
case (.trendingTags, .trendingTags):
|
||||||
|
return true
|
||||||
|
case (.profileDirectory, .profileDirectory):
|
||||||
|
return true
|
||||||
case let (.list(a), .list(b)):
|
case let (.list(a), .list(b)):
|
||||||
return a.id == b.id
|
return a.id == b.id
|
||||||
case (.addList, .addList):
|
case (.addList, .addList):
|
||||||
|
@ -317,10 +433,15 @@ extension ExploreViewController {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
switch self {
|
switch self {
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
hasher.combine("bookmarks")
|
hasher.combine("bookmarks")
|
||||||
|
case .trendingTags:
|
||||||
|
hasher.combine("trendingTags")
|
||||||
|
case .profileDirectory:
|
||||||
|
hasher.combine("profileDirectory")
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
hasher.combine("list")
|
hasher.combine("list")
|
||||||
hasher.combine(list.id)
|
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 {
|
extension ExploreViewController: InstanceTimelineViewControllerDelegate {
|
||||||
|
@ -404,12 +474,13 @@ extension ExploreViewController: InstanceTimelineViewControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ExploreViewController {
|
extension ExploreViewController: UICollectionViewDragDelegate {
|
||||||
override func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||||
let accountID = mastodonController.accountInfo?.id else {
|
let accountID = mastodonController.accountInfo?.id else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
let provider: NSItemProvider
|
let provider: NSItemProvider
|
||||||
switch item {
|
switch item {
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
|
@ -425,11 +496,7 @@ extension ExploreViewController {
|
||||||
case let .savedInstance(url):
|
case let .savedInstance(url):
|
||||||
provider = NSItemProvider(object: url as NSURL)
|
provider = NSItemProvider(object: url as NSURL)
|
||||||
// todo: should dragging public timelines into new windows be supported?
|
// todo: should dragging public timelines into new windows be supported?
|
||||||
case .addList:
|
case .trendingTags, .profileDirectory, .addList, .addSavedHashtag, .findInstance:
|
||||||
return []
|
|
||||||
case .addSavedHashtag:
|
|
||||||
return []
|
|
||||||
case .findInstance:
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return [UIDragItem(itemProvider: provider)]
|
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
|
view.isHidden = false
|
||||||
|
|
||||||
if UIAccessibility.prefersCrossFadeTransitionsBackwardsCompat {
|
if UIAccessibility.prefersCrossFadeTransitions {
|
||||||
view.alpha = 0
|
view.alpha = 0
|
||||||
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
|
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
|
||||||
self.view.alpha = 1
|
self.view.alpha = 1
|
||||||
|
|
|
@ -87,8 +87,8 @@ class FastSwitchingAccountView: UIView {
|
||||||
let controller = MastodonController.getForAccount(account)
|
let controller = MastodonController.getForAccount(account)
|
||||||
controller.getOwnAccount { [weak self] (result) in
|
controller.getOwnAccount { [weak self] (result) in
|
||||||
guard let self = self, case let .success(account) = result else { return }
|
guard let self = self, case let .success(account) = result else { return }
|
||||||
self.avatarRequest = ImageCache.avatars.get(account.avatar) { [weak avatarImageView] (data) in
|
self.avatarRequest = ImageCache.avatars.get(account.avatar) { [weak avatarImageView] (_, image) in
|
||||||
guard let avatarImageView = avatarImageView, let data = data, let image = UIImage(data: data) else { return }
|
guard let avatarImageView = avatarImageView, let image = image else { return }
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
avatarImageView.image = image
|
avatarImageView.image = image
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,19 +85,20 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
overrideUserInterfaceStyle = .dark
|
overrideUserInterfaceStyle = .dark
|
||||||
view.backgroundColor = .black
|
view.backgroundColor = .black
|
||||||
|
|
||||||
if let data = cache.get(url) {
|
// always load full resolution from disk for large image, in case the cache is scaled
|
||||||
createLargeImage(data: data, url: url)
|
if let entry = cache.get(url, loadOriginal: true) {
|
||||||
|
createLargeImage(data: entry.data, image: entry.image, url: url)
|
||||||
} else {
|
} else {
|
||||||
createPreview()
|
createPreview()
|
||||||
|
|
||||||
loadingVC = LoadingViewController()
|
loadingVC = LoadingViewController()
|
||||||
embedChild(loadingVC!)
|
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 }
|
guard let self = self else { return }
|
||||||
self.imageRequest = nil
|
self.imageRequest = nil
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.loadingVC?.removeViewAndController()
|
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 }
|
guard !loaded else { return }
|
||||||
loaded = true
|
loaded = true
|
||||||
|
|
||||||
let image: UIImage?
|
if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) {
|
||||||
if Preferences.shared.grayscaleImages {
|
|
||||||
image = ImageGrayscalifier.convert(url: url, data: data)
|
|
||||||
} else {
|
|
||||||
image = UIImage(data: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let image = image {
|
|
||||||
let gifData = url.pathExtension == "gif" ? data : nil
|
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 {
|
class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
||||||
|
|
||||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||||
if UIAccessibility.prefersCrossFadeTransitionsBackwardsCompat {
|
if UIAccessibility.prefersCrossFadeTransitions {
|
||||||
return 0.2
|
return 0.2
|
||||||
} else {
|
} else {
|
||||||
return 0.4
|
return 0.4
|
||||||
|
@ -51,7 +51,7 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if UIAccessibility.prefersCrossFadeTransitionsBackwardsCompat {
|
if UIAccessibility.prefersCrossFadeTransitions {
|
||||||
animateCrossFadeTransition(using: transitionContext)
|
animateCrossFadeTransition(using: transitionContext)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if UIAccessibility.prefersCrossFadeTransitionsBackwardsCompat && !transitionContext.isInteractive {
|
if UIAccessibility.prefersCrossFadeTransitions && !transitionContext.isInteractive {
|
||||||
animateCrossFadeTransition(using: transitionContext)
|
animateCrossFadeTransition(using: transitionContext)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ class EditListAccountsViewController: EnhancedTableViewController {
|
||||||
guard case let .account(id) = item else { fatalError() }
|
guard case let .account(id) = item else { fatalError() }
|
||||||
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "accountCell", for: indexPath) as! AccountTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: "accountCell", for: indexPath) as! AccountTableViewCell
|
||||||
|
cell.delegate = self
|
||||||
cell.updateUI(accountID: id)
|
cell.updateUI(accountID: id)
|
||||||
return cell
|
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)
|
embedChild(newRoot)
|
||||||
|
|
||||||
if direction != .none {
|
if direction != .none {
|
||||||
if UIAccessibility.prefersCrossFadeTransitionsBackwardsCompat {
|
if UIAccessibility.prefersCrossFadeTransitions {
|
||||||
newRoot.view.alpha = 0
|
newRoot.view.alpha = 0
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseInOut) {
|
UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseInOut) {
|
||||||
|
|
|
@ -9,13 +9,11 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
protocol MainSidebarViewControllerDelegate: class {
|
protocol MainSidebarViewControllerDelegate: class {
|
||||||
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
|
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
|
||||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item)
|
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
class MainSidebarViewController: UIViewController {
|
class MainSidebarViewController: UIViewController {
|
||||||
|
|
||||||
private weak var mastodonController: MastodonController!
|
private weak var mastodonController: MastodonController!
|
||||||
|
@ -34,7 +32,7 @@ class MainSidebarViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
var exploreTabItems: [Item] {
|
var exploreTabItems: [Item] {
|
||||||
var items: [Item] = [.search, .bookmarks]
|
var items: [Item] = [.search, .bookmarks, .trendingTags, .profileDirectory]
|
||||||
let snapshot = dataSource.snapshot()
|
let snapshot = dataSource.snapshot()
|
||||||
for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) {
|
for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) {
|
||||||
items.append(.list(list))
|
items.append(.list(list))
|
||||||
|
@ -88,6 +86,10 @@ class MainSidebarViewController: UIViewController {
|
||||||
|
|
||||||
applyInitialSnapshot()
|
applyInitialSnapshot()
|
||||||
|
|
||||||
|
if mastodonController.instance == nil {
|
||||||
|
mastodonController.getOwnInstance(completion: self.ownInstanceLoaded(_:))
|
||||||
|
}
|
||||||
|
|
||||||
select(item: .tab(.timelines), animated: false)
|
select(item: .tab(.timelines), animated: false)
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil)
|
||||||
|
@ -95,6 +97,8 @@ class MainSidebarViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func select(item: Item, animated: Bool) {
|
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 }
|
guard let indexPath = dataSource.indexPath(for: item) else { return }
|
||||||
collectionView.selectItem(at: indexPath, animated: animated, scrollPosition: .top)
|
collectionView.selectItem(at: indexPath, animated: animated, scrollPosition: .top)
|
||||||
itemLastSelectedTimestamps[item] = Date()
|
itemLastSelectedTimestamps[item] = Date()
|
||||||
|
@ -130,7 +134,7 @@ class MainSidebarViewController: UIViewController {
|
||||||
|
|
||||||
private func applyInitialSnapshot() {
|
private func applyInitialSnapshot() {
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections(Section.allCases)
|
snapshot.appendSections(Section.allCases.filter { $0 != .discover })
|
||||||
snapshot.appendItems([
|
snapshot.appendItems([
|
||||||
.tab(.timelines),
|
.tab(.timelines),
|
||||||
.tab(.notifications),
|
.tab(.notifications),
|
||||||
|
@ -141,6 +145,13 @@ class MainSidebarViewController: UIViewController {
|
||||||
snapshot.appendItems([
|
snapshot.appendItems([
|
||||||
.tab(.compose)
|
.tab(.compose)
|
||||||
], toSection: .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)
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
reloadLists()
|
reloadLists()
|
||||||
|
@ -148,6 +159,18 @@ class MainSidebarViewController: UIViewController {
|
||||||
reloadSavedInstances()
|
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() {
|
private func reloadLists() {
|
||||||
let request = Client.getLists()
|
let request = Client.getLists()
|
||||||
mastodonController.run(request) { [weak self] (response) in
|
mastodonController.run(request) { [weak self] (response) in
|
||||||
|
@ -159,29 +182,47 @@ class MainSidebarViewController: UIViewController {
|
||||||
exploreSnapshot.append(lists.map { .list($0) }, to: .listsHeader)
|
exploreSnapshot.append(lists.map { .list($0) }, to: .listsHeader)
|
||||||
exploreSnapshot.append([.addList], to: .listsHeader)
|
exploreSnapshot.append([.addList], to: .listsHeader)
|
||||||
DispatchQueue.main.async {
|
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() {
|
@objc private func reloadSavedHashtags() {
|
||||||
|
let selected = collectionView.indexPathsForSelectedItems?.first
|
||||||
|
|
||||||
var hashtagsSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
var hashtagsSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
||||||
hashtagsSnapshot.append([.savedHashtagsHeader])
|
hashtagsSnapshot.append([.savedHashtagsHeader])
|
||||||
hashtagsSnapshot.expand([.savedHashtagsHeader])
|
hashtagsSnapshot.expand([.savedHashtagsHeader])
|
||||||
let sortedHashtags = SavedDataManager.shared.sortedHashtags(for: mastodonController.accountInfo!)
|
let sortedHashtags = SavedDataManager.shared.sortedHashtags(for: mastodonController.accountInfo!)
|
||||||
hashtagsSnapshot.append(sortedHashtags.map { .savedHashtag($0) }, to: .savedHashtagsHeader)
|
hashtagsSnapshot.append(sortedHashtags.map { .savedHashtag($0) }, to: .savedHashtagsHeader)
|
||||||
hashtagsSnapshot.append([.addSavedHashtag], 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() {
|
@objc private func reloadSavedInstances() {
|
||||||
|
let selected = collectionView.indexPathsForSelectedItems?.first
|
||||||
|
|
||||||
var instancesSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
var instancesSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
||||||
instancesSnapshot.append([.savedInstancesHeader])
|
instancesSnapshot.append([.savedInstancesHeader])
|
||||||
instancesSnapshot.expand([.savedInstancesHeader])
|
instancesSnapshot.expand([.savedInstancesHeader])
|
||||||
let sortedInstances = SavedDataManager.shared.savedInstances(for: mastodonController.accountInfo!)
|
let sortedInstances = SavedDataManager.shared.savedInstances(for: mastodonController.accountInfo!)
|
||||||
instancesSnapshot.append(sortedInstances.map { .savedInstance($0) }, to: .savedInstancesHeader)
|
instancesSnapshot.append(sortedInstances.map { .savedInstance($0) }, to: .savedInstancesHeader)
|
||||||
instancesSnapshot.append([.addSavedInstance], 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
|
// todo: deduplicate with ExploreViewController
|
||||||
|
@ -250,11 +291,11 @@ class MainSidebarViewController: UIViewController {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
extension MainSidebarViewController {
|
extension MainSidebarViewController {
|
||||||
enum Section: Int, Hashable, CaseIterable {
|
enum Section: Int, Hashable, CaseIterable {
|
||||||
case tabs
|
case tabs
|
||||||
case compose
|
case compose
|
||||||
|
case discover
|
||||||
case lists
|
case lists
|
||||||
case savedHashtags
|
case savedHashtags
|
||||||
case savedInstances
|
case savedInstances
|
||||||
|
@ -262,6 +303,7 @@ extension MainSidebarViewController {
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case tab(MainTabBarViewController.Tab)
|
case tab(MainTabBarViewController.Tab)
|
||||||
case search, bookmarks
|
case search, bookmarks
|
||||||
|
case trendingTags, profileDirectory
|
||||||
case listsHeader, list(List), addList
|
case listsHeader, list(List), addList
|
||||||
case savedHashtagsHeader, savedHashtag(Hashtag), addSavedHashtag
|
case savedHashtagsHeader, savedHashtag(Hashtag), addSavedHashtag
|
||||||
case savedInstancesHeader, savedInstance(URL), addSavedInstance
|
case savedInstancesHeader, savedInstance(URL), addSavedInstance
|
||||||
|
@ -274,6 +316,10 @@ extension MainSidebarViewController {
|
||||||
return "Search"
|
return "Search"
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return "Bookmarks"
|
return "Bookmarks"
|
||||||
|
case .trendingTags:
|
||||||
|
return "Trending Hashtags"
|
||||||
|
case .profileDirectory:
|
||||||
|
return "Profile Directory"
|
||||||
case .listsHeader:
|
case .listsHeader:
|
||||||
return "Lists"
|
return "Lists"
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
|
@ -303,6 +349,10 @@ extension MainSidebarViewController {
|
||||||
return "magnifyingglass"
|
return "magnifyingglass"
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return "bookmark"
|
return "bookmark"
|
||||||
|
case .trendingTags:
|
||||||
|
return "arrow.up.arrow.down"
|
||||||
|
case .profileDirectory:
|
||||||
|
return "person.2.fill"
|
||||||
case .list(_):
|
case .list(_):
|
||||||
return "list.bullet"
|
return "list.bullet"
|
||||||
case .savedHashtag(_):
|
case .savedHashtag(_):
|
||||||
|
@ -360,7 +410,6 @@ fileprivate extension MainTabBarViewController.Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
extension MainSidebarViewController: UICollectionViewDelegate {
|
extension MainSidebarViewController: UICollectionViewDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||||
previouslySelectedItem = selectedItem
|
previouslySelectedItem = selectedItem
|
||||||
|
@ -395,7 +444,6 @@ extension MainSidebarViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
extension MainSidebarViewController: UICollectionViewDragDelegate {
|
extension MainSidebarViewController: UICollectionViewDragDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||||
|
@ -408,7 +456,6 @@ extension MainSidebarViewController: UICollectionViewDragDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
extension MainSidebarViewController: InstanceTimelineViewControllerDelegate {
|
extension MainSidebarViewController: InstanceTimelineViewControllerDelegate {
|
||||||
func didSaveInstance(url: URL) {
|
func didSaveInstance(url: URL) {
|
||||||
dismiss(animated: true) {
|
dismiss(animated: true) {
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
class MainSplitViewController: UISplitViewController {
|
class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
@ -103,7 +102,6 @@ class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
extension MainSplitViewController: UISplitViewControllerDelegate {
|
extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
/// Transfer the navigation stack for a sidebar item to a destination navgiation controller.
|
/// Transfer the navigation stack for a sidebar item to a destination navgiation controller.
|
||||||
/// - Parameter dropFirst: Remove the first view controller from the item's navigation stack before transferring.
|
/// - Parameter 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)
|
tabBarViewController.select(tab: .explore)
|
||||||
|
|
||||||
case .bookmarks, .list(_), .savedHashtag(_), .savedInstance(_):
|
case .bookmarks, .trendingTags, .profileDirectory, .list(_), .savedHashtag(_), .savedInstance(_):
|
||||||
tabBarViewController.select(tab: .explore)
|
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
|
// 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.
|
// in compact mode and performing a search.
|
||||||
|
@ -279,6 +277,10 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
exploreItem = .savedHashtag(hashtagVC.hashtag)
|
exploreItem = .savedHashtag(hashtagVC.hashtag)
|
||||||
} else if let instanceVC = tabNavigationStack[1] as? InstanceTimelineViewController {
|
} else if let instanceVC = tabNavigationStack[1] as? InstanceTimelineViewController {
|
||||||
exploreItem = .savedInstance(instanceVC.instanceURL)
|
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)
|
transferNavigationStack(from: tabNavController, to: exploreItem!, skipFirst: 1, prepend: toPrepend)
|
||||||
|
|
||||||
|
@ -307,7 +309,6 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
||||||
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController) {
|
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController) {
|
||||||
presentCompose()
|
presentCompose()
|
||||||
|
@ -322,7 +323,6 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
fileprivate extension MainSidebarViewController.Item {
|
fileprivate extension MainSidebarViewController.Item {
|
||||||
func createRootViewController(_ mastodonController: MastodonController) -> UIViewController? {
|
func createRootViewController(_ mastodonController: MastodonController) -> UIViewController? {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -332,19 +332,22 @@ fileprivate extension MainSidebarViewController.Item {
|
||||||
return SearchViewController(mastodonController: mastodonController)
|
return SearchViewController(mastodonController: mastodonController)
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return BookmarksTableViewController(mastodonController: mastodonController)
|
return BookmarksTableViewController(mastodonController: mastodonController)
|
||||||
|
case .trendingTags:
|
||||||
|
return TrendingHashtagsViewController(mastodonController: mastodonController)
|
||||||
|
case .profileDirectory:
|
||||||
|
return ProfileDirectoryViewController(mastodonController: mastodonController)
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
return ListTimelineViewController(for: list, mastodonController: mastodonController)
|
return ListTimelineViewController(for: list, mastodonController: mastodonController)
|
||||||
case let .savedHashtag(hashtag):
|
case let .savedHashtag(hashtag):
|
||||||
return HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController)
|
return HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController)
|
||||||
case let .savedInstance(url):
|
case let .savedInstance(url):
|
||||||
return InstanceTimelineViewController(for: url, parentMastodonController: mastodonController)
|
return InstanceTimelineViewController(for: url, parentMastodonController: mastodonController)
|
||||||
default:
|
case .listsHeader, .addList, .savedHashtagsHeader, .addSavedHashtag, .savedInstancesHeader, .addSavedInstance:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
extension MainSplitViewController: TuskerRootViewController {
|
extension MainSplitViewController: TuskerRootViewController {
|
||||||
@objc func presentCompose() {
|
@objc func presentCompose() {
|
||||||
let vc = ComposeHostingController(mastodonController: mastodonController)
|
let vc = ComposeHostingController(mastodonController: mastodonController)
|
||||||
|
@ -381,7 +384,6 @@ extension MainSplitViewController: TuskerRootViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
|
||||||
extension MainSplitViewController: BackgroundableViewController {
|
extension MainSplitViewController: BackgroundableViewController {
|
||||||
func sceneDidEnterBackground() {
|
func sceneDidEnterBackground() {
|
||||||
if traitCollection.horizontalSizeClass == .compact {
|
if traitCollection.horizontalSizeClass == .compact {
|
||||||
|
|
|
@ -98,7 +98,9 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
guard case let .success(newNotifications, pagination) = response else { fatalError() }
|
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)
|
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||||
|
|
||||||
|
@ -211,7 +213,7 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
for indexPath in indexPaths {
|
||||||
for notification in item(for: indexPath).notifications {
|
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
|
automationSection
|
||||||
cachingSection
|
cachingSection
|
||||||
}
|
}
|
||||||
.insetOrGroupedListStyle()
|
.listStyle(InsetGroupedListStyle())
|
||||||
.navigationBarTitle(Text("Advanced"))
|
.navigationBarTitle(Text("Advanced"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ struct AppearancePrefsView : View {
|
||||||
accountsSection
|
accountsSection
|
||||||
postsSection
|
postsSection
|
||||||
}
|
}
|
||||||
.insetOrGroupedListStyle()
|
.listStyle(InsetGroupedListStyle())
|
||||||
.navigationBarTitle(Text("Appearance"))
|
.navigationBarTitle(Text("Appearance"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ struct BehaviorPrefsView: View {
|
||||||
linksSection
|
linksSection
|
||||||
contentWarningsSection
|
contentWarningsSection
|
||||||
}
|
}
|
||||||
.insetOrGroupedListStyle()
|
.listStyle(InsetGroupedListStyle())
|
||||||
.navigationBarTitle(Text("Behavior"))
|
.navigationBarTitle(Text("Behavior"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ struct ComposingPrefsView: View {
|
||||||
composingSection
|
composingSection
|
||||||
replyingSection
|
replyingSection
|
||||||
}
|
}
|
||||||
.insetOrGroupedListStyle()
|
.listStyle(InsetGroupedListStyle())
|
||||||
.navigationBarTitle("Composing")
|
.navigationBarTitle("Composing")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,11 +38,9 @@ struct LocalAccountAvatarView: View {
|
||||||
let controller = MastodonController.getForAccount(localAccountInfo)
|
let controller = MastodonController.getForAccount(localAccountInfo)
|
||||||
controller.getOwnAccount { (result) in
|
controller.getOwnAccount { (result) in
|
||||||
guard case let .success(account) = result else { return }
|
guard case let .success(account) = result else { return }
|
||||||
_ = ImageCache.avatars.get(account.avatar) { (data) in
|
_ = ImageCache.avatars.get(account.avatar) { (_, image) in
|
||||||
if let data = data, let image = UIImage(data: data) {
|
DispatchQueue.main.async {
|
||||||
DispatchQueue.main.async {
|
self.avatarImage = image
|
||||||
self.avatarImage = image
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ struct MediaPrefsView: View {
|
||||||
List {
|
List {
|
||||||
viewingSection
|
viewingSection
|
||||||
}
|
}
|
||||||
.insetOrGroupedListStyle()
|
.listStyle(InsetGroupedListStyle())
|
||||||
.navigationBarTitle("Media")
|
.navigationBarTitle("Media")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ struct PreferencesView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.insetOrGroupedListStyle()
|
.listStyle(InsetGroupedListStyle())
|
||||||
.navigationBarTitle(Text("Preferences"), displayMode: .inline)
|
.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
|
#if DEBUG
|
||||||
struct PreferencesView_Previews : PreviewProvider {
|
struct PreferencesView_Previews : PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
|
|
|
@ -14,7 +14,7 @@ struct SilentActionPrefs : View {
|
||||||
List(Array(preferences.silentActions.keys), id: \.self) { source in
|
List(Array(preferences.silentActions.keys), id: \.self) { source in
|
||||||
SilentActionPermissionCell(source: source)
|
SilentActionPermissionCell(source: source)
|
||||||
}
|
}
|
||||||
.insetOrGroupedListStyle()
|
.listStyle(InsetGroupedListStyle())
|
||||||
// .navigationBarTitle("Silent Action Permissions")
|
// .navigationBarTitle("Silent Action Permissions")
|
||||||
// see FB6838291
|
// see FB6838291
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ struct WellnessPrefsView: View {
|
||||||
notificationsMode
|
notificationsMode
|
||||||
grayscaleImages
|
grayscaleImages
|
||||||
}
|
}
|
||||||
.insetOrGroupedListStyle()
|
.listStyle(InsetGroupedListStyle())
|
||||||
.navigationBarTitle(Text("Digital Wellness"))
|
.navigationBarTitle(Text("Digital Wellness"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,17 +43,10 @@ class MyProfileViewController: ProfileViewController {
|
||||||
|
|
||||||
private func setAvatarTabBarImage<Account: AccountProtocol>(account: Account) {
|
private func setAvatarTabBarImage<Account: AccountProtocol>(account: Account) {
|
||||||
let avatarURL = account.avatar
|
let avatarURL = account.avatar
|
||||||
_ = ImageCache.avatars.get(avatarURL, completion: { [weak self] (data) in
|
_ = ImageCache.avatars.get(avatarURL, completion: { [weak self] (_, image) in
|
||||||
guard let self = self, let data = data else { return }
|
guard let self = self,
|
||||||
|
let image = image,
|
||||||
let maybeGrayscale: UIImage?
|
let maybeGrayscale = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||||
if Preferences.shared.grayscaleImages {
|
|
||||||
maybeGrayscale = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
|
||||||
} else {
|
|
||||||
maybeGrayscale = UIImage(data: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let image = maybeGrayscale else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +56,7 @@ class MyProfileViewController: ProfileViewController {
|
||||||
let tabBarImage = UIGraphicsImageRenderer(size: size).image { (_) in
|
let tabBarImage = UIGraphicsImageRenderer(size: size).image { (_) in
|
||||||
let radius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
let radius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
||||||
UIBezierPath(roundedRect: rect, cornerRadius: radius).addClip()
|
UIBezierPath(roundedRect: rect, cornerRadius: radius).addClip()
|
||||||
image.draw(in: rect)
|
maybeGrayscale.draw(in: rect)
|
||||||
}
|
}
|
||||||
let alwaysOriginalImage = tabBarImage.withRenderingMode(.alwaysOriginal)
|
let alwaysOriginalImage = tabBarImage.withRenderingMode(.alwaysOriginal)
|
||||||
self.tabBarItem.image = alwaysOriginalImage
|
self.tabBarItem.image = alwaysOriginalImage
|
||||||
|
|
|
@ -145,7 +145,9 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.newer = pagination?.newer
|
if let newer = pagination?.newer {
|
||||||
|
self.newer = newer
|
||||||
|
}
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
completion(statuses.map { ($0.id, .unknown) })
|
||||||
|
@ -182,7 +184,8 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
|
||||||
}
|
}
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: newPinnedStatues) {
|
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 pinnedStatues = newPinnedStatues.map { (status) -> TimelineEntry in
|
||||||
let state: StatusState
|
let state: StatusState
|
||||||
if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) {
|
if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) {
|
||||||
|
@ -193,13 +196,14 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
|
||||||
return (status.id, state)
|
return (status.id, state)
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if self.sections.count < 1 {
|
|
||||||
self.sections.append(pinnedStatues)
|
|
||||||
} else {
|
|
||||||
self.sections[0] = pinnedStatues
|
|
||||||
}
|
|
||||||
UIView.performWithoutAnimation {
|
UIView.performWithoutAnimation {
|
||||||
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
|
if self.sections.count < 1 {
|
||||||
|
self.sections.append(pinnedStatues)
|
||||||
|
self.tableView.insertSections(IndexSet(integer: 0), with: .none)
|
||||||
|
} else {
|
||||||
|
self.sections[0] = pinnedStatues
|
||||||
|
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -238,33 +242,20 @@ extension ProfileStatusesViewController: StatusTableViewCellDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching {
|
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
let ids = indexPaths.map { item(for: $0).id }
|
||||||
let statusID = item(for: indexPath).id
|
prefetchStatuses(with: ids)
|
||||||
|
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
|
||||||
for attachment in status.attachments where attachment.kind == .image {
|
|
||||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
let ids: [String] = indexPaths.compactMap {
|
||||||
let statusID = item(for: indexPath).id
|
guard $0.section < sections.count,
|
||||||
|
$0.row < sections[$0.section].count else {
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
return nil
|
||||||
continue
|
|
||||||
}
|
|
||||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
|
||||||
for attachment in status.attachments where attachment.kind == .image {
|
|
||||||
ImageCache.avatars.cancelWithoutCallback(attachment.url)
|
|
||||||
}
|
}
|
||||||
|
return item(for: $0).id
|
||||||
}
|
}
|
||||||
|
cancelPrefetchingStatuses(with: ids)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,13 +69,11 @@ class ProfileViewController: UIPageViewController {
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .systemBackground
|
||||||
|
|
||||||
let composeButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composeMentioning))
|
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: [
|
||||||
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
|
||||||
UIAction(title: "Direct Message", image: UIImage(systemName: Status.Visibility.direct.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
|
self.composeDirectMentioning()
|
||||||
self.composeDirectMentioning()
|
})
|
||||||
})
|
])
|
||||||
])
|
|
||||||
}
|
|
||||||
navigationItem.rightBarButtonItem = composeButton
|
navigationItem.rightBarButtonItem = composeButton
|
||||||
|
|
||||||
headerView = ProfileHeaderView.create()
|
headerView = ProfileHeaderView.create()
|
||||||
|
|
|
@ -132,6 +132,7 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
activityIndicator.isHidden = false
|
activityIndicator.isHidden = false
|
||||||
activityIndicator.startAnimating()
|
activityIndicator.startAnimating()
|
||||||
|
|
||||||
|
let resultTypes = self.resultTypes
|
||||||
let request = Client.search(query: query, types: resultTypes, resolve: true, limit: 10)
|
let request = Client.search(query: query, types: resultTypes, resolve: true, limit: 10)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
guard case let .success(results, _) = response else { fatalError() }
|
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.appendSections([.accounts])
|
||||||
snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts)
|
snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts)
|
||||||
addAccounts(results.accounts)
|
addAccounts(results.accounts)
|
||||||
}
|
}
|
||||||
if !results.hashtags.isEmpty {
|
if !results.hashtags.isEmpty && (resultTypes == nil || resultTypes!.contains(.hashtags)) {
|
||||||
snapshot.appendSections([.hashtags])
|
snapshot.appendSections([.hashtags])
|
||||||
snapshot.appendItems(results.hashtags.map { .hashtag($0) }, toSection: .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.appendSections([.statuses])
|
||||||
snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses)
|
snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses)
|
||||||
addStatuses(results.statuses)
|
addStatuses(results.statuses)
|
||||||
|
|
|
@ -113,7 +113,11 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
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) {
|
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
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]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
let ids = indexPaths.map { item(for: $0).id }
|
||||||
guard let status = mastodonController.persistentContainer.status(for: item(for: indexPath).id) else {
|
prefetchStatuses(with: ids)
|
||||||
continue
|
|
||||||
}
|
|
||||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
|
||||||
for attachment in status.attachments where attachment.kind == .image {
|
|
||||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
let ids: [String] = indexPaths.compactMap {
|
||||||
// todo: this means when removing cells, we can't cancel prefetching
|
guard $0.section < sections.count,
|
||||||
// is this an issue?
|
$0.row < sections[$0.section].count else {
|
||||||
guard indexPath.section < sections.count,
|
return nil
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
return item(for: $0).id
|
||||||
}
|
}
|
||||||
|
cancelPrefetchingStatuses(with: ids)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ extension MenuPreviewProvider {
|
||||||
guard mastodonController.loggedIn else {
|
guard mastodonController.loggedIn else {
|
||||||
return [
|
return [
|
||||||
openInSafariAction(url: account.url),
|
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 }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
||||||
})
|
})
|
||||||
|
@ -54,14 +54,14 @@ extension MenuPreviewProvider {
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
if accountID != mastodonController.account.id,
|
if accountID != mastodonController.account.id {
|
||||||
#available(iOS 14.0, *) {
|
|
||||||
actionsSection.append(UIDeferredMenuElement({ (elementHandler) in
|
actionsSection.append(UIDeferredMenuElement({ (elementHandler) in
|
||||||
guard let mastodonController = self.mastodonController else {
|
guard let mastodonController = self.mastodonController else {
|
||||||
elementHandler([])
|
elementHandler([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let request = Client.getRelationships(accounts: [account.id])
|
let request = Client.getRelationships(accounts: [account.id])
|
||||||
|
// talk about callback hell :/
|
||||||
mastodonController.run(request) { [weak self] (response) in
|
mastodonController.run(request) { [weak self] (response) in
|
||||||
if let self = self,
|
if let self = self,
|
||||||
case let .success(results, _) = response,
|
case let .success(results, _) = response,
|
||||||
|
@ -89,7 +89,7 @@ extension MenuPreviewProvider {
|
||||||
|
|
||||||
let shareSection = [
|
let shareSection = [
|
||||||
openInSafariAction(url: account.url),
|
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 }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
||||||
})
|
})
|
||||||
|
@ -104,7 +104,7 @@ extension MenuPreviewProvider {
|
||||||
func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] {
|
func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] {
|
||||||
return [
|
return [
|
||||||
openInSafariAction(url: url),
|
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 }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forURL: url, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forURL: url, sourceView: sourceView)
|
||||||
})
|
})
|
||||||
|
@ -139,7 +139,7 @@ extension MenuPreviewProvider {
|
||||||
guard mastodonController.loggedIn else {
|
guard mastodonController.loggedIn else {
|
||||||
return [
|
return [
|
||||||
openInSafariAction(url: status.url!),
|
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 }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
||||||
})
|
})
|
||||||
|
@ -190,7 +190,7 @@ extension MenuPreviewProvider {
|
||||||
|
|
||||||
var shareSection = [
|
var shareSection = [
|
||||||
openInSafariAction(url: status.url!),
|
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 }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
||||||
}),
|
}),
|
||||||
|
@ -223,8 +223,8 @@ extension MenuPreviewProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openInSafariAction(url: URL) -> UIAction {
|
private func openInSafariAction(url: URL) -> UIAction {
|
||||||
return createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in
|
return createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { [weak self] (_) in
|
||||||
self.navigationDelegate?.selected(url: url, allowUniversalLinks: false)
|
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() {
|
private func pruneOffscreenRows() {
|
||||||
guard let lastVisibleRow = lastLastVisibleRow else {
|
guard let lastVisibleRow = lastLastVisibleRow,
|
||||||
|
// never remove the last section
|
||||||
|
sections.count - headerSectionsCount() > 1 else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let lastSectionIndex = sections.count - 1
|
let lastSectionIndex = sections.count - 1
|
||||||
|
@ -134,11 +136,11 @@ class TimelineLikeTableViewController<Item>: EnhancedTableViewController, Refres
|
||||||
}
|
}
|
||||||
willRemoveRows(at: indexPathsToRemove)
|
willRemoveRows(at: indexPathsToRemove)
|
||||||
|
|
||||||
sections.removeSubrange(sectionsToRemove)
|
|
||||||
|
|
||||||
UIView.performWithoutAnimation {
|
UIView.performWithoutAnimation {
|
||||||
tableView.deleteSections(IndexSet(sectionsToRemove), with: .none)
|
tableView.deleteSections(IndexSet(sectionsToRemove), with: .none)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sections.removeSubrange(sectionsToRemove)
|
||||||
} else if lastVisibleRow.section == lastSectionIndex {
|
} else if lastVisibleRow.section == lastSectionIndex {
|
||||||
let lastSection = sections.last!
|
let lastSection = sections.last!
|
||||||
let lastRowIndex = lastSection.count - 1
|
let lastRowIndex = lastSection.count - 1
|
||||||
|
@ -146,7 +148,7 @@ class TimelineLikeTableViewController<Item>: EnhancedTableViewController, Refres
|
||||||
if lastVisibleRow.row < lastRowIndex - pageSize {
|
if lastVisibleRow.row < lastRowIndex - pageSize {
|
||||||
// if there are more than pageSize rows in the current section below the last visible one
|
// 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 {
|
let indexPathsToRemove = rowIndicesInLastSectionToRemove.map {
|
||||||
IndexPath(row: $0, section: lastSectionIndex)
|
IndexPath(row: $0, section: lastSectionIndex)
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@available(iOS 13.4, *)
|
|
||||||
class TrackpadScrollGestureRecognizer: UIPanGestureRecognizer {
|
class TrackpadScrollGestureRecognizer: UIPanGestureRecognizer {
|
||||||
|
|
||||||
override init(target: Any?, action: Selector?) {
|
override init(target: Any?, action: Selector?) {
|
||||||
|
|
|
@ -12,6 +12,8 @@ import Pachyderm
|
||||||
|
|
||||||
protocol TuskerNavigationDelegate: UIViewController {
|
protocol TuskerNavigationDelegate: UIViewController {
|
||||||
var apiController: MastodonController { get }
|
var apiController: MastodonController { get }
|
||||||
|
|
||||||
|
func conversation(mainStatusID: String, state: StatusState) -> ConversationTableViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TuskerNavigationDelegate {
|
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) {
|
func selected(status statusID: String) {
|
||||||
self.selected(status: statusID, state: .unknown)
|
self.selected(status: statusID, state: .unknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
func selected(status statusID: String, state: StatusState) {
|
func selected(status statusID: String, state: StatusState) {
|
||||||
// todo: is this necessary? should the conversation main status cell prevent this
|
show(conversation(mainStatusID: statusID, state: state), sender: self)
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(editing draft: Draft) {
|
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 status = apiController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
|
||||||
guard let url = status.url else { fatalError("Missing url for 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
|
return UIActivityViewController(activityItems: [url, StatusActivityItemSource(status)], applicationActivities: nil)
|
||||||
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 {
|
private func moreOptions(forAccount accountID: String) -> UIActivityViewController {
|
||||||
guard let account = apiController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
|
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)
|
||||||
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?) {
|
func showMoreOptions(forStatus statusID: String, sourceView: UIView?) {
|
||||||
|
|
|
@ -2,9 +2,17 @@
|
||||||
/// From https://github.com/woltapp/blurhash/blob/b23214ddcab803fe1ec9a3e6b20558caf33a23a5/Swift/BlurHashDecode.swift
|
/// From https://github.com/woltapp/blurhash/blob/b23214ddcab803fe1ec9a3e6b20558caf33a23a5/Swift/BlurHashDecode.swift
|
||||||
import UIKit
|
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 {
|
extension UIImage {
|
||||||
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
|
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 sizeFlag = String(blurHash[0]).decode83()
|
||||||
let numY = (sizeFlag / 9) + 1
|
let numY = (sizeFlag / 9) + 1
|
||||||
|
|
|
@ -63,19 +63,16 @@ class AccountTableViewCell: UITableViewCell {
|
||||||
let accountID = self.accountID
|
let accountID = self.accountID
|
||||||
|
|
||||||
let avatarURL = account.avatar
|
let avatarURL = account.avatar
|
||||||
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
guard let self = self else { return }
|
||||||
self.avatarRequest = nil
|
self.avatarRequest = nil
|
||||||
|
|
||||||
let image: UIImage?
|
guard let image = image,
|
||||||
if self.isGrayscale {
|
self.accountID == accountID,
|
||||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { return }
|
||||||
} else {
|
|
||||||
image = UIImage(data: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarImageView.image = image
|
self.avatarImageView.image = transformedImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,11 +69,11 @@ class LargeAccountDetailView: UIView {
|
||||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||||
usernameLabel.text = "@\(account.acct)"
|
usernameLabel.text = "@\(account.acct)"
|
||||||
|
|
||||||
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (_, image) in
|
||||||
guard let self = self, let data = data else { return }
|
guard let self = self, let image = image else { return }
|
||||||
self.avatarRequest = nil
|
self.avatarRequest = nil
|
||||||
DispatchQueue.main.async {
|
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 {
|
var body: some View {
|
||||||
if #available(iOS 14.0, *) {
|
text
|
||||||
text
|
.font(.system(size: CGFloat(fontSize), weight: .semibold))
|
||||||
.font(.system(size: CGFloat(fontSize), weight: .semibold))
|
.onAppear(perform: self.loadEmojis)
|
||||||
.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() {
|
private func loadEmojis() {
|
||||||
let fullRange = NSRange(account.displayName.startIndex..., in: account.displayName)
|
let fullRange = NSRange(account.displayName.startIndex..., in: account.displayName)
|
||||||
let matches = emojiRegex.matches(in: account.displayName, options: [], range: fullRange)
|
let matches = emojiRegex.matches(in: account.displayName, options: [], range: fullRange)
|
||||||
|
@ -54,9 +47,9 @@ struct AccountDisplayNameLabel<Account: AccountProtocol>: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
group.enter()
|
group.enter()
|
||||||
let request = ImageCache.emojis.get(emoji.url) { (data) in
|
let request = ImageCache.emojis.get(emoji.url) { (_, image) in
|
||||||
defer { group.leave() }
|
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 size = CGSize(width: fontSize, height: fontSize)
|
||||||
let renderer = UIGraphicsImageRenderer(size: size)
|
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() {
|
func loadImage() {
|
||||||
let attachmentURL = attachment.url
|
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 }
|
guard let self = self, let data = data else { return }
|
||||||
self.attachmentRequest = nil
|
self.attachmentRequest = nil
|
||||||
if self.attachment.url.pathExtension == "gif" {
|
if self.attachment.url.pathExtension == "gif" {
|
||||||
|
|
|
@ -43,20 +43,13 @@ extension BaseEmojiLabel {
|
||||||
foundEmojis = true
|
foundEmojis = true
|
||||||
|
|
||||||
group.enter()
|
group.enter()
|
||||||
let request = ImageCache.emojis.get(emoji.url) { (data) in
|
let request = ImageCache.emojis.get(emoji.url) { (_, image) in
|
||||||
defer { group.leave() }
|
defer { group.leave() }
|
||||||
guard let data = data else {
|
guard let image = image,
|
||||||
|
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: emoji.url, image: image) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let image: UIImage?
|
emojiImages[emoji.shortcode] = transformedImage
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if let request = request {
|
if let request = request {
|
||||||
emojiRequests.append(request)
|
emojiRequests.append(request)
|
||||||
|
|
|
@ -63,20 +63,13 @@ class ContentTextView: LinkTextView {
|
||||||
|
|
||||||
for emoji in emojis {
|
for emoji in emojis {
|
||||||
group.enter()
|
group.enter()
|
||||||
_ = ImageCache.emojis.get(emoji.url) { (data) in
|
_ = ImageCache.emojis.get(emoji.url) { (_, image) in
|
||||||
defer { group.leave() }
|
defer { group.leave() }
|
||||||
guard let data = data else {
|
guard let image = image,
|
||||||
|
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: emoji.url, image: image) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let image: UIImage?
|
emojiImages[emoji.shortcode] = transformedImage
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,16 +33,12 @@ struct CustomEmojiImageView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadImage() {
|
private func loadImage() {
|
||||||
request = ImageCache.emojis.get(emoji.url) { (data) in
|
request = ImageCache.emojis.get(emoji.url) { (_, image) in
|
||||||
if let data = data, let image = UIImage(data: data) {
|
DispatchQueue.main.async {
|
||||||
DispatchQueue.main.async {
|
self.request = nil
|
||||||
self.request = nil
|
if let image = image {
|
||||||
self.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"?>
|
<?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"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<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="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
@ -17,8 +18,8 @@
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<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">
|
<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"/>
|
<rect key="frame" x="16" y="11" width="71.5" height="22"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
<nil key="textColor"/>
|
<nil key="textColor"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</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) {
|
private func updateThumbnail(url: URL) {
|
||||||
thumbnailImageView.image = nil
|
thumbnailImageView.image = nil
|
||||||
thumbnailURL = url
|
thumbnailURL = url
|
||||||
thumbnailRequest = ImageCache.attachments.get(url) { [weak self] (data) in
|
thumbnailRequest = ImageCache.attachments.get(url) { [weak self] (_, image) in
|
||||||
guard let self = self, self.thumbnailURL == url, let data = data, let image = UIImage(data: data) else { return }
|
guard let self = self, self.thumbnailURL == url, let image = image else { return }
|
||||||
self.thumbnailRequest = nil
|
self.thumbnailRequest = nil
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.thumbnailImageView.image = image
|
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.masksToBounds = true
|
||||||
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
||||||
let avatarURL = account.avatar
|
let avatarURL = account.avatar
|
||||||
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||||
guard let self = self, let data = data, self.group.id == group.id else { return }
|
guard let self = self else { return }
|
||||||
|
guard let image = image,
|
||||||
let image: UIImage?
|
self.group.id == group.id,
|
||||||
if self.isGrayscale {
|
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
|
||||||
} else {
|
|
||||||
image = UIImage(data: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let image = image {
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarRequests.removeValue(forKey: account.id)
|
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)
|
actionAvatarStackView.addArrangedSubview(imageView)
|
||||||
|
@ -133,21 +132,20 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
let avatarURL = account.avatar
|
let avatarURL = account.avatar
|
||||||
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||||
guard let self = self, let data = data, self.group.id == groupID else { return }
|
guard let self = self else { return }
|
||||||
|
guard let image = image,
|
||||||
let image: UIImage?
|
self.group.id == groupID,
|
||||||
if self.isGrayscale {
|
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
|
||||||
} else {
|
|
||||||
image = UIImage(data: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let image = image {
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarRequests.removeValue(forKey: account.id)
|
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.masksToBounds = true
|
||||||
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
||||||
let avatarURL = account.avatar
|
let avatarURL = account.avatar
|
||||||
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||||
guard let self = self, let data = data, self.group.id == group.id else { return }
|
guard let self = self,
|
||||||
|
let image = image,
|
||||||
let image: UIImage?
|
self.group.id == group.id,
|
||||||
if Preferences.shared.grayscaleImages {
|
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
return
|
||||||
} else {
|
|
||||||
image = UIImage(data: data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let image = image {
|
DispatchQueue.main.async {
|
||||||
DispatchQueue.main.async {
|
self.avatarRequests.removeValue(forKey: account.id)
|
||||||
self.avatarRequests.removeValue(forKey: account.id)
|
imageView.image = transformedImage
|
||||||
imageView.image = image
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
avatarStackView.addArrangedSubview(imageView)
|
avatarStackView.addArrangedSubview(imageView)
|
||||||
|
@ -103,21 +99,20 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
let avatarURL = account.avatar
|
let avatarURL = account.avatar
|
||||||
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||||
guard let self = self, let data = data, self.group.id == groupID else { return }
|
guard let self = self else { return }
|
||||||
|
guard let image = image,
|
||||||
let image: UIImage?
|
self.group.id == groupID,
|
||||||
if self.isGrayscale {
|
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
|
||||||
} else {
|
|
||||||
image = UIImage(data: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let image = image {
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarRequests.removeValue(forKey: account.id)
|
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"?>
|
<?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"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18091"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
@ -49,7 +48,7 @@
|
||||||
</label>
|
</label>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</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"/>
|
<rect key="frame" x="34" y="12.5" width="32" height="30"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="width" constant="30" id="gvV-4g-2Xr"/>
|
<constraint firstAttribute="width" constant="30" id="gvV-4g-2Xr"/>
|
||||||
|
@ -76,7 +75,7 @@
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
</objects>
|
</objects>
|
||||||
<resources>
|
<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">
|
<systemColor name="secondaryLabelColor">
|
||||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
|
|
|
@ -68,21 +68,18 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
||||||
actionLabel.setEmojis(account.emojis, identifier: account.id)
|
actionLabel.setEmojis(account.emojis, identifier: account.id)
|
||||||
}
|
}
|
||||||
let avatarURL = account.avatar
|
let avatarURL = account.avatar
|
||||||
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||||
guard let self = self, self.account == account, let data = data else { return }
|
guard let self = self else { return }
|
||||||
self.avatarRequest = nil
|
self.avatarRequest = nil
|
||||||
|
|
||||||
let image: UIImage?
|
guard self.account == account,
|
||||||
if self.isGrayscale {
|
let image = image,
|
||||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||||
} else {
|
return
|
||||||
image = UIImage(data: data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let image = image {
|
DispatchQueue.main.async {
|
||||||
DispatchQueue.main.async {
|
self.avatarImageView.image = transformedImage
|
||||||
self.avatarImageView.image = image
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,10 +74,8 @@ class ProfileHeaderView: UIView {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||||
|
|
||||||
moreButton.addInteraction(UIPointerInteraction(delegate: self))
|
moreButton.addInteraction(UIPointerInteraction(delegate: self))
|
||||||
if #available(iOS 14.0, *) {
|
moreButton.showsMenuAsPrimaryAction = true
|
||||||
moreButton.showsMenuAsPrimaryAction = true
|
moreButton.isContextMenuInteractionEnabled = true
|
||||||
moreButton.isContextMenuInteractionEnabled = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createObservers() {
|
private func createObservers() {
|
||||||
|
@ -110,9 +108,7 @@ class ProfileHeaderView: UIView {
|
||||||
|
|
||||||
updateImages(account: account)
|
updateImages(account: account)
|
||||||
|
|
||||||
if #available(iOS 14.0, *) {
|
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForProfile(accountID: accountID, sourceView: moreButton))
|
||||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForProfile(accountID: accountID, sourceView: moreButton))
|
|
||||||
}
|
|
||||||
|
|
||||||
noteTextView.navigationDelegate = delegate
|
noteTextView.navigationDelegate = delegate
|
||||||
noteTextView.setTextFromHtml(account.note)
|
noteTextView.setTextFromHtml(account.note)
|
||||||
|
@ -191,35 +187,32 @@ class ProfileHeaderView: UIView {
|
||||||
|
|
||||||
let accountID = account.id
|
let accountID = account.id
|
||||||
let avatarURL = account.avatar
|
let avatarURL = account.avatar
|
||||||
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
// always load original for avatars, because ImageCache.avatars stores them scaled-down in memory
|
||||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
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
|
self.avatarRequest = nil
|
||||||
|
|
||||||
let image: UIImage?
|
|
||||||
if self.isGrayscale {
|
|
||||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
|
||||||
} else {
|
|
||||||
image = UIImage(data: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarImageView.image = image
|
self.avatarImageView.image = transformedImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let header = account.header {
|
if let header = account.header {
|
||||||
headerRequest = ImageCache.headers.get(header) { [weak self] (data) in
|
headerRequest = ImageCache.headers.get(header) { [weak self] (_, image) in
|
||||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
guard let self = self,
|
||||||
|
let image = image,
|
||||||
|
self.accountID == accountID,
|
||||||
|
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: header, image: image) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
self.headerRequest = nil
|
self.headerRequest = nil
|
||||||
|
|
||||||
let image: UIImage?
|
|
||||||
if self.isGrayscale {
|
|
||||||
image = ImageGrayscalifier.convert(url: header, data: data)
|
|
||||||
} else {
|
|
||||||
image = UIImage(data: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.headerImageView.image = image
|
self.headerImageView.image = transformedImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -227,14 +220,6 @@ class ProfileHeaderView: UIView {
|
||||||
|
|
||||||
// MARK: Interaction
|
// 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() {
|
@objc func avatarPressed() {
|
||||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
|
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
|
||||||
return
|
return
|
||||||
|
@ -256,7 +241,6 @@ class ProfileHeaderView: UIView {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 13.4, *)
|
|
||||||
extension ProfileHeaderView: UIPointerInteractionDelegate {
|
extension ProfileHeaderView: UIPointerInteractionDelegate {
|
||||||
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
|
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
|
||||||
let preview = UITargetedPreview(view: moreButton)
|
let preview = UITargetedPreview(view: moreButton)
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-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"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18091"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.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">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wT9-2J-uSY">
|
||||||
<rect key="frame" x="16" y="134" width="120" height="120"/>
|
<rect key="frame" x="16" y="134" width="120" height="120"/>
|
||||||
<subviews>
|
<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"/>
|
<rect key="frame" x="2" y="2" width="116" height="116"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="116" id="eDg-Vc-o8R"/>
|
<constraint firstAttribute="height" constant="116" id="eDg-Vc-o8R"/>
|
||||||
|
@ -58,9 +57,6 @@
|
||||||
<userDefinedRuntimeAttributes>
|
<userDefinedRuntimeAttributes>
|
||||||
<userDefinedRuntimeAttribute type="image" keyPath="image" value="ellipsis" catalog="system"/>
|
<userDefinedRuntimeAttribute type="image" keyPath="image" value="ellipsis" catalog="system"/>
|
||||||
</userDefinedRuntimeAttributes>
|
</userDefinedRuntimeAttributes>
|
||||||
<connections>
|
|
||||||
<action selector="morePressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="Td6-rw-Xvr"/>
|
|
||||||
</connections>
|
|
||||||
</view>
|
</view>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="u4P-3i-gEq">
|
<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"/>
|
<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 favoriteButton: UIButton!
|
||||||
@IBOutlet weak var reblogButton: UIButton!
|
@IBOutlet weak var reblogButton: UIButton!
|
||||||
@IBOutlet weak var moreButton: UIButton!
|
@IBOutlet weak var moreButton: UIButton!
|
||||||
|
private(set) var prevThreadLinkView: UIView?
|
||||||
|
private(set) var nextThreadLinkView: UIView?
|
||||||
|
|
||||||
var statusID: String!
|
var statusID: String!
|
||||||
var accountID: String!
|
var accountID: String!
|
||||||
|
@ -94,9 +96,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!]
|
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!]
|
||||||
attachmentsView.isAccessibilityElement = true
|
attachmentsView.isAccessibilityElement = true
|
||||||
|
|
||||||
if #available(iOS 14.0, *) {
|
moreButton.showsMenuAsPrimaryAction = true
|
||||||
moreButton.showsMenuAsPrimaryAction = true
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
|
|
||||||
|
@ -211,10 +211,8 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
reblogButton.accessibilityLabel = NSLocalizedString("Reblog", comment: "reblog button accessibility label")
|
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
|
||||||
// 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))
|
||||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForStatus(status, sourceView: moreButton))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(account: AccountMO) {
|
func updateUI(account: AccountMO) {
|
||||||
|
@ -260,18 +258,14 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
let avatarURL = account.avatar
|
let avatarURL = account.avatar
|
||||||
let accountID = account.id
|
let accountID = account.id
|
||||||
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
|
||||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
guard let self = self,
|
||||||
|
let image = image,
|
||||||
let image: UIImage?
|
self.accountID == accountID,
|
||||||
if self.isGrayscale {
|
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { return }
|
||||||
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
|
||||||
} else {
|
|
||||||
image = UIImage(data: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarImageView.image = image
|
self.avatarImageView.image = transformedImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,6 +276,52 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
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() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue