Compare commits
No commits in common. "5e7a1e5974a479fcc54aa6c900c88e4af7407968" and "dbdf1d39bd579ff1c72e95f6ca5ea4d1ee00dd20" have entirely different histories.
5e7a1e5974
...
dbdf1d39bd
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -1,32 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2022.1 (33)
|
|
||||||
Features/Improvements:
|
|
||||||
- Show notifications when subscribed to other people's posts
|
|
||||||
- Use context menu for filter/sort in Profile Directory
|
|
||||||
- Enable data detectors (flight numbers, addresses, shippment numbers, phone numbers, currency (iOS 16), and physical units (iOS 16)) for the main status in Conversation
|
|
||||||
- iPadOS: Two column navigation
|
|
||||||
- In potrait orientation with the sidebar hidden, or in landscape, Tusker uses two column navigation on iPad
|
|
||||||
- Selecting something will open a second column in which navigation takes place
|
|
||||||
- The second column can be closed from its top level
|
|
||||||
- You can drill down farther inside the second column
|
|
||||||
- Selecting a different item in the first column replaces the second column
|
|
||||||
- iPadOS: Move Trending Hashtags/Links to Explore screen (formerly Search)
|
|
||||||
- Trending Statuses and Profile Directory will also be moved in a future version
|
|
||||||
- iPadOS: Add context menu action for deleting drafts
|
|
||||||
- iOS 16: Add Live Text to images in the gallery view
|
|
||||||
- iOS 16: Show favorite/reblog context menu actions
|
|
||||||
- iOS 16: Show full size previews when long-pressing attachments on the Compose screen
|
|
||||||
- iOS 16: Show formatting actions in edit menu on Compose screen
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Fix attachments on Pleroma not being served as the correct content type
|
|
||||||
- Fix not being able to open some hashtags with non-ASCII characters
|
|
||||||
- Fix crash when leaving the app shortly after opening it
|
|
||||||
- Fix crash when loading notifications on Pixelfed
|
|
||||||
- Fix crash due to retain cycle when changing preferences
|
|
||||||
- iPadOS: Fix crash when opening a conversation in a new window
|
|
||||||
|
|
||||||
## 2022.1 (31)
|
## 2022.1 (31)
|
||||||
Bugfixes:
|
Bugfixes:
|
||||||
- Fix not being able to post attachments with descriptions
|
- Fix not being able to post attachments with descriptions
|
||||||
|
|
|
@ -16,7 +16,7 @@ let package = Package(
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
// Dependencies declare other packages that this package depends on.
|
// Dependencies declare other packages that this package depends on.
|
||||||
.package(url: "https://github.com/karwa/swift-url.git", branch: "main"),
|
.package(url: "https://github.com/karwa/swift-url.git", from: "0.3.1"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum DirectoryOrder: String, CaseIterable {
|
public enum DirectoryOrder: String {
|
||||||
case active
|
case active
|
||||||
case new
|
case new
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,9 +39,3 @@ extension Emoji: CustomDebugStringConvertible {
|
||||||
return ":\(shortcode):"
|
return ":\(shortcode):"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Emoji: Equatable {
|
|
||||||
public static func ==(lhs: Emoji, rhs: Emoji) -> Bool {
|
|
||||||
return lhs.shortcode == rhs.shortcode && lhs.url == rhs.url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,13 +12,13 @@ import WebURLFoundationExtras
|
||||||
|
|
||||||
public class Hashtag: Codable {
|
public class Hashtag: Codable {
|
||||||
public let name: String
|
public let name: String
|
||||||
public let url: WebURL
|
public let url: URL
|
||||||
/// Only present when returned from the trending hashtags endpoint
|
/// Only present when returned from the trending hashtags endpoint
|
||||||
public let history: [History]?
|
public let history: [History]?
|
||||||
|
|
||||||
public init(name: String, url: URL) {
|
public init(name: String, url: URL) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.url = WebURL(url)!
|
self.url = url
|
||||||
self.history = nil
|
self.history = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,14 +26,25 @@ public class Hashtag: Codable {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
self.name = try container.decode(String.self, forKey: .name)
|
self.name = try container.decode(String.self, forKey: .name)
|
||||||
// pixelfed (possibly others) don't fully escape special characters in the hashtag url
|
// pixelfed (possibly others) don't fully escape special characters in the hashtag url
|
||||||
self.url = try container.decode(WebURL.self, forKey: .url)
|
do {
|
||||||
|
let webURL = try container.decode(WebURL.self, forKey: .url)
|
||||||
|
if let url = URL(webURL) {
|
||||||
|
self.url = url
|
||||||
|
} else {
|
||||||
|
let s = try? container.decode(String.self, forKey: .url)
|
||||||
|
throw DecodingError.dataCorruptedError(forKey: .url, in: container, debugDescription: "unable to convert WebURL \(s?.debugDescription ?? "nil") to URL")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
let s = try? container.decode(String.self, forKey: .url)
|
||||||
|
throw DecodingError.dataCorruptedError(forKey: .url, in: container, debugDescription: "unable to decode WebURL from \(s?.debugDescription ?? "nil")")
|
||||||
|
}
|
||||||
self.history = try container.decodeIfPresent([History].self, forKey: .history)
|
self.history = try container.decodeIfPresent([History].self, forKey: .history)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
public func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
try container.encode(name, forKey: .name)
|
try container.encode(name, forKey: .name)
|
||||||
try container.encode(url, forKey: .url)
|
try container.encode(url.absoluteString, forKey: .url)
|
||||||
try container.encodeIfPresent(history, forKey: .history)
|
try container.encodeIfPresent(history, forKey: .history)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,14 +16,6 @@ public class Mention: Codable {
|
||||||
/// The instance-local ID of the user being mentioned.
|
/// The instance-local ID of the user being mentioned.
|
||||||
public let id: String
|
public let id: String
|
||||||
|
|
||||||
public required init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
self.username = try container.decode(String.self, forKey: .username)
|
|
||||||
self.acct = try container.decode(String.self, forKey: .acct)
|
|
||||||
self.id = try container.decode(String.self, forKey: .id)
|
|
||||||
self.url = try container.decode(WebURL.self, forKey: .url)
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case url
|
case url
|
||||||
case username
|
case username
|
||||||
|
|
|
@ -21,10 +21,6 @@ public class Notification: Decodable {
|
||||||
self.id = try container.decode(String.self, forKey: .id)
|
self.id = try container.decode(String.self, forKey: .id)
|
||||||
if let kind = try? container.decode(Kind.self, forKey: .kind) {
|
if let kind = try? container.decode(Kind.self, forKey: .kind) {
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
} else if let s = try? container.decode(String.self, forKey: .kind),
|
|
||||||
s == "status" {
|
|
||||||
// represent notifications of other people posting as regular mentions for now
|
|
||||||
self.kind = .mention
|
|
||||||
} else {
|
} else {
|
||||||
self.kind = .unknown
|
self.kind = .unknown
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
//
|
|
||||||
// URLTests.swift
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 5/17/22.
|
|
||||||
//
|
|
||||||
|
|
||||||
import XCTest
|
|
||||||
import WebURL
|
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
class URLTests: XCTestCase {
|
|
||||||
|
|
||||||
func testDecodeURL() {
|
|
||||||
XCTAssertNotNil(WebURL(URL(string: "https://xn--baw-joa.social/@unituebingen")!))
|
|
||||||
XCTAssertNotNil(WebURL("https://xn--baw-joa.social/@unituebingen"))
|
|
||||||
XCTAssertNotNil(URLComponents(string: "https://xn--baw-joa.social/test/é"))
|
|
||||||
XCTAssertNotNil(WebURL("https://xn--baw-joa.social/test/é"))
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
XCTAssertNotNil(try? URL.ParseStrategy().parse("https://xn--baw-joa.social/test/é"))
|
|
||||||
XCTAssertNotNil(try? URL.ParseStrategy().parse("http://見.香港/热狗/🌭"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -18,7 +18,10 @@
|
||||||
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 */; };
|
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 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; };
|
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.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 */; };
|
||||||
|
@ -85,7 +88,6 @@
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
|
||||||
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */; };
|
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */; };
|
||||||
D63A8D0B2561C27F00D9DFFF /* ProfileStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63A8D0A2561C27F00D9DFFF /* ProfileStatusesViewController.swift */; };
|
D63A8D0B2561C27F00D9DFFF /* ProfileStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63A8D0A2561C27F00D9DFFF /* ProfileStatusesViewController.swift */; };
|
||||||
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63D8DF32850FE7A008D95E1 /* ViewTags.swift */; };
|
|
||||||
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; };
|
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; };
|
||||||
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */; };
|
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */; };
|
||||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
|
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
|
||||||
|
@ -219,11 +221,13 @@
|
||||||
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB4423216AF800E5038B /* FollowAccountActivity.swift */; };
|
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB4423216AF800E5038B /* FollowAccountActivity.swift */; };
|
||||||
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB4723216B1D00E5038B /* AccountActivity.swift */; };
|
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB4723216B1D00E5038B /* AccountActivity.swift */; };
|
||||||
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB4923216F0400E5038B /* UnfollowAccountActivity.swift */; };
|
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB4923216F0400E5038B /* UnfollowAccountActivity.swift */; };
|
||||||
|
D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */ = {isa = PBXBuildFile; productRef = D6B0539E23BD2BA300A066FA /* SheetController */; };
|
||||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A123BD2C0600A066FA /* AssetPickerViewController.swift */; };
|
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A123BD2C0600A066FA /* AssetPickerViewController.swift */; };
|
||||||
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A323BD2C8100A066FA /* AssetCollectionsListViewController.swift */; };
|
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A323BD2C8100A066FA /* AssetCollectionsListViewController.swift */; };
|
||||||
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */; };
|
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */; };
|
||||||
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */; };
|
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */; };
|
||||||
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */; };
|
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */; };
|
||||||
|
D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */; };
|
||||||
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */; };
|
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */; };
|
||||||
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */; };
|
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */; };
|
||||||
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */; };
|
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */; };
|
||||||
|
@ -281,17 +285,13 @@
|
||||||
D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AA265AAD6B00C4AA01 /* Media.xcassets */; };
|
D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AA265AAD6B00C4AA01 /* Media.xcassets */; };
|
||||||
D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; };
|
D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; };
|
||||||
D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */; };
|
D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */; };
|
||||||
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
D6E343BA265AAD8C00C4AA01 /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = D6E343B9265AAD8C00C4AA01 /* Action.js */; };
|
D6E343BA265AAD8C00C4AA01 /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = D6E343B9265AAD8C00C4AA01 /* Action.js */; };
|
||||||
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */; };
|
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4267625327FB400C02E1C /* ComposeAutocompleteView.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 */; };
|
||||||
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D6E57FA525C26FAB00341037 /* Localizable.stringsdict */; };
|
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D6E57FA525C26FAB00341037 /* Localizable.stringsdict */; };
|
||||||
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */; };
|
|
||||||
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */; };
|
|
||||||
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */; };
|
|
||||||
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */; };
|
|
||||||
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E9CDA7281A427800BBC98E /* PostService.swift */; };
|
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E9CDA7281A427800BBC98E /* PostService.swift */; };
|
||||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */; };
|
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */; };
|
||||||
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
|
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
|
||||||
|
@ -330,15 +330,15 @@
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
D6E3438F2659849800C4AA01 /* Embed Foundation Extensions */ = {
|
D6E3438F2659849800C4AA01 /* Embed App Extensions */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
dstPath = "";
|
dstPath = "";
|
||||||
dstSubfolderSpec = 13;
|
dstSubfolderSpec = 13;
|
||||||
files = (
|
files = (
|
||||||
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed Foundation Extensions */,
|
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed App Extensions */,
|
||||||
);
|
);
|
||||||
name = "Embed Foundation Extensions";
|
name = "Embed App Extensions";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
D6F953E52125197500CF0F2B /* Embed Frameworks */ = {
|
D6F953E52125197500CF0F2B /* Embed Frameworks */ = {
|
||||||
|
@ -365,7 +365,10 @@
|
||||||
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>"; };
|
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 /* TrendHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendHistoryView.swift; sourceTree = "<group>"; };
|
D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendHistoryView.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>"; };
|
||||||
|
@ -431,7 +434,6 @@
|
||||||
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>"; };
|
||||||
D6370B9B24421FF30092A7FF /* Tusker.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Tusker.xcdatamodel; sourceTree = "<group>"; };
|
D6370B9B24421FF30092A7FF /* Tusker.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Tusker.xcdatamodel; sourceTree = "<group>"; };
|
||||||
D63A8D0A2561C27F00D9DFFF /* ProfileStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusesViewController.swift; sourceTree = "<group>"; };
|
D63A8D0A2561C27F00D9DFFF /* ProfileStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusesViewController.swift; sourceTree = "<group>"; };
|
||||||
D63D8DF32850FE7A008D95E1 /* ViewTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewTags.swift; sourceTree = "<group>"; };
|
|
||||||
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
|
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
|
||||||
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectImageButton.swift; sourceTree = "<group>"; };
|
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectImageButton.swift; sourceTree = "<group>"; };
|
||||||
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
|
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
|
||||||
|
@ -570,6 +572,7 @@
|
||||||
D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewController.swift; sourceTree = "<group>"; };
|
D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewController.swift; sourceTree = "<group>"; };
|
||||||
D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AssetCollectionViewCell.xib; sourceTree = "<group>"; };
|
D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AssetCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||||
|
D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerSheetContainerViewController.swift; sourceTree = "<group>"; };
|
||||||
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OppositeCollapseKeywordsView.swift; sourceTree = "<group>"; };
|
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OppositeCollapseKeywordsView.swift; sourceTree = "<group>"; };
|
||||||
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabbedPageViewController.swift; sourceTree = "<group>"; };
|
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabbedPageViewController.swift; sourceTree = "<group>"; };
|
||||||
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrayscalifier.swift; sourceTree = "<group>"; };
|
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrayscalifier.swift; sourceTree = "<group>"; };
|
||||||
|
@ -643,10 +646,6 @@
|
||||||
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>"; };
|
||||||
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>"; };
|
D6E57FA425C26FAB00341037 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagCollectionViewCell.swift; sourceTree = "<group>"; };
|
|
||||||
D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinkCardCollectionViewCell.swift; sourceTree = "<group>"; };
|
|
||||||
D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TrendingLinkCardCollectionViewCell.xib; sourceTree = "<group>"; };
|
|
||||||
D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitNavigationController.swift; sourceTree = "<group>"; };
|
|
||||||
D6E9CDA7281A427800BBC98E /* PostService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostService.swift; sourceTree = "<group>"; };
|
D6E9CDA7281A427800BBC98E /* PostService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostService.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>"; };
|
||||||
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
|
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -666,6 +665,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */,
|
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */,
|
||||||
|
D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */,
|
||||||
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */,
|
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */,
|
||||||
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
|
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
|
||||||
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
|
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
|
||||||
|
@ -714,7 +714,8 @@
|
||||||
children = (
|
children = (
|
||||||
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */,
|
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */,
|
||||||
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */,
|
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */,
|
||||||
D6E77D08286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift */,
|
D6093FAE25BE0B01004811E6 /* TrendingHashtagTableViewCell.swift */,
|
||||||
|
D6093FAF25BE0B01004811E6 /* TrendingHashtagTableViewCell.xib */,
|
||||||
);
|
);
|
||||||
path = "Hashtag Cell";
|
path = "Hashtag Cell";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -800,11 +801,10 @@
|
||||||
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */,
|
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */,
|
||||||
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */,
|
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */,
|
||||||
D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */,
|
D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */,
|
||||||
D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */,
|
|
||||||
D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */,
|
|
||||||
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
|
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
|
||||||
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
|
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
|
||||||
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
|
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
|
||||||
|
D600613D25D07E170067FAD6 /* ProfileDirectoryFilterView.swift */,
|
||||||
);
|
);
|
||||||
path = Explore;
|
path = Explore;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1240,6 +1240,7 @@
|
||||||
D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */,
|
D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */,
|
||||||
D626493E23C101C500612E6E /* AlbumAssetCollectionViewController.swift */,
|
D626493E23C101C500612E6E /* AlbumAssetCollectionViewController.swift */,
|
||||||
D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */,
|
D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */,
|
||||||
|
D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */,
|
||||||
);
|
);
|
||||||
path = "Asset Picker";
|
path = "Asset Picker";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1300,7 +1301,6 @@
|
||||||
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */,
|
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */,
|
||||||
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
||||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
||||||
D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */,
|
|
||||||
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
|
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
|
||||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
||||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */,
|
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */,
|
||||||
|
@ -1372,7 +1372,6 @@
|
||||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
|
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
|
||||||
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
|
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
|
||||||
D62E9988279DB2D100C26176 /* InstanceFeatures.swift */,
|
D62E9988279DB2D100C26176 /* InstanceFeatures.swift */,
|
||||||
D63D8DF32850FE7A008D95E1 /* ViewTags.swift */,
|
|
||||||
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */,
|
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */,
|
||||||
D6AEBB3F2321640F00E5038B /* Activities */,
|
D6AEBB3F2321640F00E5038B /* Activities */,
|
||||||
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
||||||
|
@ -1492,7 +1491,7 @@
|
||||||
D6D4DDCA212518A000E1C4BB /* Resources */,
|
D6D4DDCA212518A000E1C4BB /* Resources */,
|
||||||
D6F953E52125197500CF0F2B /* Embed Frameworks */,
|
D6F953E52125197500CF0F2B /* Embed Frameworks */,
|
||||||
D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */,
|
D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */,
|
||||||
D6E3438F2659849800C4AA01 /* Embed Foundation Extensions */,
|
D6E3438F2659849800C4AA01 /* Embed App Extensions */,
|
||||||
D6F1F9E127B0677000CB7D88 /* ShellScript */,
|
D6F1F9E127B0677000CB7D88 /* ShellScript */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
|
@ -1502,6 +1501,7 @@
|
||||||
);
|
);
|
||||||
name = Tusker;
|
name = Tusker;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
|
D6B0539E23BD2BA300A066FA /* SheetController */,
|
||||||
D69CCBBE249E6EFD000AF167 /* CrashReporter */,
|
D69CCBBE249E6EFD000AF167 /* CrashReporter */,
|
||||||
D60CFFDA24A290BA00D00083 /* SwiftSoup */,
|
D60CFFDA24A290BA00D00083 /* SwiftSoup */,
|
||||||
D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
|
D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
|
||||||
|
@ -1573,7 +1573,7 @@
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 1250;
|
LastSwiftUpdateCheck = 1250;
|
||||||
LastUpgradeCheck = 1400;
|
LastUpgradeCheck = 1250;
|
||||||
ORGANIZATIONNAME = Shadowfacts;
|
ORGANIZATIONNAME = Shadowfacts;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
D6D4DDCB212518A000E1C4BB = {
|
D6D4DDCB212518A000E1C4BB = {
|
||||||
|
@ -1611,6 +1611,7 @@
|
||||||
);
|
);
|
||||||
mainGroup = D6D4DDC3212518A000E1C4BB;
|
mainGroup = D6D4DDC3212518A000E1C4BB;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
|
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */,
|
||||||
D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */,
|
D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */,
|
||||||
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
||||||
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
|
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
|
||||||
|
@ -1633,7 +1634,6 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */,
|
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */,
|
||||||
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */,
|
|
||||||
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.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 */,
|
||||||
|
@ -1650,6 +1650,7 @@
|
||||||
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */,
|
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */,
|
||||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */,
|
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist 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 */,
|
||||||
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
|
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
|
||||||
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
|
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
|
||||||
|
@ -1740,6 +1741,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 */,
|
||||||
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */,
|
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */,
|
||||||
|
@ -1807,7 +1809,6 @@
|
||||||
D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */,
|
D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */,
|
||||||
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
||||||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
||||||
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */,
|
|
||||||
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
||||||
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
||||||
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
||||||
|
@ -1820,7 +1821,6 @@
|
||||||
D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */,
|
D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */,
|
||||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
|
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
|
||||||
D6C143E025354E34007DC240 /* EmojiPickerCollectionViewController.swift in Sources */,
|
D6C143E025354E34007DC240 /* EmojiPickerCollectionViewController.swift in Sources */,
|
||||||
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
|
||||||
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
||||||
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
||||||
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,
|
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,
|
||||||
|
@ -1853,6 +1853,7 @@
|
||||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
||||||
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
||||||
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
|
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
|
||||||
|
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||||
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */,
|
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */,
|
||||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||||
|
@ -1883,6 +1884,7 @@
|
||||||
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
|
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
|
||||||
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */,
|
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */,
|
||||||
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */,
|
D6C94D892139E6EC00CB5196 /* AttachmentView.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 */,
|
||||||
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */,
|
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */,
|
||||||
|
@ -1923,7 +1925,6 @@
|
||||||
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */,
|
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */,
|
||||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
||||||
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
||||||
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */,
|
|
||||||
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */,
|
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */,
|
||||||
D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */,
|
D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */,
|
||||||
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
||||||
|
@ -1967,7 +1968,6 @@
|
||||||
D6C99FCB24FADC91005C74D3 /* MainComposeTextView.swift in Sources */,
|
D6C99FCB24FADC91005C74D3 /* MainComposeTextView.swift in Sources */,
|
||||||
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
|
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
|
||||||
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */,
|
D6E4267725327FB400C02E1C /* ComposeAutocompleteView.swift in Sources */,
|
||||||
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
|
|
||||||
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */,
|
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */,
|
||||||
D677284E24ECC01D00C732D3 /* Draft.swift in Sources */,
|
D677284E24ECC01D00C732D3 /* Draft.swift in Sources */,
|
||||||
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
|
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
|
||||||
|
@ -2198,10 +2198,11 @@
|
||||||
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 = 33;
|
CURRENT_PROJECT_VERSION = 31;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2228,10 +2229,11 @@
|
||||||
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 = 33;
|
CURRENT_PROJECT_VERSION = 31;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2337,7 +2339,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 33;
|
CURRENT_PROJECT_VERSION = 31;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2364,7 +2366,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 33;
|
CURRENT_PROJECT_VERSION = 31;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2459,6 +2461,14 @@
|
||||||
minimumVersion = 1.8.0;
|
minimumVersion = 1.8.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://git.shadowfacts.net/shadowfacts/SheetController.git";
|
||||||
|
requirement = {
|
||||||
|
branch = master;
|
||||||
|
kind = branch;
|
||||||
|
};
|
||||||
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
@ -2481,6 +2491,11 @@
|
||||||
package = D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */;
|
package = D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */;
|
||||||
productName = CrashReporter;
|
productName = CrashReporter;
|
||||||
};
|
};
|
||||||
|
D6B0539E23BD2BA300A066FA /* SheetController */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */;
|
||||||
|
productName = SheetController;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
|
||||||
/* Begin XCVersionGroup section */
|
/* Begin XCVersionGroup section */
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1400"
|
LastUpgradeVersion = "1250"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1400"
|
LastUpgradeVersion = "1250"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -99,11 +99,6 @@
|
||||||
value = "1"
|
value = "1"
|
||||||
isEnabled = "NO">
|
isEnabled = "NO">
|
||||||
</EnvironmentVariable>
|
</EnvironmentVariable>
|
||||||
<EnvironmentVariable
|
|
||||||
key = "CG_NUMERICS_SHOW_BACKTRACE"
|
|
||||||
value = ""
|
|
||||||
isEnabled = "NO">
|
|
||||||
</EnvironmentVariable>
|
|
||||||
<EnvironmentVariable
|
<EnvironmentVariable
|
||||||
key = "DEBUG_BLUR_HASH"
|
key = "DEBUG_BLUR_HASH"
|
||||||
value = "1"
|
value = "1"
|
||||||
|
|
|
@ -41,6 +41,7 @@ class ImageCache {
|
||||||
let wrappedCompletion: ((Data?, UIImage?) -> Void)?
|
let wrappedCompletion: ((Data?, UIImage?) -> Void)?
|
||||||
if let completion = completion {
|
if let completion = completion {
|
||||||
wrappedCompletion = { (data, image) in
|
wrappedCompletion = { (data, image) in
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
if !loadOriginal,
|
if !loadOriginal,
|
||||||
let size = self.desiredPixelSize {
|
let size = self.desiredPixelSize {
|
||||||
image?.prepareThumbnail(of: size, completionHandler: {
|
image?.prepareThumbnail(of: size, completionHandler: {
|
||||||
|
@ -51,6 +52,11 @@ class ImageCache {
|
||||||
completion(data, $0)
|
completion(data, $0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
self.backgroundQueue.async {
|
||||||
|
completion(data, image)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
wrappedCompletion = nil
|
wrappedCompletion = nil
|
||||||
|
|
|
@ -48,7 +48,7 @@ class MastodonController: ObservableObject {
|
||||||
@Published private(set) var instanceFeatures = InstanceFeatures()
|
@Published private(set) var instanceFeatures = InstanceFeatures()
|
||||||
private(set) var customEmojis: [Emoji]?
|
private(set) var customEmojis: [Emoji]?
|
||||||
|
|
||||||
private var pendingOwnInstanceRequestCallbacks = [(Result<Instance, Client.Error>) -> Void]()
|
private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]()
|
||||||
private var ownInstanceRequest: URLSessionTask?
|
private var ownInstanceRequest: URLSessionTask?
|
||||||
|
|
||||||
var loggedIn: Bool {
|
var loggedIn: Bool {
|
||||||
|
@ -159,28 +159,15 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
|
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
|
||||||
getOwnInstanceInternal(retryAttempt: 0) {
|
getOwnInstanceInternal(retryAttempt: 0, completion: completion)
|
||||||
if case let .success(instance) = $0 {
|
|
||||||
completion?(instance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Instance) -> Void)?) {
|
||||||
func getOwnInstance() async throws -> Instance {
|
|
||||||
return try await withCheckedThrowingContinuation({ continuation in
|
|
||||||
getOwnInstanceInternal(retryAttempt: 0) { result in
|
|
||||||
continuation.resume(with: result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Result<Instance, Client.Error>) -> Void)?) {
|
|
||||||
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
|
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
|
||||||
assert(Thread.isMainThread)
|
assert(Thread.isMainThread)
|
||||||
|
|
||||||
if let instance = self.instance {
|
if let instance = self.instance {
|
||||||
completion?(.success(instance))
|
completion?(instance)
|
||||||
} else {
|
} else {
|
||||||
if let completion = completion {
|
if let completion = completion {
|
||||||
pendingOwnInstanceRequestCallbacks.append(completion)
|
pendingOwnInstanceRequestCallbacks.append(completion)
|
||||||
|
@ -190,7 +177,7 @@ class MastodonController: ObservableObject {
|
||||||
let request = Client.getInstance()
|
let request = Client.getInstance()
|
||||||
ownInstanceRequest = run(request) { (response) in
|
ownInstanceRequest = run(request) { (response) in
|
||||||
switch response {
|
switch response {
|
||||||
case .failure(let error):
|
case .failure(_):
|
||||||
let delay: DispatchTimeInterval
|
let delay: DispatchTimeInterval
|
||||||
switch retryAttempt {
|
switch retryAttempt {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -203,10 +190,6 @@ class MastodonController: ObservableObject {
|
||||||
delay = .seconds(60)
|
delay = .seconds(60)
|
||||||
default:
|
default:
|
||||||
// if we've failed four times, just give up :/
|
// if we've failed four times, just give up :/
|
||||||
for completion in self.pendingOwnInstanceRequestCallbacks {
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
self.pendingOwnInstanceRequestCallbacks = []
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||||
|
@ -221,7 +204,7 @@ class MastodonController: ObservableObject {
|
||||||
self.instanceFeatures.update(instance: instance, nodeInfo: self.nodeInfo)
|
self.instanceFeatures.update(instance: instance, nodeInfo: self.nodeInfo)
|
||||||
|
|
||||||
for completion in self.pendingOwnInstanceRequestCallbacks {
|
for completion in self.pendingOwnInstanceRequestCallbacks {
|
||||||
completion(.success(instance))
|
completion(instance)
|
||||||
}
|
}
|
||||||
self.pendingOwnInstanceRequestCallbacks = []
|
self.pendingOwnInstanceRequestCallbacks = []
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ struct MenuController {
|
||||||
let data: Any
|
let data: Any
|
||||||
if case let .tab(tab) = item {
|
if case let .tab(tab) = item {
|
||||||
data = tab.rawValue
|
data = tab.rawValue
|
||||||
} else if case .explore = item {
|
} else if case .search = item {
|
||||||
data = "search"
|
data = "search"
|
||||||
} else if case .bookmarks = item {
|
} else if case .bookmarks = item {
|
||||||
data = "bookmarks"
|
data = "bookmarks"
|
||||||
|
@ -42,7 +42,7 @@ struct MenuController {
|
||||||
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"),
|
||||||
sidebarCommand(item: .explore, command: "3"),
|
sidebarCommand(item: .search, command: "3"),
|
||||||
sidebarCommand(item: .bookmarks, command: "4"),
|
sidebarCommand(item: .bookmarks, command: "4"),
|
||||||
sidebarCommand(item: .tab(.myProfile), command: "5"),
|
sidebarCommand(item: .tab(.myProfile), command: "5"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
@objc(SavedHashtag)
|
@objc(SavedHashtag)
|
||||||
public final class SavedHashtag: NSManagedObject {
|
public final class SavedHashtag: NSManagedObject {
|
||||||
|
@ -33,6 +32,6 @@ extension SavedHashtag {
|
||||||
convenience init(hashtag: Hashtag, context: NSManagedObjectContext) {
|
convenience init(hashtag: Hashtag, context: NSManagedObjectContext) {
|
||||||
self.init(context: context)
|
self.init(context: context)
|
||||||
self.name = hashtag.name
|
self.name = hashtag.name
|
||||||
self.url = URL(hashtag.url)!
|
self.url = hashtag.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,10 +34,6 @@ struct InstanceFeatures {
|
||||||
instanceType != .pixelfed
|
instanceType != .pixelfed
|
||||||
}
|
}
|
||||||
|
|
||||||
var trends: Bool {
|
|
||||||
instanceType == .mastodon
|
|
||||||
}
|
|
||||||
|
|
||||||
var trendingStatusesAndLinks: Bool {
|
var trendingStatusesAndLinks: Bool {
|
||||||
instanceType == .mastodon && version != nil && version! >= Version(3, 5, 0)
|
instanceType == .mastodon && version != nil && version! >= Version(3, 5, 0)
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ public struct LazilyDecoding<Enclosing, Value: Codable> {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LazilyDecoding {
|
extension LazilyDecoding {
|
||||||
init<T>(arrayFrom keyPath: ReferenceWritableKeyPath<Enclosing, Data?>) where Value == [T] {
|
init<T: Codable>(arrayFrom keyPath: ReferenceWritableKeyPath<Enclosing, Data?>) where Value == [T] {
|
||||||
self.init(from: keyPath, fallback: [])
|
self.init(from: keyPath, fallback: [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,10 +123,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
rootVC.sceneDidEnterBackground()
|
rootVC.sceneDidEnterBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
if let context = scene.session.mastodonController?.persistentContainer.viewContext,
|
if let context = scene.session.mastodonController?.persistentContainer.viewContext {
|
||||||
// if the user quickly opens and then closes the app, this may race with loading the persistent store, so in that event we skip cleanup/save
|
|
||||||
let psc = context.persistentStoreCoordinator,
|
|
||||||
!psc.persistentStores.isEmpty {
|
|
||||||
var minDate = Date()
|
var minDate = Date()
|
||||||
minDate.addTimeInterval(-7 * 24 * 60 * 60)
|
minDate.addTimeInterval(-7 * 24 * 60 * 60)
|
||||||
|
|
||||||
|
|
|
@ -48,13 +48,13 @@ enum CompositionAttachmentData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getData(completion: @escaping (Result<(Data, UTType), Error>) -> Void) {
|
func getData(completion: @escaping (Result<(Data, String), Error>) -> Void) {
|
||||||
switch self {
|
switch self {
|
||||||
case let .image(image):
|
case let .image(image):
|
||||||
// Export as JPEG instead of PNG, otherweise photos straight from the camera are too large
|
// Export as JPEG instead of PNG, otherweise photos straight from the camera are too large
|
||||||
// for Mastodon in its default configuration (max of 10MB).
|
// for Mastodon in its default configuration (max of 10MB).
|
||||||
// The quality of 0.8 was chosen completely arbitrarily, it may need to be tuned in the future.
|
// The quality of 0.8 was chosen completely arbitrarily, it may need to be tuned in the future.
|
||||||
completion(.success((image.jpegData(compressionQuality: 0.8)!, .jpeg)))
|
completion(.success((image.jpegData(compressionQuality: 0.8)!, "image/jpeg")))
|
||||||
case let .asset(asset):
|
case let .asset(asset):
|
||||||
if asset.mediaType == .image {
|
if asset.mediaType == .image {
|
||||||
let options = PHImageRequestOptions()
|
let options = PHImageRequestOptions()
|
||||||
|
@ -68,19 +68,19 @@ enum CompositionAttachmentData {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let utType: UTType
|
let mimeType: String
|
||||||
if dataUTI == "public.heic" {
|
if dataUTI == "public.heic" {
|
||||||
// neither Mastodon nor Pleroma handles HEIC well, so convert to JPEG
|
// neither Mastodon nor Pleroma handles HEIC well, so convert to JPEG
|
||||||
let image = CIImage(data: data)!
|
let image = CIImage(data: data)!
|
||||||
let context = CIContext()
|
let context = CIContext()
|
||||||
let colorSpace = image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!
|
let colorSpace = image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!
|
||||||
data = context.jpegRepresentation(of: image, colorSpace: colorSpace, options: [:])!
|
data = context.jpegRepresentation(of: image, colorSpace: colorSpace, options: [:])!
|
||||||
utType = .jpeg
|
mimeType = "image/jpeg"
|
||||||
} else {
|
} else {
|
||||||
utType = UTType(dataUTI)!
|
mimeType = UTType(dataUTI)!.preferredMIMEType!
|
||||||
}
|
}
|
||||||
|
|
||||||
completion(.success((data, utType)))
|
completion(.success((data, mimeType)))
|
||||||
}
|
}
|
||||||
} else if asset.mediaType == .video {
|
} else if asset.mediaType == .video {
|
||||||
let options = PHVideoRequestOptions()
|
let options = PHVideoRequestOptions()
|
||||||
|
@ -109,11 +109,11 @@ enum CompositionAttachmentData {
|
||||||
|
|
||||||
case let .drawing(drawing):
|
case let .drawing(drawing):
|
||||||
let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1)
|
let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1)
|
||||||
completion(.success((image.pngData()!, .png)))
|
completion(.success((image.pngData()!, "image/png")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Result<(Data, UTType), Error>) -> Void) {
|
private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Result<(Data, String), Error>) -> Void) {
|
||||||
session.outputFileType = .mp4
|
session.outputFileType = .mp4
|
||||||
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
|
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
|
||||||
session.exportAsynchronously {
|
session.exportAsynchronously {
|
||||||
|
@ -123,7 +123,7 @@ enum CompositionAttachmentData {
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: session.outputURL!)
|
let data = try Data(contentsOf: session.outputURL!)
|
||||||
completion(.success((data, .mpeg4Movie)))
|
completion(.success((data, "video/mp4")))
|
||||||
} catch {
|
} catch {
|
||||||
completion(.failure(.videoExport(error)))
|
completion(.failure(.videoExport(error)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
enum StatusFormat: CaseIterable {
|
enum StatusFormat: CaseIterable {
|
||||||
case bold, italics, strikethrough, code
|
case italics, bold, strikethrough, code
|
||||||
|
|
||||||
var insertionResult: FormatInsertionResult? {
|
var insertionResult: FormatInsertionResult? {
|
||||||
switch Preferences.shared.statusContentType {
|
switch Preferences.shared.statusContentType {
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
//
|
||||||
|
// AssetPickerSheetContainerViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 1/1/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SheetController
|
||||||
|
import Photos
|
||||||
|
|
||||||
|
class AssetPickerSheetContainerViewController: SheetContainerViewController {
|
||||||
|
|
||||||
|
let assetPicker = AssetPickerViewController()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(content: assetPicker)
|
||||||
|
|
||||||
|
assetPicker.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
assetPicker.view.layer.masksToBounds = true
|
||||||
|
|
||||||
|
delegate = self
|
||||||
|
assetPicker.delegate = self
|
||||||
|
detents = [.bottom, .middle, .top]
|
||||||
|
|
||||||
|
overrideUserInterfaceStyle = .dark
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
assetPicker.view.layer.cornerRadius = view.bounds.width * 0.02
|
||||||
|
// don't round bottom corners, since they'll always be cut off by the device
|
||||||
|
assetPicker.view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||||
|
|
||||||
|
super.viewDidLoad()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AssetPickerSheetContainerViewController: SheetContainerViewControllerDelegate {
|
||||||
|
func sheetContainerContentScrollView(_ sheetContainer: SheetContainerViewController) -> UIScrollView? {
|
||||||
|
if let vc = assetPicker.visibleViewController as? UITableViewController {
|
||||||
|
return vc.tableView
|
||||||
|
} else if let vc = assetPicker.visibleViewController as? UICollectionViewController {
|
||||||
|
return vc.collectionView
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func sheetContainer(_ sheetContainer: SheetContainerViewController, topContentOffsetForScrollView scrollView: UIScrollView) -> CGFloat {
|
||||||
|
return assetPicker.navigationBar.bounds.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AssetPickerSheetContainerViewController: UINavigationControllerDelegate {
|
||||||
|
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
|
||||||
|
contentScrollViewChanged()
|
||||||
|
// viewController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,11 +37,15 @@ struct ComposeAttachmentRow: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
Button(action: self.removeAttachment) {
|
Button(action: self.removeAttachment) {
|
||||||
Label("Delete", systemImage: "trash")
|
Label("Delete", systemImage: "trash")
|
||||||
}.foregroundStyle(.red)
|
}.foregroundStyle(.red)
|
||||||
} previewIfAvailable: {
|
} else {
|
||||||
ComposeAttachmentImage(attachment: attachment, fullSize: true)
|
Button(action: self.removeAttachment) {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
|
@ -147,18 +151,6 @@ extension ComposeAttachmentRow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension View {
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@ViewBuilder
|
|
||||||
func contextMenu<M: View, P: View>(@ViewBuilder menuItems: () -> M, @ViewBuilder previewIfAvailable preview: () -> P) -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self.contextMenu(menuItems: menuItems, preview: preview)
|
|
||||||
} else {
|
|
||||||
self.contextMenu(menuItems: menuItems)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//struct ComposeAttachmentRow_Previews: PreviewProvider {
|
//struct ComposeAttachmentRow_Previews: PreviewProvider {
|
||||||
// static var previews: some View {
|
// static var previews: some View {
|
||||||
// ComposeAttachmentRow()
|
// ComposeAttachmentRow()
|
||||||
|
|
|
@ -50,7 +50,7 @@ struct ComposeAttachmentsList: View {
|
||||||
.disabled(!canAddAttachment)
|
.disabled(!canAddAttachment)
|
||||||
.foregroundColor(.blue)
|
.foregroundColor(.blue)
|
||||||
.frame(height: cellHeight / 2)
|
.frame(height: cellHeight / 2)
|
||||||
.sheetOrPopover(isPresented: $isShowingAssetPickerPopover, content: self.assetPickerPopover)
|
.popover(isPresented: $isShowingAssetPickerPopover, content: self.assetPickerPopover)
|
||||||
.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) {
|
||||||
|
@ -70,9 +70,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))
|
||||||
}
|
}
|
||||||
.listStyle(PlainListStyle())
|
.listStyle(PlainListStyle())
|
||||||
// todo: scrollDisabled doesn't remove the need for manually calculating the frame height
|
|
||||||
.frame(height: totalListHeight)
|
.frame(height: totalListHeight)
|
||||||
.scrollDisabledIfAvailable(totalHeight: totalListHeight)
|
|
||||||
.onAppear(perform: self.didAppear)
|
.onAppear(perform: self.didAppear)
|
||||||
.onReceive(draft.$attachments, perform: self.attachmentsChanged)
|
.onReceive(draft.$attachments, perform: self.attachmentsChanged)
|
||||||
}
|
}
|
||||||
|
@ -112,15 +110,11 @@ struct ComposeAttachmentsList: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func didAppear() {
|
private func didAppear() {
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
// these appearance proxy hacks are no longer necessary
|
|
||||||
} else {
|
|
||||||
let proxy = UITableView.appearance(whenContainedInInstancesOf: [ComposeHostingController.self])
|
let proxy = UITableView.appearance(whenContainedInInstancesOf: [ComposeHostingController.self])
|
||||||
// enable drag and drop to reorder on iPhone
|
// enable drag and drop to reorder on iPhone
|
||||||
proxy.dragInteractionEnabled = true
|
proxy.dragInteractionEnabled = true
|
||||||
proxy.isScrollEnabled = false
|
proxy.isScrollEnabled = false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private func attachmentsChanged(attachments: [CompositionAttachment]) {
|
private func attachmentsChanged(attachments: [CompositionAttachment]) {
|
||||||
var copy = rowHeights
|
var copy = rowHeights
|
||||||
|
@ -136,21 +130,14 @@ struct ComposeAttachmentsList: View {
|
||||||
private func assetPickerPopover() -> some View {
|
private func assetPickerPopover() -> some View {
|
||||||
ComposeAssetPicker(draft: draft, delegate: uiState.delegate?.assetPickerDelegate)
|
ComposeAssetPicker(draft: draft, delegate: uiState.delegate?.assetPickerDelegate)
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
// on iPadOS 16, this is necessary to dismiss the popover when collapsing from regular -> compact size class
|
|
||||||
// otherwise, the popover isn't visible but it's still "presented", so the sheet can't be shown
|
|
||||||
self.isShowingAssetPickerPopover = false
|
self.isShowingAssetPickerPopover = false
|
||||||
}
|
}
|
||||||
// on iPadOS 16, this is necessary to show the dark color in the popover arrow
|
|
||||||
.background(Color(.systemBackground))
|
|
||||||
.environment(\.colorScheme, .dark)
|
.environment(\.colorScheme, .dark)
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
.withSheetDetentsIfAvailable()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addAttachment() {
|
private func addAttachment() {
|
||||||
if #available(iOS 16.0, *) {
|
if horizontalSizeClass == .regular {
|
||||||
isShowingAssetPickerPopover = true
|
|
||||||
} else if horizontalSizeClass == .regular {
|
|
||||||
isShowingAssetPickerPopover = true
|
isShowingAssetPickerPopover = true
|
||||||
} else {
|
} else {
|
||||||
uiState.delegate?.presentAssetPickerSheet()
|
uiState.delegate?.presentAssetPickerSheet()
|
||||||
|
@ -193,52 +180,12 @@ struct ComposeAttachmentsList: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate extension View {
|
fileprivate extension View {
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func sheetOrPopover(isPresented: Binding<Bool>, @ViewBuilder content: @escaping () -> some View) -> some View {
|
func onDragWithPreviewIfAvailable<V>(_ data: @escaping () -> NSItemProvider, preview: () -> V) -> some View where V : View {
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 15.0, *) {
|
||||||
self.modifier(SheetOrPopover(isPresented: isPresented, view: content))
|
self.onDrag(data, preview: preview)
|
||||||
} else {
|
} else {
|
||||||
self.popover(isPresented: isPresented, content: content)
|
self.onDrag(data)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@ViewBuilder
|
|
||||||
func withSheetDetentsIfAvailable() -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self
|
|
||||||
.presentationDetents([.medium, .large])
|
|
||||||
.presentationDragIndicator(.visible)
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@ViewBuilder
|
|
||||||
func scrollDisabledIfAvailable(totalHeight: CGFloat) -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self.scrollDisabled(true)
|
|
||||||
} else {
|
|
||||||
self.frame(height: totalHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS 16.0, *)
|
|
||||||
struct SheetOrPopover<V: View>: ViewModifier {
|
|
||||||
@Binding var isPresented: Bool
|
|
||||||
@ViewBuilder let view: () -> V
|
|
||||||
|
|
||||||
@Environment(\.horizontalSizeClass) var sizeClass
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
let _ = print("isPresented: \(isPresented)")
|
|
||||||
if sizeClass == .compact {
|
|
||||||
content.sheet(isPresented: $isPresented, content: view)
|
|
||||||
} else {
|
|
||||||
content.popover(isPresented: $isPresented, content: view)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@ struct ComposeAutocompleteView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
suggestionsView
|
suggestionsView
|
||||||
|
// animate changes of the scroll view items
|
||||||
|
.animation(.default)
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.overlay(borderColor.frame(height: 0.5), alignment: .top)
|
.overlay(borderColor.frame(height: 0.5), alignment: .top)
|
||||||
}
|
}
|
||||||
|
@ -83,8 +85,8 @@ struct ComposeAutocompleteMentionsView: View {
|
||||||
}
|
}
|
||||||
.frame(height: 30)
|
.frame(height: 30)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
|
.animation(.linear(duration: 0.1))
|
||||||
}
|
}
|
||||||
.animation(.linear(duration: 0.1), value: accounts)
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
@ -165,7 +167,7 @@ struct ComposeAutocompleteMentionsView: View {
|
||||||
.map(\.0)
|
.map(\.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum EitherAccount: Equatable {
|
private enum EitherAccount {
|
||||||
case pachyderm(Account)
|
case pachyderm(Account)
|
||||||
case coreData(AccountMO)
|
case coreData(AccountMO)
|
||||||
|
|
||||||
|
@ -195,10 +197,6 @@ struct ComposeAutocompleteMentionsView: View {
|
||||||
return account.avatar
|
return account.avatar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: EitherAccount, rhs: EitherAccount) -> Bool {
|
|
||||||
return lhs.id == rhs.id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,7 +212,7 @@ struct ComposeAutocompleteEmojisView: View {
|
||||||
HStack(alignment: expanded ? .top : .center, spacing: 0) {
|
HStack(alignment: expanded ? .top : .center, spacing: 0) {
|
||||||
if case let .emoji(query) = uiState.autocompleteState {
|
if case let .emoji(query) = uiState.autocompleteState {
|
||||||
emojiList(query: query)
|
emojiList(query: query)
|
||||||
.animation(.default, value: expanded)
|
.animation(.default)
|
||||||
.transition(.move(edge: .bottom))
|
.transition(.move(edge: .bottom))
|
||||||
} else {
|
} else {
|
||||||
// when the autocomplete view is animating out, the autocomplete state is nil
|
// when the autocomplete view is animating out, the autocomplete state is nil
|
||||||
|
@ -261,8 +259,8 @@ struct ComposeAutocompleteEmojisView: View {
|
||||||
}
|
}
|
||||||
.frame(height: 30)
|
.frame(height: 30)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
|
.animation(.linear(duration: 0.2))
|
||||||
}
|
}
|
||||||
.animation(.linear(duration: 0.2), value: emojis)
|
|
||||||
|
|
||||||
Spacer(minLength: 30)
|
Spacer(minLength: 30)
|
||||||
}
|
}
|
||||||
|
@ -321,8 +319,8 @@ struct ComposeAutocompleteHashtagsView: View {
|
||||||
}
|
}
|
||||||
.frame(height: 30)
|
.frame(height: 30)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
|
.animation(.linear(duration: 0.1))
|
||||||
}
|
}
|
||||||
.animation(.linear(duration: 0.1), value: hashtags)
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,17 +111,6 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
||||||
self.updateAutocompleteState(textField: textField)
|
self.updateAutocompleteState(textField: textField)
|
||||||
}
|
}
|
||||||
|
|
||||||
func textField(_ textField: UITextField, editMenuForCharactersIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
|
|
||||||
var actions = suggestedActions
|
|
||||||
if range.length == 0 {
|
|
||||||
actions.append(UIAction(title: "Insert Emoji", image: UIImage(systemName: "face.smiling"), handler: { [weak self] _ in
|
|
||||||
self?.uiState.shouldEmojiAutocompletionBeginExpanded = true
|
|
||||||
self?.beginAutocompletingEmoji()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
return UIMenu(children: actions)
|
|
||||||
}
|
|
||||||
|
|
||||||
func beginAutocompletingEmoji() {
|
func beginAutocompletingEmoji() {
|
||||||
textField?.insertText(":")
|
textField?.insertText(":")
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,9 @@ protocol ComposeHostingControllerDelegate: AnyObject {
|
||||||
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
|
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let VISIBILITY_BAR_BUTTON_TAG = 42001
|
||||||
|
private let LOCAL_ONLY_BAR_BUTTON_TAG = 42002
|
||||||
|
|
||||||
class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
|
|
||||||
weak var delegate: ComposeHostingControllerDelegate?
|
weak var delegate: ComposeHostingControllerDelegate?
|
||||||
|
@ -132,12 +135,12 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
items.append(UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(cwButtonPressed)))
|
items.append(UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(cwButtonPressed)))
|
||||||
|
|
||||||
let visibilityItem = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
|
let visibilityItem = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
|
||||||
visibilityItem.tag = ViewTags.composeVisibilityBarButton
|
visibilityItem.tag = VISIBILITY_BAR_BUTTON_TAG
|
||||||
items.append(visibilityItem)
|
items.append(visibilityItem)
|
||||||
|
|
||||||
if mastodonController.instanceFeatures.localOnlyPosts {
|
if mastodonController.instanceFeatures.localOnlyPosts {
|
||||||
let item = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
|
let item = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
|
||||||
item.tag = ViewTags.composeLocalOnlyBarButton
|
item.tag = LOCAL_ONLY_BAR_BUTTON_TAG
|
||||||
items.append(item)
|
items.append(item)
|
||||||
localOnlyChanged(draft.localOnly)
|
localOnlyChanged(draft.localOnly)
|
||||||
}
|
}
|
||||||
|
@ -240,7 +243,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
|
|
||||||
private func visibilityChanged(_ newVisibility: Status.Visibility) {
|
private func visibilityChanged(_ newVisibility: Status.Visibility) {
|
||||||
for toolbar in [mainToolbar, inputAccessoryToolbar] {
|
for toolbar in [mainToolbar, inputAccessoryToolbar] {
|
||||||
guard let item = toolbar?.items?.first(where: { $0.tag == ViewTags.composeVisibilityBarButton }) else {
|
guard let item = toolbar?.items?.first(where: { $0.tag == VISIBILITY_BAR_BUTTON_TAG }) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
item.image = UIImage(systemName: newVisibility.imageName)
|
item.image = UIImage(systemName: newVisibility.imageName)
|
||||||
|
@ -257,7 +260,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
|
|
||||||
private func localOnlyChanged(_ localOnly: Bool) {
|
private func localOnlyChanged(_ localOnly: Bool) {
|
||||||
for toolbar in [mainToolbar, inputAccessoryToolbar] {
|
for toolbar in [mainToolbar, inputAccessoryToolbar] {
|
||||||
guard let item = toolbar?.items?.first(where: { $0.tag == ViewTags.composeLocalOnlyBarButton }) else {
|
guard let item = toolbar?.items?.first(where: { $0.tag == LOCAL_ONLY_BAR_BUTTON_TAG }) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if localOnly {
|
if localOnly {
|
||||||
|
@ -339,6 +342,7 @@ extension ComposeHostingController: ComposeUIStateDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentAssetPickerSheet() {
|
func presentAssetPickerSheet() {
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
let picker = AssetPickerViewController()
|
let picker = AssetPickerViewController()
|
||||||
picker.assetPickerDelegate = self
|
picker.assetPickerDelegate = self
|
||||||
picker.modalPresentationStyle = .pageSheet
|
picker.modalPresentationStyle = .pageSheet
|
||||||
|
@ -347,6 +351,15 @@ extension ComposeHostingController: ComposeUIStateDelegate {
|
||||||
sheet.detents = [.medium(), .large()]
|
sheet.detents = [.medium(), .large()]
|
||||||
sheet.prefersEdgeAttachedInCompactHeight = true
|
sheet.prefersEdgeAttachedInCompactHeight = true
|
||||||
self.present(picker, animated: true)
|
self.present(picker, animated: true)
|
||||||
|
} else {
|
||||||
|
presentOldAssetPickerSheet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentOldAssetPickerSheet() {
|
||||||
|
let sheetContainer = AssetPickerSheetContainerViewController()
|
||||||
|
sheetContainer.assetPicker.assetPickerDelegate = self
|
||||||
|
self.present(sheetContainer, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentComposeDrawing() {
|
func presentComposeDrawing() {
|
||||||
|
@ -461,3 +474,13 @@ extension ComposeHostingController: ComposeDrawingViewControllerDelegate {
|
||||||
dismiss(animated: true)
|
dismiss(animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate extension UIAction {
|
||||||
|
convenience init(title: String, subtitle: String?, image: UIImage?, state: UIAction.State, handler: @escaping UIActionHandler) {
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
self.init(title: title, subtitle: subtitle, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
|
||||||
|
} else {
|
||||||
|
self.init(title: title, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -60,9 +60,7 @@ struct ComposePollView: View {
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
// use .animation(nil) on pickers so frame doesn't have a size change animation when the text changes
|
// use .animation(nil) on pickers so frame doesn't have a size change animation when the text changes
|
||||||
// this is deprecated in iOS 15, but using .animation(nil, value: poll.multiple) does not work (it still animates)
|
Picker(selection: $poll.multiple, label: Text(poll.multiple ? "Allow multiple choices" : "Single choice")) {
|
||||||
// nor does setting that on the Text rather than the Picker
|
|
||||||
Picker(selection: $poll.multiple, label: Text(poll.multiple ? "Allow multiple choice" : "Single choice")) {
|
|
||||||
Text("Allow multiple choices").tag(true)
|
Text("Allow multiple choices").tag(true)
|
||||||
Text("Single choice").tag(false)
|
Text("Single choice").tag(false)
|
||||||
}
|
}
|
||||||
|
@ -156,7 +154,8 @@ struct ComposePollOption: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Checkbox(radiusFraction: poll.multiple ? 0.1 : 0.5, borderWidth: 2)
|
Checkbox(radiusFraction: poll.multiple ? 0.1 : 0.5, borderWidth: 2)
|
||||||
.animation(.default, value: poll.multiple)
|
.animation(.default)
|
||||||
|
|
||||||
|
|
||||||
textField
|
textField
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ protocol ComposeUIStateDelegate: AnyObject {
|
||||||
var assetPickerDelegate: AssetPickerViewControllerDelegate? { get }
|
var assetPickerDelegate: AssetPickerViewControllerDelegate? { get }
|
||||||
|
|
||||||
func dismissCompose(mode: ComposeUIState.DismissMode)
|
func dismissCompose(mode: ComposeUIState.DismissMode)
|
||||||
// @available(iOS, obsoleted: 16.0)
|
|
||||||
func presentAssetPickerSheet()
|
func presentAssetPickerSheet()
|
||||||
func presentComposeDrawing()
|
func presentComposeDrawing()
|
||||||
|
|
||||||
|
@ -49,7 +48,7 @@ extension ComposeUIState {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeUIState {
|
extension ComposeUIState {
|
||||||
enum AutocompleteState: Equatable {
|
enum AutocompleteState {
|
||||||
case mention(String)
|
case mention(String)
|
||||||
case emoji(String)
|
case emoji(String)
|
||||||
case hashtag(String)
|
case hashtag(String)
|
||||||
|
|
|
@ -89,7 +89,6 @@ struct ComposeView: View {
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
mainStack(outerMinY: outer.frame(in: .global).minY)
|
mainStack(outerMinY: outer.frame(in: .global).minY)
|
||||||
}
|
}
|
||||||
.scrollDismissesKeyboardInteractivelyIfAvailable()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let poster = poster {
|
if let poster = poster {
|
||||||
|
@ -119,7 +118,7 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.transition(.move(edge: .bottom))
|
.transition(.move(edge: .bottom))
|
||||||
.animation(.default, value: uiState.autocompleteState)
|
.animation(.default)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mainStack(outerMinY: CGFloat) -> some View {
|
func mainStack(outerMinY: CGFloat) -> some View {
|
||||||
|
@ -147,6 +146,7 @@ struct ComposeView: View {
|
||||||
if let poll = draft.poll {
|
if let poll = draft.poll {
|
||||||
ComposePollView(draft: draft, poll: poll)
|
ComposePollView(draft: draft, poll: poll)
|
||||||
.transition(.opacity.combined(with: .asymmetric(insertion: .scale(scale: 0.5, anchor: .leading), removal: .scale(scale: 0.5, anchor: .trailing))))
|
.transition(.opacity.combined(with: .asymmetric(insertion: .scale(scale: 0.5, anchor: .leading), removal: .scale(scale: 0.5, anchor: .trailing))))
|
||||||
|
.animation(.default)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,18 +250,6 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension View {
|
|
||||||
@available(iOS, obsoleted: 16.0)
|
|
||||||
@ViewBuilder
|
|
||||||
func scrollDismissesKeyboardInteractivelyIfAvailable() -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self.scrollDismissesKeyboard(.interactively)
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//struct ComposeView_Previews: PreviewProvider {
|
//struct ComposeView_Previews: PreviewProvider {
|
||||||
// static var previews: some View {
|
// static var previews: some View {
|
||||||
// ComposeView()
|
// ComposeView()
|
||||||
|
|
|
@ -193,42 +193,8 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
self.updateAutocompleteState()
|
self.updateAutocompleteState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
|
|
||||||
var actions = suggestedActions
|
|
||||||
if Preferences.shared.statusContentType != .plain,
|
|
||||||
let index = suggestedActions.firstIndex(where: { ($0 as? UIMenu)?.identifier.rawValue == "com.apple.menu.format" }) {
|
|
||||||
if range.length > 0 {
|
|
||||||
let formatMenu = suggestedActions[index] as! UIMenu
|
|
||||||
let newFormatMenu = formatMenu.replacingChildren(StatusFormat.allCases.map { fmt in
|
|
||||||
UIAction(title: fmt.accessibilityLabel, image: fmt.image) { [weak self] _ in
|
|
||||||
self?.applyFormat(fmt)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
actions[index] = newFormatMenu
|
|
||||||
} else {
|
|
||||||
actions.remove(at: index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if range.length == 0 {
|
|
||||||
actions.append(UIAction(title: "Insert Emoji", image: UIImage(systemName: "face.smiling"), handler: { [weak self] _ in
|
|
||||||
self?.uiState.shouldEmojiAutocompletionBeginExpanded = true
|
|
||||||
self?.beginAutocompletingEmoji()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
return UIMenu(children: actions)
|
|
||||||
}
|
|
||||||
|
|
||||||
func beginAutocompletingEmoji() {
|
func beginAutocompletingEmoji() {
|
||||||
guard let textView = textView else {
|
textView?.insertText(":")
|
||||||
return
|
|
||||||
}
|
|
||||||
var insertSpace = false
|
|
||||||
if let text = textView.text,
|
|
||||||
textView.selectedRange.upperBound > 0 {
|
|
||||||
let characterBeforeCursorIndex = text.utf16.index(before: text.utf16.index(text.startIndex, offsetBy: textView.selectedRange.upperBound))
|
|
||||||
insertSpace = !text[characterBeforeCursorIndex].isWhitespace
|
|
||||||
}
|
|
||||||
textView.insertText((insertSpace ? " " : "") + ":")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func autocomplete(with string: String) {
|
func autocomplete(with string: String) {
|
||||||
|
|
|
@ -124,8 +124,13 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
visibilityBarButtonItem = UIBarButtonItem(image: ConversationTableViewController.showPostsImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
visibilityBarButtonItem = UIBarButtonItem(image: ConversationTableViewController.showPostsImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
||||||
visibilityBarButtonItem.isSelected = showStatusesAutomatically
|
visibilityBarButtonItem.isSelected = showStatusesAutomatically
|
||||||
|
} else {
|
||||||
|
let initialImage = showStatusesAutomatically ? ConversationTableViewController.hidePostsImage : ConversationTableViewController.showPostsImage
|
||||||
|
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
||||||
|
}
|
||||||
navigationItem.rightBarButtonItem = visibilityBarButtonItem
|
navigationItem.rightBarButtonItem = visibilityBarButtonItem
|
||||||
// disable transparent background when scroll to top because it looks weird when items earlier in the thread load in
|
// disable transparent background when scroll to top because it looks weird when items earlier in the thread load in
|
||||||
// (it remains transparent slightly too long, resulting in a flash of the content under the transparent bar)
|
// (it remains transparent slightly too long, resulting in a flash of the content under the transparent bar)
|
||||||
|
@ -170,7 +175,7 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections([.statuses])
|
snapshot.appendSections([.statuses])
|
||||||
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
||||||
await dataSource.apply(snapshot, animatingDifferences: false)
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
loadingState = .loadedMain
|
loadingState = .loadedMain
|
||||||
|
|
||||||
|
@ -391,7 +396,15 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
tableView.beginUpdates()
|
tableView.beginUpdates()
|
||||||
tableView.endUpdates()
|
tableView.endUpdates()
|
||||||
|
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
visibilityBarButtonItem.isSelected = showStatusesAutomatically
|
visibilityBarButtonItem.isSelected = showStatusesAutomatically
|
||||||
|
} else {
|
||||||
|
if showStatusesAutomatically {
|
||||||
|
visibilityBarButtonItem.image = ConversationTableViewController.hidePostsImage
|
||||||
|
} else {
|
||||||
|
visibilityBarButtonItem.image = ConversationTableViewController.showPostsImage
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,18 +110,6 @@ class DraftsTableViewController: UITableViewController {
|
||||||
tableView.deleteRows(at: [indexPath], with: .automatic)
|
tableView.deleteRows(at: [indexPath], with: .automatic)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
|
||||||
return UIContextMenuConfiguration(actionProvider: { _ in
|
|
||||||
return UIMenu(children: [
|
|
||||||
UIAction(title: "Delete Draft", image: UIImage(systemName: "trash"), attributes: .destructive, handler: { [unowned self] _ in
|
|
||||||
DraftsManager.shared.remove(self.draft(for: indexPath))
|
|
||||||
drafts.remove(at: indexPath.row)
|
|
||||||
tableView.deleteRows(at: [indexPath], with: .automatic)
|
|
||||||
})
|
|
||||||
])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
||||||
@objc func cancelPressed() {
|
@objc func cancelPressed() {
|
||||||
|
|
|
@ -9,20 +9,19 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class AddSavedHashtagViewController: UIViewController {
|
class AddSavedHashtagViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
var resultsController: SearchResultsViewController!
|
var resultsController: SearchResultsViewController!
|
||||||
var searchController: UISearchController!
|
var searchController: UISearchController!
|
||||||
|
|
||||||
private var collectionView: UICollectionView!
|
var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(style: .grouped)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -34,37 +33,17 @@ class AddSavedHashtagViewController: UIViewController {
|
||||||
|
|
||||||
title = NSLocalizedString("Search", comment: "search screen title")
|
title = NSLocalizedString("Search", comment: "search screen title")
|
||||||
|
|
||||||
view.backgroundColor = .systemGroupedBackground
|
tableView.register(UINib(nibName: "TrendingHashtagTableViewCell", bundle: .main), forCellReuseIdentifier: "trendingTagCell")
|
||||||
|
tableView.rowHeight = 60 // 44 for content + 2 * 8 spacing
|
||||||
|
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||||
config.headerMode = .supplementary
|
|
||||||
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
|
||||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
|
||||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
||||||
collectionView.delegate = self
|
|
||||||
view.addSubview(collectionView)
|
|
||||||
|
|
||||||
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { (headerView, collectionView, indexPath) in
|
|
||||||
var config = headerView.defaultContentConfiguration()
|
|
||||||
config.text = NSLocalizedString("Trending Hashtags", comment: "trending hashtags section title")
|
|
||||||
headerView.contentConfiguration = config
|
|
||||||
}
|
|
||||||
let registration = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { cell, indexPath, hashtag in
|
|
||||||
cell.updateUI(hashtag: hashtag)
|
|
||||||
}
|
|
||||||
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { (collectionView, indexPath, item) in
|
|
||||||
switch item {
|
switch item {
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: hashtag)
|
let cell = tableView.dequeueReusableCell(withIdentifier: "trendingTagCell", for: indexPath) as! TrendingHashtagTableViewCell
|
||||||
}
|
cell.updateUI(hashtag: hashtag)
|
||||||
}
|
return cell
|
||||||
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
|
|
||||||
if elementKind == UICollectionView.elementKindSectionHeader {
|
|
||||||
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath)
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
resultsController = HashtagSearchResultsViewController(mastodonController: mastodonController)
|
resultsController = HashtagSearchResultsViewController(mastodonController: mastodonController)
|
||||||
resultsController.delegate = self
|
resultsController.delegate = self
|
||||||
|
@ -113,6 +92,17 @@ class AddSavedHashtagViewController: UIViewController {
|
||||||
presentingViewController!.dismiss(animated: true)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
||||||
@objc func cancelButtonPressed() {
|
@objc func cancelButtonPressed() {
|
||||||
|
@ -125,23 +115,14 @@ extension AddSavedHashtagViewController {
|
||||||
enum Section {
|
enum Section {
|
||||||
case trendingTags
|
case trendingTags
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case tag(Hashtag)
|
case tag(Hashtag)
|
||||||
}
|
}
|
||||||
// class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
|
||||||
// override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AddSavedHashtagViewController: UICollectionViewDelegate {
|
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||||
switch dataSource.itemIdentifier(for: indexPath) {
|
return NSLocalizedString("Trending Hashtags", comment: "trending hashtags seciton title")
|
||||||
case nil:
|
|
||||||
return
|
|
||||||
case let .tag(hashtag):
|
|
||||||
selectHashtag(hashtag)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import CoreData
|
import CoreData
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
class ExploreViewController: UIViewController, UICollectionViewDelegate {
|
||||||
|
|
||||||
|
@ -583,10 +582,7 @@ extension ExploreViewController: UICollectionViewDragDelegate {
|
||||||
activity.displaysAuxiliaryScene = true
|
activity.displaysAuxiliaryScene = true
|
||||||
provider = NSItemProvider(object: activity)
|
provider = NSItemProvider(object: activity)
|
||||||
case let .savedHashtag(hashtag):
|
case let .savedHashtag(hashtag):
|
||||||
guard let url = URL(hashtag.url) else {
|
provider = NSItemProvider(object: hashtag.url as NSURL)
|
||||||
return []
|
|
||||||
}
|
|
||||||
provider = NSItemProvider(object: url as NSURL)
|
|
||||||
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
|
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
|
||||||
activity.displaysAuxiliaryScene = true
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
//
|
||||||
|
// 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)
|
||||||
|
fromLabel.textAlignment = .right
|
||||||
|
|
||||||
|
let sortLabel = UILabel()
|
||||||
|
sortLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
sortLabel.text = NSLocalizedString("Sort By", comment: "profile directory sort label")
|
||||||
|
sortLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
|
sortLabel.textAlignment = .right
|
||||||
|
|
||||||
|
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: self.scope.selectedSegmentIndex)!
|
||||||
|
let order = sort.selectedSegmentIndex == 0 ? DirectoryOrder.active : .new
|
||||||
|
onFilterChanged?(scope, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileDirectoryFilterView {
|
||||||
|
enum Scope: Int, Equatable {
|
||||||
|
case instance, everywhere
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,9 +16,6 @@ class ProfileDirectoryViewController: UIViewController {
|
||||||
private var collectionView: UICollectionView!
|
private var collectionView: UICollectionView!
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
private var scope: Scope = .everywhere
|
|
||||||
private var order: DirectoryOrder = .active
|
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
@ -34,10 +31,14 @@ class ProfileDirectoryViewController: UIViewController {
|
||||||
|
|
||||||
title = NSLocalizedString("Profile Directory", comment: "profile directory title")
|
title = NSLocalizedString("Profile Directory", comment: "profile directory title")
|
||||||
|
|
||||||
// todo: it would be nice if there were a better "filter" icon
|
let configuration = UICollectionViewCompositionalLayoutConfiguration()
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "scope"), menu: nil)
|
configuration.boundarySupplementaryItems = [
|
||||||
updateFilterMenu()
|
NSCollectionLayoutBoundarySupplementaryItem(
|
||||||
|
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100)),
|
||||||
|
elementKind: "filter",
|
||||||
|
alignment: .top
|
||||||
|
)
|
||||||
|
]
|
||||||
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) in
|
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) in
|
||||||
let itemHeight = NSCollectionLayoutDimension.absolute(200)
|
let itemHeight = NSCollectionLayoutDimension.absolute(200)
|
||||||
let itemWidth: NSCollectionLayoutDimension
|
let itemWidth: NSCollectionLayoutDimension
|
||||||
|
@ -59,18 +60,19 @@ class ProfileDirectoryViewController: UIViewController {
|
||||||
section.interGroupSpacing = 16
|
section.interGroupSpacing = 16
|
||||||
section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
|
section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
|
||||||
return section
|
return section
|
||||||
})
|
}, configuration: configuration)
|
||||||
|
|
||||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
collectionView.backgroundColor = .secondarySystemBackground
|
collectionView.backgroundColor = .secondarySystemBackground
|
||||||
collectionView.register(UINib(nibName: "FeaturedProfileCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "featuredProfileCell")
|
collectionView.register(UINib(nibName: "FeaturedProfileCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "featuredProfileCell")
|
||||||
|
collectionView.register(ProfileDirectoryFilterView.self, forSupplementaryViewOfKind: "filter", withReuseIdentifier: "filter")
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dragDelegate = self
|
collectionView.dragDelegate = self
|
||||||
view.addSubview(collectionView)
|
view.addSubview(collectionView)
|
||||||
|
|
||||||
dataSource = createDataSource()
|
dataSource = createDataSource()
|
||||||
updateProfiles()
|
updateProfiles(local: true, order: .active)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||||
|
@ -80,44 +82,26 @@ class ProfileDirectoryViewController: UIViewController {
|
||||||
cell.updateUI(account: account)
|
cell.updateUI(account: account)
|
||||||
return cell
|
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
|
return dataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateFilterMenu() {
|
private func updateProfiles(local: Bool, order: DirectoryOrder) {
|
||||||
let scopeMenu = UIMenu(options: .displayInline, children: [
|
|
||||||
UIAction(title: "Everywhere", subtitle: "Users from the whole network", image: UIImage(systemName: "globe"), state: scope == .everywhere ? .on : .off, handler: { [unowned self] _ in
|
|
||||||
self.scope = .everywhere
|
|
||||||
self.updateFilterMenu()
|
|
||||||
self.updateProfiles()
|
|
||||||
}),
|
|
||||||
UIAction(title: mastodonController.accountInfo!.instanceURL.host!, subtitle: "Only users from your instance", image: UIImage(systemName: "house"), state: scope == .instance ? .on : .off, handler: { [unowned self] _ in
|
|
||||||
self.scope = .instance
|
|
||||||
self.updateFilterMenu()
|
|
||||||
self.updateProfiles()
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
let orderMenu = UIMenu(options: .displayInline, children: DirectoryOrder.allCases.map { order in
|
|
||||||
UIAction(title: order.title, subtitle: order.subtitle, image: nil, state: self.order == order ? .on : .off) { [unowned self] _ in
|
|
||||||
self.order = order
|
|
||||||
self.updateFilterMenu()
|
|
||||||
self.updateProfiles()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
navigationItem.rightBarButtonItem!.menu = UIMenu(children: [
|
|
||||||
scopeMenu,
|
|
||||||
orderMenu,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateProfiles() {
|
|
||||||
let scope = self.scope
|
|
||||||
let order = self.order
|
|
||||||
let local = scope == .everywhere
|
|
||||||
let request = Client.getFeaturedProfiles(local: local, order: order)
|
let request = Client.getFeaturedProfiles(local: local, order: order)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
guard case let .success(accounts, _) = response,
|
guard case let .success(accounts, _) = response else {
|
||||||
self.scope == scope,
|
|
||||||
self.order == order else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,28 +188,3 @@ extension ProfileDirectoryViewController: UICollectionViewDragDelegate {
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileDirectoryViewController {
|
|
||||||
enum Scope: CaseIterable {
|
|
||||||
case instance, everywhere
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension DirectoryOrder {
|
|
||||||
var title: String {
|
|
||||||
switch self {
|
|
||||||
case .active:
|
|
||||||
return "Active"
|
|
||||||
case .new:
|
|
||||||
return "New"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var subtitle: String {
|
|
||||||
switch self {
|
|
||||||
case .active:
|
|
||||||
return "Users who have posted lately"
|
|
||||||
case .new:
|
|
||||||
return "Recently joined users"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,19 +8,19 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
class TrendingHashtagsViewController: UIViewController {
|
class TrendingHashtagsViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
private var collectionView: UICollectionView!
|
private var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(style: .grouped)
|
||||||
|
|
||||||
|
dragEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -32,24 +32,15 @@ class TrendingHashtagsViewController: UIViewController {
|
||||||
|
|
||||||
title = NSLocalizedString("Trending Hashtags", comment: "trending hashtags screen title")
|
title = NSLocalizedString("Trending Hashtags", comment: "trending hashtags screen title")
|
||||||
|
|
||||||
view.backgroundColor = .systemGroupedBackground
|
tableView.register(UINib(nibName: "TrendingHashtagTableViewCell", bundle: .main), forCellReuseIdentifier: "trendingTagCell")
|
||||||
|
tableView.rowHeight = 60 // 44 for content + 2 * 8 spacing
|
||||||
|
|
||||||
let config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: tableView) { (tableView, indexPath, item) in
|
||||||
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
|
||||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
|
||||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
||||||
collectionView.delegate = self
|
|
||||||
collectionView.dragDelegate = self
|
|
||||||
view.addSubview(collectionView)
|
|
||||||
|
|
||||||
let registration = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { cell, indexPath, hashtag in
|
|
||||||
cell.updateUI(hashtag: hashtag)
|
|
||||||
}
|
|
||||||
|
|
||||||
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { (collectionView, indexPath, item) in
|
|
||||||
switch item {
|
switch item {
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: hashtag)
|
let cell = tableView.dequeueReusableCell(withIdentifier: "trendingTagCell", for: indexPath) as! TrendingHashtagTableViewCell
|
||||||
|
cell.updateUI(hashtag: hashtag)
|
||||||
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,10 +56,46 @@ class TrendingHashtagsViewController: UIViewController {
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections([.trendingTags])
|
snapshot.appendSections([.trendingTags])
|
||||||
snapshot.appendItems(hashtags.map { .tag($0) })
|
snapshot.appendItems(hashtags.map { .tag($0) })
|
||||||
await dataSource.apply(snapshot)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
|
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||||
|
case let .tag(hashtag) = item else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return UIContextMenuConfiguration(identifier: nil) {
|
||||||
|
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
|
||||||
|
} actionProvider: { (_) in
|
||||||
|
UIMenu(children: self.actionsForHashtag(hashtag, sourceView: self.tableView.cellForRow(at: indexPath)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||||
|
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||||
|
case let .tag(hashtag) = item else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let provider = NSItemProvider(object: hashtag.url as NSURL)
|
||||||
|
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
|
provider.registerObject(activity, visibility: .all)
|
||||||
|
}
|
||||||
|
return [UIDragItem(itemProvider: provider)]
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingHashtagsViewController {
|
extension TrendingHashtagsViewController {
|
||||||
|
@ -80,45 +107,6 @@ extension TrendingHashtagsViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingHashtagsViewController: UICollectionViewDelegate {
|
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
|
||||||
case let .tag(hashtag) = item else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
|
||||||
case let .tag(hashtag) = item else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return UIContextMenuConfiguration(identifier: nil) {
|
|
||||||
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
|
|
||||||
} actionProvider: { (_) in
|
|
||||||
UIMenu(children: self.actionsForHashtag(hashtag, sourceView: self.collectionView.cellForItem(at: indexPath)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TrendingHashtagsViewController: UICollectionViewDragDelegate {
|
|
||||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
|
||||||
case let .tag(hashtag) = item,
|
|
||||||
let url = URL(hashtag.url) else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
let provider = NSItemProvider(object: url as NSURL)
|
|
||||||
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
|
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
provider.registerObject(activity, visibility: .all)
|
|
||||||
}
|
|
||||||
return [UIDragItem(itemProvider: provider)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TrendingHashtagsViewController: TuskerNavigationDelegate {
|
extension TrendingHashtagsViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,141 +0,0 @@
|
||||||
//
|
|
||||||
// TrendingLinkCardCollectionViewCell.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 6/29/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
|
||||||
|
|
||||||
private var card: Card?
|
|
||||||
private var isGrayscale = false
|
|
||||||
private var thumbnailRequest: ImageCache.Request?
|
|
||||||
|
|
||||||
@IBOutlet weak var thumbnailView: UIImageView!
|
|
||||||
@IBOutlet weak var titleLabel: UILabel!
|
|
||||||
@IBOutlet weak var providerLabel: UILabel!
|
|
||||||
@IBOutlet weak var activityLabel: UILabel!
|
|
||||||
@IBOutlet weak var historyView: TrendHistoryView!
|
|
||||||
|
|
||||||
override func awakeFromNib() {
|
|
||||||
super.awakeFromNib()
|
|
||||||
|
|
||||||
layer.shadowOpacity = 0.2
|
|
||||||
layer.shadowRadius = 8
|
|
||||||
layer.shadowOffset = .zero
|
|
||||||
layer.masksToBounds = false
|
|
||||||
updateLayerColors()
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutSubviews() {
|
|
||||||
super.layoutSubviews()
|
|
||||||
|
|
||||||
contentView.layer.cornerRadius = 0.05 * bounds.width
|
|
||||||
thumbnailView.layer.cornerRadius = 0.05 * bounds.width
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUI(card: Card) {
|
|
||||||
self.card = card
|
|
||||||
self.thumbnailView.image = nil
|
|
||||||
|
|
||||||
updateGrayscaleableUI(card: card)
|
|
||||||
updateUIForPreferences()
|
|
||||||
|
|
||||||
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
titleLabel.text = title
|
|
||||||
|
|
||||||
let provider = card.providerName!.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
providerLabel.text = provider
|
|
||||||
|
|
||||||
let sorted = card.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, +)
|
|
||||||
|
|
||||||
// U+2009 THIN SPACE
|
|
||||||
let activityStr = NSMutableAttributedString(string: "\(accounts.formatted())\u{2009}")
|
|
||||||
activityStr.append(NSAttributedString(attachment: NSTextAttachment(image: UIImage(systemName: "person")!)))
|
|
||||||
activityStr.append(NSAttributedString(string: ", \(uses.formatted())\u{2009}"))
|
|
||||||
activityStr.append(NSAttributedString(attachment: NSTextAttachment(image: UIImage(systemName: "square.text.square")!)))
|
|
||||||
activityLabel.attributedText = activityStr
|
|
||||||
|
|
||||||
historyView.setHistory(card.history)
|
|
||||||
historyView.isHidden = card.history == nil || card.history!.count < 2
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func updateUIForPreferences() {
|
|
||||||
if isGrayscale != Preferences.shared.grayscaleImages,
|
|
||||||
let card {
|
|
||||||
updateGrayscaleableUI(card: card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateGrayscaleableUI(card: Card) {
|
|
||||||
isGrayscale = Preferences.shared.grayscaleImages
|
|
||||||
|
|
||||||
if let imageURL = card.image,
|
|
||||||
let url = URL(imageURL) {
|
|
||||||
thumbnailRequest = ImageCache.attachments.get(url, completion: { _, image in
|
|
||||||
guard let image,
|
|
||||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.thumbnailView.image = transformedImage
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if thumbnailRequest != nil {
|
|
||||||
loadBlurHash(card: card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadBlurHash(card: Card) {
|
|
||||||
guard let hash = card.blurhash else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let imageViewSize = self.thumbnailView.bounds.size
|
|
||||||
AttachmentView.queue.async { [weak self] in
|
|
||||||
let size: CGSize
|
|
||||||
if let width = card.width, let height = card.height {
|
|
||||||
size = CGSize(width: width, height: height)
|
|
||||||
} else {
|
|
||||||
size = imageViewSize
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let preview = UIImage(blurHash: hash, size: size) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
guard let self,
|
|
||||||
self.card?.url == card.url,
|
|
||||||
self.thumbnailView.image == nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.thumbnailView.image = preview
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
||||||
super.traitCollectionDidChange(previousTraitCollection)
|
|
||||||
updateLayerColors()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateLayerColors() {
|
|
||||||
if traitCollection.userInterfaceStyle == .dark {
|
|
||||||
// clippingView.layer.borderColor = UIColor.darkGray.withAlphaComponent(0.5).cgColor
|
|
||||||
layer.shadowColor = UIColor.darkGray.cgColor
|
|
||||||
} else {
|
|
||||||
// clippingView.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
|
|
||||||
layer.shadowColor = UIColor.black.cgColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21179.7" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
|
||||||
<device id="retina6_0" orientation="portrait" appearance="light"/>
|
|
||||||
<dependencies>
|
|
||||||
<deployment identifier="iOS"/>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21169.4"/>
|
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
|
||||||
<capability name="collection view cell content view" 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="izA-ZZ-g7F" customClass="TrendingLinkCardCollectionViewCell" customModule="Tusker" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="300" height="400"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
|
||||||
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="Zb0-aW-Sen">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="300" height="400"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<subviews>
|
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="h3b-Mf-lD6">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="300" height="225"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" secondItem="h3b-Mf-lD6" secondAttribute="height" multiplier="4:3" id="QDY-8a-LYC"/>
|
|
||||||
</constraints>
|
|
||||||
</imageView>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ho3-cU-IGi">
|
|
||||||
<rect key="frame" x="16" y="330.66666666666674" width="268" height="20.333333333333314"/>
|
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
|
|
||||||
<nil key="textColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Provider" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="O9r-10-LDD">
|
|
||||||
<rect key="frame" x="16.000000000000004" y="355" width="57.333333333333343" height="18"/>
|
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
|
||||||
<nil key="textColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Activity" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ULe-Gd-t1S">
|
|
||||||
<rect key="frame" x="16" y="377" width="43" height="15"/>
|
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
|
||||||
<nil key="textColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LZj-Ii-63i" customClass="TrendHistoryView" customModule="Tusker" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="200" y="355" width="100" height="44"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" constant="100" id="cUc-p7-aLH"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
</subviews>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="bottomMargin" secondItem="ULe-Gd-t1S" secondAttribute="bottom" id="6UL-8b-Aia"/>
|
|
||||||
<constraint firstItem="h3b-Mf-lD6" firstAttribute="top" secondItem="Zb0-aW-Sen" secondAttribute="top" id="EFg-Yr-vdt"/>
|
|
||||||
<constraint firstItem="Ho3-cU-IGi" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leadingMargin" id="Ga8-LQ-f4N"/>
|
|
||||||
<constraint firstItem="ULe-Gd-t1S" firstAttribute="top" secondItem="O9r-10-LDD" secondAttribute="bottom" constant="4" id="HPD-qN-k3z"/>
|
|
||||||
<constraint firstAttribute="bottom" secondItem="LZj-Ii-63i" secondAttribute="bottom" constant="1" id="HWu-In-Uem"/>
|
|
||||||
<constraint firstItem="O9r-10-LDD" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leadingMargin" id="Hz8-Bw-jpl"/>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="LZj-Ii-63i" secondAttribute="trailing" id="J9c-CF-3EF"/>
|
|
||||||
<constraint firstItem="ULe-Gd-t1S" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leadingMargin" id="KEj-En-StX"/>
|
|
||||||
<constraint firstItem="Ho3-cU-IGi" firstAttribute="top" secondItem="h3b-Mf-lD6" secondAttribute="bottom" constant="4" id="PjW-V1-oDs"/>
|
|
||||||
<constraint firstItem="LZj-Ii-63i" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="O9r-10-LDD" secondAttribute="trailing" id="WNr-ZP-o9a"/>
|
|
||||||
<constraint firstItem="LZj-Ii-63i" firstAttribute="top" secondItem="Ho3-cU-IGi" secondAttribute="bottom" constant="4" id="fpM-Hp-Oyf"/>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="h3b-Mf-lD6" secondAttribute="trailing" id="kBD-1R-bh7"/>
|
|
||||||
<constraint firstItem="LZj-Ii-63i" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ULe-Gd-t1S" secondAttribute="trailing" id="ruZ-p8-n0x"/>
|
|
||||||
<constraint firstAttribute="trailingMargin" secondItem="Ho3-cU-IGi" secondAttribute="trailing" id="ubj-f6-bXE"/>
|
|
||||||
<constraint firstItem="h3b-Mf-lD6" firstAttribute="leading" secondItem="Zb0-aW-Sen" secondAttribute="leading" id="wF1-Gm-nVQ"/>
|
|
||||||
<constraint firstItem="O9r-10-LDD" firstAttribute="top" secondItem="Ho3-cU-IGi" secondAttribute="bottom" constant="4" id="yPq-dT-uib"/>
|
|
||||||
</constraints>
|
|
||||||
</collectionViewCellContentView>
|
|
||||||
<connections>
|
|
||||||
<outlet property="activityLabel" destination="ULe-Gd-t1S" id="wqe-G6-IB3"/>
|
|
||||||
<outlet property="historyView" destination="LZj-Ii-63i" id="MVF-az-uyA"/>
|
|
||||||
<outlet property="providerLabel" destination="O9r-10-LDD" id="xAF-NW-ymm"/>
|
|
||||||
<outlet property="thumbnailView" destination="h3b-Mf-lD6" id="4mF-bJ-ALY"/>
|
|
||||||
<outlet property="titleLabel" destination="Ho3-cU-IGi" id="ltu-ey-chT"/>
|
|
||||||
</connections>
|
|
||||||
<point key="canvasLocation" x="0.0" y="-13.507109004739336"/>
|
|
||||||
</collectionViewCell>
|
|
||||||
</objects>
|
|
||||||
<resources>
|
|
||||||
<systemColor name="systemBackgroundColor">
|
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
</systemColor>
|
|
||||||
</resources>
|
|
||||||
</document>
|
|
|
@ -113,10 +113,6 @@ class TrendingLinkTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func updateUIForPreferences() {
|
@objc private func updateUIForPreferences() {
|
||||||
if isGrayscale != Preferences.shared.grayscaleImages,
|
|
||||||
let card {
|
|
||||||
updateGrayscaleableUI(card: card)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateGrayscaleableUI(card: Card) {
|
private func updateGrayscaleableUI(card: Card) {
|
||||||
|
|
|
@ -55,7 +55,7 @@ class TrendingLinksViewController: EnhancedTableViewController {
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections([.links])
|
snapshot.appendSections([.links])
|
||||||
snapshot.appendItems(links.map(Item.init))
|
snapshot.appendItems(links.map(Item.init))
|
||||||
await dataSource.apply(snapshot)
|
dataSource.apply(snapshot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import VisionKit
|
|
||||||
|
|
||||||
protocol LargeImageContentView: UIView {
|
protocol LargeImageContentView: UIView {
|
||||||
var animationImage: UIImage? { get }
|
var animationImage: UIImage? { get }
|
||||||
|
@ -17,12 +16,7 @@ protocol LargeImageContentView: UIView {
|
||||||
func grayscaleStateChanged()
|
func grayscaleStateChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
class LargeImageImageContentView: UIImageView, LargeImageContentView {
|
class LargeImageImageContentView: GIFImageView, LargeImageContentView {
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
|
||||||
@available(iOS 16.0, *)
|
|
||||||
private static let analyzer = ImageAnalyzer()
|
|
||||||
#endif
|
|
||||||
|
|
||||||
var animationImage: UIImage? { image! }
|
var animationImage: UIImage? { image! }
|
||||||
|
|
||||||
|
@ -31,33 +25,13 @@ class LargeImageImageContentView: UIImageView, LargeImageContentView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sourceData: Data?
|
private var sourceData: Data?
|
||||||
private weak var owner: UIViewController?
|
|
||||||
|
|
||||||
init(image: UIImage, owner: UIViewController?) {
|
|
||||||
self.owner = owner
|
|
||||||
|
|
||||||
|
init(image: UIImage) {
|
||||||
super.init(image: image)
|
super.init(image: image)
|
||||||
|
|
||||||
contentMode = .scaleAspectFit
|
contentMode = .scaleAspectFit
|
||||||
isUserInteractionEnabled = true
|
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
|
||||||
if #available(iOS 16.0, *),
|
|
||||||
ImageAnalyzer.isSupported {
|
|
||||||
let interaction = ImageAnalysisInteraction()
|
|
||||||
interaction.delegate = self
|
|
||||||
interaction.preferredInteractionTypes = .automatic
|
|
||||||
addInteraction(interaction)
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
let result = try await LargeImageImageContentView.analyzer.analyze(image, configuration: ImageAnalyzer.Configuration([.text, .machineReadableCode]))
|
|
||||||
interaction.analysis = result
|
|
||||||
} catch {
|
|
||||||
// if analysis fails, we just don't show anything
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -82,15 +56,6 @@ class LargeImageImageContentView: UIImageView, LargeImageContentView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
|
||||||
@available(iOS 16.0, *)
|
|
||||||
extension LargeImageImageContentView: ImageAnalysisInteractionDelegate {
|
|
||||||
func presentingViewController(for interaction: ImageAnalysisInteraction) -> UIViewController? {
|
|
||||||
return owner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
class LargeImageGifContentView: GIFImageView, LargeImageContentView {
|
class LargeImageGifContentView: GIFImageView, LargeImageContentView {
|
||||||
var animationImage: UIImage? { image }
|
var animationImage: UIImage? { image }
|
||||||
|
|
||||||
|
|
|
@ -138,9 +138,9 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
content = LargeImageGifContentView(gifController: gifController)
|
content = LargeImageGifContentView(gifController: gifController)
|
||||||
} else {
|
} else {
|
||||||
if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) {
|
if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) {
|
||||||
content = LargeImageImageContentView(image: transformedImage, owner: self)
|
content = LargeImageImageContentView(image: transformedImage)
|
||||||
} else {
|
} else {
|
||||||
content = LargeImageImageContentView(image: image, owner: self)
|
content = LargeImageImageContentView(image: image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: source) {
|
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: source) {
|
||||||
image = grayscale
|
image = grayscale
|
||||||
}
|
}
|
||||||
setContent(LargeImageImageContentView(image: image, owner: self))
|
setContent(LargeImageImageContentView(image: image))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ class MainSidebarViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
var exploreTabItems: [Item] {
|
var exploreTabItems: [Item] {
|
||||||
var items: [Item] = [.explore, .bookmarks, .trendingStatuses, .profileDirectory]
|
var items: [Item] = [.search, .bookmarks, .trendingStatuses, .trendingTags, .trendingLinks, .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))
|
||||||
|
@ -154,7 +154,7 @@ class MainSidebarViewController: UIViewController {
|
||||||
snapshot.appendItems([
|
snapshot.appendItems([
|
||||||
.tab(.timelines),
|
.tab(.timelines),
|
||||||
.tab(.notifications),
|
.tab(.notifications),
|
||||||
.explore,
|
.search,
|
||||||
.bookmarks,
|
.bookmarks,
|
||||||
.tab(.myProfile)
|
.tab(.myProfile)
|
||||||
], toSection: .tabs)
|
], toSection: .tabs)
|
||||||
|
@ -177,10 +177,12 @@ class MainSidebarViewController: UIViewController {
|
||||||
var discoverSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
var discoverSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
||||||
discoverSnapshot.append([.discoverHeader])
|
discoverSnapshot.append([.discoverHeader])
|
||||||
discoverSnapshot.append([
|
discoverSnapshot.append([
|
||||||
|
.trendingTags,
|
||||||
.profileDirectory,
|
.profileDirectory,
|
||||||
], to: .discoverHeader)
|
], to: .discoverHeader)
|
||||||
if mastodonController.instanceFeatures.trendingStatusesAndLinks {
|
if mastodonController.instanceFeatures.trendingStatusesAndLinks {
|
||||||
discoverSnapshot.insert([.trendingStatuses], before: .profileDirectory)
|
discoverSnapshot.insert([.trendingStatuses], before: .trendingTags)
|
||||||
|
discoverSnapshot.insert([.trendingLinks], after: .trendingTags)
|
||||||
}
|
}
|
||||||
dataSource.apply(discoverSnapshot, to: .discover)
|
dataSource.apply(discoverSnapshot, to: .discover)
|
||||||
}
|
}
|
||||||
|
@ -343,7 +345,7 @@ class MainSidebarViewController: UIViewController {
|
||||||
return UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode)
|
return UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode)
|
||||||
case .tab(.compose):
|
case .tab(.compose):
|
||||||
return UserActivityManager.newPostActivity(accountID: id)
|
return UserActivityManager.newPostActivity(accountID: id)
|
||||||
case .explore:
|
case .search:
|
||||||
return UserActivityManager.searchActivity()
|
return UserActivityManager.searchActivity()
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return UserActivityManager.bookmarksActivity()
|
return UserActivityManager.bookmarksActivity()
|
||||||
|
@ -382,8 +384,8 @@ extension MainSidebarViewController {
|
||||||
}
|
}
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case tab(MainTabBarViewController.Tab)
|
case tab(MainTabBarViewController.Tab)
|
||||||
case explore, bookmarks
|
case search, bookmarks
|
||||||
case discoverHeader, trendingStatuses, profileDirectory
|
case discoverHeader, trendingStatuses, trendingTags, trendingLinks, 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
|
||||||
|
@ -392,14 +394,18 @@ extension MainSidebarViewController {
|
||||||
switch self {
|
switch self {
|
||||||
case let .tab(tab):
|
case let .tab(tab):
|
||||||
return tab.title
|
return tab.title
|
||||||
case .explore:
|
case .search:
|
||||||
return "Explore"
|
return "Search"
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return "Bookmarks"
|
return "Bookmarks"
|
||||||
case .discoverHeader:
|
case .discoverHeader:
|
||||||
return "Discover"
|
return "Discover"
|
||||||
case .trendingStatuses:
|
case .trendingStatuses:
|
||||||
return "Trending Posts"
|
return "Trending Posts"
|
||||||
|
case .trendingTags:
|
||||||
|
return "Trending Hashtags"
|
||||||
|
case .trendingLinks:
|
||||||
|
return "Trending Links"
|
||||||
case .profileDirectory:
|
case .profileDirectory:
|
||||||
return "Profile Directory"
|
return "Profile Directory"
|
||||||
case .listsHeader:
|
case .listsHeader:
|
||||||
|
@ -427,12 +433,16 @@ extension MainSidebarViewController {
|
||||||
switch self {
|
switch self {
|
||||||
case let .tab(tab):
|
case let .tab(tab):
|
||||||
return tab.imageName
|
return tab.imageName
|
||||||
case .explore:
|
case .search:
|
||||||
return "magnifyingglass"
|
return "magnifyingglass"
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return "bookmark"
|
return "bookmark"
|
||||||
case .trendingStatuses:
|
case .trendingStatuses:
|
||||||
return "square.text.square"
|
return "doc.text.image"
|
||||||
|
case .trendingTags:
|
||||||
|
return "number"
|
||||||
|
case .trendingLinks:
|
||||||
|
return "link"
|
||||||
case .profileDirectory:
|
case .profileDirectory:
|
||||||
return "person.2.fill"
|
return "person.2.fill"
|
||||||
case .list(_):
|
case .list(_):
|
||||||
|
@ -540,7 +550,8 @@ extension MainSidebarViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
guard #available(iOS 15.0, *),
|
||||||
|
let item = dataSource.itemIdentifier(for: indexPath),
|
||||||
let activity = userActivityForItem(item) else {
|
let activity = userActivityForItem(item) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,8 @@ class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
private var tabBarViewController: MainTabBarViewController!
|
private var tabBarViewController: MainTabBarViewController!
|
||||||
|
|
||||||
// private var secondaryNavController: UINavigationController! {
|
private var secondaryNavController: UINavigationController! {
|
||||||
// viewController(for: .secondary) as? UINavigationController
|
viewController(for: .secondary) as? UINavigationController
|
||||||
// }
|
|
||||||
private var secondaryNavController: SplitNavigationController! {
|
|
||||||
viewController(for: .secondary) as? SplitNavigationController
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
|
@ -49,10 +46,7 @@ class MainSplitViewController: UISplitViewController {
|
||||||
setViewController(sidebar, for: .primary)
|
setViewController(sidebar, for: .primary)
|
||||||
primaryBackgroundStyle = .sidebar
|
primaryBackgroundStyle = .sidebar
|
||||||
|
|
||||||
// let secondaryNav = EnhancedNavigationViewController()
|
setViewController(EnhancedNavigationViewController(), for: .secondary)
|
||||||
// secondaryNav.useBrowserStyleNavigation = true
|
|
||||||
let splitNav = SplitNavigationController()
|
|
||||||
setViewController(splitNav, for: .secondary)
|
|
||||||
// don't unnecesarily construct a content VC unless the we're in actually split mode
|
// don't unnecesarily construct a content VC unless the we're in actually split mode
|
||||||
// when we change from compact -> split for the first time, the VC will be transferred anyways
|
// when we change from compact -> split for the first time, the VC will be transferred anyways
|
||||||
if traitCollection.horizontalSizeClass != .compact {
|
if traitCollection.horizontalSizeClass != .compact {
|
||||||
|
@ -104,7 +98,7 @@ class MainSplitViewController: UISplitViewController {
|
||||||
item = .tab(MainTabBarViewController.Tab(rawValue: index)!)
|
item = .tab(MainTabBarViewController.Tab(rawValue: index)!)
|
||||||
} else if let str = command.propertyList as? String {
|
} else if let str = command.propertyList as? String {
|
||||||
if str == "search" {
|
if str == "search" {
|
||||||
item = .explore
|
item = .search
|
||||||
} else if str == "bookmarks" {
|
} else if str == "bookmarks" {
|
||||||
item = .bookmarks
|
item = .bookmarks
|
||||||
} else {
|
} else {
|
||||||
|
@ -175,7 +169,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
$0.1 > $1.1
|
$0.1 > $1.1
|
||||||
}
|
}
|
||||||
if let mostRecentExploreItem = mostRecentExploreItem?.0,
|
if let mostRecentExploreItem = mostRecentExploreItem?.0,
|
||||||
mostRecentExploreItem != .explore {
|
mostRecentExploreItem != .search {
|
||||||
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
||||||
// Pop back to root, so we're appending to the Explore VC instead of some other VC
|
// Pop back to root, so we're appending to the Explore VC instead of some other VC
|
||||||
exploreNav.popToRootViewController(animated: false)
|
exploreNav.popToRootViewController(animated: false)
|
||||||
|
@ -192,7 +186,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
// sidebar items that map 1 <-> 1 can be transferred directly
|
// sidebar items that map 1 <-> 1 can be transferred directly
|
||||||
tabBarViewController.select(tab: tab)
|
tabBarViewController.select(tab: tab)
|
||||||
|
|
||||||
case .explore:
|
case .search:
|
||||||
// Search sidebar item maps to the Explore tab with the search controller/results visible
|
// Search sidebar item maps to the Explore tab with the search controller/results visible
|
||||||
// The nav stack can't be copied directly, since the split VC uses a different SearchViewController
|
// The nav stack can't be copied directly, since the split VC uses a different SearchViewController
|
||||||
// so that explore items aren't shown multiple times.
|
// so that explore items aren't shown multiple times.
|
||||||
|
@ -221,11 +215,11 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
explore.resultsController.loadResults(from: search.resultsController)
|
explore.resultsController.loadResults(from: search.resultsController)
|
||||||
|
|
||||||
// Transfer the navigation stack, dropping the search VC, to keep anything the user has opened
|
// Transfer the navigation stack, dropping the search VC, to keep anything the user has opened
|
||||||
transferNavigationStack(from: .explore, to: exploreNav, dropFirst: true, append: true)
|
transferNavigationStack(from: .search, to: exploreNav, dropFirst: true, append: true)
|
||||||
|
|
||||||
tabBarViewController.select(tab: .explore)
|
tabBarViewController.select(tab: .explore)
|
||||||
|
|
||||||
case .bookmarks, .trendingStatuses, .profileDirectory, .list(_), .savedHashtag(_), .savedInstance(_):
|
case .bookmarks, .trendingStatuses, .trendingTags, .trendingLinks, .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.
|
||||||
|
@ -276,7 +270,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
// For other items, the 2nd VC in the nav stack determines which sidebar item they map to.
|
// For other items, the 2nd VC in the nav stack determines which sidebar item they map to.
|
||||||
// Search screen has special considerations, all others can be transferred directly.
|
// Search screen has special considerations, all others can be transferred directly.
|
||||||
if tabNavigationStack.count == 1 || ((tabNavigationStack.first as? ExploreViewController)?.searchController?.isActive ?? false) {
|
if tabNavigationStack.count == 1 || ((tabNavigationStack.first as? ExploreViewController)?.searchController?.isActive ?? false) {
|
||||||
exploreItem = .explore
|
exploreItem = .search
|
||||||
let searchVC = SearchViewController(mastodonController: mastodonController)
|
let searchVC = SearchViewController(mastodonController: mastodonController)
|
||||||
searchVC.loadViewIfNeeded()
|
searchVC.loadViewIfNeeded()
|
||||||
let explore = tabNavigationStack.first as! ExploreViewController
|
let explore = tabNavigationStack.first as! ExploreViewController
|
||||||
|
@ -304,9 +298,9 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
case is TrendingStatusesViewController:
|
case is TrendingStatusesViewController:
|
||||||
exploreItem = .trendingStatuses
|
exploreItem = .trendingStatuses
|
||||||
case is TrendingHashtagsViewController:
|
case is TrendingHashtagsViewController:
|
||||||
exploreItem = .explore
|
exploreItem = .trendingTags
|
||||||
case is TrendingLinksViewController:
|
case is TrendingLinksViewController:
|
||||||
exploreItem = .explore
|
exploreItem = .trendingLinks
|
||||||
case is ProfileDirectoryViewController:
|
case is ProfileDirectoryViewController:
|
||||||
exploreItem = .profileDirectory
|
exploreItem = .profileDirectory
|
||||||
default:
|
default:
|
||||||
|
@ -358,12 +352,16 @@ fileprivate extension MainSidebarViewController.Item {
|
||||||
switch self {
|
switch self {
|
||||||
case let .tab(tab):
|
case let .tab(tab):
|
||||||
return tab.createViewController(mastodonController)
|
return tab.createViewController(mastodonController)
|
||||||
case .explore:
|
case .search:
|
||||||
return SearchViewController(mastodonController: mastodonController)
|
return SearchViewController(mastodonController: mastodonController)
|
||||||
case .bookmarks:
|
case .bookmarks:
|
||||||
return BookmarksTableViewController(mastodonController: mastodonController)
|
return BookmarksTableViewController(mastodonController: mastodonController)
|
||||||
case .trendingStatuses:
|
case .trendingStatuses:
|
||||||
return TrendingStatusesViewController(mastodonController: mastodonController)
|
return TrendingStatusesViewController(mastodonController: mastodonController)
|
||||||
|
case .trendingTags:
|
||||||
|
return TrendingHashtagsViewController(mastodonController: mastodonController)
|
||||||
|
case .trendingLinks:
|
||||||
|
return TrendingLinksViewController(mastodonController: mastodonController)
|
||||||
case .profileDirectory:
|
case .profileDirectory:
|
||||||
return ProfileDirectoryViewController(mastodonController: mastodonController)
|
return ProfileDirectoryViewController(mastodonController: mastodonController)
|
||||||
case let .list(list):
|
case let .list(list):
|
||||||
|
@ -380,7 +378,7 @@ fileprivate extension MainSidebarViewController.Item {
|
||||||
|
|
||||||
extension MainSplitViewController: TuskerRootViewController {
|
extension MainSplitViewController: TuskerRootViewController {
|
||||||
@objc func presentCompose() {
|
@objc func presentCompose() {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
if #available(iOS 15.0, *), UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id)
|
let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id)
|
||||||
let options = UIWindowScene.ActivationRequestOptions()
|
let options = UIWindowScene.ActivationRequestOptions()
|
||||||
options.preferredPresentationStyle = .prominent
|
options.preferredPresentationStyle = .prominent
|
||||||
|
@ -435,8 +433,8 @@ extension MainSplitViewController: TuskerRootViewController {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if sidebar.selectedItem != .explore {
|
if sidebar.selectedItem != .search {
|
||||||
select(item: .explore)
|
select(item: .search)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let searchViewController = secondaryNavController.viewControllers.first as? SearchViewController else {
|
guard let searchViewController = secondaryNavController.viewControllers.first as? SearchViewController else {
|
||||||
|
|
|
@ -141,9 +141,7 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
if let vc = vc as? UINavigationController {
|
if let vc = vc as? UINavigationController {
|
||||||
return vc
|
return vc
|
||||||
} else {
|
} else {
|
||||||
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
return EnhancedNavigationViewController(rootViewController: vc)
|
||||||
// nav.useBrowserStyleNavigation = true
|
|
||||||
return nav
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,7 +228,7 @@ extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
||||||
|
|
||||||
extension MainTabBarViewController: TuskerRootViewController {
|
extension MainTabBarViewController: TuskerRootViewController {
|
||||||
@objc func presentCompose() {
|
@objc func presentCompose() {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
if #available(iOS 15.0, *), UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id)
|
let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id)
|
||||||
let options = UIWindowScene.ActivationRequestOptions()
|
let options = UIWindowScene.ActivationRequestOptions()
|
||||||
options.preferredPresentationStyle = .prominent
|
options.preferredPresentationStyle = .prominent
|
||||||
|
|
|
@ -107,13 +107,11 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
completion(.failure(.client(error)))
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
case let .success(notifications, _):
|
case let .success(notifications, pagination):
|
||||||
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
|
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
|
||||||
|
|
||||||
if !notifications.isEmpty {
|
self.newer = pagination?.newer
|
||||||
self.newer = .after(id: notifications.first!.id, count: nil)
|
self.older = pagination?.older
|
||||||
self.older = .before(id: notifications.last!.id, count: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(notifications: notifications) {
|
self.mastodonController.persistentContainer.addAll(notifications: notifications) {
|
||||||
var snapshot = Snapshot()
|
var snapshot = Snapshot()
|
||||||
|
@ -137,9 +135,9 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
completion(.failure(.client(error)))
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
case let .success(newNotifications, _):
|
case let .success(newNotifications, pagination):
|
||||||
if !newNotifications.isEmpty {
|
if let older = pagination?.older {
|
||||||
self.older = .before(id: newNotifications.last!.id, count: nil)
|
self.older = older
|
||||||
}
|
}
|
||||||
|
|
||||||
let olderGroups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
let olderGroups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||||
|
@ -168,13 +166,15 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
completion(.failure(.client(error)))
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
case let .success(newNotifications, _):
|
case let .success(newNotifications, pagination):
|
||||||
guard !newNotifications.isEmpty else {
|
guard !newNotifications.isEmpty else {
|
||||||
completion(.failure(.allCaughtUp))
|
completion(.failure(.allCaughtUp))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.newer = .after(id: newNotifications.first!.id, count: nil)
|
if let newer = pagination?.newer {
|
||||||
|
self.newer = newer
|
||||||
|
}
|
||||||
|
|
||||||
let newerGroups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
let newerGroups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||||
|
|
||||||
|
@ -255,12 +255,9 @@ extension NotificationsTableViewController: MenuActionProvider {
|
||||||
|
|
||||||
extension NotificationsTableViewController: StatusTableViewCellDelegate {
|
extension NotificationsTableViewController: StatusTableViewCellDelegate {
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
} else {
|
|
||||||
cellHeightChanged()
|
cellHeightChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
|
|
|
@ -81,9 +81,6 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
searchController.searchBar.searchTextField.autocapitalizationType = .none
|
searchController.searchBar.searchTextField.autocapitalizationType = .none
|
||||||
navigationItem.searchController = searchController
|
navigationItem.searchController = searchController
|
||||||
navigationItem.hidesSearchBarWhenScrolling = false
|
navigationItem.hidesSearchBarWhenScrolling = false
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
navigationItem.preferredSearchBarPlacement = .stacked
|
|
||||||
}
|
|
||||||
definesPresentationContext = true
|
definesPresentationContext = true
|
||||||
|
|
||||||
urlHandler = urlCheckerSubject
|
urlHandler = urlCheckerSubject
|
||||||
|
|
|
@ -38,7 +38,7 @@ struct OppositeCollapseKeywordsView: View {
|
||||||
FocusableTextField(placeholder: "Add Keyword", text: $valueToAdd, becomeFirstResponder: $makeAddFieldFirstResponder, onCommit: self.addKeyword)
|
FocusableTextField(placeholder: "Add Keyword", text: $valueToAdd, becomeFirstResponder: $makeAddFieldFirstResponder, onCommit: self.addKeyword)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.default, value: keywords.map(\.id))
|
.animation(.default)
|
||||||
.listStyle(GroupedListStyle())
|
.listStyle(GroupedListStyle())
|
||||||
}
|
}
|
||||||
.onAppear(perform: updateAppearance)
|
.onAppear(perform: updateAppearance)
|
||||||
|
@ -46,12 +46,8 @@ struct OppositeCollapseKeywordsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateAppearance() {
|
private func updateAppearance() {
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
// no longer necessary
|
|
||||||
} else {
|
|
||||||
UIScrollView.appearance(whenContainedInInstancesOf: [PreferencesNavigationController.self]).keyboardDismissMode = .interactive
|
UIScrollView.appearance(whenContainedInInstancesOf: [PreferencesNavigationController.self]).keyboardDismissMode = .interactive
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private func commitExisting(at index: Int) -> () -> Void {
|
private func commitExisting(at index: Int) -> () -> Void {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -16,7 +16,9 @@ struct WellnessPrefsView: View {
|
||||||
showFavAndReblogCount
|
showFavAndReblogCount
|
||||||
notificationsMode
|
notificationsMode
|
||||||
grayscaleImages
|
grayscaleImages
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
disableInfiniteScrolling
|
disableInfiniteScrolling
|
||||||
|
}
|
||||||
hideDiscover
|
hideDiscover
|
||||||
}
|
}
|
||||||
.listStyle(InsetGroupedListStyle())
|
.listStyle(InsetGroupedListStyle())
|
||||||
|
|
|
@ -265,12 +265,9 @@ extension ProfileStatusesViewController: TuskerNavigationDelegate {
|
||||||
|
|
||||||
extension ProfileStatusesViewController: StatusTableViewCellDelegate {
|
extension ProfileStatusesViewController: StatusTableViewCellDelegate {
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
} else {
|
|
||||||
cellHeightChanged()
|
cellHeightChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
|
|
|
@ -7,17 +7,11 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
|
||||||
import SafariServices
|
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
class SearchViewController: UIViewController {
|
class SearchViewController: UIViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
private var collectionView: UICollectionView!
|
|
||||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
|
||||||
|
|
||||||
var resultsController: SearchResultsViewController!
|
var resultsController: SearchResultsViewController!
|
||||||
var searchController: UISearchController!
|
var searchController: UISearchController!
|
||||||
|
|
||||||
|
@ -28,7 +22,7 @@ class SearchViewController: UIViewController {
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
title = NSLocalizedString("Explore", comment: "explore tab title")
|
title = NSLocalizedString("Search", comment: "search tab title")
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -38,46 +32,12 @@ class SearchViewController: UIViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
|
view.backgroundColor = .systemBackground
|
||||||
let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex]
|
|
||||||
switch sectionIdentifier {
|
|
||||||
case .trendingHashtags:
|
|
||||||
var listConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
|
|
||||||
listConfig.headerMode = .supplementary
|
|
||||||
return .list(using: listConfig, layoutEnvironment: environment)
|
|
||||||
|
|
||||||
case .trendingLinks:
|
|
||||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
|
|
||||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
|
||||||
// todo: i really wish i could just say the height is automatic and let autolayout figure out what it needs to be
|
|
||||||
// using .estimated(whatever) constrains the height to exactly whatever
|
|
||||||
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(250), heightDimension: .estimated(280))
|
|
||||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
|
||||||
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(8), top: nil, trailing: .fixed(8), bottom: nil)
|
|
||||||
let section = NSCollectionLayoutSection(group: group)
|
|
||||||
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
|
|
||||||
section.boundarySupplementaryItems = [
|
|
||||||
NSCollectionLayoutBoundarySupplementaryItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(12)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading)
|
|
||||||
]
|
|
||||||
return section
|
|
||||||
|
|
||||||
default:
|
|
||||||
fatalError("unimplemented")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
|
||||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
||||||
collectionView.delegate = self
|
|
||||||
collectionView.dragDelegate = self
|
|
||||||
collectionView.backgroundColor = .secondarySystemBackground
|
|
||||||
view.addSubview(collectionView)
|
|
||||||
|
|
||||||
dataSource = createDataSource()
|
|
||||||
|
|
||||||
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
||||||
resultsController.exploreNavigationController = self.navigationController
|
resultsController.exploreNavigationController = self.navigationController
|
||||||
searchController = UISearchController(searchResultsController: resultsController)
|
searchController = UISearchController(searchResultsController: resultsController)
|
||||||
searchController.obscuresBackgroundDuringPresentation = true
|
searchController.obscuresBackgroundDuringPresentation = false
|
||||||
searchController.searchBar.autocapitalizationType = .none
|
searchController.searchBar.autocapitalizationType = .none
|
||||||
searchController.searchBar.delegate = resultsController
|
searchController.searchBar.delegate = resultsController
|
||||||
searchController.hidesNavigationBarDuringPresentation = false
|
searchController.hidesNavigationBarDuringPresentation = false
|
||||||
|
@ -85,21 +45,6 @@ class SearchViewController: UIViewController {
|
||||||
|
|
||||||
navigationItem.searchController = searchController
|
navigationItem.searchController = searchController
|
||||||
navigationItem.hidesSearchBarWhenScrolling = false
|
navigationItem.hidesSearchBarWhenScrolling = false
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
navigationItem.preferredSearchBarPlacement = .stacked
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
|
|
||||||
Task(priority: .userInitiated) {
|
|
||||||
if (try? await mastodonController.getOwnInstance()) != nil {
|
|
||||||
await applySnapshot()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
@ -114,216 +59,4 @@ class SearchViewController: UIViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
|
||||||
let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [unowned self] (headerView, collectionView, indexPath) in
|
|
||||||
let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
|
|
||||||
var config = UIListContentConfiguration.groupedHeader()
|
|
||||||
config.text = section.title
|
|
||||||
headerView.contentConfiguration = config
|
|
||||||
}
|
|
||||||
|
|
||||||
let trendingHashtagCell = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { (cell, indexPath, hashtag) in
|
|
||||||
cell.updateUI(hashtag: hashtag)
|
|
||||||
}
|
|
||||||
let trendingLinkCell = UICollectionView.CellRegistration<TrendingLinkCardCollectionViewCell, Card>(cellNib: UINib(nibName: "TrendingLinkCardCollectionViewCell", bundle: .main)) { (cell, indexPath, card) in
|
|
||||||
cell.updateUI(card: card)
|
|
||||||
}
|
|
||||||
|
|
||||||
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
|
|
||||||
switch item {
|
|
||||||
case let .tag(hashtag):
|
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: trendingHashtagCell, for: indexPath, item: hashtag)
|
|
||||||
|
|
||||||
case let .link(card):
|
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: trendingLinkCell, for: indexPath, item: card)
|
|
||||||
|
|
||||||
default:
|
|
||||||
fatalError("todo")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
|
|
||||||
if elementKind == UICollectionView.elementKindSectionHeader {
|
|
||||||
return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath)
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dataSource
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func applySnapshot() async {
|
|
||||||
guard mastodonController.instanceFeatures.trends,
|
|
||||||
!Preferences.shared.hideDiscover else {
|
|
||||||
await dataSource.apply(NSDiffableDataSourceSnapshot())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
|
||||||
|
|
||||||
let hashtagsReq = Client.getTrendingHashtags(limit: 5)
|
|
||||||
async let hashtags = try? mastodonController.run(hashtagsReq).0
|
|
||||||
let linksReq = Client.getTrendingLinks(limit: 10)
|
|
||||||
async let links = try? mastodonController.run(linksReq).0
|
|
||||||
|
|
||||||
if let hashtags = await hashtags {
|
|
||||||
snapshot.appendSections([.trendingHashtags])
|
|
||||||
snapshot.appendItems(hashtags.map { .tag($0) }, toSection: .trendingHashtags)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let links = await links {
|
|
||||||
snapshot.appendSections([.trendingLinks])
|
|
||||||
snapshot.appendItems(links.map { .link($0) }, toSection: .trendingLinks)
|
|
||||||
}
|
|
||||||
|
|
||||||
await dataSource.apply(snapshot)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
|
||||||
Task {
|
|
||||||
await applySnapshot()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SearchViewController {
|
|
||||||
enum Section {
|
|
||||||
case trendingHashtags
|
|
||||||
case trendingLinks
|
|
||||||
case trendingStatuses
|
|
||||||
case profileSuggestions
|
|
||||||
|
|
||||||
var title: String {
|
|
||||||
switch self {
|
|
||||||
case .trendingHashtags:
|
|
||||||
return "Trending Hashtags"
|
|
||||||
case .trendingLinks:
|
|
||||||
return "Trending Links"
|
|
||||||
case .trendingStatuses:
|
|
||||||
return "Trending Statuses"
|
|
||||||
case .profileSuggestions:
|
|
||||||
return "Suggested Accounts"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enum Item: Equatable, Hashable {
|
|
||||||
case status(String)
|
|
||||||
case tag(Hashtag)
|
|
||||||
case link(Card)
|
|
||||||
|
|
||||||
static func == (lhs: SearchViewController.Item, rhs: SearchViewController.Item) -> Bool {
|
|
||||||
switch (lhs, rhs) {
|
|
||||||
case let (.status(a), .status(b)):
|
|
||||||
return a == b
|
|
||||||
case let (.tag(a), .tag(b)):
|
|
||||||
return a == b
|
|
||||||
case let (.link(a), .link(b)):
|
|
||||||
return a.url == b.url
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
|
||||||
switch self {
|
|
||||||
case let .status(id):
|
|
||||||
hasher.combine("status")
|
|
||||||
hasher.combine(id)
|
|
||||||
case let .tag(tag):
|
|
||||||
hasher.combine("tag")
|
|
||||||
hasher.combine(tag.name)
|
|
||||||
case let .link(card):
|
|
||||||
hasher.combine("link")
|
|
||||||
hasher.combine(card.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SearchViewController: UICollectionViewDelegate {
|
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch item {
|
|
||||||
case let .tag(hashtag):
|
|
||||||
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
|
||||||
|
|
||||||
case let .link(card):
|
|
||||||
if let url = URL(card.url) {
|
|
||||||
selected(url: url)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
fatalError("todo")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch item {
|
|
||||||
case let .tag(hashtag):
|
|
||||||
return UIContextMenuConfiguration(identifier: nil) {
|
|
||||||
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
|
|
||||||
} actionProvider: { (_) in
|
|
||||||
UIMenu(children: self.actionsForHashtag(hashtag, sourceView: self.collectionView.cellForItem(at: indexPath)))
|
|
||||||
}
|
|
||||||
|
|
||||||
case let .link(card):
|
|
||||||
guard let url = URL(card.url) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return UIContextMenuConfiguration {
|
|
||||||
SFSafariViewController(url: url)
|
|
||||||
} actionProvider: { _ in
|
|
||||||
UIMenu(children: self.actionsForTrendingLink(card: card))
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
fatalError("todo")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SearchViewController: UICollectionViewDragDelegate {
|
|
||||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
switch item {
|
|
||||||
case let .tag(hashtag):
|
|
||||||
guard let url = URL(hashtag.url) else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
let provider = NSItemProvider(object: url as NSURL)
|
|
||||||
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
|
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
provider.registerObject(activity, visibility: .all)
|
|
||||||
}
|
|
||||||
return [UIDragItem(itemProvider: provider)]
|
|
||||||
|
|
||||||
case let .link(card):
|
|
||||||
guard let url = URL(card.url) else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))]
|
|
||||||
|
|
||||||
default:
|
|
||||||
fatalError("todo")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SearchViewController: TuskerNavigationDelegate {
|
|
||||||
var apiController: MastodonController { mastodonController }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SearchViewController: ToastableViewController {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SearchViewController: MenuActionProvider {
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,7 +164,8 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if Preferences.shared.disableInfiniteScrolling && !didConfirmLoadMore {
|
if #available(iOS 15.0, *),
|
||||||
|
Preferences.shared.disableInfiniteScrolling && !didConfirmLoadMore {
|
||||||
var snapshot = currentSnapshot()
|
var snapshot = currentSnapshot()
|
||||||
guard !snapshot.itemIdentifiers(inSection: .footer).contains(.confirmLoadMore) else {
|
guard !snapshot.itemIdentifiers(inSection: .footer).contains(.confirmLoadMore) else {
|
||||||
// todo: need something more accurate than "success"/"failure"
|
// todo: need something more accurate than "success"/"failure"
|
||||||
|
@ -289,12 +290,9 @@ extension TimelineTableViewController: TuskerNavigationDelegate {
|
||||||
|
|
||||||
extension TimelineTableViewController: StatusTableViewCellDelegate {
|
extension TimelineTableViewController: StatusTableViewCellDelegate {
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
} else {
|
|
||||||
cellHeightChanged()
|
cellHeightChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension TimelineTableViewController: MenuActionProvider {
|
extension TimelineTableViewController: MenuActionProvider {
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,9 +34,7 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
dataSource = UITableViewDiffableDataSource(tableView: tableView) { [unowned self] (tableView, indexPath, item) in
|
dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: self.cellProvider)
|
||||||
self.cellProvider(tableView, indexPath, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
tableView.estimatedRowHeight = 140
|
tableView.estimatedRowHeight = 140
|
||||||
|
@ -163,7 +161,6 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS, deprecated: 16.0)
|
|
||||||
func cellHeightChanged() {
|
func cellHeightChanged() {
|
||||||
// causes the table view to recalculate the cell heights
|
// causes the table view to recalculate the cell heights
|
||||||
tableView.beginUpdates()
|
tableView.beginUpdates()
|
||||||
|
|
|
@ -10,78 +10,42 @@ import UIKit
|
||||||
|
|
||||||
class EnhancedNavigationViewController: UINavigationController {
|
class EnhancedNavigationViewController: UINavigationController {
|
||||||
|
|
||||||
var useBrowserStyleNavigation = false
|
|
||||||
|
|
||||||
var poppedViewControllers = [UIViewController]()
|
var poppedViewControllers = [UIViewController]()
|
||||||
var skipResetPoppedOnNextPush = false
|
var skipResetPoppedOnNextPush = false
|
||||||
|
|
||||||
private var interactivePushTransition: InteractivePushTransition!
|
private var interactivePushTransition: InteractivePushTransition!
|
||||||
|
|
||||||
override var viewControllers: [UIViewController] {
|
|
||||||
didSet {
|
|
||||||
poppedViewControllers = []
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
// TODO: this for loop might not be necessary
|
|
||||||
for vc in viewControllers {
|
|
||||||
configureNavItem(vc.navigationItem)
|
|
||||||
}
|
|
||||||
updateTopNavItemState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
self.interactivePushTransition = InteractivePushTransition(navigationController: self)
|
self.interactivePushTransition = InteractivePushTransition(navigationController: self)
|
||||||
|
|
||||||
if #available(iOS 16.0, *),
|
|
||||||
let topViewController {
|
|
||||||
configureNavItem(topViewController.navigationItem)
|
|
||||||
updateTopNavItemState()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func popViewController(animated: Bool) -> UIViewController? {
|
override func popViewController(animated: Bool) -> UIViewController? {
|
||||||
let popped = performAfterAnimating(block: {
|
if let popped = super.popViewController(animated: animated) {
|
||||||
super.popViewController(animated: animated)
|
|
||||||
}, after: {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self.updateTopNavItemState()
|
|
||||||
}
|
|
||||||
}, animated: animated)
|
|
||||||
if let popped {
|
|
||||||
poppedViewControllers.insert(popped, at: 0)
|
poppedViewControllers.insert(popped, at: 0)
|
||||||
}
|
|
||||||
return popped
|
return popped
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func popToRootViewController(animated: Bool) -> [UIViewController]? {
|
override func popToRootViewController(animated: Bool) -> [UIViewController]? {
|
||||||
let popped = performAfterAnimating(block: {
|
if let popped = super.popToRootViewController(animated: animated) {
|
||||||
super.popToRootViewController(animated: animated)
|
|
||||||
}, after: {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self.updateTopNavItemState()
|
|
||||||
}
|
|
||||||
}, animated: animated)
|
|
||||||
if let popped {
|
|
||||||
poppedViewControllers = popped
|
poppedViewControllers = popped
|
||||||
}
|
|
||||||
return popped
|
return popped
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
|
override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
|
||||||
let popped = performAfterAnimating(block: {
|
if let popped = super.popToViewController(viewController, animated: animated) {
|
||||||
super.popToViewController(viewController, animated: animated)
|
|
||||||
}, after: {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self.updateTopNavItemState()
|
|
||||||
}
|
|
||||||
}, animated: animated)
|
|
||||||
if let popped {
|
|
||||||
poppedViewControllers.insert(contentsOf: popped, at: 0)
|
poppedViewControllers.insert(contentsOf: popped, at: 0)
|
||||||
}
|
|
||||||
return popped
|
return popped
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
|
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
|
||||||
|
@ -90,49 +54,7 @@ class EnhancedNavigationViewController: UINavigationController {
|
||||||
} else {
|
} else {
|
||||||
self.poppedViewControllers = []
|
self.poppedViewControllers = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
configureNavItem(viewController.navigationItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.pushViewController(viewController, animated: animated)
|
super.pushViewController(viewController, animated: animated)
|
||||||
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
updateTopNavItemState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func pushPoppedViewController() {
|
|
||||||
guard !poppedViewControllers.isEmpty else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
skipResetPoppedOnNextPush = true
|
|
||||||
pushViewController(poppedViewControllers.removeFirst(), animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func pushToPoppedViewController(_ target: UIViewController) {
|
|
||||||
guard poppedViewControllers.contains(target) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var toInsert: [UIViewController] = []
|
|
||||||
while true {
|
|
||||||
let vc = poppedViewControllers.removeFirst()
|
|
||||||
if vc == target {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
toInsert.append(vc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// match the system behavior when popping multiple by animated-ly pushing the final destination one,
|
|
||||||
// and then intersiting the intermediary ones before it, as if they'd all been pushed together
|
|
||||||
performAfterAnimating(block: {
|
|
||||||
pushViewController(target, animated: true)
|
|
||||||
}, after: {
|
|
||||||
self.viewControllers.insert(contentsOf: toInsert, at: self.viewControllers.count - 1)
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
self.updateTopNavItemState()
|
|
||||||
}
|
|
||||||
}, animated: true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func onWillShow() {
|
func onWillShow() {
|
||||||
|
@ -151,94 +73,6 @@ class EnhancedNavigationViewController: UINavigationController {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 16.0, *)
|
|
||||||
private func configureNavItem(_ navItem: UINavigationItem) {
|
|
||||||
guard useBrowserStyleNavigation,
|
|
||||||
UIDevice.current.userInterfaceIdiom != .phone else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
navItem.style = .browser
|
|
||||||
navItem.hidesBackButton = true
|
|
||||||
if let titleView = navItem.titleView,
|
|
||||||
titleView.tag != ViewTags.navEmptyTitleView {
|
|
||||||
// blergh, i don't like changing this out from under some other view controller
|
|
||||||
// we use an empty view because otherwise the title label displays in addition to the new title view bar button item
|
|
||||||
navItem.titleView = UIView()
|
|
||||||
navItem.titleView?.tag = ViewTags.navEmptyTitleView
|
|
||||||
// TODO: centerItemGroups don't animate out during nav transitions, the just (dis)appear abruptly
|
|
||||||
navItem.centerItemGroups = [
|
|
||||||
.fixedGroup(items: [UIBarButtonItem(customView: titleView)])
|
|
||||||
]
|
|
||||||
}
|
|
||||||
let back = UIBarButtonItem(image: UIImage(systemName: "chevron.backward"), style: .plain, target: self, action: #selector(goBack))
|
|
||||||
back.tag = ViewTags.navBackBarButton
|
|
||||||
back.menu = UIMenu(children: [
|
|
||||||
UIDeferredMenuElement({ [unowned self] completion in
|
|
||||||
completion(self.viewControllers.dropLast(1).reversed().map { vc in
|
|
||||||
UIAction(title: vc.navigationItem.title ?? "Back") { [weak self] _ in
|
|
||||||
_ = self?.popToViewController(vc, animated: true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
])
|
|
||||||
let forward = UIBarButtonItem(image: UIImage(systemName: "chevron.forward"), style: .plain, target: self, action: #selector(goForward))
|
|
||||||
forward.tag = ViewTags.navForwardBarButton
|
|
||||||
forward.menu = UIMenu(children: [
|
|
||||||
UIDeferredMenuElement.uncached({ [unowned self] completion in
|
|
||||||
completion(poppedViewControllers.map { vc in
|
|
||||||
UIAction(title: vc.navigationItem.title ?? "Forward") { [weak self] _ in
|
|
||||||
self?.pushToPoppedViewController(vc)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
])
|
|
||||||
navItem.leadingItemGroups = [
|
|
||||||
.fixedGroup(items: [
|
|
||||||
back,
|
|
||||||
forward,
|
|
||||||
])
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS 16.0, *)
|
|
||||||
private func updateTopNavItemState() {
|
|
||||||
guard useBrowserStyleNavigation,
|
|
||||||
UIDevice.current.userInterfaceIdiom != .phone,
|
|
||||||
let vc = topViewController,
|
|
||||||
let group = vc.navigationItem.leadingItemGroups.first,
|
|
||||||
group.barButtonItems.count == 2,
|
|
||||||
group.barButtonItems[0].tag == ViewTags.navBackBarButton,
|
|
||||||
group.barButtonItems[1].tag == ViewTags.navForwardBarButton else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
group.barButtonItems[0].isEnabled = viewControllers.count > 1
|
|
||||||
group.barButtonItems[1].isEnabled = !poppedViewControllers.isEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
private func performAfterAnimating<R>(block: () -> R, after: @escaping () -> Void, animated: Bool) -> R {
|
|
||||||
if animated {
|
|
||||||
CATransaction.begin()
|
|
||||||
let result = block()
|
|
||||||
CATransaction.setCompletionBlock {
|
|
||||||
after()
|
|
||||||
}
|
|
||||||
CATransaction.commit()
|
|
||||||
return result
|
|
||||||
} else {
|
|
||||||
let result = block()
|
|
||||||
after()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func goBack() {
|
|
||||||
_ = popViewController(animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func goForward() {
|
|
||||||
pushPoppedViewController()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension EnhancedNavigationViewController: BackgroundableViewController {
|
extension EnhancedNavigationViewController: BackgroundableViewController {
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
protocol MenuActionProvider: AnyObject {
|
protocol MenuActionProvider: AnyObject {
|
||||||
var navigationDelegate: TuskerNavigationDelegate? { get }
|
var navigationDelegate: TuskerNavigationDelegate? { get }
|
||||||
|
@ -60,7 +59,7 @@ extension MenuActionProvider {
|
||||||
draft.visibility = .direct
|
draft.visibility = .direct
|
||||||
self.navigationDelegate?.compose(editing: draft)
|
self.navigationDelegate?.compose(editing: draft)
|
||||||
}),
|
}),
|
||||||
UIDeferredMenuElement.uncached({ (elementHandler) in
|
UIDeferredMenuElement.uncachedIfPossible({ (elementHandler) in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
if let action = await self.followAction(for: accountID, mastodonController: mastodonController) {
|
if let action = await self.followAction(for: accountID, mastodonController: mastodonController) {
|
||||||
elementHandler([action])
|
elementHandler([action])
|
||||||
|
@ -117,12 +116,7 @@ extension MenuActionProvider {
|
||||||
actionsSection = []
|
actionsSection = []
|
||||||
}
|
}
|
||||||
|
|
||||||
let shareSection: [UIMenuElement]
|
let shareSection = actionsForURL(hashtag.url, sourceView: sourceView)
|
||||||
if let url = URL(hashtag.url) {
|
|
||||||
shareSection = actionsForURL(url, sourceView: sourceView)
|
|
||||||
} else {
|
|
||||||
shareSection = []
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
||||||
|
@ -143,9 +137,10 @@ extension MenuActionProvider {
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
let bookmarked = status.bookmarked ?? false
|
let bookmarked = status.bookmarked ?? false
|
||||||
|
|
||||||
var toggleableSection = [
|
var actionsSection = [
|
||||||
createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in
|
createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let request = (bookmarked ? Status.unbookmark : Status.bookmark)(status.id)
|
let request = (bookmarked ? Status.unbookmark : Status.bookmark)(status.id)
|
||||||
|
@ -166,57 +161,6 @@ extension MenuActionProvider {
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
let favorited = status.favourited
|
|
||||||
// TODO: move this color into an asset catalog or something
|
|
||||||
var favImage = UIImage(systemName: favorited ? "star.fill" : "star")!
|
|
||||||
if favorited {
|
|
||||||
favImage = favImage.withTintColor(UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1), renderingMode: .alwaysOriginal)
|
|
||||||
}
|
|
||||||
toggleableSection.insert(createAction(identifier: "favorite", title: favorited ? "Unfavorite" : "Favorite", image: favImage, handler: { [weak self] _ in
|
|
||||||
guard let self = self else { return }
|
|
||||||
let request = (favorited ? Status.favourite : Status.unfavourite)(status.id)
|
|
||||||
self.mastodonController?.run(request, completion: { response in
|
|
||||||
switch response {
|
|
||||||
case .success(let status, _):
|
|
||||||
self.mastodonController?.persistentContainer.addOrUpdate(status: status)
|
|
||||||
case .failure(let error):
|
|
||||||
if let toastable = self.toastableViewController {
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error \(favorited ? "Unf" : "F")avoriting", in: toastable, retryAction: nil)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
toastable.showToast(configuration: config, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}), at: 0)
|
|
||||||
|
|
||||||
let reblogged = status.reblogged
|
|
||||||
var reblogImage = UIImage(systemName: "repeat")!
|
|
||||||
if reblogged {
|
|
||||||
reblogImage = reblogImage.withTintColor(UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1), renderingMode: .alwaysOriginal)
|
|
||||||
}
|
|
||||||
toggleableSection.insert(createAction(identifier: "reblog", title: reblogged ? "Unreblog" : "Reblog", image: reblogImage, handler: { [weak self] _ in
|
|
||||||
guard let self = self else { return }
|
|
||||||
let request = (reblogged ? Status.reblog : Status.unreblog)(status.id)
|
|
||||||
self.mastodonController?.run(request, completion: { response in
|
|
||||||
switch response {
|
|
||||||
case .success(let status, _):
|
|
||||||
self.mastodonController?.persistentContainer.addOrUpdate(status: status)
|
|
||||||
case .failure(let error):
|
|
||||||
if let toastable = self.toastableViewController {
|
|
||||||
let config = ToastConfiguration(from: error, with: "Error \(reblogged ? "Unr" : "R")eblogging", in: toastable, retryAction: nil)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
toastable.showToast(configuration: config, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}), at: 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
var actionsSection: [UIAction] = []
|
|
||||||
|
|
||||||
if includeReply {
|
if includeReply {
|
||||||
actionsSection.insert(createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { [weak self] (_) in
|
actionsSection.insert(createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { [weak self] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
@ -228,7 +172,7 @@ extension MenuActionProvider {
|
||||||
// only allow muting conversations that either current user posted or is participating in (technically, is mentioned, since that's the best we can do)
|
// only allow muting conversations that either current user posted or is participating in (technically, is mentioned, since that's the best we can do)
|
||||||
if status.account.id == account.id || status.mentions.contains(where: { $0.id == account.id }) {
|
if status.account.id == account.id || status.mentions.contains(where: { $0.id == account.id }) {
|
||||||
let muted = status.muted
|
let muted = status.muted
|
||||||
toggleableSection.append(createAction(identifier: "mute", title: muted ? "Unmute Conversation" : "Mute Conversation", systemImageName: muted ? "speaker" : "speaker.slash", handler: { [weak self] (_) in
|
actionsSection.append(createAction(identifier: "mute", title: muted ? "Unmute Conversation" : "Mute Conversation", systemImageName: muted ? "speaker" : "speaker.slash", handler: { [weak self] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let request = (muted ? Status.unmuteConversation : Status.muteConversation)(status.id)
|
let request = (muted ? Status.unmuteConversation : Status.muteConversation)(status.id)
|
||||||
self.mastodonController?.run(request) { (response) in
|
self.mastodonController?.run(request) { (response) in
|
||||||
|
@ -251,7 +195,7 @@ extension MenuActionProvider {
|
||||||
if account.id == status.account.id,
|
if account.id == status.account.id,
|
||||||
mastodonController.instanceFeatures.profilePinnedStatuses {
|
mastodonController.instanceFeatures.profilePinnedStatuses {
|
||||||
let pinned = status.pinned ?? false
|
let pinned = status.pinned ?? false
|
||||||
toggleableSection.append(createAction(identifier: "pin", title: pinned ? "Unpin from Profile" : "Pin to Profile", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
|
actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin from Profile" : "Pin to Profile", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let request = (pinned ? Status.unpin : Status.pin)(status.id)
|
let request = (pinned ? Status.unpin : Status.pin)(status.id)
|
||||||
self.mastodonController?.run(request, completion: { [weak self] (response) in
|
self.mastodonController?.run(request, completion: { [weak self] (response) in
|
||||||
|
@ -306,18 +250,10 @@ extension MenuActionProvider {
|
||||||
|
|
||||||
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID))
|
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID))
|
||||||
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
return [
|
return [
|
||||||
UIMenu(options: .displayInline, preferredElementSize: .medium, children: toggleableSection + actionsSection),
|
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
||||||
UIMenu(options: .displayInline, children: shareSection),
|
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection),
|
||||||
]
|
]
|
||||||
} else {
|
|
||||||
return [
|
|
||||||
UIMenu(options: .displayInline, children: shareSection),
|
|
||||||
UIMenu(options: .displayInline, children: toggleableSection),
|
|
||||||
UIMenu(options: .displayInline, children: actionsSection),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionsForTrendingLink(card: Card) -> [UIMenuElement] {
|
func actionsForTrendingLink(card: Card) -> [UIMenuElement] {
|
||||||
|
@ -350,10 +286,6 @@ extension MenuActionProvider {
|
||||||
} else {
|
} else {
|
||||||
image = nil
|
image = nil
|
||||||
}
|
}
|
||||||
return createAction(identifier: identifier, title: title, image: image, handler: handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createAction(identifier: String, title: String, image: UIImage?, handler: @escaping UIActionHandler) -> UIAction {
|
|
||||||
return UIAction(title: title, image: image, identifier: UIAction.Identifier(identifier), discoverabilityTitle: nil, attributes: [], state: .off, handler: handler)
|
return UIAction(title: title, image: image, identifier: UIAction.Identifier(identifier), discoverabilityTitle: nil, attributes: [], state: .off, handler: handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -364,13 +296,17 @@ extension MenuActionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addOpenInNewWindow(actions: inout [UIAction], activity: @escaping @autoclosure () -> NSUserActivity) {
|
private func addOpenInNewWindow(actions: inout [UIAction], activity: @escaping @autoclosure () -> NSUserActivity) {
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
let options = UIWindowScene.ActivationRequestOptions()
|
let options = UIWindowScene.ActivationRequestOptions()
|
||||||
options.preferredPresentationStyle = .automatic
|
options.preferredPresentationStyle = .automatic
|
||||||
actions.append(UIWindowScene.ActivationAction { (_) in
|
actions.append(UIWindowScene.ActivationAction { (_) in
|
||||||
let activity = activity()
|
return .init(userActivity: activity(), options: options, preview: nil)
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
return .init(userActivity: activity, options: options, preview: nil)
|
|
||||||
})
|
})
|
||||||
|
} else if UIApplication.shared.supportsMultipleScenes {
|
||||||
|
actions.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "rectangle.badge.plus", handler: { (_) in
|
||||||
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity(), options: nil, errorHandler: nil)
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func followAction(for accountID: String, mastodonController: MastodonController) async -> UIMenuElement? {
|
private func followAction(for accountID: String, mastodonController: MastodonController) async -> UIMenuElement? {
|
||||||
|
@ -421,3 +357,13 @@ extension SFSafariViewController: CustomPreviewPresenting {
|
||||||
presenter.present(self, animated: true)
|
presenter.present(self, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension UIDeferredMenuElement {
|
||||||
|
static func uncachedIfPossible(_ elementProvider: @escaping (@escaping ([UIMenuElement]) -> Void) -> Void) -> UIDeferredMenuElement {
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
return UIDeferredMenuElement.uncached(elementProvider)
|
||||||
|
} else {
|
||||||
|
return UIDeferredMenuElement(elementProvider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -24,12 +24,6 @@ class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDel
|
||||||
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
||||||
|
|
||||||
self.delegate = self
|
self.delegate = self
|
||||||
|
|
||||||
// this needs to happen in init because EnhancedNavigationViewController expects to be able to look at the titleView
|
|
||||||
// before the view has necessarily loaded
|
|
||||||
segmentedControl = UISegmentedControl(items: titles)
|
|
||||||
segmentedControl.addTarget(self, action: #selector(segmentedControlChanged), for: .valueChanged)
|
|
||||||
navigationItem.titleView = segmentedControl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -41,6 +35,10 @@ class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDel
|
||||||
|
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .systemBackground
|
||||||
|
|
||||||
|
segmentedControl = UISegmentedControl(items: titles)
|
||||||
|
segmentedControl.addTarget(self, action: #selector(segmentedControlChanged), for: .valueChanged)
|
||||||
|
navigationItem.titleView = segmentedControl
|
||||||
|
|
||||||
segmentedControl.selectedSegmentIndex = 0
|
segmentedControl.selectedSegmentIndex = 0
|
||||||
selectPage(at: 0, animated: false)
|
selectPage(at: 0, animated: false)
|
||||||
|
|
||||||
|
|
|
@ -1,262 +0,0 @@
|
||||||
//
|
|
||||||
// SplitNavigationController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 7/1/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class SplitNavigationController: UIViewController {
|
|
||||||
|
|
||||||
private let rootNav = SplitRootNavigationController()
|
|
||||||
private let secondaryNav = SplitSecondaryNavigationController()
|
|
||||||
private let separatorView = UIView()
|
|
||||||
|
|
||||||
private var constraints: [NSLayoutConstraint] = []
|
|
||||||
|
|
||||||
var viewControllers: [UIViewController] {
|
|
||||||
get {
|
|
||||||
return rootNav.viewControllers + secondaryNav.viewControllers
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
if newValue.isEmpty {
|
|
||||||
rootNav.viewControllers = []
|
|
||||||
secondaryNav.viewControllers = []
|
|
||||||
} else if canShowSecondaryNav {
|
|
||||||
var newValue = newValue
|
|
||||||
rootNav.viewControllers = [newValue.removeFirst()]
|
|
||||||
secondaryNav.viewControllers = newValue
|
|
||||||
} else {
|
|
||||||
rootNav.viewControllers = newValue
|
|
||||||
secondaryNav.viewControllers = []
|
|
||||||
}
|
|
||||||
updateSecondaryNavVisibility()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This property is only valid after the view has been laid out.
|
|
||||||
private var canShowSecondaryNav: Bool {
|
|
||||||
// minimum of 360pt for each column
|
|
||||||
// this allows split navigation on all ipads in portrait w/ sidebar hidden and in landscape (regardless of sidebar)
|
|
||||||
(viewIfLoaded?.bounds.width ?? 0) >= 720
|
|
||||||
}
|
|
||||||
|
|
||||||
init(rootViewController: UIViewController? = nil) {
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
|
|
||||||
rootNav.showImpl = { [unowned self] vc, sender in
|
|
||||||
if self.canShowSecondaryNav {
|
|
||||||
self.setSecondaryViewControllers([vc], animated: true)
|
|
||||||
|
|
||||||
// the split nav shouldn't really be reaching down into the inner VCs like this,
|
|
||||||
// but I can't think of a cleaner way
|
|
||||||
if let tableVC = sender as? UITableViewController,
|
|
||||||
let selectedIndexPath = tableVC.tableView.indexPathForSelectedRow {
|
|
||||||
tableVC.tableView.deselectRow(at: selectedIndexPath, animated: true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.rootNav.pushViewController(vc, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
secondaryNav.closeSecondaryImpl = { [unowned self] in
|
|
||||||
self.popToRootViewController(animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let rootViewController {
|
|
||||||
rootNav.viewControllers = [rootViewController]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
embedChild(rootNav, layout: false)
|
|
||||||
embedChild(secondaryNav, layout: false)
|
|
||||||
rootNav.view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
secondaryNav.view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
separatorView.backgroundColor = .separator
|
|
||||||
separatorView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
view.addSubview(separatorView)
|
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
rootNav.view.topAnchor.constraint(equalTo: view.topAnchor),
|
|
||||||
rootNav.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
||||||
rootNav.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
||||||
|
|
||||||
separatorView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
||||||
separatorView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
||||||
separatorView.leadingAnchor.constraint(equalTo: rootNav.view.trailingAnchor),
|
|
||||||
separatorView.widthAnchor.constraint(equalToConstant: 0.5),
|
|
||||||
|
|
||||||
secondaryNav.view.topAnchor.constraint(equalTo: view.topAnchor),
|
|
||||||
secondaryNav.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
||||||
secondaryNav.view.leadingAnchor.constraint(equalTo: separatorView.trailingAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
updateSecondaryNavVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func show(_ vc: UIViewController, sender: Any?) {
|
|
||||||
if !canShowSecondaryNav {
|
|
||||||
rootNav.pushViewController(vc, animated: true)
|
|
||||||
} else if rootNav.viewControllers.isEmpty {
|
|
||||||
rootNav.pushViewController(vc, animated: false)
|
|
||||||
} else {
|
|
||||||
secondaryNav.pushViewController(vc, animated: true)
|
|
||||||
}
|
|
||||||
updateSecondaryNavVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
||||||
super.traitCollectionDidChange(previousTraitCollection)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillLayoutSubviews() {
|
|
||||||
super.viewWillLayoutSubviews()
|
|
||||||
|
|
||||||
if !isLayingOutForAnimation {
|
|
||||||
updateSecondaryNavVisibility()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateSecondaryNavVisibility() {
|
|
||||||
guard isViewLoaded else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if canShowSecondaryNav {
|
|
||||||
if rootNav.viewControllers.count > 1 {
|
|
||||||
var vcs = rootNav.viewControllers
|
|
||||||
let root = vcs.removeFirst()
|
|
||||||
rootNav.viewControllers = [root]
|
|
||||||
// this shouldn't be necessary since the vcs are removed from their parent vc by setting rootNav.viewControllers
|
|
||||||
// but it doesn't remove the views from their superview (until the next runloop iteration?)
|
|
||||||
// so we need to do that ourselves before we can set them on the secondary nav (otherwise it raises an exception)
|
|
||||||
vcs.forEach { $0.removeViewAndController() }
|
|
||||||
secondaryNav.viewControllers = vcs
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !secondaryNav.viewControllers.isEmpty {
|
|
||||||
let firstSecondary = secondaryNav.viewControllers.first!
|
|
||||||
// remove the left bar button item so that the builtin Back item shows
|
|
||||||
if firstSecondary.navigationItem.leftBarButtonItem?.tag == ViewTags.splitNavCloseSecondaryButton {
|
|
||||||
firstSecondary.navigationItem.leftBarButtonItem = nil
|
|
||||||
}
|
|
||||||
rootNav.viewControllers.append(contentsOf: secondaryNav.viewControllers)
|
|
||||||
secondaryNav.viewControllers = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setSecondaryVisible(canShowSecondaryNav && !secondaryNav.viewControllers.isEmpty)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setSecondaryVisible(_ visible: Bool) {
|
|
||||||
guard isViewLoaded else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
NSLayoutConstraint.deactivate(constraints)
|
|
||||||
if visible {
|
|
||||||
constraints = [
|
|
||||||
rootNav.view.trailingAnchor.constraint(equalTo: view.centerXAnchor),
|
|
||||||
secondaryNav.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
constraints = [
|
|
||||||
rootNav.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
||||||
secondaryNav.view.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
NSLayoutConstraint.activate(constraints)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setSecondaryViewControllers(_ vcs: [UIViewController], animated: Bool) {
|
|
||||||
if animated {
|
|
||||||
if vcs.isEmpty {
|
|
||||||
popToRootViewController(animated: true)
|
|
||||||
} else {
|
|
||||||
let wasVisible = !secondaryNav.viewControllers.isEmpty
|
|
||||||
secondaryNav.viewControllers = vcs
|
|
||||||
secondaryNav.view.frame = CGRect(x: view.bounds.width, y: 0, width: view.bounds.width / 2, height: view.bounds.height)
|
|
||||||
secondaryNav.view.layoutIfNeeded()
|
|
||||||
if !wasVisible {
|
|
||||||
let animator = UIViewPropertyAnimator(duration: 0.35, curve: .easeInOut) {
|
|
||||||
self.updateSecondaryNavVisibility()
|
|
||||||
self.view.layoutIfNeeded()
|
|
||||||
}
|
|
||||||
animator.startAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
secondaryNav.viewControllers = vcs
|
|
||||||
updateSecondaryNavVisibility()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var isLayingOutForAnimation = false
|
|
||||||
|
|
||||||
func popToRootViewController(animated: Bool) {
|
|
||||||
if animated {
|
|
||||||
// we don't update secondaryNav.viewControllers until after the animation is completed
|
|
||||||
// otherwise the secondary nav's contents disappear immediately, rather than sliding off-screen
|
|
||||||
let animator = UIViewPropertyAnimator(duration: 0.35, curve: .easeInOut) {
|
|
||||||
self.isLayingOutForAnimation = true
|
|
||||||
self.setSecondaryVisible(false)
|
|
||||||
self.view.layoutIfNeeded()
|
|
||||||
}
|
|
||||||
animator.addCompletion { _ in
|
|
||||||
self.secondaryNav.viewControllers = []
|
|
||||||
self.isLayingOutForAnimation = false
|
|
||||||
// self.updateSecondaryNavVisibility()
|
|
||||||
}
|
|
||||||
animator.startAnimation()
|
|
||||||
} else {
|
|
||||||
self.secondaryNav.viewControllers = []
|
|
||||||
self.updateSecondaryNavVisibility()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SplitRootNavigationController: UINavigationController {
|
|
||||||
fileprivate var showImpl: ((UIViewController, Any?) -> Void)!
|
|
||||||
|
|
||||||
override func show(_ vc: UIViewController, sender: Any?) {
|
|
||||||
showImpl(vc, sender)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SplitSecondaryNavigationController: EnhancedNavigationViewController {
|
|
||||||
fileprivate var closeSecondaryImpl: (() -> Void)!
|
|
||||||
|
|
||||||
override var viewControllers: [UIViewController] {
|
|
||||||
didSet {
|
|
||||||
if let first = viewControllers.first {
|
|
||||||
configureSecondarySplitCloseButton(for: first)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func configureSecondarySplitCloseButton(for viewController: UIViewController) {
|
|
||||||
guard viewController.navigationItem.leftBarButtonItem?.tag != ViewTags.splitNavCloseSecondaryButton else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let item = UIBarButtonItem(title: "Close", style: .done, target: self, action: #selector(closeSecondary))
|
|
||||||
item.tag = ViewTags.splitNavCloseSecondaryButton
|
|
||||||
viewController.navigationItem.leftBarButtonItem = item
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func closeSecondary() {
|
|
||||||
closeSecondaryImpl()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
||||||
|
|
||||||
// Based on MVCTodo by Dave DeLong: https://github.com/davedelong/MVCTodo/blob/841649dd6aa31bacda3ad7ef9a9a836f66281e50/MVCTodo/Extensions/UIViewController.swift
|
// Based on MVCTodo by Dave DeLong: https://github.com/davedelong/MVCTodo/blob/841649dd6aa31bacda3ad7ef9a9a836f66281e50/MVCTodo/Extensions/UIViewController.swift
|
||||||
extension UIViewController {
|
extension UIViewController {
|
||||||
func embedChild(_ newChild: UIViewController, in container: UIView? = nil, layout: Bool = true) {
|
func embedChild(_ newChild: UIViewController, in container: UIView? = nil) {
|
||||||
// if the view controller is already a child of something else, remove it
|
// if the view controller is already a child of something else, remove it
|
||||||
if let oldParent = newChild.parent, oldParent != self {
|
if let oldParent = newChild.parent, oldParent != self {
|
||||||
newChild.beginAppearanceTransition(false, animated: false)
|
newChild.beginAppearanceTransition(false, animated: false)
|
||||||
|
@ -36,7 +36,7 @@ extension UIViewController {
|
||||||
newChild.beginAppearanceTransition(true, animated: false)
|
newChild.beginAppearanceTransition(true, animated: false)
|
||||||
addChild(newChild)
|
addChild(newChild)
|
||||||
newChild.didMove(toParent: self)
|
newChild.didMove(toParent: self)
|
||||||
targetContainer.embedSubview(newChild.view, layout: layout)
|
targetContainer.embedSubview(newChild.view)
|
||||||
newChild.endAppearanceTransition()
|
newChild.endAppearanceTransition()
|
||||||
} else {
|
} else {
|
||||||
// the view controller is already a child
|
// the view controller is already a child
|
||||||
|
@ -45,7 +45,7 @@ extension UIViewController {
|
||||||
// we don't do the appearance transition stuff here,
|
// we don't do the appearance transition stuff here,
|
||||||
// because the vc is already a child, so *presumably*
|
// because the vc is already a child, so *presumably*
|
||||||
// that transition stuff has already appened
|
// that transition stuff has already appened
|
||||||
targetContainer.embedSubview(newChild.view, layout: layout)
|
targetContainer.embedSubview(newChild.view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,17 +57,15 @@ extension UIViewController {
|
||||||
|
|
||||||
// Based on MVCTodo by Dave DeLong: https://github.com/davedelong/MVCTodo/blob/841649dd6aa31bacda3ad7ef9a9a836f66281e50/MVCTodo/Extensions/UIView.swift
|
// Based on MVCTodo by Dave DeLong: https://github.com/davedelong/MVCTodo/blob/841649dd6aa31bacda3ad7ef9a9a836f66281e50/MVCTodo/Extensions/UIView.swift
|
||||||
extension UIView {
|
extension UIView {
|
||||||
func embedSubview(_ subview: UIView, layout: Bool = true) {
|
func embedSubview(_ subview: UIView) {
|
||||||
if subview.superview == self { return }
|
if subview.superview == self { return }
|
||||||
|
|
||||||
if subview.superview != nil {
|
if subview.superview != nil {
|
||||||
subview.removeFromSuperview()
|
subview.removeFromSuperview()
|
||||||
}
|
}
|
||||||
|
|
||||||
addSubview(subview)
|
|
||||||
|
|
||||||
if layout {
|
|
||||||
subview.frame = bounds
|
subview.frame = bounds
|
||||||
|
addSubview(subview)
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
subview.leadingAnchor.constraint(equalTo: leadingAnchor),
|
subview.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
@ -76,7 +74,6 @@ extension UIView {
|
||||||
subview.bottomAnchor.constraint(equalTo: bottomAnchor)
|
subview.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func isContainedWithin(_ other: UIView) -> Bool {
|
func isContainedWithin(_ other: UIView) -> Bool {
|
||||||
var current: UIView? = self
|
var current: UIView? = self
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import UniformTypeIdentifiers
|
|
||||||
|
|
||||||
class PostService: ObservableObject {
|
class PostService: ObservableObject {
|
||||||
private let mastodonController: MastodonController
|
private let mastodonController: MastodonController
|
||||||
|
@ -67,15 +66,15 @@ class PostService: ObservableObject {
|
||||||
attachments.reserveCapacity(draft.attachments.count)
|
attachments.reserveCapacity(draft.attachments.count)
|
||||||
for (index, attachment) in draft.attachments.enumerated() {
|
for (index, attachment) in draft.attachments.enumerated() {
|
||||||
let data: Data
|
let data: Data
|
||||||
let utType: UTType
|
let mimeType: String
|
||||||
do {
|
do {
|
||||||
(data, utType) = try await getData(for: attachment)
|
(data, mimeType) = try await getData(for: attachment)
|
||||||
currentStep += 1
|
currentStep += 1
|
||||||
} catch let error as CompositionAttachmentData.Error {
|
} catch let error as CompositionAttachmentData.Error {
|
||||||
throw Error.attachmentData(index: index, cause: error)
|
throw Error.attachmentData(index: index, cause: error)
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
let uploaded = try await uploadAttachment(data: data, utType: utType, description: attachment.attachmentDescription)
|
let uploaded = try await uploadAttachment(data: data, mimeType: mimeType, description: attachment.attachmentDescription)
|
||||||
attachments.append(uploaded)
|
attachments.append(uploaded)
|
||||||
currentStep += 1
|
currentStep += 1
|
||||||
} catch let error as Client.Error {
|
} catch let error as Client.Error {
|
||||||
|
@ -85,7 +84,7 @@ class PostService: ObservableObject {
|
||||||
return attachments
|
return attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getData(for attachment: CompositionAttachment) async throws -> (Data, UTType) {
|
private func getData(for attachment: CompositionAttachment) async throws -> (Data, String) {
|
||||||
return try await withCheckedThrowingContinuation { continuation in
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
attachment.data.getData { result in
|
attachment.data.getData { result in
|
||||||
switch result {
|
switch result {
|
||||||
|
@ -98,8 +97,8 @@ class PostService: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func uploadAttachment(data: Data, utType: UTType, description: String?) async throws -> Attachment {
|
private func uploadAttachment(data: Data, mimeType: String, description: String?) async throws -> Attachment {
|
||||||
let formAttachment = FormAttachment(mimeType: utType.preferredMIMEType!, data: data, fileName: "file.\(utType.preferredFilenameExtension!)")
|
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file")
|
||||||
let req = Client.upload(attachment: formAttachment, description: description)
|
let req = Client.upload(attachment: formAttachment, description: description)
|
||||||
return try await mastodonController.run(req).0
|
return try await mastodonController.run(req).0
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ extension TuskerNavigationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(editing draft: Draft) {
|
func compose(editing draft: Draft) {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
if #available(iOS 15.0, *), UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
let compose = UserActivityManager.editDraftActivity(id: draft.id, accountID: apiController.accountInfo!.id)
|
let compose = UserActivityManager.editDraftActivity(id: draft.id, accountID: apiController.accountInfo!.id)
|
||||||
let options = UIWindowScene.ActivationRequestOptions()
|
let options = UIWindowScene.ActivationRequestOptions()
|
||||||
options.preferredPresentationStyle = .prominent
|
options.preferredPresentationStyle = .prominent
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
//
|
|
||||||
// ViewTags.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 6/8/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct ViewTags {
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
static let composeVisibilityBarButton = 42001
|
|
||||||
static let composeLocalOnlyBarButton = 42002
|
|
||||||
static let navBackBarButton = 42003
|
|
||||||
static let navForwardBarButton = 42004
|
|
||||||
static let navEmptyTitleView = 42005
|
|
||||||
static let splitNavCloseSecondaryButton = 42006
|
|
||||||
}
|
|
|
@ -23,6 +23,7 @@ class ConfirmLoadMoreTableViewCell: UITableViewCell {
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
var config = UIButton.Configuration.tinted()
|
var config = UIButton.Configuration.tinted()
|
||||||
config.title = "Load More"
|
config.title = "Load More"
|
||||||
config.showsActivityIndicator = false
|
config.showsActivityIndicator = false
|
||||||
|
@ -32,18 +33,23 @@ class ConfirmLoadMoreTableViewCell: UITableViewCell {
|
||||||
button.configuration?.showsActivityIndicator = self.isLoading
|
button.configuration?.showsActivityIndicator = self.isLoading
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
confirmButton.setNeedsUpdateConfiguration()
|
confirmButton.setNeedsUpdateConfiguration()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@IBAction func loadMorePressed(_ sender: Any) {
|
@IBAction func loadMorePressed(_ sender: Any) {
|
||||||
confirmLoadMore?()
|
confirmLoadMore?()
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
confirmButton.setNeedsUpdateConfiguration()
|
confirmButton.setNeedsUpdateConfiguration()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,12 +108,10 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
// screws up its determination of the line height making multiple lines of emojis squash together
|
// screws up its determination of the line height making multiple lines of emojis squash together
|
||||||
attributed.append(NSAttributedString(string: "\n", attributes: [.font: defaultFont]))
|
attributed.append(NSAttributedString(string: "\n", attributes: [.font: defaultFont]))
|
||||||
case "a":
|
case "a":
|
||||||
let href = try! node.attr("href")
|
if let link = try? node.attr("href"),
|
||||||
if let webURL = WebURL(href),
|
let webURL = WebURL(link),
|
||||||
let url = URL(webURL) {
|
let url = URL(webURL) {
|
||||||
attributed.addAttribute(.link, value: url, range: attributed.fullRange)
|
attributed.addAttribute(.link, value: url, range: attributed.fullRange)
|
||||||
} else if let url = URL(string: href) {
|
|
||||||
attributed.addAttribute(.link, value: url, range: attributed.fullRange)
|
|
||||||
}
|
}
|
||||||
case "p":
|
case "p":
|
||||||
attributed.append(NSAttributedString(string: "\n\n", attributes: [.font: defaultFont]))
|
attributed.append(NSAttributedString(string: "\n\n", attributes: [.font: defaultFont]))
|
||||||
|
@ -195,39 +193,16 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
|
|
||||||
func getLinkAtPoint(_ point: CGPoint) -> (URL, NSRange)? {
|
func getLinkAtPoint(_ point: CGPoint) -> (URL, NSRange)? {
|
||||||
let locationInTextContainer = CGPoint(x: point.x - textContainerInset.left, y: point.y - textContainerInset.top)
|
let locationInTextContainer = CGPoint(x: point.x - textContainerInset.left, y: point.y - textContainerInset.top)
|
||||||
if #available(iOS 16.0, *),
|
|
||||||
let textLayoutManager {
|
|
||||||
guard let fragment = textLayoutManager.textLayoutFragment(for: point),
|
|
||||||
let lineFragment = fragment.textLineFragments.first(where: { lineFragment in
|
|
||||||
lineFragment.typographicBounds.offsetBy(dx: fragment.layoutFragmentFrame.minX, dy: fragment.layoutFragmentFrame.minY).contains(point)
|
|
||||||
}) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let charIndex = lineFragment.characterIndex(for: point)
|
|
||||||
|
|
||||||
var range = NSRange()
|
|
||||||
guard let link = lineFragment.attributedString.attribute(.link, at: charIndex, longestEffectiveRange: &range, in: lineFragment.attributedString.fullRange) as? URL else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// lineFragment.attributedString is the NSTextLayoutFragment's string, and so range is in its index space
|
|
||||||
// but we need to return a range in our whole attributedString's space, so convert it
|
|
||||||
let textLayoutFragmentStart = textLayoutManager.offset(from: textLayoutManager.documentRange.location, to: fragment.rangeInElement.location)
|
|
||||||
let rangeInSelf = NSRange(location: range.location + textLayoutFragmentStart, length: range.length)
|
|
||||||
return (link, rangeInSelf)
|
|
||||||
} else {
|
|
||||||
var partialFraction: CGFloat = 0
|
var partialFraction: CGFloat = 0
|
||||||
let characterIndex = layoutManager.characterIndex(for: locationInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: &partialFraction)
|
let characterIndex = layoutManager.characterIndex(for: locationInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: &partialFraction)
|
||||||
guard characterIndex < textStorage.length && partialFraction < 1 else {
|
if characterIndex < textStorage.length && partialFraction < 1 {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var range = NSRange()
|
var range = NSRange()
|
||||||
guard let link = textStorage.attribute(.link, at: characterIndex, longestEffectiveRange: &range, in: textStorage.fullRange) as? URL else {
|
if let link = textStorage.attribute(.link, at: characterIndex, longestEffectiveRange: &range, in: textStorage.fullRange) as? URL {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return (link, range)
|
return (link, range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func handleLinkTapped(url: URL, text: String) {
|
func handleLinkTapped(url: URL, text: String) {
|
||||||
if let mention = getMention(for: url, text: text) {
|
if let mention = getMention(for: url, text: text) {
|
||||||
|
@ -270,9 +245,8 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
|
|
||||||
extension ContentTextView: UITextViewDelegate {
|
extension ContentTextView: UITextViewDelegate {
|
||||||
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||||
// generally disable the text view's link interactions, we handle tapping links ourself with a gesture recognizer
|
// disable the text view's link interactions, we handle tapping links ourself with a gesture recognizer
|
||||||
// the builtin data detectors use the x-apple-data-detectors scheme, and we allow the text view to handle those itself
|
return false
|
||||||
return URL.scheme == "x-apple-data-detectors"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,26 +295,9 @@ extension ContentTextView: UIContextMenuInteractionDelegate {
|
||||||
|
|
||||||
// Determine the line rects that the link takes up in the coordinate space of this view.
|
// Determine the line rects that the link takes up in the coordinate space of this view.
|
||||||
var rects = [CGRect]()
|
var rects = [CGRect]()
|
||||||
if #available(iOS 16.0, *),
|
|
||||||
let textLayoutManager,
|
|
||||||
let contentManager = textLayoutManager.textContentManager {
|
|
||||||
// convert from NSRange to NSTextRange
|
|
||||||
// i have no idea under what circumstances any of these calls could fail
|
|
||||||
guard let startLoc = contentManager.location(contentManager.documentRange.location, offsetBy: range.location),
|
|
||||||
let endLoc = contentManager.location(startLoc, offsetBy: range.length),
|
|
||||||
let textRange = NSTextRange(location: startLoc, end: endLoc) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// .standard because i have no idea what the difference is
|
|
||||||
textLayoutManager.enumerateTextSegments(in: textRange, type: .standard, options: []) { range, rect, float, textContainer in
|
|
||||||
rects.append(rect)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
layoutManager.enumerateEnclosingRects(forGlyphRange: range, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0), in: textContainer) { (rect, stop) in
|
layoutManager.enumerateEnclosingRects(forGlyphRange: range, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0), in: textContainer) { (rect, stop) in
|
||||||
rects.append(rect)
|
rects.append(rect)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Try to create a snapshot view of this view to disply as the preview.
|
// Try to create a snapshot view of this view to disply as the preview.
|
||||||
// If a snapshot view cannot be created, we bail and use the system-provided preview.
|
// If a snapshot view cannot be created, we bail and use the system-provided preview.
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
//
|
|
||||||
// TrendingHashtagCollectionViewCell.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 6/29/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
class TrendingHashtagCollectionViewCell: UICollectionViewCell {
|
|
||||||
|
|
||||||
private let hashtagLabel = UILabel()
|
|
||||||
private let peopleTodayLabel = UILabel()
|
|
||||||
private let historyView = TrendHistoryView()
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
|
|
||||||
backgroundColor = .systemBackground
|
|
||||||
|
|
||||||
hashtagLabel.font = .preferredFont(forTextStyle: .title2)
|
|
||||||
peopleTodayLabel.font = .preferredFont(forTextStyle: .caption1)
|
|
||||||
|
|
||||||
let vStack = UIStackView(arrangedSubviews: [
|
|
||||||
hashtagLabel,
|
|
||||||
peopleTodayLabel,
|
|
||||||
])
|
|
||||||
vStack.axis = .vertical
|
|
||||||
vStack.alignment = .fill
|
|
||||||
vStack.distribution = .fill
|
|
||||||
vStack.spacing = 0
|
|
||||||
|
|
||||||
let hStack = UIStackView(arrangedSubviews: [
|
|
||||||
vStack,
|
|
||||||
historyView,
|
|
||||||
])
|
|
||||||
hStack.axis = .horizontal
|
|
||||||
hStack.alignment = .center
|
|
||||||
hStack.distribution = .fill
|
|
||||||
hStack.spacing = 8
|
|
||||||
hStack.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(hStack)
|
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
hStack.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 1),
|
|
||||||
trailingAnchor.constraint(equalToSystemSpacingAfter: hStack.trailingAnchor, multiplier: 1),
|
|
||||||
hStack.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
|
||||||
hStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
|
|
||||||
|
|
||||||
historyView.widthAnchor.constraint(equalToConstant: 100),
|
|
||||||
historyView.heightAnchor.constraint(equalToConstant: 44),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
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,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: TrendHistoryView!
|
||||||
|
|
||||||
|
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,67 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" 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="20020"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<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="TrendHistoryView" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="188" y="11" width="100" height="44"/>
|
||||||
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<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>
|
||||||
|
</document>
|
|
@ -171,6 +171,7 @@ class ProfileHeaderView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateRelationship() {
|
private func updateRelationship() {
|
||||||
|
// todo: mastodonController should never be nil, but ProfileHeaderViews are getting leaked
|
||||||
guard let mastodonController = mastodonController,
|
guard let mastodonController = mastodonController,
|
||||||
let relationship = mastodonController.persistentContainer.relationship(forAccount: accountID) else {
|
let relationship = mastodonController.persistentContainer.relationship(forAccount: accountID) else {
|
||||||
return
|
return
|
||||||
|
@ -180,6 +181,7 @@ class ProfileHeaderView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func updateUIForPreferences() {
|
@objc private func updateUIForPreferences() {
|
||||||
|
// todo: mastodonController should never be nil, but ProfileHeaderViews are getting leaked
|
||||||
guard let mastodonController = mastodonController,
|
guard let mastodonController = mastodonController,
|
||||||
// nil if prefs changed before own account is loaded
|
// nil if prefs changed before own account is loaded
|
||||||
let accountID = accountID,
|
let accountID = accountID,
|
||||||
|
|
|
@ -12,7 +12,6 @@ import Combine
|
||||||
import AVKit
|
import AVKit
|
||||||
|
|
||||||
protocol StatusTableViewCellDelegate: TuskerNavigationDelegate, MenuActionProvider {
|
protocol StatusTableViewCellDelegate: TuskerNavigationDelegate, MenuActionProvider {
|
||||||
// @available(iOS, obsoleted: 16.0)
|
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell)
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -333,12 +332,8 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
@IBAction func collapseButtonPressed() {
|
@IBAction func collapseButtonPressed() {
|
||||||
setCollapsed(!collapsed, animated: true)
|
setCollapsed(!collapsed, animated: true)
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
invalidateIntrinsicContentSize()
|
|
||||||
} else {
|
|
||||||
delegate?.statusCellCollapsedStateChanged(self)
|
delegate?.statusCellCollapsedStateChanged(self)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func setCollapsed(_ collapsed: Bool, animated: Bool) {
|
func setCollapsed(_ collapsed: Bool, animated: Bool) {
|
||||||
self.collapsed = collapsed
|
self.collapsed = collapsed
|
||||||
|
|
|
@ -50,10 +50,6 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
]
|
]
|
||||||
|
|
||||||
contentTextView.defaultFont = .systemFont(ofSize: 18)
|
contentTextView.defaultFont = .systemFont(ofSize: 18)
|
||||||
contentTextView.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber]
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
contentTextView.dataDetectorTypes.formUnion([.money, .physicalValue])
|
|
||||||
}
|
|
||||||
|
|
||||||
profileDetailContainerView.addInteraction(UIContextMenuInteraction(delegate: self))
|
profileDetailContainerView.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||||
|
|
||||||
|
|
|
@ -1,9 +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="21179.7" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21169.4"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
||||||
<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"/>
|
||||||
|
@ -108,7 +108,7 @@
|
||||||
<action selector="collapseButtonPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="JaH-xX-UOD"/>
|
<action selector="collapseButtonPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="JaH-xX-UOD"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="waJ-f5-LKv" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="waJ-f5-LKv" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="83" width="277" height="82.5"/>
|
<rect key="frame" x="0.0" y="83" width="277" height="82.5"/>
|
||||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
<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"/>
|
<color key="textColor" systemColor="labelColor"/>
|
||||||
|
@ -272,14 +272,14 @@
|
||||||
</view>
|
</view>
|
||||||
</objects>
|
</objects>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="104"/>
|
<image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="106"/>
|
||||||
<image name="chevron.down" catalog="system" width="128" height="70"/>
|
<image name="chevron.down" catalog="system" width="128" height="72"/>
|
||||||
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
||||||
<image name="pin.fill" catalog="system" width="116" height="128"/>
|
<image name="pin.fill" catalog="system" width="119" height="128"/>
|
||||||
<image name="repeat" catalog="system" width="128" height="98"/>
|
<image name="repeat" catalog="system" width="128" height="98"/>
|
||||||
<image name="star.fill" catalog="system" width="128" height="116"/>
|
<image name="star.fill" catalog="system" width="128" height="116"/>
|
||||||
<systemColor name="labelColor">
|
<systemColor name="labelColor">
|
||||||
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
<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"/>
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
class StatusContentTextView: ContentTextView {
|
class StatusContentTextView: ContentTextView {
|
||||||
|
|
||||||
|
@ -42,7 +41,7 @@ class StatusContentTextView: ContentTextView {
|
||||||
let mastodonController = mastodonController,
|
let mastodonController = mastodonController,
|
||||||
let status = mastodonController.persistentContainer.status(for: statusID) {
|
let status = mastodonController.persistentContainer.status(for: statusID) {
|
||||||
hashtag = status.hashtags.first { (hashtag) in
|
hashtag = status.hashtags.first { (hashtag) in
|
||||||
URL(hashtag.url) == url
|
hashtag.url == url
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hashtag = nil
|
hashtag = nil
|
||||||
|
|
|
@ -24,7 +24,11 @@ class PublicTimelineDescriptionTableViewCell: UITableViewCell {
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
contentView.backgroundColor = .tintColor
|
contentView.backgroundColor = .tintColor
|
||||||
|
} else {
|
||||||
|
contentView.backgroundColor = .systemBlue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateLabel() {
|
private func updateLabel() {
|
||||||
|
|
|
@ -42,10 +42,7 @@ class TrendHistoryView: UIView {
|
||||||
|
|
||||||
private func createLayers() {
|
private func createLayers() {
|
||||||
guard let history = history,
|
guard let history = history,
|
||||||
history.count >= 2,
|
history.count >= 2 else { return }
|
||||||
!bounds.isEmpty else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let maxUses = history.max(by: { $0.uses < $1.uses })!.uses
|
let maxUses = history.max(by: { $0.uses < $1.uses })!.uses
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue